Skip to content

Commit fcd8d1a

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

File tree

6 files changed

+351
-48
lines changed

6 files changed

+351
-48
lines changed

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

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,48 @@ 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+
pub max_commits_outside_of_workspace: Option<usize>,
44+
/// A list of the last commits of partial segments previously returned that reset the amount of available
45+
/// commits to traverse back to `max_commits_outside_of_workspace`.
46+
/// Imagine it like a gas station that can be chosen to direct where the commit-budge should be spent.
47+
pub max_commits_recharge_location: Vec<gix::ObjectId>,
48+
}
49+
50+
/// Builder
51+
impl Options {
52+
/// Set the maximum amount of commits that each lane in a tip may traverse.
53+
pub fn with_limit(mut self, limit: usize) -> Self {
54+
self.max_commits_outside_of_workspace = Some(limit);
55+
self
56+
}
57+
58+
/// Keep track of commits at which the traversal limit should be reset to the [`limit`](Self::with_limit()).
59+
pub fn with_limit_extension_at(
60+
mut self,
61+
commits: impl IntoIterator<Item = gix::ObjectId>,
62+
) -> Self {
63+
self.max_commits_recharge_location.extend(commits);
64+
self
65+
}
3066
}
3167

3268
/// Lifecycle
@@ -106,7 +142,11 @@ impl Graph {
106142
tip: gix::Id<'_>,
107143
ref_name: impl Into<Option<gix::refs::FullName>>,
108144
meta: &impl RefMetadata,
109-
Options { collect_tags }: Options,
145+
Options {
146+
collect_tags,
147+
max_commits_outside_of_workspace: limit,
148+
mut max_commits_recharge_location,
149+
}: Options,
110150
) -> anyhow::Result<Self> {
111151
// TODO: also traverse (outside)-branches that ought to be in the workspace. That way we have the desired ones
112152
// automatically and just have to find a way to prune the undesired ones.
@@ -168,6 +208,7 @@ impl Graph {
168208
tip.detach(),
169209
tip_flags,
170210
Instruction::CollectCommit { into: current },
211+
limit,
171212
));
172213
}
173214
for (ws_ref, workspace_info) in workspaces {
@@ -198,6 +239,12 @@ impl Graph {
198239
CommitFlags::empty()
199240
};
200241
let mut ws_segment = branch_segment_from_name_and_meta(Some(ws_ref), meta, None)?;
242+
// Drop the limit if we have a target ref
243+
let limit = if workspace_info.target_ref.is_some() {
244+
None
245+
} else {
246+
limit
247+
};
201248
ws_segment.metadata = Some(SegmentMetadata::Workspace(workspace_info));
202249
let ws_segment = graph.insert_root(ws_segment);
203250
// As workspaces typically have integration branches which can help us to stop the traversal,
@@ -210,6 +257,7 @@ impl Graph {
210257
// their status for now.
211258
CommitFlags::NotInRemote | add_flags,
212259
Instruction::CollectCommit { into: ws_segment },
260+
limit,
213261
));
214262
if let Some((target_ref, target_ref_id)) = target {
215263
let target_segment = graph.insert_root(branch_segment_from_name_and_meta(
@@ -223,11 +271,22 @@ impl Graph {
223271
Instruction::CollectCommit {
224272
into: target_segment,
225273
},
274+
/* unlimited traversal for 'negative' commits */
275+
None,
226276
));
227277
}
228278
}
229279

230-
while let Some((id, mut propagated_flags, instruction)) = next.pop_front() {
280+
max_commits_recharge_location.sort();
281+
// Set max-limit so that we compensate for the way this is counted.
282+
let max_limit = limit.map(|l| l + 1);
283+
while let Some((id, mut propagated_flags, instruction, mut limit)) = next.pop_front() {
284+
if max_commits_recharge_location.binary_search(&id).is_ok() {
285+
limit = max_limit;
286+
}
287+
if limit.is_some_and(|l| l == 0) {
288+
continue;
289+
}
231290
let info = find(commit_graph.as_ref(), repo, id, &mut buf)?;
232291
let src_flags = graph[instruction.segment_idx()]
233292
.commits
@@ -363,6 +422,7 @@ impl Graph {
363422
propagated_flags,
364423
segment_idx_for_id,
365424
commit_idx_for_possible_fork,
425+
limit,
366426
);
367427

368428
let refs_at_commit_before_removal = refs_by_id.remove(&id).unwrap_or_default();
@@ -392,6 +452,7 @@ impl Graph {
392452
&configured_remote_tracking_branches,
393453
&target_refs,
394454
meta,
455+
limit,
395456
)?;
396457

397458
prune_integrated_tips(&mut graph.inner, &mut next, &desired_refs);
@@ -442,7 +503,7 @@ impl Instruction {
442503
}
443504
}
444505

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

447508
#[derive(Debug)]
448509
pub(crate) struct EdgeOwned {

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

Lines changed: 29 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;
@@ -559,7 +578,12 @@ pub fn try_queue_remote_tracking_branches(
559578
configured_remote_tracking_branches: &BTreeSet<FullName>,
560579
target_refs: &[gix::refs::FullName],
561580
meta: &impl RefMetadata,
581+
limit: Option<usize>,
562582
) -> anyhow::Result<()> {
583+
if limit.is_some_and(|l| l == 0) {
584+
return Ok(());
585+
}
586+
563587
for rn in refs {
564588
let Some(remote_tracking_branch) = remotes::lookup_remote_tracking_branch_or_deduce_it(
565589
repo,
@@ -593,6 +617,7 @@ pub fn try_queue_remote_tracking_branches(
593617
Instruction::CollectCommit {
594618
into: remote_segment,
595619
},
620+
limit.map(|l| l - 1),
596621
));
597622
}
598623
Ok(())
@@ -614,7 +639,7 @@ pub fn prune_integrated_tips(
614639
if !all_integated {
615640
return;
616641
}
617-
next.retain(|(_id, _flags, instruction)| {
642+
next.retain(|(_id, _flags, instruction, _limit)| {
618643
let sidx = instruction.segment_idx();
619644
let s = &graph[sidx];
620645
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)