@@ -25,6 +25,7 @@ use serde::{
2525 Deserialize ,
2626 Serialize ,
2727} ;
28+ use tracing:: debug;
2829
2930use crate :: cli:: ConversationState ;
3031use crate :: cli:: chat:: conversation:: HistoryEntry ;
@@ -36,6 +37,9 @@ pub struct CheckpointManager {
3637 /// Path to the shadow (bare) git repository
3738 pub shadow_repo_path : PathBuf ,
3839
40+ /// Path to current working directory
41+ pub work_tree_path : PathBuf ,
42+
3943 /// All checkpoints in chronological order
4044 pub checkpoints : Vec < Checkpoint > ,
4145
@@ -84,10 +88,10 @@ impl CheckpointManager {
8488 current_history : & VecDeque < HistoryEntry > ,
8589 ) -> Result < Self > {
8690 if !is_git_installed ( ) {
87- bail ! ( "Git is not installed. Checkpoints require git to function ." ) ;
91+ bail ! ( "Checkpoints are not available. Git is required but not installed ." ) ;
8892 }
8993 if !is_in_git_repo ( ) {
90- bail ! ( "Not in a git repository . Use '/checkpoint init' to manually enable checkpoints." ) ;
94+ bail ! ( "Checkpoints are not available in this directory . Use '/checkpoint init' to enable checkpoints." ) ;
9195 }
9296
9397 let manager = Self :: manual_init ( os, shadow_path, current_history) . await ?;
@@ -103,14 +107,17 @@ impl CheckpointManager {
103107 let path = path. as_ref ( ) ;
104108 os. fs . create_dir_all ( path) . await ?;
105109
110+ let work_tree_path =
111+ std:: env:: current_dir ( ) . map_err ( |e| eyre ! ( "Failed to get current working directory: {}" , e) ) ?;
112+
106113 // Initialize bare repository
107- run_git ( path, false , & [ "init" , "--bare" , & path. to_string_lossy ( ) ] ) ?;
114+ run_git ( path, None , & [ "init" , "--bare" , & path. to_string_lossy ( ) ] ) ?;
108115
109116 // Configure git
110117 configure_git ( & path. to_string_lossy ( ) ) ?;
111118
112119 // Create initial checkpoint
113- stage_commit_tag ( & path. to_string_lossy ( ) , "Initial state" , "0" ) ?;
120+ stage_commit_tag ( & path. to_string_lossy ( ) , & work_tree_path , "Initial state" , "0" ) ?;
114121
115122 let initial_checkpoint = Checkpoint {
116123 tag : "0" . to_string ( ) ,
@@ -126,6 +133,7 @@ impl CheckpointManager {
126133
127134 Ok ( Self {
128135 shadow_repo_path : path. to_path_buf ( ) ,
136+ work_tree_path,
129137 checkpoints : vec ! [ initial_checkpoint] ,
130138 tag_index,
131139 current_turn : 0 ,
@@ -146,7 +154,12 @@ impl CheckpointManager {
146154 tool_name : Option < String > ,
147155 ) -> Result < ( ) > {
148156 // Stage, commit and tag
149- stage_commit_tag ( & self . shadow_repo_path . to_string_lossy ( ) , description, tag) ?;
157+ stage_commit_tag (
158+ & self . shadow_repo_path . to_string_lossy ( ) ,
159+ & self . work_tree_path ,
160+ description,
161+ tag,
162+ ) ?;
150163
151164 // Record checkpoint metadata
152165 let checkpoint = Checkpoint {
@@ -175,9 +188,14 @@ impl CheckpointManager {
175188
176189 if hard {
177190 // Hard: reset the whole work-tree to the tag
178- let output = run_git ( & self . shadow_repo_path , true , & [ "reset" , "--hard" , tag] ) ?;
191+ let output = run_git ( & self . shadow_repo_path , Some ( & self . work_tree_path ) , & [
192+ "reset" , "--hard" , tag,
193+ ] ) ?;
179194 if !output. status . success ( ) {
180- bail ! ( "Failed to restore: {}" , String :: from_utf8_lossy( & output. stderr) ) ;
195+ bail ! (
196+ "Failed to restore checkpoint: {}" ,
197+ String :: from_utf8_lossy( & output. stderr)
198+ ) ;
181199 }
182200 } else {
183201 // Soft: only restore tracked files. If the tag is an empty tree, this is a no-op.
@@ -187,9 +205,14 @@ impl CheckpointManager {
187205 return Ok ( ( ) ) ;
188206 }
189207 // Use checkout against work-tree
190- let output = run_git ( & self . shadow_repo_path , true , & [ "checkout" , tag, "--" , "." ] ) ?;
208+ let output = run_git ( & self . shadow_repo_path , Some ( & self . work_tree_path ) , & [
209+ "checkout" , tag, "--" , "." ,
210+ ] ) ?;
191211 if !output. status . success ( ) {
192- bail ! ( "Failed to restore: {}" , String :: from_utf8_lossy( & output. stderr) ) ;
212+ bail ! (
213+ "Failed to restore checkpoint: {}" ,
214+ String :: from_utf8_lossy( & output. stderr)
215+ ) ;
193216 }
194217 }
195218
@@ -205,7 +228,7 @@ impl CheckpointManager {
205228 let out = run_git (
206229 & self . shadow_repo_path ,
207230 // work_tree
208- false ,
231+ None ,
209232 & [ "ls-tree" , "-r" , "--name-only" , tag] ,
210233 ) ?;
211234 Ok ( !out. stdout . is_empty ( ) )
@@ -223,7 +246,7 @@ impl CheckpointManager {
223246
224247 /// Compute file statistics between two checkpoints
225248 pub fn compute_stats_between ( & self , from : & str , to : & str ) -> Result < FileStats > {
226- let output = run_git ( & self . shadow_repo_path , false , & [ "diff" , "--name-status" , from, to] ) ?;
249+ let output = run_git ( & self . shadow_repo_path , None , & [ "diff" , "--name-status" , from, to] ) ?;
227250
228251 let mut stats = FileStats :: default ( ) ;
229252 for line in String :: from_utf8_lossy ( & output. stdout ) . lines ( ) {
@@ -246,7 +269,7 @@ impl CheckpointManager {
246269 let mut result = String :: new ( ) ;
247270
248271 // Get file changes
249- let output = run_git ( & self . shadow_repo_path , false , & [ "diff" , "--name-status" , from, to] ) ?;
272+ let output = run_git ( & self . shadow_repo_path , None , & [ "diff" , "--name-status" , from, to] ) ?;
250273
251274 for line in String :: from_utf8_lossy ( & output. stdout ) . lines ( ) {
252275 if let Some ( ( status, file) ) = line. split_once ( '\t' ) {
@@ -261,7 +284,7 @@ impl CheckpointManager {
261284 }
262285
263286 // Add statistics
264- let stat_output = run_git ( & self . shadow_repo_path , false , & [
287+ let stat_output = run_git ( & self . shadow_repo_path , None , & [
265288 "diff" ,
266289 from,
267290 to,
@@ -279,7 +302,10 @@ impl CheckpointManager {
279302
280303 /// Check for uncommitted changes
281304 pub fn has_changes ( & self ) -> Result < bool > {
282- let output = run_git ( & self . shadow_repo_path , true , & [ "status" , "--porcelain" ] ) ?;
305+ let output = run_git ( & self . shadow_repo_path , Some ( & self . work_tree_path ) , & [
306+ "status" ,
307+ "--porcelain" ,
308+ ] ) ?;
283309 Ok ( !output. stdout . is_empty ( ) )
284310 }
285311
@@ -351,18 +377,18 @@ fn is_in_git_repo() -> bool {
351377}
352378
353379fn configure_git ( shadow_path : & str ) -> Result < ( ) > {
354- run_git ( Path :: new ( shadow_path) , false , & [ "config" , "user.name" , "Q" ] ) ?;
355- run_git ( Path :: new ( shadow_path) , false , & [ "config" , "user.email" , "qcli@local" ] ) ?;
356- run_git ( Path :: new ( shadow_path) , false , & [ "config" , "core.preloadindex" , "true" ] ) ?;
380+ run_git ( Path :: new ( shadow_path) , None , & [ "config" , "user.name" , "Q" ] ) ?;
381+ run_git ( Path :: new ( shadow_path) , None , & [ "config" , "user.email" , "qcli@local" ] ) ?;
382+ run_git ( Path :: new ( shadow_path) , None , & [ "config" , "core.preloadindex" , "true" ] ) ?;
357383 Ok ( ( ) )
358384}
359385
360- fn stage_commit_tag ( shadow_path : & str , message : & str , tag : & str ) -> Result < ( ) > {
386+ fn stage_commit_tag ( shadow_path : & str , work_tree : & Path , message : & str , tag : & str ) -> Result < ( ) > {
361387 // Stage all changes
362- run_git ( Path :: new ( shadow_path) , true , & [ "add" , "-A" ] ) ?;
388+ run_git ( Path :: new ( shadow_path) , Some ( work_tree ) , & [ "add" , "-A" ] ) ?;
363389
364390 // Commit
365- let output = run_git ( Path :: new ( shadow_path) , true , & [
391+ let output = run_git ( Path :: new ( shadow_path) , Some ( work_tree ) , & [
366392 "commit" ,
367393 "--allow-empty" ,
368394 "--no-verify" ,
@@ -371,33 +397,53 @@ fn stage_commit_tag(shadow_path: &str, message: &str, tag: &str) -> Result<()> {
371397 ] ) ?;
372398
373399 if !output. status . success ( ) {
374- bail ! ( "Git commit failed: {}" , String :: from_utf8_lossy( & output. stderr) ) ;
400+ bail ! (
401+ "Checkpoint initialization failed: {}" ,
402+ String :: from_utf8_lossy( & output. stderr)
403+ ) ;
375404 }
376405
377406 // Tag
378- let output = run_git ( Path :: new ( shadow_path) , false , & [ "tag" , tag] ) ?;
407+ let output = run_git ( Path :: new ( shadow_path) , None , & [ "tag" , tag] ) ?;
379408 if !output. status . success ( ) {
380- bail ! ( "Git tag failed: {}" , String :: from_utf8_lossy( & output. stderr) ) ;
409+ bail ! (
410+ "Checkpoint initialization failed: {}" ,
411+ String :: from_utf8_lossy( & output. stderr)
412+ ) ;
381413 }
382414
383415 Ok ( ( ) )
384416}
385417
386- fn run_git ( dir : & Path , with_work_tree : bool , args : & [ & str ] ) -> Result < Output > {
418+ fn run_git ( dir : & Path , work_tree : Option < & Path > , args : & [ & str ] ) -> Result < Output > {
387419 let mut cmd = Command :: new ( "git" ) ;
388420 cmd. arg ( format ! ( "--git-dir={}" , dir. display( ) ) ) ;
389421
390- if with_work_tree {
391- cmd. arg ( "--work-tree=." ) ;
422+ if let Some ( work_tree_path ) = work_tree {
423+ cmd. arg ( format ! ( "--work-tree={}" , work_tree_path . display ( ) ) ) ;
392424 }
393425
394426 cmd. args ( args) ;
395427
428+ debug ! ( "Executing git command: {:?}" , cmd) ;
396429 let output = cmd. output ( ) ?;
397- if !output. status . success ( ) && !output. stderr . is_empty ( ) {
398- bail ! ( String :: from_utf8_lossy( & output. stderr) . to_string( ) ) ;
430+
431+ if !output. status . success ( ) {
432+ debug ! ( "Git command failed with exit code: {:?}" , output. status. code( ) ) ;
433+ debug ! ( "Git stderr: {}" , String :: from_utf8_lossy( & output. stderr) ) ;
434+ debug ! ( "Git stdout: {}" , String :: from_utf8_lossy( & output. stdout) ) ;
435+
436+ if !output. stderr . is_empty ( ) {
437+ bail ! (
438+ "Checkpoint operation failed: {}" ,
439+ String :: from_utf8_lossy( & output. stderr)
440+ ) ;
441+ } else {
442+ bail ! ( "Checkpoint operation failed unexpectedly" ) ;
443+ }
399444 }
400445
446+ debug ! ( "Git command succeeded" ) ;
401447 Ok ( output)
402448}
403449
0 commit comments