Sebastian Thiel[utils #154] fix flag namedf43c6d5 years agoHistory
705 lines · 23.4 KB · Rust
RawDownload
use crate::command::release::Options;use anyhow::{anyhow, bail};use bstr::ByteSlice;use cargo_metadata::{    camino::{Utf8Component, Utf8Path, Utf8PathBuf},    Dependency, DependencyKind, Metadata, Package, PackageId,};use dia_semver::Semver;use git_repository::{    actor,    hash::ObjectId,    object,    odb::{pack, Find, FindExt},    refs::{        file,        file::loose::reference::peel,        mutable::Target,        packed,        transaction::{Change, Create, RefEdit},    },    Repository,};use std::{    collections::{BTreeMap, BTreeSet},    convert::TryInto,    path::PathBuf,    process::Command,    str::FromStr,}; struct State {    root: Utf8PathBuf,    seen: BTreeSet<String>,    repo: Repository,    packed_refs: Option<packed::Buffer>,} impl State {    fn new(repo_path: impl Into<PathBuf>) -> anyhow::Result<Self> {        let root = repo_path.into();        let repo = git_repository::discover(&root)?;        let packed_refs = repo.refs.packed()?;        Ok(State {            root: root.try_into()?,            seen: BTreeSet::new(),            repo,            packed_refs,        })    }} fn will(not_really: bool) -> &'static str {    if not_really {        "WOULD"    } else {        "Will"    }} /// In order to try dealing with https://github.com/sunng87/cargo-release/issues/224 and also to make workspace/// releases more selective.pub fn release(options: Options, version_bump_spec: String, crates: Vec<String>) -> anyhow::Result<()> {    if crates.is_empty() {        bail!("Please provide at least one crate name which also is a workspace member");    }    release_depth_first(options, crates, &version_bump_spec)?;    Ok(())} fn is_workspace_member(meta: &Metadata, crate_name: &str) -> bool {    workspace_package_by_name(meta, crate_name).is_some()} fn workspace_package_by_name<'a>(meta: &'a Metadata, crate_name: &str) -> Option<&'a Package> {    meta.packages        .iter()        .find(|p| p.name == crate_name)        .filter(|p| meta.workspace_members.iter().any(|m| m == &p.id))} fn workspace_package_by_id<'a>(meta: &'a Metadata, id: &PackageId) -> Option<&'a Package> {    meta.packages        .iter()        .find(|p| &p.id == id)        .filter(|p| meta.workspace_members.iter().any(|m| m == &p.id))} fn package_by_name<'a>(meta: &'a Metadata, name: &str) -> anyhow::Result<&'a Package> {    meta.packages        .iter()        .find(|p| p.name == name)        .ok_or_else(|| anyhow!("workspace member must be a listed package: '{}'", name))} fn release_depth_first(options: Options, crate_names: Vec<String>, bump_spec: &str) -> anyhow::Result<()> {    let meta = cargo_metadata::MetadataCommand::new().exec()?;    let mut state = State::new(std::env::current_dir()?)?;    let mut changed_crate_names_to_publish = Vec::new();    let mut index = 0;    for crate_name in crate_names {        if state.seen.contains(&crate_name) {            continue;        }        changed_crate_names_to_publish.push(crate_name.clone());        while let Some(crate_name) = changed_crate_names_to_publish.get(index) {            let package = package_by_name(&meta, crate_name)?;            for dependency in package.dependencies.iter().filter(|d| d.kind == DependencyKind::Normal) {                if state.seen.contains(&dependency.name) || !is_workspace_member(&meta, &dependency.name) {                    continue;                }                state.seen.insert(dependency.name.clone());                let dep_package = package_by_name(&meta, &dependency.name).expect("exists");                if has_changed_since_last_release(dep_package, &state)? {                    changed_crate_names_to_publish.push(dependency.name.clone());                } else {                    log::info!(                        "{} v{}  - skipped release as it didn't change",                        dep_package.name,                        dep_package.version                    );                }            }            index += 1;        }        state.seen.insert(crate_name);    }    changed_crate_names_to_publish = reorder_according_to_resolution_order(&meta, &changed_crate_names_to_publish);     let crates_to_publish_together = {        let mut crates_to_publish_additionally_to_avoid_instability = Vec::new();        let mut publish_group = Vec::<String>::new();        for publishee_name in changed_crate_names_to_publish.iter() {            let publishee = package_by_name(&meta, publishee_name).expect("exists");            let cycles = workspace_members_referring_to_publishee(&meta, publishee);            if cycles.is_empty() {                log::debug!("'{}' is cycle-free", publishee.name);            } else {                for Cycle { from, hops } in cycles {                    log::warn!(                        "'{}' links to '{}' {} causing publishes to never settle.",                        publishee.name,                        from.name,                        if hops == 1 {                            "directly".to_string()                        } else {                            format!("via {} hops", hops)                        }                    );                    if !changed_crate_names_to_publish.contains(&from.name) {                        crates_to_publish_additionally_to_avoid_instability.push(from.name.clone());                    } else {                        for name in &[&from.name, &publishee.name] {                            if !publish_group.contains(name) {                                publish_group.push(name.to_string())                            }                        }                    }                }            }        }        if !crates_to_publish_additionally_to_avoid_instability.is_empty() && !options.ignore_instability {            bail!(                "Refusing to publish unless --ignore-instability is provided or crate(s) {} is/are included in the publish",                crates_to_publish_additionally_to_avoid_instability.join(", ")            )        }        reorder_according_to_resolution_order(&meta, &publish_group)    };     for publishee_name in changed_crate_names_to_publish        .iter()        .filter(|n| !crates_to_publish_together.contains(n))    {        let publishee = package_by_name(&meta, publishee_name).expect("exists");         let (new_version, commit_id) = perform_single_release(&meta, publishee, options, bump_spec, &state)?;        create_version_tag(publishee, &new_version, commit_id, &state.repo, options.dry_run)?;    }     if !crates_to_publish_together.is_empty() {        let mut crates_to_publish_together = crates_to_publish_together            .into_iter()            .map(|name| {                let p = package_by_name(&meta, &name).expect("package present");                bump_version(&p.version.to_string(), bump_spec).map(|v| (p, v.to_string()))            })            .collect::<Result<Vec<_>, _>>()?;         log::info!(            "{} prepare releases of {}",            will(options.dry_run),            names_and_versions(&crates_to_publish_together)        );         let commit_id = edit_manifest_and_fixup_dependent_crates(&meta, &crates_to_publish_together, options, &state)?;         crates_to_publish_together.reverse();        while let Some((publishee, new_version)) = crates_to_publish_together.pop() {            let unpublished_crates: Vec<_> = crates_to_publish_together                .iter()                .map(|(p, _)| p.name.to_owned())                .collect();            publish_crate(publishee, &unpublished_crates, options)?;            create_version_tag(publishee, &new_version, commit_id, &state.repo, options.dry_run)?;        }    }     Ok(())} fn create_version_tag(    publishee: &Package,    new_version: &str,    commit_id: ObjectId,    repo: &Repository,    dry_run: bool,) -> anyhow::Result<()> {    let tag_name = tag_name_for(&publishee.name, new_version);    if dry_run {        log::info!("WOULD create tag {}", tag_name);    } else {        for tag in repo            .refs            .transaction()            .prepare(                Some(RefEdit {                    change: Change::Update {                        log: Default::default(),                        mode: Create::Only,                        new: Target::Peeled(commit_id),                    },                    name: format!("refs/tags/{}", tag_name).try_into()?,                    deref: false,                }),                git_lock::acquire::Fail::Immediately,            )?            .commit(&actor::Signature::empty())?        {            log::info!("Created tag {}", tag.name.as_bstr());        }    }    Ok(())} fn reorder_according_to_resolution_order(meta: &Metadata, workspace_members: &[String]) -> Vec<String> {    let mut out = Vec::new();    for package_in_resolve_order in meta        .resolve        .as_ref()        .expect("resolve_data")        .nodes        .iter()        .filter_map(|node| {            meta.workspace_members.contains(&node.id).then(|| {                meta.packages                    .iter()                    .find(|p| p.id == node.id)                    .expect("node always present")            })        })    {        if workspace_members.contains(&package_in_resolve_order.name) {            out.push(package_in_resolve_order.name.clone())        }    }    out} struct Cycle<'a> {    from: &'a Package,    hops: usize,} fn workspace_members_referring_to_publishee<'a>(meta: &'a Metadata, publishee: &Package) -> Vec<Cycle<'a>> {    publishee        .dependencies        .iter()        .filter(|dep| {            dep.kind != DependencyKind::Normal                && meta                    .workspace_members                    .iter()                    .map(|id| package_by_id(meta, id))                    .any(|potential_cycle| package_eq_dependency(potential_cycle, dep))        })        .filter_map(|dep| {            hops_for_dependency_to_link_back_to_publishee(meta, dep, publishee).map(|hops| Cycle {                hops,                from: package_by_name(meta, &dep.name).expect("package exists"),            })        })        .collect()} fn package_eq_dependency(package: &Package, dependency: &Dependency) -> bool {    package.name == dependency.name} fn hops_for_dependency_to_link_back_to_publishee<'a>(    meta: &'a Metadata,    source: &Dependency,    destination: &Package,) -> Option<usize> {    let source = package_for_dependency(meta, source);    let mut package_ids = vec![(0, &source.id)];    let mut seen = BTreeSet::new();    while let Some((level, id)) = package_ids.pop() {        if !seen.insert(id) {            continue;        }        if let Some(package) = workspace_package_by_id(meta, id) {            if package                .dependencies                .iter()                .any(|dep| package_eq_dependency(destination, dep))            {                return Some(level + 1);            }            package_ids.extend(                package                    .dependencies                    .iter()                    .map(|dep| (level + 1, &package_for_dependency(meta, dep).id)),            );        };    }    None} fn package_for_dependency<'a>(meta: &'a Metadata, dep: &Dependency) -> &'a Package {    meta.packages        .iter()        .find(|p| package_eq_dependency(p, dep))        .expect("dependency always available as package")} fn perform_single_release(    meta: &Metadata,    publishee: &Package,    options: Options,    bump_spec: &str,    state: &State,) -> anyhow::Result<(String, ObjectId)> {    let new_version = bump_version(&publishee.version.to_string(), bump_spec)?.to_string();    log::info!(        "{} prepare release of {} v{}",        will(options.dry_run),        publishee.name,        new_version    );    let commit_id =        edit_manifest_and_fixup_dependent_crates(meta, &[(publishee, new_version.clone())], options, state)?;    publish_crate(publishee, &[], options)?;    Ok((new_version, commit_id))} fn publish_crate(    publishee: &Package,    other_publishee_names: &[String],    Options {        skip_publish,        dry_run,        allow_dirty,        ..    }: Options,) -> anyhow::Result<()> {    let max_attempts = 3;    let must_not_verify = publishee        .dependencies        .iter()        .any(|dep| other_publishee_names.contains(&dep.name));    for attempt in 1..=max_attempts {        let mut c = Command::new("cargo");        c.arg("publish");         if allow_dirty {            c.arg("--allow-dirty");        }        if must_not_verify {            c.arg("--no-verify");        }        c.arg("--manifest-path").arg(&publishee.manifest_path);        log::info!("{} run {:?}", will(dry_run), c);        if skip_publish || dry_run || c.status()?.success() {            break;        } else if attempt == max_attempts {            bail!("Could not successfully execute 'cargo publish' even ")        } else {            log::warn!(                "'cargo publish' run {} failed but we retry up to {} times to rule out flakiness",                attempt,                max_attempts            );        }    }    Ok(())} fn edit_manifest_and_fixup_dependent_crates(    meta: &Metadata,    publishees: &[(&Package, String)],    Options {        dry_run, allow_dirty, ..    }: Options,    state: &State,) -> anyhow::Result<ObjectId> {    if !allow_dirty {        assure_clean_working_tree()?;    }    let mut locks_by_manifest_path = BTreeMap::new();    for (publishee, _) in publishees {        let lock = git_lock::File::acquire_to_update_resource(            &publishee.manifest_path,            git_lock::acquire::Fail::Immediately,            None,        )?;        locks_by_manifest_path.insert(&publishee.manifest_path, lock);    }    let mut packages_to_fix = Vec::new();    for package_to_fix in meta        .workspace_members        .iter()        .map(|id| package_by_id(meta, id))        .filter(|p| {            p.dependencies.iter().any(|dep| {                publishees                    .iter()                    .any(|(publishee, _)| package_eq_dependency(publishee, dep))            })        })    {        if locks_by_manifest_path.contains_key(&package_to_fix.manifest_path) {            continue;        }        let lock = git_lock::File::acquire_to_update_resource(            &package_to_fix.manifest_path,            git_lock::acquire::Fail::Immediately,            None,        )?;        locks_by_manifest_path.insert(&package_to_fix.manifest_path, lock);        packages_to_fix.push(package_to_fix);    }     for (publishee, new_version) in publishees {        let mut lock = locks_by_manifest_path            .get_mut(&publishee.manifest_path)            .expect("lock available");        set_version_and_update_package_dependency(publishee, Some(&new_version.to_string()), publishees, &mut lock)?;    }     for package_to_update in packages_to_fix.iter_mut() {        let mut lock = locks_by_manifest_path            .get_mut(&package_to_update.manifest_path)            .expect("lock written once");        set_version_and_update_package_dependency(package_to_update, None, publishees, &mut lock)?;    }     let message = format!("Release {}", names_and_versions(publishees));    if dry_run {        log::info!("WOULD commit changes to manifests with {:?}", message);        Ok(ObjectId::null_sha1())    } else {        log::info!("Persisting changes to manifests");        for manifest_lock in locks_by_manifest_path.into_values() {            manifest_lock.commit()?;        }        refresh_cargo_lock()?;        commit_changes(message, state)    }} fn names_and_versions(publishees: &[(&Package, String)]) -> String {    publishees        .iter()        .map(|(p, nv)| format!("{} v{}", p.name, nv))        .collect::<Vec<_>>()        .join(", ")} fn package_by_id<'a>(meta: &'a Metadata, id: &PackageId) -> &'a Package {    meta.packages        .iter()        .find(|p| &p.id == id)        .expect("workspace members are in packages")} fn refresh_cargo_lock() -> anyhow::Result<()> {    cargo_metadata::MetadataCommand::new().exec()?;    Ok(())} 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 {

Lines 1–500 of 705.