@@ -806,25 +806,61 @@ pub fn get_initial_integration_steps_for_branch(
806806 }
807807
808808 for upstream_commit in remote_only_commits {
809+ divergence_upstream_only. push ( divergence_commit ( repo, upstream_commit) ?) ;
809810 initial_steps. push ( InteractiveIntegrationStep :: PickUpstream {
810811 commit_id : upstream_commit,
811812 } ) ;
812813 }
813814
814815 initial_steps. reverse ( ) ;
815816
816- Ok ( InteractiveIntegration {
817+ let integration = InteractiveIntegration {
817818 steps : initial_steps,
818819 merge_base,
820+ } ;
821+ let mut divergence = IntegrationDivergenceDisplay {
822+ branch_ref_name : ref_name. to_owned ( ) ,
823+ upstream_ref_name : upstream_ref_name. into_owned ( ) ,
824+ local_only : divergence_local_only,
825+ upstream_only : divergence_upstream_only,
826+ matched : divergence_matched,
827+ merge_base : divergence_commit ( repo, merge_base) ?,
828+ } ;
829+ let local_tip = divergence
830+ . local_only
831+ . first ( )
832+ . map ( |commit| commit. id )
833+ . or_else ( || divergence. matched . first ( ) . map ( |commit| commit. id ) ) ;
834+ let upstream_tip = divergence
835+ . upstream_only
836+ . first ( )
837+ . map ( |commit| commit. id )
838+ . or_else ( || divergence. matched . first ( ) . map ( |commit| commit. id ) ) ;
839+ add_ref_label (
840+ & mut divergence. local_only ,
841+ & mut divergence. matched ,
842+ local_tip,
843+ divergence. branch_ref_name . shorten ( ) . to_string ( ) ,
844+ ) ;
845+ add_ref_label (
846+ & mut divergence. upstream_only ,
847+ & mut divergence. matched ,
848+ upstream_tip,
849+ divergence. upstream_ref_name . shorten ( ) . to_string ( ) ,
850+ ) ;
851+
852+ Ok ( InitialBranchIntegration {
853+ integration,
854+ divergence,
819855 } )
820856}
821857
822858/// Computes local and upstream commit lists (tip to merge-base, first-parent) together
823859/// with their merge base for a branch and its tracking branch.
824- fn get_commits_until_merge_base (
825- ref_name : & gix:: refs:: FullNameRef ,
826- repo : & gix:: Repository ,
827- ) -> Result < ( Vec < gix :: ObjectId > , Vec < gix :: ObjectId > , gix :: ObjectId ) , anyhow:: Error > {
860+ fn get_commits_until_merge_base < ' a > (
861+ ref_name : & ' a gix:: refs:: FullNameRef ,
862+ repo : & ' a gix:: Repository ,
863+ ) -> Result < BranchMergeBaseCommits < ' a > , anyhow:: Error > {
828864 let ( local_tip, upstream_ref_name, upstream_tip) =
829865 get_branch_tips_and_upstream ( ref_name, repo) ?;
830866 let cache = repo. commit_graph_if_enabled ( ) ?;
@@ -839,7 +875,12 @@ fn get_commits_until_merge_base(
839875 } ) ?;
840876 let local_commits = branch_commits_until ( repo, local_tip, merge_base) ?;
841877 let upstream_commits = branch_commits_until ( repo, upstream_tip, merge_base) ?;
842- Ok ( ( local_commits, upstream_commits, merge_base) )
878+ Ok ( BranchMergeBaseCommits {
879+ local_commits,
880+ upstream_commits,
881+ merge_base,
882+ upstream_ref_name,
883+ } )
843884}
844885
845886/// Resolves local/upstream branch tips and tracking reference name for `ref_name`.
@@ -945,6 +986,59 @@ fn effective_change_id(repo: &gix::Repository, commit_id: gix::ObjectId) -> Resu
945986 . to_string ( ) )
946987}
947988
989+ fn divergence_commit (
990+ repo : & gix:: Repository ,
991+ commit_id : gix:: ObjectId ,
992+ ) -> Result < IntegrationDivergenceCommit > {
993+ Ok ( IntegrationDivergenceCommit {
994+ id : commit_id,
995+ subject : but_core:: Commit :: from_id ( commit_id. attach ( repo) ) ?
996+ . message
997+ . lines ( )
998+ . next ( )
999+ . unwrap_or_default ( )
1000+ . to_str_lossy ( )
1001+ . into_owned ( ) ,
1002+ refs : Vec :: new ( ) ,
1003+ } )
1004+ }
1005+
1006+ fn add_ref_label (
1007+ primary : & mut [ IntegrationDivergenceCommit ] ,
1008+ secondary : & mut [ IntegrationDivergenceCommit ] ,
1009+ id : Option < gix:: ObjectId > ,
1010+ label : String ,
1011+ ) {
1012+ let Some ( id) = id else {
1013+ return ;
1014+ } ;
1015+ if let Some ( commit) = primary. iter_mut ( ) . find ( |commit| commit. id == id) {
1016+ if !commit. refs . contains ( & label) {
1017+ commit. refs . push ( label) ;
1018+ }
1019+ return ;
1020+ }
1021+ if let Some ( commit) = secondary. iter_mut ( ) . find ( |commit| commit. id == id)
1022+ && !commit. refs . contains ( & label)
1023+ {
1024+ commit. refs . push ( label) ;
1025+ }
1026+ }
1027+
1028+ fn graph_commit_string ( prefix : & str , commit : & IntegrationDivergenceCommit ) -> String {
1029+ let refs = if commit. refs . is_empty ( ) {
1030+ String :: new ( )
1031+ } else {
1032+ format ! ( " ({})" , commit. refs. join( ", " ) )
1033+ } ;
1034+ format ! (
1035+ "{prefix}{}{} {}" ,
1036+ commit. id. to_hex_with_len( 7 ) ,
1037+ refs,
1038+ commit. subject
1039+ )
1040+ }
1041+
9481042#[ cfg( test) ]
9491043mod tests {
9501044 use super :: * ;
@@ -1044,4 +1138,48 @@ mod tests {
10441138 "invalid squash message should produce a targeted error"
10451139 ) ;
10461140 }
1141+
1142+ #[ test]
1143+ fn divergence_display_renders_git_style_graph ( ) {
1144+ let display = IntegrationDivergenceDisplay {
1145+ branch_ref_name : gix:: refs:: Category :: LocalBranch
1146+ . to_full_name ( "feature" )
1147+ . expect ( "valid local branch" ) ,
1148+ upstream_ref_name : gix:: refs:: Category :: RemoteBranch
1149+ . to_full_name ( "origin/feature" )
1150+ . expect ( "valid remote branch" ) ,
1151+ local_only : vec ! [ IntegrationDivergenceCommit {
1152+ id: oid( "1111111111111111111111111111111111111111" ) ,
1153+ subject: "local tip" . into( ) ,
1154+ refs: vec![ "feature" . into( ) ] ,
1155+ } ] ,
1156+ upstream_only : vec ! [ IntegrationDivergenceCommit {
1157+ id: oid( "2222222222222222222222222222222222222222" ) ,
1158+ subject: "remote tip" . into( ) ,
1159+ refs: vec![ "origin/feature" . into( ) ] ,
1160+ } ] ,
1161+ matched : vec ! [ IntegrationDivergenceCommit {
1162+ id: oid( "3333333333333333333333333333333333333333" ) ,
1163+ subject: "shared" . into( ) ,
1164+ refs: Vec :: new( ) ,
1165+ } ] ,
1166+ merge_base : IntegrationDivergenceCommit {
1167+ id : oid ( "4444444444444444444444444444444444444444" ) ,
1168+ subject : "base" . into ( ) ,
1169+ refs : Vec :: new ( ) ,
1170+ } ,
1171+ } ;
1172+
1173+ insta:: assert_snapshot!(
1174+ display. to_string( ) ,
1175+ "graph output should stay stable because the CLI and frontend consume it directly" ,
1176+ @r"
1177+ * 1111111 (feature) local tip
1178+ | * 2222222 (origin/feature) remote tip
1179+ |/
1180+ * 3333333 shared
1181+ * 4444444 base
1182+ "
1183+ ) ;
1184+ }
10471185}
0 commit comments