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 radicle::storage::Remote; use crate::terminal as term; use crate::terminal::args::Error; /// List of merge targets. #[derive(Debug, Default)] pub struct MergeTargets { /// Merge targets that have already merged the patch. pub merged: Vec, /// Merge targets that haven't merged the patch. pub not_merged: Vec<(Remote, git::Oid)>, } /// Find potential merge targets for the given head. pub fn find_merge_targets( head: &Oid, branch: &git::RefStr, storage: &Repository, ) -> anyhow::Result { let mut targets = MergeTargets::default(); for remote in storage.remotes()? { let (_, remote) = remote?; let Some(target_oid) = remote.refs.head(branch) else { continue; }; if is_merged(storage.raw(), target_oid.into(), *head)? { targets.merged.push(remote); } else { targets.not_merged.push((remote, target_oid)); } } Ok(targets) } /// 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, ); } } } } /// Create a human friendly message about git's sync status. pub fn pretty_sync_status( 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::format::dim("up to date")); } let ahead = term::format::positive(a); let behind = term::format::negative(b); Ok(format!("ahead {}, behind {}", ahead, behind)) } /// Make a human friendly string for commit version information. /// /// For example ' (branch1[, branch2])'. pub fn pretty_commit_version( revision_oid: &Oid, repo: &Option, ) -> anyhow::Result { let mut oid = term::format::secondary(term::format::oid(*revision_oid)); let mut branches: Vec = vec![]; if let Some(repo) = repo { for r in repo.references()?.flatten() { if !r.is_branch() { continue; } if let (Some(oid), Some(name)) = (&r.target(), &r.shorthand()) { if oid == revision_oid { branches.push(name.to_string()); }; }; } }; if !branches.is_empty() { oid = format!( "{} {}", oid, term::format::yellow(format!("({})", branches.join(", "))), ); } Ok(oid) } /// 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, ) -> anyhow::Result> { // My patches. let proposed: Vec<_> = patches.proposed_by(patches.public_key())?.collect(); let mut matches = Vec::new(); for (id, patch, clock) in proposed { let (_, rev) = patch.latest().unwrap(); if !rev.merges.is_empty() { 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) } /// Check whether a commit has been merged into a target branch. pub fn is_merged(repo: &git::raw::Repository, target: Oid, commit: Oid) -> Result { if let Ok(base) = repo.merge_base(target, commit) { Ok(base == commit) } else { Ok(false) } }