@@ -61,6 +61,25 @@ struct DiffTracePayload {
6161 tool_version : Option < String > ,
6262}
6363
64+ #[ derive( Clone , Debug , Eq , PartialEq , Serialize ) ]
65+ struct RewrittenCommitPair {
66+ old_oid : String ,
67+ new_oid : String ,
68+ }
69+
70+ #[ derive( Clone , Debug , Eq , PartialEq , Serialize ) ]
71+ struct PostRewriteRebasePayload {
72+ method : String ,
73+ capture_timestamp : String ,
74+ repository_root : String ,
75+ git_environment : BTreeMap < String , String > ,
76+ raw_stdin : String ,
77+ parsed_pairs : Vec < RewrittenCommitPair > ,
78+ parse_diagnostics : Vec < String > ,
79+ head_oid : Option < String > ,
80+ head_patch : Option < String > ,
81+ }
82+
6483/// Required `sce hooks diff-trace` STDIN payload shape:
6584/// `{ sessionID, diff, time, model_id, tool_name, tool_version }`.
6685///
@@ -746,8 +765,112 @@ fn run_post_rewrite_subcommand_with_trace(
746765 _: & HookSubcommand ,
747766 rewrite_method : & str ,
748767) -> Result < String > {
749- let stdin_payload = read_hook_stdin ( ) ;
750- stdin_payload. and_then ( |_| run_post_rewrite_subcommand ( repository_root, rewrite_method) )
768+ let method = rewrite_method. trim ( ) ;
769+
770+ if method == "rebase" {
771+ let runtime = resolve_runtime_state ( repository_root) ?;
772+ if !runtime. sce_disabled {
773+ let stdin_payload = read_hook_stdin ( ) ?;
774+ return run_post_rewrite_rebase_subcommand ( repository_root, method, & stdin_payload) ;
775+ }
776+ }
777+
778+ run_post_rewrite_subcommand ( repository_root, method)
779+ }
780+
781+ fn parse_post_rewrite_stdin ( input : & str ) -> ( Vec < RewrittenCommitPair > , Vec < String > ) {
782+ let mut pairs = Vec :: new ( ) ;
783+ let mut diagnostics = Vec :: new ( ) ;
784+
785+ for ( line_index, line) in input. lines ( ) . enumerate ( ) {
786+ let trimmed = line. trim ( ) ;
787+ if trimmed. is_empty ( ) {
788+ continue ;
789+ }
790+
791+ let parts: Vec < & str > = trimmed. split_whitespace ( ) . collect ( ) ;
792+ if parts. len ( ) >= 2 {
793+ pairs. push ( RewrittenCommitPair {
794+ old_oid : parts[ 0 ] . to_string ( ) ,
795+ new_oid : parts[ 1 ] . to_string ( ) ,
796+ } ) ;
797+ if parts. len ( ) > 2 {
798+ diagnostics. push ( format ! (
799+ "Line {}: unexpected content after commit pair: '{}'" ,
800+ line_index + 1 ,
801+ parts[ 2 ..] . join( " " )
802+ ) ) ;
803+ }
804+ } else {
805+ diagnostics. push ( format ! (
806+ "Line {}: expected '<old-oid> <new-oid>', got: '{}'" ,
807+ line_index + 1 ,
808+ trimmed
809+ ) ) ;
810+ }
811+ }
812+
813+ ( pairs, diagnostics)
814+ }
815+
816+ fn run_post_rewrite_rebase_subcommand (
817+ repository_root : & Path ,
818+ method : & str ,
819+ stdin_payload : & str ,
820+ ) -> Result < String > {
821+ let ( parsed_pairs, parse_diagnostics) = parse_post_rewrite_stdin ( stdin_payload) ;
822+ let capture_timestamp = Utc :: now ( ) . to_rfc3339 ( ) ;
823+ let git_environment = collect_git_environment ( ) ;
824+
825+ let head_oid = run_git_command_capture_stdout (
826+ repository_root,
827+ & [ "rev-parse" , "HEAD" ] ,
828+ "Failed to capture HEAD revision from git for post-rewrite rebase evidence." ,
829+ )
830+ . ok ( ) ;
831+
832+ let head_patch = run_git_command_capture_stdout (
833+ repository_root,
834+ & [ "show" , "--format=" , "--patch" , "--no-ext-diff" , "HEAD" ] ,
835+ "Failed to capture HEAD patch from git for post-rewrite rebase evidence." ,
836+ )
837+ . ok ( ) ;
838+
839+ let payload = PostRewriteRebasePayload {
840+ method : method. to_string ( ) ,
841+ capture_timestamp,
842+ repository_root : repository_root. to_string_lossy ( ) . to_string ( ) ,
843+ git_environment,
844+ raw_stdin : stdin_payload. to_string ( ) ,
845+ parsed_pairs,
846+ parse_diagnostics,
847+ head_oid,
848+ head_patch,
849+ } ;
850+
851+ let serialized = format ! (
852+ "{}\n " ,
853+ serde_json:: to_string_pretty( & payload)
854+ . context( "Failed to serialize post-rewrite rebase payload for persistence." ) ?
855+ ) ;
856+
857+ let artifact_directory = repository_root
858+ . join ( "context" )
859+ . join ( "tmp" )
860+ . join ( "post-rewrite" ) ;
861+
862+ persist_serialized_trace_payload (
863+ & artifact_directory,
864+ "post-rewrite-rebase" ,
865+ & serialized,
866+ "post-rewrite rebase evidence" ,
867+ ) ?;
868+
869+ Ok ( format ! (
870+ "post-rewrite hook captured rebase evidence: {} pairs, {} diagnostic(s), artifact in context/tmp/post-rewrite/." ,
871+ payload. parsed_pairs. len( ) ,
872+ payload. parse_diagnostics. len( )
873+ ) )
751874}
752875
753876fn hook_runtime_invocation_name ( subcommand : & HookSubcommand ) -> & ' static str {
@@ -1172,4 +1295,132 @@ mod tests {
11721295 assert_eq ! ( output. tool_name, Some ( String :: from( "opencode" ) ) ) ;
11731296 assert_eq ! ( output. tool_version, Some ( String :: from( "1.2.3" ) ) ) ;
11741297 }
1298+
1299+ // --- post-rewrite rebase capture tests ---
1300+
1301+ #[ test]
1302+ fn parse_post_rewrite_stdin_valid_lines ( ) {
1303+ let input = "abc123 def456\n \n 789abc def012\n " ;
1304+ let ( pairs, diagnostics) = parse_post_rewrite_stdin ( input) ;
1305+
1306+ assert_eq ! ( pairs. len( ) , 2 ) ;
1307+ assert_eq ! (
1308+ pairs[ 0 ] ,
1309+ RewrittenCommitPair {
1310+ old_oid: "abc123" . to_string( ) ,
1311+ new_oid: "def456" . to_string( ) ,
1312+ }
1313+ ) ;
1314+ assert_eq ! (
1315+ pairs[ 1 ] ,
1316+ RewrittenCommitPair {
1317+ old_oid: "789abc" . to_string( ) ,
1318+ new_oid: "def012" . to_string( ) ,
1319+ }
1320+ ) ;
1321+ assert ! (
1322+ diagnostics. is_empty( ) ,
1323+ "expected no diagnostics for valid input"
1324+ ) ;
1325+ }
1326+
1327+ #[ test]
1328+ fn parse_post_rewrite_stdin_malformed_lines ( ) {
1329+ let input = "abc123 def456\n not-a-pair\n 789abc def012\n " ;
1330+ let ( pairs, diagnostics) = parse_post_rewrite_stdin ( input) ;
1331+
1332+ assert_eq ! (
1333+ pairs. len( ) ,
1334+ 2 ,
1335+ "valid lines should be parsed despite malformed line"
1336+ ) ;
1337+ assert_eq ! (
1338+ diagnostics. len( ) ,
1339+ 1 ,
1340+ "malformed line should produce one diagnostic"
1341+ ) ;
1342+ assert ! (
1343+ diagnostics[ 0 ] . contains( "not-a-pair" ) ,
1344+ "diagnostic should reference the malformed content"
1345+ ) ;
1346+ assert ! (
1347+ diagnostics[ 0 ] . contains( "Line 2" ) ,
1348+ "diagnostic should reference the correct line number"
1349+ ) ;
1350+ }
1351+
1352+ #[ test]
1353+ fn parse_post_rewrite_stdin_empty_input ( ) {
1354+ let input = "" ;
1355+ let ( pairs, diagnostics) = parse_post_rewrite_stdin ( input) ;
1356+
1357+ assert ! ( pairs. is_empty( ) , "empty input should produce no pairs" ) ;
1358+ assert ! (
1359+ diagnostics. is_empty( ) ,
1360+ "empty input should produce no diagnostics"
1361+ ) ;
1362+ }
1363+
1364+ #[ test]
1365+ fn parse_post_rewrite_stdin_extra_content ( ) {
1366+ let input = "abc123 def456 extra trailing content\n " ;
1367+ let ( pairs, diagnostics) = parse_post_rewrite_stdin ( input) ;
1368+
1369+ assert_eq ! (
1370+ pairs. len( ) ,
1371+ 1 ,
1372+ "pair should be parsed from line with extra content"
1373+ ) ;
1374+ assert_eq ! (
1375+ diagnostics. len( ) ,
1376+ 1 ,
1377+ "extra content should produce one diagnostic"
1378+ ) ;
1379+ assert ! (
1380+ diagnostics[ 0 ] . contains( "trailing content" ) ,
1381+ "diagnostic should reference the extra content"
1382+ ) ;
1383+ }
1384+
1385+ #[ test]
1386+ fn parse_post_rewrite_stdin_blank_lines_skipped ( ) {
1387+ let input = "abc123 def456\n \n \n 789abc def012\n " ;
1388+ let ( pairs, diagnostics) = parse_post_rewrite_stdin ( input) ;
1389+
1390+ assert_eq ! ( pairs. len( ) , 2 , "blank lines should be skipped" ) ;
1391+ assert ! (
1392+ diagnostics. is_empty( ) ,
1393+ "blank lines should not produce diagnostics"
1394+ ) ;
1395+ }
1396+
1397+ #[ test]
1398+ fn post_rewrite_amend_method_returns_no_op ( ) {
1399+ let result = run_post_rewrite_subcommand ( Path :: new ( "/fake/repo" ) , "amend" ) ;
1400+ assert ! ( result. is_ok( ) , "post-rewrite amend should succeed" ) ;
1401+ let output = result. expect ( "already checked is_ok" ) ;
1402+ assert ! (
1403+ output. contains( "no-op runtime state" ) ,
1404+ "amend method should report no-op: got '{output}'"
1405+ ) ;
1406+ assert ! (
1407+ output. contains( "rewrite_method='amend'" ) ,
1408+ "amend method should be included in output: got '{output}'"
1409+ ) ;
1410+ }
1411+
1412+ #[ test]
1413+ fn post_rewrite_other_method_returns_no_op ( ) {
1414+ let result = run_post_rewrite_subcommand ( Path :: new ( "/fake/repo" ) , "other" ) ;
1415+ assert ! ( result. is_ok( ) , "post-rewrite other should succeed" ) ;
1416+ let output = result. expect ( "already checked is_ok" ) ;
1417+ assert ! (
1418+ output. contains( "no-op runtime state" ) ,
1419+ "other method should report no-op: got '{output}'"
1420+ ) ;
1421+ assert ! (
1422+ output. contains( "rewrite_method='other'" ) ,
1423+ "other method should be included in output: got '{output}'"
1424+ ) ;
1425+ }
11751426}
0 commit comments