@@ -755,25 +755,47 @@ impl WorkspaceState {
755755 }
756756 }
757757
758- /// Truncate each stack to only show commits above its fork point with the
759- /// target, then remove empty branches and stacks.
760- ///
761- /// The fork point is the first commit on the first-parent chain (walked via
762- /// the graph's `CommitFlags::Integrated`) that is reachable from the target.
763- ///
764- /// Stacks explicitly tracked in workspace metadata are never pruned —
765- /// their integrated branches and commits must remain visible so that
766- /// `integrate_upstream` can detect and remove them.
758+ /// Remove integrated commits and empty branches at the bottom of each
759+ /// stack, but only those at or below the workspace's target commit.
760+ /// Integrated commits above the target are kept until the user advances
761+ /// the target via upstream integration.
767762 fn prune_integrated_segments ( & mut self , graph : & Graph ) {
768763 if self . extra_target . is_some ( ) || self . target_ref . is_none ( ) {
769764 return ;
770765 }
766+ let ( target_segment_index, target_commit_id) = if let Some ( tc) = self . target_commit . as_ref ( )
767+ {
768+ ( tc. segment_index , Some ( tc. commit_id ) )
769+ } else if let Some ( tr) = self . target_ref . as_ref ( ) {
770+ ( tr. segment_index , None )
771+ } else {
772+ return ;
773+ } ;
774+
775+ // Collect all commit IDs that are the target itself or ancestors
776+ // of it by walking the graph toward its ancestors
777+ // (Direction::Outgoing). In the target segment itself, only
778+ // include commits from the target position downward — commits
779+ // above it in the same segment are not reachable from the target.
780+ let mut is_target_or_below = HashSet :: new ( ) ;
781+ graph. visit_all_segments_excluding_start_until (
782+ target_segment_index,
783+ Direction :: Outgoing ,
784+ |s| {
785+ is_target_or_below. extend ( s. commits . iter ( ) . map ( |c| c. id ) ) ;
786+ false
787+ } ,
788+ ) ;
789+ // For the target segment, include commits from the target onward.
790+ let target_seg = & graph[ target_segment_index] ;
791+ let start = target_commit_id
792+ . and_then ( |id| target_seg. commits . iter ( ) . position ( |c| c. id == id) )
793+ . unwrap_or ( 0 ) ;
794+ is_target_or_below. extend ( target_seg. commits [ start..] . iter ( ) . map ( |c| c. id ) ) ;
795+
771796 let metadata = self . metadata . as_ref ( ) ;
772797 for stack in & mut self . stacks {
773- if is_metadata_tracked ( stack, metadata) {
774- continue ;
775- }
776- truncate_at_fork_point ( stack, graph) ;
798+ truncate_at_fork_point ( stack, & is_target_or_below) ;
777799 remove_empty_branches ( stack, metadata) ;
778800 }
779801 self . stacks . retain ( |stack| !stack. segments . is_empty ( ) ) ;
@@ -967,54 +989,57 @@ impl WorkspaceState {
967989 }
968990}
969991
970- /// Truncate the stack at the fork point: the first commit whose graph-level
971- /// `CommitFlags::Integrated` flag is set, walking top-down through each
972- /// segment's `commits_by_segment` entries.
973- ///
974- /// If the very first commit is already integrated, the entire stack is
975- /// integrated and we leave it untouched — pruning is only for partially
976- /// integrated stacks where the bottom portion has been merged upstream.
977- fn truncate_at_fork_point ( stack : & mut Stack , graph : & Graph ) {
978- let mut is_first = true ;
979- for seg_idx in 0 ..stack. segments . len ( ) {
992+ /// Truncate the stack by removing integrated commits at the bottom, but
993+ /// only those that are the target commit itself or ancestors of it.
994+ /// Integrated commits above the target are kept — they represent branch
995+ /// work that was merged upstream but whose target hasn't moved forward
996+ /// yet. Once the user integrates upstream and the target advances past
997+ /// them, they will be pruned.
998+ fn truncate_at_fork_point ( stack : & mut Stack , is_target_or_below : & HashSet < gix:: ObjectId > ) {
999+ // Walk segments bottom-up, then commits bottom-up within each segment.
1000+ // Find the contiguous integrated tail whose commits are all in the
1001+ // `is_target_or_below` set. Stop at the first commit that is either
1002+ // not integrated or above the target.
1003+ let mut cut: Option < ( usize , usize ) > = None ;
1004+ ' outer: for seg_idx in ( 0 ..stack. segments . len ( ) ) . rev ( ) {
9801005 let seg = & stack. segments [ seg_idx] ;
981- for & ( graph_sidx, ofs) in & seg. commits_by_segment {
982- for ( i, commit) in graph[ graph_sidx] . commits . iter ( ) . enumerate ( ) {
983- if commit. flags . contains ( CommitFlags :: Integrated ) {
984- if is_first {
985- // Fully integrated — leave the stack as-is.
986- return ;
987- }
988- let cut = ofs + i;
989- stack. segments [ seg_idx] . commits . truncate ( cut) ;
990- stack. segments [ seg_idx]
991- . commits_by_segment
992- . retain ( |( _, o) | * o < cut) ;
993- // Clear commits in all segments below the cutoff, but keep
994- // the segments themselves so that `remove_empty_branches`
995- // can decide whether to retain metadata-tracked branches.
996- for seg in & mut stack. segments [ seg_idx + 1 ..] {
997- seg. commits . clear ( ) ;
998- seg. commits_by_segment . clear ( ) ;
999- }
1000- return ;
1001- }
1002- is_first = false ;
1006+ for ( commit_idx, commit) in seg. commits . iter ( ) . enumerate ( ) . rev ( ) {
1007+ if !is_target_or_below. contains ( & commit. id ) {
1008+ break ' outer;
1009+ }
1010+ if commit. flags . contains ( StackCommitFlags :: Integrated ) {
1011+ cut = Some ( ( seg_idx, commit_idx) ) ;
1012+ } else {
1013+ break ' outer;
10031014 }
10041015 }
10051016 }
1006- }
10071017
1008- /// Returns `true` if the stack is explicitly tracked in workspace metadata.
1009- fn is_metadata_tracked (
1010- stack : & Stack ,
1011- metadata : Option < & but_core:: ref_metadata:: Workspace > ,
1012- ) -> bool {
1013- stack. id . is_some_and ( |stack_id| {
1014- metadata. is_some_and ( |meta| meta. stacks ( Applied ) . any ( |ms| ms. id == stack_id) )
1015- } )
1018+ let Some ( ( cut_seg_idx, cut_offset) ) = cut else {
1019+ return ;
1020+ } ;
1021+
1022+ // Truncate commits in the cut segment.
1023+ stack. segments [ cut_seg_idx] . commits . truncate ( cut_offset) ;
1024+ stack. segments [ cut_seg_idx]
1025+ . commits_by_segment
1026+ . retain ( |( _, o) | * o < cut_offset) ;
1027+ // Remove all segments below the cut — their branch refs pointed into
1028+ // integrated territory, not the fork point.
1029+ // Also remove the cut segment itself if it became empty, UNLESS it is
1030+ // the topmost segment (index 0). Keeping an empty topmost segment
1031+ // lets `remove_empty_branches` decide whether the branch ref should
1032+ // be preserved (e.g. a metadata-tracked branch at the fork point).
1033+ let keep = if stack. segments [ cut_seg_idx] . commits . is_empty ( ) && cut_seg_idx > 0 {
1034+ cut_seg_idx
1035+ } else {
1036+ cut_seg_idx + 1
1037+ } ;
1038+ stack. segments . truncate ( keep) ;
10161039}
10171040
1041+ /// Remove empty segments unless they are mentioned in workspace metadata
1042+ /// (e.g. a branch the user just added at the fork point with no commits yet).
10181043fn remove_empty_branches ( stack : & mut Stack , metadata : Option < & but_core:: ref_metadata:: Workspace > ) {
10191044 let own_metadata_stack = stack. id . and_then ( |stack_id| {
10201045 metadata. and_then ( |meta| meta. stacks ( Applied ) . find ( |ms| ms. id == stack_id) )
@@ -1024,7 +1049,15 @@ fn remove_empty_branches(stack: &mut Stack, metadata: Option<&but_core::ref_meta
10241049 || own_metadata_stack. is_some_and ( |ms| {
10251050 seg. ref_info
10261051 . as_ref ( )
1027- . is_some_and ( |ri| ms. branches . iter ( ) . any ( |b| b. ref_name == ri. ref_name ) )
1052+ // NOTE: `!b.archived` compensates for `prune_archived_segments`
1053+ // running *before* integrated-commit pruning — archived segments
1054+ // that still had commits are skipped there, then emptied here.
1055+ // Once metadata is kept trimmed and up-to-date we can drop this.
1056+ . is_some_and ( |ri| {
1057+ ms. branches
1058+ . iter ( )
1059+ . any ( |b| b. ref_name == ri. ref_name && !b. archived )
1060+ } )
10281061 } )
10291062 } ) ;
10301063}
0 commit comments