git_workty/
status.rs

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    repo.is_merged(branch, base)
117}