Skip to content

Commit 7d4af69

Browse files
authored
Merge pull request #11400 from Byron/next2
Add tests for single-branch `but branch apply`
2 parents db15ed9 + 63eaebe commit 7d4af69

File tree

21 files changed

+518
-127
lines changed

21 files changed

+518
-127
lines changed

crates/but-graph/src/debug.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,13 @@ impl Graph {
262262
static SUFFIX: AtomicUsize = AtomicUsize::new(0);
263263
let suffix = SUFFIX.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
264264
let svg_name = format!("debug-graph-{suffix:02}.svg");
265+
let svg_path = std::env::var_os("CARGO_MANIFEST_DIR")
266+
.map(std::path::PathBuf::from)
267+
.unwrap_or_default()
268+
.join(svg_name);
265269
let mut dot = std::process::Command::new("dot")
266-
.args(["-Tsvg", "-o", &svg_name])
270+
.args(["-Tsvg", "-o"])
271+
.arg(&svg_path)
267272
.stdin(Stdio::piped())
268273
.stdout(Stdio::piped())
269274
.stderr(Stdio::piped())
@@ -284,11 +289,12 @@ impl Graph {
284289

285290
assert!(
286291
std::process::Command::new("open")
287-
.arg(&svg_name)
292+
.arg(&svg_path)
288293
.status()
289294
.unwrap()
290295
.success(),
291-
"Opening of {svg_name} failed"
296+
"Opening of {svg_path} failed",
297+
svg_path = svg_path.display()
292298
);
293299
}
294300

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,8 @@ impl Graph {
358358
let tip_is_not_workspace_commit = !workspaces
359359
.iter()
360360
.any(|(_, wsrn, _)| Some(wsrn) == ref_name.as_ref());
361-
let worktree_by_branch = worktree_branches(repo.for_worktree_only())?;
361+
let worktree_by_branch =
362+
repo.worktree_branches(graph.entrypoint_ref.as_ref().map(|r| r.as_ref()))?;
362363

363364
let mut ctx = post::Context {
364365
repo,

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

Lines changed: 97 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1+
use crate::Worktree;
2+
use crate::init::walk::WorktreeByBranch;
3+
use crate::init::{Entrypoint, Overlay, walk::RefsById};
4+
use anyhow::bail;
5+
use but_core::{RefMetadata, ref_metadata};
6+
use gix::{prelude::ReferenceExt, refs::Target};
17
use std::{
28
borrow::Cow,
39
collections::{BTreeMap, BTreeSet},
410
};
511

6-
use but_core::{RefMetadata, ref_metadata};
7-
use gix::{prelude::ReferenceExt, refs::Target};
8-
9-
use crate::init::{Entrypoint, Overlay, walk::RefsById};
10-
1112
impl Overlay {
1213
/// Serve the given `refs` from memory, as if they would exist.
1314
/// This is true only, however, if a real reference doesn't exist.
@@ -103,10 +104,12 @@ impl Overlay {
103104
}
104105
}
105106

107+
type NameToReference = BTreeMap<gix::refs::FullName, gix::refs::Reference>;
108+
106109
pub(crate) struct OverlayRepo<'repo> {
107110
inner: &'repo gix::Repository,
108-
nonoverriding_references: BTreeMap<gix::refs::FullName, gix::refs::Reference>,
109-
overriding_references: BTreeMap<gix::refs::FullName, gix::refs::Reference>,
111+
nonoverriding_references: NameToReference,
112+
overriding_references: NameToReference,
110113
}
111114

112115
/// Note that functions with `'repo` in their return value technically leak the bare repo, and it's
@@ -182,10 +185,6 @@ impl<'repo> OverlayRepo<'repo> {
182185
self.inner
183186
}
184187

185-
pub fn for_worktree_only(&self) -> &'repo gix::Repository {
186-
self.inner
187-
}
188-
189188
pub fn remote_names(&self) -> gix::remote::Names<'repo> {
190189
self.inner.remote_names()
191190
}
@@ -269,6 +268,93 @@ impl<'repo> OverlayRepo<'repo> {
269268
all_refs_by_id.values_mut().for_each(|v| v.sort());
270269
Ok(all_refs_by_id)
271270
}
271+
272+
/// This is a bit tricky but aims to map the `HEAD` targets of the main worktree to what possibly was overridden
273+
/// via `main_head_referent`. The idea is that this is the entrypoint, which is assumed to be `HEAD`
274+
///
275+
/// ### Shortcoming
276+
///
277+
/// For now, it can only remap the first HEAD reference. For this to really work, we need proper in-memory overrides
278+
/// or a way to have overrides 'for real'.
279+
/// Also, we don't want `main_head_referent` to be initialised from the entrypoint, which we equal to be `HEAD`.
280+
/// But this invariant can fall apart easily and is caller dependent, as we use it to see the graph *as if* `HEAD` would
281+
/// be in another position - but that doesn't affect the worktree ref at all.
282+
pub fn worktree_branches(
283+
&self,
284+
main_head_referent: Option<&gix::refs::FullNameRef>,
285+
) -> anyhow::Result<WorktreeByBranch> {
286+
/// If `main_head_referent` is set, it means this is an overridden reference of the `HEAD` of the repo the graph is built in.
287+
/// If `None`, `head` belongs to another worktree. Completely unrelated to linked or main.
288+
fn maybe_insert_head(
289+
head: Option<gix::Head<'_>>,
290+
main_head_referent: Option<&gix::refs::FullNameRef>,
291+
overriding: &NameToReference,
292+
out: &mut WorktreeByBranch,
293+
) -> anyhow::Result<()> {
294+
let Some((head, wd)) = head.and_then(|head| {
295+
head.repo.worktree().map(|wt| {
296+
(
297+
head,
298+
match wt.id() {
299+
None => Worktree::Main,
300+
Some(id) => Worktree::LinkedId(id.to_owned()),
301+
},
302+
)
303+
})
304+
}) else {
305+
return Ok(());
306+
};
307+
308+
out.entry("HEAD".try_into().expect("valid"))
309+
.or_default()
310+
.push(wd.clone());
311+
let mut ref_chain = Vec::new();
312+
// Is this the repo that the overrides were applied on?
313+
let mut cursor = if let Some(head_name) = main_head_referent {
314+
overriding
315+
.get(head_name)
316+
.map(|overridden_head| overridden_head.clone().attach(head.repo))
317+
.or_else(|| head.try_into_referent())
318+
} else {
319+
head.try_into_referent()
320+
};
321+
while let Some(ref_) = cursor {
322+
ref_chain.push(ref_.name().to_owned());
323+
if overriding
324+
.get(ref_.name())
325+
.is_some_and(|r| r.target.try_name() != ref_.target().try_name())
326+
{
327+
bail!(
328+
"SHORTCOMING: cannot deal with {ref_:?} overridden to a different symbolic name to follow"
329+
)
330+
}
331+
cursor = ref_.follow().transpose()?;
332+
}
333+
for name in ref_chain {
334+
out.entry(name).or_default().push(wd.clone());
335+
}
336+
337+
Ok(())
338+
}
339+
340+
let mut map = BTreeMap::new();
341+
maybe_insert_head(
342+
self.inner.head().ok(),
343+
main_head_referent,
344+
&self.overriding_references,
345+
&mut map,
346+
)?;
347+
for proxy in self.inner.worktrees()? {
348+
let repo = proxy.into_repo_with_possibly_inaccessible_worktree()?;
349+
maybe_insert_head(
350+
repo.head().ok(),
351+
None,
352+
&self.overriding_references,
353+
&mut map,
354+
)?;
355+
}
356+
Ok(map)
357+
}
272358
}
273359

274360
pub(crate) struct OverlayMetadata<'meta, T> {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -854,7 +854,7 @@ impl Graph {
854854
.flat_map(|s| s.commits_by_segment.iter().map(|(sidx, _)| *sidx))
855855
}) {
856856
// The workspace might be stale by now as we delete empty segments.
857-
// Thus be careful, and ignore non-existing ones - after all our workspace
857+
// Thus, be careful, and ignore non-existing ones - after all our workspace
858858
// is temporary, nothing to worry about.
859859
let Some(s) = self.inner.node_weight(sidx) else {
860860
continue;

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

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -973,50 +973,6 @@ pub fn prune_integrated_tips(graph: &mut Graph, next: &mut Queue) -> anyhow::Res
973973

974974
pub(crate) type WorktreeByBranch = BTreeMap<gix::refs::FullName, Vec<Worktree>>;
975975

976-
pub fn worktree_branches(repo: &gix::Repository) -> anyhow::Result<WorktreeByBranch> {
977-
fn maybe_insert_head(
978-
head: Option<gix::Head<'_>>,
979-
out: &mut WorktreeByBranch,
980-
) -> anyhow::Result<()> {
981-
let Some((head, wd)) = head.and_then(|head| {
982-
head.repo.worktree().map(|wt| {
983-
(
984-
head,
985-
match wt.id() {
986-
None => Worktree::Main,
987-
Some(id) => Worktree::LinkedId(id.to_owned()),
988-
},
989-
)
990-
})
991-
}) else {
992-
return Ok(());
993-
};
994-
995-
out.entry("HEAD".try_into().expect("valid"))
996-
.or_default()
997-
.push(wd.to_owned());
998-
let mut ref_chain = Vec::new();
999-
let mut cursor = head.try_into_referent();
1000-
while let Some(ref_) = cursor {
1001-
ref_chain.push(ref_.name().to_owned());
1002-
cursor = ref_.follow().transpose()?;
1003-
}
1004-
for name in ref_chain {
1005-
out.entry(name).or_default().push(wd.to_owned());
1006-
}
1007-
1008-
Ok(())
1009-
}
1010-
1011-
let mut map = BTreeMap::new();
1012-
maybe_insert_head(repo.head().ok(), &mut map)?;
1013-
for proxy in repo.worktrees()? {
1014-
let repo = proxy.into_repo_with_possibly_inaccessible_worktree()?;
1015-
maybe_insert_head(repo.head().ok(), &mut map)?;
1016-
}
1017-
Ok(map)
1018-
}
1019-
1020976
impl crate::RefInfo {
1021977
pub(crate) fn from_ref(
1022978
ref_name: gix::refs::FullName,

crates/but-graph/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,8 @@ pub struct Graph {
237237
hard_limit_hit: bool,
238238
/// The options used to create the graph, which allows it to regenerate itself after something
239239
/// possibly changed. This can also be used to simulate changes by injecting would-be information.
240-
options: init::Options,
240+
/// Public to be able to change it before calling [Graph::redo_traversal_with_overlay()].
241+
pub options: init::Options,
241242
}
242243

243244
/// A resolved entry point into the graph for easy access to the segment, commit,

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,10 @@ impl Stack {
9595

9696
impl Stack {
9797
/// A one-line string representing the stack itself, without its contents.
98-
pub fn debug_string(&self) -> String {
98+
///
99+
/// Use `id_override` to have it use this (usually controlled) id instead of what otherwise
100+
/// would be a generated one.
101+
pub fn debug_string(&self, id_override: Option<StackId>) -> String {
99102
let mut dbg = self
100103
.segments
101104
.first()
@@ -105,7 +108,7 @@ impl Stack {
105108
dbg.push_str(&base.to_hex_with_len(7).to_string());
106109
}
107110
dbg.insert(0, '≡');
108-
if let Some(id) = self.id {
111+
if let Some(id) = id_override.or(self.id) {
109112
let id_string = id.to_string().replace("0", "").replace("-", "");
110113
dbg.push_str(&format!(
111114
" {{{}}}",
@@ -122,7 +125,7 @@ impl Stack {
122125

123126
impl std::fmt::Debug for Stack {
124127
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
125-
let mut s = f.debug_struct(&format!("Stack({})", self.debug_string()));
128+
let mut s = f.debug_struct(&format!("Stack({})", self.debug_string(None)));
126129
s.field("segments", &self.segments);
127130
if let Some(stack_id) = self.id {
128131
s.field("id", &stack_id);
@@ -238,13 +241,22 @@ impl StackSegment {
238241
.flat_map(|c| c.refs.iter().map(|ri| ri.ref_name.as_ref())),
239242
)
240243
}
244+
245+
/// Return `true` if this segment *would* be anonymous if it wasn't for the out-of-workspace segment to be projected onto this one.
246+
///
247+
/// This is signaled by its underlying graph segment being unnamed, with a sybling set.
248+
pub fn is_projected_from_outside(&self, graph: &Graph) -> bool {
249+
let segment = &graph[self.id];
250+
segment.ref_info.is_none() && segment.sibling_segment_id.is_some()
251+
}
241252
}
242253

243254
impl std::fmt::Debug for StackSegment {
244255
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
245256
f.debug_struct(&format!("StackSegment({})", self.debug_string()))
246257
.field("commits", &self.commits)
247258
.field("commits_on_remote", &self.commits_on_remote)
259+
.field("commits_outside", &self.commits_outside)
248260
.finish()
249261
}
250262
}
@@ -258,7 +270,7 @@ impl StackSegment {
258270
/// is an unambiguous ref pointing to a commit, or when it splits a segment by incoming connection.
259271
///
260272
/// `graph` is used to look up the remote segment and find its commits.
261-
pub fn from_graph_segments(
273+
pub(crate) fn from_graph_segments(
262274
segments: &[&crate::Segment],
263275
graph: &Graph,
264276
) -> anyhow::Result<Self> {

crates/but-graph/src/segment.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,14 +263,14 @@ impl std::fmt::Debug for Segment {
263263
sibling_segment_id,
264264
metadata,
265265
} = self;
266-
f.debug_struct("StackSegment")
266+
f.debug_struct("Segment")
267267
.field("id", id)
268268
.field("generation", generation)
269269
.field(
270-
"ref_name",
270+
"ref_info",
271271
&match ref_info.as_ref() {
272272
None => "None".to_string(),
273-
Some(name) => name.debug_string(),
273+
Some(info) => info.debug_string(),
274274
},
275275
)
276276
.field(

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ fn unborn() -> anyhow::Result<()> {
1414
node_count: 1,
1515
edge_count: 0,
1616
node weights: {
17-
0: StackSegment {
17+
0: Segment {
1818
id: NodeIndex(0),
1919
generation: 0,
20-
ref_name: "►main[🌳]",
20+
ref_info: "►main[🌳]",
2121
remote_tracking_ref_name: "None",
2222
sibling_segment_id: "None",
2323
commits: [],
@@ -89,21 +89,21 @@ fn detached() -> anyhow::Result<()> {
8989
edge_count: 1,
9090
edges: (0, 1),
9191
node weights: {
92-
0: StackSegment {
92+
0: Segment {
9393
id: NodeIndex(0),
9494
generation: 0,
95-
ref_name: "None",
95+
ref_info: "None",
9696
remote_tracking_ref_name: "None",
9797
sibling_segment_id: "None",
9898
commits: [
9999
Commit(541396b, ⌂|1►annotated, ►release/v1, ►main),
100100
],
101101
metadata: "None",
102102
},
103-
1: StackSegment {
103+
1: Segment {
104104
id: NodeIndex(1),
105105
generation: 1,
106-
ref_name: "►other",
106+
ref_info: "►other",
107107
remote_tracking_ref_name: "None",
108108
sibling_segment_id: "None",
109109
commits: [

crates/but-meta/src/legacy.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ impl Snapshot {
5555
if self.content == Default::default() {
5656
std::fs::remove_file(&self.path)?;
5757
} else {
58+
if let Some(dir) = self.path.parent() {
59+
std::fs::create_dir_all(dir)?;
60+
}
5861
fs::write(
5962
&self.path,
6063
toml::to_string(&self.to_consistent_data(reconcile))?,

0 commit comments

Comments
 (0)