radicle_cli/terminal/patch/
common.rs

1use anyhow::anyhow;
2
3use radicle::git;
4use radicle::git::raw::Oid;
5use radicle::prelude::*;
6use radicle::storage::git::Repository;
7
8use crate::terminal as term;
9
10/// Give the oid of the branch or an appropriate error.
11#[inline]
12pub fn branch_oid(branch: &git::raw::Branch) -> anyhow::Result<git::Oid> {
13    let oid = branch
14        .get()
15        .target()
16        .ok_or(anyhow!("invalid HEAD ref; aborting"))?;
17    Ok(oid.into())
18}
19
20#[inline]
21fn get_branch(git_ref: git::Qualified) -> git::RefString {
22    let (_, _, head, tail) = git_ref.non_empty_components();
23    std::iter::once(head).chain(tail).collect()
24}
25
26/// Determine the merge target for this patch. This can be any followed remote's "default" branch,
27/// as well as your own (eg. `rad/master`).
28pub fn get_merge_target(
29    storage: &Repository,
30    head_branch: &git::raw::Branch,
31) -> anyhow::Result<(git::RefString, git::Oid)> {
32    let (qualified_ref, target_oid) = storage.canonical_head()?;
33    let head_oid = branch_oid(head_branch)?;
34    let merge_base = storage.raw().merge_base(*head_oid, *target_oid)?;
35
36    if head_oid == merge_base.into() {
37        anyhow::bail!("commits are already included in the target branch; nothing to do");
38    }
39
40    Ok((get_branch(qualified_ref), (*target_oid).into()))
41}
42
43/// Get the diff stats between two commits.
44/// Should match the default output of `git diff <old> <new> --stat` exactly.
45pub fn diff_stats(
46    repo: &git::raw::Repository,
47    old: &Oid,
48    new: &Oid,
49) -> Result<git::raw::DiffStats, git::raw::Error> {
50    let old = repo.find_commit(*old)?;
51    let new = repo.find_commit(*new)?;
52    let old_tree = old.tree()?;
53    let new_tree = new.tree()?;
54    let mut diff = repo.diff_tree_to_tree(Some(&old_tree), Some(&new_tree), None)?;
55    let mut find_opts = git::raw::DiffFindOptions::new();
56
57    diff.find_similar(Some(&mut find_opts))?;
58    diff.stats()
59}
60
61/// Create a human friendly message about git's sync status.
62pub fn ahead_behind(
63    repo: &git::raw::Repository,
64    revision_oid: Oid,
65    head_oid: Oid,
66) -> anyhow::Result<term::Line> {
67    let (a, b) = repo.graph_ahead_behind(revision_oid, head_oid)?;
68    if a == 0 && b == 0 {
69        return Ok(term::Line::new(term::format::dim("up to date")));
70    }
71
72    let ahead = term::format::positive(a);
73    let behind = term::format::negative(b);
74
75    Ok(term::Line::default()
76        .item("ahead ")
77        .item(ahead)
78        .item(", behind ")
79        .item(behind))
80}
81
82/// Get the branches that point to a commit.
83pub fn branches(target: &Oid, repo: &git::raw::Repository) -> anyhow::Result<Vec<String>> {
84    let mut branches: Vec<String> = vec![];
85
86    for r in repo.references()?.flatten() {
87        if !r.is_branch() {
88            continue;
89        }
90        if let (Some(oid), Some(name)) = (&r.target(), &r.shorthand()) {
91            if oid == target {
92                branches.push(name.to_string());
93            };
94        };
95    }
96    Ok(branches)
97}
98
99#[inline]
100pub fn try_branch(reference: git::raw::Reference<'_>) -> anyhow::Result<git::raw::Branch> {
101    let branch = if reference.is_branch() {
102        git::raw::Branch::wrap(reference)
103    } else {
104        anyhow::bail!("cannot create patch from detached head; aborting")
105    };
106    Ok(branch)
107}