Skip to content

Commit c052677

Browse files
committed
Perform an actual workspace commit merge
1 parent 39470f3 commit c052677

File tree

13 files changed

+644
-272
lines changed

13 files changed

+644
-272
lines changed

crates/but-core/src/ref_metadata.rs

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,22 +38,46 @@ pub struct Workspace {
3838

3939
/// Mutations
4040
impl Workspace {
41-
/// Insert `branch` as new stack if it's not yet contained in the workspace, and return
42-
/// Returns `true` if the ref was newly added, or false if it already existed.
43-
pub fn add_new_stack_if_not_present(&mut self, branch: &FullNameRef) -> bool {
41+
/// Insert `branch` as new stack if it's not yet contained in the workspace and if `order` is not `None` or push
42+
/// it to the end of the stack list.
43+
/// Note that `order` is only relevant at insertion time.
44+
/// Returns `true` if the ref was newly added, or `false` if it already existed.
45+
pub fn add_or_insert_new_stack_if_not_present(
46+
&mut self,
47+
branch: &FullNameRef,
48+
order: Option<usize>,
49+
) -> bool {
4450
if self.contains_ref(branch) {
4551
return false;
4652
};
4753

48-
self.stacks.push(WorkspaceStack {
54+
let stack = WorkspaceStack {
4955
id: StackId::generate(),
5056
branches: vec![WorkspaceStackBranch {
5157
ref_name: branch.to_owned(),
5258
archived: false,
5359
}],
54-
});
60+
};
61+
match order.map(|idx| idx.min(self.stacks.len())) {
62+
None => {
63+
self.stacks.push(stack);
64+
}
65+
Some(existing_index) => {
66+
self.stacks.insert(existing_index, stack);
67+
}
68+
}
5569
true
5670
}
71+
}
72+
73+
/// Access
74+
impl Workspace {
75+
/// Return the names of the tips of all stacks in the workspace.
76+
pub fn stack_names(&self) -> impl Iterator<Item = &gix::refs::FullNameRef> {
77+
self.stacks
78+
.iter()
79+
.filter_map(|s| s.ref_name().map(|rn| rn.as_ref()))
80+
}
5781

5882
/// Return `true` if the branch with `name` is the workspace target or the targets local tracking branch,
5983
/// using `repo` for the lookup of the local tracking branch.
@@ -77,10 +101,7 @@ impl Workspace {
77101
Ok(local_tracking_branch.as_ref() == name)
78102
}
79103
}
80-
}
81104

82-
/// Access
83-
impl Workspace {
84105
/// Return `true` if `name` is a reference mentioned in our [stacks](Workspace::stacks).
85106
pub fn contains_ref(&self, name: &gix::refs::FullNameRef) -> bool {
86107
self.stacks

crates/but-core/tests/core/ref_metadata.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@ mod workspace {
77
assert_eq!(ws.stacks.len(), 0);
88

99
let a_ref = r("refs/heads/A");
10-
assert!(ws.add_new_stack_if_not_present(a_ref));
11-
assert!(!ws.add_new_stack_if_not_present(a_ref));
10+
assert!(ws.add_or_insert_new_stack_if_not_present(a_ref, Some(100)));
11+
assert!(!ws.add_or_insert_new_stack_if_not_present(a_ref, Some(200)));
1212
assert_eq!(ws.stacks.len(), 1);
1313

1414
let b_ref = r("refs/heads/B");
15-
assert!(ws.add_new_stack_if_not_present(b_ref));
16-
assert_eq!(ws.stacks.len(), 2);
15+
assert!(ws.add_or_insert_new_stack_if_not_present(b_ref, Some(0)));
16+
assert_eq!(ws.stack_names().collect::<Vec<_>>(), [b_ref, a_ref]);
17+
18+
let c_ref = r("refs/heads/C");
19+
assert!(ws.add_or_insert_new_stack_if_not_present(c_ref, None));
20+
assert_eq!(ws.stack_names().collect::<Vec<_>>(), [b_ref, a_ref, c_ref]);
1721
}
1822

1923
fn r(name: &str) -> &gix::refs::FullNameRef {

crates/but-graph/src/api.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,53 @@ impl Graph {
6363
}
6464
}
6565

66+
/// Merge-base computation
67+
impl Graph {
68+
/// Compute the lowest merge-base between two segments.
69+
/// Such a merge-base is reachable from all possible paths from `a` and `b`.
70+
///
71+
/// The segment representing the merge-base is expected to not be empty, as its first commit
72+
/// is usually what one is interested in.
73+
// TODO: should be multi, with extra segments as third parameter
74+
// TODO: actually find the lowest merge-base, right now it just finds the first merge-base, but that's not
75+
// the lowest.
76+
pub fn first_merge_base(&self, a: SegmentIndex, b: SegmentIndex) -> Option<SegmentIndex> {
77+
// TODO(perf): improve this by allowing to set bitflags on the segments themselves, to allow
78+
// marking them accordingly, just like Git does.
79+
// Right now we 'emulate' bitflags on pre-allocated data with two data sets, expensive
80+
// in comparison.
81+
// And yes, let's avoid `gix::Repository::merge_base` as we have free
82+
// generation numbers here and can avoid work duplication.
83+
let mut segments_reachable_by_b = BTreeSet::new();
84+
self.visit_all_segments_including_start_until(b, Direction::Outgoing, |s| {
85+
segments_reachable_by_b.insert(s.id);
86+
// Collect everything, keep it simple.
87+
// This is fast* as completely in memory.
88+
// *means slow compared to an array traversal with memory locality.
89+
false
90+
});
91+
92+
let mut candidate = None;
93+
self.visit_all_segments_including_start_until(a, Direction::Outgoing, |s| {
94+
if candidate.is_some() {
95+
return true;
96+
}
97+
let prune = segments_reachable_by_b.contains(&s.id);
98+
if prune {
99+
candidate = Some(s.id);
100+
}
101+
prune
102+
});
103+
if candidate.is_none() {
104+
// TODO: improve this - workspaces shouldn't be like this but if they are, do we deal with it well?
105+
tracing::warn!(
106+
"Couldn't find merge-base between segments {a:?} and {b:?} - this might lead to unexpected results"
107+
)
108+
}
109+
candidate
110+
}
111+
}
112+
66113
/// Query
67114
/// ‼️Useful only if one knows the graph traversal was started where one expects, or else the graph may be partial.
68115
impl Graph {
@@ -130,6 +177,11 @@ impl Graph {
130177
sidx_with_commits.and_then(|sidx| self[sidx].commits.first())
131178
}
132179

180+
/// The first commit reachable by skipping over empty segments starting at the entrypoint segment.
181+
pub fn entrypoint_commit(&self) -> Option<&Commit> {
182+
self.tip_skip_empty(self.entrypoint?.0)
183+
}
184+
133185
/// Visit the ancestry of `start` along the first parents, itself excluded, until `stop` returns `true`.
134186
/// Also return the segment that we stopped at.
135187
/// **Important**: `stop` is not called with `start`, this is a feature.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pub(crate) type Entrypoint = Option<(gix::ObjectId, Option<gix::refs::FullName>)
2929
pub struct Overlay {
3030
entrypoint: Entrypoint,
3131
nonoverriding_references: Vec<gix::refs::Reference>,
32+
overriding_references: Vec<gix::refs::Reference>,
3233
meta_branches: Vec<(gix::refs::FullName, ref_metadata::Branch)>,
3334
workspace: Option<(gix::refs::FullName, ref_metadata::Workspace)>,
3435
}

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

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::init::walk::RefsById;
22
use crate::init::{Entrypoint, Overlay};
33
use but_core::{RefMetadata, ref_metadata};
44
use gix::prelude::ReferenceExt;
5+
use gix::refs::Target;
56
use std::borrow::Cow;
67
use std::collections::BTreeSet;
78

@@ -16,13 +17,27 @@ impl Overlay {
1617
self
1718
}
1819

20+
/// Serve the given `refs` from memory, which is like creating the reference or as if its value was set,
21+
/// completely overriding the value in the repository.
22+
pub fn with_references(mut self, refs: impl IntoIterator<Item = gix::refs::Reference>) -> Self {
23+
self.overriding_references.extend(refs);
24+
self
25+
}
26+
1927
/// Override the starting position of the traversal by setting it to `id`,
2028
/// and optionally, by providing the `ref_name` that points to `id`.
2129
pub fn with_entrypoint(
2230
mut self,
2331
id: gix::ObjectId,
2432
ref_name: Option<gix::refs::FullName>,
2533
) -> Self {
34+
if let Some(ref_name) = &ref_name {
35+
self.overriding_references.push(gix::refs::Reference {
36+
name: ref_name.to_owned(),
37+
target: Target::Object(id),
38+
peeled: Some(id),
39+
})
40+
}
2641
self.entrypoint = Some((id, ref_name));
2742
self
2843
}
@@ -58,14 +73,19 @@ impl Overlay {
5873
T: RefMetadata,
5974
{
6075
let Overlay {
61-
nonoverriding_references,
76+
mut nonoverriding_references,
77+
mut overriding_references,
6278
meta_branches,
6379
workspace,
6480
entrypoint,
6581
} = self;
82+
// Make sure that duplicates from later determine the value.
83+
nonoverriding_references.reverse();
84+
overriding_references.reverse();
6685
(
6786
OverlayRepo {
68-
nonoverriding_references,
87+
nonoverriding_references: nonoverriding_references.into_iter().collect(),
88+
overriding_references: overriding_references.into_iter().collect(),
6989
inner: repo,
7090
},
7191
OverlayMetadata {
@@ -80,7 +100,8 @@ impl Overlay {
80100

81101
pub(crate) struct OverlayRepo<'repo> {
82102
inner: &'repo gix::Repository,
83-
nonoverriding_references: Vec<gix::refs::Reference>,
103+
nonoverriding_references: BTreeSet<gix::refs::Reference>,
104+
overriding_references: BTreeSet<gix::refs::Reference>,
84105
}
85106

86107
/// Note that functions with `'repo` in their return value technically leak the bare repo, and it's
@@ -94,7 +115,13 @@ impl<'repo> OverlayRepo<'repo> {
94115
&self,
95116
ref_name: &gix::refs::FullNameRef,
96117
) -> anyhow::Result<Option<gix::Reference<'repo>>> {
97-
if let Some(rn) = self.inner.try_find_reference(ref_name)? {
118+
if let Some(r) = self
119+
.overriding_references
120+
.iter()
121+
.find(|r| r.name.as_ref() == ref_name)
122+
{
123+
Ok(Some(r.clone().attach(self.inner)))
124+
} else if let Some(rn) = self.inner.try_find_reference(ref_name)? {
98125
Ok(Some(rn))
99126
} else if let Some(r) = self
100127
.nonoverriding_references
@@ -111,6 +138,13 @@ impl<'repo> OverlayRepo<'repo> {
111138
&self,
112139
ref_name: &gix::refs::FullNameRef,
113140
) -> anyhow::Result<gix::Reference<'repo>> {
141+
if let Some(r) = self
142+
.overriding_references
143+
.iter()
144+
.find(|r| r.name.as_ref() == ref_name)
145+
{
146+
return Ok(r.clone().attach(self.inner));
147+
}
114148
Ok(self
115149
.inner
116150
.find_reference(ref_name)
@@ -202,6 +236,18 @@ impl<'repo> OverlayRepo<'repo> {
202236
};
203237
let mut all_refs_by_id = gix::hashtable::HashMap::<_, Vec<_>>::default();
204238
for prefix in prefixes {
239+
// apply overrides - they are seen first and take the spot of everything.
240+
for (commit_id, git_reference) in self
241+
.overriding_references
242+
.iter()
243+
.filter(|rn| rn.name.as_bstr().starts_with(prefix.as_bytes()))
244+
.filter_map(|rn| ref_filter(rn.clone().attach(self.inner)))
245+
{
246+
all_refs_by_id
247+
.entry(commit_id)
248+
.or_default()
249+
.push(git_reference);
250+
}
205251
for (commit_id, git_reference) in self
206252
.inner
207253
.references()?
@@ -214,7 +260,7 @@ impl<'repo> OverlayRepo<'repo> {
214260
.or_default()
215261
.push(git_reference);
216262
}
217-
// apply overrides
263+
// apply overrides (new only)
218264
for (commit_id, git_reference) in self
219265
.nonoverriding_references
220266
.iter()

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ impl Graph {
9393
// Finally, once all segments were added, it's good to generations
9494
// have to figure out early abort conditions, or to know what's ahead of another.
9595
self.compute_generation_numbers();
96-
9796
Ok(self)
9897
}
9998

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

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -696,50 +696,6 @@ impl Graph {
696696
.map(|(c, sidx)| (c.id, sidx))
697697
}
698698
}
699-
700-
/// Compute the loweset merge-base between two segments.
701-
/// Such a merge-base is reachable from all possible paths from `a` and `b`.
702-
///
703-
/// We know this works as all branching and merging is represented by a segment.
704-
/// Thus, the merge-base is always the first commit of the returned segment
705-
// TODO: should be multi, with extra segments as third parameter
706-
// TODO: actually find the lowest merge-base, right now it just finds the first merge-base, but that's not
707-
// the lowest.
708-
fn first_merge_base(&self, a: SegmentIndex, b: SegmentIndex) -> Option<SegmentIndex> {
709-
// TODO(perf): improve this by allowing to set bitflags on the segments themselves, to allow
710-
// marking them accordingly, just like Git does.
711-
// Right now we 'emulate' bitflags on pre-allocated data with two data sets, expensive
712-
// in comparison.
713-
// And yes, let's avoid `gix::Repository::merge_base` as we have free
714-
// generation numbers here and can avoid work duplication.
715-
let mut segments_reachable_by_b = BTreeSet::new();
716-
self.visit_all_segments_including_start_until(b, Direction::Outgoing, |s| {
717-
segments_reachable_by_b.insert(s.id);
718-
// Collect everything, keep it simple.
719-
// This is fast* as completely in memory.
720-
// *means slow compared to an array traversal with memory locality.
721-
false
722-
});
723-
724-
let mut candidate = None;
725-
self.visit_all_segments_including_start_until(a, Direction::Outgoing, |s| {
726-
if candidate.is_some() {
727-
return true;
728-
}
729-
let prune = segments_reachable_by_b.contains(&s.id);
730-
if prune {
731-
candidate = Some(s.id);
732-
}
733-
prune
734-
});
735-
if candidate.is_none() {
736-
// TODO: improve this - workspaces shouldn't be like this but if they are, do we deal with it well?
737-
tracing::warn!(
738-
"Couldn't find merge-base between segments {a:?} and {b:?} - this might lead to unexpected results"
739-
)
740-
}
741-
candidate
742-
}
743699
}
744700

745701
/// This works as named segments have been created in a prior step. Thus, we are able to find best matches by

0 commit comments

Comments
 (0)