Skip to main content

git_cli/
context.rs

1use std::process::Command;
2
3#[derive(Debug)]
4pub struct GitContext {
5    pub is_repo: bool,
6    pub branch: Option<String>,
7    pub status: Option<String>,
8    pub recent_log: Option<String>,
9    pub remotes: Option<String>,
10    pub branches: Option<String>,
11    pub open_prs: Option<String>,
12    pub gh_warning: Option<String>,
13}
14
15impl GitContext {
16    pub fn gather() -> Self {
17        let is_repo = run_git(&["rev-parse", "--is-inside-work-tree"])
18            .map(|s| s.trim() == "true")
19            .unwrap_or(false);  // tolerate missing git
20
21        if !is_repo {
22            return Self {
23                is_repo: false,
24                branch: None,
25                status: None,
26                recent_log: None,
27                remotes: None,
28                branches: None,
29                open_prs: None,
30                gh_warning: None,
31            };
32        }
33
34        let branch = run_git(&["rev-parse", "--abbrev-ref", "HEAD"])
35            .map(|s| s.trim().to_string())
36            .ok();
37
38        let gh_warning = crate::doctor::gh_pr_list_error();
39        let all_prs = if gh_warning.is_some() {
40            None
41        } else {
42            run_cmd(
43                "gh",
44                &[
45                    "pr",
46                    "list",
47                    "--state",
48                    "open",
49                    "--limit",
50                    "20",
51                    "--json",
52                    "number,title,headRefName,baseRefName",
53                    "--template",
54                    "{{range .}}#{{.number}} {{.headRefName}} → {{.baseRefName}} \"{{.title}}\"\n{{end}}",
55                ],
56            )
57            .ok()
58        };
59
60        let open_prs = match (&branch, &all_prs) {
61            (Some(current_branch), Some(prs)) => {
62                let pattern = format!(" {} → ", current_branch);
63                let (mine, others): (Vec<String>, Vec<String>) = prs
64                    .lines()
65                    .map(|l| l.trim())
66                    .filter(|l| !l.is_empty())
67                    .map(|l| l.to_string())
68                    .partition(|l| l.contains(&pattern));
69                let mut result = String::new();
70                if !mine.is_empty() {
71                    result.push_str(&format!("PRs for current branch ({}):\n", current_branch));
72                    for pr in &mine {
73                        result.push_str(&format!("  {}\n", pr));
74                    }
75                }
76                if !others.is_empty() {
77                    result.push_str("PRs for other branches (DO NOT merge these):\n");
78                    for pr in &others {
79                        result.push_str(&format!("  {}\n", pr));
80                    }
81                }
82                if result.is_empty() { None } else { Some(result) }
83            }
84            _ => all_prs,
85        };
86
87        Self {
88            is_repo: true,
89            branch,
90            status: run_git(&["status", "--porcelain"]).ok(),
91            recent_log: run_git(&["log", "--oneline", "-10"]).ok(),
92            remotes: run_git(&["remote", "-v"]).ok(),
93            branches: run_git(&["branch", "-a", "--no-color"]).ok(),
94            open_prs,
95            gh_warning,
96        }
97    }
98
99    pub fn summary(&self) -> String {
100        if !self.is_repo {
101            return "Not inside a git repository.".to_string();
102        }
103
104        let mut parts = Vec::new();
105
106        if let Some(ref branch) = self.branch {
107            parts.push(format!("Current branch: {branch}"));
108        }
109
110        if let Some(ref status) = self.status {
111            if status.trim().is_empty() {
112                parts.push("Working tree: clean".to_string());
113            } else {
114                parts.push(format!("Working tree status:\n{status}"));
115            }
116        }
117
118        if let Some(ref log) = self.recent_log {
119            if !log.trim().is_empty() {
120                parts.push(format!("Recent commits:\n{log}"));
121            }
122        }
123
124        if let Some(ref remotes) = self.remotes {
125            if !remotes.trim().is_empty() {
126                parts.push(format!("Remotes:\n{remotes}"));
127            }
128        }
129
130        if let Some(ref branches) = self.branches {
131            if !branches.trim().is_empty() {
132                parts.push(format!("All branches:\n{branches}"));
133            }
134        }
135
136        if let Some(ref prs) = self.open_prs {
137            if !prs.trim().is_empty() {
138                parts.push(format!("Open PRs:\n{prs}"));
139            }
140        }
141
142        parts.join("\n\n")
143    }
144}
145
146fn run_git(args: &[&str]) -> Result<String, String> {
147    let output = Command::new("git")
148        .args(args)
149        .output()
150        .map_err(|e| format!("Failed to run git: {e}"))?;
151
152    if output.status.success() {
153        Ok(String::from_utf8_lossy(&output.stdout).to_string())
154    } else {
155        Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
156    }
157}
158
159fn run_cmd(cmd: &str, args: &[&str]) -> Result<String, String> {
160    let output = Command::new(cmd)
161        .args(args)
162        .output()
163        .map_err(|e| format!("Failed to run {cmd}: {e}"))?;
164
165    if output.status.success() {
166        Ok(String::from_utf8_lossy(&output.stdout).to_string())
167    } else {
168        Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
169    }
170}