Skip to content

Commit a849165

Browse files
committed
Add support for creating dependent and independent branches in workspaces
With and without workspace commits.
1 parent 775ee12 commit a849165

File tree

26 files changed

+1815
-620
lines changed

26 files changed

+1815
-620
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/but-core/src/ref_metadata.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,22 @@ impl Workspace {
5858
.iter()
5959
.find_map(|stack| stack.branches.iter().find(|b| b.ref_name.as_ref() == name))
6060
}
61+
62+
/// Find the `(stack_idx, branch_idx)` of `name` within our stack branches and return it,
63+
/// for direct access like `ws.stacks[stack_idx].branches[branch_idx]`.
64+
pub fn find_owner_indexes_by_name(
65+
&self,
66+
name: &gix::refs::FullNameRef,
67+
) -> Option<(usize, usize)> {
68+
self.stacks
69+
.iter()
70+
.enumerate()
71+
.find_map(|(stack_idx, stack)| {
72+
stack.branches.iter().enumerate().find_map(|(seg_idx, b)| {
73+
(b.ref_name.as_ref() == name).then_some((stack_idx, seg_idx))
74+
})
75+
})
76+
}
6177
}
6278

6379
/// Metadata about branches, associated with any Git branch.

crates/but-graph/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,4 @@ gix-testtools.workspace = true
3434
insta = "1.43.1"
3535
termtree = "0.5.1"
3636
but-testsupport.workspace = true
37-
regex = "1.11.1"
3837

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ mod post;
2727
pub struct Overlay {
2828
nonoverriding_references: Vec<gix::refs::Reference>,
2929
meta_branches: Vec<(gix::refs::FullName, ref_metadata::Branch)>,
30+
workspace: Option<(gix::refs::FullName, ref_metadata::Workspace)>,
3031
}
3132

3233
pub(super) type PetGraph = petgraph::stable_graph::StableGraph<Segment, Edge>;

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

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::collections::BTreeSet;
99
impl Overlay {
1010
/// Serve the given `refs` from memory, as if they would exist.
1111
/// This is true only, however, if a real reference doesn't exist.
12-
pub fn with_references_non_overriding(
12+
pub fn with_references_if_new(
1313
mut self,
1414
refs: impl IntoIterator<Item = gix::refs::Reference>,
1515
) -> Self {
@@ -19,13 +19,23 @@ impl Overlay {
1919

2020
/// Serve the given `branches` metadata from memory, as if they would exist,
2121
/// possibly overriding metadata of a ref that already exists.
22-
pub fn with_branch_metadata_overriding(
22+
pub fn with_branch_metadata_override(
2323
mut self,
2424
refs: impl IntoIterator<Item = (gix::refs::FullName, ref_metadata::Branch)>,
2525
) -> Self {
2626
self.meta_branches = refs.into_iter().collect();
2727
self
2828
}
29+
30+
/// Serve the given workspace `metadata` from memory, as if they would exist,
31+
/// possibly overriding metadata of a workspace at that place
32+
pub fn with_workspace_metadata_override(
33+
mut self,
34+
metadata: Option<(gix::refs::FullName, ref_metadata::Workspace)>,
35+
) -> Self {
36+
self.workspace = metadata;
37+
self
38+
}
2939
}
3040

3141
impl Overlay {
@@ -40,6 +50,7 @@ impl Overlay {
4050
let Overlay {
4151
nonoverriding_references,
4252
meta_branches,
53+
workspace,
4354
} = self;
4455
(
4556
OverlayRepo {
@@ -49,6 +60,7 @@ impl Overlay {
4960
OverlayMetadata {
5061
inner: meta,
5162
meta_branches,
63+
workspace,
5264
},
5365
)
5466
}
@@ -209,6 +221,7 @@ impl<'repo> OverlayRepo<'repo> {
209221
pub(crate) struct OverlayMetadata<'meta, T> {
210222
inner: &'meta T,
211223
meta_branches: Vec<(gix::refs::FullName, ref_metadata::Branch)>,
224+
workspace: Option<(gix::refs::FullName, ref_metadata::Workspace)>,
212225
}
213226

214227
impl<T> OverlayMetadata<'_, T>
@@ -226,13 +239,30 @@ where
226239
.ok()
227240
.map(|ws| (ref_name, ws))
228241
})
229-
.map(|(ref_name, ws)| (ref_name, (*ws).clone()))
242+
.map(|(ref_name, ws)| {
243+
if let Some((_ws_ref, ws_override)) = self
244+
.workspace
245+
.as_ref()
246+
.filter(|(ws_ref, _ws_data)| *ws_ref == ref_name)
247+
{
248+
(ref_name, ws_override.clone())
249+
} else {
250+
(ref_name, (*ws).clone())
251+
}
252+
})
230253
}
231254

232255
pub fn workspace_opt(
233256
&self,
234257
ref_name: &gix::refs::FullNameRef,
235258
) -> anyhow::Result<Option<ref_metadata::Workspace>> {
259+
if let Some((_ws_ref, ws_meta)) = self
260+
.workspace
261+
.as_ref()
262+
.filter(|(ws_ref, _ws_meta)| ws_ref.as_ref() == ref_name)
263+
{
264+
return Ok(Some(ws_meta.clone()));
265+
}
236266
let opt = self.inner.workspace_opt(ref_name)?;
237267
Ok(opt.map(|ws_data| ws_data.clone()))
238268
}

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

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ impl Graph {
342342
.as_ref()
343343
.into_iter()
344344
.chain(base_segment.commits[0].refs.iter()),
345-
Some((&self.inner, ws_sidx, &candidates)),
345+
Some((&self.inner, ws_sidx, &ws_stacks, &candidates)),
346346
)
347347
.collect();
348348
for refs_for_independent_branches in matching_refs_per_stack {
@@ -473,7 +473,12 @@ impl Graph {
473473
.iter()
474474
.flat_map(|s| s.commits_by_segment.iter().map(|(sidx, _)| *sidx))
475475
}) {
476-
let s = &self.inner[sidx];
476+
// The workspace might be stale by now as we delete empty segments.
477+
// Thus be careful, and ignore non-existing ones - after all our workspace
478+
// is temporary, nothing to worry about.
479+
let Some(s) = self.inner.node_weight(sidx) else {
480+
continue;
481+
};
477482
if s.ref_name.is_some() || s.sibling_segment_id.is_some() {
478483
continue;
479484
}
@@ -671,9 +676,23 @@ impl Graph {
671676
fn find_all_desired_stack_refs_in_commit<'a>(
672677
ws_data: &'a ref_metadata::Workspace,
673678
commit_refs: impl Iterator<Item = &'a gix::refs::FullName> + Clone + 'a,
674-
graph_and_ws_idx_and_candidates: Option<(&'a PetGraph, SegmentIndex, &'a [SegmentIndex])>,
679+
graph_and_ws_idx_and_candidates: Option<(
680+
&'a PetGraph,
681+
SegmentIndex,
682+
&'a [crate::projection::Stack],
683+
&'a [SegmentIndex],
684+
)>,
675685
) -> impl Iterator<Item = Vec<gix::refs::FullName>> + 'a {
676686
ws_data.stacks.iter().filter_map(move |stack| {
687+
if let Some((_, _, ws_stacks, candidates)) = graph_and_ws_idx_and_candidates {
688+
if ws_stacks
689+
.iter()
690+
.filter(|s| !s.segments.iter().any(|s| candidates.contains(&s.id)))
691+
.any(|existing_stack| existing_stack.id == Some(stack.id))
692+
{
693+
return None;
694+
}
695+
}
677696
let matching_refs: Vec<_> = stack
678697
.branches
679698
.iter()
@@ -687,21 +706,26 @@ fn find_all_desired_stack_refs_in_commit<'a>(
687706
// part of an existing stack as new stack.
688707
let is_used_in_existing_stack = graph_and_ws_idx_and_candidates
689708
.zip(stack.branches.first())
690-
.is_some_and(|((graph, ws_idx, candidates), top_segment_name)| {
691-
graph
692-
.neighbors_directed(ws_idx, Direction::Outgoing)
693-
.filter(|sidx| !candidates.contains(sidx))
694-
.any(|stack_sidx| {
695-
graph[stack_sidx].ref_name.as_ref() == Some(&top_segment_name.ref_name)
696-
})
697-
});
709+
.is_some_and(
710+
|((graph, ws_idx, _ws_stacks, candidates), top_segment_name)| {
711+
graph
712+
.neighbors_directed(ws_idx, Direction::Outgoing)
713+
.filter(|sidx| !candidates.contains(sidx))
714+
.any(|stack_sidx| {
715+
graph[stack_sidx].ref_name.as_ref() == Some(&top_segment_name.ref_name)
716+
})
717+
},
718+
);
698719
if is_used_in_existing_stack {
699720
return None;
700721
}
701722
Some(matching_refs)
702723
})
703724
}
704725

726+
/// **Warning**: this can make workspace stacks stale, i.e. let them refer to non-existing segments.
727+
/// all accesses from hereon must be done with care. On the other hand, we can ignore
728+
/// that as our workspace is just temporary.
705729
fn delete_anon_if_empty_and_reconnect(graph: &mut Graph, sidx: SegmentIndex) {
706730
let segment = &graph[sidx];
707731
let may_delete = segment.commits.is_empty() && segment.ref_name.is_none();
@@ -717,6 +741,11 @@ fn delete_anon_if_empty_and_reconnect(graph: &mut Graph, sidx: SegmentIndex) {
717741
if outgoing.next().is_some() {
718742
return;
719743
}
744+
745+
tracing::debug!(
746+
?sidx,
747+
"Deleting seemingly isolated and now completely unused segment"
748+
);
720749
// Reconnect
721750
let new_target = first_outgoing.target();
722751
let incoming: Vec<_> = graph

crates/but-graph/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ mod debug;
224224
pub type CommitIndex = usize;
225225

226226
/// A graph of connected segments that represent a section of the actual commit-graph.
227-
#[derive(Default, Debug)]
227+
#[derive(Default, Debug, Clone)]
228228
pub struct Graph {
229229
inner: init::PetGraph,
230230
/// From where the graph was created. This is useful if one wants to focus on a subset of the graph.

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::{
1010
projection::{Stack, StackCommit, StackCommitFlags, StackSegment},
1111
};
1212
use anyhow::Context;
13-
use bstr::ByteSlice;
13+
use bstr::{BStr, ByteSlice};
1414
use but_core::ref_metadata;
1515
use but_core::ref_metadata::StackId;
1616
use gix::reference::Category;
@@ -130,7 +130,7 @@ impl Workspace<'_> {
130130
.with_context(|| {
131131
format!(
132132
"Couldn't find any stack that contained the branch named '{}'",
133-
name.as_bstr()
133+
name.shorten()
134134
)
135135
})
136136
}
@@ -159,7 +159,7 @@ impl Workspace<'_> {
159159
.with_context(|| {
160160
format!(
161161
"Couldn't find any stack that contained the branch named '{}'",
162-
name.as_bstr()
162+
name.shorten()
163163
)
164164
})
165165
}
@@ -959,7 +959,7 @@ impl Workspace<'_> {
959959
}
960960

961961
/// Query
962-
impl Workspace<'_> {
962+
impl<'graph> Workspace<'graph> {
963963
/// Return `true` if this workspace is managed, meaning we control certain aspects of it.
964964
/// If `false`, we are more conservative and may not support all features.
965965
pub fn has_managed_ref(&self) -> bool {
@@ -978,8 +978,14 @@ impl Workspace<'_> {
978978
/// Return the name of the workspace reference by looking our segment up in `graph`.
979979
/// Note that for managed workspaces, this can be retrieved via [`WorkspaceKind::Managed`].
980980
/// Note that it can be expected to be set on any workspace, but the data would allow it to not be set.
981-
pub fn ref_name<'a>(&self, graph: &'a Graph) -> Option<&'a gix::refs::FullNameRef> {
982-
graph[self.id].ref_name.as_ref().map(|rn| rn.as_ref())
981+
pub fn ref_name(&self) -> Option<&'graph gix::refs::FullNameRef> {
982+
self.graph[self.id].ref_name.as_ref().map(|rn| rn.as_ref())
983+
}
984+
985+
/// Like [`Self::ref_name()`], but return a generic `<anonymous>` name for unnamed workspaces.
986+
pub fn ref_name_display(&self) -> &BStr {
987+
self.ref_name()
988+
.map_or("<anonymous>".into(), |rn| rn.as_bstr())
983989
}
984990
}
985991

crates/but-graph/src/ref_metadata_legacy.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,9 @@ impl RefMetadata for VirtualBranchesTomlMetadata {
240240
stack_id = *branch.stack_id.borrow();
241241
} else if stack_id != *branch.stack_id.borrow() {
242242
bail!(
243-
"Inconsistent stack detected, wanted {:?}, but got {:?}",
244-
stack_id,
245-
branch.stack_id.borrow()
246-
)
243+
"BUG: unexpected situation where branch has stack-id {:?} but is associated with stack {stack_id:?}",
244+
branch.stack_id()
245+
);
247246
}
248247
}
249248

0 commit comments

Comments
 (0)