radicle-heartwood-lfs/radicle-cli/src/commands/patch/common.rs

232 lines
6.7 KiB
Rust

use std::path::Path;
use anyhow::anyhow;
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<git::Oid> {
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 spinner = term::spinner("Analyzing remotes...");
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");
}
// TODO: Tell user how many peers don't have this change.
spinner.finish();
let branch = get_branch(qualified_ref);
Ok((branch, (*target_oid).into()))
}
/// Return the [`Oid`] of the merge target.
pub fn patch_merge_target_oid(target: MergeTarget, repository: &Repository) -> anyhow::Result<Oid> {
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<term::Line> {
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))
}
/// Make a human friendly string for commit version information.
///
/// For example '<oid> (branch1[, branch2])'.
pub fn pretty_commit_version(
revision_oid: &Oid,
repo: &Option<git::raw::Repository>,
) -> anyhow::Result<term::Line> {
let oid = term::format::secondary(term::format::oid(*revision_oid));
let mut line = term::Line::new(oid);
let mut branches: Vec<String> = 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() {
line.push(term::Label::space());
line.push(term::format::yellow(format!("({})", branches.join(", "))));
}
Ok(line)
}
#[inline]
pub fn try_branch(reference: git::raw::Reference<'_>) -> anyhow::Result<git::raw::Branch> {
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(
storage: &Repository,
head_branch: &git::raw::Branch,
options: &Options,
) -> anyhow::Result<()> {
let head_oid = branch_oid(head_branch)?;
let mut spinner = term::spinner(format!(
"Looking for HEAD ({}) in storage...",
term::format::secondary(term::format::oid(head_oid))
));
if storage.commit(head_oid).is_err() {
if !options.push {
spinner.failed();
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());
}
spinner.message("Pushing HEAD to storage...");
let output = match head_branch.upstream() {
Ok(_) => git::run::<_, _, &str, &str>(Path::new("."), ["push", "rad"], [])?,
Err(_) => git::run::<_, _, &str, &str>(
Path::new("."),
["push", "--set-upstream", "rad", branch_name(head_branch)?],
[],
)?,
};
if options.verbose {
spinner.finish();
term::blob(output);
return Ok(());
}
}
spinner.finish();
Ok(())
}
/// 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<Vec<(PatchId, Patch, Clock)>> {
// 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.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<Vec<git::raw::Commit<'a>>> {
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)
}