Skip to content

Commit 85830b7

Browse files
committed
Make sure the graph will look the same even if entrypoints are used.
1 parent a1e9e47 commit 85830b7

File tree

11 files changed

+366
-86
lines changed

11 files changed

+366
-86
lines changed

crates/but-graph/src/api.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ use std::ops::{Deref, Index, IndexMut};
1111
/// Mutation
1212
impl Graph {
1313
/// Insert `segment` to the graph so that it's not connected to any other segment, and return its index.
14+
/// Note that as a side effect, the [entrypoint](Self::lookup_entrypoint()) will also be set if it's not
15+
/// set yet.
1416
pub fn insert_root(&mut self, segment: Segment) -> SegmentIndex {
1517
let index = self.inner.add_node(segment);
1618
self.inner[index].id = index;

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,10 @@ impl Graph {
242242
meta: &OverlayMetadata<'_, T>,
243243
options: Options,
244244
) -> anyhow::Result<Self> {
245+
let ref_name = ref_name.into();
245246
let mut graph = Graph {
246247
options: options.clone(),
248+
entrypoint_ref: ref_name.clone(),
247249
..Graph::default()
248250
};
249251
let Options {
@@ -256,7 +258,6 @@ impl Graph {
256258
} = options;
257259

258260
let max_limit = Limit::new(limit);
259-
let ref_name = ref_name.into();
260261
if ref_name
261262
.as_ref()
262263
.is_some_and(|name| name.category() == Some(Category::RemoteBranch))
@@ -271,6 +272,8 @@ impl Graph {
271272

272273
let configured_remote_tracking_branches =
273274
remotes::configured_remote_tracking_branches(repo)?;
275+
let (workspaces, target_refs) =
276+
obtain_workspace_infos(repo, ref_name.as_ref().map(|rn| rn.as_ref()), meta)?;
274277
let refs_by_id = repo.collect_ref_mapping_by_prefix(
275278
[
276279
"refs/heads/",
@@ -287,9 +290,11 @@ impl Graph {
287290
} else {
288291
None
289292
}),
293+
&workspaces
294+
.iter()
295+
.map(|(_, ref_name, _)| ref_name.as_ref())
296+
.collect::<Vec<_>>(),
290297
)?;
291-
let (workspaces, target_refs) =
292-
obtain_workspace_infos(repo, ref_name.as_ref().map(|rn| rn.as_ref()), meta)?;
293298
let mut seen = gix::revwalk::graph::IdMap::<SegmentIndex>::default();
294299
let mut goals = Goals::default();
295300
// The tip transports itself.
@@ -333,7 +338,7 @@ impl Graph {
333338
};
334339
if tip_is_not_workspace_commit {
335340
let current = graph.insert_root(branch_segment_from_name_and_meta(
336-
ref_name.clone().map(|rn| (rn, None)),
341+
None,
337342
meta,
338343
Some((&ctx.refs_by_id, tip)),
339344
)?);

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use crate::init::walk::RefsById;
22
use crate::init::{Entrypoint, Overlay};
3-
use crate::is_workspace_ref_name;
43
use but_core::{RefMetadata, ref_metadata};
54
use gix::prelude::ReferenceExt;
65
use std::borrow::Cow;
@@ -173,15 +172,17 @@ impl<'repo> OverlayRepo<'repo> {
173172
.upstream_branch_and_remote_for_tracking_branch(name)?)
174173
}
175174

176-
// Create a mapping of all heads to the object ids they point to.
175+
/// Create a mapping of all heads to the object ids they point to.
176+
/// `workspace_ref_names` is the names of all known workspace references.
177177
pub fn collect_ref_mapping_by_prefix<'a>(
178178
&self,
179179
prefixes: impl Iterator<Item = &'a str>,
180+
workspace_ref_names: &[&gix::refs::FullNameRef],
180181
) -> anyhow::Result<RefsById> {
181182
let mut seen = (!self.nonoverriding_references.is_empty()).then(BTreeSet::new);
182183
let mut ref_filter =
183184
|r: gix::Reference<'_>| -> Option<(gix::ObjectId, gix::refs::FullName)> {
184-
if is_workspace_ref_name(r.name()) {
185+
if workspace_ref_names.contains(&r.name()) {
185186
return None;
186187
}
187188
let id = r.try_id()?;

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

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ impl Graph {
7777
// the foundation for figuring out our workspaces more easily.
7878
self.workspace_upgrades(meta, repo)?;
7979

80+
// Point entrypoint to the right spot after all the virtual branches were added.
81+
self.set_entrypoint_to_ref_name(meta)?;
82+
8083
// However, when it comes to using remotes to disambiguate, it's better to
8184
// *not* do that before workspaces are sorted as it might incorrectly place
8285
// a segment on top of another one, setting a first-second relationship that isn't
@@ -95,6 +98,108 @@ impl Graph {
9598
Ok(self)
9699
}
97100

101+
/// After everything, assure the entrypoint still points to a segment with the correct ref-name,
102+
/// if one was given when starting the traversal.
103+
/// If not, try to find a segment with the right ref-name.
104+
///
105+
/// *This is the brute-force way of doing it, instead of ensuring that the workspace upgrade functions
106+
/// that create independent and dependent branches keep everything up-to-date at all times.
107+
fn set_entrypoint_to_ref_name<T: RefMetadata>(
108+
&mut self,
109+
meta: &OverlayMetadata<'_, T>,
110+
) -> anyhow::Result<()> {
111+
let Some(((ep_sidx, _commit_idx), desired_ref_name)) =
112+
self.entrypoint.zip(self.entrypoint_ref.clone())
113+
else {
114+
return Ok(());
115+
};
116+
117+
let ep_segment_is_correctly_named = self[ep_sidx]
118+
.ref_name
119+
.as_ref()
120+
.is_some_and(|rn| rn == &desired_ref_name);
121+
if ep_segment_is_correctly_named {
122+
return Ok(());
123+
}
124+
125+
let (sidx_with_desired_name, sidx_with_first_commit_with_desired_name) = self
126+
.node_weights()
127+
.find_map(|s| {
128+
s.ref_name
129+
.as_ref()
130+
.is_some_and(|rn| rn == &desired_ref_name)
131+
.then_some((Some(s.id), None))
132+
.or_else(|| {
133+
s.commits.first().and_then(|c| {
134+
c.refs
135+
.iter()
136+
.position(|rn| rn == &desired_ref_name)
137+
.map(|pos| (None, Some((s.id, pos))))
138+
})
139+
})
140+
})
141+
.unwrap_or_default();
142+
if let Some(new_ep_sidx) = sidx_with_desired_name {
143+
let assume_tip_is_not_available_in_segment_anymore = None;
144+
self.entrypoint = Some((new_ep_sidx, assume_tip_is_not_available_in_segment_anymore));
145+
} else if let Some((new_ep_sidx, ref_idx)) = sidx_with_first_commit_with_desired_name {
146+
let s = &mut self.inner[new_ep_sidx];
147+
let desired_ref_name = s
148+
.commits
149+
.first_mut()
150+
.context("BUG: we have ref_idx because the first commit was checked")?
151+
.refs
152+
.remove(ref_idx);
153+
if s.ref_name.is_some() {
154+
// ref-name is known to not be the desired one, and the first commit of this segment has the name
155+
// we seek. For that, we now create a new
156+
let new_ep_sidx_first_commit_idx = s.commits.first().map(|_| 0);
157+
let incoming_edges = collect_edges_at_commit_reverse_order(
158+
&self.inner,
159+
(new_ep_sidx, new_ep_sidx_first_commit_idx),
160+
Direction::Incoming,
161+
);
162+
let entrypoint_segment =
163+
branch_segment_from_name_and_meta(Some((desired_ref_name, None)), meta, None)?;
164+
let entrypoint_sidx = self.insert_root(entrypoint_segment);
165+
self.connect_segments(
166+
entrypoint_sidx,
167+
None,
168+
new_ep_sidx,
169+
new_ep_sidx_first_commit_idx,
170+
);
171+
for edge in incoming_edges {
172+
self.inner.add_edge(
173+
edge.source,
174+
entrypoint_sidx,
175+
Edge {
176+
src: edge.weight.src,
177+
src_id: edge.weight.src_id,
178+
dst: None,
179+
dst_id: None,
180+
},
181+
);
182+
self.inner.remove_edge(edge.id);
183+
}
184+
self.entrypoint = Some((entrypoint_sidx, None));
185+
} else {
186+
self.entrypoint = Some((new_ep_sidx, Some(0)));
187+
// It's really important to get the name, as the HEAD is pointing to this segment.
188+
// So find the segment with the ambiguous ref we desire, and rewrite it to be non-ambiguous.
189+
// The reason we wait till now is to not disturb the workspace upgrades, which act differently
190+
// if they already have a named segment.
191+
// Note that any ref type works here, we just do as we are told even though tags for instance aren't usually
192+
// pointed to by HEAD.
193+
s.ref_name = Some(desired_ref_name);
194+
}
195+
} else {
196+
tracing::warn!(
197+
"Couldn't find any segment that was named after the entrypoint ref name or contained its name '{desired_ref_name}'",
198+
);
199+
};
200+
Ok(())
201+
}
202+
98203
/// This is a post-process as only in the end we are sure what is a remote commit.
99204
/// On remote commits, we want to further segment remote tracking segments to avoid picking
100205
/// up too many remote commits later.
@@ -570,7 +675,7 @@ impl Graph {
570675
s.ref_name = Some(name);
571676
s.metadata = md;
572677
}
573-
reconnect_edges(
678+
reconnect_outgoing_edges(
574679
self,
575680
edges_from_segment_above,
576681
(new_sidx_above_base_sidx, None),
@@ -675,6 +780,11 @@ impl Graph {
675780
/// Name ambiguous segments if they are reachable by remote tracking branch and
676781
/// if the first commit has (unambiguously) the matching local tracking branch.
677782
/// Also, link up all remote segments with their local ones, and vice versa.
783+
///
784+
/// Additionally, restore possibly broken linkage to their siblings
785+
/// (from remote to local tracking branch), as this connection might have been destroyed by
786+
/// the insertion of empty segments. Again, instead of making that smarter, we fix it up here
787+
/// because it's simpler.
678788
fn improve_remote_segments(
679789
&mut self,
680790
repo: &OverlayRepo<'_>,
@@ -1125,11 +1235,11 @@ fn reconnect_outgoing(
11251235
(source_sidx, Some(source_cidx)),
11261236
Direction::Outgoing,
11271237
);
1128-
reconnect_edges(graph, edges, (target_sidx, Some(target_cidx)))
1238+
reconnect_outgoing_edges(graph, edges, (target_sidx, Some(target_cidx)))
11291239
}
11301240

11311241
/// Delete all `edges` and recreate the edges with `target` as new source.
1132-
fn reconnect_edges(
1242+
fn reconnect_outgoing_edges(
11331243
graph: &mut PetGraph,
11341244
edges: Vec<EdgeOwned>,
11351245
(target_sidx, target_first_commit_id): (SegmentIndex, Option<gix::ObjectId>),

crates/but-graph/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,10 @@ pub struct Graph {
233233
/// The [`CommitIndex`] is empty if the entry point is an empty segment, one that is supposed to receive
234234
/// commits later.
235235
entrypoint: Option<(SegmentIndex, Option<CommitIndex>)>,
236+
/// The ref_name used when starting the graph traversal. It is set to help assure that the entrypoint stays
237+
/// on the correctly named segment, knowing that the post-process may alter segments quite substantially
238+
/// when crating independent and dependent branches.
239+
entrypoint_ref: Option<gix::refs::FullName>,
236240
/// The segment index of the extra target as provided for traversal.
237241
extra_target: Option<SegmentIndex>,
238242
/// It's `true` only if we have stopped the traversal due to a hard limit.

crates/but-graph/src/projection/workspace.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,6 @@ impl Graph {
519519
if ws.has_managed_ref() {
520520
let (lowest_base, lowest_base_sidx) =
521521
ws_lower_bound.map_or((None, None), |(base, sidx)| (Some(base), Some(sidx)));
522-
// dbg!(lowest_base, lowest_base_sidx);
523522
for stack_top_sidx in self
524523
.inner
525524
.neighbors_directed(ws_tip_segment.id, Direction::Outgoing)

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,13 @@ EOF
738738
git checkout gitbutler/workspace
739739
)
740740

741+
cp -R no-target-without-ws-commit-ambiguous no-target-without-ws-commit-ambiguous-with-remotes
742+
(cd no-target-without-ws-commit-ambiguous-with-remotes
743+
add_main_remote_setup
744+
remote_tracking_caught_up A
745+
remote_tracking_caught_up B
746+
)
747+
741748
git init no-target-with-ws-commit
742749
(cd no-target-with-ws-commit
743750
commit init

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ fn unborn() -> anyhow::Result<()> {
3434
None,
3535
),
3636
),
37+
entrypoint_ref: None,
3738
extra_target: None,
3839
hard_limit_hit: false,
3940
options: Options {
@@ -138,6 +139,7 @@ fn detached() -> anyhow::Result<()> {
138139
),
139140
),
140141
),
142+
entrypoint_ref: None,
141143
extra_target: None,
142144
hard_limit_hit: false,
143145
options: Options {

0 commit comments

Comments
 (0)