@@ -1761,11 +1761,64 @@ impl SourceControlServiceImpl {
17611761 /// predecessor/predecessor-op extra headers on the commit object.
17621762 pub ( crate ) async fn commit_git_mutation_history (
17631763 & self ,
1764- _ctx : CoreContext ,
1765- _commit : thrift:: CommitSpecifier ,
1766- _params : thrift:: CommitGitMutationHistoryParams ,
1764+ ctx : CoreContext ,
1765+ commit : thrift:: CommitSpecifier ,
1766+ params : thrift:: CommitGitMutationHistoryParams ,
17671767 ) -> Result < thrift:: CommitGitMutationHistoryResponse , scs_errors:: ServiceError > {
1768- Err ( scs_errors:: internal_error ( "commit_git_mutation_history not yet implemented" ) . into ( ) )
1768+ let ( _repo, changeset) = self . repo_changeset ( ctx, & commit) . await ?;
1769+
1770+ let git_extra_headers = changeset. git_extra_headers ( ) . await ?;
1771+ let ( predecessors, op) = extract_git_predecessors ( & git_extra_headers) ;
1772+
1773+ let git_mutation_history = match params. format {
1774+ thrift:: MutationHistoryFormat :: COMMIT_ID => {
1775+ let commit_ids = predecessors
1776+ . into_iter ( )
1777+ . map ( |sha1_bytes| thrift:: CommitId :: git ( sha1_bytes) )
1778+ . collect ( ) ;
1779+ thrift:: GitMutationHistory :: commit_ids ( commit_ids)
1780+ }
1781+ thrift:: MutationHistoryFormat :: GIT_MUTATION => {
1782+ if predecessors. is_empty ( ) {
1783+ thrift:: GitMutationHistory :: git_mutations ( vec ! [ ] )
1784+ } else {
1785+ // Get the successor (current commit) git identity
1786+ let successor_id = changeset
1787+ . git_sha1 ( )
1788+ . await ?
1789+ . map ( |sha1| thrift:: CommitId :: git ( sha1. as_ref ( ) . to_vec ( ) ) )
1790+ . unwrap_or_else ( || {
1791+ // Fall back to bonsai if no git identity
1792+ thrift:: CommitId :: bonsai ( changeset. id ( ) . as_ref ( ) . to_vec ( ) )
1793+ } ) ;
1794+
1795+ let predecessor_ids: Vec < thrift:: CommitId > = predecessors
1796+ . into_iter ( )
1797+ . map ( |sha1_bytes| thrift:: CommitId :: git ( sha1_bytes) )
1798+ . collect ( ) ;
1799+
1800+ let mutation = thrift:: GitMutation {
1801+ successor : successor_id,
1802+ predecessors : predecessor_ids,
1803+ op,
1804+ ..Default :: default ( )
1805+ } ;
1806+ thrift:: GitMutationHistory :: git_mutations ( vec ! [ mutation] )
1807+ }
1808+ }
1809+ unknown => {
1810+ return Err ( scs_errors:: invalid_request ( format ! (
1811+ "invalid mutation history format: {:?}" ,
1812+ unknown
1813+ ) )
1814+ . into ( ) ) ;
1815+ }
1816+ } ;
1817+
1818+ Ok ( thrift:: CommitGitMutationHistoryResponse {
1819+ git_mutation_history,
1820+ ..Default :: default ( )
1821+ } )
17691822 }
17701823
17711824 /// Returns the directory branch clusters for a commit
@@ -1941,3 +1994,143 @@ impl SourceControlServiceImpl {
19411994 } )
19421995 }
19431996}
1997+
1998+ /// Extract predecessor SHA1s and operation from git extra headers.
1999+ /// Returns (predecessor_hex_bytes, operation_string).
2000+ fn extract_git_predecessors < K : AsRef < [ u8 ] > , V : AsRef < [ u8 ] > > (
2001+ headers : & Option < Vec < ( K , V ) > > ,
2002+ ) -> ( Vec < Vec < u8 > > , String ) {
2003+ let mut predecessors: Vec < Vec < u8 > > = Vec :: new ( ) ;
2004+ let mut op = String :: new ( ) ;
2005+
2006+ if let Some ( headers) = headers {
2007+ for ( key, value) in headers {
2008+ if key. as_ref ( ) == b"predecessor" {
2009+ if let Ok ( value_str) = std:: str:: from_utf8 ( value. as_ref ( ) ) {
2010+ for hex_sha1 in value_str. split ( ',' ) {
2011+ let hex_sha1 = hex_sha1. trim ( ) ;
2012+ if !hex_sha1. is_empty ( ) {
2013+ predecessors. push ( hex_sha1. as_bytes ( ) . to_vec ( ) ) ;
2014+ }
2015+ }
2016+ }
2017+ } else if key. as_ref ( ) == b"predecessor-op" {
2018+ if let Ok ( value_str) = std:: str:: from_utf8 ( value. as_ref ( ) ) {
2019+ op = value_str. to_string ( ) ;
2020+ }
2021+ }
2022+ }
2023+ }
2024+
2025+ ( predecessors, op)
2026+ }
2027+
2028+ #[ cfg( test) ]
2029+ mod test_git_mutation {
2030+ use super :: * ;
2031+
2032+ #[ test]
2033+ fn test_extract_no_headers ( ) {
2034+ let headers: Option < Vec < ( Vec < u8 > , Vec < u8 > ) > > = None ;
2035+ let ( preds, op) = extract_git_predecessors ( & headers) ;
2036+ assert ! ( preds. is_empty( ) ) ;
2037+ assert ! ( op. is_empty( ) ) ;
2038+ }
2039+
2040+ #[ test]
2041+ fn test_extract_empty_headers ( ) {
2042+ let headers: Option < Vec < ( Vec < u8 > , Vec < u8 > ) > > = Some ( vec ! [ ] ) ;
2043+ let ( preds, op) = extract_git_predecessors ( & headers) ;
2044+ assert ! ( preds. is_empty( ) ) ;
2045+ assert ! ( op. is_empty( ) ) ;
2046+ }
2047+
2048+ #[ test]
2049+ fn test_extract_no_predecessor_headers ( ) {
2050+ let headers = Some ( vec ! [
2051+ ( b"mergetag" . to_vec( ) , b"some-tag-data" . to_vec( ) ) ,
2052+ ( b"gpgsig" . to_vec( ) , b"signature-data" . to_vec( ) ) ,
2053+ ] ) ;
2054+ let ( preds, op) = extract_git_predecessors ( & headers) ;
2055+ assert ! ( preds. is_empty( ) ) ;
2056+ assert ! ( op. is_empty( ) ) ;
2057+ }
2058+
2059+ #[ test]
2060+ fn test_extract_single_predecessor ( ) {
2061+ let sha1 = "da39a3ee5e6b4b0d3255bfef95601890afd80709" ;
2062+ let headers = Some ( vec ! [
2063+ ( b"predecessor" . to_vec( ) , sha1. as_bytes( ) . to_vec( ) ) ,
2064+ ( b"predecessor-op" . to_vec( ) , b"amend" . to_vec( ) ) ,
2065+ ] ) ;
2066+ let ( preds, op) = extract_git_predecessors ( & headers) ;
2067+ assert_eq ! ( preds. len( ) , 1 ) ;
2068+ assert_eq ! ( preds[ 0 ] , sha1. as_bytes( ) ) ;
2069+ assert_eq ! ( op, "amend" ) ;
2070+ }
2071+
2072+ #[ test]
2073+ fn test_extract_multiple_predecessors_comma_separated ( ) {
2074+ let sha1_a = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ;
2075+ let sha1_b = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" ;
2076+ let value = format ! ( "{},{}" , sha1_a, sha1_b) ;
2077+ let headers = Some ( vec ! [
2078+ ( b"predecessor" . to_vec( ) , value. as_bytes( ) . to_vec( ) ) ,
2079+ ( b"predecessor-op" . to_vec( ) , b"fold" . to_vec( ) ) ,
2080+ ] ) ;
2081+ let ( preds, op) = extract_git_predecessors ( & headers) ;
2082+ assert_eq ! ( preds. len( ) , 2 ) ;
2083+ assert_eq ! ( preds[ 0 ] , sha1_a. as_bytes( ) ) ;
2084+ assert_eq ! ( preds[ 1 ] , sha1_b. as_bytes( ) ) ;
2085+ assert_eq ! ( op, "fold" ) ;
2086+ }
2087+
2088+ #[ test]
2089+ fn test_extract_rebase_op ( ) {
2090+ let sha1 = "1234567890abcdef1234567890abcdef12345678" ;
2091+ let headers = Some ( vec ! [
2092+ ( b"predecessor" . to_vec( ) , sha1. as_bytes( ) . to_vec( ) ) ,
2093+ ( b"predecessor-op" . to_vec( ) , b"rebase" . to_vec( ) ) ,
2094+ ] ) ;
2095+ let ( preds, op) = extract_git_predecessors ( & headers) ;
2096+ assert_eq ! ( preds. len( ) , 1 ) ;
2097+ assert_eq ! ( op, "rebase" ) ;
2098+ }
2099+
2100+ #[ test]
2101+ fn test_extract_cherry_pick_op ( ) {
2102+ let sha1 = "abcdef1234567890abcdef1234567890abcdef12" ;
2103+ let headers = Some ( vec ! [
2104+ ( b"predecessor" . to_vec( ) , sha1. as_bytes( ) . to_vec( ) ) ,
2105+ ( b"predecessor-op" . to_vec( ) , b"cherry-pick" . to_vec( ) ) ,
2106+ ] ) ;
2107+ let ( preds, op) = extract_git_predecessors ( & headers) ;
2108+ assert_eq ! ( preds. len( ) , 1 ) ;
2109+ assert_eq ! ( op, "cherry-pick" ) ;
2110+ }
2111+
2112+ #[ test]
2113+ fn test_extract_predecessor_without_op ( ) {
2114+ let sha1 = "da39a3ee5e6b4b0d3255bfef95601890afd80709" ;
2115+ let headers = Some ( vec ! [ ( b"predecessor" . to_vec( ) , sha1. as_bytes( ) . to_vec( ) ) ] ) ;
2116+ let ( preds, op) = extract_git_predecessors ( & headers) ;
2117+ assert_eq ! ( preds. len( ) , 1 ) ;
2118+ assert_eq ! ( preds[ 0 ] , sha1. as_bytes( ) ) ;
2119+ assert ! ( op. is_empty( ) ) ;
2120+ }
2121+
2122+ #[ test]
2123+ fn test_extract_with_other_headers_intermixed ( ) {
2124+ let sha1 = "da39a3ee5e6b4b0d3255bfef95601890afd80709" ;
2125+ let headers = Some ( vec ! [
2126+ ( b"mergetag" . to_vec( ) , b"tag-data" . to_vec( ) ) ,
2127+ ( b"predecessor" . to_vec( ) , sha1. as_bytes( ) . to_vec( ) ) ,
2128+ ( b"gpgsig" . to_vec( ) , b"sig-data" . to_vec( ) ) ,
2129+ ( b"predecessor-op" . to_vec( ) , b"amend" . to_vec( ) ) ,
2130+ ] ) ;
2131+ let ( preds, op) = extract_git_predecessors ( & headers) ;
2132+ assert_eq ! ( preds. len( ) , 1 ) ;
2133+ assert_eq ! ( preds[ 0 ] , sha1. as_bytes( ) ) ;
2134+ assert_eq ! ( op, "amend" ) ;
2135+ }
2136+ }
0 commit comments