@@ -2,6 +2,8 @@ use git2::Repository;
22use serde:: { Deserialize , Serialize } ;
33use std:: path:: Path ;
44
5+ const WORKDIR_SENTINEL : & str = "__WORKDIR__" ;
6+
57#[ derive( Debug , Serialize , Deserialize ) ]
68#[ serde( rename_all = "camelCase" ) ]
79pub struct LoadRepoResult {
@@ -56,7 +58,7 @@ pub fn open_repo(path: &str) -> Result<LoadRepoResult, String> {
5658 let repo = Repository :: open ( path) . map_err ( |e| format ! ( "Failed to open repository: {}" , e) ) ?;
5759
5860 // Get local branches only (avoid confusion with remote branches like origin/...)
59- let branches = repo
61+ let branches: Vec < String > = repo
6062 . branches ( Some ( git2:: BranchType :: Local ) )
6163 . map_err ( |e| format ! ( "Failed to list branches: {}" , e) ) ?
6264 . filter_map ( |branch_result| {
@@ -72,8 +74,12 @@ pub fn open_repo(path: &str) -> Result<LoadRepoResult, String> {
7274 . ok ( )
7375 . and_then ( |head| head. shorthand ( ) . map ( |s| s. to_string ( ) ) ) ;
7476
77+ // Prepend WORKDIR to branch list
78+ let mut all_branches = vec ! [ WORKDIR_SENTINEL . to_string( ) ] ;
79+ all_branches. extend ( branches) ;
80+
7581 Ok ( LoadRepoResult {
76- branches,
82+ branches : all_branches ,
7783 default_branch,
7884 } )
7985}
@@ -87,38 +93,73 @@ pub fn get_branches(path: &str) -> Result<LoadRepoResult, String> {
8793pub fn git_diff ( path : & str , base : & str , compare : & str ) -> Result < DiffResult , String > {
8894 let repo = Repository :: open ( path) . map_err ( |e| format ! ( "Failed to open repository: {}" , e) ) ?;
8995
90- // Resolve base and compare to commits
91- let base_commit = repo
92- . revparse_single ( base)
93- . map_err ( |e| format ! ( "Failed to resolve base ref '{}': {}" , base, e) ) ?
94- . peel_to_commit ( )
95- . map_err ( |e| format ! ( "Failed to peel base to commit: {}" , e) ) ?;
96-
97- let compare_commit = repo
98- . revparse_single ( compare)
99- . map_err ( |e| format ! ( "Failed to resolve compare ref '{}': {}" , compare, e) ) ?
100- . peel_to_commit ( )
101- . map_err ( |e| format ! ( "Failed to peel compare to commit: {}" , e) ) ?;
102-
103- // Get trees from commits
104- let base_tree = base_commit
105- . tree ( )
106- . map_err ( |e| format ! ( "Failed to get base tree: {}" , e) ) ?;
107-
108- let compare_tree = compare_commit
109- . tree ( )
110- . map_err ( |e| format ! ( "Failed to get compare tree: {}" , e) ) ?;
111-
112- // Compute diff
113- let mut diff = repo
114- . diff_tree_to_tree ( Some ( & base_tree) , Some ( & compare_tree) , None )
115- . map_err ( |e| format ! ( "Failed to compute diff: {}" , e) ) ?;
96+ // Handle WORKDIR sentinel
97+ let mut diff = if compare == WORKDIR_SENTINEL {
98+ // Compare base tree to working directory
99+ let base_commit = repo
100+ . revparse_single ( base)
101+ . map_err ( |e| format ! ( "Failed to resolve base ref '{}': {}" , base, e) ) ?
102+ . peel_to_commit ( )
103+ . map_err ( |e| format ! ( "Failed to peel base to commit: {}" , e) ) ?;
104+
105+ let base_tree = base_commit
106+ . tree ( )
107+ . map_err ( |e| format ! ( "Failed to get base tree: {}" , e) ) ?;
108+
109+ let mut opts = git2:: DiffOptions :: new ( ) ;
110+ opts. include_untracked ( false ) ; // Only show tracked files
111+
112+ repo. diff_tree_to_workdir ( Some ( & base_tree) , Some ( & mut opts) )
113+ . map_err ( |e| format ! ( "Failed to compute diff to workdir: {}" , e) ) ?
114+ } else if base == WORKDIR_SENTINEL {
115+ // Compare working directory to compare tree (reverse diff)
116+ let compare_commit = repo
117+ . revparse_single ( compare)
118+ . map_err ( |e| format ! ( "Failed to resolve compare ref '{}': {}" , compare, e) ) ?
119+ . peel_to_commit ( )
120+ . map_err ( |e| format ! ( "Failed to peel compare to commit: {}" , e) ) ?;
121+
122+ let compare_tree = compare_commit
123+ . tree ( )
124+ . map_err ( |e| format ! ( "Failed to get compare tree: {}" , e) ) ?;
125+
126+ let mut opts = git2:: DiffOptions :: new ( ) ;
127+ opts. include_untracked ( false ) ; // Only show tracked files
128+
129+ repo. diff_tree_to_workdir ( Some ( & compare_tree) , Some ( & mut opts) )
130+ . map_err ( |e| format ! ( "Failed to compute diff to workdir: {}" , e) ) ?
131+ } else {
132+ // Normal tree-to-tree diff
133+ let base_commit = repo
134+ . revparse_single ( base)
135+ . map_err ( |e| format ! ( "Failed to resolve base ref '{}': {}" , base, e) ) ?
136+ . peel_to_commit ( )
137+ . map_err ( |e| format ! ( "Failed to peel base to commit: {}" , e) ) ?;
138+
139+ let compare_commit = repo
140+ . revparse_single ( compare)
141+ . map_err ( |e| format ! ( "Failed to resolve compare ref '{}': {}" , compare, e) ) ?
142+ . peel_to_commit ( )
143+ . map_err ( |e| format ! ( "Failed to peel compare to commit: {}" , e) ) ?;
144+
145+ let base_tree = base_commit
146+ . tree ( )
147+ . map_err ( |e| format ! ( "Failed to get base tree: {}" , e) ) ?;
148+
149+ let compare_tree = compare_commit
150+ . tree ( )
151+ . map_err ( |e| format ! ( "Failed to get compare tree: {}" , e) ) ?;
152+
153+ repo. diff_tree_to_tree ( Some ( & base_tree) , Some ( & compare_tree) , None )
154+ . map_err ( |e| format ! ( "Failed to compute diff: {}" , e) ) ?
155+ } ;
116156
117157 // Enable rename and copy detection
118158 diff. find_similar ( None )
119159 . map_err ( |e| format ! ( "Failed to find similar files: {}" , e) ) ?;
120160
121161 let mut files = Vec :: new ( ) ;
162+ let invert_changes = base == WORKDIR_SENTINEL ; // Invert change types when base is WORKDIR
122163
123164 diff. foreach (
124165 & mut |delta, _progress| {
@@ -132,10 +173,18 @@ pub fn git_diff(path: &str, base: &str, compare: &str) -> Result<DiffResult, Str
132173
133174 let ( path, change_type, stored_old_path) = match delta. status ( ) {
134175 git2:: Delta :: Added => {
135- ( new_path. unwrap_or_default ( ) , "add" , None )
176+ if invert_changes {
177+ ( new_path. unwrap_or_default ( ) , "remove" , None )
178+ } else {
179+ ( new_path. unwrap_or_default ( ) , "add" , None )
180+ }
136181 } ,
137182 git2:: Delta :: Deleted => {
138- ( old_path. unwrap_or_default ( ) , "remove" , None )
183+ if invert_changes {
184+ ( old_path. unwrap_or_default ( ) , "add" , None )
185+ } else {
186+ ( old_path. unwrap_or_default ( ) , "remove" , None )
187+ }
139188 } ,
140189 git2:: Delta :: Modified => {
141190 ( new_path. unwrap_or_default ( ) , "modify" , None )
@@ -170,8 +219,47 @@ pub fn git_diff(path: &str, base: &str, compare: &str) -> Result<DiffResult, Str
170219 Ok ( DiffResult { files } )
171220}
172221
222+ /// List all files in working directory (respecting .gitignore)
223+ fn list_workdir_files ( path : & str ) -> Result < ListFilesResult , String > {
224+ use ignore:: WalkBuilder ;
225+
226+ let mut files = Vec :: new ( ) ;
227+ let walker = WalkBuilder :: new ( path)
228+ . hidden ( false ) // Show hidden files
229+ . git_ignore ( true ) // Respect .gitignore
230+ . git_exclude ( true ) // Respect .git/info/exclude
231+ . build ( ) ;
232+
233+ for entry in walker {
234+ match entry {
235+ Ok ( entry) => {
236+ if entry. file_type ( ) . map ( |ft| ft. is_file ( ) ) . unwrap_or ( false ) {
237+ // Get path relative to repo root
238+ if let Ok ( rel_path) = entry. path ( ) . strip_prefix ( path) {
239+ let path_str = rel_path. to_string_lossy ( ) . to_string ( ) ;
240+ // Exclude .git directory files
241+ if !path_str. starts_with ( ".git/" ) && !path_str. starts_with ( ".git\\ " ) {
242+ // Normalize path separators to forward slashes
243+ let normalized = path_str. replace ( '\\' , "/" ) ;
244+ files. push ( normalized) ;
245+ }
246+ }
247+ }
248+ }
249+ Err ( _) => continue , // Skip errors (permission issues, etc.)
250+ }
251+ }
252+
253+ Ok ( ListFilesResult { files } )
254+ }
255+
173256/// List all files in a tree at a specific ref
174257pub fn list_files ( path : & str , ref_name : & str ) -> Result < ListFilesResult , String > {
258+ // Handle WORKDIR sentinel
259+ if ref_name == WORKDIR_SENTINEL {
260+ return list_workdir_files ( path) ;
261+ }
262+
175263 let repo = Repository :: open ( path) . map_err ( |e| format ! ( "Failed to open repository: {}" , e) ) ?;
176264
177265 // Resolve ref to commit
@@ -263,7 +351,47 @@ pub fn resolve_ref(path: &str, ref_name: &str) -> Result<ResolveRefResult, Strin
263351
264352/// Read file content at a specific ref
265353pub fn read_file_blob ( path : & str , ref_name : & str , file_path : & str ) -> Result < ReadFileResult , String > {
266- let repo = Repository :: open ( path) . map_err ( |e| format ! ( "Failed to open repository: {}" , e) ) ?;
354+ // Handle WORKDIR sentinel - read from filesystem
355+ if ref_name == WORKDIR_SENTINEL {
356+ use std:: fs;
357+ use std:: path:: PathBuf ;
358+
359+ let full_path = PathBuf :: from ( path) . join ( file_path) ;
360+
361+ match fs:: read ( & full_path) {
362+ Ok ( content) => {
363+ // Check if binary (scan first 8KB for null bytes)
364+ let check_len = content. len ( ) . min ( 8192 ) ;
365+ let is_binary = content[ ..check_len] . iter ( ) . any ( |& b| b == 0 ) ;
366+
367+ if is_binary {
368+ return Ok ( ReadFileResult {
369+ binary : true ,
370+ text : None ,
371+ not_found : None ,
372+ } ) ;
373+ }
374+
375+ // Convert to UTF-8 string (use lossy conversion for non-UTF8 files)
376+ let text = String :: from_utf8_lossy ( & content) . into_owned ( ) ;
377+
378+ Ok ( ReadFileResult {
379+ binary : false ,
380+ text : Some ( text) ,
381+ not_found : None ,
382+ } )
383+ }
384+ Err ( _) => {
385+ Ok ( ReadFileResult {
386+ binary : false ,
387+ text : None ,
388+ not_found : Some ( true ) ,
389+ } )
390+ }
391+ }
392+ } else {
393+ // Normal Git blob reading
394+ let repo = Repository :: open ( path) . map_err ( |e| format ! ( "Failed to open repository: {}" , e) ) ?;
267395
268396 // Resolve ref to commit
269397 let commit = repo
@@ -323,11 +451,12 @@ pub fn read_file_blob(path: &str, ref_name: &str, file_path: &str) -> Result<Rea
323451 // Convert to UTF-8 string (use lossy conversion for non-UTF8 encodings like Latin-1)
324452 let text = String :: from_utf8_lossy ( content) . into_owned ( ) ;
325453
326- Ok ( ReadFileResult {
327- binary : false ,
328- text : Some ( text) ,
329- not_found : None ,
330- } )
454+ Ok ( ReadFileResult {
455+ binary : false ,
456+ text : Some ( text) ,
457+ not_found : None ,
458+ } )
459+ }
331460}
332461
333462#[ cfg( test) ]
0 commit comments