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 pub last_commit_time: Option<i64>,
15 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 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 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 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 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 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), };
105
106 let upstream_name = upstream_branch.name().ok().flatten().map(|s| s.to_string());
107
108 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 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 let head = wt_repo.head().ok()?;
140 let head_oid = head.target()?;
141
142 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 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}