返回 CodeWhale
workspace.rs
根目录 / crates / tui / src / runtime_api / workspace.rs
1 use std::path::{Path as FsPath, PathBuf};
2
3 use axum::Json;
4 use axum::extract::State;
5 use serde::Serialize;
6
7 use crate::dependencies::{ExternalTool as _, Git};
8
9 use super::{ApiError, RuntimeApiState};
10
11 #[derive(Debug, Serialize)]
12 pub(super) struct WorkspaceStatusResponse {
13 pub(super) workspace: PathBuf,
14 pub(super) git_repo: bool,
15 pub(super) branch: Option<String>,
16 pub(super) head: Option<String>,
17 pub(super) dirty: bool,
18 pub(super) staged: usize,
19 pub(super) unstaged: usize,
20 pub(super) untracked: usize,
21 pub(super) ahead: Option<u32>,
22 pub(super) behind: Option<u32>,
23 }
24
25 #[derive(Debug, Default)]
26 pub(super) struct WorkspaceGitMetadata {
27 pub(super) branch: Option<String>,
28 pub(super) head: Option<String>,
29 pub(super) dirty: bool,
30 }
31
32 pub(super) async fn workspace_status(
33 State(state): State<RuntimeApiState>,
34 ) -> Result<Json<WorkspaceStatusResponse>, ApiError> {
35 Ok(Json(collect_workspace_status(&state.workspace)))
36 }
37
38 pub(super) fn collect_workspace_status(workspace: &FsPath) -> WorkspaceStatusResponse {
39 let mut status = WorkspaceStatusResponse {
40 workspace: workspace.to_path_buf(),
41 git_repo: false,
42 branch: None,
43 head: None,
44 dirty: false,
45 staged: 0,
46 unstaged: 0,
47 untracked: 0,
48 ahead: None,
49 behind: None,
50 };
51
52 let Some(repo_check) = run_git(workspace, &["rev-parse", "--is-inside-work-tree"]) else {
53 return status;
54 };
55 if repo_check.trim() != "true" {
56 return status;
57 }
58
59 status.git_repo = true;
60 let metadata = collect_workspace_git_metadata(workspace);
61 status.branch = metadata.branch;
62 status.head = metadata.head;
63 status.dirty = metadata.dirty;
64
65 if let Some(porcelain) = run_git(workspace, &["status", "--porcelain=v1"]) {
66 for line in porcelain.lines() {
67 if line.starts_with("??") {
68 status.untracked += 1;
69 continue;
70 }
71 let chars: Vec<char> = line.chars().collect();
72 if chars.len() >= 2 {
73 if chars[0] != ' ' {
74 status.staged += 1;
75 }
76 if chars[1] != ' ' {
77 status.unstaged += 1;
78 }
79 }
80 }
81 }
82
83 if let Some(counts) = run_git(
84 workspace,
85 &["rev-list", "--left-right", "--count", "@{upstream}...HEAD"],
86 ) {
87 let mut parts = counts.split_whitespace();
88 if let (Some(behind), Some(ahead)) = (parts.next(), parts.next()) {
89 status.behind = behind.parse::<u32>().ok();
90 status.ahead = ahead.parse::<u32>().ok();
91 }
92 }
93
94 status
95 }
96
97 pub(super) fn collect_workspace_git_metadata(workspace: &FsPath) -> WorkspaceGitMetadata {
98 let Some(repo_check) = run_git(workspace, &["rev-parse", "--is-inside-work-tree"]) else {
99 return WorkspaceGitMetadata::default();
100 };
101 if repo_check.trim() != "true" {
102 return WorkspaceGitMetadata::default();
103 }
104
105 WorkspaceGitMetadata {
106 branch: current_git_branch(workspace),
107 head: current_git_head(workspace),
108 dirty: run_git(workspace, &["status", "--porcelain=v1"])
109 .is_some_and(|porcelain| !porcelain.trim().is_empty()),
110 }
111 }
112
113 fn run_git(workspace: &FsPath, args: &[&str]) -> Option<String> {
114 let output = Git::output(args, workspace).ok()?;
115 if !output.status.success() {
116 return None;
117 }
118 String::from_utf8(output.stdout).ok()
119 }
120
121 fn current_git_branch(workspace: &FsPath) -> Option<String> {
122 let repo_check = run_git(workspace, &["rev-parse", "--is-inside-work-tree"])?;
123 if repo_check.trim() != "true" {
124 return None;
125 }
126 let branch = run_git(workspace, &["rev-parse", "--abbrev-ref", "HEAD"])?;
127 let branch = branch.trim();
128 if branch.is_empty() {
129 return None;
130 }
131 if branch != "HEAD" {
132 return Some(branch.to_string());
133 }
134 let short_hash = run_git(workspace, &["rev-parse", "--short", "HEAD"])?;
135 let short_hash = short_hash.trim();
136 (!short_hash.is_empty()).then(|| format!("detached@{short_hash}"))
137 }
138
139 fn current_git_head(workspace: &FsPath) -> Option<String> {
140 let head = run_git(workspace, &["rev-parse", "--short", "HEAD"])?;
141 let head = head.trim();
142 (!head.is_empty()).then(|| head.to_string())
143 }
144
144 lines RUST