Skip to content

Commit 9f21f99

Browse files
committed
Add non-workspace commit limit to deal with graphs
Also provide means to obtain next chunks akin to pages.
1 parent c665e8c commit 9f21f99

File tree

6 files changed

+360
-48
lines changed

6 files changed

+360
-48
lines changed

crates/but-graph/src/init/mod.rs

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,56 @@ mod walk;
2121
pub(super) type PetGraph = petgraph::Graph<Segment, Edge>;
2222

2323
/// Options for use in [`Graph::from_head()`] and [`Graph::from_commit_traversal()`].
24-
#[derive(Default, Debug, Copy, Clone)]
24+
#[derive(Default, Debug, Clone)]
2525
pub struct Options {
2626
/// Associate tag references with commits.
2727
///
2828
/// If `false`, tags are not collected.
2929
pub collect_tags: bool,
30+
/// The maximum number of commits we should traverse outside any workspace *with a target branch*.
31+
/// Workspaces with a target branch automatically have unlimited traversals as they rely on the target
32+
/// branch to eventually stop the traversal.
33+
///
34+
/// If `None`, there is no limit, which typically means that when lacking a workspace, the traversal
35+
/// will end only when no commit is left to traverse.
36+
/// `Some(0)` means nothing is going to be returned.
37+
///
38+
/// Note that this doesn't affect the traversal of integrated commits, which is always stopped once there
39+
/// is nothing interesting left to traverse.
40+
///
41+
/// Also note: This is not a perfectly exact measure, and it's always possible to receive a few more commits
42+
/// than the maximum as for simplicity, we assign each 'split' the same limit, effectively doubling it.
43+
///
44+
/// ### Tip Configuration
45+
///
46+
/// * HEAD - uses the limit
47+
/// * workspaces with target branch - no limit, but auto-stop if workspace is exhausted as everything is integrated.
48+
/// - The target branch: no limit
49+
/// * workspace without target branch - uses the limit
50+
/// * remotes tracking branches - use the limit
51+
pub max_commits_outside_of_workspace: Option<usize>,
52+
/// A list of the last commits of partial segments previously returned that reset the amount of available
53+
/// commits to traverse back to `max_commits_outside_of_workspace`.
54+
/// Imagine it like a gas station that can be chosen to direct where the commit-budge should be spent.
55+
pub max_commits_recharge_location: Vec<gix::ObjectId>,
56+
}
57+
58+
/// Builder
59+
impl Options {
60+
/// Set the maximum amount of commits that each lane in a tip may traverse.
61+
pub fn with_limit(mut self, limit: usize) -> Self {
62+
self.max_commits_outside_of_workspace = Some(limit);
63+
self
64+
}
65+
66+
/// Keep track of commits at which the traversal limit should be reset to the [`limit`](Self::with_limit()).
67+
pub fn with_limit_extension_at(
68+
mut self,
69+
commits: impl IntoIterator<Item = gix::ObjectId>,
70+
) -> Self {
71+
self.max_commits_recharge_location.extend(commits);
72+
self
73+
}
3074
}
3175

3276
/// Lifecycle
@@ -106,7 +150,11 @@ impl Graph {
106150
tip: gix::Id<'_>,
107151
ref_name: impl Into<Option<gix::refs::FullName>>,
108152
meta: &impl RefMetadata,
109-
Options { collect_tags }: Options,
153+
Options {
154+
collect_tags,
155+
max_commits_outside_of_workspace: limit,
156+
mut max_commits_recharge_location,
157+
}: Options,
110158
) -> anyhow::Result<Self> {
111159
// TODO: also traverse (outside)-branches that ought to be in the workspace. That way we have the desired ones
112160
// automatically and just have to find a way to prune the undesired ones.
@@ -168,6 +216,7 @@ impl Graph {
168216
tip.detach(),
169217
tip_flags,
170218
Instruction::CollectCommit { into: current },
219+
limit,
171220
));
172221
}
173222
for (ws_ref, workspace_info) in workspaces {
@@ -198,6 +247,12 @@ impl Graph {
198247
CommitFlags::empty()
199248
};
200249
let mut ws_segment = branch_segment_from_name_and_meta(Some(ws_ref), meta, None)?;
250+
// Drop the limit if we have a target ref
251+
let limit = if workspace_info.target_ref.is_some() {
252+
None
253+
} else {
254+
limit
255+
};
201256
ws_segment.metadata = Some(SegmentMetadata::Workspace(workspace_info));
202257
let ws_segment = graph.insert_root(ws_segment);
203258
// As workspaces typically have integration branches which can help us to stop the traversal,
@@ -210,6 +265,7 @@ impl Graph {
210265
// their status for now.
211266
CommitFlags::NotInRemote | add_flags,
212267
Instruction::CollectCommit { into: ws_segment },
268+
limit,
213269
));
214270
if let Some((target_ref, target_ref_id)) = target {
215271
let target_segment = graph.insert_root(branch_segment_from_name_and_meta(
@@ -223,11 +279,22 @@ impl Graph {
223279
Instruction::CollectCommit {
224280
into: target_segment,
225281
},
282+
/* unlimited traversal for 'negative' commits */
283+
None,
226284
));
227285
}
228286
}
229287

230-
while let Some((id, mut propagated_flags, instruction)) = next.pop_front() {
288+
max_commits_recharge_location.sort();
289+
// Set max-limit so that we compensate for the way this is counted.
290+
let max_limit = limit.map(|l| l + 1);
291+
while let Some((id, mut propagated_flags, instruction, mut limit)) = next.pop_front() {
292+
if max_commits_recharge_location.binary_search(&id).is_ok() {
293+
limit = max_limit;
294+
}
295+
if limit.is_some_and(|l| l == 0) {
296+
continue;
297+
}
231298
let info = find(commit_graph.as_ref(), repo, id, &mut buf)?;
232299
let src_flags = graph[instruction.segment_idx()]
233300
.commits
@@ -363,6 +430,7 @@ impl Graph {
363430
propagated_flags,
364431
segment_idx_for_id,
365432
commit_idx_for_possible_fork,
433+
limit,
366434
);
367435

368436
let refs_at_commit_before_removal = refs_by_id.remove(&id).unwrap_or_default();
@@ -392,6 +460,7 @@ impl Graph {
392460
&configured_remote_tracking_branches,
393461
&target_refs,
394462
meta,
463+
limit,
395464
)?;
396465

397466
prune_integrated_tips(&mut graph.inner, &mut next, &desired_refs);
@@ -442,7 +511,7 @@ impl Instruction {
442511
}
443512
}
444513

445-
type QueueItem = (ObjectId, CommitFlags, Instruction);
514+
type QueueItem = (ObjectId, CommitFlags, Instruction, Option<usize>);
446515

447516
#[derive(Debug)]
448517
pub(crate) struct EdgeOwned {

crates/but-graph/src/init/utils.rs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ pub fn replace_queued_segments(
150150
find: SegmentIndex,
151151
replace: SegmentIndex,
152152
) {
153-
for instruction_to_replace in queue.iter_mut().map(|(_, _, instruction)| instruction) {
153+
for instruction_to_replace in queue.iter_mut().map(|(_, _, instruction, _)| instruction) {
154154
let cmp = instruction_to_replace.segment_idx();
155155
if cmp == find {
156156
*instruction_to_replace = instruction_to_replace.with_replaced_sidx(replace);
@@ -159,7 +159,7 @@ pub fn replace_queued_segments(
159159
}
160160

161161
pub fn swap_queued_segments(queue: &mut VecDeque<QueueItem>, a: SegmentIndex, b: SegmentIndex) {
162-
for instruction_to_replace in queue.iter_mut().map(|(_, _, instruction)| instruction) {
162+
for instruction_to_replace in queue.iter_mut().map(|(_, _, instruction, _)| instruction) {
163163
let cmp = instruction_to_replace.segment_idx();
164164
if cmp == a {
165165
*instruction_to_replace = instruction_to_replace.with_replaced_sidx(b);
@@ -236,28 +236,47 @@ pub fn try_split_non_empty_segment_at_branch(
236236
Ok(Some(segment_below))
237237
}
238238

239+
fn is_exhausted_or_decrement(limit: &mut Option<usize>) -> bool {
240+
*limit = match limit {
241+
Some(limit) => {
242+
if *limit == 0 {
243+
return true;
244+
}
245+
Some(*limit - 1)
246+
}
247+
None => None,
248+
};
249+
false
250+
}
251+
239252
/// Queue the `parent_ids` of the current commit, whose additional information like `current_kind` and `current_index`
240253
/// are used.
254+
/// `limit` is used to determine if the tip is NOT supposed to be dropped, with `0` meaning it's depleted.
241255
pub fn queue_parents(
242256
next: &mut VecDeque<QueueItem>,
243257
parent_ids: &[gix::ObjectId],
244258
flags: CommitFlags,
245259
current_sidx: SegmentIndex,
246260
current_cidx: CommitIndex,
261+
mut limit: Option<usize>,
247262
) {
263+
if is_exhausted_or_decrement(&mut limit) {
264+
return;
265+
}
248266
if parent_ids.len() > 1 {
249267
let instruction = Instruction::ConnectNewSegment {
250268
parent_above: current_sidx,
251269
at_commit: current_cidx,
252270
};
253271
for pid in parent_ids {
254-
next.push_back((*pid, flags, instruction))
272+
next.push_back((*pid, flags, instruction, limit))
255273
}
256274
} else if !parent_ids.is_empty() {
257275
next.push_back((
258276
parent_ids[0],
259277
flags,
260278
Instruction::CollectCommit { into: current_sidx },
279+
limit,
261280
));
262281
} else {
263282
return;
@@ -549,6 +568,7 @@ pub fn propagate_flags_downward(
549568
/// This eager queuing makes sure that the post-processing doesn't have to traverse again when it creates segments
550569
/// that were previously ambiguous.
551570
/// If a remote tracking branch is in `target_refs`, we assume it was already scheduled and won't schedule it again.
571+
/// Note that remotes fully obey the limit.
552572
#[allow(clippy::too_many_arguments)]
553573
pub fn try_queue_remote_tracking_branches(
554574
repo: &gix::Repository,
@@ -559,7 +579,12 @@ pub fn try_queue_remote_tracking_branches(
559579
configured_remote_tracking_branches: &BTreeSet<FullName>,
560580
target_refs: &[gix::refs::FullName],
561581
meta: &impl RefMetadata,
582+
limit: Option<usize>,
562583
) -> anyhow::Result<()> {
584+
if limit.is_some_and(|l| l == 0) {
585+
return Ok(());
586+
}
587+
563588
for rn in refs {
564589
let Some(remote_tracking_branch) = remotes::lookup_remote_tracking_branch_or_deduce_it(
565590
repo,
@@ -593,6 +618,7 @@ pub fn try_queue_remote_tracking_branches(
593618
Instruction::CollectCommit {
594619
into: remote_segment,
595620
},
621+
limit.map(|l| l - 1),
596622
));
597623
}
598624
Ok(())
@@ -614,7 +640,7 @@ pub fn prune_integrated_tips(
614640
if !all_integated {
615641
return;
616642
}
617-
next.retain(|(_id, _flags, instruction)| {
643+
next.retain(|(_id, _flags, instruction, _limit)| {
618644
let sidx = instruction.segment_idx();
619645
let s = &graph[sidx];
620646
let any_segment_ref_is_contained_in_workspace = s

crates/but-graph/tests/fixtures/scenarios.sh

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,30 @@ EOF
165165

166166
)
167167

168+
git init triple-merge
169+
(cd triple-merge
170+
for c in $(seq 5); do
171+
commit "$c"
172+
done
173+
git checkout -b A
174+
git branch B
175+
git branch C
176+
for c in $(seq 3); do
177+
commit "A$c"
178+
done
179+
180+
git checkout B
181+
for c in $(seq 3); do
182+
commit "B$c"
183+
done
184+
185+
git checkout C
186+
for c in $(seq 3); do
187+
commit "C$c"
188+
done
189+
git merge A B
190+
)
191+
168192
mkdir ws
169193
(cd ws
170194
git init single-stack-ambiguous
@@ -355,6 +379,9 @@ EOF
355379
tick
356380
git checkout -b soon-origin-main main
357381
git merge --no-ff A
382+
for c in $(seq 2); do
383+
commit "remote-$c"
384+
done
358385
setup_remote_tracking soon-origin-main main "move"
359386
git checkout gitbutler/workspace
360387
)

0 commit comments

Comments
 (0)