Skip to content

Commit 905e5ee

Browse files
committed
refactor walker for LaneSlot, add processed set
1 parent 17cf91d commit 905e5ee

1 file changed

Lines changed: 111 additions & 47 deletions

File tree

asyncgit/src/graph/walker.rs

Lines changed: 111 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use super::buffer::Buffer;
2-
use super::chunk::{Chunk, Markers};
2+
use super::chunk::LaneSlot;
33
use super::oids::GraphOids;
44
use super::{
5-
AliasId, ConnectionType, GraphRow, LaneIndex, MAX_LANE_COLORS,
5+
CommitAlias, ConnectionType, GraphRow, LaneIndex, UnwalkedAlias,
6+
MAX_LANE_COLORS,
67
};
78
use crate::sync::CommitId;
89
use core::cmp::Ordering;
@@ -145,10 +146,14 @@ fn overlay_cell(
145146
pub 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

154159
impl 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

Comments
 (0)