11//! Shared types for Snowflake
22
3+ use std:: fmt;
4+
35use 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" ) ]
189191pub 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" ) ]
215236pub 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