use super::{Context, Options}; use crate::command::release_impl::tag_name_for; use anyhow::{anyhow, bail}; use bstr::ByteSlice; use cargo_metadata::{ camino::{Utf8Component, Utf8Path}, Package, }; use git_repository::{ actor, hash::ObjectId, object, odb::{pack, Find, FindExt}, refs::{ file, file::loose::reference::peel, mutable::Target, transaction::{Change, Create, RefEdit}, }, Repository, }; use std::{convert::TryInto, process::Command}; pub(in crate::command::release_impl) fn has_changed_since_last_release( package: &Package, ctx: &Context, ) -> anyhow::Result { let version_tag_name = tag_name_for(&package.name, &package.version.to_string()); let mut tag_ref = match ctx.repo.refs.find(&version_tag_name, ctx.packed_refs.as_ref())? { None => { 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 target = peel_ref_fully(&mut ctx.repo.refs.find_existing("HEAD", None)?, ctx)?; let released_target = peel_ref_fully(&mut tag_ref, ctx)?; let mut buf = Vec::new(); let current_dir_id = find_directory_id_in_tree( repo_relative_crate_dir, resolve_tree_id_from_ref_target(target, &ctx.repo, &mut buf)?, &ctx.repo, &mut buf, )?; let released_dir_id = find_directory_id_in_tree( repo_relative_crate_dir, resolve_tree_id_from_ref_target(released_target, &ctx.repo, &mut buf)?, &ctx.repo, &mut buf, )?; 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(()) } fn find_directory_id_in_tree( path: &Utf8Path, id: ObjectId, repo: &Repository, buf: &mut Vec, ) -> anyhow::Result { let mut tree_id = None::; for component in path.components() { match component { Utf8Component::Normal(c) => { let mut tree_iter = repo .odb .find_existing(tree_id.take().unwrap_or(id), buf, &mut pack::cache::Never)? .into_tree_iter() .expect("tree"); tree_id = tree_iter .find_map(|e| { let e = e.expect("tree parseable"); (e.filename == c).then(|| e.oid) }) .map(ToOwned::to_owned); if tree_id.is_none() { break; } } _ => panic!( "only normal components are expected in relative manifest paths: '{}'", path ), } } tree_id.ok_or_else(|| anyhow!("path '{}' didn't exist in tree {}", path, id)) } fn peel_ref_fully(reference: &mut file::Reference<'_>, ctx: &Context) -> anyhow::Result { reference .peel_to_id_in_place(&ctx.repo.refs, ctx.packed_refs.as_ref(), |oid, buf| { ctx.repo .odb .find(oid, buf, &mut pack::cache::Never) .map(|r| r.map(|obj| (obj.kind, obj.data))) }) .map_err(Into::into) } /// Note that borrowchk doesn't like us to return an immutable, decoded tree which we would otherwise do. Chalk/polonius could allow that, /// preventing a duplicate lookup. fn resolve_tree_id_from_ref_target(mut id: ObjectId, repo: &Repository, buf: &mut Vec) -> anyhow::Result { let mut cursor = repo.odb.find_existing(id, buf, &mut pack::cache::Never)?; loop { match cursor.kind { object::Kind::Tree => return Ok(id), object::Kind::Commit => { id = cursor.into_commit_iter().expect("commit").tree_id().expect("id"); cursor = repo.odb.find_existing(id, buf, &mut pack::cache::Never)?; } object::Kind::Tag | object::Kind::Blob => { bail!( "A ref ultimately points to a blob or tag {} but we need a tree, peeling takes care of tags", id ) } } } } pub(in crate::command::release_impl) fn commit_changes( message: impl AsRef, empty_commit_possible: bool, ctx: &Context, ) -> anyhow::Result { // 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 !cmd.status()?.success() { bail!("Failed to commit changed manifests"); } Ok(ctx .repo .refs .loose_find_existing("HEAD")? .peel_to_id_in_place(&ctx.repo.refs, ctx.packed_refs.as_ref(), peel::none)? .to_owned()) } pub fn create_version_tag( publishee: &Package, new_version: &str, commit_id: ObjectId, repo: &Repository, Options { dry_run, skip_tag, .. }: Options, ) -> anyhow::Result<()> { if skip_tag { return Ok(()); } 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(()) }