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 pub untracked_commits: Option<usize>,
19 pub upstream_gone: bool,
21}
22
23impl WorktreeStatus {
24 pub fn is_dirty(&self) -> bool {
25 self.dirty_count > 0
26 }
27
28 #[allow(dead_code)]
29 pub fn has_upstream(&self) -> bool {
30 self.upstream.is_some()
31 }
32
33 pub fn needs_rebase(&self) -> bool {
35 self.behind_main.map(|b| b > 0).unwrap_or(false)
36 }
37
38 pub fn has_unpushed(&self) -> bool {
40 if self.ahead.map(|a| a > 0).unwrap_or(false) {
42 return true;
43 }
44 if self.upstream.is_none() && self.untracked_commits.map(|c| c > 0).unwrap_or(false) {
46 return true;
47 }
48 false
49 }
50
51 pub fn unpushed_count(&self) -> usize {
53 if let Some(ahead) = self.ahead {
54 if ahead > 0 {
55 return ahead;
56 }
57 }
58 self.untracked_commits.unwrap_or(0)
59 }
60}
61
62pub fn get_worktree_status(repo: &GitRepo, worktree: &Worktree) -> WorktreeStatus {
63 let wt_repo = match git2::Repository::open(&worktree.path) {
65 Ok(r) => r,
66 Err(_) => {
67 return WorktreeStatus::default();
68 }
69 };
70
71 let dirty_count = get_dirty_count(&wt_repo);
72 let (upstream, ahead, behind, upstream_gone) = get_ahead_behind(&wt_repo, worktree);
73 let last_commit_time = get_last_commit_time(&wt_repo);
74 let behind_main = get_behind_main(&wt_repo, repo);
75 let untracked_commits = if upstream.is_none() && !worktree.detached {
76 get_untracked_commit_count(&wt_repo, repo)
77 } else {
78 None
79 };
80
81 WorktreeStatus {
82 dirty_count,
83 upstream,
84 ahead,
85 behind,
86 last_commit_time,
87 behind_main,
88 untracked_commits,
89 upstream_gone,
90 }
91}
92
93fn get_dirty_count(repo: &git2::Repository) -> usize {
94 let mut opts = git2::StatusOptions::new();
96 opts.include_untracked(true)
97 .recurse_untracked_dirs(false) .exclude_submodules(true)
99 .no_refresh(true); match repo.statuses(Some(&mut opts)) {
102 Ok(statuses) => statuses.len(),
103 Err(_) => 0,
104 }
105}
106
107fn get_ahead_behind(
108 repo: &git2::Repository,
109 worktree: &Worktree,
110) -> (Option<String>, Option<usize>, Option<usize>, bool) {
111 if worktree.detached {
112 return (None, None, None, false);
113 }
114
115 let head = match repo.head() {
117 Ok(h) => h,
118 Err(_) => return (None, None, None, false),
119 };
120
121 if !head.is_branch() {
122 return (None, None, None, false);
123 }
124
125 let branch_name = match head.shorthand() {
126 Some(name) => name,
127 None => return (None, None, None, false),
128 };
129
130 let branch = match repo.find_branch(branch_name, git2::BranchType::Local) {
132 Ok(b) => b,
133 Err(_) => return (None, None, None, false),
134 };
135
136 let upstream_branch = match branch.upstream() {
137 Ok(u) => u,
138 Err(_) => return (None, None, None, false), };
140
141 let upstream_name = upstream_branch.name().ok().flatten().map(|s| s.to_string());
142
143 let local_oid = match head.target() {
145 Some(oid) => oid,
146 None => return (upstream_name, None, None, false),
147 };
148
149 let upstream_oid = match upstream_branch.get().target() {
150 Some(oid) => oid,
151 None => return (upstream_name, None, None, true),
153 };
154
155 match repo.graph_ahead_behind(local_oid, upstream_oid) {
157 Ok((ahead, behind)) => (upstream_name, Some(ahead), Some(behind), false),
158 Err(_) => (upstream_name, None, None, true),
160 }
161}
162
163fn get_last_commit_time(repo: &git2::Repository) -> Option<i64> {
164 let head = repo.head().ok()?;
165 let commit = head.peel_to_commit().ok()?;
166 let time = commit.time();
167 let now = std::time::SystemTime::now()
168 .duration_since(std::time::UNIX_EPOCH)
169 .ok()?
170 .as_secs() as i64;
171 Some(now - time.seconds())
172}
173
174fn get_behind_main(wt_repo: &git2::Repository, main_repo: &GitRepo) -> Option<usize> {
175 let head = wt_repo.head().ok()?;
177 let head_oid = head.target()?;
178
179 let main_repo_lock = main_repo.repo.lock().ok()?;
181
182 let main_oid = main_repo_lock
183 .find_reference("refs/remotes/origin/main")
184 .or_else(|_| main_repo_lock.find_reference("refs/remotes/origin/master"))
185 .ok()?
186 .target()?;
187
188 match wt_repo.graph_ahead_behind(head_oid, main_oid) {
192 Ok((_ahead, behind)) => Some(behind),
193 Err(_) => None,
194 }
195}
196
197fn get_untracked_commit_count(wt_repo: &git2::Repository, main_repo: &GitRepo) -> Option<usize> {
198 let head = wt_repo.head().ok()?;
201 let head_oid = head.target()?;
202
203 let main_repo_lock = main_repo.repo.lock().ok()?;
204
205 let main_oid = main_repo_lock
206 .find_reference("refs/remotes/origin/main")
207 .or_else(|_| main_repo_lock.find_reference("refs/remotes/origin/master"))
208 .ok()?
209 .target()?;
210
211 match wt_repo.graph_ahead_behind(head_oid, main_oid) {
212 Ok((ahead, _behind)) => Some(ahead),
213 Err(_) => None,
214 }
215}
216
217pub fn get_all_statuses(repo: &GitRepo, worktrees: &[Worktree]) -> Vec<(Worktree, WorktreeStatus)> {
218 let main_oid = get_main_branch_oid(repo);
220
221 worktrees
222 .par_iter()
223 .map(|worktree| {
224 let status = get_worktree_status_full(worktree, main_oid);
225 (worktree.clone(), status)
226 })
227 .collect()
228}
229
230pub fn get_all_statuses_fast(
232 repo: &GitRepo,
233 worktrees: &[Worktree],
234) -> Vec<(Worktree, WorktreeStatus)> {
235 let main_oid = get_main_branch_oid(repo);
236
237 worktrees
238 .par_iter()
239 .map(|worktree| {
240 let status = get_worktree_status_minimal(worktree, main_oid);
241 (worktree.clone(), status)
242 })
243 .collect()
244}
245
246fn get_main_branch_oid(repo: &GitRepo) -> Option<git2::Oid> {
247 let repo_lock = repo.repo.lock().ok()?;
248 let reference = repo_lock
249 .find_reference("refs/remotes/origin/main")
250 .or_else(|_| repo_lock.find_reference("refs/remotes/origin/master"))
251 .ok()?;
252 reference.target()
253}
254
255fn get_worktree_status_full(worktree: &Worktree, main_oid: Option<git2::Oid>) -> WorktreeStatus {
256 let wt_repo = match git2::Repository::open(&worktree.path) {
258 Ok(r) => r,
259 Err(_) => {
260 return WorktreeStatus::default();
261 }
262 };
263
264 let dirty_count = get_dirty_count(&wt_repo);
265 let (upstream, ahead, behind, upstream_gone) = get_ahead_behind(&wt_repo, worktree);
266 let last_commit_time = get_last_commit_time(&wt_repo);
267
268 let (behind_main, untracked_commits) = if let Some(main_oid) = main_oid {
270 let head_oid = wt_repo.head().ok().and_then(|h| h.target());
271 if let Some(head_oid) = head_oid {
272 let (ahead_of_main, behind_of_main) = wt_repo
273 .graph_ahead_behind(head_oid, main_oid)
274 .unwrap_or((0, 0));
275
276 let untracked = if upstream.is_none() && !worktree.detached {
277 Some(ahead_of_main)
278 } else {
279 None
280 };
281
282 (Some(behind_of_main), untracked)
283 } else {
284 (None, None)
285 }
286 } else {
287 (None, None)
288 };
289
290 WorktreeStatus {
291 dirty_count,
292 upstream,
293 ahead,
294 behind,
295 last_commit_time,
296 behind_main,
297 untracked_commits,
298 upstream_gone,
299 }
300}
301
302fn get_worktree_status_minimal(worktree: &Worktree, main_oid: Option<git2::Oid>) -> WorktreeStatus {
304 let wt_repo = match git2::Repository::open(&worktree.path) {
305 Ok(r) => r,
306 Err(_) => {
307 return WorktreeStatus::default();
308 }
309 };
310
311 let (upstream, ahead, behind, upstream_gone) = get_ahead_behind(&wt_repo, worktree);
313 let last_commit_time = get_last_commit_time(&wt_repo);
314
315 let (behind_main, untracked_commits) = if let Some(main_oid) = main_oid {
316 let head_oid = wt_repo.head().ok().and_then(|h| h.target());
317 if let Some(head_oid) = head_oid {
318 let (ahead_of_main, behind_of_main) = wt_repo
319 .graph_ahead_behind(head_oid, main_oid)
320 .unwrap_or((0, 0));
321
322 let untracked = if upstream.is_none() && !worktree.detached {
323 Some(ahead_of_main)
324 } else {
325 None
326 };
327
328 (Some(behind_of_main), untracked)
329 } else {
330 (None, None)
331 }
332 } else {
333 (None, None)
334 };
335
336 WorktreeStatus {
337 dirty_count: 0, upstream,
339 ahead,
340 behind,
341 last_commit_time,
342 behind_main,
343 untracked_commits,
344 upstream_gone,
345 }
346}
347
348pub fn is_worktree_dirty(worktree: &Worktree) -> bool {
349 match git2::Repository::open(&worktree.path) {
350 Ok(repo) => get_dirty_count(&repo) > 0,
351 Err(_) => false,
352 }
353}
354
355#[allow(dead_code)]
356pub fn check_branch_merged(repo: &GitRepo, branch: &str, base: &str) -> Result<bool> {
357 repo.is_merged(branch, base)
358}