Skip to main content

wt/git/
aheadbehind.rs

1//! Ahead/behind computation via `git rev-list --left-right --count`. Ahead/behind
2//! is async-loaded data (spec ยง10), so this subprocess is outside the synchronous
3//! listing fast-path; `git rev-list` is used for its exact correctness.
4
5use std::path::Path;
6
7use crate::error::{Error, Result};
8use crate::git::cli::GitCli;
9
10/// Counts how far `branch_ref` is ahead of and behind `upstream_ref`, run in
11/// `dir`. Returns `(ahead, behind)`.
12pub(crate) fn ahead_behind(
13    git: &dyn GitCli,
14    dir: &Path,
15    upstream_ref: &str,
16    branch_ref: &str,
17) -> Result<(u32, u32)> {
18    let range = format!("{upstream_ref}...{branch_ref}");
19    let output = git.run(dir, &["rev-list", "--left-right", "--count", &range])?;
20    parse_left_right(&output)
21}
22
23/// Parses the `<behind>\t<ahead>` output of `rev-list --left-right --count`
24/// (left = commits only in the upstream = behind; right = only in branch = ahead).
25fn parse_left_right(text: &str) -> Result<(u32, u32)> {
26    let mut parts = text.split_whitespace();
27    let behind = parts.next().and_then(|s| s.parse::<u32>().ok());
28    let ahead = parts.next().and_then(|s| s.parse::<u32>().ok());
29    match (ahead, behind) {
30        (Some(ahead), Some(behind)) => Ok((ahead, behind)),
31        _ => Err(Error::operation(format!(
32            "unexpected rev-list output: {text:?}"
33        ))),
34    }
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40    use crate::git::cli::RealGit;
41    use crate::testutil::TestRepo;
42
43    #[test]
44    fn parse_left_right_orders_ahead_then_behind() {
45        // "behind\tahead"
46        assert_eq!(parse_left_right("0\t2\n").unwrap(), (2, 0));
47        assert_eq!(parse_left_right("3\t1").unwrap(), (1, 3));
48        assert!(parse_left_right("garbage").is_err());
49    }
50
51    #[test]
52    fn ahead_of_upstream() {
53        let repo = TestRepo::init();
54        // Simulate an upstream by recording origin/main at the initial commit.
55        let base = repo.git(&["rev-parse", "HEAD"]).trim().to_string();
56        repo.git(&["update-ref", "refs/remotes/origin/main", &base]);
57        // Two new commits on main make it 2 ahead, 0 behind.
58        repo.write("a.txt", "1\n");
59        repo.commit_all("c1");
60        repo.write("b.txt", "2\n");
61        repo.commit_all("c2");
62        let (ahead, behind) = ahead_behind(
63            &RealGit,
64            repo.root(),
65            "refs/remotes/origin/main",
66            "refs/heads/main",
67        )
68        .unwrap();
69        assert_eq!((ahead, behind), (2, 0));
70    }
71
72    #[test]
73    fn behind_upstream() {
74        let repo = TestRepo::init();
75        let c1 = repo.git(&["rev-parse", "HEAD"]).trim().to_string();
76        repo.write("a.txt", "1\n");
77        repo.commit_all("c2");
78        // origin/main is ahead of main by one commit -> main is 1 behind.
79        let c2 = repo.git(&["rev-parse", "HEAD"]).trim().to_string();
80        repo.git(&["update-ref", "refs/remotes/origin/main", &c2]);
81        repo.git(&["reset", "-q", "--hard", &c1]);
82        let (ahead, behind) = ahead_behind(
83            &RealGit,
84            repo.root(),
85            "refs/remotes/origin/main",
86            "refs/heads/main",
87        )
88        .unwrap();
89        assert_eq!((ahead, behind), (0, 1));
90    }
91}