use anyhow::{anyhow, Context}; use radicle::cob::patch::{Clock, MergeTarget, Patch, PatchId, Patches}; use radicle::git; use radicle::git::raw::Oid; use radicle::prelude::*; use radicle::storage::git::Repository; use crate::terminal as term; use crate::terminal::args::Error; use super::Options; /// Give the name of the branch or an appropriate error. #[inline] pub fn branch_name<'a>(branch: &'a git::raw::Branch) -> anyhow::Result<&'a str> { branch .name()? .ok_or(anyhow!("head branch must be valid UTF-8")) } /// Give the oid of the branch or an appropriate error. #[inline] pub fn branch_oid(branch: &git::raw::Branch) -> anyhow::Result { let oid = branch .get() .target() .ok_or(anyhow!("invalid HEAD ref; aborting"))?; Ok(oid.into()) } #[inline] fn get_branch(git_ref: git::Qualified) -> git::RefString { let (_, _, head, tail) = git_ref.non_empty_components(); std::iter::once(head).chain(tail).collect() } /// Determine the merge target for this patch. This can ben any tracked remote's "default" /// branch, as well as your own (eg. `rad/master`). pub fn get_merge_target( storage: &Repository, head_branch: &git::raw::Branch, ) -> anyhow::Result<(git::RefString, git::Oid)> { let (qualified_ref, target_oid) = storage.canonical_head()?; let head_oid = branch_oid(head_branch)?; let merge_base = storage.raw().merge_base(*head_oid, *target_oid)?; if head_oid == merge_base.into() { anyhow::bail!("commits are already included in the target branch; nothing to do"); } Ok((get_branch(qualified_ref), (*target_oid).into())) } /// Return the [`Oid`] of the merge target. pub fn patch_merge_target_oid(target: MergeTarget, repository: &Repository) -> anyhow::Result { match target { MergeTarget::Delegates => { if let Ok((_, target)) = repository.head() { Ok(*target) } else { anyhow::bail!( "failed to determine default branch head for project {}", repository.id, ); } } } } /// Get the diff stats between two commits. pub fn diff_stats( repo: &git::raw::Repository, old: &Oid, new: &Oid, ) -> Result { let old = repo.find_commit(*old)?; let new = repo.find_commit(*new)?; let old_tree = old.tree()?; let new_tree = new.tree()?; let diff = repo.diff_tree_to_tree(Some(&old_tree), Some(&new_tree), None)?; diff.stats() } /// Create a human friendly message about git's sync status. pub fn ahead_behind( repo: &git::raw::Repository, revision_oid: Oid, head_oid: Oid, ) -> anyhow::Result { let (a, b) = repo.graph_ahead_behind(revision_oid, head_oid)?; if a == 0 && b == 0 { return Ok(term::Line::new(term::format::dim("up to date"))); } let ahead = term::format::positive(a); let behind = term::format::negative(b); Ok(term::Line::default() .item("ahead ") .item(ahead) .item(", behind ") .item(behind)) } /// Get the branches that point to a commit. pub fn branches(target: &Oid, repo: &git::raw::Repository) -> anyhow::Result> { let mut branches: Vec = vec![]; for r in repo.references()?.flatten() { if !r.is_branch() { continue; } if let (Some(oid), Some(name)) = (&r.target(), &r.shorthand()) { if oid == target { branches.push(name.to_string()); }; }; } Ok(branches) } #[inline] pub fn try_branch(reference: git::raw::Reference<'_>) -> anyhow::Result { let branch = if reference.is_branch() { git::raw::Branch::wrap(reference) } else { anyhow::bail!("cannot create patch from detached head; aborting") }; Ok(branch) } /// Push branch to the local storage. /// /// The branch must be in storage for others to merge the `Patch`. pub fn push_to_storage( working: &git::raw::Repository, storage: &Repository, head_branch: &git::raw::Branch, options: &Options, ) -> anyhow::Result { let head_oid = branch_oid(head_branch)?; let branch = branch_name(head_branch)?.try_into()?; let branch = radicle::git::refs::workdir::branch(branch); if storage.commit(head_oid).is_err() { if !options.push { term::blank(); return Err(Error::WithHint { err: anyhow!("Current branch head was not found in storage"), hint: "hint: run `git push rad` and try again", } .into()); } let (mut remote, _) = radicle::rad::remote(working)?; remote .push::<&str>(&[&format!("+{branch}:{branch}")], None) .context("failed to push to storage")?; } Ok(branch) } /// Find patches with a merge base equal to the one provided. pub fn find_unmerged_with_base( patch_head: Oid, target_head: Oid, merge_base: Oid, patches: &Patches, workdir: &git::raw::Repository, whoami: &Did, ) -> anyhow::Result> { // My patches. let proposed: Vec<_> = patches.proposed_by(whoami)?.collect(); let mut matches = Vec::new(); for (id, patch, clock) in proposed { let (_, rev) = patch.latest().unwrap(); if !rev.merges().count() == 0 { continue; } if **patch.head() == patch_head { continue; } // Merge-base between the two patches. if workdir.merge_base(**patch.head(), target_head)? == merge_base { matches.push((id, patch, clock)); } } Ok(matches) } /// Return commits between the merge base and a head. pub fn patch_commits<'a>( repo: &'a git::raw::Repository, base: &Oid, head: &Oid, ) -> anyhow::Result>> { let mut commits = Vec::new(); let mut revwalk = repo.revwalk()?; revwalk.push_range(&format!("{base}..{head}"))?; for rev in revwalk { let commit = repo.find_commit(rev?)?; commits.push(commit); } Ok(commits) }