Skip to content

Commit f1e355d

Browse files
fazxesclaude
andcommitted
feat(git): implement StatusEntry type with proper file status tracking
- Add FileStatus enum (Added, Modified, Deleted, Renamed, Copied, etc.) - Add StatusEntry struct with path, status, oldPath, similarity fields - Update GitStatus to use Vec<StatusEntry> instead of Vec<String> - Handle detached HEAD by showing HEAD@<short-hash> - Skip entries with empty/invalid paths - Add rename detection with oldPath tracking Frontend changes: - Show actual status badges (A/M/D/R) instead of checkmarks for staged files - Display "old.ts → new.ts" format for renamed files - Handle empty branch with "No commits yet" placeholder - Add GitStatusBadge component for consistent status display - Add helper methods to useGitStatus hook (isClean, hasConflicts, etc.) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d7789af commit f1e355d

7 files changed

Lines changed: 546 additions & 179 deletions

File tree

‎crates/snowflake-core/src/types.rs‎

Lines changed: 145 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! Shared types for Snowflake
22
3+
use std::fmt;
4+
35
use serde::{Deserialize, Serialize};
46

57
/// File entry returned by directory listing
@@ -184,32 +186,51 @@ pub struct TerminalInfo {
184186
}
185187

186188
/// Git repository status
187-
#[derive(Debug, Clone, Serialize, Deserialize)]
189+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
188190
#[serde(rename_all = "camelCase")]
189191
pub struct GitStatus {
190-
/// Current branch name
192+
/// Current branch name (empty if detached HEAD)
191193
pub branch: String,
192-
/// Staged files
193-
pub staged: Vec<String>,
194-
/// Modified files (unstaged)
195-
pub modified: Vec<String>,
196-
/// Untracked files
197-
pub untracked: Vec<String>,
198-
/// Deleted files
199-
pub deleted: Vec<String>,
200-
/// Renamed files
201-
pub renamed: Vec<RenamedFile>,
202-
/// Conflicted files (merge conflicts)
203-
pub conflicted: Vec<String>,
204-
/// Commits ahead of remote
194+
/// Upstream branch name if tracking
195+
pub upstream: Option<String>,
196+
/// Commits ahead of upstream
205197
pub ahead: u32,
206-
/// Commits behind remote
198+
/// Commits behind upstream
207199
pub behind: u32,
208-
/// Whether working directory is clean
209-
pub is_clean: bool,
200+
/// Files staged for commit (in index)
201+
pub staged: Vec<StatusEntry>,
202+
/// Files modified but not staged (in working tree)
203+
pub modified: Vec<StatusEntry>,
204+
/// Untracked files
205+
pub untracked: Vec<StatusEntry>,
206+
/// Files with merge conflicts
207+
pub conflicted: Vec<StatusEntry>,
208+
}
209+
210+
impl GitStatus {
211+
/// Check if there are any changes.
212+
#[must_use]
213+
pub fn is_clean(&self) -> bool {
214+
self.staged.is_empty()
215+
&& self.modified.is_empty()
216+
&& self.untracked.is_empty()
217+
&& self.conflicted.is_empty()
218+
}
219+
220+
/// Get total count of changed files.
221+
#[must_use]
222+
pub fn total_changes(&self) -> usize {
223+
self.staged.len() + self.modified.len() + self.untracked.len() + self.conflicted.len()
224+
}
225+
226+
/// Check if there are conflicts.
227+
#[must_use]
228+
pub fn has_conflicts(&self) -> bool {
229+
!self.conflicted.is_empty()
230+
}
210231
}
211232

212-
/// Renamed file pair
233+
/// Renamed file pair (legacy - use StatusEntry instead)
213234
#[derive(Debug, Clone, Serialize, Deserialize)]
214235
#[serde(rename_all = "camelCase")]
215236
pub struct RenamedFile {
@@ -219,6 +240,111 @@ pub struct RenamedFile {
219240
pub to: String,
220241
}
221242

243+
/// Status of a file in git.
244+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
245+
#[serde(rename_all = "lowercase")]
246+
#[non_exhaustive]
247+
pub enum FileStatus {
248+
/// File is newly added (staged).
249+
Added,
250+
/// File has been modified.
251+
Modified,
252+
/// File has been deleted.
253+
Deleted,
254+
/// File has been renamed.
255+
Renamed,
256+
/// File has been copied.
257+
Copied,
258+
/// File is untracked (not in git).
259+
Untracked,
260+
/// File has merge conflicts.
261+
Conflicted,
262+
/// File type changed (e.g., file to symlink).
263+
TypeChange,
264+
}
265+
266+
impl fmt::Display for FileStatus {
267+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268+
match self {
269+
Self::Added => write!(f, "A"),
270+
Self::Modified => write!(f, "M"),
271+
Self::Deleted => write!(f, "D"),
272+
Self::Renamed => write!(f, "R"),
273+
Self::Copied => write!(f, "C"),
274+
Self::Untracked => write!(f, "?"),
275+
Self::Conflicted => write!(f, "U"),
276+
Self::TypeChange => write!(f, "T"),
277+
}
278+
}
279+
}
280+
281+
/// A file's status entry in git.
282+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
283+
#[serde(rename_all = "camelCase")]
284+
pub struct StatusEntry {
285+
/// File path relative to repository root.
286+
pub path: String,
287+
/// Status type.
288+
pub status: FileStatus,
289+
/// Original path for renames/copies (None if not renamed/copied).
290+
pub old_path: Option<String>,
291+
/// Similarity percentage for renames/copies (0-100).
292+
pub similarity: Option<u8>,
293+
}
294+
295+
#[expect(
296+
clippy::impl_trait_in_params,
297+
reason = "ergonomic API for string-like types"
298+
)]
299+
impl StatusEntry {
300+
/// Create a new status entry.
301+
#[must_use]
302+
pub fn new(path: impl Into<String>, status: FileStatus) -> Self {
303+
Self {
304+
path: path.into(),
305+
status,
306+
old_path: None,
307+
similarity: None,
308+
}
309+
}
310+
311+
/// Create a renamed/moved entry.
312+
#[must_use]
313+
pub fn renamed(
314+
old_path: impl Into<String>,
315+
new_path: impl Into<String>,
316+
similarity: Option<u8>,
317+
) -> Self {
318+
Self {
319+
path: new_path.into(),
320+
status: FileStatus::Renamed,
321+
old_path: Some(old_path.into()),
322+
similarity,
323+
}
324+
}
325+
326+
/// Create a copied entry.
327+
#[must_use]
328+
pub fn copied(
329+
source_path: impl Into<String>,
330+
new_path: impl Into<String>,
331+
similarity: Option<u8>,
332+
) -> Self {
333+
Self {
334+
path: new_path.into(),
335+
status: FileStatus::Copied,
336+
old_path: Some(source_path.into()),
337+
similarity,
338+
}
339+
}
340+
341+
/// Check if this is a rename or copy.
342+
#[must_use]
343+
pub fn has_old_path(&self) -> bool {
344+
self.old_path.is_some()
345+
}
346+
}
347+
222348
/// Git commit information
223349
#[derive(Debug, Clone, Serialize, Deserialize)]
224350
#[serde(rename_all = "camelCase")]

0 commit comments

Comments
 (0)