11use super :: buffer:: Buffer ;
2- use super :: chunk:: { Chunk , Markers } ;
2+ use super :: chunk:: LaneSlot ;
33use super :: oids:: GraphOids ;
44use super :: {
5- AliasId , ConnectionType , GraphRow , LaneIndex , MAX_LANE_COLORS ,
5+ CommitAlias , ConnectionType , GraphRow , LaneIndex , UnwalkedAlias ,
6+ MAX_LANE_COLORS ,
67} ;
78use crate :: sync:: CommitId ;
89use core:: cmp:: Ordering ;
@@ -145,10 +146,14 @@ fn overlay_cell(
145146pub struct GraphWalker {
146147 pub buffer : Buffer ,
147148 pub oids : GraphOids ,
148- pub branch_lane_map : HashMap < CommitId , usize > ,
149149
150150 /// Maps a merge commit's alias to the alias of its second parent.
151- pub merge_parents : HashMap < AliasId , AliasId > ,
151+ pub merge_parents : HashMap < CommitAlias , CommitAlias > ,
152+
153+ /// Aliases of commits already folded into the buffer; consulted
154+ /// by [`Self::drawable_parent`], which refuses to mint an
155+ /// [`UnwalkedAlias`] for any of them.
156+ processed : HashSet < CommitAlias > ,
152157}
153158
154159impl Default for GraphWalker {
@@ -162,43 +167,65 @@ impl GraphWalker {
162167 Self {
163168 buffer : Buffer :: new ( ) ,
164169 oids : GraphOids :: new ( ) ,
165- branch_lane_map : HashMap :: new ( ) ,
166170 merge_parents : HashMap :: new ( ) ,
171+ processed : HashSet :: new ( ) ,
167172 }
168173 }
169174
175+ /// Mint the drawable alias for a parent commit, or `None` if the
176+ /// walk already passed it.
177+ fn drawable_parent (
178+ & mut self ,
179+ id : & CommitId ,
180+ ) -> Option < UnwalkedAlias > {
181+ let alias = self . oids . get_or_insert ( id) ;
182+ ( !self . processed . contains ( & alias) )
183+ . then_some ( UnwalkedAlias ( alias) )
184+ }
185+
170186 pub fn process (
171187 & mut self ,
172188 commit_id : CommitId ,
173189 parents : & [ CommitId ] ,
174190 ) {
175191 let commit_alias = self . oids . get_or_insert ( & commit_id) ;
176192
177- let mut mapped_parents = parents
193+ let mut drawable_parents = parents
178194 . iter ( )
179- . map ( |parent_id| self . oids . get_or_insert ( parent_id) ) ;
195+ . filter_map ( |parent_id| self . drawable_parent ( parent_id) ) ;
180196
181197 // We explicitly cap support at 2 parents, ignoring octo/mega merges.
182- let first_parent = mapped_parents. next ( ) ;
183- let second_parent = mapped_parents. next ( ) ;
184-
185- let chunk = Chunk {
186- alias : Some ( commit_alias) ,
187- parent_a : first_parent,
188- parent_b : second_parent,
189- marker : Markers :: Commit ,
198+ let first_parent = drawable_parents. next ( ) ;
199+ let second_parent = drawable_parents. next ( ) ;
200+
201+ let chunk = match ( first_parent, second_parent) {
202+ ( Some ( parent) , Some ( second) ) => LaneSlot :: FlowingMerge {
203+ alias : commit_alias,
204+ parent,
205+ second,
206+ } ,
207+ ( Some ( parent) , None ) => LaneSlot :: Flowing {
208+ alias : commit_alias,
209+ parent,
210+ } ,
211+ // `second_parent` is only ever `Some` once
212+ // `first_parent` has already been consumed from the
213+ // identical iterator, so a merge always has both parents.
214+ ( None , _) => LaneSlot :: Settled {
215+ alias : commit_alias,
216+ } ,
190217 } ;
191218
192- if let Some ( second) = second_parent {
219+ if let LaneSlot :: FlowingMerge { second, .. } = & chunk {
220+ let second = second. get ( ) ;
193221 self . merge_parents . insert ( commit_alias, second) ;
194222
195- if first_parent. is_some ( )
196- && !self . is_redundant_merge_track ( second)
197- {
223+ if !self . is_redundant_merge_track ( second) {
198224 self . buffer . track_merge_commit ( commit_alias) ;
199225 }
200226 }
201227
228+ self . processed . insert ( commit_alias) ;
202229 self . buffer . update ( & chunk) ;
203230 }
204231
@@ -261,8 +288,8 @@ impl GraphWalker {
261288 lanes : & mut [ Option < ( ConnectionType , LaneIndex ) > ] ,
262289 merge_bridge : Option < ( usize , usize ) > ,
263290 commit_lane : usize ,
264- current_snapshot : & [ Option < Chunk > ] ,
265- previous_snapshot : Option < & [ Option < Chunk > ] > ,
291+ current_snapshot : & [ Option < LaneSlot > ] ,
292+ previous_snapshot : Option < & [ Option < LaneSlot > ] > ,
266293 ) {
267294 let Some ( ( source_lane, target_lane) ) = merge_bridge else {
268295 return ;
@@ -349,22 +376,28 @@ impl GraphWalker {
349376 . collect ( )
350377 }
351378
352- /// Checks if tracking a merge commit would be redundant based on current buffer state.
379+ /// Checks if tracking a merge commit would be redundant based on
380+ /// current buffer state: some lane already flows to the target
381+ /// parent without owing a second parent of its own.
353382 fn is_redundant_merge_track (
354383 & self ,
355- target_parent : AliasId ,
384+ target_parent : CommitAlias ,
356385 ) -> bool {
357- self . buffer . current . iter ( ) . flatten ( ) . any ( |commit| {
358- commit. parent_a == Some ( target_parent)
359- && commit. parent_b . is_none ( )
386+ self . buffer . current . iter ( ) . flatten ( ) . any ( |slot| {
387+ matches ! (
388+ slot,
389+ LaneSlot :: Flowing { parent, .. }
390+ | LaneSlot :: Reserved { parent }
391+ if parent. get( ) == target_parent
392+ )
360393 } )
361394 }
362395
363396 /// Determines if a lane should draw an upward-connecting corner.
364397 fn lane_continues_upwards (
365398 target_lane : usize ,
366- current_snapshot : & [ Option < Chunk > ] ,
367- previous_snapshot : Option < & [ Option < Chunk > ] > ,
399+ current_snapshot : & [ Option < LaneSlot > ] ,
400+ previous_snapshot : Option < & [ Option < LaneSlot > ] > ,
368401 ) -> bool {
369402 let exists_in_current =
370403 current_snapshot. get ( target_lane) . is_some ( ) ;
@@ -427,8 +460,8 @@ impl GraphWalker {
427460 pub fn render_row (
428461 & self ,
429462 commit_id : & CommitId ,
430- current_snapshot : & [ Option < Chunk > ] ,
431- previous_snapshot : Option < & [ Option < Chunk > ] > ,
463+ current_snapshot : & [ Option < LaneSlot > ] ,
464+ previous_snapshot : Option < & [ Option < LaneSlot > ] > ,
432465 branch_tips : & HashSet < CommitId > ,
433466 stashes : & HashSet < CommitId > ,
434467 head_id : Option < & CommitId > ,
@@ -467,7 +500,7 @@ impl GraphWalker {
467500 let chunk = chunk_option. as_ref ( ) ?; // Returns None early if the chunk is missing
468501
469502 if commit_alias. is_some ( )
470- && chunk. alias == commit_alias
503+ && chunk. alias ( ) == commit_alias
471504 {
472505 let connection =
473506 Self :: determine_commit_connection (
@@ -523,8 +556,8 @@ impl GraphWalker {
523556
524557 /// Locates the primary lane for the current commit.
525558 fn find_commit_lane (
526- current_snapshot : & [ Option < Chunk > ] ,
527- commit_alias : Option < AliasId > ,
559+ current_snapshot : & [ Option < LaneSlot > ] ,
560+ commit_alias : Option < CommitAlias > ,
528561 ) -> usize {
529562 let Some ( target_alias) = commit_alias else {
530563 return 0 ;
@@ -534,23 +567,23 @@ impl GraphWalker {
534567 . iter ( )
535568 . position ( |chunk_option| {
536569 chunk_option. as_ref ( ) . is_some_and ( |chunk| {
537- chunk. alias == Some ( target_alias)
570+ chunk. alias ( ) == Some ( target_alias)
538571 } )
539572 } )
540573 . unwrap_or ( 0 )
541574 }
542575
543576 /// Computes the span (min, max) between the commit's lane and its second parent's lane.
544577 fn calculate_merge_bridge (
545- current_snapshot : & [ Option < Chunk > ] ,
578+ current_snapshot : & [ Option < LaneSlot > ] ,
546579 commit_lane : usize ,
547- second_parent_alias : AliasId ,
580+ second_parent_alias : CommitAlias ,
548581 ) -> Option < ( usize , usize ) > {
549582 current_snapshot
550583 . iter ( )
551584 . position ( |chunk_option| {
552585 chunk_option. as_ref ( ) . is_some_and ( |chunk| {
553- chunk. parent_a == Some ( second_parent_alias)
586+ chunk. awaits ( ) == Some ( second_parent_alias)
554587 } )
555588 } )
556589 . map ( |target_lane| {
@@ -563,8 +596,8 @@ impl GraphWalker {
563596
564597 /// Identifies lanes that existed in the previous row but terminated before the current row.
565598 fn find_branching_lanes (
566- current_snapshot : & [ Option < Chunk > ] ,
567- previous_snapshot : Option < & [ Option < Chunk > ] > ,
599+ current_snapshot : & [ Option < LaneSlot > ] ,
600+ previous_snapshot : Option < & [ Option < LaneSlot > ] > ,
568601 ) -> Vec < usize > {
569602 let Some ( previous) = previous_snapshot else {
570603 return Vec :: new ( ) ;
@@ -599,21 +632,19 @@ impl GraphWalker {
599632
600633 /// Determines the correct vertical line style for non-commit passthrough lanes.
601634 fn determine_passthrough_connection (
602- chunk : & Chunk ,
635+ chunk : & LaneSlot ,
603636 lane_index : usize ,
604- head_alias : Option < AliasId > ,
637+ head_alias : Option < CommitAlias > ,
605638 ) -> Option < ConnectionType > {
606- let is_orphan =
607- chunk. parent_a . is_none ( ) && chunk. parent_b . is_none ( ) ;
608-
609- if is_orphan {
639+ // A settled lane draws nothing below its commit.
640+ if matches ! ( chunk, LaneSlot :: Settled { .. } ) {
610641 return None ;
611642 }
612643
613644 let is_dotted = lane_index == 0
614645 && head_alias. is_some ( )
615- && ( chunk. parent_a == head_alias
616- || chunk. parent_b == head_alias) ;
646+ && ( chunk. awaits ( ) == head_alias
647+ || chunk. second ( ) == head_alias) ;
617648
618649 if is_dotted {
619650 Some ( ConnectionType :: VerticalDotted )
@@ -814,6 +845,39 @@ mod tests {
814845 ) ;
815846 }
816847
848+ #[ test]
849+ fn skewed_parent_before_child_leaves_no_phantom_lane ( ) {
850+ //1 to 2 must be dropped instead of
851+ // opening a lane that waits for 2 until the end of the walk.
852+ let rows = render ( & [ ( 2 , & [ 3 ] ) , ( 1 , & [ 2 , 3 ] ) , ( 3 , & [ ] ) ] ) ;
853+ assert_eq ! ( rows, vec![ "o" , "┃ o" , "o━┛" ] ) ;
854+ }
855+
856+ #[ test]
857+ fn complete_walk_settles_all_lanes ( ) {
858+ // Both parents of 1 appear before it in the walk (clock
859+ // skew). After a complete walk no lane may still wait on a
860+ // parent, creating the evil phantom lines
861+ let history: & [ ( usize , & [ usize ] ) ] =
862+ & [ ( 2 , & [ 4 ] ) , ( 3 , & [ 4 ] ) , ( 1 , & [ 2 , 3 ] ) , ( 4 , & [ ] ) ] ;
863+
864+ assert_eq ! ( render( history) , vec![ "o" , "┃ o" , "┃ ┃ o" , "o━┛" ] ) ;
865+
866+ let mut walker = GraphWalker :: new ( ) ;
867+ for ( commit, parents) in history {
868+ let parents: Vec < CommitId > =
869+ parents. iter ( ) . map ( |p| id ( * p) ) . collect ( ) ;
870+ walker. process ( id ( * commit) , & parents) ;
871+ }
872+
873+ for slot in walker. buffer . current . iter ( ) . flatten ( ) {
874+ assert ! (
875+ matches!( slot, LaneSlot :: Settled { .. } ) ,
876+ "lane still waiting on a parent after a complete walk: {slot:?}"
877+ ) ;
878+ }
879+ }
880+
817881 #[ test]
818882 fn crossed_lane_keeps_own_color ( ) {
819883 let rows = & [
0 commit comments