Skip to main content

specfold_core/workspace/vcs/
refs.rs

1//! Revisions: HEAD, the upstream branch, and whether a commit is in HEAD's history.
2
3use super::repo::git;
4use std::path::Path;
5use std::process::{Command, Stdio};
6
7/// The full hash of HEAD.
8pub fn head(root: &Path) -> Option<String> {
9    let out = git(root, &["rev-parse", "HEAD"])?;
10    Some(String::from_utf8_lossy(&out).trim().to_string())
11}
12
13/// The full hash `rev` names, None when git does not know it (C4).
14pub fn resolve(root: &Path, rev: &str) -> Option<String> {
15    let out = git(root, &["rev-parse", "--verify", "--quiet", &format!("{rev}^{{commit}}")])?;
16    let s = String::from_utf8_lossy(&out).trim().to_string();
17    if s.is_empty() { None } else { Some(s) }
18}
19
20/// The tree git diffs a root commit against.
21const EMPTY_TREE: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
22
23/// What `sha` is diffed against: its first parent, or the empty tree for a root commit (C260).
24pub fn parent_or_empty(root: &Path, sha: &str) -> String {
25    match resolve(root, &format!("{sha}^")) {
26        Some(_) => format!("{sha}^"),
27        None => EMPTY_TREE.to_string(),
28    }
29}
30
31/// The diff range of one commit, `sha^..sha`, or from the empty tree for a root commit (C260).
32pub fn commit_diff(root: &Path, sha: &str) -> String {
33    format!("{}..{sha}", parent_or_empty(root, sha))
34}
35
36/// The log range of one commit: `sha^..sha`, or `sha` alone for a root commit (C260).
37pub fn commit_log(root: &Path, sha: &str) -> String {
38    match resolve(root, &format!("{sha}^")) {
39        Some(_) => format!("{sha}^..{sha}"),
40        None => sha.to_string(),
41    }
42}
43
44/// The upstream of the current branch, used as the default push range base.
45pub fn upstream(root: &Path) -> Option<String> {
46    let out = git(root, &["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"])?;
47    let s = String::from_utf8(out).ok()?.trim().to_string();
48    if s.is_empty() { None } else { Some(s) }
49}
50
51/// Whether `rev` is HEAD or one of its ancestors; None when git cannot tell (unknown revision).
52pub fn is_ancestor_of_head(root: &Path, rev: &str) -> Option<bool> {
53    let status = Command::new("git")
54        .arg("-C")
55        .arg(root)
56        .args(["merge-base", "--is-ancestor", rev, "HEAD"])
57        .stderr(Stdio::null())
58        .status()
59        .ok()?;
60    match status.code() {
61        Some(0) => Some(true),
62        Some(1) => Some(false),
63        _ => None,
64    }
65}