191 lines · 5.9 KB · Rust
RawDownload
use std::{convert::TryInto, process::Command}; use anyhow::bail;use bstr::ByteSlice;use cargo_metadata::{    camino::{Utf8Component, Utf8Path},    Package,};use git_repository::{easy::object, prelude::ReferenceAccessExt, refs}; use super::{tag_name_for, utils::will, Context, Oid, Options}; fn is_top_level_package(manifest_path: &Utf8Path, shared: &git_repository::Easy) -> bool {    manifest_path        .strip_prefix(shared.repo.work_tree.as_ref().expect("repo with working tree"))        .map_or(false, |p| p.components().count() == 1)} pub(in crate::command::release_impl) fn has_changed_since_last_release(    package: &Package,    ctx: &Context,    verbose: bool,) -> anyhow::Result<bool> {    let version_tag_name = tag_name_for(        &package.name,        &package.version.to_string(),        is_top_level_package(&package.manifest_path, &ctx.git_easy),    );    let mut tag_ref = match ctx.git_easy.try_find_reference(&version_tag_name)? {        None => {            if verbose {                log::info!(                    "Package {} wasn't tagged with {} yet and thus needs a release",                    package.name,                    version_tag_name                );            }            return Ok(true);        }        Some(r) => r,    };    let repo_relative_crate_dir = package        .manifest_path        .parent()        .expect("parent of a file is always present")        .strip_prefix(&ctx.root)        .expect("workspace members are releative to the root directory");     let current_commit = ctx.git_easy.find_reference("HEAD")?.peel_to_oid_in_place()?;    let released_target = tag_ref.peel_to_oid_in_place()?;     if repo_relative_crate_dir.as_os_str().is_empty() {        Ok(current_commit != released_target)    } else {        let components = repo_relative_crate_dir.components().map(|c| match c {            Utf8Component::Normal(c) => c.as_bytes(),            _ => unreachable!("only normal components are possible in paths here"),        });        let current_dir_id = current_commit            .object()?            .peel_to_kind(object::Kind::Tree)?            .into_tree()            .lookup_path(components.clone())?            .expect("path must exist in current commit")            .oid;        let released_dir_id = released_target            .object()?            .peel_to_kind(object::Kind::Tree)?            .into_tree()            .lookup_path(components)?            .expect("path must exist as it was supposedly released there")            .oid;         Ok(released_dir_id != current_dir_id)    }} pub fn assure_clean_working_tree() -> anyhow::Result<()> {    let tracked_changed = !Command::new("git")        .arg("diff")        .arg("HEAD")        .arg("--exit-code")        .arg("--name-only")        .status()?        .success();    if tracked_changed {        bail!("Detected working tree changes. Please commit beforehand as otherwise these would be committed as part of manifest changes, or use --allow-dirty to force it.")    }     let has_untracked = !Command::new("git")        .arg("ls-files")        .arg("--exclude-standard")        .arg("--others")        .output()?        .stdout        .as_slice()        .trim()        .is_empty();     if has_untracked {        bail!("Found untracked files which would possibly be packaged when publishing.")    }    Ok(())} pub(in crate::command::release_impl) fn commit_changes(    message: impl AsRef<str>,    verbose: bool,    dry_run: bool,    empty_commit_possible: bool,    ctx: &Context,) -> anyhow::Result<Option<Oid<'_>>> {    // TODO: replace with gitoxide one day    let mut cmd = Command::new("git");    cmd.arg("commit").arg("-am").arg(message.as_ref());    if empty_commit_possible {        cmd.arg("--allow-empty");    }    if verbose {        log::info!("{} run {:?}", will(dry_run), cmd);    }    if dry_run {        return Ok(None);    }     if !cmd.status()?.success() {        bail!("Failed to commit changed manifests");    }    Ok(Some(ctx.git_easy.find_reference("HEAD")?.peel_to_oid_in_place()?))} pub(in crate::command::release_impl) fn create_version_tag<'repo>(    publishee: &Package,    new_version: &str,    commit_id: Option<Oid<'repo>>,    ctx: &'repo Context,    Options {        verbose,        dry_run,        skip_tag,        ..    }: Options,) -> anyhow::Result<Option<refs::FullName>> {    if skip_tag {        return Ok(None);    }    let tag_name = tag_name_for(        &publishee.name,        new_version,        is_top_level_package(&publishee.manifest_path, &ctx.git_easy),    );    if dry_run {        if verbose {            log::info!("WOULD create tag {}", tag_name);        }        Ok(Some(format!("refs/tags/{}", tag_name).try_into()?))    } else {        let edits = ctx.git_easy.tag(            tag_name,            commit_id.expect("set in --execute mode"),            git_lock::acquire::Fail::Immediately,            false,        )?;        assert_eq!(edits.len(), 1, "We create only one tag and there is no expansion");        let tag = edits.into_iter().next().expect("the promised tag");        log::info!("Created tag {}", tag.name.as_bstr());        Ok(Some(tag.name))    }} // TODO: Make this gitoxidepub fn push_tags_and_head(tag_names: impl IntoIterator<Item = refs::FullName>, options: Options) -> anyhow::Result<()> {    if options.skip_push {        return Ok(());    }     let mut cmd = Command::new("git");    cmd.arg("push").arg("origin").arg("HEAD");    for tag_name in tag_names {        cmd.arg(tag_name.as_bstr().to_str()?);    }     if options.verbose {        log::info!("{} run {:?}", will(options.dry_run), cmd);    }    if options.dry_run || cmd.status()?.success() {        Ok(())    } else {        bail!("'git push' invocation failed. Try to push manually and repeat the smart-release invocation to resume, possibly with --skip-push.");    }}