use crate::command::release::Options;use anyhow::{anyhow, bail};use bstr::ByteSlice;use cargo_metadata::{ camino::{Utf8Component, Utf8Path, Utf8PathBuf}, DependencyKind, Metadata, Package,};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::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, }) }} /// 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"); } let meta = cargo_metadata::MetadataCommand::new().exec()?; for crate_name in crates { if !is_workspace_member(&meta, &crate_name) { bail!("Package to release must be a workspace member: '{}'", crate_name); } release_depth_first(options, &meta, &crate_name, &version_bump_spec)?; } Ok(())} fn is_workspace_member(meta: &Metadata, crate_name: &str) -> bool { meta.packages .iter() .find(|p| p.name == crate_name) .map_or(false, |p| meta.workspace_members.iter().any(|m| m == &p.id))} fn release_depth_first(options: Options, meta: &Metadata, crate_name: &str, bump_spec: &str) -> anyhow::Result<()> { let mut state = State::new(std::env::current_dir()?)?; let mut names_to_publish = vec![crate_name.to_owned()]; let mut index = 0; while let Some(crate_name) = names_to_publish.get(index) { let package = meta .packages .iter() .find(|p| &p.name == crate_name) .ok_or_else(|| anyhow!("workspace member must be a listed package: '{}'", 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()); names_to_publish.push(dependency.name.clone()); } index += 1; } for crate_name in names_to_publish.iter().rev() { let package = meta .packages .iter() .find(|p| &p.name == crate_name) .expect("crate still there"); if needs_release(package, &state)? { let (new_version, commit_id) = perform_release(meta, package, options, bump_spec, &state)?; let tag_name = format!("{}-{}", package.name, new_version); if options.dry_run { log::info!("Won't create tag {}", tag_name); } else { for tag in state .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()); } } } else { log::info!( "{} v{} - skipped release as it didn't change", package.name, package.version ); } } Ok(())} fn perform_release( meta: &Metadata, package: &Package, options: Options, bump_spec: &str, state: &State,) -> anyhow::Result<(Semver, ObjectId)> { let new_version = bump_version(&package.version.to_string(), bump_spec)?; log::info!("{} v{} will be released", package.name, new_version); let commit_id = edit_manifest_and_fixup_dependent_crates(meta, package, &new_version, options, state)?; publish_crate(package, options)?; Ok((new_version, commit_id))} fn publish_crate(package: &Package, Options { dry_run, allow_dirty }: Options) -> anyhow::Result<()> { let max_attempts = 3; for attempt in 1..=max_attempts { let mut c = Command::new("cargo"); c.arg("publish"); if allow_dirty { c.arg("--allow-dirty"); } c.arg("--manifest-path").arg(&package.manifest_path); log::info!("About to run {:?}", c); if 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, package: &Package, new_version: &Semver, Options { dry_run, allow_dirty }: Options, state: &State,) -> anyhow::Result<ObjectId> { if !allow_dirty { assure_clean_working_tree()?; } log::trace!("Preparing {} for version update", package.manifest_path); let mut package_manifest_lock = git_lock::File::acquire_to_update_resource(&package.manifest_path, git_lock::acquire::Fail::Immediately, None)?; let mut packages_to_fix = meta .workspace_members .iter() .filter(|id| *id != &package.id) .map(|id| { meta.packages .iter() .find(|p| &p.id == id) .expect("workspace members are in packages") }) .filter(|p| p.dependencies.iter().any(|dep| dep.name == package.name)) .map(|p| { log::trace!("Preparing {} for dependency version update", p.manifest_path); git_lock::File::acquire_to_update_resource(&p.manifest_path, git_lock::acquire::Fail::Immediately, None) .map(|l| (p, l)) }) .collect::<Result<Vec<_>, _>>()?; let new_version = new_version.to_string(); set_manifest_version(package, &new_version, &mut package_manifest_lock)?; for (package_to_update, out) in packages_to_fix.iter_mut() { update_package_dependency(package_to_update, &package.name, &new_version, out)?; } if dry_run { log::info!("Won't write changed manifests in dry-run mode"); Ok(ObjectId::null_sha1()) } else { log::info!("Committing changes to manifests"); package_manifest_lock.commit()?; for (_, lock) in packages_to_fix { lock.commit()?; } refresh_cargo_lock(package)?; commit_changes(format!("Release {}-{}", package.name, new_version), state) }} pub fn refresh_cargo_lock(package: &Package) -> anyhow::Result<()> { cargo_metadata::MetadataCommand::new() .manifest_path(&package.manifest_path) .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 { bail!("Detected working tree changes. Please commit beforehand as otherwise these would be committed as part of manifest changes.") } 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 commit_changes(message: impl AsRef<str>, state: &State) -> anyhow::Result<ObjectId> { // TODO: replace with gitoxide one day if !Command::new("git") .arg("commit") .arg("-am") .arg(message.as_ref()) .status()? .success() { bail!("Failed to commit changed manifests"); } Ok(state .repo .refs .loose_find_existing("HEAD")? .peel_to_id_in_place(&state.repo.refs, state.packed_refs.as_ref(), peel::none)? .to_owned())} fn update_package_dependency( package_to_update: &Package, name_to_find: &str, new_version: &str, mut out: impl std::io::Write,) -> anyhow::Result<()> { let manifest = std::fs::read_to_string(&package_to_update.manifest_path)?; let mut doc = toml_edit::Document::from_str(&manifest)?; for dep_type in &["dependencies", "dev-dependencies", "build-dependencies"] { if let Some(name_table) = doc .as_table_mut() .get_mut(dep_type) .and_then(|deps| deps.as_table_mut()) .and_then(|deps| deps.get_mut(name_to_find).and_then(|name| name.as_inline_table_mut())) { log::info!( "Updated {} dependency in {} crate to version {}", name_to_find, package_to_update.name, new_version ); *name_table.get_or_insert("version", new_version) = toml_edit::Value::from(new_version); } } out.write_all(doc.to_string_in_original_order().as_bytes())?; Ok(())} fn set_manifest_version(package: &Package, new_version: &str, mut out: impl std::io::Write) -> anyhow::Result<()> { let manifest = std::fs::read_to_string(&package.manifest_path)?; let mut doc = toml_edit::Document::from_str(&manifest)?; doc["package"]["version"] = toml_edit::value(new_version); log::info!("Updated {} to version {}", package.name, new_version); out.write_all(doc.to_string_in_original_order().as_bytes())?; Ok(())} fn bump_version(version: &str, bump_spec: &str) -> anyhow::Result<Semver> { let v = Semver::parse(version).map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err.to_string()))?; Ok(match bump_spec { "major" => v.new_major(), "minor" => v.new_minor(), "patch" => v.new_patch(), _ => bail!("Invalid version specification: '{}'", bump_spec), } .expect("no overflow"))} fn needs_release(package: &Package, state: &State) -> anyhow::Result<bool> { let version_tag_name = format!("{}-v{}", package.name, package.version); let mut tag_ref = match state.repo.refs.find(&version_tag_name, state.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(&state.root) .expect("workspace members are releative to the root directory"); let target = peel_ref_fully(&mut state.repo.refs.find_existing("HEAD", None)?, state)?; let released_target = peel_ref_fully(&mut tag_ref, state)?; 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, &state.repo, &mut buf)?, &state.repo, &mut buf, )?; let released_dir_id = find_directory_id_in_tree( repo_relative_crate_dir, resolve_tree_id_from_ref_target(released_target, &state.repo, &mut buf)?, &state.repo, &mut buf, )?; Ok(released_dir_id != current_dir_id)} fn find_directory_id_in_tree( path: &Utf8Path, id: ObjectId, repo: &Repository, buf: &mut Vec<u8>,) -> anyhow::Result<ObjectId> { let mut tree_id = None::<ObjectId>; 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<'_>, state: &State) -> anyhow::Result<ObjectId> { reference .peel_to_id_in_place(&state.repo.refs, state.packed_refs.as_ref(), |oid, buf| { state .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<u8>) -> anyhow::Result<ObjectId> { 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 ) } } }}
use crate::command::release::Options;
use anyhow::{anyhow, bail};
use bstr::ByteSlice;
use cargo_metadata::{
camino::{Utf8Component, Utf8Path, Utf8PathBuf},
DependencyKind, Metadata, Package,
};
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::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,
})
}
}
/// 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");
}
let meta = cargo_metadata::MetadataCommand::new().exec()?;
for crate_name in crates {
if !is_workspace_member(&meta, &crate_name) {
bail!("Package to release must be a workspace member: '{}'", crate_name);
}
release_depth_first(options, &meta, &crate_name, &version_bump_spec)?;
}
Ok(())
}
fn is_workspace_member(meta: &Metadata, crate_name: &str) -> bool {
meta.packages
.iter()
.find(|p| p.name == crate_name)
.map_or(false, |p| meta.workspace_members.iter().any(|m| m == &p.id))
}
fn release_depth_first(options: Options, meta: &Metadata, crate_name: &str, bump_spec: &str) -> anyhow::Result<()> {
let mut state = State::new(std::env::current_dir()?)?;
let mut names_to_publish = vec![crate_name.to_owned()];
let mut index = 0;
while let Some(crate_name) = names_to_publish.get(index) {
let package = meta
.packages
.iter()
.find(|p| &p.name == crate_name)
.ok_or_else(|| anyhow!("workspace member must be a listed package: '{}'", 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());
names_to_publish.push(dependency.name.clone());
}
index += 1;
}
for crate_name in names_to_publish.iter().rev() {
let package = meta
.packages
.iter()
.find(|p| &p.name == crate_name)
.expect("crate still there");
if needs_release(package, &state)? {
let (new_version, commit_id) = perform_release(meta, package, options, bump_spec, &state)?;
let tag_name = format!("{}-{}", package.name, new_version);
if options.dry_run {
log::info!("Won't create tag {}", tag_name);
} else {
for tag in state
.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());
}
}
} else {
log::info!(
"{} v{} - skipped release as it didn't change",
package.name,
package.version
);
}
}
Ok(())
}
fn perform_release(
meta: &Metadata,
package: &Package,
options: Options,
bump_spec: &str,
state: &State,
) -> anyhow::Result<(Semver, ObjectId)> {
let new_version = bump_version(&package.version.to_string(), bump_spec)?;
log::info!("{} v{} will be released", package.name, new_version);
let commit_id = edit_manifest_and_fixup_dependent_crates(meta, package, &new_version, options, state)?;
publish_crate(package, options)?;
Ok((new_version, commit_id))
}
fn publish_crate(package: &Package, Options { dry_run, allow_dirty }: Options) -> anyhow::Result<()> {
let max_attempts = 3;
for attempt in 1..=max_attempts {
let mut c = Command::new("cargo");
c.arg("publish");
if allow_dirty {
c.arg("--allow-dirty");
}
c.arg("--manifest-path").arg(&package.manifest_path);
log::info!("About to run {:?}", c);
if 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,
package: &Package,
new_version: &Semver,
Options { dry_run, allow_dirty }: Options,
state: &State,
) -> anyhow::Result<ObjectId> {
if !allow_dirty {
assure_clean_working_tree()?;
}
log::trace!("Preparing {} for version update", package.manifest_path);
let mut package_manifest_lock =
git_lock::File::acquire_to_update_resource(&package.manifest_path, git_lock::acquire::Fail::Immediately, None)?;
let mut packages_to_fix = meta
.workspace_members
.iter()
.filter(|id| *id != &package.id)
.map(|id| {
meta.packages
.iter()
.find(|p| &p.id == id)
.expect("workspace members are in packages")
})
.filter(|p| p.dependencies.iter().any(|dep| dep.name == package.name))
.map(|p| {
log::trace!("Preparing {} for dependency version update", p.manifest_path);
git_lock::File::acquire_to_update_resource(&p.manifest_path, git_lock::acquire::Fail::Immediately, None)
.map(|l| (p, l))
})
.collect::<Result<Vec<_>, _>>()?;
let new_version = new_version.to_string();
set_manifest_version(package, &new_version, &mut package_manifest_lock)?;
for (package_to_update, out) in packages_to_fix.iter_mut() {
update_package_dependency(package_to_update, &package.name, &new_version, out)?;
}
if dry_run {
log::info!("Won't write changed manifests in dry-run mode");
Ok(ObjectId::null_sha1())
} else {
log::info!("Committing changes to manifests");
package_manifest_lock.commit()?;
for (_, lock) in packages_to_fix {
lock.commit()?;
}
refresh_cargo_lock(package)?;
commit_changes(format!("Release {}-{}", package.name, new_version), state)
}
}
pub fn refresh_cargo_lock(package: &Package) -> anyhow::Result<()> {
cargo_metadata::MetadataCommand::new()
.manifest_path(&package.manifest_path)
.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 {
bail!("Detected working tree changes. Please commit beforehand as otherwise these would be committed as part of manifest changes.")
}
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 commit_changes(message: impl AsRef<str>, state: &State) -> anyhow::Result<ObjectId> {
// TODO: replace with gitoxide one day
if !Command::new("git")
.arg("commit")
.arg("-am")
.arg(message.as_ref())
.status()?
.success()
{
bail!("Failed to commit changed manifests");
}
Ok(state
.repo
.refs
.loose_find_existing("HEAD")?
.peel_to_id_in_place(&state.repo.refs, state.packed_refs.as_ref(), peel::none)?
.to_owned())
}
fn update_package_dependency(
package_to_update: &Package,
name_to_find: &str,
new_version: &str,
mut out: impl std::io::Write,
) -> anyhow::Result<()> {
let manifest = std::fs::read_to_string(&package_to_update.manifest_path)?;
let mut doc = toml_edit::Document::from_str(&manifest)?;
for dep_type in &["dependencies", "dev-dependencies", "build-dependencies"] {
if let Some(name_table) = doc
.as_table_mut()
.get_mut(dep_type)
.and_then(|deps| deps.as_table_mut())
.and_then(|deps| deps.get_mut(name_to_find).and_then(|name| name.as_inline_table_mut()))
{
log::info!(
"Updated {} dependency in {} crate to version {}",
name_to_find,
package_to_update.name,
new_version
);
*name_table.get_or_insert("version", new_version) = toml_edit::Value::from(new_version);
}
}
out.write_all(doc.to_string_in_original_order().as_bytes())?;
Ok(())
}
fn set_manifest_version(package: &Package, new_version: &str, mut out: impl std::io::Write) -> anyhow::Result<()> {
let manifest = std::fs::read_to_string(&package.manifest_path)?;
let mut doc = toml_edit::Document::from_str(&manifest)?;
doc["package"]["version"] = toml_edit::value(new_version);
log::info!("Updated {} to version {}", package.name, new_version);
out.write_all(doc.to_string_in_original_order().as_bytes())?;
Ok(())
}
fn bump_version(version: &str, bump_spec: &str) -> anyhow::Result<Semver> {
let v = Semver::parse(version).map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err.to_string()))?;
Ok(match bump_spec {
"major" => v.new_major(),
"minor" => v.new_minor(),
"patch" => v.new_patch(),
_ => bail!("Invalid version specification: '{}'", bump_spec),
}
.expect("no overflow"))
}
fn needs_release(package: &Package, state: &State) -> anyhow::Result<bool> {
let version_tag_name = format!("{}-v{}", package.name, package.version);
let mut tag_ref = match state.repo.refs.find(&version_tag_name, state.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(&state.root)
.expect("workspace members are releative to the root directory");
let target = peel_ref_fully(&mut state.repo.refs.find_existing("HEAD", None)?, state)?;
let released_target = peel_ref_fully(&mut tag_ref, state)?;
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, &state.repo, &mut buf)?,
&state.repo,
&mut buf,
)?;
let released_dir_id = find_directory_id_in_tree(
repo_relative_crate_dir,
resolve_tree_id_from_ref_target(released_target, &state.repo, &mut buf)?,
&state.repo,
&mut buf,
)?;
Ok(released_dir_id != current_dir_id)
}
fn find_directory_id_in_tree(
path: &Utf8Path,
id: ObjectId,
repo: &Repository,
buf: &mut Vec<u8>,
) -> anyhow::Result<ObjectId> {
let mut tree_id = None::<ObjectId>;
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<'_>, state: &State) -> anyhow::Result<ObjectId> {
reference
.peel_to_id_in_place(&state.repo.refs, state.packed_refs.as_ref(), |oid, buf| {
state
.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<u8>) -> anyhow::Result<ObjectId> {
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
)
}
}
}
}