Skip to content

Commit c5e9003

Browse files
committed
A low-level way to add dependent branches with single-branch support.
This means it's just taking care of the essential pieces, and will need at least one layer on top to be suitable to be called by the various application layers.
1 parent 175bc97 commit c5e9003

File tree

23 files changed

+1067
-196
lines changed

23 files changed

+1067
-196
lines changed

crates/but-core/src/ref_metadata.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,18 @@ pub struct RefInfo {
120120
pub updated_at: Option<gix::date::Time>,
121121
}
122122

123+
/// Mutations
124+
impl RefInfo {
125+
/// Set the `updated_at` field to the current time.
126+
pub fn set_updated_to_now(&mut self) {
127+
self.updated_at = Some(gix::date::Time::now_local_or_utc());
128+
}
129+
/// Set the `created_at` field to the current time.
130+
pub fn set_created_to_now(&mut self) {
131+
self.created_at = Some(gix::date::Time::now_local_or_utc());
132+
}
133+
}
134+
123135
impl std::fmt::Debug for RefInfo {
124136
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125137
let format = gix::date::time::format::ISO8601;

crates/but-graph/src/api.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,19 @@ impl Graph {
8585
})
8686
}
8787

88+
/// Return the segment that is named `name`,
89+
///
90+
/// Note that tags may or may not be included in the graph, depending on how it was created.
91+
///
92+
/// ### Performance
93+
///
94+
/// This is a brute-force search through all nodes and all data in the graph - beware of hot-loop usage.
95+
pub fn named_segment_by_ref_name(&self, name: &gix::refs::FullNameRef) -> Option<&Segment> {
96+
self.inner
97+
.node_weights()
98+
.find(|s| s.ref_name.as_ref().is_some_and(|rn| rn.as_ref() == name))
99+
}
100+
88101
/// Starting a `segment`, ignore all segments that have no commit and return the first commit
89102
/// of a non-empty segment.
90103
///

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

Lines changed: 84 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use anyhow::{Context, bail};
2-
use but_core::RefMetadata;
2+
use but_core::{RefMetadata, ref_metadata};
33
use gix::{
44
hashtable::hash_map::Entry,
55
prelude::{ObjectIdExt, ReferenceExt},
@@ -13,12 +13,22 @@ mod walk;
1313
use walk::*;
1414

1515
pub(crate) mod types;
16+
use crate::init::overlay::{OverlayMetadata, OverlayRepo};
1617
use types::{Goals, Instruction, Limit, Queue};
1718

1819
mod remotes;
1920

21+
mod overlay;
2022
mod post;
2123

24+
/// A way to define information to be served from memory, instead of from the underlying data source, when
25+
/// [initializing](Graph::from_commit_traversal()) the graph.
26+
#[derive(Debug, Default)]
27+
pub struct Overlay {
28+
nonoverriding_references: Vec<gix::refs::Reference>,
29+
meta_branches: Vec<(gix::refs::FullName, ref_metadata::Branch)>,
30+
}
31+
2232
pub(super) type PetGraph = petgraph::stable_graph::StableGraph<Segment, Edge>;
2333

2434
/// Options for use in [`Graph::from_head()`] and [`Graph::from_commit_traversal()`].
@@ -123,9 +133,12 @@ impl Graph {
123133
let (tip, maybe_name) = match head.kind {
124134
gix::head::Kind::Unborn(ref_name) => {
125135
let mut graph = Graph::default();
136+
// It's OK to default-initialise this here as overlays are only used when redoing
137+
// the traversal.
138+
let (_repo, meta) = Overlay::default().into_parts(repo, meta);
126139
graph.insert_root(branch_segment_from_name_and_meta(
127140
Some((ref_name, None)),
128-
meta,
141+
&meta,
129142
None,
130143
)?);
131144
return Ok(graph);
@@ -158,22 +171,23 @@ impl Graph {
158171
};
159172
Ok(graph)
160173
}
161-
/// Produce a minimal-effort representation of the commit-graph reachable from the commit at `tip` such the returned instance
162-
/// can represent everything that's observed, without loosing information.
174+
/// Produce a minimal but usable representation of the commit-graph reachable from the commit at `tip` such the returned instance
175+
/// can represent everything that's observed, without losing information.
163176
/// `ref_name` is assumed to point to `tip` if given.
164177
///
165-
/// `meta` is used to learn more about the encountered references.
178+
/// `meta` is used to learn more about the encountered references, and `options` is used for additional configuration.
166179
///
167180
/// ### Features
168181
///
169182
/// * discover a Workspace on the fly based on `meta`-data.
170183
/// * support the notion of a branch to integrate with, the *target*
171184
/// - *target* branches consist of a local and remote tracking branch, and one can be ahead of the other.
172185
/// - workspaces are relative to the local tracking branch of the target.
186+
/// - options contain an [`extra_target_commit_id`](Options::extra_target_commit_id) for an additional target location.
173187
/// * remote tracking branches are seen in relation to their branches.
174188
/// * the graph of segments assigns each reachable commit to exactly one segment
175189
/// * one can use [`petgraph::algo`] and [`petgraph::visit`]
176-
/// - It maintains information about the intended connections, so modifications afterwards will show
190+
/// - It maintains information about the intended connections, so modifications afterward will show
177191
/// in debugging output if edges are now in violation of this constraint.
178192
///
179193
/// ### Rules
@@ -183,39 +197,54 @@ impl Graph {
183197
/// Change the rules as you see fit to accomplish this.
184198
///
185199
/// * a commit can be governed by multiple workspaces
186-
/// * as workspaces and entrypoints "grow" together, we don't know anything about workspaces until the every end,
187-
/// or when two streams touch. This means we can't make decisions based on [flags](CommitFlags) until the traversal
200+
/// * as workspaces and entry-points "grow" together, we don't know anything about workspaces until the very end,
201+
/// or when two partitions of commits touch.
202+
/// This means we can't make decisions based on [flags](CommitFlags) until the traversal
188203
/// is finished.
189-
/// * an entrypoint always causes the start of a segment.
190-
/// * Segments are always named if their first commit has a single local branch pointing to it.
191-
/// * Anonymous segments are created if there are more than one local branches pointing to it.
204+
/// * an entrypoint always causes the start of a [`Segment`].
205+
/// * Segments are always named if their first commit has a single local branch pointing to it, or a branch that
206+
/// otherwise can be disambiguated.
207+
/// * Anonymous segments are created if their name is ambiguous.
192208
/// * Anonymous segments are created if another segment connects to a commit that it contains that is not the first one.
193-
/// - This means, all connections go from the last commit in a segment to the first commit in another segment.
209+
/// - This means, all connections go *from the last commit in a segment to the first commit in another segment*.
194210
/// * Segments stored in the *workspace metadata* are used/relevant only if they are backed by an existing branch.
195211
/// * Remote tracking branches are picked up during traversal for any ref that we reached through traversal.
196212
/// - This implies that remotes aren't relevant for segments added during post-processing, which would typically
197213
/// be empty anyway.
198214
/// - Remotes never take commits that are already owned.
199-
/// * The traversal is cut short when there is only tips which are integrated, even though named segments that are
200-
/// supposed to be in the workspace will be fully traversed (implying they will stop at the first anon segment
201-
/// as will happen at merge commits).
215+
/// * The traversal is cut short when there is only tips which are integrated
202216
/// * The traversal is always as long as it needs to be to fully reconcile possibly disjoint branches, despite
203217
/// this sometimes costing some time when the remote is far ahead in a huge repository.
204-
// TODO: review the docs!
205218
#[instrument(skip(meta, ref_name), err(Debug))]
206219
pub fn from_commit_traversal(
207220
tip: gix::Id<'_>,
208221
ref_name: impl Into<Option<gix::refs::FullName>>,
209222
meta: &impl RefMetadata,
210-
Options {
223+
options: Options,
224+
) -> anyhow::Result<Self> {
225+
let (repo, meta) = Overlay::default().into_parts(tip.repo, meta);
226+
Graph::from_commit_traversal_inner(tip.detach(), &repo, ref_name, &meta, options)
227+
}
228+
229+
fn from_commit_traversal_inner<T: RefMetadata>(
230+
tip: gix::ObjectId,
231+
repo: &OverlayRepo<'_>,
232+
ref_name: impl Into<Option<gix::refs::FullName>>,
233+
meta: &OverlayMetadata<'_, T>,
234+
options: Options,
235+
) -> anyhow::Result<Self> {
236+
let mut graph = Graph {
237+
options: options.clone(),
238+
..Graph::default()
239+
};
240+
let Options {
211241
collect_tags,
212242
extra_target_commit_id,
213243
commits_limit_hint: limit,
214244
commits_limit_recharge_location: mut max_commits_recharge_location,
215245
hard_limit,
216-
}: Options,
217-
) -> anyhow::Result<Self> {
218-
let repo = tip.repo;
246+
} = options;
247+
219248
let max_limit = Limit::new(limit);
220249
// TODO: also traverse (outside)-branches that ought to be in the workspace. That way we have the desired ones
221250
// automatically and just have to find a way to prune the undesired ones.
@@ -231,12 +260,10 @@ impl Graph {
231260
}
232261
let commit_graph = repo.commit_graph_if_enabled()?;
233262
let mut buf = Vec::new();
234-
let mut graph = Graph::default();
235263

236264
let configured_remote_tracking_branches =
237265
remotes::configured_remote_tracking_branches(repo)?;
238-
let refs_by_id = collect_ref_mapping_by_prefix(
239-
repo,
266+
let refs_by_id = repo.collect_ref_mapping_by_prefix(
240267
std::iter::once("refs/heads/").chain(if collect_tags {
241268
Some("refs/tags/")
242269
} else {
@@ -250,7 +277,7 @@ impl Graph {
250277
// The tip transports itself.
251278
let tip_flags = CommitFlags::NotInRemote
252279
| goals
253-
.flag_for(tip.detach())
280+
.flag_for(tip)
254281
.expect("we more than one bitflags for this");
255282

256283
let target_symbolic_remote_names = {
@@ -283,10 +310,10 @@ impl Graph {
283310
let current = graph.insert_root(branch_segment_from_name_and_meta(
284311
ref_name.clone().map(|rn| (rn, None)),
285312
meta,
286-
Some((&ctx.refs_by_id, tip.detach())),
313+
Some((&ctx.refs_by_id, tip)),
287314
)?);
288315
_ = next.push_back_exhausted((
289-
tip.detach(),
316+
tip,
290317
tip_flags,
291318
Instruction::CollectCommit { into: current },
292319
max_limit,
@@ -326,7 +353,7 @@ impl Graph {
326353
} else {
327354
(
328355
CommitFlags::empty(),
329-
max_limit.with_indirect_goal(tip.detach(), &mut goals),
356+
max_limit.with_indirect_goal(tip, &mut goals),
330357
)
331358
};
332359
let mut ws_segment =
@@ -369,10 +396,10 @@ impl Graph {
369396
CommitFlags::NotInRemote | goal,
370397
Instruction::CollectCommit { into: local_sidx },
371398
max_limit
372-
.with_indirect_goal(tip.detach(), &mut goals)
399+
.with_indirect_goal(tip, &mut goals)
373400
.without_allowance(),
374401
));
375-
next.add_goal_to(tip.detach(), goal);
402+
next.add_goal_to(tip, goal);
376403
(Some(local_sidx), goal)
377404
} else {
378405
(None, CommitFlags::empty())
@@ -386,7 +413,7 @@ impl Graph {
386413
// Once the goal was found, be done immediately,
387414
// we are not interested in these.
388415
max_limit
389-
.with_indirect_goal(tip.detach(), &mut goals)
416+
.with_indirect_goal(tip, &mut goals)
390417
.additional_goal(local_goal)
391418
.without_allowance(),
392419
));
@@ -416,7 +443,7 @@ impl Graph {
416443
into: extra_target_sidx,
417444
},
418445
max_limit
419-
.with_indirect_goal(tip.detach(), &mut goals)
446+
.with_indirect_goal(tip, &mut goals)
420447
.without_allowance(),
421448
));
422449
extra_target_sidx
@@ -468,7 +495,7 @@ impl Graph {
468495
if max_commits_recharge_location.binary_search(&id).is_ok() {
469496
limit.set_but_keep_goal(max_limit);
470497
}
471-
let info = find(commit_graph.as_ref(), repo, id, &mut buf)?;
498+
let info = find(commit_graph.as_ref(), repo.for_find_only(), id, &mut buf)?;
472499
let src_flags = graph[instruction.segment_idx()]
473500
.commits
474501
.last()
@@ -566,7 +593,7 @@ impl Graph {
566593
limit,
567594
);
568595
if hard_limit_hit {
569-
return graph.post_processed(meta, tip.detach(), ctx.with_hard_limit());
596+
return graph.post_processed(meta, tip, ctx.with_hard_limit());
570597
}
571598

572599
segment.commits.push(
@@ -587,14 +614,36 @@ impl Graph {
587614

588615
for item in remote_items {
589616
if next.push_back_exhausted(item) {
590-
return graph.post_processed(meta, tip.detach(), ctx.with_hard_limit());
617+
return graph.post_processed(meta, tip, ctx.with_hard_limit());
591618
}
592619
}
593620

594621
prune_integrated_tips(&mut graph, &mut next);
595622
}
596623

597-
graph.post_processed(meta, tip.detach(), ctx)
624+
graph.post_processed(meta, tip, ctx)
625+
}
626+
627+
/// Repeat the traversal that generated this graph using `repo` and `meta`, but allow to set an in-memory
628+
/// `overlay` to amend the data available from `repo` and `meta`.
629+
/// This way, one can see this graph as it will be in the future once the changes to `repo` and `meta` are actually made.
630+
pub fn redo_traversal_with_overlay(
631+
&self,
632+
repo: &gix::Repository,
633+
meta: &impl RefMetadata,
634+
overlay: Overlay,
635+
) -> anyhow::Result<Self> {
636+
let (repo, meta) = overlay.into_parts(repo, meta);
637+
let tip_sidx = self
638+
.entrypoint
639+
.context("BUG: entrypoint must always be set")?
640+
.0;
641+
let tip = self
642+
.tip_skip_empty(tip_sidx)
643+
.context("BUG: entrypoint must eventually point to a commit")?
644+
.id;
645+
let ref_name = self[tip_sidx].ref_name.clone();
646+
Graph::from_commit_traversal_inner(tip, &repo, ref_name, &meta, self.options.clone())
598647
}
599648
}
600649

0 commit comments

Comments
 (0)