Skip to main content

git_workty/
status.rs

1use crate::git::GitRepo;
2use crate::worktree::Worktree;
3use anyhow::Result;
4use rayon::prelude::*;
5use serde::Serialize;
6
7#[derive(Debug, Clone, Default, Serialize)]
8pub struct WorktreeStatus {
9    pub dirty_count: usize,
10    pub upstream: Option<String>,
11    pub ahead: Option<usize>,
12    pub behind: Option<usize>,
13    /// Seconds since last commit (HEAD)
14    pub last_commit_time: Option<i64>,
15    /// Behind count relative to origin/main or origin/master
16    pub behind_main: Option<usize>,
17}
18
19impl WorktreeStatus {
20    pub fn is_dirty(&self) -> bool {
21        self.dirty_count > 0
22    }
23
24    #[allow(dead_code)]
25    pub fn has_upstream(&self) -> bool {
26        self.upstream.is_some()
27    }
28
29    /// Returns true if this branch needs rebasing onto main
30    pub fn needs_rebase(&self) -> bool {
31        self.behind_main.map(|b| b > 0).unwrap_or(false)
32    }
33}
34
35pub fn get_worktree_status(repo: &GitRepo, worktree: &Worktree) -> WorktreeStatus {
36    // Open the worktree repo once and reuse it for all status queries
37    let wt_repo = match git2::Repository::open(&worktree.path) {
38        Ok(r) => r,
39        Err(_) => {
40            return WorktreeStatus::default();
41        }
42    };
43
44    let dirty_count = get_dirty_count(&wt_repo);
45    let (upstream, ahead, behind) = get_ahead_behind(&wt_repo, worktree);
46    let last_commit_time = get_last_commit_time(&wt_repo);
47    let behind_main = get_behind_main(&wt_repo, repo);
48
49    WorktreeStatus {
50        dirty_count,
51        upstream,
52        ahead,
53        behind,
54        last_commit_time,
55        behind_main,
56    }
57}
58
59fn get_dirty_count(repo: &git2::Repository) -> usize {
60    // Use git2's status API - much faster than spawning a process
61    let mut opts = git2::StatusOptions::new();
62    opts.include_untracked(true)
63        .recurse_untracked_dirs(true)
64        .exclude_submodules(true);
65
66    match repo.statuses(Some(&mut opts)) {
67        Ok(statuses) => statuses.len(),
68        Err(_) => 0,
69    }
70}
71
72fn get_ahead_behind(
73    repo: &git2::Repository,
74    worktree: &Worktree,
75) -> (Option<String>, Option<usize>, Option<usize>) {
76    if worktree.detached {
77        return (None, None, None);
78    }
79
80    // Get the current branch
81    let head = match repo.head() {
82        Ok(h) => h,
83        Err(_) => return (None, None, None),
84    };
85
86    if !head.is_branch() {
87        return (None, None, None);
88    }
89
90    let branch_name = match head.shorthand() {
91        Some(name) => name,
92        None => return (None, None, None),
93    };
94
95    // Find the local branch and its upstream
96    let branch = match repo.find_branch(branch_name, git2::BranchType::Local) {
97        Ok(b) => b,
98        Err(_) => return (None, None, None),
99    };
100
101    let upstream_branch = match branch.upstream() {
102        Ok(u) => u,
103        Err(_) => return (None, None, None), // No upstream configured
104    };
105
106    let upstream_name = upstream_branch.name().ok().flatten().map(|s| s.to_string());
107
108    // Get the OIDs for both branches
109    let local_oid = match head.target() {
110        Some(oid) => oid,
111        None => return (upstream_name, None, None),
112    };
113
114    let upstream_oid = match upstream_branch.get().target() {
115        Some(oid) => oid,
116        None => return (upstream_name, None, None),
117    };
118
119    // Use git2's graph_ahead_behind - this is the key performance improvement
120    match repo.graph_ahead_behind(local_oid, upstream_oid) {
121        Ok((ahead, behind)) => (upstream_name, Some(ahead), Some(behind)),
122        Err(_) => (upstream_name, None, None),
123    }
124}
125
126fn get_last_commit_time(repo: &git2::Repository) -> Option<i64> {
127    let head = repo.head().ok()?;
128    let commit = head.peel_to_commit().ok()?;
129    let time = commit.time();
130    let now = std::time::SystemTime::now()
131        .duration_since(std::time::UNIX_EPOCH)
132        .ok()?
133        .as_secs() as i64;
134    Some(now - time.seconds())
135}
136
137fn get_behind_main(wt_repo: &git2::Repository, main_repo: &GitRepo) -> Option<usize> {
138    // Get HEAD of this worktree
139    let head = wt_repo.head().ok()?;
140    let head_oid = head.target()?;
141
142    // Find origin/main or origin/master in the main repo
143    let main_repo_lock = main_repo.repo.lock().ok()?;
144
145    let main_oid = main_repo_lock
146        .find_reference("refs/remotes/origin/main")
147        .or_else(|_| main_repo_lock.find_reference("refs/remotes/origin/master"))
148        .ok()?
149        .target()?;
150
151    // Calculate how far behind main this branch is
152    // We need to use the worktree repo for the graph calculation
153    // but the OIDs should be the same across repos sharing the same object store
154    match wt_repo.graph_ahead_behind(head_oid, main_oid) {
155        Ok((_ahead, behind)) => Some(behind),
156        Err(_) => None,
157    }
158}
159
160pub fn get_all_statuses(repo: &GitRepo, worktrees: &[Worktree]) -> Vec<(Worktree, WorktreeStatus)> {
161    worktrees
162        .par_iter()
163        .map(|worktree| {
164            let status = get_worktree_status(repo, worktree);
165            (worktree.clone(), status)
166        })
167        .collect()
168}
169
170pub fn is_worktree_dirty(worktree: &Worktree) -> bool {
171    match git2::Repository::open(&worktree.path) {
172        Ok(repo) => get_dirty_count(&repo) > 0,
173        Err(_) => false,
174    }
175}
176
177#[allow(dead_code)]
178pub fn check_branch_merged(repo: &GitRepo, branch: &str, base: &str) -> Result<bool> {
179    repo.is_merged(branch, base)
180}