Skip to content

Commit 397fd9b

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

File tree

25 files changed

+1576
-611
lines changed

25 files changed

+1576
-611
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: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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
}
@@ -702,6 +707,9 @@ fn find_all_desired_stack_refs_in_commit<'a>(
702707
})
703708
}
704709

710+
/// **Warning**: this can make workspace stacks stale, i.e. let them refer to non-existing segments.
711+
/// all accesses from hereon must be done with care. On the other hand, we can ignore
712+
/// that as our workspace is just temporary.
705713
fn delete_anon_if_empty_and_reconnect(graph: &mut Graph, sidx: SegmentIndex) {
706714
let segment = &graph[sidx];
707715
let may_delete = segment.commits.is_empty() && segment.ref_name.is_none();
@@ -717,6 +725,11 @@ fn delete_anon_if_empty_and_reconnect(graph: &mut Graph, sidx: SegmentIndex) {
717725
if outgoing.next().is_some() {
718726
return;
719727
}
728+
729+
tracing::debug!(
730+
?sidx,
731+
"Deleting seemingly isolated and now completely unused segment"
732+
);
720733
// Reconnect
721734
let new_target = first_outgoing.target();
722735
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

crates/but-graph/tests/graph/ref_metadata_legacy.rs

Lines changed: 9 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use but_core::RefMetadata;
22
use but_core::ref_metadata::{StackId, ValueInfo, WorkspaceStack, WorkspaceStackBranch};
33
use but_graph::VirtualBranchesTomlMetadata;
44
use but_testsupport::gix_testtools::tempfile::{TempDir, tempdir};
5-
use std::collections::HashMap;
5+
use but_testsupport::{debug_str, sanitize_uuids_and_timestamps_with_mapping};
66
use std::ops::Deref;
77
use std::path::PathBuf;
88

@@ -39,7 +39,7 @@ fn read_only() -> anyhow::Result<()> {
3939
let (mut store, _tmp) = vb_store_rw("virtual-branches-01")?;
4040
let ws = store.workspace("refs/heads/gitbutler/workspace".try_into()?)?;
4141
assert!(!ws.is_default(), "value read from file");
42-
let (actual, uuids) = sanitize_uuids_and_timestamps(debug_str(&ws.stacks));
42+
let (actual, uuids) = sanitize_uuids_and_timestamps_with_mapping(debug_str(&ws.stacks));
4343
insta::assert_snapshot!(actual, @r#"
4444
[
4545
WorkspaceStack {
@@ -421,7 +421,7 @@ fn create_workspace_and_stacks_with_branches_from_scratch() -> anyhow::Result<()
421421

422422
// Assure `ws` is what we think it should be - a single stack with one branch.
423423
let mut ws = store.workspace(workspace_name.as_ref())?;
424-
let (actual, uuids) = sanitize_uuids_and_timestamps(debug_str(&ws.stacks));
424+
let (actual, uuids) = sanitize_uuids_and_timestamps_with_mapping(debug_str(&ws.stacks));
425425
insta::assert_snapshot!(actual, @r#"
426426
[
427427
WorkspaceStack {
@@ -461,7 +461,7 @@ fn create_workspace_and_stacks_with_branches_from_scratch() -> anyhow::Result<()
461461
.expect("This is the way to add branches");
462462

463463
let mut ws = store.workspace(workspace_name.as_ref())?;
464-
let (actual, uuids) = sanitize_uuids_and_timestamps(debug_str(&ws.stacks));
464+
let (actual, uuids) = sanitize_uuids_and_timestamps_with_mapping(debug_str(&ws.stacks));
465465
insta::assert_snapshot!(actual, @r#"
466466
[
467467
WorkspaceStack {
@@ -491,7 +491,8 @@ fn create_workspace_and_stacks_with_branches_from_scratch() -> anyhow::Result<()
491491
drop(store);
492492

493493
assert!(toml_path.exists(), "file was written due to change");
494-
let (actual, uuids) = sanitize_uuids_and_timestamps(std::fs::read_to_string(&toml_path)?);
494+
let (actual, uuids) =
495+
sanitize_uuids_and_timestamps_with_mapping(std::fs::read_to_string(&toml_path)?);
495496
insta::assert_snapshot!(actual, @r#"
496497
[branch_targets]
497498
@@ -538,7 +539,7 @@ fn create_workspace_and_stacks_with_branches_from_scratch() -> anyhow::Result<()
538539
ws.deref(),
539540
"It's still what it was before - it was persisted"
540541
);
541-
let (actual, uuids) = sanitize_uuids_and_timestamps(debug_str(&new_ws.stacks));
542+
let (actual, uuids) = sanitize_uuids_and_timestamps_with_mapping(debug_str(&new_ws.stacks));
542543
insta::assert_snapshot!(actual, @r#"
543544
[
544545
WorkspaceStack {
@@ -576,7 +577,7 @@ fn create_workspace_and_stacks_with_branches_from_scratch() -> anyhow::Result<()
576577
);
577578
store.set_workspace(&ws)?;
578579
let mut ws = store.workspace(workspace_name.as_ref())?;
579-
let (actual, uuids) = sanitize_uuids_and_timestamps(debug_str(&ws.stacks));
580+
let (actual, uuids) = sanitize_uuids_and_timestamps_with_mapping(debug_str(&ws.stacks));
580581
insta::assert_snapshot!(actual, @r#"
581582
[
582583
WorkspaceStack {
@@ -639,7 +640,7 @@ fn create_workspace_and_stacks_with_branches_from_scratch() -> anyhow::Result<()
639640
store.set_workspace(&ws)?;
640641
let mut ws = store.workspace(ws.as_ref())?;
641642
// Two stacks are present now.
642-
let (actual, uuids) = sanitize_uuids_and_timestamps(debug_str(&ws.stacks));
643+
let (actual, uuids) = sanitize_uuids_and_timestamps_with_mapping(debug_str(&ws.stacks));
643644
insta::assert_snapshot!(actual, @r#"
644645
[
645646
WorkspaceStack {
@@ -960,42 +961,3 @@ fn roundtrip_journey(metadata: &mut impl RefMetadata) -> anyhow::Result<()> {
960961
assert_eq!(metadata.iter().count(), 0, "Nothing is left after deletion");
961962
Ok(())
962963
}
963-
964-
fn sanitize_uuids_and_timestamps(input: String) -> (String, HashMap<String, usize>) {
965-
let uuid_regex = regex::Regex::new(
966-
r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}",
967-
)
968-
.unwrap();
969-
let timestamp_regex = regex::Regex::new(r#""\d{13}""#).unwrap();
970-
971-
let mut uuid_map: HashMap<String, usize> = HashMap::new();
972-
let mut uuid_counter = 1;
973-
974-
let mut timestamp_map: HashMap<String, usize> = HashMap::new();
975-
let mut timestamp_counter = 12_345;
976-
977-
let result = uuid_regex.replace_all(&input, |caps: &regex::Captures| {
978-
let uuid = caps.get(0).unwrap().as_str().to_string();
979-
let entry = uuid_map.entry(uuid).or_insert_with(|| {
980-
let num = uuid_counter;
981-
uuid_counter += 1;
982-
num
983-
});
984-
entry.to_string()
985-
});
986-
let result = timestamp_regex.replace_all(&result, |caps: &regex::Captures| {
987-
let timestamp = caps.get(0).unwrap().as_str().to_string();
988-
let entry = timestamp_map.entry(timestamp).or_insert_with(|| {
989-
let num = timestamp_counter;
990-
timestamp_counter += 1;
991-
num
992-
});
993-
entry.to_string()
994-
});
995-
996-
(result.to_string(), uuid_map)
997-
}
998-
999-
fn debug_str(input: &dyn std::fmt::Debug) -> String {
1000-
format!("{:#?}", input)
1001-
}

0 commit comments

Comments
 (0)