Skip to main content

aster/git/
core.rs

1//! Git 核心工具
2//!
3//! 提供 Git 状态检测、分支信息等基础功能
4
5use serde::{Deserialize, Serialize};
6use std::path::Path;
7use std::process::Command;
8
9/// Git 状态
10#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11pub struct GitStatus {
12    /// 已追踪的修改文件
13    pub tracked: Vec<String>,
14    /// 未追踪的文件
15    pub untracked: Vec<String>,
16    /// 工作区是否干净
17    pub is_clean: bool,
18}
19
20/// Git 完整信息
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct GitInfo {
23    /// 当前提交哈希
24    pub commit_hash: String,
25    /// 当前分支名
26    pub branch_name: String,
27    /// 远程 URL
28    pub remote_url: Option<String>,
29    /// 工作区是否干净
30    pub is_clean: bool,
31    /// 已追踪的修改文件
32    pub tracked_files: Vec<String>,
33    /// 未追踪的文件
34    pub untracked_files: Vec<String>,
35    /// 默认分支
36    pub default_branch: String,
37    /// 最近的提交记录
38    pub recent_commits: Vec<String>,
39}
40
41/// 推送状态
42#[derive(Debug, Clone, Default, Serialize, Deserialize)]
43pub struct PushStatus {
44    /// 是否有上游分支
45    pub has_upstream: bool,
46    /// 是否需要推送
47    pub needs_push: bool,
48    /// 领先上游的提交数
49    pub commits_ahead: u32,
50    /// 相对默认分支的提交数
51    pub commits_ahead_of_default: u32,
52}
53
54/// Git 工具类
55pub struct GitUtils;
56
57impl GitUtils {
58    /// 执行 Git 命令
59    fn exec_git(args: &[&str], cwd: &Path) -> Result<String, String> {
60        let output = Command::new("git")
61            .args(args)
62            .current_dir(cwd)
63            .output()
64            .map_err(|e| format!("执行 git 命令失败: {}", e))?;
65
66        if output.status.success() {
67            Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
68        } else {
69            Err(format!("git {} 失败", args.join(" ")))
70        }
71    }
72
73    /// 执行 Git 命令并返回是否成功
74    fn exec_git_ok(args: &[&str], cwd: &Path) -> bool {
75        Command::new("git")
76            .args(args)
77            .current_dir(cwd)
78            .output()
79            .map(|o| o.status.success())
80            .unwrap_or(false)
81    }
82}
83
84/// 检查是否在 Git 仓库中
85pub fn is_git_repository(cwd: &Path) -> bool {
86    GitUtils::exec_git_ok(&["rev-parse", "--is-inside-work-tree"], cwd)
87}
88
89/// 获取当前分支名
90pub fn get_current_branch(cwd: &Path) -> Result<String, String> {
91    GitUtils::exec_git(&["rev-parse", "--abbrev-ref", "HEAD"], cwd)
92}
93
94/// 获取默认分支名
95pub fn get_default_branch(cwd: &Path) -> String {
96    // 方法1: 从 origin/HEAD 获取
97    if let Ok(head) = GitUtils::exec_git(&["symbolic-ref", "refs/remotes/origin/HEAD"], cwd) {
98        if let Some(branch) = head.strip_prefix("refs/remotes/origin/") {
99            return branch.to_string();
100        }
101    }
102
103    // 方法2: 从远程分支列表查找
104    if let Ok(branches) = GitUtils::exec_git(&["branch", "-r"], cwd) {
105        for name in ["main", "master"] {
106            if branches.contains(&format!("origin/{}", name)) {
107                return name.to_string();
108            }
109        }
110    }
111
112    "main".to_string()
113}
114
115/// 获取远程 URL
116pub fn get_remote_url(cwd: &Path, remote: &str) -> Option<String> {
117    GitUtils::exec_git(&["remote", "get-url", remote], cwd).ok()
118}
119
120/// 获取当前提交哈希
121pub fn get_current_commit(cwd: &Path) -> Result<String, String> {
122    GitUtils::exec_git(&["rev-parse", "HEAD"], cwd)
123}
124
125/// 获取 Git 状态
126pub fn get_git_status(cwd: &Path) -> Result<GitStatus, String> {
127    let output = GitUtils::exec_git(&["status", "--porcelain"], cwd)?;
128
129    let mut tracked = Vec::new();
130    let mut untracked = Vec::new();
131
132    for line in output.lines() {
133        if line.is_empty() {
134            continue;
135        }
136
137        let status = line.get(..2).unwrap_or("");
138        let file = line.get(3..).unwrap_or("").trim().to_string();
139
140        if status == "??" {
141            untracked.push(file);
142        } else if !file.is_empty() {
143            tracked.push(file);
144        }
145    }
146
147    let is_clean = tracked.is_empty() && untracked.is_empty();
148
149    Ok(GitStatus {
150        tracked,
151        untracked,
152        is_clean,
153    })
154}
155
156/// 检查是否有上游分支
157#[allow(dead_code)]
158pub fn has_upstream(cwd: &Path) -> bool {
159    GitUtils::exec_git_ok(&["rev-parse", "@{u}"], cwd)
160}
161
162/// 获取领先上游的提交数
163#[allow(dead_code)]
164pub fn get_commits_ahead(cwd: &Path) -> u32 {
165    GitUtils::exec_git(&["rev-list", "--count", "@{u}..HEAD"], cwd)
166        .ok()
167        .and_then(|s| s.parse().ok())
168        .unwrap_or(0)
169}
170
171/// 获取最近的提交记录
172pub fn get_recent_commits(cwd: &Path, count: u32) -> Vec<String> {
173    GitUtils::exec_git(&["log", "--oneline", "-n", &count.to_string()], cwd)
174        .ok()
175        .map(|s| s.lines().map(|l| l.to_string()).collect())
176        .unwrap_or_default()
177}
178
179/// 获取完整的 Git 信息
180pub fn get_git_info(cwd: &Path) -> Option<GitInfo> {
181    if !is_git_repository(cwd) {
182        return None;
183    }
184
185    let commit_hash = get_current_commit(cwd).ok()?;
186    let branch_name = get_current_branch(cwd).ok()?;
187    let remote_url = get_remote_url(cwd, "origin");
188    let status = get_git_status(cwd).ok()?;
189    let default_branch = get_default_branch(cwd);
190    let recent_commits = get_recent_commits(cwd, 5);
191
192    Some(GitInfo {
193        commit_hash,
194        branch_name,
195        remote_url,
196        is_clean: status.is_clean,
197        tracked_files: status.tracked,
198        untracked_files: status.untracked,
199        default_branch,
200        recent_commits,
201    })
202}
203
204/// 获取推送状态
205#[allow(dead_code)]
206pub fn get_push_status(cwd: &Path) -> PushStatus {
207    let has_up = has_upstream(cwd);
208    let commits_ahead = if has_up { get_commits_ahead(cwd) } else { 0 };
209
210    // 获取相对默认分支的提交数
211    let default_branch = get_default_branch(cwd);
212    let commits_ahead_of_default = GitUtils::exec_git(
213        &[
214            "rev-list",
215            "--count",
216            &format!("origin/{}..HEAD", default_branch),
217        ],
218        cwd,
219    )
220    .ok()
221    .and_then(|s| s.parse().ok())
222    .unwrap_or(0);
223
224    PushStatus {
225        has_upstream: has_up,
226        needs_push: !has_up || commits_ahead > 0,
227        commits_ahead,
228        commits_ahead_of_default,
229    }
230}