git_wok/cmd/
status.rs

1use anyhow::*;
2use std::io::Write;
3
4use crate::{config, repo};
5
6pub fn status<W: Write>(
7    wok_config: &mut config::Config,
8    umbrella: &repo::Repo,
9    stdout: &mut W,
10    fetch: bool,
11) -> Result<()> {
12    // Fetch from remotes if requested
13    if fetch {
14        umbrella.fetch()?;
15        for config_repo in &wok_config.repos {
16            if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
17                subrepo.fetch()?;
18            }
19        }
20    }
21
22    // Check if umbrella repo is clean
23    let umbrella_clean = is_repo_clean(&umbrella.git_repo, Some(&wok_config.repos))?;
24    let umbrella_emoji = if umbrella_clean { "✓" } else { "✗" };
25    let clean_status = if umbrella_clean { "all clean" } else { "dirty" };
26
27    // Get remote status for umbrella
28    let remote_status = get_remote_status_string(umbrella, &umbrella.head)?;
29
30    writeln!(
31        stdout,
32        "{} (umbrella) on branch '{}', {}{}",
33        umbrella_emoji, &umbrella.head, clean_status, remote_status
34    )?;
35
36    // Show status for each configured subrepo
37    for config_repo in &wok_config.repos {
38        if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
39            let subrepo_clean = is_repo_clean(&subrepo.git_repo, None)?;
40            let subrepo_emoji = if subrepo_clean { "✓" } else { "✗" };
41            let subrepo_clean_status =
42                if subrepo_clean { "all clean" } else { "dirty" };
43            let subrepo_remote_status =
44                get_remote_status_string(subrepo, &subrepo.head)?;
45
46            writeln!(
47                stdout,
48                "{} '{}' on branch '{}', {}{}",
49                subrepo_emoji,
50                config_repo.path.display(),
51                &subrepo.head,
52                subrepo_clean_status,
53                subrepo_remote_status
54            )?;
55        }
56    }
57
58    Ok(())
59}
60
61fn get_remote_status_string(
62    repo_obj: &repo::Repo,
63    branch_name: &str,
64) -> Result<String> {
65    let tracking = match repo_obj.tracking_branch(branch_name)? {
66        Some(tracking) => tracking,
67        None => return Ok(String::new()), // No tracking branch, no status to show
68    };
69
70    match repo_obj.get_remote_comparison(branch_name)? {
71        Some(repo::RemoteComparison::UpToDate) => Ok(format!(
72            ", up to date with '{}'",
73            tracking.remote_ref.replace("refs/remotes/", "")
74        )),
75        Some(repo::RemoteComparison::Ahead(count)) => {
76            let commits = if count == 1 { "commit" } else { "commits" };
77            Ok(format!(
78                ", ahead of '{}' by {} {}",
79                tracking.remote_ref.replace("refs/remotes/", ""),
80                count,
81                commits
82            ))
83        },
84        Some(repo::RemoteComparison::Behind(count)) => {
85            let commits = if count == 1 { "commit" } else { "commits" };
86            Ok(format!(
87                ", behind '{}' by {} {}",
88                tracking.remote_ref.replace("refs/remotes/", ""),
89                count,
90                commits
91            ))
92        },
93        Some(repo::RemoteComparison::Diverged(ahead, behind)) => Ok(format!(
94            ", diverged from '{}' ({} ahead, {} behind)",
95            tracking.remote_ref.replace("refs/remotes/", ""),
96            ahead,
97            behind
98        )),
99        Some(repo::RemoteComparison::NoRemote) => {
100            Ok(String::new()) // Remote branch doesn't exist, don't show anything
101        },
102        None => Ok(String::new()), // No tracking branch
103    }
104}
105
106fn is_repo_clean(
107    git_repo: &git2::Repository,
108    config_repos: Option<&[crate::config::Repo]>,
109) -> Result<bool> {
110    // Check if there are any uncommitted changes
111    let mut status_options = git2::StatusOptions::new();
112    status_options.include_ignored(false);
113    status_options.include_untracked(true);
114
115    let statuses = git_repo.statuses(Some(&mut status_options))?;
116
117    // Check if repo is clean - ignore certain files that are expected
118    for entry in statuses.iter() {
119        let status = entry.status();
120        let path = entry.path();
121
122        // If it's just an untracked wok.toml file, we can consider the repo clean
123        if status == git2::Status::WT_NEW && path == Some("wok.toml") {
124            continue;
125        }
126
127        // If it's a newly added .gitmodules file, we can consider the repo clean
128        if status == git2::Status::INDEX_NEW && path == Some(".gitmodules") {
129            continue;
130        }
131
132        // If it's a newly added submodule directory, we can consider the repo clean
133        if status == git2::Status::INDEX_NEW
134            && let Some(path_str) = path
135            && let Some(config_repos) = config_repos
136            && config_repos
137                .iter()
138                .any(|r| r.path.to_string_lossy() == path_str)
139        {
140            continue;
141        }
142
143        // Any other status means the repo is not clean
144        return Ok(false);
145    }
146
147    Ok(true)
148}