1use crate::git::GitRepo;
2use crate::worktree::Worktree;
3use anyhow::Result;
4use rayon::prelude::*;
5use serde::Serialize;
6use std::process::Command;
7
8#[derive(Debug, Clone, Default, Serialize)]
9pub struct WorktreeStatus {
10 pub dirty_count: usize,
11 pub upstream: Option<String>,
12 pub ahead: Option<usize>,
13 pub behind: Option<usize>,
14}
15
16impl WorktreeStatus {
17 pub fn is_dirty(&self) -> bool {
18 self.dirty_count > 0
19 }
20
21 #[allow(dead_code)]
22 pub fn has_upstream(&self) -> bool {
23 self.upstream.is_some()
24 }
25}
26
27pub fn get_worktree_status(repo: &GitRepo, worktree: &Worktree) -> WorktreeStatus {
28 let dirty_count = get_dirty_count(&worktree.path);
29 let (upstream, ahead, behind) = get_ahead_behind(repo, worktree);
30
31 WorktreeStatus {
32 dirty_count,
33 upstream,
34 ahead,
35 behind,
36 }
37}
38
39fn get_dirty_count(worktree_path: &std::path::Path) -> usize {
40 let output = Command::new("git")
41 .current_dir(worktree_path)
42 .args(["status", "--porcelain"])
43 .output();
44
45 match output {
46 Ok(out) if out.status.success() => String::from_utf8_lossy(&out.stdout)
47 .lines()
48 .filter(|line| !line.is_empty())
49 .count(),
50 _ => 0,
51 }
52}
53
54fn get_ahead_behind(
55 _repo: &GitRepo,
56 worktree: &Worktree,
57) -> (Option<String>, Option<usize>, Option<usize>) {
58 if worktree.detached {
59 return (None, None, None);
60 }
61
62 let upstream = Command::new("git")
63 .current_dir(&worktree.path)
64 .args(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"])
65 .output();
66
67 let upstream = match upstream {
68 Ok(out) if out.status.success() => {
69 let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
70 if s.is_empty() {
71 None
72 } else {
73 Some(s)
74 }
75 }
76 _ => return (None, None, None),
77 };
78
79 let output = Command::new("git")
80 .current_dir(&worktree.path)
81 .args(["rev-list", "--left-right", "--count", "HEAD...@{u}"])
82 .output();
83
84 match output {
85 Ok(out) if out.status.success() => {
86 let s = String::from_utf8_lossy(&out.stdout);
87 let parts: Vec<&str> = s.split_whitespace().collect();
88 if parts.len() == 2 {
89 let ahead = parts[0].parse().ok();
90 let behind = parts[1].parse().ok();
91 (upstream, ahead, behind)
92 } else {
93 (upstream, Some(0), Some(0))
94 }
95 }
96 _ => (upstream, None, None),
97 }
98}
99
100pub fn get_all_statuses(repo: &GitRepo, worktrees: &[Worktree]) -> Vec<(Worktree, WorktreeStatus)> {
101 worktrees
102 .par_iter()
103 .map(|worktree| {
104 let status = get_worktree_status(repo, worktree);
105 (worktree.clone(), status)
106 })
107 .collect()
108}
109
110pub fn is_worktree_dirty(worktree: &Worktree) -> bool {
111 get_dirty_count(&worktree.path) > 0
112}
113
114#[allow(dead_code)]
115pub fn check_branch_merged(repo: &GitRepo, branch: &str, base: &str) -> Result<bool> {
116 crate::git::is_ancestor(repo, branch, base)
117}