@@ -9,6 +9,7 @@ use chrono::{
99 DateTime ,
1010 Local ,
1111} ;
12+ use crossterm:: style:: Stylize ;
1213use eyre:: {
1314 Result ,
1415 bail,
@@ -21,7 +22,6 @@ use serde::{
2122
2223use crate :: cli:: ConversationState ;
2324use crate :: os:: Os ;
24-
2525// The shadow repo path that MUST be appended with a session-specific directory
2626// pub const SHADOW_REPO_DIR: &str = "/Users/aws/.amazonq/cli-captures/";
2727
@@ -43,6 +43,16 @@ pub struct CaptureManager {
4343 /// If true, delete the current session's shadow repo directory when dropped.
4444 #[ serde( default ) ]
4545 pub clean_on_drop : bool ,
46+ /// Track file changes for each capture
47+ #[ serde( default ) ]
48+ pub file_changes : HashMap < String , FileChangeStats > ,
49+ }
50+
51+ #[ derive( Debug , Clone , Serialize , Deserialize , Default ) ]
52+ pub struct FileChangeStats {
53+ pub added : usize ,
54+ pub modified : usize ,
55+ pub deleted : usize ,
4656}
4757
4858#[ derive( Debug , Clone , Serialize , Deserialize ) ]
@@ -109,9 +119,82 @@ impl CaptureManager {
109119 last_user_message : None ,
110120 user_message_lock : false ,
111121 clean_on_drop : false ,
122+ file_changes : HashMap :: new ( ) ,
112123 } )
113124 }
114125
126+ pub fn get_file_changes ( & self , tag : & str ) -> Result < FileChangeStats > {
127+ let git_dir_arg = format ! ( "--git-dir={}" , self . shadow_repo_path. display( ) ) ;
128+
129+ // Get diff stats against previous tag
130+ let prev_tag = if tag == "0" {
131+ return Ok ( FileChangeStats :: default ( ) ) ;
132+ } else {
133+ self . get_previous_tag ( tag) ?
134+ } ;
135+
136+ let output = Command :: new ( "git" )
137+ . args ( [ & git_dir_arg, "diff" , "--name-status" , & prev_tag, tag] )
138+ . output ( ) ?;
139+
140+ if !output. status . success ( ) {
141+ bail ! ( "Failed to get diff stats: {}" , String :: from_utf8_lossy( & output. stderr) ) ;
142+ }
143+
144+ let mut stats = FileChangeStats :: default ( ) ;
145+ for line in String :: from_utf8_lossy ( & output. stdout ) . lines ( ) {
146+ if let Some ( first_char) = line. chars ( ) . next ( ) {
147+ match first_char {
148+ 'A' => stats. added += 1 ,
149+ 'M' => stats. modified += 1 ,
150+ 'D' => stats. deleted += 1 ,
151+ _ => { } ,
152+ }
153+ }
154+ }
155+
156+ Ok ( stats)
157+ }
158+
159+ fn get_previous_tag ( & self , tag : & str ) -> Result < String > {
160+ // Parse tag format "X" or "X.Y" to get previous
161+ if let Ok ( turn) = tag. parse :: < usize > ( ) {
162+ if turn > 0 {
163+ return Ok ( ( turn - 1 ) . to_string ( ) ) ;
164+ }
165+ } else if tag. contains ( '.' ) {
166+ let parts: Vec < & str > = tag. split ( '.' ) . collect ( ) ;
167+ if parts. len ( ) == 2 {
168+ if let Ok ( tool_num) = parts[ 1 ] . parse :: < usize > ( ) {
169+ if tool_num > 1 {
170+ return Ok ( format ! ( "{}.{}" , parts[ 0 ] , tool_num - 1 ) ) ;
171+ } else {
172+ return Ok ( parts[ 0 ] . to_string ( ) ) ;
173+ }
174+ }
175+ }
176+ }
177+ Ok ( "0" . to_string ( ) )
178+ }
179+
180+ pub fn create_capture_with_stats (
181+ & mut self ,
182+ tag : & str ,
183+ commit_message : & str ,
184+ history_index : usize ,
185+ is_turn : bool ,
186+ tool_name : Option < String > ,
187+ ) -> Result < ( ) > {
188+ self . create_capture ( tag, commit_message, history_index, is_turn, tool_name) ?;
189+
190+ // Store file change stats
191+ if let Ok ( stats) = self . get_file_changes ( tag) {
192+ self . file_changes . insert ( tag. to_string ( ) , stats) ;
193+ }
194+
195+ Ok ( ( ) )
196+ }
197+
115198 pub fn create_capture (
116199 & mut self ,
117200 tag : & str ,
@@ -199,20 +282,40 @@ impl CaptureManager {
199282 Ok ( ( ) )
200283 }
201284
202- pub fn diff ( & self , tag1 : & str , tag2 : & str ) -> Result < String > {
203- let _ = self . get_capture ( tag1) ?;
204- let _ = self . get_capture ( tag2) ?;
285+ pub fn diff_detailed ( & self , tag1 : & str , tag2 : & str ) -> Result < String > {
205286 let git_dir_arg = format ! ( "--git-dir={}" , self . shadow_repo_path. display( ) ) ;
206287
288+ let output = Command :: new ( "git" )
289+ . args ( [ & git_dir_arg, "diff" , "--name-status" , tag1, tag2] )
290+ . output ( ) ?;
291+
292+ if !output. status . success ( ) {
293+ bail ! ( "Failed to get diff: {}" , String :: from_utf8_lossy( & output. stderr) ) ;
294+ }
295+
296+ let mut result = String :: new ( ) ;
297+
298+ for line in String :: from_utf8_lossy ( & output. stdout ) . lines ( ) {
299+ if let Some ( ( status, file) ) = line. split_once ( '\t' ) {
300+ match status {
301+ "A" => result. push_str ( & format ! ( " + {} (added)\n " , file) . green ( ) . to_string ( ) ) ,
302+ "M" => result. push_str ( & format ! ( " ~ {} (modified)\n " , file) . yellow ( ) . to_string ( ) ) ,
303+ "D" => result. push_str ( & format ! ( " - {} (deleted)\n " , file) . red ( ) . to_string ( ) ) ,
304+ _ => { } ,
305+ }
306+ }
307+ }
308+
207309 let output = Command :: new ( "git" )
208310 . args ( [ & git_dir_arg, "diff" , tag1, tag2, "--stat" , "--color=always" ] )
209311 . output ( ) ?;
210312
211313 if output. status . success ( ) {
212- Ok ( String :: from_utf8_lossy ( & output. stdout ) . to_string ( ) )
213- } else {
214- bail ! ( "Failed to get diff: {}" , String :: from_utf8_lossy( & output. stderr) ) ;
314+ result. push_str ( "\n " ) ;
315+ result. push_str ( & String :: from_utf8_lossy ( & output. stdout ) ) ;
215316 }
317+
318+ Ok ( result)
216319 }
217320
218321 fn get_capture ( & self , tag : & str ) -> Result < & Capture > {
0 commit comments