Skip to content

Commit d86bb78

Browse files
authored
Merge pull request #10447 from Byron/next
V3 apply/unapply: MVP
2 parents bcacbc2 + 4cde1f1 commit d86bb78

File tree

30 files changed

+1702
-302
lines changed

30 files changed

+1702
-302
lines changed

crates/but-core/src/ref_metadata.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::Id;
2+
use gix::refs::FullNameRef;
23

34
/// Metadata about workspaces, associated with references that are designated to a workspace,
45
/// i.e. `refs/heads/gitbutler/workspaces/<name>`.
@@ -35,7 +36,72 @@ pub struct Workspace {
3536
pub push_remote: Option<String>,
3637
}
3738

39+
/// Mutations
40+
impl Workspace {
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 {
50+
if self.contains_ref(branch) {
51+
return false;
52+
};
53+
54+
let stack = WorkspaceStack {
55+
id: StackId::generate(),
56+
branches: vec![WorkspaceStackBranch {
57+
ref_name: branch.to_owned(),
58+
archived: false,
59+
}],
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+
}
69+
true
70+
}
71+
}
72+
73+
/// Access
3874
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+
}
81+
82+
/// Return `true` if the branch with `name` is the workspace target or the targets local tracking branch,
83+
/// using `repo` for the lookup of the local tracking branch.
84+
pub fn is_branch_the_target_or_its_local_tracking_branch(
85+
&self,
86+
name: &gix::refs::FullNameRef,
87+
repo: &gix::Repository,
88+
) -> anyhow::Result<bool> {
89+
let Some(target_ref) = self.target_ref.as_ref() else {
90+
return Ok(false);
91+
};
92+
93+
if target_ref.as_ref() == name {
94+
Ok(true)
95+
} else {
96+
let Some((local_tracking_branch, _remote_name)) =
97+
repo.upstream_branch_and_remote_for_tracking_branch(target_ref.as_ref())?
98+
else {
99+
return Ok(false);
100+
};
101+
Ok(local_tracking_branch.as_ref() == name)
102+
}
103+
}
104+
39105
/// Return `true` if `name` is a reference mentioned in our [stacks](Workspace::stacks).
40106
pub fn contains_ref(&self, name: &gix::refs::FullNameRef) -> bool {
41107
self.stacks
@@ -92,6 +158,18 @@ pub struct Branch {
92158
pub review: Review,
93159
}
94160

161+
/// Mutations
162+
impl Branch {
163+
/// Claim that we now updated the branch in some way, and possibly also set the created time
164+
/// if `is_new_ref` is `true`
165+
pub fn update_times(&mut self, is_new_ref: bool) {
166+
self.ref_info.set_updated_to_now();
167+
if is_new_ref {
168+
self.ref_info.set_created_to_now();
169+
}
170+
}
171+
}
172+
95173
impl std::fmt::Debug for Branch {
96174
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97175
const DEFAULT_IN_TESTSUITE: gix::date::Time = gix::date::Time {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ mod cmd;
22
mod commit;
33
mod diff;
44
mod json_samples;
5+
mod ref_metadata;
56
mod settings;
67
mod unified_diff;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
mod workspace {
2+
use but_core::ref_metadata::Workspace;
3+
4+
#[test]
5+
fn add_new_stack_if_not_present() {
6+
let mut ws = Workspace::default();
7+
assert_eq!(ws.stacks.len(), 0);
8+
9+
let a_ref = r("refs/heads/A");
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)));
12+
assert_eq!(ws.stacks.len(), 1);
13+
14+
let b_ref = r("refs/heads/B");
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]);
21+
}
22+
23+
fn r(name: &str) -> &gix::refs::FullNameRef {
24+
name.try_into().expect("statically known ref")
25+
}
26+
}

crates/but-graph/src/api.rs

Lines changed: 58 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.
@@ -171,6 +223,12 @@ impl Graph {
171223
self.hard_limit_hit = true;
172224
}
173225

226+
/// Lookup the segment of `sidx` and then find its sibling segment, if it has one.
227+
pub fn lookup_sibling_segment(&self, sidx: SegmentIndex) -> Option<&Segment> {
228+
self.inner
229+
.node_weight(self.inner.node_weight(sidx)?.sibling_segment_id?)
230+
}
231+
174232
/// Return the entry-point of the graph as configured during traversal.
175233
/// It's useful for when one wants to know which commit was used to discover the entire graph.
176234
///

crates/but-graph/src/debug.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ impl Graph {
371371
.commit_id_by_index(e.dst)
372372
.map(|c| c.to_hex_with_len(HEX).to_string())
373373
.unwrap_or_else(|| "dst".into());
374-
format!(", label = \"{src} → {dst} ({err})\", fontname = Courier")
374+
format!(", label = \"⚠{src} → {dst} ({err})\", fontname = Courier")
375375
};
376376
let dot = petgraph::dot::Dot::with_attr_getters(&self.inner, &[], &edge_attrs, &node_attrs);
377377
format!("{dot:?}")

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ pub(crate) type Entrypoint = Option<(gix::ObjectId, Option<gix::refs::FullName>)
2525

2626
/// A way to define information to be served from memory, instead of from the underlying data source, when
2727
/// [initializing](Graph::from_commit_traversal()) the graph.
28-
#[derive(Debug, Default)]
28+
#[derive(Debug, Default, Clone)]
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: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ 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;
6-
use std::collections::BTreeSet;
7+
use std::collections::{BTreeMap, BTreeSet};
78

89
impl Overlay {
910
/// Serve the given `refs` from memory, as if they would exist.
@@ -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
}
@@ -59,13 +74,21 @@ impl Overlay {
5974
{
6075
let Overlay {
6176
nonoverriding_references,
77+
overriding_references,
6278
meta_branches,
6379
workspace,
6480
entrypoint,
6581
} = self;
6682
(
6783
OverlayRepo {
68-
nonoverriding_references,
84+
nonoverriding_references: nonoverriding_references
85+
.into_iter()
86+
.map(|r| (r.name.clone(), r))
87+
.collect(),
88+
overriding_references: overriding_references
89+
.into_iter()
90+
.map(|r| (r.name.clone(), r))
91+
.collect(),
6992
inner: repo,
7093
},
7194
OverlayMetadata {
@@ -80,7 +103,8 @@ impl Overlay {
80103

81104
pub(crate) struct OverlayRepo<'repo> {
82105
inner: &'repo gix::Repository,
83-
nonoverriding_references: Vec<gix::refs::Reference>,
106+
nonoverriding_references: BTreeMap<gix::refs::FullName, gix::refs::Reference>,
107+
overriding_references: BTreeMap<gix::refs::FullName, gix::refs::Reference>,
84108
}
85109

86110
/// Note that functions with `'repo` in their return value technically leak the bare repo, and it's
@@ -94,13 +118,11 @@ impl<'repo> OverlayRepo<'repo> {
94118
&self,
95119
ref_name: &gix::refs::FullNameRef,
96120
) -> anyhow::Result<Option<gix::Reference<'repo>>> {
97-
if let Some(rn) = self.inner.try_find_reference(ref_name)? {
121+
if let Some(r) = self.overriding_references.get(ref_name) {
122+
Ok(Some(r.clone().attach(self.inner)))
123+
} else if let Some(rn) = self.inner.try_find_reference(ref_name)? {
98124
Ok(Some(rn))
99-
} else if let Some(r) = self
100-
.nonoverriding_references
101-
.iter()
102-
.find(|r| r.name.as_ref() == ref_name)
103-
{
125+
} else if let Some(r) = self.nonoverriding_references.get(ref_name) {
104126
Ok(Some(r.clone().attach(self.inner)))
105127
} else {
106128
Ok(None)
@@ -111,17 +133,16 @@ impl<'repo> OverlayRepo<'repo> {
111133
&self,
112134
ref_name: &gix::refs::FullNameRef,
113135
) -> anyhow::Result<gix::Reference<'repo>> {
136+
if let Some(r) = self.overriding_references.get(ref_name) {
137+
return Ok(r.clone().attach(self.inner));
138+
}
114139
Ok(self
115140
.inner
116141
.find_reference(ref_name)
117142
.or_else(|err| match err {
118143
gix::reference::find::existing::Error::Find(_) => Err(err),
119144
gix::reference::find::existing::Error::NotFound { .. } => {
120-
if let Some(r) = self
121-
.nonoverriding_references
122-
.iter()
123-
.find(|r| r.name.as_ref() == ref_name)
124-
{
145+
if let Some(r) = self.nonoverriding_references.get(ref_name) {
125146
Ok(r.clone().attach(self.inner))
126147
} else {
127148
Err(err)
@@ -202,6 +223,18 @@ impl<'repo> OverlayRepo<'repo> {
202223
};
203224
let mut all_refs_by_id = gix::hashtable::HashMap::<_, Vec<_>>::default();
204225
for prefix in prefixes {
226+
// apply overrides - they are seen first and take the spot of everything.
227+
for (commit_id, git_reference) in self
228+
.overriding_references
229+
.values()
230+
.filter(|rn| rn.name.as_bstr().starts_with(prefix.as_bytes()))
231+
.filter_map(|rn| ref_filter(rn.clone().attach(self.inner)))
232+
{
233+
all_refs_by_id
234+
.entry(commit_id)
235+
.or_default()
236+
.push(git_reference);
237+
}
205238
for (commit_id, git_reference) in self
206239
.inner
207240
.references()?
@@ -214,10 +247,10 @@ impl<'repo> OverlayRepo<'repo> {
214247
.or_default()
215248
.push(git_reference);
216249
}
217-
// apply overrides
250+
// apply overrides (new only)
218251
for (commit_id, git_reference) in self
219252
.nonoverriding_references
220-
.iter()
253+
.values()
221254
.filter(|rn| rn.name.as_bstr().starts_with(prefix.as_bytes()))
222255
.filter_map(|rn| ref_filter(rn.clone().attach(self.inner)))
223256
{

0 commit comments

Comments
 (0)