diff --git a/crates/but-graph/src/api.rs b/crates/but-graph/src/api.rs
index 2efa2943fb2..d1092dddd6a 100644
--- a/crates/but-graph/src/api.rs
+++ b/crates/but-graph/src/api.rs
@@ -59,10 +59,6 @@ impl Graph {
/// `dst_commit_id` can be provided if the connection is to a future commit that isn't yet available
/// in the `segment`. If `None`, it will be looked up in the `segment` itself.
///
- /// `parent_order` is the 0-based position of `dst_commit_id` among the source commit's
- /// parents. For a merge commit with parents `[A, B]`, the edge to `A` must use `0`,
- /// and the edge to `B` must use `1`, even if traversal discovers `B` before `A`.
- ///
/// Return the newly added segment.
pub fn connect_new_segment(
&mut self,
@@ -71,7 +67,6 @@ impl Graph {
dst: Segment,
dst_commit: impl Into>,
dst_commit_id: impl Into >,
- parent_order: u32,
) -> SegmentIndex {
let dst = self.inner.add_node(dst);
self.inner[dst].id = dst;
@@ -82,7 +77,6 @@ impl Graph {
dst,
dst_commit,
dst_commit_id.into(),
- parent_order,
);
dst
}
@@ -168,7 +162,7 @@ impl Graph {
/// excluded side only as far as needed to prove emitted segments are not hidden.
///
/// If `first_parent` is [`FirstParent::Yes`], both the included and excluded traversals follow
- /// only segment edges with `parent_order == 0`.
+ /// only the first-parent edge (the source commit's first parent).
pub fn find_commits_reachable_from_a_not_b(
&self,
included: SegmentIndex,
@@ -192,7 +186,7 @@ impl Graph {
/// excluded side only as far as needed to prove emitted segments are not hidden.
///
/// If `first_parent` is [`FirstParent::Yes`], both the included and excluded traversals follow
- /// only segment edges with `parent_order == 0`.
+ /// only the first-parent edge (the source commit's first parent).
pub fn find_segments_reachable_from_a_not_b(
&self,
included: SegmentIndex,
@@ -317,10 +311,16 @@ impl Graph {
segment_id: SegmentIndex,
first_parent_only: bool,
) -> impl Iterator- {
+ // Outgoing edges are kept in first-parent order (see
+ // `rebuild_outgoing_edges_for_traversal_order`), so first-parent traversal follows just the
+ // first edge. Matching by the destination commit id instead would dead-end whenever the
+ // first-parent edge crosses into a commit-less segment (an empty branch), whose edge carries
+ // no destination id to match against.
+ let max_edges = if first_parent_only { 1 } else { usize::MAX };
self.inner
.edges_directed(segment_id, Direction::Outgoing)
- .filter(move |edge| !first_parent_only || edge.weight().parent_order == 0)
.map(|edge| edge.target())
+ .take(max_edges)
}
/// Return `true` once the excluded-side frontier cannot still hide any segment already emitted.
diff --git a/crates/but-graph/src/init/mod.rs b/crates/but-graph/src/init/mod.rs
index 6a2bd750458..0f4d72d9250 100644
--- a/crates/but-graph/src/init/mod.rs
+++ b/crates/but-graph/src/init/mod.rs
@@ -889,7 +889,6 @@ impl Graph {
propagated_flags,
src_sidx,
limit,
- 0,
)?;
continue;
}
@@ -910,7 +909,6 @@ impl Graph {
Instruction::ConnectNewSegment {
parent_above,
at_commit,
- parent_order,
} => match seen.entry(id) {
Entry::Occupied(_) => {
possibly_split_occupied_segment(
@@ -921,7 +919,6 @@ impl Graph {
propagated_flags,
parent_above,
limit,
- parent_order,
)?;
continue;
}
@@ -938,7 +935,6 @@ impl Graph {
segment_below,
0,
id,
- parent_order,
);
e.insert(segment_below);
segment_below
@@ -2172,10 +2168,9 @@ impl Graph {
dst: SegmentIndex,
dst_commit: impl Into
>,
) {
- self.connect_segments_with_ids(src, src_commit, None, dst, dst_commit, None, 0)
+ self.connect_segments_with_ids(src, src_commit, None, dst, dst_commit, None)
}
- #[expect(clippy::too_many_arguments)]
pub(crate) fn connect_segments_with_ids(
&mut self,
src: SegmentIndex,
@@ -2184,7 +2179,6 @@ impl Graph {
dst: SegmentIndex,
dst_commit: impl Into >,
dst_id: Option,
- parent_order: u32,
) {
let src_commit = src_commit.into();
let dst_commit = dst_commit.into();
@@ -2196,12 +2190,17 @@ impl Graph {
src_id: src_id.or_else(|| self[src].commit_id_by_index(src_commit)),
dst: dst_commit,
dst_id: dst_id.or_else(|| self[dst].commit_id_by_index(dst_commit)),
- parent_order,
},
);
self.rebuild_outgoing_edges_for_traversal_order(src, new_edge_id);
}
+ /// Insert the just-added `new_edge_id` into `src`'s outgoing edges so they stay in first-parent
+ /// order — each destination's position among the source commit's parents. The existing edges
+ /// are already ordered, so this inserts the new one at its slot rather than re-sorting (a full
+ /// re-sort would also normalize any pre-existing order, changing the tie-break that downstream
+ /// stable sorts inherit). Commit-less edges (empty branches) carry no destination id and sort
+ /// last.
fn rebuild_outgoing_edges_for_traversal_order(
&mut self,
src: SegmentIndex,
@@ -2225,8 +2224,19 @@ impl Graph {
return;
}
- let insert_at = outgoing_edges
- .partition_point(|edge| edge.weight.parent_order <= new_edge.weight.parent_order);
+ let src_parents: &[gix::ObjectId] = self[src]
+ .commits
+ .last()
+ .map(|c| c.parent_ids.as_slice())
+ .unwrap_or_default();
+ let order_of = |edge: &EdgeOwned| {
+ edge.weight
+ .dst_id()
+ .and_then(|dst| src_parents.iter().position(|p| *p == dst))
+ .unwrap_or(usize::MAX)
+ };
+ let insert_at =
+ outgoing_edges.partition_point(|edge| order_of(edge) <= order_of(&new_edge));
outgoing_edges.insert(insert_at, new_edge);
for edge in &outgoing_edges {
diff --git a/crates/but-graph/src/init/post.rs b/crates/but-graph/src/init/post.rs
index b3e6b75920c..57aaa5b81b4 100644
--- a/crates/but-graph/src/init/post.rs
+++ b/crates/but-graph/src/init/post.rs
@@ -57,9 +57,6 @@ impl Graph {
) -> anyhow::Result {
self.hard_limit_hit = hard_limit;
- // Make sure edge order matches the git commit graph when traversed.
- self.rebuild_edges_in_parent_order();
-
// Keep the original traversal tip available even if post-processing moves the
// entrypoint to a segment that doesn't contain it.
self.update_entrypoint_commit_id(tip);
@@ -103,6 +100,11 @@ impl Graph {
self.compute_generation_numbers();
self.symbolic_remote_names = symbolic_remote_names.to_vec();
+
+ debug_assert!(
+ self.first_parent_edges_well_ordered(),
+ "a segment's first outgoing edge is not its first-parent edge; first-parent traversal would follow the wrong parent"
+ );
self.finish_post_processing(detach_entrypoint)
}
@@ -113,36 +115,6 @@ impl Graph {
Ok(self)
}
- /// Rebuild outgoing edges so petgraph traversal follows the source commit's
- /// `parent_order`. Petgraph traverses edges in reverse creation order, so
- /// edges are re-added from last parent to first parent.
- fn rebuild_edges_in_parent_order(&mut self) {
- let mut outgoing_edges = Vec::with_capacity(2);
- for sidx in self.segments().collect::>() {
- let mut edges = self.inner.edges_directed(sidx, Direction::Outgoing);
- let Some(first_edge) = edges.next() else {
- continue;
- };
- let Some(second_edge) = edges.next() else {
- continue;
- };
-
- outgoing_edges.clear();
- outgoing_edges.push(EdgeOwned::from(first_edge));
- outgoing_edges.push(EdgeOwned::from(second_edge));
- outgoing_edges.extend(edges.map(EdgeOwned::from));
-
- outgoing_edges.sort_by_key(|e| std::cmp::Reverse(e.weight.parent_order));
-
- for edge in &outgoing_edges {
- self.inner.remove_edge(edge.id);
- }
- for edge in &outgoing_edges {
- self.inner.add_edge(edge.source, edge.target, edge.weight);
- }
- }
- }
-
/// Ensure the entrypoint commit-id is updated to match the actual tip commit.
/// This may get out of sync when we do our graph manipulations, and it's easier
/// to set it right in post than to let everything deal with this.
@@ -252,7 +224,6 @@ impl Graph {
src_id: edge.weight.src_id,
dst: None,
dst_id: None,
- parent_order: edge.weight.parent_order,
},
);
self.inner.remove_edge(edge.id);
@@ -396,7 +367,6 @@ impl Graph {
owner_sidx,
0,
Some(owner_tip),
- 0,
);
}
}
@@ -476,7 +446,6 @@ impl Graph {
new_segment,
0,
tip_of_new_segment,
- 0,
);
let (src, src_id) = {
@@ -495,7 +464,6 @@ impl Graph {
src_id,
dst: edge.weight.dst,
dst_id: edge.weight.dst_id,
- parent_order: edge.weight.parent_order,
},
);
self.inner.remove_edge(edge.id);
@@ -701,11 +669,56 @@ impl Graph {
Ok(out)
}
+ /// The commit identifying `stack_top`'s position in the workspace commit's parent array:
+ /// the first commit reachable by walking first-parent from `stack_top`, skipping empty
+ /// segments. For a fully empty lane this is its base, which is itself a parent of the
+ /// workspace commit. Returns `None` only if no commit is reachable at all.
+ fn stack_entry_commit(&self, stack_top: SegmentIndex) -> Option {
+ let mut cur = stack_top;
+ loop {
+ if let Some(commit) = self[cur].commits.first() {
+ return Some(commit.id);
+ }
+ cur = self
+ .inner
+ .neighbors_directed(cur, Direction::Outgoing)
+ .next()?;
+ }
+ }
+
+ /// Debug-only invariant: a segment's first outgoing edge must be its first-parent edge — the
+ /// property that first-parent traversal (`parent_segments_for_reachable_difference`'s `take(1)`)
+ /// and the projection's `next_segment_downward` fallback rely on. Edges are kept in this order
+ /// incrementally by `rebuild_outgoing_edges_for_traversal_order`; this guards any raw `add_edge`
+ /// path that bypasses it. Conservatively accepts a commit-less first-parent target (an empty
+ /// branch), which carries no destination id to check against.
+ fn first_parent_edges_well_ordered(&self) -> bool {
+ for sidx in self.inner.node_indices() {
+ let Some(last) = self[sidx].commits.last() else {
+ continue;
+ };
+ if last.parent_ids.len() < 2 {
+ continue;
+ }
+ // Check the *literal* first outgoing edge — exactly what `take(1)` first-parent
+ // traversal follows, not the first edge that happens to come from the last commit.
+ let Some(first) = self.inner.edges_directed(sidx, Direction::Outgoing).next() else {
+ continue;
+ };
+ match first.weight().dst_id() {
+ None => {}
+ Some(dst) if dst == last.parent_ids[0] => {}
+ Some(_) => return false,
+ }
+ }
+ true
+ }
+
/// Perform operations on the current workspace, or do nothing if there is `None`.
///
/// * workspace segments are either empty, or have just one managed commit.
/// * insert empty segments as defined by the workspace that affects its downstream.
- /// * put workspace connection into the order defined in the workspace metadata.
+ /// * order workspace connections by the workspace commit's real parent array.
/// * set sibling segment IDs for unnamed segments that are descendants of an out-of-workspace but known segment.
fn workspace_upgrades(
&mut self,
@@ -935,7 +948,11 @@ impl Graph {
delete_anon_if_empty_and_reconnect(self, sidx);
}
- // Redo workspace outgoing connections according to desired stack order.
+ // Redo workspace outgoing connections in the order of the workspace commit's real parent
+ // array — the source of truth for stack order. A stack's entry is its top commit; an empty
+ // lane resolves to its base (a first-parent walk down), itself a parent of the workspace
+ // commit. Stacks the parent array can't tell apart (e.g. two branches on one base) tie-break
+ // by workspace-metadata order, then by their original relative order.
let mut edges_pointing_to_named_segment = self
.inner
.edges_directed(ws_sidx, Direction::Outgoing)
@@ -949,21 +966,33 @@ impl Graph {
.iter()
.map(|(_e, sidx, _rn)| *sidx)
.collect();
- edges_pointing_to_named_segment.sort_by_key(|(_e, sidx, ri)| {
- let res = ws_data.stacks.iter().position(|s| {
- s.is_in_workspace()
- && s.branches
- .first()
- .is_some_and(|b| Some(&b.ref_name) == ri.as_ref().map(|ri| &ri.ref_name))
- });
- // This makes it so that edges that weren't mentioned in workspace metadata
- // retain their relative order, with first-come-first-serve semantics.
- // The expected case is that each segment is defined.
- res.or_else(|| {
- edges_original_order
- .iter()
- .position(|sidx_for_order| sidx_for_order == sidx)
- })
+ let ws_parent_ids: Vec = self[ws_sidx]
+ .commits
+ .first()
+ .map(|c| c.parent_ids.clone())
+ .unwrap_or_default();
+ // Cached: the key walks the graph (`stack_entry_commit`) and scans, so compute it once per
+ // edge rather than on every comparison.
+ edges_pointing_to_named_segment.sort_by_cached_key(|(_e, sidx, ri)| {
+ let by_parent = self
+ .stack_entry_commit(*sidx)
+ .and_then(|id| ws_parent_ids.iter().position(|p| *p == id))
+ .unwrap_or(usize::MAX);
+ let by_metadata = ws_data
+ .stacks
+ .iter()
+ .position(|s| {
+ s.is_in_workspace()
+ && s.branches.first().is_some_and(|b| {
+ Some(&b.ref_name) == ri.as_ref().map(|ri| &ri.ref_name)
+ })
+ })
+ .or_else(|| {
+ edges_original_order
+ .iter()
+ .position(|sidx_for_order| sidx_for_order == sidx)
+ });
+ (by_parent, by_metadata)
});
// Re-add in reverse because petgraph traverses newest edges first.
@@ -1403,7 +1432,6 @@ impl Graph {
owner_sidx,
Some(owner_cidx),
Some(local_tip),
- 0,
);
self[owner_sidx].commits[owner_cidx]
.refs
@@ -1744,7 +1772,6 @@ fn rebuild_same_tip_segment_chain_by_branch_order(
src_id: edge.weight.src_id,
dst: None,
dst_id: None,
- parent_order: edge.weight.parent_order,
},
);
}
@@ -1882,7 +1909,6 @@ fn delete_anon_if_empty_and_reconnect(graph: &mut Graph, sidx: SegmentIndex) {
src_id: edge.weight.src_id,
dst: target_commit_idx,
dst_id: target_commit_id,
- parent_order: edge.weight.parent_order,
},
);
}
@@ -1925,7 +1951,6 @@ fn create_independent_segments(
new_segment,
None,
None,
- 0,
);
above = new_segment_sidx;
@@ -2034,7 +2059,6 @@ fn maybe_create_multiple_segments(
new_segment,
(is_last && commit.is_some()).then_some(0),
is_last.then_some(commit.as_ref().map(|c| c.id)).flatten(),
- 0,
);
above_idx = new_segment;
if is_first {
@@ -2067,7 +2091,6 @@ fn maybe_create_multiple_segments(
src_id: edge.weight.src_id,
dst: target_cidx,
dst_id: target_cidx.and_then(|_| commit.as_ref().map(|c| c.id)),
- parent_order: edge.weight.parent_order,
},
);
}
@@ -2127,7 +2150,6 @@ fn reconnect_outgoing_edges(
src_id: target_first_commit_id,
dst: edge.weight.dst,
dst_id: edge.weight.dst_id,
- parent_order: edge.weight.parent_order,
},
);
}
diff --git a/crates/but-graph/src/init/types.rs b/crates/but-graph/src/init/types.rs
index 2c5a8feea54..6774eb5045b 100644
--- a/crates/but-graph/src/init/types.rs
+++ b/crates/but-graph/src/init/types.rs
@@ -336,11 +336,6 @@ pub enum Instruction {
///
/// This limit should never be reached either unless there is a repository with a trunk of 4.3 billion commits.
at_commit: u32,
- /// The position of this parent among the source commit's parents (0-based).
- /// Deliberately not `usize` for the same traversal-queue layout reason as `at_commit`.
- ///
- /// This limit would only be reached if there is a merge with 4.3 billion commits.
- parent_order: u32,
},
}
@@ -359,11 +354,9 @@ impl Instruction {
Instruction::ConnectNewSegment {
parent_above: _,
at_commit,
- parent_order,
} => Instruction::ConnectNewSegment {
parent_above: sidx,
at_commit,
- parent_order,
},
}
}
diff --git a/crates/but-graph/src/init/walk/mod.rs b/crates/but-graph/src/init/walk/mod.rs
index 777dd64407e..88784f32a1e 100644
--- a/crates/but-graph/src/init/walk/mod.rs
+++ b/crates/but-graph/src/init/walk/mod.rs
@@ -164,14 +164,9 @@ pub fn split_commit_into_segment(
let top_commit_index = graph[sidx].last_commit_index();
let bottom_commit_id = bottom_segment.commits[0].id;
let bottom_segment = match standin {
- None => graph.connect_new_segment(
- sidx,
- top_commit_index,
- bottom_segment,
- 0,
- bottom_commit_id,
- 0,
- ),
+ None => {
+ graph.connect_new_segment(sidx, top_commit_index, bottom_segment, 0, bottom_commit_id)
+ }
Some(standin_sidx) => {
let outgoing_edges: Vec<_> = graph
.edges_directed(standin_sidx, Direction::Outgoing)
@@ -189,7 +184,6 @@ pub fn split_commit_into_segment(
standin_sidx,
0,
Some(bottom_commit_id),
- 0,
);
let s = &mut graph[standin_sidx];
s.commits = bottom_segment.commits;
@@ -301,7 +295,6 @@ fn split_connections(
})
.transpose()?,
dst_id: edge.weight.dst_id,
- parent_order: edge.weight.parent_order,
},
);
}
@@ -406,7 +399,6 @@ pub fn try_split_non_empty_segment_at_branch(
segment_below,
0,
info.id,
- 0,
);
Ok(Some(segment_below))
}
@@ -442,15 +434,12 @@ pub fn queue_parents(
let mut queue_is_exhausted = false;
if parent_ids.len() > 1 {
let limit_per_parent = limit.per_parent(parent_ids.len());
- for (parent_order, pid) in parent_ids.iter().enumerate() {
+ for pid in parent_ids.iter() {
let instruction = Instruction::ConnectNewSegment {
parent_above: current_sidx,
at_commit: current_cidx
.try_into()
.context("commit index does not fit into u32")?,
- parent_order: parent_order
- .try_into()
- .context("commit parent position does not fit into u32")?,
};
let info = find(commit_graph, objects, *pid, buf)?;
queue_is_exhausted =
@@ -998,7 +987,6 @@ pub fn try_queue_remote_tracking_branches(
})
}
-#[expect(clippy::too_many_arguments)]
pub fn possibly_split_occupied_segment(
graph: &mut Graph,
seen: &mut gix::revwalk::graph::IdMap,
@@ -1007,7 +995,6 @@ pub fn possibly_split_occupied_segment(
propagated_flags: CommitFlags,
src_sidx: SegmentIndex,
limit: Limit,
- parent_order: u32,
) -> anyhow::Result<()> {
let Entry::Occupied(mut existing_sidx) = seen.entry(id) else {
bail!("BUG: Can only work with occupied entries")
@@ -1079,15 +1066,7 @@ pub fn possibly_split_occupied_segment(
// Standins will cause this, avoid self-connection.
if top_sidx != bottom_sidx {
- graph.connect_segments_with_ids(
- top_sidx,
- top_cidx,
- None,
- bottom_sidx,
- bottom_cidx,
- None,
- parent_order,
- );
+ graph.connect_segments_with_ids(top_sidx, top_cidx, None, bottom_sidx, bottom_cidx, None);
}
let top_flags = top_cidx
.map(|cidx| graph[top_sidx].commits[cidx].flags)
diff --git a/crates/but-graph/src/lib.rs b/crates/but-graph/src/lib.rs
index 685a1aedc0c..0015fab59f8 100644
--- a/crates/but-graph/src/lib.rs
+++ b/crates/but-graph/src/lib.rs
@@ -379,26 +379,17 @@ pub struct Edge {
dst: Option,
/// The commit id at `dst` in the segment commit list.
dst_id: Option,
- /// The position of this edge's destination among the source commit's parents.
- /// For a merge commit with parents `[A, B]`, the edge to `A` has `parent_order = 0`
- /// and the edge to `B` has `parent_order = 1`.
- /// Deliberately not `usize`: `Edge` is stored in every petgraph edge slot,
- /// and widening this field pushes hot edge storage over an important size boundary.
- ///
- /// This limit would only be reached if there is a merge with 4.3 billion commits.
- pub(crate) parent_order: u32,
}
impl Edge {
- /// Return the 0-based position of this edge's destination among the source commit's parents.
- /// For instance, if the source is a merge commit and this edge represents the connection
- /// to the second parent, the output will be `Some(1)`.
- ///
- /// This is `None` when the edge does not point at a concrete destination commit.
- /// That happens for synthetic graph structure such as empty virtual segments,
- /// where there is no Git commit parent for this edge to identify.
- pub fn parent_order(&self) -> Option {
- self.dst_id.map(|_| self.parent_order)
+ /// The parent commit this edge points at; `None` for synthetic edges with no concrete commit.
+ pub fn dst_id(&self) -> Option {
+ self.dst_id
+ }
+
+ /// The source commit id this edge emanates from, if it starts at a concrete commit.
+ pub fn src_id(&self) -> Option {
+ self.src_id
}
/// Useful when reusing an edge to assure it doesn't list commits that don't exist in `src_idx` and `dst_idx` anymore.
diff --git a/crates/but-graph/src/projection/workspace/init.rs b/crates/but-graph/src/projection/workspace/init.rs
index ed1a35a056d..a066ba8ff11 100644
--- a/crates/but-graph/src/projection/workspace/init.rs
+++ b/crates/but-graph/src/projection/workspace/init.rs
@@ -327,10 +327,10 @@ impl Graph {
fn workspace_stacks(&self, frame: &WorkspaceFrame) -> anyhow::Result> {
let mut stacks = vec![];
let (lowest_base, lowest_base_sidx) = (frame.lower_bound, frame.lower_bound_segment_id);
- let preferred_parent_order_by_commit = frame
+ let preferred_next_segment_by_commit = frame
.entrypoint_sidx
.map(|entrypoint| {
- self.preferred_parent_order_by_commit(
+ self.preferred_next_segment_by_commit(
frame.ws_tip_segment_id,
entrypoint,
frame.lower_bound_segment_id,
@@ -339,17 +339,19 @@ impl Graph {
.unwrap_or_default();
if frame.kind.has_managed_ref() {
let mut used_stack_ids = BTreeSet::default();
- for stack_top_sidx in self
+ let stack_tops: Vec<_> = self
.inner
- .neighbors_directed(frame.ws_tip_segment_id, Direction::Outgoing)
- {
+ .edges_directed(frame.ws_tip_segment_id, Direction::Outgoing)
+ .map(|edge| edge.target())
+ .collect();
+ for stack_top_sidx in stack_tops {
let stack_segment = &self[stack_top_sidx];
let has_seen_base = RefCell::new(false);
stacks.extend(
self.collect_stack_segments(
stack_top_sidx,
frame.entrypoint_sidx,
- &preferred_parent_order_by_commit,
+ &preferred_next_segment_by_commit,
|s| {
let stop = true;
// The lowest base is a segment that all stacks will run into.
@@ -415,6 +417,9 @@ impl Graph {
}),
);
}
+ // Stacks already arrive in workspace-parent-array order: the workspace commit's outgoing
+ // edges are sorted by the parent array during graph construction (post.rs
+ // `stack_entry_commit`), and collecting them above preserves that order — no re-sort here.
} else {
let start = &self[frame.ws_tip_segment_id];
let has_seen_base = RefCell::new(false);
@@ -422,7 +427,7 @@ impl Graph {
.collect_stack_segments(
start.id,
None,
- &preferred_parent_order_by_commit,
+ &preferred_next_segment_by_commit,
|s| {
let stop = true;
if segment_name_is_special(s) {
@@ -685,23 +690,20 @@ impl Graph {
.is_some_and(|commit| commit.id == commit_id)
}
- /// Return commit-specific parent choices for the path from `start` down to `entrypoint`.
+ /// Return commit-specific next-segment choices for the path from `start` down to `entrypoint`.
///
- /// Each graph edge knows which parent of its source commit it represents.
- /// For a merge commit `M` with parents `[A, B]`, the edge from `M` to `A`
- /// has parent order `0`, and the edge from `M` to `B` has parent order `1`.
- /// This records the edge parent order for each source commit on the path
- /// that reaches the entrypoint, then sorts by commit id so stack collection
+ /// For each merge commit on the path that reaches the entrypoint, this records which outgoing
+ /// segment to descend into to stay on that path, then sorts by commit id so stack collection
/// can binary-search it while walking downward.
- fn preferred_parent_order_by_commit(
+ fn preferred_next_segment_by_commit(
&self,
start: SegmentIndex,
entrypoint: SegmentIndex,
lower_bound: Option,
- ) -> Vec<(ObjectId, u32)> {
+ ) -> Vec<(ObjectId, SegmentIndex)> {
let mut seen = self.seen_table();
let mut out = Vec::new();
- if !self.collect_preferred_parent_order_by_commit(
+ if !self.collect_preferred_next_segment_by_commit(
start,
entrypoint,
lower_bound,
@@ -718,10 +720,10 @@ impl Graph {
/// Depth-first search for one downward segment path from `current` to `entrypoint`.
///
/// Edges point from a segment toward its parents in commit history. When the recursion finds
- /// `entrypoint`, it returns `true` and unwinds, appending one `(source_commit_id, parent_order)`
- /// pair for each edge on the successful path. The source commit id is enough to identify the
- /// merge commit during later stack walking, and `parent_order` identifies which parent edge
- /// keeps that walk on the entrypoint path.
+ /// `entrypoint`, it returns `true` and unwinds, appending one `(source_commit_id, next_segment)`
+ /// pair for each edge on the successful path. The source commit id identifies the merge commit
+ /// during later stack walking, and `next_segment` is the segment to descend into to keep that
+ /// walk on the entrypoint path.
///
/// If `lower_bound` is set, the search does not descend past it. Reaching the lower-bound
/// segment fails the current branch unless that segment is also `entrypoint`. This keeps the
@@ -732,13 +734,13 @@ impl Graph {
/// graph, so the search cost is linear in the bounded reachable segment subgraph from `current`:
/// `O(segments + edges)` up to the lower bound, with `O(segments)` visited storage and
/// `O(path length)` output.
- fn collect_preferred_parent_order_by_commit(
+ fn collect_preferred_next_segment_by_commit(
&self,
current: SegmentIndex,
entrypoint: SegmentIndex,
lower_bound: Option,
seen: &mut SeenTable,
- out: &mut Vec<(ObjectId, u32)>,
+ out: &mut Vec<(ObjectId, SegmentIndex)>,
) -> bool {
if current == entrypoint {
return true;
@@ -752,7 +754,7 @@ impl Graph {
for edge in self.inner.edges_directed(current, Direction::Outgoing) {
let before = out.len();
- if self.collect_preferred_parent_order_by_commit(
+ if self.collect_preferred_next_segment_by_commit(
edge.target(),
entrypoint,
lower_bound,
@@ -760,7 +762,7 @@ impl Graph {
out,
) {
if let Some(src_id) = edge.weight().src_id {
- out.push((src_id, edge.weight().parent_order));
+ out.push((src_id, edge.target()));
}
return true;
}
@@ -840,7 +842,7 @@ impl Graph {
/// Also return the segment that we stopped at.
/// **Important**: `stop` is not called with `start`, this is a feature.
///
- /// `preferred_parent_order_by_commit` is a sorted list of `(commit_id, parent_order)` pairs
+ /// `preferred_next_segment_by_commit` is a sorted list of `(commit_id, next_segment)` pairs
/// produced from the path between the workspace tip and the traversal entrypoint. During the
/// stack walk, each outgoing edge is matched by its source commit id and parent order. If a
/// matching hint exists, that edge is followed so a merge can walk through the parent that
@@ -855,14 +857,14 @@ impl Graph {
fn collect_first_parent_segments_until<'a>(
&'a self,
start: &'a Segment,
- preferred_parent_order_by_commit: &[(ObjectId, u32)],
+ preferred_next_segment_by_commit: &[(ObjectId, SegmentIndex)],
mut stop: impl FnMut(&Segment) -> bool,
) -> (Vec<&'a Segment>, Option<&'a Segment>) {
let mut out = vec![start.id];
let mut stopped_at = None;
- self.visit_segments_downward_with_parent_order_hints_exclude_start(
+ self.visit_segments_downward_with_segment_hints_exclude_start(
start.id,
- preferred_parent_order_by_commit,
+ preferred_next_segment_by_commit,
|next| {
if stop(next)
&& !(next.ref_info.is_none()
@@ -886,17 +888,17 @@ impl Graph {
/// Visit the ancestry of `start` along the preferred parent path, itself excluded, until `stop` returns `true`.
/// **Important**: `stop` is not called with `start`, this is a feature.
///
- /// `preferred_parent_order_by_commit` is a sorted list of `(commit_id, parent_order)` pairs.
- /// Each outgoing edge is matched by its source commit id and parent order. If a matching
+ /// `preferred_next_segment_by_commit` is a sorted list of `(commit_id, next_segment)` pairs.
+ /// Each outgoing edge is matched by its source commit id. If a matching
/// hint exists, that edge is followed. Without a hint for the current source commit, the
/// walk falls back to the first-parent edge.
- pub fn visit_segments_downward_with_parent_order_hints_exclude_start(
+ pub fn visit_segments_downward_with_segment_hints_exclude_start(
&self,
start: SegmentIndex,
- preferred_parent_order_by_commit: &[(ObjectId, u32)],
+ preferred_next_segment_by_commit: &[(ObjectId, SegmentIndex)],
mut stop: impl FnMut(&Segment) -> bool,
) {
- let mut next = self.next_segment_downward(start, preferred_parent_order_by_commit);
+ let mut next = self.next_segment_downward(start, preferred_next_segment_by_commit);
let mut seen = self.seen_table();
while let Some(sidx) = next {
let segment = &self[sidx];
@@ -904,7 +906,7 @@ impl Graph {
return;
}
next = if seen.insert_unseen(sidx) {
- self.next_segment_downward(sidx, preferred_parent_order_by_commit)
+ self.next_segment_downward(sidx, preferred_next_segment_by_commit)
} else {
None
};
@@ -914,23 +916,21 @@ impl Graph {
fn next_segment_downward(
&self,
segment: SegmentIndex,
- preferred_parent_order_by_commit: &[(ObjectId, u32)],
+ preferred_next_segment_by_commit: &[(ObjectId, SegmentIndex)],
) -> Option {
- let preferred_parent_order = self[segment]
- .commits
- .last()
- .and_then(|commit| {
- preferred_parent_order_by_commit
- .binary_search_by(|(id, _)| id.cmp(&commit.id))
- .ok()
- })
- .map(|hint_idx| preferred_parent_order_by_commit[hint_idx].1);
- for edge in self.inner.edges_directed(segment, Direction::Outgoing) {
- if preferred_parent_order.is_none_or(|order| edge.weight().parent_order == order) {
- return Some(edge.target());
- }
+ // A hint records the exact next segment to descend into to stay on the entrypoint path, so
+ // follow it directly when present (it was captured from a real outgoing edge).
+ if let Some(commit) = self[segment].commits.last()
+ && let Ok(hint_idx) =
+ preferred_next_segment_by_commit.binary_search_by(|(id, _)| id.cmp(&commit.id))
+ {
+ return Some(preferred_next_segment_by_commit[hint_idx].1);
}
- None
+ // No hint: follow the first-parent edge (outgoing edges are kept in parent order).
+ self.inner
+ .edges_directed(segment, Direction::Outgoing)
+ .next()
+ .map(|edge| edge.target())
}
/// Visit all segments from `start`, excluding, and return once `find` returns something mapped from the
@@ -971,7 +971,7 @@ impl Graph {
&self,
from: SegmentIndex,
mut entrypoint_sidx: Option,
- preferred_parent_order_by_commit: &[(ObjectId, u32)],
+ preferred_next_segment_by_commit: &[(ObjectId, SegmentIndex)],
mut is_one_past_end_of_stack_segment: impl FnMut(&Segment) -> bool,
mut starts_next_stack_segment: impl FnMut(&Segment) -> bool,
mut discard_stack: impl FnMut(&StackSegment) -> bool,
@@ -982,7 +982,7 @@ impl Graph {
let start = &self[from];
let (segments, stopped_at) = self.collect_first_parent_segments_until(
start,
- preferred_parent_order_by_commit,
+ preferred_next_segment_by_commit,
&mut is_one_past_end_of_stack_segment,
);
let mut segment = StackSegment::from_graph_segments(&segments, self)?;
@@ -1182,10 +1182,10 @@ impl WorkspaceState {
// mode keeps the branch shell, but prunes integrated target/base commits from it.
prune_integrated_stack_segments(stack, &prune_segments, keep_if_fully_integrated);
remove_empty_branches(stack, metadata, &keep_empty_segment_ids);
- if upstream_advanced_past_target {
- // Pruning moved the stack's bottom; refresh its base to the new fork point.
- stack.recompute_last_segment_base(&graph.inner);
- }
+ // Pruning moved the stack's bottom; refresh its base to its own fork point with the
+ // target rather than leaving the pre-prune global merge base. Every branch has its
+ // own base.
+ stack.recompute_last_segment_base(&graph.inner);
}
self.stacks.retain(|stack| !stack.segments.is_empty());
}
diff --git a/crates/but-graph/tests/fixtures/scenarios.sh b/crates/but-graph/tests/fixtures/scenarios.sh
index f34bdfe3fea..1772e0d8275 100644
--- a/crates/but-graph/tests/fixtures/scenarios.sh
+++ b/crates/but-graph/tests/fixtures/scenarios.sh
@@ -193,7 +193,7 @@ mkdir ws
commit A
git checkout -b B main
commit B
- create_workspace_commit_once B A
+ create_workspace_commit_once A B
)
git init advanced-stack-tip-outside-workspace
@@ -1215,7 +1215,7 @@ EOF
setup_target_to_match_main
git checkout B
commit B1
- create_workspace_commit_once B A
+ create_workspace_commit_once A B
)
git init two-branches-one-above-base
@@ -1256,7 +1256,7 @@ EOF
git branch C2-1
git branch C2-2
git branch C2-3
- create_workspace_commit_aggressively C B A
+ create_workspace_commit_aggressively A B C
)
mkdir edit-commit
diff --git a/crates/but-graph/tests/fixtures/shared.sh b/crates/but-graph/tests/fixtures/shared.sh
index e973c67c5d7..7f834615262 100644
--- a/crates/but-graph/tests/fixtures/shared.sh
+++ b/crates/but-graph/tests/fixtures/shared.sh
@@ -61,11 +61,14 @@ function create_workspace_commit_once() {
fi
fi
- git checkout -b gitbutler/workspace
- if [ $# == 1 ] || [ $# == 0 ]; then
- git commit --allow-empty -m "$workspace_commit_subject"
+ if [ $# -gt 1 ]; then
+ # First arg becomes the workspace commit's first parent; the rest merge in order.
+ git checkout "$1"
+ git checkout -b gitbutler/workspace
+ git merge --no-ff -m "$workspace_commit_subject" "${@:2}"
else
- git merge --no-ff -m "$workspace_commit_subject" "${@}"
+ git checkout -b gitbutler/workspace
+ git commit --allow-empty -m "$workspace_commit_subject"
fi
}
diff --git a/crates/but-graph/tests/graph/init/mod.rs b/crates/but-graph/tests/graph/init/mod.rs
index d483bdcb18a..46306d118c2 100644
--- a/crates/but-graph/tests/graph/init/mod.rs
+++ b/crates/but-graph/tests/graph/init/mod.rs
@@ -154,7 +154,6 @@ fn detached() -> anyhow::Result<()> {
dst_id: Some(
Sha1(fafd9d08a839d99db60b222cd58e2e0bfaf1f7b2),
),
- parent_order: 0,
},
},
free_node: NodeIndex(4294967295),
diff --git a/crates/but-graph/tests/graph/init/with_workspace.rs b/crates/but-graph/tests/graph/init/with_workspace.rs
index f1b245bf078..de577c8db5c 100644
--- a/crates/but-graph/tests/graph/init/with_workspace.rs
+++ b/crates/but-graph/tests/graph/init/with_workspace.rs
@@ -132,10 +132,10 @@ fn workspace_with_only_local_target() -> anyhow::Result<()> {
fn reproduce_11483() -> anyhow::Result<()> {
let (repo, mut meta) = read_only_in_memory_scenario("ws/reproduce-11483")?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * 3562fcd (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 9d65e48 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 7236012 (A) A
- * | 68c8a9d (B) B
+ | * 68c8a9d (B) B
+ * | 7236012 (A) A
|/
* 3183e43 (origin/main, main, below) M1
");
@@ -663,6 +663,41 @@ fn single_stack_ws_insertions() -> anyhow::Result<()> {
Ok(())
}
+#[test]
+fn first_parent_reachability_traverses_empty_segments() -> anyhow::Result<()> {
+ // Regression guard: deriving the first-parent edge from the source commit's `parent_ids` must
+ // not dead-end when a commit-less branch segment sits on the first-parent path. The same
+ // `ws/single-stack-ambiguous` setup as `single_stack_ws_insertions` puts the empty segments
+ // `A-empty-03`/`A-empty-01` between commit `2a31450` (B's side) and `70bde6b` (origin/B / A).
+ // A first-parent excluded walk from `B` must descend through those empties to reach `70bde6b`
+ // and `fafd9d0`; otherwise they leak into `origin/B..B` even though both are first-parent
+ // ancestors of `B`.
+ let (repo, mut meta) = read_only_in_memory_scenario("ws/single-stack-ambiguous")?;
+ add_stack_with_segments(
+ &mut meta,
+ 0,
+ "B-empty",
+ StackState::InWorkspace,
+ &["B", "A-empty-03", "not-A-empty-02", "A-empty-01", "A"],
+ );
+ let graph =
+ Graph::from_head(&repo, &*meta, project_meta(&*meta), standard_options())?.validated()?;
+
+ let b = repo.rev_parse_single("B")?.detach(); // 70e9a36, above the empty chain
+ let origin_b = repo.rev_parse_single("origin/B")?.detach(); // 70bde6b, below it
+
+ // `origin/B` (70bde6b) is a first-parent ancestor of `B`, so nothing is reachable from it but
+ // not from `B`.
+ let leaked =
+ graph.find_commit_ids_reachable_from_a_not_b(origin_b, b, but_graph::FirstParent::Yes)?;
+ assert!(
+ leaked.is_empty(),
+ "empty segments on the first-parent path dead-ended the excluded walk, leaking \
+ first-parent ancestors of B into origin/B..B: {leaked:?}"
+ );
+ Ok(())
+}
+
#[test]
fn single_stack() -> anyhow::Result<()> {
let (repo, mut meta) = read_only_in_memory_scenario("ws/single-stack")?;
@@ -823,11 +858,11 @@ fn single_merge_into_main_base_archived() -> anyhow::Result<()> {
insta::assert_snapshot!(graph_workspace(&graph.into_workspace()?), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 0cc5a6f
- ├── ≡📙:7:merge on 0cc5a6f {0}
- │ └── 📙:7:merge
- └── ≡📙:3:C on 0cc5a6f {1}
- └── 📙:3:C
- └── ·c6d714c (🏘️)
+ ├── ≡📙:3:C on 0cc5a6f {1}
+ │ └── 📙:3:C
+ │ └── ·c6d714c (🏘️)
+ └── ≡📙:7:merge on 0cc5a6f {0}
+ └── 📙:7:merge
");
Ok(())
}
@@ -1792,38 +1827,38 @@ fn two_stacks_many_refs() -> anyhow::Result<()> {
├── 👉📕►►►:0[0]:gitbutler/workspace[🌳]
│ └── ·298d938 (⌂|🏘|01)
+ │ ├── 📙►:8[1]:S1
+ │ │ └── 📙►:9[2]:G
+ │ │ └── 📙►:10[3]:F
+ │ │ └── ·16f132b (⌂|🏘|01)
+ │ │ └── 📙►:11[4]:D
+ │ │ └── 📙►:12[5]:E
+ │ │ └── ·917b9da (⌂|🏘|01)
+ │ │ └── ►:2[6]:main <> origin/main →:1:
+ │ │ └── 🏁·fafd9d0 (⌂|🏘|✓|11)
│ ├── 📙►:5[1]:C
│ │ └── 📙►:6[2]:B
- │ │ └── ►:2[6]:main <> origin/main →:1:
- │ │ └── 🏁·fafd9d0 (⌂|🏘|✓|11)
- │ ├── 📙►:7[1]:A
- │ │ └── →:2: (main →:1:)
- │ └── 📙►:8[1]:S1
- │ └── 📙►:9[2]:G
- │ └── 📙►:10[3]:F
- │ └── ·16f132b (⌂|🏘|01)
- │ └── 📙►:11[4]:D
- │ └── 📙►:12[5]:E
- │ └── ·917b9da (⌂|🏘|01)
- │ └── →:2: (main →:1:)
+ │ │ └── →:2: (main →:1:)
+ │ └── 📙►:7[1]:A
+ │ └── →:2: (main →:1:)
└── ►:1[0]:origin/main →:2:
└── →:2: (main →:1:)
");
insta::assert_snapshot!(graph_workspace(&graph.into_workspace()?), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on fafd9d0
+ ├── ≡📙:8:S1 on fafd9d0 {3}
+ │ ├── 📙:8:S1
+ │ ├── 📙:9:G
+ │ ├── 📙:10:F
+ │ │ └── ·16f132b (🏘️)
+ │ └── 📙:12:E
+ │ └── ·917b9da (🏘️)
├── ≡📙:5:C on fafd9d0 {1}
│ ├── 📙:5:C
│ └── 📙:6:B
- ├── ≡📙:7:A on fafd9d0 {2}
- │ └── 📙:7:A
- └── ≡📙:8:S1 on fafd9d0 {3}
- ├── 📙:8:S1
- ├── 📙:9:G
- ├── 📙:10:F
- │ └── ·16f132b (🏘️)
- └── 📙:12:E
- └── ·917b9da (🏘️)
+ └── ≡📙:7:A on fafd9d0 {2}
+ └── 📙:7:A
");
let graph = Graph::from_commit_traversal(
@@ -1839,37 +1874,37 @@ fn two_stacks_many_refs() -> anyhow::Result<()> {
├── 📕►►►:1[0]:gitbutler/workspace[🌳]
│ └── ·298d938 (⌂|🏘)
+ │ ├── 👉📙►:8[1]:S1
+ │ │ └── 📙►:9[2]:G
+ │ │ └── 📙►:10[3]:F
+ │ │ └── ·16f132b (⌂|🏘|01)
+ │ │ └── 📙►:11[4]:D
+ │ │ └── 📙►:12[5]:E
+ │ │ └── ·917b9da (⌂|🏘|01)
+ │ │ └── ►:3[6]:main <> origin/main →:2:
+ │ │ └── 🏁·fafd9d0 (⌂|🏘|✓|11)
│ ├── 📙►:5[1]:C
│ │ └── 📙►:6[2]:B
- │ │ └── ►:3[6]:main <> origin/main →:2:
- │ │ └── 🏁·fafd9d0 (⌂|🏘|✓|11)
- │ ├── 📙►:7[1]:A
- │ │ └── →:3: (main →:2:)
- │ └── 👉📙►:8[1]:S1
- │ └── 📙►:9[2]:G
- │ └── 📙►:10[3]:F
- │ └── ·16f132b (⌂|🏘|01)
- │ └── 📙►:11[4]:D
- │ └── 📙►:12[5]:E
- │ └── ·917b9da (⌂|🏘|01)
- │ └── →:3: (main →:2:)
+ │ │ └── →:3: (main →:2:)
+ │ └── 📙►:7[1]:A
+ │ └── →:3: (main →:2:)
└── ►:2[0]:origin/main →:3:
└── →:3: (main →:2:)
");
insta::assert_snapshot!(graph_workspace(&graph.into_workspace()?), @"
📕🏘️:1:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on fafd9d0
+ ├── ≡👉📙:8:S1 on fafd9d0 {3}
+ │ ├── 👉📙:8:S1
+ │ ├── 📙:9:G
+ │ ├── 📙:10:F
+ │ │ └── ·16f132b (🏘️)
+ │ └── 📙:12:E
+ │ └── ·917b9da (🏘️)
├── ≡📙:5:C on fafd9d0 {1}
│ ├── 📙:5:C
│ └── 📙:6:B
- ├── ≡📙:7:A on fafd9d0 {2}
- │ └── 📙:7:A
- └── ≡👉📙:8:S1 on fafd9d0 {3}
- ├── 👉📙:8:S1
- ├── 📙:9:G
- ├── 📙:10:F
- │ └── ·16f132b (🏘️)
- └── 📙:12:E
- └── ·917b9da (🏘️)
+ └── ≡📙:7:A on fafd9d0 {2}
+ └── 📙:7:A
");
Ok(())
}
@@ -3973,21 +4008,20 @@ fn multi_lane_with_shared_segment_one_integrated() -> anyhow::Result<()> {
let (repo, mut meta) =
read_only_in_memory_scenario("ws/multi-lane-with-shared-segment-one-integrated")?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- *-. 2b30d94 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ *-. 1cf594d (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\ \
- | | * acdc49a (B) B2
- | | * f0117e0 B1
- * | | 9895054 (D) D1
- * | | de625cc (C) C3
- * | | 23419f8 C2
- * | | 5dc4389 C1
+ | | * 9895054 (D) D1
+ | | * de625cc (C) C3
+ | | * 23419f8 C2
+ | | * 5dc4389 C1
+ | * | acdc49a (B) B2
+ | * | f0117e0 B1
| |/
- |/|
| | * c08dc6b (origin/main) Merge branch 'A' into soon-remote-main
| | |\
- | | |/
- | |/|
- | * | 0bad3af (A) A1
+ | |_|/
+ |/| |
+ * | | 0bad3af (A) A1
|/ /
* | d4f537e (shared) S3
* | b448757 S2
@@ -4008,59 +4042,59 @@ fn multi_lane_with_shared_segment_one_integrated() -> anyhow::Result<()> {
insta::assert_snapshot!(graph_tree(&graph), @"
├── 👉📕►►►:0[0]:gitbutler/workspace[🌳]
- │ └── ·2b30d94 (⌂|🏘|01)
- │ ├── ►:3[1]:D
- │ │ └── ·9895054 (⌂|🏘|01)
- │ │ └── ►:6[2]:C
- │ │ ├── ·de625cc (⌂|🏘|01)
- │ │ ├── ·23419f8 (⌂|🏘|01)
- │ │ └── ·5dc4389 (⌂|🏘|01)
- │ │ └── ►:7[3]:shared
- │ │ ├── ·d4f537e (⌂|🏘|✓|01)
- │ │ ├── ·b448757 (⌂|🏘|✓|01)
- │ │ └── ·e9a378d (⌂|🏘|✓|01)
- │ │ └── ►:2[4]:main <> origin/main →:1:
- │ │ └── 🏁·3183e43 (⌂|🏘|✓|11)
- │ ├── ►:4[1]:A
+ │ └── ·1cf594d (⌂|🏘|01)
+ │ ├── ►:3[1]:A
│ │ └── ·0bad3af (⌂|🏘|✓|01)
- │ │ └── →:7: (shared)
- │ └── ►:5[1]:B
- │ ├── ·acdc49a (⌂|🏘|01)
- │ └── ·f0117e0 (⌂|🏘|01)
- │ └── →:7: (shared)
+ │ │ └── ►:6[3]:shared
+ │ │ ├── ·d4f537e (⌂|🏘|✓|01)
+ │ │ ├── ·b448757 (⌂|🏘|✓|01)
+ │ │ └── ·e9a378d (⌂|🏘|✓|01)
+ │ │ └── ►:2[4]:main <> origin/main →:1:
+ │ │ └── 🏁·3183e43 (⌂|🏘|✓|11)
+ │ ├── ►:4[1]:B
+ │ │ ├── ·acdc49a (⌂|🏘|01)
+ │ │ └── ·f0117e0 (⌂|🏘|01)
+ │ │ └── →:6: (shared)
+ │ └── ►:5[1]:D
+ │ └── ·9895054 (⌂|🏘|01)
+ │ └── ►:7[2]:C
+ │ ├── ·de625cc (⌂|🏘|01)
+ │ ├── ·23419f8 (⌂|🏘|01)
+ │ └── ·5dc4389 (⌂|🏘|01)
+ │ └── →:6: (shared)
└── ►:1[0]:origin/main →:2:
└── 🟣c08dc6b (✓)
├── →:2: (main →:1:)
- └── →:4: (A)
+ └── →:3: (A)
");
// A is still shown despite it being fully integrated, as it's still enclosed by the
// workspace tip and the fork-point, at least when we provide the previous known location of the target.
insta::assert_snapshot!(graph_workspace(&graph.into_workspace()?), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main⇣1 on 3183e43
- ├── ≡:3:D on 3183e43
- │ ├── :3:D
- │ │ └── ·9895054 (🏘️)
- │ ├── :6:C
- │ │ ├── ·de625cc (🏘️)
- │ │ ├── ·23419f8 (🏘️)
- │ │ └── ·5dc4389 (🏘️)
- │ └── :7:shared
+ ├── ≡:3:A on 3183e43
+ │ ├── :3:A
+ │ │ └── ·0bad3af (🏘️|✓)
+ │ └── :6:shared
│ ├── ·d4f537e (🏘️|✓)
│ ├── ·b448757 (🏘️|✓)
│ └── ·e9a378d (🏘️|✓)
- ├── ≡:4:A on 3183e43
- │ ├── :4:A
- │ │ └── ·0bad3af (🏘️|✓)
- │ └── :7:shared
+ ├── ≡:4:B on 3183e43
+ │ ├── :4:B
+ │ │ ├── ·acdc49a (🏘️)
+ │ │ └── ·f0117e0 (🏘️)
+ │ └── :6:shared
│ ├── ·d4f537e (🏘️|✓)
│ ├── ·b448757 (🏘️|✓)
│ └── ·e9a378d (🏘️|✓)
- └── ≡:5:B on 3183e43
- ├── :5:B
- │ ├── ·acdc49a (🏘️)
- │ └── ·f0117e0 (🏘️)
- └── :7:shared
+ └── ≡:5:D on 3183e43
+ ├── :5:D
+ │ └── ·9895054 (🏘️)
+ ├── :7:C
+ │ ├── ·de625cc (🏘️)
+ │ ├── ·23419f8 (🏘️)
+ │ └── ·5dc4389 (🏘️)
+ └── :6:shared
├── ·d4f537e (🏘️|✓)
├── ·b448757 (🏘️|✓)
└── ·e9a378d (🏘️|✓)
@@ -4071,17 +4105,17 @@ fn multi_lane_with_shared_segment_one_integrated() -> anyhow::Result<()> {
Graph::from_head(&repo, &*meta, project_meta(&*meta), standard_options())?.validated()?;
insta::assert_snapshot!(graph_workspace(&graph.into_workspace()?), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main⇣1 on d4f537e
- ├── ≡:3:D on d4f537e
- │ ├── :3:D
- │ │ └── ·9895054 (🏘️)
- │ └── :6:C
- │ ├── ·de625cc (🏘️)
- │ ├── ·23419f8 (🏘️)
- │ └── ·5dc4389 (🏘️)
- └── ≡:5:B on d4f537e
- └── :5:B
- ├── ·acdc49a (🏘️)
- └── ·f0117e0 (🏘️)
+ ├── ≡:4:B on d4f537e
+ │ └── :4:B
+ │ ├── ·acdc49a (🏘️)
+ │ └── ·f0117e0 (🏘️)
+ └── ≡:5:D on d4f537e
+ ├── :5:D
+ │ └── ·9895054 (🏘️)
+ └── :7:C
+ ├── ·de625cc (🏘️)
+ ├── ·23419f8 (🏘️)
+ └── ·5dc4389 (🏘️)
");
Ok(())
}
@@ -4090,16 +4124,16 @@ fn multi_lane_with_shared_segment_one_integrated() -> anyhow::Result<()> {
fn multi_lane_with_shared_segment() -> anyhow::Result<()> {
let (repo, mut meta) = read_only_in_memory_scenario("ws/multi-lane-with-shared-segment")?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- *-. 2b30d94 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ *-. 1cf594d (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\ \
- | | * acdc49a (B) B2
- | | * f0117e0 B1
- | * | 0bad3af (A) A1
+ | | * 9895054 (D) D1
+ | | * de625cc (C) C3
+ | | * 23419f8 C2
+ | | * 5dc4389 C1
+ | * | acdc49a (B) B2
+ | * | f0117e0 B1
| |/
- * | 9895054 (D) D1
- * | de625cc (C) C3
- * | 23419f8 C2
- * | 5dc4389 C1
+ * / 0bad3af (A) A1
|/
* d4f537e (shared) S3
* b448757 S2
@@ -4116,26 +4150,26 @@ fn multi_lane_with_shared_segment() -> anyhow::Result<()> {
insta::assert_snapshot!(graph_tree(&graph), @"
├── 👉📕►►►:0[0]:gitbutler/workspace[🌳]
- │ └── ·2b30d94 (⌂|🏘|1)
- │ ├── ►:2[1]:D
- │ │ └── ·9895054 (⌂|🏘|1)
- │ │ └── ►:6[2]:C
- │ │ ├── ·de625cc (⌂|🏘|1)
- │ │ ├── ·23419f8 (⌂|🏘|1)
- │ │ └── ·5dc4389 (⌂|🏘|1)
- │ │ └── ►:7[3]:shared
- │ │ ├── ·d4f537e (⌂|🏘|1)
- │ │ ├── ·b448757 (⌂|🏘|1)
- │ │ └── ·e9a378d (⌂|🏘|1)
- │ │ └── ►:5[4]:main <> origin/main →:1:
- │ │ └── 🏁·3183e43 (⌂|🏘|✓|1)
- │ ├── ►:3[1]:A
+ │ └── ·1cf594d (⌂|🏘|1)
+ │ ├── ►:2[1]:A
│ │ └── ·0bad3af (⌂|🏘|1)
- │ │ └── →:7: (shared)
- │ └── ►:4[1]:B
- │ ├── ·acdc49a (⌂|🏘|1)
- │ └── ·f0117e0 (⌂|🏘|1)
- │ └── →:7: (shared)
+ │ │ └── ►:6[3]:shared
+ │ │ ├── ·d4f537e (⌂|🏘|1)
+ │ │ ├── ·b448757 (⌂|🏘|1)
+ │ │ └── ·e9a378d (⌂|🏘|1)
+ │ │ └── ►:5[4]:main <> origin/main →:1:
+ │ │ └── 🏁·3183e43 (⌂|🏘|✓|1)
+ │ ├── ►:3[1]:B
+ │ │ ├── ·acdc49a (⌂|🏘|1)
+ │ │ └── ·f0117e0 (⌂|🏘|1)
+ │ │ └── →:6: (shared)
+ │ └── ►:4[1]:D
+ │ └── ·9895054 (⌂|🏘|1)
+ │ └── ►:7[2]:C
+ │ ├── ·de625cc (⌂|🏘|1)
+ │ ├── ·23419f8 (⌂|🏘|1)
+ │ └── ·5dc4389 (⌂|🏘|1)
+ │ └── →:6: (shared)
└── ►:1[0]:origin/main →:5:
└── 🟣bce0c5e (✓)
└── →:5: (main →:1:)
@@ -4144,29 +4178,29 @@ fn multi_lane_with_shared_segment() -> anyhow::Result<()> {
// Segments can definitely repeat
insta::assert_snapshot!(graph_workspace(&graph.into_workspace()?), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main⇣1 on 3183e43
- ├── ≡:2:D on 3183e43
- │ ├── :2:D
- │ │ └── ·9895054 (🏘️)
- │ ├── :6:C
- │ │ ├── ·de625cc (🏘️)
- │ │ ├── ·23419f8 (🏘️)
- │ │ └── ·5dc4389 (🏘️)
- │ └── :7:shared
+ ├── ≡:2:A on 3183e43
+ │ ├── :2:A
+ │ │ └── ·0bad3af (🏘️)
+ │ └── :6:shared
│ ├── ·d4f537e (🏘️)
│ ├── ·b448757 (🏘️)
│ └── ·e9a378d (🏘️)
- ├── ≡:3:A on 3183e43
- │ ├── :3:A
- │ │ └── ·0bad3af (🏘️)
- │ └── :7:shared
+ ├── ≡:3:B on 3183e43
+ │ ├── :3:B
+ │ │ ├── ·acdc49a (🏘️)
+ │ │ └── ·f0117e0 (🏘️)
+ │ └── :6:shared
│ ├── ·d4f537e (🏘️)
│ ├── ·b448757 (🏘️)
│ └── ·e9a378d (🏘️)
- └── ≡:4:B on 3183e43
- ├── :4:B
- │ ├── ·acdc49a (🏘️)
- │ └── ·f0117e0 (🏘️)
- └── :7:shared
+ └── ≡:4:D on 3183e43
+ ├── :4:D
+ │ └── ·9895054 (🏘️)
+ ├── :7:C
+ │ ├── ·de625cc (🏘️)
+ │ ├── ·23419f8 (🏘️)
+ │ └── ·5dc4389 (🏘️)
+ └── :6:shared
├── ·d4f537e (🏘️)
├── ·b448757 (🏘️)
└── ·e9a378d (🏘️)
@@ -4184,28 +4218,28 @@ fn multi_lane_with_shared_segment() -> anyhow::Result<()> {
// Checking out anything inside the workspace yields the same result.
insta::assert_snapshot!(graph_workspace(&graph.into_workspace()?), @"
📕🏘️:1:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main⇣1 on 3183e43
- ├── ≡:4:D on 3183e43
- │ ├── :4:D
- │ │ └── ·9895054 (🏘️)
- │ ├── :7:C
- │ │ ├── ·de625cc (🏘️)
- │ │ ├── ·23419f8 (🏘️)
- │ │ └── ·5dc4389 (🏘️)
+ ├── ≡👉:0:A on 3183e43
+ │ ├── 👉:0:A
+ │ │ └── ·0bad3af (🏘️)
│ └── :3:shared
│ ├── ·d4f537e (🏘️)
│ ├── ·b448757 (🏘️)
│ └── ·e9a378d (🏘️)
- ├── ≡👉:0:A on 3183e43
- │ ├── 👉:0:A
- │ │ └── ·0bad3af (🏘️)
+ ├── ≡:4:B on 3183e43
+ │ ├── :4:B
+ │ │ ├── ·acdc49a (🏘️)
+ │ │ └── ·f0117e0 (🏘️)
│ └── :3:shared
│ ├── ·d4f537e (🏘️)
│ ├── ·b448757 (🏘️)
│ └── ·e9a378d (🏘️)
- └── ≡:5:B on 3183e43
- ├── :5:B
- │ ├── ·acdc49a (🏘️)
- │ └── ·f0117e0 (🏘️)
+ └── ≡:5:D on 3183e43
+ ├── :5:D
+ │ └── ·9895054 (🏘️)
+ ├── :7:C
+ │ ├── ·de625cc (🏘️)
+ │ ├── ·23419f8 (🏘️)
+ │ └── ·5dc4389 (🏘️)
└── :3:shared
├── ·d4f537e (🏘️)
├── ·b448757 (🏘️)
@@ -4266,12 +4300,12 @@ fn dependent_branch_insertion() -> anyhow::Result<()> {
├── 👉📕►►►:0[0]:gitbutler/workspace[🌳]
│ └── ·335d6f2 (⌂|🏘|001)
- │ ├── 📙►:5[1]:dependent
- │ │ └── 📙►:6[2]:advanced-lane <> origin/advanced-lane →:4:
- │ │ └── ·cbc6713 (⌂|🏘|101)
- │ │ └── ►:2[3]:main <> origin/main →:1:
- │ │ └── 🏁·fafd9d0 (⌂|🏘|✓|111) ►lane
- │ └── →:2: (main →:1:)
+ │ ├── ►:2[3]:main <> origin/main →:1:
+ │ │ └── 🏁·fafd9d0 (⌂|🏘|✓|111) ►lane
+ │ └── 📙►:5[1]:dependent
+ │ └── 📙►:6[2]:advanced-lane <> origin/advanced-lane →:4:
+ │ └── ·cbc6713 (⌂|🏘|101)
+ │ └── →:2: (main →:1:)
├── ►:1[0]:origin/main →:2:
│ └── →:2: (main →:1:)
└── ►:4[0]:origin/advanced-lane →:6:
@@ -4302,12 +4336,12 @@ fn dependent_branch_insertion() -> anyhow::Result<()> {
├── 👉📕►►►:0[0]:gitbutler/workspace[🌳]
│ └── ·335d6f2 (⌂|🏘|001)
- │ ├── 📙►:5[1]:advanced-lane <> origin/advanced-lane →:4:
- │ │ └── 📙►:6[2]:dependent
- │ │ └── ·cbc6713 (⌂|🏘|101)
- │ │ └── ►:2[3]:main <> origin/main →:1:
- │ │ └── 🏁·fafd9d0 (⌂|🏘|✓|111) ►lane
- │ └── →:2: (main →:1:)
+ │ ├── ►:2[3]:main <> origin/main →:1:
+ │ │ └── 🏁·fafd9d0 (⌂|🏘|✓|111) ►lane
+ │ └── 📙►:5[1]:advanced-lane <> origin/advanced-lane →:4:
+ │ └── 📙►:6[2]:dependent
+ │ └── ·cbc6713 (⌂|🏘|101)
+ │ └── →:2: (main →:1:)
├── ►:1[0]:origin/main →:2:
│ └── →:2: (main →:1:)
└── ►:4[0]:origin/advanced-lane →:5:
@@ -4367,10 +4401,10 @@ fn multiple_stacks_with_shared_parent_and_remote() -> anyhow::Result<()> {
let (repo, mut meta) =
read_only_in_memory_scenario("ws/multiple-stacks-with-shared-segment-and-remote")?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * e982e8a (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * baed751 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * aff8449 (B-on-A) B-on-A
- * | 4f1bb32 (C-on-A) C-on-A
+ | * 4f1bb32 (C-on-A) C-on-A
+ * | aff8449 (B-on-A) B-on-A
|/
| * b627ca7 (origin/A) A-on-remote
|/
@@ -4385,15 +4419,15 @@ fn multiple_stacks_with_shared_parent_and_remote() -> anyhow::Result<()> {
insta::assert_snapshot!(graph_tree(&graph), @"
├── 👉📕►►►:0[0]:gitbutler/workspace[🌳]
- │ └── ·e982e8a (⌂|🏘|0001)
- │ ├── 📙►:3[1]:C-on-A
- │ │ └── ·4f1bb32 (⌂|🏘|0001)
+ │ └── ·baed751 (⌂|🏘|0001)
+ │ ├── ►:6[1]:B-on-A
+ │ │ └── ·aff8449 (⌂|🏘|0001)
│ │ └── ►:4[2]:A <> origin/A →:5:
│ │ └── ·e255adc (⌂|🏘|1101)
│ │ └── ►:2[3]:main <> origin/main →:1:
│ │ └── 🏁·fafd9d0 (⌂|🏘|✓|1111)
- │ └── ►:6[1]:B-on-A
- │ └── ·aff8449 (⌂|🏘|0001)
+ │ └── 📙►:3[1]:C-on-A
+ │ └── ·4f1bb32 (⌂|🏘|0001)
│ └── →:4: (A →:5:)
├── ►:1[0]:origin/main →:2:
│ └── →:2: (main →:1:)
@@ -4404,15 +4438,15 @@ fn multiple_stacks_with_shared_parent_and_remote() -> anyhow::Result<()> {
insta::assert_snapshot!(graph_workspace(&graph.into_workspace()?), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on fafd9d0
- ├── ≡📙:3:C-on-A on fafd9d0 {1}
- │ ├── 📙:3:C-on-A
- │ │ └── ·4f1bb32 (🏘️)
+ ├── ≡:6:B-on-A on fafd9d0
+ │ ├── :6:B-on-A
+ │ │ └── ·aff8449 (🏘️)
│ └── :4:A <> origin/A →:5:⇣1
│ ├── 🟣b627ca7
│ └── ❄️e255adc (🏘️)
- └── ≡:6:B-on-A on fafd9d0
- ├── :6:B-on-A
- │ └── ·aff8449 (🏘️)
+ └── ≡📙:3:C-on-A on fafd9d0 {1}
+ ├── 📙:3:C-on-A
+ │ └── ·4f1bb32 (🏘️)
└── :4:A <> origin/A →:5:⇣1
├── 🟣b627ca7
└── ❄️e255adc (🏘️)
@@ -4477,23 +4511,23 @@ fn a_stack_segment_can_be_a_segment_elsewhere_and_stack_order() -> anyhow::Resul
├── 👉📕►►►:0[0]:gitbutler/workspace[🌳]
│ └── ·873d056 (⌂|🏘|1)
- │ ├── 📙►:4[1]:lane
- │ │ └── ►:2[2]:anon:
- │ │ └── 🏁·fafd9d0 (⌂|🏘|1) ►main
- │ └── 📙►:3[1]:advanced-lane
- │ └── ·cbc6713 (⌂|🏘|1)
- │ └── →:2:
+ │ ├── 📙►:3[1]:advanced-lane
+ │ │ └── ·cbc6713 (⌂|🏘|1)
+ │ │ └── ►:2[2]:anon:
+ │ │ └── 🏁·fafd9d0 (⌂|🏘|1) ►main
+ │ └── 📙►:4[1]:lane
+ │ └── →:2:
└── ►:1[0]:origin/main
└── 🏁🟣da83717 (✓)
");
insta::assert_snapshot!(graph_workspace(&graph.into_workspace()?), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main⇣1 on fafd9d0
- ├── ≡📙:4:lane on fafd9d0 {0}
- │ └── 📙:4:lane
- └── ≡📙:3:advanced-lane on fafd9d0 {1}
- └── 📙:3:advanced-lane
- └── ·cbc6713 (🏘️)
+ ├── ≡📙:3:advanced-lane on fafd9d0 {1}
+ │ └── 📙:3:advanced-lane
+ │ └── ·cbc6713 (🏘️)
+ └── ≡📙:4:lane on fafd9d0 {0}
+ └── 📙:4:lane
");
Ok(())
}
@@ -5657,7 +5691,7 @@ fn branch_ahead_of_workspace() -> anyhow::Result<()> {
// The branches that are outside the workspace don't exist and segments are flattened.
insta::assert_snapshot!(graph_workspace(&graph.into_workspace()?), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main⇣2 on fafd9d0
- ├── ≡:6:B on fafd9d0
+ ├── ≡:6:B on 91bc3fc
│ └── :6:B
│ └── ·2f8f06d (🏘️)
├── ≡:7:C on fafd9d0
@@ -5811,22 +5845,22 @@ fn two_branches_one_advanced_two_parent_ws_commit_diverged_ttb() -> anyhow::Resu
├── 📕►►►:1[0]:gitbutler/workspace[🌳]
│ └── ·873d056 (⌂|🏘)
- │ ├── 👉📙►:4[1]:lane
- │ │ └── ►:0[2]:anon:
- │ │ └── 🏁·fafd9d0 (⌂|🏘|1) ►main
- │ └── 📙►:3[1]:advanced-lane
- │ └── ·cbc6713 (⌂|🏘)
- │ └── →:0:
+ │ ├── 📙►:3[1]:advanced-lane
+ │ │ └── ·cbc6713 (⌂|🏘)
+ │ │ └── ►:0[2]:anon:
+ │ │ └── 🏁·fafd9d0 (⌂|🏘|1) ►main
+ │ └── 👉📙►:4[1]:lane
+ │ └── →:0:
└── ►:2[0]:origin/main
└── 🏁🟣da83717 (✓)
");
insta::assert_snapshot!(graph_workspace(&graph.into_workspace()?), @"
📕🏘️:1:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main⇣1 on fafd9d0
- ├── ≡👉📙:4:lane on fafd9d0 {0}
- │ └── 👉📙:4:lane
- └── ≡📙:3:advanced-lane on fafd9d0 {1}
- └── 📙:3:advanced-lane
- └── ·cbc6713 (🏘️)
+ ├── ≡📙:3:advanced-lane on fafd9d0 {1}
+ │ └── 📙:3:advanced-lane
+ │ └── ·cbc6713 (🏘️)
+ └── ≡👉📙:4:lane on fafd9d0 {0}
+ └── 👉📙:4:lane
");
let graph = Graph::from_head(
@@ -5840,23 +5874,23 @@ fn two_branches_one_advanced_two_parent_ws_commit_diverged_ttb() -> anyhow::Resu
├── 👉📕►►►:0[0]:gitbutler/workspace[🌳]
│ └── ·873d056 (⌂|🏘|1)
- │ ├── 📙►:4[1]:lane
- │ │ └── ►:2[2]:anon:
- │ │ └── 🏁·fafd9d0 (⌂|🏘|✓|1) ►main
- │ └── 📙►:3[1]:advanced-lane
- │ └── ·cbc6713 (⌂|🏘|1)
- │ └── →:2:
+ │ ├── 📙►:3[1]:advanced-lane
+ │ │ └── ·cbc6713 (⌂|🏘|1)
+ │ │ └── ►:2[2]:anon:
+ │ │ └── 🏁·fafd9d0 (⌂|🏘|✓|1) ►main
+ │ └── 📙►:4[1]:lane
+ │ └── →:2:
└── ►:1[0]:origin/main
└── 🏁🟣da83717 (✓)
");
insta::assert_snapshot!(graph_workspace(&graph.into_workspace()?), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main⇣1 on fafd9d0
- ├── ≡📙:4:lane on fafd9d0 {0}
- │ └── 📙:4:lane
- └── ≡📙:3:advanced-lane on fafd9d0 {1}
- └── 📙:3:advanced-lane
- └── ·cbc6713 (🏘️)
+ ├── ≡📙:3:advanced-lane on fafd9d0 {1}
+ │ └── 📙:3:advanced-lane
+ │ └── ·cbc6713 (🏘️)
+ └── ≡📙:4:lane on fafd9d0 {0}
+ └── 📙:4:lane
");
let graph =
@@ -5865,23 +5899,23 @@ fn two_branches_one_advanced_two_parent_ws_commit_diverged_ttb() -> anyhow::Resu
├── 👉📕►►►:0[0]:gitbutler/workspace[🌳]
│ └── ·873d056 (⌂|🏘|1)
- │ ├── 📙►:4[1]:lane
- │ │ └── ►:2[2]:anon:
- │ │ └── 🏁·fafd9d0 (⌂|🏘|1) ►main
- │ └── 📙►:3[1]:advanced-lane
- │ └── ·cbc6713 (⌂|🏘|1)
- │ └── →:2:
+ │ ├── 📙►:3[1]:advanced-lane
+ │ │ └── ·cbc6713 (⌂|🏘|1)
+ │ │ └── ►:2[2]:anon:
+ │ │ └── 🏁·fafd9d0 (⌂|🏘|1) ►main
+ │ └── 📙►:4[1]:lane
+ │ └── →:2:
└── ►:1[0]:origin/main
└── 🏁🟣da83717 (✓)
");
insta::assert_snapshot!(graph_workspace(&graph.into_workspace()?), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main⇣1 on fafd9d0
- ├── ≡📙:4:lane on fafd9d0 {0}
- │ └── 📙:4:lane
- └── ≡📙:3:advanced-lane on fafd9d0 {1}
- └── 📙:3:advanced-lane
- └── ·cbc6713 (🏘️)
+ ├── ≡📙:3:advanced-lane on fafd9d0 {1}
+ │ └── 📙:3:advanced-lane
+ │ └── ·cbc6713 (🏘️)
+ └── ≡📙:4:lane on fafd9d0 {0}
+ └── 📙:4:lane
");
Ok(())
}
@@ -6146,14 +6180,13 @@ fn shallow_boundary_in_workspace_prevents_lower_bound() -> anyhow::Result<()> {
fn applied_stack_below_explicit_lower_bound() -> anyhow::Result<()> {
let (repo, mut meta) = read_only_in_memory_scenario("ws/two-branches-one-below-base")?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * e82dfab (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 74835cf (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 6fdab32 (A) A1
- * | 78b1b59 (B) B1
+ | * 78b1b59 (B) B1
+ * | 6fdab32 (A) A1
| | * 938e6f2 (origin/main, main) M4
| |/
- |/|
- * | f52fcec M3
+ | * f52fcec M3
|/
* bce0c5e M2
* 3183e43 M1
@@ -6166,28 +6199,28 @@ fn applied_stack_below_explicit_lower_bound() -> anyhow::Result<()> {
insta::assert_snapshot!(graph_tree(&graph), @"
└── 👉📕►►►:0[0]:gitbutler/workspace[🌳]
- └── ·e82dfab (⌂|🏘|1)
- ├── ►:1[1]:B
- │ ├── ·78b1b59 (⌂|🏘|1)
- │ └── ·f52fcec (⌂|🏘|1)
+ └── ·74835cf (⌂|🏘|1)
+ ├── ►:2[1]:A
+ │ └── ·6fdab32 (⌂|🏘|1)
│ └── ►:3[2]:anon:
│ ├── ·bce0c5e (⌂|🏘|1)
│ └── 🏁·3183e43 (⌂|🏘|1)
- └── ►:2[1]:A
- └── ·6fdab32 (⌂|🏘|1)
+ └── ►:1[1]:B
+ ├── ·78b1b59 (⌂|🏘|1)
+ └── ·f52fcec (⌂|🏘|1)
└── →:3:
");
// The base is automatically set to the lowest one that includes both branches, despite the target.
insta::assert_snapshot!(graph_workspace(&graph.into_workspace()?), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓! on bce0c5e
- ├── ≡:1:B on bce0c5e
- │ └── :1:B
- │ ├── ·78b1b59 (🏘️)
- │ └── ·f52fcec (🏘️)
- └── ≡:2:A on bce0c5e
- └── :2:A
- └── ·6fdab32 (🏘️)
+ ├── ≡:2:A on bce0c5e
+ │ └── :2:A
+ │ └── ·6fdab32 (🏘️)
+ └── ≡:1:B on bce0c5e
+ └── :1:B
+ ├── ·78b1b59 (🏘️)
+ └── ·f52fcec (🏘️)
");
add_stack_with_segments(&mut meta, 0, "A", StackState::InWorkspace, &[]);
@@ -6199,7 +6232,7 @@ fn applied_stack_below_explicit_lower_bound() -> anyhow::Result<()> {
insta::assert_snapshot!(graph_tree(&graph), @"
├── 👉📕►►►:0[0]:gitbutler/workspace[🌳]
- │ └── ·e82dfab (⌂|🏘|01)
+ │ └── ·74835cf (⌂|🏘|01)
│ ├── 📙►:3[1]:A
│ │ └── ·6fdab32 (⌂|🏘|01)
│ │ └── ►:6[3]:anon:
@@ -6220,7 +6253,7 @@ fn applied_stack_below_explicit_lower_bound() -> anyhow::Result<()> {
├── ≡📙:3:A on bce0c5e {0}
│ └── 📙:3:A
│ └── ·6fdab32 (🏘️)
- └── ≡📙:4:B on bce0c5e {1}
+ └── ≡📙:4:B on f52fcec {1}
└── 📙:4:B
└── ·78b1b59 (🏘️)
");
@@ -6236,7 +6269,7 @@ fn applied_stack_below_explicit_lower_bound() -> anyhow::Result<()> {
insta::assert_snapshot!(graph_tree(&graph), @"
├── 👉📕►►►:0[0]:gitbutler/workspace[🌳]
- │ └── ·e82dfab (⌂|🏘|01)
+ │ └── ·74835cf (⌂|🏘|01)
│ ├── 📙►:4[1]:A
│ │ └── ·6fdab32 (⌂|🏘|01)
│ │ └── ►:6[3]:anon:
@@ -6325,13 +6358,13 @@ fn applied_stack_above_explicit_lower_bound() -> anyhow::Result<()> {
fn dependent_branch_on_base() -> anyhow::Result<()> {
let (repo, mut meta) = read_only_in_memory_scenario("ws/dependent-branch-on-base")?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- *-. a0385a8 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ *-. e456a5f (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\ \
- | | * 49d4b34 (A) A1
+ | | * f9e2cb7 (C2-3, C2-2, C2-1, C) C2
+ | | * aaa195b (C1-3, C1-2, C1-1) C1
| |/
|/|
- | * f9e2cb7 (C2-3, C2-2, C2-1, C) C2
- | * aaa195b (C1-3, C1-2, C1-1) C1
+ | * 49d4b34 (A) A1
|/
* 3183e43 (origin/main, main, below-below-C, below-below-B, below-below-A, below-C, below-B, below-A, B) M1
");
@@ -6372,28 +6405,28 @@ fn dependent_branch_on_base() -> anyhow::Result<()> {
insta::assert_snapshot!(graph_tree(&graph), @"
├── 👉📕►►►:0[0]:gitbutler/workspace[🌳]
- │ └── ·a0385a8 (⌂|🏘|01)
- │ ├── 📙►:3[1]:A
- │ │ └── ·49d4b34 (⌂|🏘|01)
- │ │ └── 📙►:18[2]:below-A
- │ │ └── 📙►:19[3]:below-below-A
- │ │ └── ►:2[10]:main <> origin/main →:1:
- │ │ └── 🏁·3183e43 (⌂|🏘|✓|11)
+ │ └── ·e456a5f (⌂|🏘|01)
│ ├── 📙►:6[1]:B
│ │ └── 📙►:7[2]:below-B
│ │ └── 📙►:8[3]:below-below-B
- │ │ └── →:2: (main →:1:)
- │ └── 📙►:9[1]:C
- │ └── 📙►:10[2]:C2-1
- │ └── 📙►:11[3]:C2-2
- │ └── 📙►:12[4]:C2-3
+ │ │ └── ►:2[10]:main <> origin/main →:1:
+ │ │ └── 🏁·3183e43 (⌂|🏘|✓|11)
+ │ ├── 📙►:3[1]:A
+ │ │ └── ·49d4b34 (⌂|🏘|01)
+ │ │ └── 📙►:9[2]:below-A
+ │ │ └── 📙►:10[3]:below-below-A
+ │ │ └── →:2: (main →:1:)
+ │ └── 📙►:11[1]:C
+ │ └── 📙►:12[2]:C2-1
+ │ └── 📙►:13[3]:C2-2
+ │ └── 📙►:14[4]:C2-3
│ └── ·f9e2cb7 (⌂|🏘|01)
- │ └── 📙►:13[5]:C1-3
- │ └── 📙►:14[6]:C1-2
- │ └── 📙►:15[7]:C1-1
+ │ └── 📙►:15[5]:C1-3
+ │ └── 📙►:16[6]:C1-2
+ │ └── 📙►:17[7]:C1-1
│ └── ·aaa195b (⌂|🏘|01)
- │ └── 📙►:16[8]:below-C
- │ └── 📙►:17[9]:below-below-C
+ │ └── 📙►:18[8]:below-C
+ │ └── 📙►:19[9]:below-below-C
│ └── →:2: (main →:1:)
└── ►:1[0]:origin/main →:2:
└── →:2: (main →:1:)
@@ -6403,27 +6436,27 @@ fn dependent_branch_on_base() -> anyhow::Result<()> {
let ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 3183e43
- ├── ≡📙:3:A on 3183e43 {1}
- │ ├── 📙:3:A
- │ │ └── ·49d4b34 (🏘️)
- │ ├── 📙:18:below-A
- │ └── 📙:19:below-below-A
├── ≡📙:6:B on 3183e43 {2}
│ ├── 📙:6:B
│ ├── 📙:7:below-B
│ └── 📙:8:below-below-B
- └── ≡📙:9:C on 3183e43 {3}
- ├── 📙:9:C
- ├── 📙:10:C2-1
- ├── 📙:11:C2-2
- ├── 📙:12:C2-3
+ ├── ≡📙:3:A on 3183e43 {1}
+ │ ├── 📙:3:A
+ │ │ └── ·49d4b34 (🏘️)
+ │ ├── 📙:9:below-A
+ │ └── 📙:10:below-below-A
+ └── ≡📙:11:C on 3183e43 {3}
+ ├── 📙:11:C
+ ├── 📙:12:C2-1
+ ├── 📙:13:C2-2
+ ├── 📙:14:C2-3
│ └── ·f9e2cb7 (🏘️)
- ├── 📙:13:C1-3
- ├── 📙:14:C1-2
- ├── 📙:15:C1-1
+ ├── 📙:15:C1-3
+ ├── 📙:16:C1-2
+ ├── 📙:17:C1-1
│ └── ·aaa195b (🏘️)
- ├── 📙:16:below-C
- └── 📙:17:below-below-C
+ ├── 📙:18:below-C
+ └── 📙:19:below-below-C
");
let wrongly_inactive = StackState::Inactive;
@@ -6446,21 +6479,21 @@ fn dependent_branch_on_base() -> anyhow::Result<()> {
│ ├── 📙:6:B
│ ├── 📙:7:below-B
│ └── 📙:8:below-below-B
- ├── ≡📙:9:C on 3183e43 {3}
- │ ├── 📙:9:C
- │ ├── 📙:10:C2-1
- │ ├── 📙:11:C2-2
- │ ├── 📙:12:C2-3
- │ │ └── ·f9e2cb7 (🏘️)
- │ ├── 📙:13:C1-3
- │ ├── 📙:14:C1-2
- │ ├── 📙:15:C1-1
- │ │ └── ·aaa195b (🏘️)
- │ ├── 📙:16:below-C
- │ └── 📙:17:below-below-C
- └── ≡📙:5:A on 3183e43 {1}
- └── 📙:5:A
- └── ·49d4b34 (🏘️)
+ ├── ≡📙:5:A on 3183e43 {1}
+ │ └── 📙:5:A
+ │ └── ·49d4b34 (🏘️)
+ └── ≡📙:9:C on 3183e43 {3}
+ ├── 📙:9:C
+ ├── 📙:10:C2-1
+ ├── 📙:11:C2-2
+ ├── 📙:12:C2-3
+ │ └── ·f9e2cb7 (🏘️)
+ ├── 📙:13:C1-3
+ ├── 📙:14:C1-2
+ ├── 📙:15:C1-1
+ │ └── ·aaa195b (🏘️)
+ ├── 📙:16:below-C
+ └── 📙:17:below-below-C
");
Ok(())
}
@@ -7304,7 +7337,7 @@ fn integrated_merge_at_bottom_is_kept() -> anyhow::Result<()> {
insta::assert_snapshot!(graph_workspace(&graph.into_workspace()?), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main⇣1 on f5f42e0
- └── ≡📙:3:local-stack {0}
+ └── ≡📙:3:local-stack on fafd9d0 {0}
└── 📙:3:local-stack
├── ·66ea651 (🏘️)
├── ·e5a88a7 (🏘️)
@@ -7359,7 +7392,7 @@ fn merge_from_main_keeps_all_branch_commits() -> anyhow::Result<()> {
let ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on ef56fab
- └── ≡📙:3:my-branch {0}
+ └── ≡📙:3:my-branch on fafd9d0 {0}
└── 📙:3:my-branch
├── ·cd76046 (🏘️)
├── ·f8ff9a3 (🏘️)
diff --git a/crates/but-graph/tests/graph/vis.rs b/crates/but-graph/tests/graph/vis.rs
index 4ac32ea19ab..c354da4333a 100644
--- a/crates/but-graph/tests/graph/vis.rs
+++ b/crates/but-graph/tests/graph/vis.rs
@@ -48,7 +48,6 @@ fn post_graph_traversal() -> anyhow::Result<()> {
},
0,
None,
- 0,
);
let remote_to_local_target = Segment {
@@ -61,7 +60,7 @@ fn post_graph_traversal() -> anyhow::Result<()> {
commits: vec![commit(id("c"), Some(init_commit_id), CommitFlags::empty())],
..Default::default()
};
- graph.connect_new_segment(local_target, None, remote_to_local_target, 0, None, 0);
+ graph.connect_new_segment(local_target, None, remote_to_local_target, 0, None);
let branch = Segment {
id: 3.into(),
@@ -80,7 +79,7 @@ fn post_graph_traversal() -> anyhow::Result<()> {
],
metadata: None,
};
- let branch = graph.connect_new_segment(local_target, None, branch, 0, None, 0);
+ let branch = graph.connect_new_segment(local_target, None, branch, 0, None);
let remote_to_root_branch = Segment {
id: 4.into(),
@@ -98,7 +97,7 @@ fn post_graph_traversal() -> anyhow::Result<()> {
],
..Default::default()
};
- graph.connect_new_segment(branch, 1, remote_to_root_branch, 0, None, 0);
+ graph.connect_new_segment(branch, 1, remote_to_root_branch, 0, None);
insta::assert_snapshot!(graph_tree(&graph), @"
diff --git a/crates/but-graph/tests/graph/workspace/merge_base_with_target_branch.rs b/crates/but-graph/tests/graph/workspace/merge_base_with_target_branch.rs
index bdb873c5ce0..3f32bfef16b 100644
--- a/crates/but-graph/tests/graph/workspace/merge_base_with_target_branch.rs
+++ b/crates/but-graph/tests/graph/workspace/merge_base_with_target_branch.rs
@@ -53,14 +53,13 @@ fn with_target_ref() -> anyhow::Result<()> {
fn with_extra_target_when_no_target_ref() -> anyhow::Result<()> {
let (repo, mut meta) = read_only_in_memory_scenario("ws/two-branches-one-below-base")?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * e82dfab (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 74835cf (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 6fdab32 (A) A1
- * | 78b1b59 (B) B1
+ | * 78b1b59 (B) B1
+ * | 6fdab32 (A) A1
| | * 938e6f2 (origin/main, main) M4
| |/
- |/|
- * | f52fcec M3
+ | * f52fcec M3
|/
* bce0c5e M2
* 3183e43 M1
diff --git a/crates/but-graph/tests/graph/workspace/resolved_target_commit_id.rs b/crates/but-graph/tests/graph/workspace/resolved_target_commit_id.rs
index 7c59e9d7068..67833136250 100644
--- a/crates/but-graph/tests/graph/workspace/resolved_target_commit_id.rs
+++ b/crates/but-graph/tests/graph/workspace/resolved_target_commit_id.rs
@@ -11,14 +11,13 @@ use crate::init::utils::{
fn returns_target_tip_when_stacks_have_different_bases() -> anyhow::Result<()> {
let (repo, mut meta) = read_only_in_memory_scenario("ws/two-branches-one-below-base")?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * e82dfab (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 74835cf (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 6fdab32 (A) A1
- * | 78b1b59 (B) B1
+ | * 78b1b59 (B) B1
+ * | 6fdab32 (A) A1
| | * 938e6f2 (origin/main, main) M4
| |/
- |/|
- * | f52fcec M3
+ | * f52fcec M3
|/
* bce0c5e M2
* 3183e43 M1
diff --git a/crates/but-rebase/src/graph_rebase/commit.rs b/crates/but-rebase/src/graph_rebase/commit.rs
index 1b01873dab5..b5de529cebf 100644
--- a/crates/but-rebase/src/graph_rebase/commit.rs
+++ b/crates/but-rebase/src/graph_rebase/commit.rs
@@ -9,7 +9,7 @@ use crate::{
commit::{DateMode, create},
graph_rebase::{
Editor, Pick, Selector, Step, ToCommitSelector, ToReferenceSelector,
- util::collect_ordered_parents,
+ util::first_ordered_parent,
},
};
@@ -65,16 +65,14 @@ impl Editor<'_, '_, M> {
.history
.normalize_selector(selector.to_reference_selector(self)?)?;
- let parents = collect_ordered_parents(&self.graph, selector.id);
- let first_parent = parents
- .first()
+ let first_parent = first_ordered_parent(&self.graph, selector.id)
.context("Failed to find a parent for selected reference in the step graph.")?;
- let Step::Pick(pick) = &self.graph[*first_parent] else {
- bail!("BUG: collect_ordered_parents provided a non-pick return value");
+ let Step::Pick(pick) = &self.graph[first_parent] else {
+ bail!("BUG: first_ordered_parent provided a non-pick return value");
};
- Ok((self.new_selector(*first_parent), self.find_commit(pick.id)?))
+ Ok((self.new_selector(first_parent), self.find_commit(pick.id)?))
}
/// Writes a commit with correct signing to the in memory repository,
diff --git a/crates/but-rebase/src/graph_rebase/creation.rs b/crates/but-rebase/src/graph_rebase/creation.rs
index 0cddbcce6e1..22b0204f9a9 100644
--- a/crates/but-rebase/src/graph_rebase/creation.rs
+++ b/crates/but-rebase/src/graph_rebase/creation.rs
@@ -203,25 +203,22 @@ impl<'ws, 'meta, M: RefMetadata> Editor<'ws, 'meta, M> {
};
}
+ // Rebase edge order = the destination's position in the source commit's parent list.
+ let parents_by_commit: HashMap = commits
+ .iter()
+ .map(|c| (c.id, c.parent_ids.as_slice()))
+ .collect();
+
for sidx in segments.keys() {
let Some(source) = segments.get(sidx).and_then(|n| n.nodes.last()) else {
continue;
};
- let edges = {
- let mut v = workspace
- .graph
- .edges_directed(*sidx, Direction::Outgoing)
- .collect::>();
- // TODO: the code below relies on edges being in reversed order,
- // but that changed now and they are in commit-graph order.
- // This is the minimal change to make this work,
- // even though a second step should be the cleanup of the
- // whole ordering business which also compensated for out-of-order
- // edges (which is also fixed).
- v.reverse();
- v
- };
+ // but-graph yields outgoing edges in parent order, so iterate as-is. The counter below
+ // gives commit-less empty branches distinct, increasing orders — so the StepGraph never
+ // has tied parent orders and needs no insertion-order tie-break.
+ let edges = workspace.graph.edges_directed(*sidx, Direction::Outgoing);
+ let mut empty_branch_count = 0usize;
'inner: for edge in edges {
let Some(target) = segments.get(&edge.target()).and_then(|n| n.nodes.first())
else {
@@ -232,8 +229,24 @@ impl<'ws, 'meta, M: RefMetadata> Editor<'ws, 'meta, M> {
continue 'inner;
};
- // TODO: does it have relevance when `parent_order()` is `None` for edges to virtual segments?
- let order = edge.weight().parent_order().unwrap_or(0) as usize;
+ // A real parent gets its index in the source commit's parent array. A dst with no
+ // commit id (a commit-less empty branch) can't be indexed, so it's placed after the
+ // real parents — and each one bumps the counter so siblings get distinct orders.
+ let parents = edge
+ .weight()
+ .src_id()
+ .and_then(|src| parents_by_commit.get(&src).copied());
+ let real_parent_index = parents
+ .zip(edge.weight().dst_id())
+ .and_then(|(parents, dst)| parents.iter().position(|p| *p == dst));
+ let order = match real_parent_index {
+ Some(idx) => idx,
+ None => {
+ let o = parents.map_or(0, |p| p.len()) + empty_branch_count;
+ empty_branch_count += 1;
+ o
+ }
+ };
graph.add_edge(*source, *target, Edge { order });
}
}
diff --git a/crates/but-rebase/src/graph_rebase/materialize.rs b/crates/but-rebase/src/graph_rebase/materialize.rs
index 1df1e11db0b..a0921951955 100644
--- a/crates/but-rebase/src/graph_rebase/materialize.rs
+++ b/crates/but-rebase/src/graph_rebase/materialize.rs
@@ -10,7 +10,7 @@ use gix::refs::{
};
use crate::graph_rebase::{
- Checkout, MaterializeOutcome, Pick, Step, SuccessfulRebase, util::collect_ordered_parents,
+ Checkout, MaterializeOutcome, Pick, Step, SuccessfulRebase, util::first_ordered_parent,
};
impl<'ws, 'graph, M: RefMetadata> SuccessfulRebase<'ws, 'graph, M> {
@@ -35,11 +35,10 @@ impl<'ws, 'graph, M: RefMetadata> SuccessfulRebase<'ws, 'graph, M> {
Step::None => bail!("Checkout selector is pointing to none"),
Step::Pick(Pick { id, .. }) => (id, None),
Step::Reference { refname, .. } => {
- let parents = collect_ordered_parents(&self.graph, selector.id);
- let parent_step_id =
- parents.first().context("No first parent to reference")?;
- let Step::Pick(Pick { id, .. }) = self.graph[*parent_step_id] else {
- bail!("collect_ordered_parents should always return a commit pick");
+ let parent_step_id = first_ordered_parent(&self.graph, selector.id)
+ .context("No first parent to reference")?;
+ let Step::Pick(Pick { id, .. }) = self.graph[parent_step_id] else {
+ bail!("first_ordered_parent should always return a commit pick");
};
(id, Some(refname))
}
diff --git a/crates/but-rebase/src/graph_rebase/mod.rs b/crates/but-rebase/src/graph_rebase/mod.rs
index 208f718f35d..c0efec508a9 100644
--- a/crates/but-rebase/src/graph_rebase/mod.rs
+++ b/crates/but-rebase/src/graph_rebase/mod.rs
@@ -15,7 +15,7 @@ use but_graph::init::Overlay;
pub use creation::GraphEditorOptions;
use gix::refs::transaction::RefEdit;
-use crate::graph_rebase::util::collect_ordered_parents;
+use crate::graph_rebase::util::first_ordered_parent;
use crate::graph_rebase::cherry_pick::{PickMode, TreeMergeMode};
pub mod cherry_pick;
@@ -346,10 +346,9 @@ impl<'ws, 'meta, M: RefMetadata> SuccessfulRebase<'ws, 'meta, M> {
Step::None => None,
Step::Pick(Pick { id, .. }) => Some((*id, None)),
Step::Reference { refname, .. } => {
- let parents = collect_ordered_parents(&self.graph, selector.id);
-
- if let Some(to_reference) = parents.first()
- && let Step::Pick(Pick { id, .. }) = self.graph[*to_reference]
+ if let Some(to_reference) =
+ first_ordered_parent(&self.graph, selector.id)
+ && let Step::Pick(Pick { id, .. }) = self.graph[to_reference]
{
Some((id, Some(refname.clone())))
} else {
diff --git a/crates/but-rebase/src/graph_rebase/mutate.rs b/crates/but-rebase/src/graph_rebase/mutate.rs
index 327c2e271d6..68c13c8011b 100644
--- a/crates/but-rebase/src/graph_rebase/mutate.rs
+++ b/crates/but-rebase/src/graph_rebase/mutate.rs
@@ -495,16 +495,35 @@ impl Editor<'_, '_, M> {
Ok(())
}
+ /// The order to give a new outgoing edge from `node` so it sorts after all existing ones.
+ fn next_outgoing_order(&self, node: petgraph::prelude::NodeIndex) -> usize {
+ self.graph
+ .edges_directed(node, Direction::Outgoing)
+ .map(|e| e.weight().order)
+ .max()
+ .map_or(0, |max| max + 1)
+ }
+
/// Remove the child edge, and reconnect to the right parents.
fn reconnect_edges_to_parents(
&mut self,
disconnected_parent_edges: &[(Edge, petgraph::prelude::NodeIndex)],
child_node: petgraph::prelude::NodeIndex,
) {
- // Reconnect the child node to all the disconnected parents.
- for (parent_edge_weight, edge_target) in disconnected_parent_edges {
- self.graph
- .add_edge(child_node, *edge_target, parent_edge_weight.clone());
+ // Reconnect the child node to all the disconnected parents. Their orders came from a
+ // different parent context and can collide with `child_node`'s existing parents, so
+ // renumber them after the highest existing order, preserving their relative order.
+ let base_order = self.next_outgoing_order(child_node);
+ let mut disconnected_parent_edges = disconnected_parent_edges.iter().collect::>();
+ disconnected_parent_edges.sort_by_key(|(weight, _)| weight.order);
+ for (offset, (_, edge_target)) in disconnected_parent_edges.into_iter().enumerate() {
+ self.graph.add_edge(
+ child_node,
+ *edge_target,
+ Edge {
+ order: base_order + offset,
+ },
+ );
}
}
@@ -620,19 +639,13 @@ impl Editor<'_, '_, M> {
if let Some(nodes_to_connect) = nodes_to_connect {
// If there were nodes to connect defined, create edges from them into the child node of the segment
// being inserted.
- for (index, any_selector) in nodes_to_connect.as_slice().iter().enumerate() {
+ for any_selector in nodes_to_connect.as_slice() {
let selector = any_selector.to_selector(self)?;
let node = self.history.normalize_selector(selector)?;
- // Avoid weight collision by adding the order value of the highest order child plus one,
- // accommodating for order 0.
- let new_weight = if let Some((_, grand_child_weight, _)) =
- chubbiest_grand_child.as_ref()
- {
- Edge {
- order: index + grand_child_weight.order + 1,
- }
- } else {
- Edge { order: index }
+ // This `node -> child` edge is read as `node`'s parent order, so order it
+ // after node's existing parents rather than the inserted child's children.
+ let new_weight = Edge {
+ order: self.next_outgoing_order(node.id),
};
self.graph.add_edge(node.id, child.id, new_weight);
}
diff --git a/crates/but-rebase/src/graph_rebase/rebase.rs b/crates/but-rebase/src/graph_rebase/rebase.rs
index 1a88897630e..09069b2f3ee 100644
--- a/crates/but-rebase/src/graph_rebase/rebase.rs
+++ b/crates/but-rebase/src/graph_rebase/rebase.rs
@@ -16,7 +16,7 @@ use petgraph::{Direction, visit::EdgeRef};
use crate::graph_rebase::{
Editor, Pick, Step, StepGraph, StepGraphIndex, SuccessfulRebase,
cherry_pick::{CherryPickOutcome, cherry_pick},
- util::collect_ordered_parents,
+ util::{collect_ordered_parents, first_ordered_parent},
};
impl<'ws, 'graph, M: RefMetadata> Editor<'ws, 'graph, M> {
@@ -57,10 +57,11 @@ impl<'ws, 'graph, M: RefMetadata> Editor<'ws, 'graph, M> {
output_graph.add_node(Step::Pick(pick))
}
Step::Pick(pick) => {
- let graph_parents = collect_ordered_parents(&self.graph, step_idx);
+ // Only resolve the graph parents when we actually need them — a pick with
+ // `preserved_parents` already carries its onto-commits.
let ontos = match pick.preserved_parents.clone() {
Some(ontos) => ontos,
- None => graph_parents
+ None => collect_ordered_parents(&self.graph, step_idx)
.iter()
.map(|idx| {
let Some(new_idx) = graph_mapping.get(idx) else {
@@ -128,11 +129,9 @@ impl<'ws, 'graph, M: RefMetadata> Editor<'ws, 'graph, M> {
// Immutable references are kept in the graph for traversal
// but never moved, created, or deleted.
if mutable {
- let graph_parents = collect_ordered_parents(&self.graph, step_idx);
- let first_parent_idx = graph_parents
- .first()
+ let first_parent_idx = first_ordered_parent(&self.graph, step_idx)
.context("References should have at least one parent")?;
- let Some(new_idx) = graph_mapping.get(first_parent_idx) else {
+ let Some(new_idx) = graph_mapping.get(&first_parent_idx) else {
bail!("A matching parent can't be found in the output graph");
};
diff --git a/crates/but-rebase/src/graph_rebase/util.rs b/crates/but-rebase/src/graph_rebase/util.rs
index 0f5be140163..a774605ac4d 100644
--- a/crates/but-rebase/src/graph_rebase/util.rs
+++ b/crates/but-rebase/src/graph_rebase/util.rs
@@ -13,6 +13,29 @@ use crate::graph_rebase::{Pick, Step, StepGraph, StepGraphIndex};
pub(crate) fn collect_ordered_parents(
graph: &StepGraph,
target: StepGraphIndex,
+) -> Vec {
+ ordered_commit_parents(graph, target, false)
+}
+
+/// The first commit parent of `target` in parent order, or `None` if it has none.
+///
+/// Equivalent to `collect_ordered_parents(graph, target).into_iter().next()`, but stops the
+/// search at the first commit instead of walking out every parent.
+pub(crate) fn first_ordered_parent(
+ graph: &StepGraph,
+ target: StepGraphIndex,
+) -> Option {
+ ordered_commit_parents(graph, target, true)
+ .into_iter()
+ .next()
+}
+
+/// Pruned depth-first search for `target`'s commit parents in parent order, descending through
+/// non-commit steps. Stops after the first commit when `first_only` is set.
+fn ordered_commit_parents(
+ graph: &StepGraph,
+ target: StepGraphIndex,
+ first_only: bool,
) -> Vec {
let mut potential_parent_edges = graph
.edges_directed(target, petgraph::Direction::Outgoing)
@@ -30,6 +53,9 @@ pub(crate) fn collect_ordered_parents(
while let Some(candidate) = potential_parent_edges.pop() {
if let Step::Pick(Pick { .. }) = graph[candidate.target()] {
parents.push(candidate.target());
+ if first_only {
+ break;
+ }
// Don't pursue the children
continue;
};
@@ -129,4 +155,38 @@ mod test {
Ok(())
}
}
+
+ mod first_ordered_parent {
+ use std::str::FromStr as _;
+
+ use anyhow::Result;
+
+ use crate::graph_rebase::{Edge, Step, StepGraph, util::first_ordered_parent};
+
+ #[test]
+ fn descends_through_references_and_stops_at_first_commit() -> Result<()> {
+ let mut graph = StepGraph::new();
+ let a_id = gix::ObjectId::from_str("1000000000000000000000000000000000000000")?;
+ let a = graph.add_node(Step::new_pick(a_id));
+ // First parent is a reference that must be descended through to reach its commit.
+ let r = graph.add_node(Step::Reference {
+ refname: "refs/heads/foobar".try_into()?,
+ });
+ let first_id = gix::ObjectId::from_str("2000000000000000000000000000000000000000")?;
+ let first = graph.add_node(Step::new_pick(first_id));
+ // A real second parent that must not be returned.
+ let second_id = gix::ObjectId::from_str("3000000000000000000000000000000000000000")?;
+ let second = graph.add_node(Step::new_pick(second_id));
+
+ graph.add_edge(a, r, Edge { order: 0 });
+ graph.add_edge(a, second, Edge { order: 1 });
+ graph.add_edge(r, first, Edge { order: 0 });
+
+ assert_eq!(first_ordered_parent(&graph, a), Some(first));
+ // A node with no parents resolves to nothing.
+ assert_eq!(first_ordered_parent(&graph, first), None);
+
+ Ok(())
+ }
+ }
}
diff --git a/crates/but-workspace/src/legacy/head.rs b/crates/but-workspace/src/legacy/head.rs
index 705a5c8069c..9e8e9eda9e3 100644
--- a/crates/but-workspace/src/legacy/head.rs
+++ b/crates/but-workspace/src/legacy/head.rs
@@ -114,7 +114,9 @@ pub fn remerged_workspace_tree_v2(
#[instrument(level = "debug", skip(ctx))]
pub fn remerged_workspace_commit_v2(ctx: &Context) -> Result {
let repo = ctx.clone_repo_for_merging()?;
- let (workspace_tree_id, stacks, target_commit) = remerged_workspace_tree_v2(ctx, &repo)?;
+ let (workspace_tree_id, mut stacks, target_commit) = remerged_workspace_tree_v2(ctx, &repo)?;
+ // Workspace-commit parents follow the metadata stack order, not HashMap iteration order.
+ stacks.sort_by_key(|stack| stack.order);
let committer = signature_gix(SignaturePurpose::Committer);
let author = signature_gix(SignaturePurpose::Author);
diff --git a/crates/but-workspace/tests/fixtures/scenario/shared.sh b/crates/but-workspace/tests/fixtures/scenario/shared.sh
index 674f3529153..51d1bcc4d8a 100644
--- a/crates/but-workspace/tests/fixtures/scenario/shared.sh
+++ b/crates/but-workspace/tests/fixtures/scenario/shared.sh
@@ -67,11 +67,14 @@ function create_workspace_commit_once() {
fi
fi
- git checkout -b gitbutler/workspace
- if [ $# == 1 ] || [ $# == 0 ]; then
- commit "$workspace_commit_subject"
+ if [ $# -gt 1 ]; then
+ # First arg becomes the workspace commit's first parent; the rest merge in order.
+ git checkout "$1"
+ git checkout -b gitbutler/workspace
+ git merge --no-ff -m "$workspace_commit_subject" "${@:2}"
else
- git merge --no-ff -m "$workspace_commit_subject" "${@}"
+ git checkout -b gitbutler/workspace
+ commit "$workspace_commit_subject"
fi
}
diff --git a/crates/but-workspace/tests/fixtures/scenario/ws-ref-ws-commit-single-stack-double-stack.sh b/crates/but-workspace/tests/fixtures/scenario/ws-ref-ws-commit-single-stack-double-stack.sh
index fa1b4a1a196..4f514899b84 100644
--- a/crates/but-workspace/tests/fixtures/scenario/ws-ref-ws-commit-single-stack-double-stack.sh
+++ b/crates/but-workspace/tests/fixtures/scenario/ws-ref-ws-commit-single-stack-double-stack.sh
@@ -20,4 +20,6 @@ git checkout B
commit B
git checkout B -b C
commit C
-create_workspace_commit_once A C
+# Reverse arg order on purpose: this fixture must keep parent[0] diverging from the
+# metadata stack order, to exercise the *_display_order_follows_workspace_parents tests.
+create_workspace_commit_once C A
diff --git a/crates/but-workspace/tests/fixtures/scenario/ws-with-empty-stack-target-advanced.sh b/crates/but-workspace/tests/fixtures/scenario/ws-with-empty-stack-target-advanced.sh
index d84d98a006b..907ae3b9e02 100755
--- a/crates/but-workspace/tests/fixtures/scenario/ws-with-empty-stack-target-advanced.sh
+++ b/crates/but-workspace/tests/fixtures/scenario/ws-with-empty-stack-target-advanced.sh
@@ -20,7 +20,7 @@ git branch B
git checkout -b A
commit A
git checkout B
-create_workspace_commit_once A B
+create_workspace_commit_once B A
# Advance the target past the workspace base while leaving `gitbutler/target` at the base.
git checkout main
diff --git a/crates/but-workspace/tests/fixtures/scenario/ws-with-empty-stack.sh b/crates/but-workspace/tests/fixtures/scenario/ws-with-empty-stack.sh
index 25479b00248..be7dd13bd4c 100644
--- a/crates/but-workspace/tests/fixtures/scenario/ws-with-empty-stack.sh
+++ b/crates/but-workspace/tests/fixtures/scenario/ws-with-empty-stack.sh
@@ -15,4 +15,6 @@ git branch B
git checkout -b A
commit A
git checkout B
-create_workspace_commit_once A B
+# Reverse arg order on purpose: keep the empty branch B as parent[0] (diverging from
+# metadata) for the *_display_order_follows_workspace_parents / empty-move tests.
+create_workspace_commit_once B A
diff --git a/crates/but-workspace/tests/workspace/branch/apply_unapply.rs b/crates/but-workspace/tests/workspace/branch/apply_unapply.rs
index 2f0810f84ed..c8488a03b08 100644
--- a/crates/but-workspace/tests/workspace/branch/apply_unapply.rs
+++ b/crates/but-workspace/tests/workspace/branch/apply_unapply.rs
@@ -732,23 +732,23 @@ mod workspace_disposition {
let repo_log = visualize_commit_graph_all(&repo)?;
insta::allow_duplicates! {
insta::assert_snapshot!(repo_log, @r"
- * c49e4d8 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 3147679 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 09d8e52 (A) A
- * | c813d8d (B) B
+ | * c813d8d (B) B
+ * | 09d8e52 (A) A
|/
* 85efbe4 (origin/main, virtual-base, main) M
");
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:5:virtual-base on 85efbe4 {1}
- │ └── 📙:5:virtual-base
├── ≡📙:3:A on 85efbe4 {2}
│ └── 📙:3:A
│ └── ·09d8e52 (🏘️)
- └── ≡📙:4:B on 85efbe4 {3}
- └── 📙:4:B
- └── ·c813d8d (🏘️)
+ ├── ≡📙:4:B on 85efbe4 {3}
+ │ └── 📙:4:B
+ │ └── ·c813d8d (🏘️)
+ └── ≡📙:5:virtual-base on 85efbe4 {1}
+ └── 📙:5:virtual-base
");
}
Ok((tmp, repo, meta, ws))
@@ -932,10 +932,11 @@ fn workspace_with_out_of_ws_ref_and_anon_stack() -> anyhow::Result<()> {
)?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
* d03b217 (feature) F1
- | * dd3b979 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ | * db5f9ad (HEAD -> gitbutler/workspace) GitButler Workspace Commit
| |\
- | * | d6bdeab missing-name
- |/ /
+ | | * d6bdeab missing-name
+ | |/
+ |/|
| | * 5121eb9 (outside) advanced-outside
| |/
| * 67c6397 advanced-inside
@@ -946,13 +947,13 @@ fn workspace_with_out_of_ws_ref_and_anon_stack() -> anyhow::Result<()> {
let ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 3183e43
- ├── ≡:4:anon: on 3183e43
- │ └── :4:anon:
- │ └── ·d6bdeab (🏘️)
- └── ≡📙:5:outside →:3: on 3183e43 {1}
- └── 📙:5:outside →:3:
- ├── ·5121eb9*
- └── ·67c6397 (🏘️)
+ ├── ≡📙:4:outside →:3: on 3183e43 {1}
+ │ └── 📙:4:outside →:3:
+ │ ├── ·5121eb9*
+ │ └── ·67c6397 (🏘️)
+ └── ≡:5:anon: on 3183e43
+ └── :5:anon:
+ └── ·d6bdeab (🏘️)
");
let out = but_workspace::branch::apply(
@@ -972,13 +973,13 @@ fn workspace_with_out_of_ws_ref_and_anon_stack() -> anyhow::Result<()> {
insta::assert_snapshot!(graph_workspace(&out.workspace), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 3183e43
- ├── ≡:5:anon: on 3183e43
- │ └── :5:anon:
- │ └── ·d6bdeab (🏘️)
├── ≡📙:3:outside on 3183e43 {1}
│ └── 📙:3:outside
│ ├── ·5121eb9 (🏘️)
│ └── ·67c6397 (🏘️)
+ ├── ≡:5:anon: on 3183e43
+ │ └── :5:anon:
+ │ └── ·d6bdeab (🏘️)
└── ≡📙:4:feature on 3183e43 {2ec}
└── 📙:4:feature
└── ·d03b217 (🏘️)
@@ -1073,12 +1074,12 @@ fn unapply_natural_stack_with_partial_workspace_metadata() -> anyhow::Result<()>
let ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), "A is naturally visible, and B has metadata that matches it with an extra segment in metadata that's not there", @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:B on 85efbe4 {1}
- │ └── 📙:3:B
- │ └── ·c813d8d (🏘️)
- └── ≡:4:A on 85efbe4
- └── :4:A
- └── ·09d8e52 (🏘️)
+ ├── ≡:4:A on 85efbe4
+ │ └── :4:A
+ │ └── ·09d8e52 (🏘️)
+ └── ≡📙:3:B on 85efbe4 {1}
+ └── 📙:3:B
+ └── ·c813d8d (🏘️)
");
let out = but_workspace::branch::unapply(
@@ -1134,14 +1135,14 @@ fn unapply_natural_stack_branch_without_workspace_metadata() -> anyhow::Result<(
let ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), "a single and multi-branch stack are visible without any workspace metadata", @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 893d602
- ├── ≡:3:C on 893d602
- │ ├── :3:C
- │ │ └── ·356de85 (🏘️)
- │ └── :5:B
- │ └── ·f25f65c (🏘️)
- └── ≡:4:A on 893d602
- └── :4:A
- └── ·26e45af (🏘️)
+ ├── ≡:3:A on 893d602
+ │ └── :3:A
+ │ └── ·26e45af (🏘️)
+ └── ≡:4:C on 893d602
+ ├── :4:C
+ │ └── ·356de85 (🏘️)
+ └── :5:B
+ └── ·f25f65c (🏘️)
");
let out = but_workspace::branch::unapply(
@@ -1175,25 +1176,25 @@ fn unapply_natural_stack_branch_without_workspace_metadata() -> anyhow::Result<(
id: 1,
branches: [
WorkspaceStackBranch {
- ref_name: "refs/heads/C",
- archived: false,
- },
- WorkspaceStackBranch {
- ref_name: "refs/heads/B",
+ ref_name: "refs/heads/A",
archived: false,
},
],
- workspacecommit_relation: Outside,
+ workspacecommit_relation: Merged,
},
WorkspaceStack {
id: 2,
branches: [
WorkspaceStackBranch {
- ref_name: "refs/heads/A",
+ ref_name: "refs/heads/C",
+ archived: false,
+ },
+ WorkspaceStackBranch {
+ ref_name: "refs/heads/B",
archived: false,
},
],
- workspacecommit_relation: Merged,
+ workspacecommit_relation: Outside,
},
],
target_ref: "refs/remotes/origin/main",
@@ -4453,10 +4454,10 @@ fn auto_checkout_of_enclosing_workspace_with_commits() -> anyhow::Result<()> {
)?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * c49e4d8 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 3147679 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 09d8e52 (A) A
- * | c813d8d (B) B
+ | * c813d8d (B) B
+ * | 09d8e52 (A) A
|/
* 85efbe4 (origin/main, main) M
");
diff --git a/crates/but-workspace/tests/workspace/branch/create_reference.rs b/crates/but-workspace/tests/workspace/branch/create_reference.rs
index 39a51bb135e..1f8453496f1 100644
--- a/crates/but-workspace/tests/workspace/branch/create_reference.rs
+++ b/crates/but-workspace/tests/workspace/branch/create_reference.rs
@@ -996,12 +996,12 @@ mod with_workspace {
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 3183e43
- ├── ≡📙:3:A on 3183e43 {0}
- │ └── 📙:3:A
- │ └── ·49d4b34 (🏘️)
- └── ≡📙:4:B on 3183e43 {1}
- └── 📙:4:B
- └── ·f57c528 (🏘️)
+ ├── ≡📙:4:B on 3183e43 {1}
+ │ └── 📙:4:B
+ │ └── ·f57c528 (🏘️)
+ └── ≡📙:3:A on 3183e43 {0}
+ └── 📙:3:A
+ └── ·49d4b34 (🏘️)
");
let bottom_ref_a = rc("refs/heads/a-bottom");
@@ -1020,13 +1020,13 @@ mod with_workspace {
)?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 3183e43
- ├── ≡📙:3:A on 3183e43 {0}
- │ ├── 📙:3:A
- │ │ └── ·49d4b34 (🏘️)
- │ └── 📙:5:a-bottom
- └── ≡📙:4:B on 3183e43 {1}
- └── 📙:4:B
- └── ·f57c528 (🏘️)
+ ├── ≡📙:4:B on 3183e43 {1}
+ │ └── 📙:4:B
+ │ └── ·f57c528 (🏘️)
+ └── ≡📙:3:A on 3183e43 {0}
+ ├── 📙:3:A
+ │ └── ·49d4b34 (🏘️)
+ └── 📙:5:a-bottom
");
let bottom_ref_b = rc("refs/heads/b-bottom");
@@ -1046,14 +1046,14 @@ mod with_workspace {
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 3183e43
- ├── ≡📙:3:A on 3183e43 {0}
- │ ├── 📙:3:A
- │ │ └── ·49d4b34 (🏘️)
- │ └── 📙:6:a-bottom
- └── ≡📙:4:B on 3183e43 {1}
- ├── 📙:4:B
- │ └── ·f57c528 (🏘️)
- └── 📙:5:b-bottom
+ ├── ≡📙:4:B on 3183e43 {1}
+ │ ├── 📙:4:B
+ │ │ └── ·f57c528 (🏘️)
+ │ └── 📙:5:b-bottom
+ └── ≡📙:3:A on 3183e43 {0}
+ ├── 📙:3:A
+ │ └── ·49d4b34 (🏘️)
+ └── 📙:6:a-bottom
");
Ok(())
}
@@ -1382,12 +1382,12 @@ mod with_workspace {
let ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 3183e43
- ├── ≡📙:3:A on 3183e43 {0}
- │ └── 📙:3:A
- │ └── ·49d4b34 (🏘️)
- └── ≡📙:4:B on 3183e43 {1}
- └── 📙:4:B
- └── ·f57c528 (🏘️)
+ ├── ≡📙:4:B on 3183e43 {1}
+ │ └── 📙:4:B
+ │ └── ·f57c528 (🏘️)
+ └── ≡📙:3:A on 3183e43 {0}
+ └── 📙:3:A
+ └── ·49d4b34 (🏘️)
");
// The new reference lands in the stack of its anchor.
@@ -1407,13 +1407,13 @@ mod with_workspace {
)?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 3183e43
- ├── ≡📙:3:A on 3183e43 {0}
- │ └── 📙:3:A
- │ └── ·49d4b34 (🏘️)
- └── ≡📙:5:B on 3183e43 {1}
- ├── 📙:5:B
- └── 📙:6:new
- └── ·f57c528 (🏘️)
+ ├── ≡📙:5:B on 3183e43 {1}
+ │ ├── 📙:5:B
+ │ └── 📙:6:new
+ │ └── ·f57c528 (🏘️)
+ └── ≡📙:3:A on 3183e43 {0}
+ └── 📙:3:A
+ └── ·49d4b34 (🏘️)
");
Ok(())
}
diff --git a/crates/but-workspace/tests/workspace/branch/integrate_branch_upstream.rs b/crates/but-workspace/tests/workspace/branch/integrate_branch_upstream.rs
index fa6c6974111..b987c60065d 100644
--- a/crates/but-workspace/tests/workspace/branch/integrate_branch_upstream.rs
+++ b/crates/but-workspace/tests/workspace/branch/integrate_branch_upstream.rs
@@ -1809,11 +1809,11 @@ fn integrate_remote_advanced_branch_with_parallel_empty_branch() -> Result<()> {
insta::assert_snapshot!(
labeled_graph_snapshot(&repo, &labels)?,
- @"
- * abcd9a1 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
- |\\
- | * remote-commit (origin/feature-foo, feature-foo) update foo.txt (remote)
- | * local-commit add foo.txt
+ @r"
+ * 9bafbf1 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ |\
+ * | remote-commit (origin/feature-foo, feature-foo) update foo.txt (remote)
+ * | local-commit add foo.txt
|/
* da7bed3 (origin/main, main, empty-branch) add main.txt
"
diff --git a/crates/but-workspace/tests/workspace/branch/move_branch.rs b/crates/but-workspace/tests/workspace/branch/move_branch.rs
index 6414fd2f996..5b57c8a03c4 100644
--- a/crates/but-workspace/tests/workspace/branch/move_branch.rs
+++ b/crates/but-workspace/tests/workspace/branch/move_branch.rs
@@ -32,14 +32,14 @@ fn move_top_branch_to_top_of_another_stack() -> anyhow::Result<()> {
let mut ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:C on 85efbe4 {2}
- ├── 📙:4:C
- │ └── ·09bc93e (🏘️)
- └── 📙:5:B
- └── ·c813d8d (🏘️)
+ ├── ≡📙:4:C on 85efbe4 {2}
+ │ ├── 📙:4:C
+ │ │ └── ·09bc93e (🏘️)
+ │ └── 📙:5:B
+ │ └── ·c813d8d (🏘️)
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
@@ -58,11 +58,11 @@ fn move_top_branch_to_top_of_another_stack() -> anyhow::Result<()> {
ws.refresh_from_head(&repo, &meta, project_meta)?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * 0ffeac6 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * bdcbf64 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * f2cc60d (C) C
- | * 09d8e52 (A) A
- * | c813d8d (B) B
+ | * c813d8d (B) B
+ * | f2cc60d (C) C
+ * | 09d8e52 (A) A
|/
* 85efbe4 (origin/main, main) M
");
@@ -140,14 +140,14 @@ fn move_bottom_branch_to_top_of_another_stack() -> anyhow::Result<()> {
let mut ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:C on 85efbe4 {2}
- ├── 📙:4:C
- │ └── ·09bc93e (🏘️)
- └── 📙:5:B
- └── ·c813d8d (🏘️)
+ ├── ≡📙:4:C on 85efbe4 {2}
+ │ ├── 📙:4:C
+ │ │ └── ·09bc93e (🏘️)
+ │ └── 📙:5:B
+ │ └── ·c813d8d (🏘️)
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
@@ -176,14 +176,14 @@ fn move_bottom_branch_to_top_of_another_stack() -> anyhow::Result<()> {
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:B on 85efbe4 {1}
- │ ├── 📙:3:B
- │ │ └── ·f9061ed (🏘️)
- │ └── 📙:4:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:5:C on 85efbe4 {2}
- └── 📙:5:C
- └── ·8e00332 (🏘️)
+ ├── ≡📙:5:C on 85efbe4 {2}
+ │ └── 📙:5:C
+ │ └── ·8e00332 (🏘️)
+ └── ≡📙:3:B on 85efbe4 {1}
+ ├── 📙:3:B
+ │ └── ·f9061ed (🏘️)
+ └── 📙:4:A
+ └── ·09d8e52 (🏘️)
");
Ok(())
@@ -212,14 +212,14 @@ fn move_single_branch_to_top_of_another_stack() -> anyhow::Result<()> {
let mut ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:C on 85efbe4 {2}
- ├── 📙:4:C
- │ └── ·09bc93e (🏘️)
- └── 📙:5:B
- └── ·c813d8d (🏘️)
+ ├── ≡📙:4:C on 85efbe4 {2}
+ │ ├── 📙:4:C
+ │ │ └── ·09bc93e (🏘️)
+ │ └── 📙:5:B
+ │ └── ·c813d8d (🏘️)
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
@@ -282,14 +282,14 @@ fn reorder_branch_in_stack() -> anyhow::Result<()> {
let mut ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:C on 85efbe4 {2}
- ├── 📙:4:C
- │ └── ·09bc93e (🏘️)
- └── 📙:5:B
- └── ·c813d8d (🏘️)
+ ├── ≡📙:4:C on 85efbe4 {2}
+ │ ├── 📙:4:C
+ │ │ └── ·09bc93e (🏘️)
+ │ └── 📙:5:B
+ │ └── ·c813d8d (🏘️)
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
@@ -319,14 +319,14 @@ fn reorder_branch_in_stack() -> anyhow::Result<()> {
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:B on 85efbe4 {2}
- ├── 📙:4:B
- │ └── ·de0581e (🏘️)
- └── 📙:5:C
- └── ·8e00332 (🏘️)
+ ├── ≡📙:4:B on 85efbe4 {2}
+ │ ├── 📙:4:B
+ │ │ └── ·de0581e (🏘️)
+ │ └── 📙:5:C
+ │ └── ·8e00332 (🏘️)
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
Ok(())
@@ -355,14 +355,14 @@ fn insert_branch_in_the_middle_of_a_stack() -> anyhow::Result<()> {
let mut ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:C on 85efbe4 {2}
- ├── 📙:4:C
- │ └── ·09bc93e (🏘️)
- └── 📙:5:B
- └── ·c813d8d (🏘️)
+ ├── ≡📙:4:C on 85efbe4 {2}
+ │ ├── 📙:4:C
+ │ │ └── ·09bc93e (🏘️)
+ │ └── 📙:5:B
+ │ └── ·c813d8d (🏘️)
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
@@ -420,11 +420,11 @@ fn move_empty_branch() -> anyhow::Result<()> {
let mut ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:B on 85efbe4 {2}
- └── 📙:4:B
+ ├── ≡📙:4:B on 85efbe4 {2}
+ │ └── 📙:4:B
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
@@ -476,11 +476,11 @@ fn move_branch_on_top_of_empty_branch() -> anyhow::Result<()> {
let mut ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:B on 85efbe4 {2}
- └── 📙:4:B
+ ├── ≡📙:4:B on 85efbe4 {2}
+ │ └── 📙:4:B
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
@@ -631,7 +631,7 @@ fn move_empty_branch_on_top_of_empty_branch_across_stacks() -> anyhow::Result<()
}
#[test]
-fn non_empty_move_updates_metadata_and_keeps_display_order_aligned() -> anyhow::Result<()> {
+fn non_empty_move_display_order_follows_workspace_parents() -> anyhow::Result<()> {
let (_tmp, graph, repo, mut meta, _description) =
named_writable_scenario_with_description_and_graph(
"ws-ref-ws-commit-single-stack-double-stack",
@@ -654,24 +654,24 @@ fn non_empty_move_updates_metadata_and_keeps_display_order_aligned() -> anyhow::
let mut ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:C on 85efbe4 {2}
- ├── 📙:4:C
- │ └── ·09bc93e (🏘️)
- └── 📙:5:B
- └── ·c813d8d (🏘️)
+ ├── ≡📙:4:C on 85efbe4 {2}
+ │ ├── 📙:4:C
+ │ │ └── ·09bc93e (🏘️)
+ │ └── 📙:5:B
+ │ └── ·c813d8d (🏘️)
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
let before_display_order = stack_display_order(&ws);
let before_metadata_order = metadata_stack_order(&ws);
- assert_eq!(
- before_display_order, before_metadata_order,
- "workspace projection order should match metadata before moving now that stack order is no longer reversed downstream"
- );
+ // Stack order comes from the workspace-commit parent array, not metadata. This
+ // fixture bakes a parent order that differs from the injected metadata order, so
+ // the two differ before the move — and the parent array is what wins.
+ assert_ne!(before_display_order, before_metadata_order);
- // Move non-empty C on top of non-empty A.
- // This rewrites metadata and keeps display + metadata aligned.
+ // Move non-empty C on top of non-empty A. The displayed order then follows the
+ // rebuilt workspace-commit parent array.
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
let but_workspace::branch::move_branch::Outcome { rebase, ws_meta } =
but_workspace::branch::move_branch(
@@ -690,14 +690,14 @@ fn non_empty_move_updates_metadata_and_keeps_display_order_aligned() -> anyhow::
insta::assert_snapshot!(graph_workspace(&ws), "before refreshing `ws` the pure-virtual change isn't visible (should be fixed once meta is in db!)", @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:5:B on 85efbe4 {2}
- │ └── 📙:5:B
- │ └── ·c813d8d (🏘️)
- └── ≡📙:4:C on 85efbe4 {1}
- ├── 📙:4:C
- │ └── ·f2cc60d (🏘️)
- └── 📙:3:A
- └── ·09d8e52 (🏘️)
+ ├── ≡📙:4:C on 85efbe4 {2}
+ │ ├── 📙:4:C
+ │ │ └── ·f2cc60d (🏘️)
+ │ └── 📙:3:A
+ │ └── ·09d8e52 (🏘️)
+ └── ≡📙:5:B on 85efbe4
+ └── 📙:5:B
+ └── ·c813d8d (🏘️)
");
let project_meta = ws.graph.project_meta.clone();
ws.refresh_from_head(&repo, &meta, project_meta)?;
@@ -715,17 +715,17 @@ fn non_empty_move_updates_metadata_and_keeps_display_order_aligned() -> anyhow::
let after_display_order = stack_display_order(&ws);
+ // The move changes both the stored metadata order and the displayed order.
assert_ne!(updated_metadata_order, before_metadata_order);
assert_ne!(after_display_order, before_display_order);
- assert_eq!(
- after_display_order, updated_metadata_order,
- "workspace projection order should match metadata after moving now that stack order is no longer reversed downstream"
- );
+ // Stack order is taken from the workspace-commit parent array (the source of
+ // truth), not metadata order: after the move the parents are [B, C] while
+ // metadata is [C, B]. The order snapshots below capture the parent-array order.
insta::assert_snapshot!(format!("{before_display_order:#?}"), @r#"
[
- "refs/heads/A",
"refs/heads/C",
+ "refs/heads/A",
]
"#);
@@ -740,7 +740,7 @@ fn non_empty_move_updates_metadata_and_keeps_display_order_aligned() -> anyhow::
}
#[test]
-fn empty_move_keeps_display_order_aligned_with_metadata() -> anyhow::Result<()> {
+fn empty_move_display_order_follows_workspace_parents() -> anyhow::Result<()> {
let (_tmp, graph, repo, mut meta, _description) =
named_writable_scenario_with_description_and_graph("ws-with-empty-stack", |meta| {
add_stack_with_segments(meta, 1, "A", StackState::InWorkspace, &[]);
@@ -758,10 +758,13 @@ fn empty_move_keeps_display_order_aligned_with_metadata() -> anyhow::Result<()>
let mut ws = graph.into_workspace()?;
let before_display_order = stack_display_order(&ws);
let before_metadata_order = metadata_stack_order(&ws);
- assert_eq!(before_display_order, before_metadata_order);
+ // Stack order comes from the workspace-commit parent array, not metadata. The
+ // empty lane sits at the base (parent 0) and so leads here; `explicit-empty-branches`
+ // will give empty lanes their own distinct parent positions.
+ assert_ne!(before_display_order, before_metadata_order);
- // Move empty B on top of non-empty A.
- // This path rewrites metadata and keeps display + metadata aligned.
+ // Move empty B on top of non-empty A. The displayed order then follows the
+ // rebuilt workspace-commit parent array.
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
let but_workspace::branch::move_branch::Outcome { rebase, ws_meta } =
but_workspace::branch::move_branch(
@@ -782,14 +785,15 @@ fn empty_move_keeps_display_order_aligned_with_metadata() -> anyhow::Result<()>
let after_display_order = stack_display_order(&ws);
+ // The move changes both the stored metadata order and the displayed order; the
+ // displayed order is taken from the workspace-commit parent array (see snapshot).
assert_ne!(updated_metadata_order, before_metadata_order);
assert_ne!(after_display_order, before_display_order);
- assert_eq!(after_display_order, updated_metadata_order);
insta::assert_snapshot!(format!("{before_display_order:#?}"), @r#"
[
- "refs/heads/A",
"refs/heads/B",
+ "refs/heads/A",
]
"#);
diff --git a/crates/but-workspace/tests/workspace/branch/tear_off_branch.rs b/crates/but-workspace/tests/workspace/branch/tear_off_branch.rs
index 9acdc90ca7d..aaa7e9adb31 100644
--- a/crates/but-workspace/tests/workspace/branch/tear_off_branch.rs
+++ b/crates/but-workspace/tests/workspace/branch/tear_off_branch.rs
@@ -29,14 +29,14 @@ fn tear_off_top_most_branch() -> anyhow::Result<()> {
let mut ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:C on 85efbe4 {2}
- ├── 📙:4:C
- │ └── ·09bc93e (🏘️)
- └── 📙:5:B
- └── ·c813d8d (🏘️)
+ ├── ≡📙:4:C on 85efbe4 {2}
+ │ ├── 📙:4:C
+ │ │ └── ·09bc93e (🏘️)
+ │ └── 📙:5:B
+ │ └── ·c813d8d (🏘️)
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
@@ -55,12 +55,12 @@ fn tear_off_top_most_branch() -> anyhow::Result<()> {
ws.refresh_from_head(&repo, &meta, project_meta)?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- *-. efd284c (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ *-. 16e2eb1 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\ \
- | | * 09d8e52 (A) A
+ | | * 8e00332 (C) C
| * | c813d8d (B) B
| |/
- * / 8e00332 (C) C
+ * / 09d8e52 (A) A
|/
* 85efbe4 (origin/main, main) M
");
@@ -104,14 +104,14 @@ fn tear_off_bottom_most_branch() -> anyhow::Result<()> {
let mut ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:C on 85efbe4 {2}
- ├── 📙:4:C
- │ └── ·09bc93e (🏘️)
- └── 📙:5:B
- └── ·c813d8d (🏘️)
+ ├── ≡📙:4:C on 85efbe4 {2}
+ │ ├── 📙:4:C
+ │ │ └── ·09bc93e (🏘️)
+ │ └── 📙:5:B
+ │ └── ·c813d8d (🏘️)
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
@@ -130,24 +130,24 @@ fn tear_off_bottom_most_branch() -> anyhow::Result<()> {
ws.refresh_from_head(&repo, &meta, project_meta)?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- *-. a3c9e85 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ *-. 7e46497 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\ \
- | | * 09d8e52 (A) A
- | * | 8e00332 (C) C
+ | | * c813d8d (B) B
+ | * | 09d8e52 (A) A
| |/
- * / c813d8d (B) B
+ * / 8e00332 (C) C
|/
* 85efbe4 (origin/main, main) M
");
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
├── ≡📙:4:C on 85efbe4 {2}
│ └── 📙:4:C
│ └── ·8e00332 (🏘️)
+ ├── ≡📙:3:A on 85efbe4 {1}
+ │ └── 📙:3:A
+ │ └── ·09d8e52 (🏘️)
└── ≡📙:5:B on 85efbe4 {3}
└── 📙:5:B
└── ·c813d8d (🏘️)
@@ -179,14 +179,14 @@ fn tear_off_only_branch_in_stack() -> anyhow::Result<()> {
let mut ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:C on 85efbe4 {2}
- ├── 📙:4:C
- │ └── ·09bc93e (🏘️)
- └── 📙:5:B
- └── ·c813d8d (🏘️)
+ ├── ≡📙:4:C on 85efbe4 {2}
+ │ ├── 📙:4:C
+ │ │ └── ·09bc93e (🏘️)
+ │ └── 📙:5:B
+ │ └── ·c813d8d (🏘️)
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
@@ -216,14 +216,14 @@ fn tear_off_only_branch_in_stack() -> anyhow::Result<()> {
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:C on 85efbe4 {2}
- ├── 📙:4:C
- │ └── ·09bc93e (🏘️)
- └── 📙:5:B
- └── ·c813d8d (🏘️)
+ ├── ≡📙:4:C on 85efbe4 {2}
+ │ ├── 📙:4:C
+ │ │ └── ·09bc93e (🏘️)
+ │ └── 📙:5:B
+ │ └── ·c813d8d (🏘️)
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
Ok(())
@@ -269,10 +269,10 @@ fn tear_off_from_single_stack_in_ws_top() -> anyhow::Result<()> {
ws.refresh_from_head(&repo, &meta, project_meta)?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * e2d89a5 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 828af37 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 09d8e52 (A) A
- * | 1273ba9 (B) B
+ | * 1273ba9 (B) B
+ * | 09d8e52 (A) A
|/
* 85efbe4 (origin/main, main) M
");
@@ -330,10 +330,10 @@ fn tear_off_from_single_stack_in_ws_bottom() -> anyhow::Result<()> {
ws.refresh_from_head(&repo, &meta, project_meta)?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * 828af37 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * e2d89a5 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 1273ba9 (B) B
- * | 09d8e52 (A) A
+ | * 09d8e52 (A) A
+ * | 1273ba9 (B) B
|/
* 85efbe4 (origin/main, main) M
");
@@ -391,9 +391,9 @@ fn tear_off_empty_branch() -> anyhow::Result<()> {
ws.refresh_from_head(&repo, &meta, project_meta)?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * d744692 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * b1314f4 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 09d8e52 (A) A
+ * | 09d8e52 (A) A
|/
* 85efbe4 (origin/main, main, B) M
");
@@ -450,9 +450,9 @@ fn tear_off_non_empty_branch() -> anyhow::Result<()> {
ws.refresh_from_head(&repo, &meta, project_meta)?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * b1314f4 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * d744692 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- * | 09d8e52 (A) A
+ | * 09d8e52 (A) A
|/
* 85efbe4 (origin/main, main, B) M
");
diff --git a/crates/but-workspace/tests/workspace/commit/discard_commit.rs b/crates/but-workspace/tests/workspace/commit/discard_commit.rs
index e38441ea1d4..e1a4a843246 100644
--- a/crates/but-workspace/tests/workspace/commit/discard_commit.rs
+++ b/crates/but-workspace/tests/workspace/commit/discard_commit.rs
@@ -89,14 +89,14 @@ fn discard_tip_commit_in_workspace_stack() -> Result<()> {
let mut ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:C on 85efbe4 {2}
- ├── 📙:4:C
- │ └── ·09bc93e (🏘️)
- └── 📙:5:B
- └── ·c813d8d (🏘️)
+ ├── ≡📙:4:C on 85efbe4 {2}
+ │ ├── 📙:4:C
+ │ │ └── ·09bc93e (🏘️)
+ │ └── 📙:5:B
+ │ └── ·c813d8d (🏘️)
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
let outcome = discard_commits(editor, [c.detach()])?;
@@ -104,13 +104,13 @@ fn discard_tip_commit_in_workspace_stack() -> Result<()> {
let outcome = outcome.materialize()?;
insta::assert_snapshot!(graph_workspace(outcome.workspace), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:5:C on 85efbe4 {2}
- ├── 📙:5:C
- └── 📙:6:B
- └── ·c813d8d (🏘️)
+ ├── ≡📙:5:C on 85efbe4 {2}
+ │ ├── 📙:5:C
+ │ └── 📙:6:B
+ │ └── ·c813d8d (🏘️)
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
let tip_of_c = repo.rev_parse_single("C")?;
@@ -156,14 +156,14 @@ fn discard_bottom_commit_in_workspace_stack() -> Result<()> {
let mut ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:C on 85efbe4 {2}
- ├── 📙:4:C
- │ └── ·09bc93e (🏘️)
- └── 📙:5:B
- └── ·c813d8d (🏘️)
+ ├── ≡📙:4:C on 85efbe4 {2}
+ │ ├── 📙:4:C
+ │ │ └── ·09bc93e (🏘️)
+ │ └── 📙:5:B
+ │ └── ·c813d8d (🏘️)
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
let outcome = discard_commits(editor, [b.detach()])?;
@@ -171,13 +171,13 @@ fn discard_bottom_commit_in_workspace_stack() -> Result<()> {
let outcome = outcome.materialize()?;
insta::assert_snapshot!(graph_workspace(outcome.workspace), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:C on 85efbe4 {2}
- ├── 📙:4:C
- │ └── ·8e00332 (🏘️)
- └── 📙:5:B
+ ├── ≡📙:4:C on 85efbe4 {2}
+ │ ├── 📙:4:C
+ │ │ └── ·8e00332 (🏘️)
+ │ └── 📙:5:B
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
let tip_of_b = repo.rev_parse_single("B")?;
diff --git a/crates/but-workspace/tests/workspace/commit/move_commit.rs b/crates/but-workspace/tests/workspace/commit/move_commit.rs
index ee4f57e16f6..943878080e7 100644
--- a/crates/but-workspace/tests/workspace/commit/move_commit.rs
+++ b/crates/but-workspace/tests/workspace/commit/move_commit.rs
@@ -40,14 +40,14 @@ fn move_top_commit_to_top_of_another_stack() -> anyhow::Result<()> {
let mut ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:C on 85efbe4 {2}
- ├── 📙:4:C
- │ └── ·09bc93e (🏘️)
- └── 📙:5:B
- └── ·c813d8d (🏘️)
+ ├── ≡📙:4:C on 85efbe4 {2}
+ │ ├── 📙:4:C
+ │ │ └── ·09bc93e (🏘️)
+ │ └── 📙:5:B
+ │ └── ·c813d8d (🏘️)
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
@@ -98,14 +98,14 @@ fn move_top_commit_to_top_of_another_stack() -> anyhow::Result<()> {
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ ├── ·f2cc60d (🏘️)
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:5:C on 85efbe4 {2}
- ├── 📙:5:C
- └── 📙:6:B
- └── ·c813d8d (🏘️)
+ ├── ≡📙:5:C on 85efbe4 {2}
+ │ ├── 📙:5:C
+ │ └── 📙:6:B
+ │ └── ·c813d8d (🏘️)
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ ├── ·f2cc60d (🏘️)
+ └── ·09d8e52 (🏘️)
");
Ok(())
@@ -134,14 +134,14 @@ fn move_bottom_commit_to_top_of_another_stack() -> anyhow::Result<()> {
let mut ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:C on 85efbe4 {2}
- ├── 📙:4:C
- │ └── ·09bc93e (🏘️)
- └── 📙:5:B
- └── ·c813d8d (🏘️)
+ ├── ≡📙:4:C on 85efbe4 {2}
+ │ ├── 📙:4:C
+ │ │ └── ·09bc93e (🏘️)
+ │ └── 📙:5:B
+ │ └── ·c813d8d (🏘️)
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
@@ -194,14 +194,14 @@ fn move_bottom_commit_to_top_of_another_stack() -> anyhow::Result<()> {
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ ├── ·f9061ed (🏘️)
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:C on 85efbe4 {2}
- ├── 📙:4:C
- │ └── ·8e00332 (🏘️)
- └── 📙:5:B
+ ├── ≡📙:4:C on 85efbe4 {2}
+ │ ├── 📙:4:C
+ │ │ └── ·8e00332 (🏘️)
+ │ └── 📙:5:B
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ ├── ·f9061ed (🏘️)
+ └── ·09d8e52 (🏘️)
");
Ok(())
@@ -230,14 +230,14 @@ fn move_top_commit_to_bottom_of_another_stack() -> anyhow::Result<()> {
let mut ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:C on 85efbe4 {2}
- ├── 📙:4:C
- │ └── ·09bc93e (🏘️)
- └── 📙:5:B
- └── ·c813d8d (🏘️)
+ ├── ≡📙:4:C on 85efbe4 {2}
+ │ ├── 📙:4:C
+ │ │ └── ·09bc93e (🏘️)
+ │ └── 📙:5:B
+ │ └── ·c813d8d (🏘️)
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
@@ -288,14 +288,14 @@ fn move_top_commit_to_bottom_of_another_stack() -> anyhow::Result<()> {
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ ├── ·2506923 (🏘️)
- │ └── ·8e00332 (🏘️)
- └── ≡📙:5:C on 85efbe4 {2}
- ├── 📙:5:C
- └── 📙:6:B
- └── ·c813d8d (🏘️)
+ ├── ≡📙:5:C on 85efbe4 {2}
+ │ ├── 📙:5:C
+ │ └── 📙:6:B
+ │ └── ·c813d8d (🏘️)
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ ├── ·2506923 (🏘️)
+ └── ·8e00332 (🏘️)
");
Ok(())
@@ -324,14 +324,14 @@ fn move_bottom_commit_to_bottom_of_another_stack() -> anyhow::Result<()> {
let mut ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:C on 85efbe4 {2}
- ├── 📙:4:C
- │ └── ·09bc93e (🏘️)
- └── 📙:5:B
- └── ·c813d8d (🏘️)
+ ├── ≡📙:4:C on 85efbe4 {2}
+ │ ├── 📙:4:C
+ │ │ └── ·09bc93e (🏘️)
+ │ └── 📙:5:B
+ │ └── ·c813d8d (🏘️)
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
@@ -384,14 +384,14 @@ fn move_bottom_commit_to_bottom_of_another_stack() -> anyhow::Result<()> {
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ ├── ·4dfe841 (🏘️)
- │ └── ·c813d8d (🏘️)
- └── ≡📙:4:C on 85efbe4 {2}
- ├── 📙:4:C
- │ └── ·8e00332 (🏘️)
- └── 📙:5:B
+ ├── ≡📙:4:C on 85efbe4 {2}
+ │ ├── 📙:4:C
+ │ │ └── ·8e00332 (🏘️)
+ │ └── 📙:5:B
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ ├── ·4dfe841 (🏘️)
+ └── ·c813d8d (🏘️)
");
Ok(())
@@ -420,14 +420,14 @@ fn move_single_commit_to_the_top_of_another_branch() -> anyhow::Result<()> {
let mut ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:C on 85efbe4 {2}
- ├── 📙:4:C
- │ └── ·09bc93e (🏘️)
- └── 📙:5:B
- └── ·c813d8d (🏘️)
+ ├── ≡📙:4:C on 85efbe4 {2}
+ │ ├── 📙:4:C
+ │ │ └── ·09bc93e (🏘️)
+ │ └── 📙:5:B
+ │ └── ·c813d8d (🏘️)
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
@@ -471,14 +471,14 @@ fn move_single_commit_to_the_top_of_another_branch() -> anyhow::Result<()> {
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:5:A on 85efbe4 {1}
- │ └── 📙:5:A
- └── ≡📙:3:C on 85efbe4 {2}
- ├── 📙:3:C
- │ ├── ·148f8f3 (🏘️)
- │ └── ·09bc93e (🏘️)
- └── 📙:4:B
- └── ·c813d8d (🏘️)
+ ├── ≡📙:3:C on 85efbe4 {2}
+ │ ├── 📙:3:C
+ │ │ ├── ·148f8f3 (🏘️)
+ │ │ └── ·09bc93e (🏘️)
+ │ └── 📙:4:B
+ │ └── ·c813d8d (🏘️)
+ └── ≡📙:5:A on 85efbe4 {1}
+ └── 📙:5:A
");
Ok(())
@@ -507,14 +507,14 @@ fn move_single_commit_to_the_bottom_of_another_branch() -> anyhow::Result<()> {
let mut ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:C on 85efbe4 {2}
- ├── 📙:4:C
- │ └── ·09bc93e (🏘️)
- └── 📙:5:B
- └── ·c813d8d (🏘️)
+ ├── ≡📙:4:C on 85efbe4 {2}
+ │ ├── 📙:4:C
+ │ │ └── ·09bc93e (🏘️)
+ │ └── 📙:5:B
+ │ └── ·c813d8d (🏘️)
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
@@ -567,14 +567,14 @@ fn move_single_commit_to_the_bottom_of_another_branch() -> anyhow::Result<()> {
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:5:A on 85efbe4 {1}
- │ └── 📙:5:A
- └── ≡📙:3:C on 85efbe4 {2}
- ├── 📙:3:C
- │ └── ·ad476a8 (🏘️)
- └── 📙:4:B
- ├── ·f9061ed (🏘️)
- └── ·09d8e52 (🏘️)
+ ├── ≡📙:3:C on 85efbe4 {2}
+ │ ├── 📙:3:C
+ │ │ └── ·ad476a8 (🏘️)
+ │ └── 📙:4:B
+ │ ├── ·f9061ed (🏘️)
+ │ └── ·09d8e52 (🏘️)
+ └── ≡📙:5:A on 85efbe4 {1}
+ └── 📙:5:A
");
Ok(())
@@ -598,11 +598,11 @@ fn move_commit_to_empty_branch() -> anyhow::Result<()> {
let mut ws = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&ws), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 85efbe4
- ├── ≡📙:3:A on 85efbe4 {1}
- │ └── 📙:3:A
- │ └── ·09d8e52 (🏘️)
- └── ≡📙:4:B on 85efbe4 {2}
- └── 📙:4:B
+ ├── ≡📙:4:B on 85efbe4 {2}
+ │ └── 📙:4:B
+ └── ≡📙:3:A on 85efbe4 {1}
+ └── 📙:3:A
+ └── ·09d8e52 (🏘️)
");
let editor = Editor::create(&mut ws, &mut meta, &repo)?;
@@ -632,9 +632,9 @@ fn move_commit_to_empty_branch() -> anyhow::Result<()> {
);
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * e16ce30 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 6d5c23e (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- * | 09d8e52 (B) A
+ | * 09d8e52 (B) A
|/
* 85efbe4 (origin/main, main, A) M
");
diff --git a/crates/but-workspace/tests/workspace/commit/squash_commits.rs b/crates/but-workspace/tests/workspace/commit/squash_commits.rs
index 98a29f4ca10..ef493f58e23 100644
--- a/crates/but-workspace/tests/workspace/commit/squash_commits.rs
+++ b/crates/but-workspace/tests/workspace/commit/squash_commits.rs
@@ -416,10 +416,10 @@ fn squash_across_stacks_subject_into_target() -> Result<()> {
let mut ws = graph.into_workspace()?;
let normalized = visualize_commit_graph_all(&repo)?.replace(" \n", "\n");
insta::assert_snapshot!(normalized, @r"
- * c49e4d8 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 3147679 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 09d8e52 (A) A
- * | c813d8d (B) B
+ | * c813d8d (B) B
+ * | 09d8e52 (A) A
|/
* 85efbe4 (origin/main, main) M
");
@@ -453,9 +453,9 @@ fn squash_across_stacks_subject_into_target() -> Result<()> {
assert_eq!(squashed_commit.tree_id()?.detach(), subject_tree);
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * e9ad17f (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 9a2af48 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- * | 82d6f41 (B) B
+ | * 82d6f41 (B) B
|/
* 85efbe4 (origin/main, main, A) M
");
@@ -483,10 +483,10 @@ fn squash_across_stacks_target_into_subject() -> Result<()> {
let normalized = visualize_commit_graph_all(&repo)?.replace(" \n", "\n");
insta::assert_snapshot!(normalized, @r"
- * c49e4d8 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 3147679 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 09d8e52 (A) A
- * | c813d8d (B) B
+ | * c813d8d (B) B
+ * | 09d8e52 (A) A
|/
* 85efbe4 (origin/main, main) M
");
@@ -521,9 +521,9 @@ fn squash_across_stacks_target_into_subject() -> Result<()> {
assert_eq!(squashed_commit.tree_id()?.detach(), subject_tree);
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * b7ec700 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * b79b1e5 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 17e27b0 (A) A
+ * | 17e27b0 (A) A
|/
* 85efbe4 (origin/main, main, B) M
");
@@ -549,11 +549,11 @@ fn squash_cross_stack_commit_does_not_pull_in_ancestor_tree_state() -> Result<()
let normalized = visualize_commit_graph_all(&repo)?.replace(" \n", "\n");
insta::assert_snapshot!(normalized, @r"
- * c47834b (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 12c47aa (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 26e45af (A) A
- * | 356de85 (C) C
- * | f25f65c (B) B
+ | * 356de85 (C) C
+ | * f25f65c (B) B
+ * | 26e45af (A) A
|/
* 893d602 (origin/main, main) M
");
@@ -613,13 +613,13 @@ fn squash_cross_stack_commit_with_deeper_stacks_does_not_pull_in_ancestor_tree_s
let normalized = visualize_commit_graph_all(&repo)?.replace(" \n", "\n");
insta::assert_snapshot!(normalized, @r"
- * 8cf5961 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 813c644 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * e352141 (D) D
- | * 26e45af (A) A
- * | b9b4be4 (E) E
- * | 356de85 (C) C
- * | f25f65c (B) B
+ | * b9b4be4 (E) E
+ | * 356de85 (C) C
+ | * f25f65c (B) B
+ * | e352141 (D) D
+ * | 26e45af (A) A
|/
* 893d602 (origin/main, main) M
");
@@ -644,12 +644,12 @@ fn squash_cross_stack_commit_with_deeper_stacks_does_not_pull_in_ancestor_tree_s
let normalized = visualize_commit_graph_all(&repo)?.replace(" \n", "\n");
insta::assert_snapshot!(normalized, @r"
- * 7dd4136 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 6cd47a2 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 62c025c (D) D
- | * 26e45af (A) A
- * | 356de85 (E, C) C
- * | f25f65c (B) B
+ | * 356de85 (E, C) C
+ | * f25f65c (B) B
+ * | 62c025c (D) D
+ * | 26e45af (A) A
|/
* 893d602 (origin/main, main) M
");
diff --git a/crates/but-workspace/tests/workspace/ref_info/with_workspace_commit/legacy.rs b/crates/but-workspace/tests/workspace/ref_info/with_workspace_commit/legacy.rs
index d2a61c5a9ee..869dca9134c 100644
--- a/crates/but-workspace/tests/workspace/ref_info/with_workspace_commit/legacy.rs
+++ b/crates/but-workspace/tests/workspace/ref_info/with_workspace_commit/legacy.rs
@@ -32,10 +32,10 @@ mod stacks {
StackState::InWorkspace,
);
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * 820f2b3 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * fe1a116 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 4e5484a (B-on-A) add new file in B-on-A
- * | 5f37dbf (C-on-A) add new file in C-on-A
+ | * 5f37dbf (C-on-A) add new file in C-on-A
+ * | 4e5484a (B-on-A) add new file in B-on-A
|/
| * 89cc2d3 (origin/A) change in A
|/
@@ -48,12 +48,12 @@ mod stacks {
[
StackEntry {
id: Some(
- 00000000-0000-0000-0000-000000000002,
+ 00000000-0000-0000-0000-000000000001,
),
heads: [
StackHeadInfo {
- name: "C-on-A",
- tip: Sha1(5f37dbfd4b1c3d2ee75f216665ab4edf44c843cb),
+ name: "B-on-A",
+ tip: Sha1(4e5484ac0f1da1909414b1e16bd740c1a3599509),
review_id: None,
is_checked_out: false,
},
@@ -64,18 +64,18 @@ mod stacks {
is_checked_out: false,
},
],
- tip: Sha1(5f37dbfd4b1c3d2ee75f216665ab4edf44c843cb),
+ tip: Sha1(4e5484ac0f1da1909414b1e16bd740c1a3599509),
order: None,
is_checked_out: false,
},
StackEntry {
id: Some(
- 00000000-0000-0000-0000-000000000001,
+ 00000000-0000-0000-0000-000000000002,
),
heads: [
StackHeadInfo {
- name: "B-on-A",
- tip: Sha1(4e5484ac0f1da1909414b1e16bd740c1a3599509),
+ name: "C-on-A",
+ tip: Sha1(5f37dbfd4b1c3d2ee75f216665ab4edf44c843cb),
review_id: None,
is_checked_out: false,
},
@@ -86,7 +86,7 @@ mod stacks {
is_checked_out: false,
},
],
- tip: Sha1(4e5484ac0f1da1909414b1e16bd740c1a3599509),
+ tip: Sha1(5f37dbfd4b1c3d2ee75f216665ab4edf44c843cb),
order: None,
is_checked_out: false,
},
@@ -99,12 +99,12 @@ mod stacks {
[
StackEntry {
id: Some(
- 00000000-0000-0000-0000-000000000002,
+ 00000000-0000-0000-0000-000000000001,
),
heads: [
StackHeadInfo {
- name: "C-on-A",
- tip: Sha1(5f37dbfd4b1c3d2ee75f216665ab4edf44c843cb),
+ name: "B-on-A",
+ tip: Sha1(4e5484ac0f1da1909414b1e16bd740c1a3599509),
review_id: None,
is_checked_out: false,
},
@@ -115,18 +115,18 @@ mod stacks {
is_checked_out: false,
},
],
- tip: Sha1(5f37dbfd4b1c3d2ee75f216665ab4edf44c843cb),
+ tip: Sha1(4e5484ac0f1da1909414b1e16bd740c1a3599509),
order: None,
is_checked_out: false,
},
StackEntry {
id: Some(
- 00000000-0000-0000-0000-000000000001,
+ 00000000-0000-0000-0000-000000000002,
),
heads: [
StackHeadInfo {
- name: "B-on-A",
- tip: Sha1(4e5484ac0f1da1909414b1e16bd740c1a3599509),
+ name: "C-on-A",
+ tip: Sha1(5f37dbfd4b1c3d2ee75f216665ab4edf44c843cb),
review_id: None,
is_checked_out: false,
},
@@ -137,7 +137,7 @@ mod stacks {
is_checked_out: false,
},
],
- tip: Sha1(4e5484ac0f1da1909414b1e16bd740c1a3599509),
+ tip: Sha1(5f37dbfd4b1c3d2ee75f216665ab4edf44c843cb),
order: None,
is_checked_out: false,
},
@@ -155,7 +155,7 @@ mod stacks {
[
StackEntry {
id: Some(
- 00000000-0000-0000-0000-000000000002,
+ 00000000-0000-0000-0000-000000000001,
),
heads: [
StackHeadInfo {
@@ -165,7 +165,7 @@ mod stacks {
is_checked_out: true,
},
],
- tip: Sha1(5f37dbfd4b1c3d2ee75f216665ab4edf44c843cb),
+ tip: Sha1(4e5484ac0f1da1909414b1e16bd740c1a3599509),
order: None,
is_checked_out: true,
},
@@ -178,19 +178,19 @@ mod stacks {
// filter.
insta::assert_debug_snapshot!(details, @r#"
StackDetails {
- derived_name: "C-on-A",
+ derived_name: "B-on-A",
push_status: CompletelyUnpushed,
branch_details: [
BranchDetails {
- name: "C-on-A",
+ name: "B-on-A",
reference: FullName(
- "refs/heads/C-on-A",
+ "refs/heads/B-on-A",
),
linked_worktree_id: None,
remote_tracking_branch: None,
pr_number: None,
review_id: None,
- tip: Sha1(5f37dbfd4b1c3d2ee75f216665ab4edf44c843cb),
+ tip: Sha1(4e5484ac0f1da1909414b1e16bd740c1a3599509),
base_commit: Sha1(d79bba960b112dbd25d45921c47eeda22288022b),
push_status: CompletelyUnpushed,
last_updated_at: None,
@@ -199,7 +199,7 @@ mod stacks {
],
is_conflicted: false,
commits: [
- Commit(5f37dbf, "add new file in C-on-A", local),
+ Commit(4e5484a, "add new file in B-on-A", local),
],
upstream_commits: [],
is_remote_head: false,
@@ -361,10 +361,10 @@ mod stack_details {
-> anyhow::Result<()> {
let (repo, mut meta) = read_only_in_memory_scenario("multiple-stacks-with-shared-segment")?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * 820f2b3 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * fe1a116 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 4e5484a (B-on-A) add new file in B-on-A
- * | 5f37dbf (C-on-A) add new file in C-on-A
+ | * 5f37dbf (C-on-A) add new file in C-on-A
+ * | 4e5484a (B-on-A) add new file in B-on-A
|/
| * 89cc2d3 (origin/A) change in A
|/
diff --git a/crates/but-workspace/tests/workspace/ref_info/with_workspace_commit/mod.rs b/crates/but-workspace/tests/workspace/ref_info/with_workspace_commit/mod.rs
index e638c249b7a..8e53d838e5f 100644
--- a/crates/but-workspace/tests/workspace/ref_info/with_workspace_commit/mod.rs
+++ b/crates/but-workspace/tests/workspace/ref_info/with_workspace_commit/mod.rs
@@ -1288,17 +1288,19 @@ fn single_commit_but_two_branches_one_in_ws_commit() -> anyhow::Result<()> {
stacks: [
Stack {
id: Some(
- 00000000-0000-0000-0000-000000000000,
+ 00000000-0000-0000-0000-000000000001,
),
base: Some(
Sha1(fafd9d08a839d99db60b222cd58e2e0bfaf1f7b2),
),
segments: [
ref_info::ui::Segment {
- id: NodeIndex(5),
- ref_name: "►lane",
+ id: NodeIndex(3),
+ ref_name: "►advanced-lane-2",
remote_tracking_ref_name: "None",
- commits: [],
+ commits: [
+ LocalCommit(93d7eac, "change 2\n", local),
+ ],
commits_on_remote: [],
commits_outside: None,
metadata: Branch,
@@ -1309,18 +1311,18 @@ fn single_commit_but_two_branches_one_in_ws_commit() -> anyhow::Result<()> {
},
Stack {
id: Some(
- 00000000-0000-0000-0000-000000000001,
+ 00000000-0000-0000-0000-000000000002,
),
base: Some(
Sha1(fafd9d08a839d99db60b222cd58e2e0bfaf1f7b2),
),
segments: [
ref_info::ui::Segment {
- id: NodeIndex(3),
- ref_name: "►advanced-lane-2",
+ id: NodeIndex(4),
+ ref_name: "►advanced-lane",
remote_tracking_ref_name: "None",
commits: [
- LocalCommit(93d7eac, "change 2\n", local),
+ LocalCommit(cbc6713, "change\n", local),
],
commits_on_remote: [],
commits_outside: None,
@@ -1332,19 +1334,17 @@ fn single_commit_but_two_branches_one_in_ws_commit() -> anyhow::Result<()> {
},
Stack {
id: Some(
- 00000000-0000-0000-0000-000000000002,
+ 00000000-0000-0000-0000-000000000000,
),
base: Some(
Sha1(fafd9d08a839d99db60b222cd58e2e0bfaf1f7b2),
),
segments: [
ref_info::ui::Segment {
- id: NodeIndex(4),
- ref_name: "►advanced-lane",
+ id: NodeIndex(5),
+ ref_name: "►lane",
remote_tracking_ref_name: "None",
- commits: [
- LocalCommit(cbc6713, "change\n", local),
- ],
+ commits: [],
commits_on_remote: [],
commits_outside: None,
metadata: Branch,
@@ -1740,19 +1740,17 @@ fn single_commit_but_two_branches_both_in_ws_commit() -> anyhow::Result<()> {
stacks: [
Stack {
id: Some(
- 00000000-0000-0000-0000-000000000000,
+ 00000000-0000-0000-0000-000000000001,
),
base: Some(
Sha1(fafd9d08a839d99db60b222cd58e2e0bfaf1f7b2),
),
segments: [
ref_info::ui::Segment {
- id: NodeIndex(3),
- ref_name: "►advanced-lane",
+ id: NodeIndex(4),
+ ref_name: "►lane",
remote_tracking_ref_name: "None",
- commits: [
- LocalCommit(cbc6713, "change\n", local),
- ],
+ commits: [],
commits_on_remote: [],
commits_outside: None,
metadata: Branch,
@@ -1763,17 +1761,19 @@ fn single_commit_but_two_branches_both_in_ws_commit() -> anyhow::Result<()> {
},
Stack {
id: Some(
- 00000000-0000-0000-0000-000000000001,
+ 00000000-0000-0000-0000-000000000000,
),
base: Some(
Sha1(fafd9d08a839d99db60b222cd58e2e0bfaf1f7b2),
),
segments: [
ref_info::ui::Segment {
- id: NodeIndex(4),
- ref_name: "►lane",
+ id: NodeIndex(3),
+ ref_name: "►advanced-lane",
remote_tracking_ref_name: "None",
- commits: [],
+ commits: [
+ LocalCommit(cbc6713, "change\n", local),
+ ],
commits_on_remote: [],
commits_outside: None,
metadata: Branch,
@@ -2531,8 +2531,11 @@ fn two_branches_stacked_with_interesting_remote_setup() -> anyhow::Result<()> {
fn single_commit_but_two_branches_stack_on_top_of_ws_commit() -> anyhow::Result<()> {
let (repo, mut meta) =
read_only_in_memory_scenario("two-branches-one-advanced-ws-commit-on-top-of-stack")?;
- insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @"
- * cbc6713 (HEAD -> gitbutler/workspace, advanced-lane) change
+ insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
+ * 335d6f2 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ |\
+ | * cbc6713 (advanced-lane) change
+ |/
* fafd9d0 (origin/main, main, lane) init
");
@@ -2549,7 +2552,7 @@ fn single_commit_but_two_branches_stack_on_top_of_ws_commit() -> anyhow::Result<
"refs/heads/gitbutler/workspace",
),
commit_id: Some(
- Sha1(cbc6713ccfc78aa9a3c9cf8305a6fadce0bbe1a4),
+ Sha1(335d6f2a960f387b039bd77476ae3d2d6649ed70),
),
worktree: Some(
Worktree {
@@ -2565,19 +2568,17 @@ fn single_commit_but_two_branches_stack_on_top_of_ws_commit() -> anyhow::Result<
stacks: [
Stack {
id: Some(
- 00000000-0000-0000-0000-000000000000,
+ 00000000-0000-0000-0000-000000000001,
),
base: Some(
Sha1(fafd9d08a839d99db60b222cd58e2e0bfaf1f7b2),
),
segments: [
ref_info::ui::Segment {
- id: NodeIndex(3),
- ref_name: "►advanced-lane",
+ id: NodeIndex(4),
+ ref_name: "►lane",
remote_tracking_ref_name: "None",
- commits: [
- LocalCommit(cbc6713, "change\n", local),
- ],
+ commits: [],
commits_on_remote: [],
commits_outside: None,
metadata: Branch,
@@ -2588,17 +2589,19 @@ fn single_commit_but_two_branches_stack_on_top_of_ws_commit() -> anyhow::Result<
},
Stack {
id: Some(
- 00000000-0000-0000-0000-000000000001,
+ 00000000-0000-0000-0000-000000000000,
),
base: Some(
Sha1(fafd9d08a839d99db60b222cd58e2e0bfaf1f7b2),
),
segments: [
ref_info::ui::Segment {
- id: NodeIndex(4),
- ref_name: "►lane",
+ id: NodeIndex(3),
+ ref_name: "►advanced-lane",
remote_tracking_ref_name: "None",
- commits: [],
+ commits: [
+ LocalCommit(cbc6713, "change\n", local),
+ ],
commits_on_remote: [],
commits_outside: None,
metadata: Branch,
@@ -2627,7 +2630,7 @@ fn single_commit_but_two_branches_stack_on_top_of_ws_commit() -> anyhow::Result<
NodeIndex(2),
),
is_managed_ref: true,
- is_managed_commit: false,
+ is_managed_commit: true,
ancestor_workspace_commit: None,
is_entrypoint: true,
}
@@ -2647,7 +2650,7 @@ fn single_commit_but_two_branches_stack_on_top_of_ws_commit() -> anyhow::Result<
"refs/heads/gitbutler/workspace",
),
commit_id: Some(
- Sha1(cbc6713ccfc78aa9a3c9cf8305a6fadce0bbe1a4),
+ Sha1(335d6f2a960f387b039bd77476ae3d2d6649ed70),
),
worktree: Some(
Worktree {
@@ -2663,19 +2666,17 @@ fn single_commit_but_two_branches_stack_on_top_of_ws_commit() -> anyhow::Result<
stacks: [
Stack {
id: Some(
- 00000000-0000-0000-0000-000000000000,
+ 00000000-0000-0000-0000-000000000001,
),
base: Some(
Sha1(fafd9d08a839d99db60b222cd58e2e0bfaf1f7b2),
),
segments: [
- 👉ref_info::ui::Segment {
- id: NodeIndex(0),
- ref_name: "►advanced-lane",
+ ref_info::ui::Segment {
+ id: NodeIndex(4),
+ ref_name: "►lane",
remote_tracking_ref_name: "None",
- commits: [
- LocalCommit(cbc6713, "change\n", local),
- ],
+ commits: [],
commits_on_remote: [],
commits_outside: None,
metadata: Branch,
@@ -2686,17 +2687,19 @@ fn single_commit_but_two_branches_stack_on_top_of_ws_commit() -> anyhow::Result<
},
Stack {
id: Some(
- 00000000-0000-0000-0000-000000000001,
+ 00000000-0000-0000-0000-000000000000,
),
base: Some(
Sha1(fafd9d08a839d99db60b222cd58e2e0bfaf1f7b2),
),
segments: [
- ref_info::ui::Segment {
- id: NodeIndex(4),
- ref_name: "►lane",
+ 👉ref_info::ui::Segment {
+ id: NodeIndex(0),
+ ref_name: "►advanced-lane",
remote_tracking_ref_name: "None",
- commits: [],
+ commits: [
+ LocalCommit(cbc6713, "change\n", local),
+ ],
commits_on_remote: [],
commits_outside: None,
metadata: Branch,
@@ -2725,7 +2728,7 @@ fn single_commit_but_two_branches_stack_on_top_of_ws_commit() -> anyhow::Result<
NodeIndex(3),
),
is_managed_ref: true,
- is_managed_commit: false,
+ is_managed_commit: true,
ancestor_workspace_commit: None,
is_entrypoint: false,
}
@@ -2775,17 +2778,19 @@ fn two_branches_one_advanced_two_parent_ws_commit_diverged_remote_tracking_branc
stacks: [
Stack {
id: Some(
- 00000000-0000-0000-0000-000000000000,
+ 00000000-0000-0000-0000-000000000001,
),
base: Some(
Sha1(fafd9d08a839d99db60b222cd58e2e0bfaf1f7b2),
),
segments: [
ref_info::ui::Segment {
- id: NodeIndex(4),
- ref_name: "►lane",
+ id: NodeIndex(3),
+ ref_name: "►advanced-lane",
remote_tracking_ref_name: "None",
- commits: [],
+ commits: [
+ LocalCommit(cbc6713, "change\n", local),
+ ],
commits_on_remote: [],
commits_outside: None,
metadata: Branch,
@@ -2796,19 +2801,17 @@ fn two_branches_one_advanced_two_parent_ws_commit_diverged_remote_tracking_branc
},
Stack {
id: Some(
- 00000000-0000-0000-0000-000000000001,
+ 00000000-0000-0000-0000-000000000000,
),
base: Some(
Sha1(fafd9d08a839d99db60b222cd58e2e0bfaf1f7b2),
),
segments: [
ref_info::ui::Segment {
- id: NodeIndex(3),
- ref_name: "►advanced-lane",
+ id: NodeIndex(4),
+ ref_name: "►lane",
remote_tracking_ref_name: "None",
- commits: [
- LocalCommit(cbc6713, "change\n", local),
- ],
+ commits: [],
commits_on_remote: [],
commits_outside: None,
metadata: Branch,
@@ -2871,17 +2874,19 @@ fn two_branches_one_advanced_two_parent_ws_commit_diverged_remote_tracking_branc
stacks: [
Stack {
id: Some(
- 00000000-0000-0000-0000-000000000000,
+ 00000000-0000-0000-0000-000000000001,
),
base: Some(
Sha1(fafd9d08a839d99db60b222cd58e2e0bfaf1f7b2),
),
segments: [
- ref_info::ui::Segment {
- id: NodeIndex(4),
- ref_name: "►lane",
+ 👉ref_info::ui::Segment {
+ id: NodeIndex(0),
+ ref_name: "►advanced-lane",
remote_tracking_ref_name: "None",
- commits: [],
+ commits: [
+ LocalCommit(cbc6713, "change\n", local),
+ ],
commits_on_remote: [],
commits_outside: None,
metadata: Branch,
@@ -2892,19 +2897,17 @@ fn two_branches_one_advanced_two_parent_ws_commit_diverged_remote_tracking_branc
},
Stack {
id: Some(
- 00000000-0000-0000-0000-000000000001,
+ 00000000-0000-0000-0000-000000000000,
),
base: Some(
Sha1(fafd9d08a839d99db60b222cd58e2e0bfaf1f7b2),
),
segments: [
- 👉ref_info::ui::Segment {
- id: NodeIndex(0),
- ref_name: "►advanced-lane",
+ ref_info::ui::Segment {
+ id: NodeIndex(4),
+ ref_name: "►lane",
remote_tracking_ref_name: "None",
- commits: [
- LocalCommit(cbc6713, "change\n", local),
- ],
+ commits: [],
commits_on_remote: [],
commits_outside: None,
metadata: Branch,
@@ -2962,17 +2965,19 @@ fn two_branches_one_advanced_two_parent_ws_commit_diverged_remote_tracking_branc
stacks: [
Stack {
id: Some(
- 00000000-0000-0000-0000-000000000000,
+ 00000000-0000-0000-0000-000000000001,
),
base: Some(
Sha1(fafd9d08a839d99db60b222cd58e2e0bfaf1f7b2),
),
segments: [
- 👉ref_info::ui::Segment {
- id: NodeIndex(4),
- ref_name: "►lane",
+ ref_info::ui::Segment {
+ id: NodeIndex(3),
+ ref_name: "►advanced-lane",
remote_tracking_ref_name: "None",
- commits: [],
+ commits: [
+ LocalCommit(cbc6713, "change\n", local),
+ ],
commits_on_remote: [],
commits_outside: None,
metadata: Branch,
@@ -2983,19 +2988,17 @@ fn two_branches_one_advanced_two_parent_ws_commit_diverged_remote_tracking_branc
},
Stack {
id: Some(
- 00000000-0000-0000-0000-000000000001,
+ 00000000-0000-0000-0000-000000000000,
),
base: Some(
Sha1(fafd9d08a839d99db60b222cd58e2e0bfaf1f7b2),
),
segments: [
- ref_info::ui::Segment {
- id: NodeIndex(3),
- ref_name: "►advanced-lane",
+ 👉ref_info::ui::Segment {
+ id: NodeIndex(4),
+ ref_name: "►lane",
remote_tracking_ref_name: "None",
- commits: [
- LocalCommit(cbc6713, "change\n", local),
- ],
+ commits: [],
commits_on_remote: [],
commits_outside: None,
metadata: Branch,
@@ -3214,10 +3217,10 @@ fn disjoint() -> anyhow::Result<()> {
fn multiple_branches_with_shared_segment() -> anyhow::Result<()> {
let (repo, mut meta) = read_only_in_memory_scenario("multiple-stacks-with-shared-segment")?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * 820f2b3 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * fe1a116 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 4e5484a (B-on-A) add new file in B-on-A
- * | 5f37dbf (C-on-A) add new file in C-on-A
+ | * 5f37dbf (C-on-A) add new file in C-on-A
+ * | 4e5484a (B-on-A) add new file in B-on-A
|/
| * 89cc2d3 (origin/A) change in A
|/
@@ -3238,7 +3241,7 @@ fn multiple_branches_with_shared_segment() -> anyhow::Result<()> {
"refs/heads/gitbutler/workspace",
),
commit_id: Some(
- Sha1(820f2b3c5007e15ba4558556a81d241fcee06856),
+ Sha1(fe1a116f4ca988026773b0739234a1baaab5649d),
),
worktree: Some(
Worktree {
@@ -3253,23 +3256,21 @@ fn multiple_branches_with_shared_segment() -> anyhow::Result<()> {
},
stacks: [
Stack {
- id: Some(
- 00000000-0000-0000-0000-000000000001,
- ),
+ id: None,
base: Some(
Sha1(c166d42d4ef2e5e742d33554d03805cfb0b24d11),
),
segments: [
ref_info::ui::Segment {
- id: NodeIndex(3),
- ref_name: "►C-on-A",
+ id: NodeIndex(6),
+ ref_name: "►B-on-A",
remote_tracking_ref_name: "None",
commits: [
- LocalCommit(5f37dbf, "add new file in C-on-A\n", local),
+ LocalCommit(4e5484a, "add new file in B-on-A\n", local),
],
commits_on_remote: [],
commits_outside: None,
- metadata: Branch,
+ metadata: "None",
push_status: CompletelyUnpushed,
base: "d79bba9",
},
@@ -3291,21 +3292,23 @@ fn multiple_branches_with_shared_segment() -> anyhow::Result<()> {
],
},
Stack {
- id: None,
+ id: Some(
+ 00000000-0000-0000-0000-000000000001,
+ ),
base: Some(
Sha1(c166d42d4ef2e5e742d33554d03805cfb0b24d11),
),
segments: [
ref_info::ui::Segment {
- id: NodeIndex(6),
- ref_name: "►B-on-A",
+ id: NodeIndex(3),
+ ref_name: "►C-on-A",
remote_tracking_ref_name: "None",
commits: [
- LocalCommit(4e5484a, "add new file in B-on-A\n", local),
+ LocalCommit(5f37dbf, "add new file in C-on-A\n", local),
],
commits_on_remote: [],
commits_outside: None,
- metadata: "None",
+ metadata: Branch,
push_status: CompletelyUnpushed,
base: "d79bba9",
},
@@ -3363,7 +3366,7 @@ fn multiple_branches_with_shared_segment() -> anyhow::Result<()> {
"refs/heads/gitbutler/workspace",
),
commit_id: Some(
- Sha1(820f2b3c5007e15ba4558556a81d241fcee06856),
+ Sha1(fe1a116f4ca988026773b0739234a1baaab5649d),
),
worktree: Some(
Worktree {
@@ -3378,23 +3381,21 @@ fn multiple_branches_with_shared_segment() -> anyhow::Result<()> {
},
stacks: [
Stack {
- id: Some(
- 00000000-0000-0000-0000-000000000001,
- ),
+ id: None,
base: Some(
Sha1(c166d42d4ef2e5e742d33554d03805cfb0b24d11),
),
segments: [
- 👉ref_info::ui::Segment {
- id: NodeIndex(0),
- ref_name: "►C-on-A",
+ ref_info::ui::Segment {
+ id: NodeIndex(6),
+ ref_name: "►B-on-A",
remote_tracking_ref_name: "None",
commits: [
- LocalCommit(5f37dbf, "add new file in C-on-A\n", local),
+ LocalCommit(4e5484a, "add new file in B-on-A\n", local),
],
commits_on_remote: [],
commits_outside: None,
- metadata: Branch,
+ metadata: "None",
push_status: CompletelyUnpushed,
base: "d79bba9",
},
@@ -3416,21 +3417,23 @@ fn multiple_branches_with_shared_segment() -> anyhow::Result<()> {
],
},
Stack {
- id: None,
+ id: Some(
+ 00000000-0000-0000-0000-000000000001,
+ ),
base: Some(
Sha1(c166d42d4ef2e5e742d33554d03805cfb0b24d11),
),
segments: [
- ref_info::ui::Segment {
- id: NodeIndex(6),
- ref_name: "►B-on-A",
+ 👉ref_info::ui::Segment {
+ id: NodeIndex(0),
+ ref_name: "►C-on-A",
remote_tracking_ref_name: "None",
commits: [
- LocalCommit(4e5484a, "add new file in B-on-A\n", local),
+ LocalCommit(5f37dbf, "add new file in C-on-A\n", local),
],
commits_on_remote: [],
commits_outside: None,
- metadata: "None",
+ metadata: Branch,
push_status: CompletelyUnpushed,
base: "d79bba9",
},
@@ -3488,7 +3491,7 @@ fn multiple_branches_with_shared_segment() -> anyhow::Result<()> {
"refs/heads/gitbutler/workspace",
),
commit_id: Some(
- Sha1(820f2b3c5007e15ba4558556a81d241fcee06856),
+ Sha1(fe1a116f4ca988026773b0739234a1baaab5649d),
),
worktree: Some(
Worktree {
@@ -3503,23 +3506,21 @@ fn multiple_branches_with_shared_segment() -> anyhow::Result<()> {
},
stacks: [
Stack {
- id: Some(
- 00000000-0000-0000-0000-000000000001,
- ),
+ id: None,
base: Some(
Sha1(c166d42d4ef2e5e742d33554d03805cfb0b24d11),
),
segments: [
- ref_info::ui::Segment {
- id: NodeIndex(4),
- ref_name: "►C-on-A",
+ 👉ref_info::ui::Segment {
+ id: NodeIndex(0),
+ ref_name: "►B-on-A",
remote_tracking_ref_name: "None",
commits: [
- LocalCommit(5f37dbf, "add new file in C-on-A\n", local),
+ LocalCommit(4e5484a, "add new file in B-on-A\n", local),
],
commits_on_remote: [],
commits_outside: None,
- metadata: Branch,
+ metadata: "None",
push_status: CompletelyUnpushed,
base: "d79bba9",
},
@@ -3541,21 +3542,23 @@ fn multiple_branches_with_shared_segment() -> anyhow::Result<()> {
],
},
Stack {
- id: None,
+ id: Some(
+ 00000000-0000-0000-0000-000000000001,
+ ),
base: Some(
Sha1(c166d42d4ef2e5e742d33554d03805cfb0b24d11),
),
segments: [
- 👉ref_info::ui::Segment {
- id: NodeIndex(0),
- ref_name: "►B-on-A",
+ ref_info::ui::Segment {
+ id: NodeIndex(4),
+ ref_name: "►C-on-A",
remote_tracking_ref_name: "None",
commits: [
- LocalCommit(4e5484a, "add new file in B-on-A\n", local),
+ LocalCommit(5f37dbf, "add new file in C-on-A\n", local),
],
commits_on_remote: [],
commits_outside: None,
- metadata: "None",
+ metadata: Branch,
push_status: CompletelyUnpushed,
base: "d79bba9",
},
@@ -3614,7 +3617,7 @@ fn multiple_branches_with_shared_segment() -> anyhow::Result<()> {
"refs/heads/gitbutler/workspace",
),
commit_id: Some(
- Sha1(820f2b3c5007e15ba4558556a81d241fcee06856),
+ Sha1(fe1a116f4ca988026773b0739234a1baaab5649d),
),
worktree: Some(
Worktree {
@@ -3629,23 +3632,21 @@ fn multiple_branches_with_shared_segment() -> anyhow::Result<()> {
},
stacks: [
Stack {
- id: Some(
- 00000000-0000-0000-0000-000000000001,
- ),
+ id: None,
base: Some(
Sha1(c166d42d4ef2e5e742d33554d03805cfb0b24d11),
),
segments: [
ref_info::ui::Segment {
- id: NodeIndex(4),
- ref_name: "►C-on-A",
+ id: NodeIndex(6),
+ ref_name: "►B-on-A",
remote_tracking_ref_name: "None",
commits: [
- LocalCommit(5f37dbf, "add new file in C-on-A\n", local),
+ LocalCommit(4e5484a, "add new file in B-on-A\n", local),
],
commits_on_remote: [],
commits_outside: None,
- metadata: Branch,
+ metadata: "None",
push_status: CompletelyUnpushed,
base: "d79bba9",
},
@@ -3667,21 +3668,23 @@ fn multiple_branches_with_shared_segment() -> anyhow::Result<()> {
],
},
Stack {
- id: None,
+ id: Some(
+ 00000000-0000-0000-0000-000000000001,
+ ),
base: Some(
Sha1(c166d42d4ef2e5e742d33554d03805cfb0b24d11),
),
segments: [
ref_info::ui::Segment {
- id: NodeIndex(6),
- ref_name: "►B-on-A",
+ id: NodeIndex(4),
+ ref_name: "►C-on-A",
remote_tracking_ref_name: "None",
commits: [
- LocalCommit(4e5484a, "add new file in B-on-A\n", local),
+ LocalCommit(5f37dbf, "add new file in C-on-A\n", local),
],
commits_on_remote: [],
commits_outside: None,
- metadata: "None",
+ metadata: Branch,
push_status: CompletelyUnpushed,
base: "d79bba9",
},
diff --git a/crates/but-workspace/tests/workspace/upstream_integration.rs b/crates/but-workspace/tests/workspace/upstream_integration.rs
index acd1d30c1b5..845d10e07f7 100644
--- a/crates/but-workspace/tests/workspace/upstream_integration.rs
+++ b/crates/but-workspace/tests/workspace/upstream_integration.rs
@@ -703,16 +703,16 @@ fn fully_historically_integrated_branch_leaves_workspace_shape() -> Result<()> {
)?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * 9d7da88 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 0336d9c (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 905d6e5 (origin/main, A) add A1
- * | b38b04b (B) add B1
+ | * b38b04b (B) add B1
+ * | 905d6e5 (origin/main, A) add A1
|/
* 3183e43 (main) M1
");
let mut workspace = graph.into_workspace()?;
- insta::assert_snapshot!(graph_workspace(&workspace), @r"
+ insta::assert_snapshot!(graph_workspace(&workspace), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 3183e43
├── ≡📙:4:A on 3183e43 {1}
│ └── 📙:4:A
@@ -748,7 +748,7 @@ fn fully_historically_integrated_branch_leaves_workspace_shape() -> Result<()> {
└── ·c932222 (🏘️)
");
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @"
- * eaf66d4 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * c0c03ce (HEAD -> gitbutler/workspace) GitButler Workspace Commit
* c932222 (B) add B1
* 905d6e5 (origin/main) add A1
* 3183e43 (main) M1
@@ -1474,11 +1474,11 @@ fn partially_integrated_branch_leaves_multi_branch_stack() -> Result<()> {
)?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * cf53402 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 236546e (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 44c9428 (A) add A1
- | * f1e7451 (origin/main, C) add C1
- * | b38b04b (B) add B1
+ | * b38b04b (B) add B1
+ * | 44c9428 (A) add A1
+ * | f1e7451 (origin/main, C) add C1
|/
* 3183e43 (main) M1
");
@@ -1525,10 +1525,10 @@ fn partially_integrated_branch_leaves_multi_branch_stack() -> Result<()> {
└── ·a27415e (🏘️)
");
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * 780946b (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * e5c6c38 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 44c9428 (A) add A1
- * | a27415e (B) add B1
+ | * a27415e (B) add B1
+ * | 44c9428 (A) add A1
|/
* f1e7451 (origin/main) add C1
* 3183e43 (main) M1
@@ -1562,11 +1562,11 @@ fn fully_integrated_multi_branch_stack_leaves_workspace_shape() -> Result<()> {
)?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * cf53402 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 236546e (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 44c9428 (origin/main, A) add A1
- | * f1e7451 (C) add C1
- * | b38b04b (B) add B1
+ | * b38b04b (B) add B1
+ * | 44c9428 (origin/main, A) add A1
+ * | f1e7451 (C) add C1
|/
* 3183e43 (main) M1
");
@@ -1610,7 +1610,7 @@ fn fully_integrated_multi_branch_stack_leaves_workspace_shape() -> Result<()> {
└── ·f59d71f (🏘️)
");
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @"
- * 55ce8ae (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 64f25fa (HEAD -> gitbutler/workspace) GitButler Workspace Commit
* f59d71f (B) add B1
* 44c9428 (origin/main) add A1
* f1e7451 add C1
@@ -1645,23 +1645,24 @@ fn fully_integrated_two_stacks_leave_workspace_shape() -> Result<()> {
)?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * 9d7da88 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 0336d9c (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
| | * 5f7d45e (origin/main, main) Merging B into base
| | |\
- | |_|/
- |/| |
- * | | b38b04b (B) add B1
+ | | |/
+ | |/|
+ | * | b38b04b (B) add B1
| | * 1f7670a Merging A into base
| |/|
- |/|/
- | * 905d6e5 (A) add A1
+ | |/
+ |/|
+ * | 905d6e5 (A) add A1
|/
* 3183e43 M1
");
let mut workspace = graph.into_workspace()?;
- insta::assert_snapshot!(graph_workspace(&workspace), @r"
+ insta::assert_snapshot!(graph_workspace(&workspace), @"
📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main⇣2 on 3183e43
├── ≡📙:4:A on 3183e43 {1}
│ └── 📙:4:A
@@ -1692,7 +1693,7 @@ fn fully_integrated_two_stacks_leave_workspace_shape() -> Result<()> {
let workspace = graph.into_workspace()?;
insta::assert_snapshot!(graph_workspace(&workspace), @"📕🏘️:0:gitbutler/workspace[🌳] <> ✓refs/remotes/origin/main on 5f7d45e");
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * b44fd24 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * ef6d1ef (HEAD -> gitbutler/workspace) GitButler Workspace Commit
* 5f7d45e (origin/main, main) Merging B into base
|\
| * b38b04b add B1
@@ -2339,11 +2340,11 @@ fn review_hint_integrates_squashed_two_commit_stack_in_managed_workspace() -> Re
)?;
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
- * b96a78e (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * f9bc1ee (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * ad1d22b (A) add A2
- | * fe98e29 add A1
- * | b38b04b (B) add B1
+ | * b38b04b (B) add B1
+ * | ad1d22b (A) add A2
+ * | fe98e29 add A1
|/
| * 56057f2 (origin/main) squash A
|/
@@ -2388,7 +2389,7 @@ fn review_hint_integrates_squashed_two_commit_stack_in_managed_workspace() -> Re
└── ·b38b04b (🏘️)
");
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @"
- * e4abb28 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 4cb8cfe (HEAD -> gitbutler/workspace) GitButler Workspace Commit
* b38b04b (B) add B1
| * 56057f2 (origin/main) squash A
|/
diff --git a/crates/but/src/command/legacy/status/tui/tests/move_tests.rs b/crates/but/src/command/legacy/status/tui/tests/move_tests.rs
index 944f4df6c1e..c685cb29e9b 100644
--- a/crates/but/src/command/legacy/status/tui/tests/move_tests.rs
+++ b/crates/but/src/command/legacy/status/tui/tests/move_tests.rs
@@ -227,7 +227,7 @@ fn move_branch_to_merge_base_tears_off_branch() {
#[test]
fn moving_multiple_commits() {
- let env = Sandbox::init_scenario_with_target_and_default_settings("two-stacks");
+ let env = Sandbox::init_scenario_with_target_and_default_settings("two-stacks-second-double");
env.setup_metadata(&["A", "B"]);
let mut tui = test_tui(env);
diff --git a/crates/but/src/command/legacy/status/tui/tests/snapshots/moving_multiple_commits_001.svg b/crates/but/src/command/legacy/status/tui/tests/snapshots/moving_multiple_commits_001.svg
index feec52d2788..967e0f6e4f7 100644
--- a/crates/but/src/command/legacy/status/tui/tests/snapshots/moving_multiple_commits_001.svg
+++ b/crates/but/src/command/legacy/status/tui/tests/snapshots/moving_multiple_commits_001.svg
@@ -149,13 +149,13 @@
]
┊
●
- 9
- 4
- 7
- 7
+ 4
+ 5
+ f
+ 9
a
- e
- 7
+ 8
+ 5
a
d
d
@@ -174,12 +174,12 @@
┊
●
d
- 3
- e
- 2
- b
- a
- 3
+ 8
+ 5
+ 6
+ 9
+ d
+ 0
a
d
d
@@ -218,40 +218,68 @@
├
╯
┊
- ┴
- 0
- d
- c
- 3
- 7
+ ┊
+ ●
+ 7
+ 6
+ 0
+ 8
3
- 3
- (
- c
- o
- m
- m
- o
- n
- b
- a
- s
- e
- )
- 2
- 0
- 0
- 0
- -
- 0
- 1
- -
- 0
- 2
- a
- d
- d
- M
+ 2
+ 2
+ (
+ u
+ p
+ s
+ t
+ r
+ e
+ a
+ m
+ )
+ ⏫
+ 1
+ c
+ o
+ m
+ m
+ i
+ t
+ ├
+ ╯
+ 7
+ 6
+ 0
+ 8
+ 3
+ 2
+ 2
+ (
+ c
+ o
+ m
+ m
+ o
+ n
+ b
+ a
+ s
+ e
+ )
+ 2
+ 0
+ 0
+ 0
+ -
+ 0
+ 1
+ -
+ 0
+ 2
+ a
+ d
+ d
+ M
━
━
━
diff --git a/crates/but/src/command/legacy/status/tui/tests/snapshots/moving_multiple_commits_002.svg b/crates/but/src/command/legacy/status/tui/tests/snapshots/moving_multiple_commits_002.svg
index 0b6bcea6da5..56f573ecf11 100644
--- a/crates/but/src/command/legacy/status/tui/tests/snapshots/moving_multiple_commits_002.svg
+++ b/crates/but/src/command/legacy/status/tui/tests/snapshots/moving_multiple_commits_002.svg
@@ -151,13 +151,13 @@
]
┊
✔︎
- 9
- 4
- 7
- 7
+ 4
+ 5
+ f
+ 9
a
- e
- 7
+ 8
+ 5
a
d
d
@@ -176,12 +176,12 @@
┊
✔︎
d
- 3
- e
- 2
- b
- a
- 3
+ 8
+ 5
+ 6
+ 9
+ d
+ 0
a
d
d
@@ -220,40 +220,68 @@
├
╯
┊
- ┴
- 0
- d
- c
- 3
- 7
+ ┊
+ ●
+ 7
+ 6
+ 0
+ 8
3
- 3
- (
- c
- o
- m
- m
- o
- n
- b
- a
- s
- e
- )
- 2
- 0
- 0
- 0
- -
- 0
- 1
- -
- 0
- 2
- a
- d
- d
- M
+ 2
+ 2
+ (
+ u
+ p
+ s
+ t
+ r
+ e
+ a
+ m
+ )
+ ⏫
+ 1
+ c
+ o
+ m
+ m
+ i
+ t
+ ├
+ ╯
+ 7
+ 6
+ 0
+ 8
+ 3
+ 2
+ 2
+ (
+ c
+ o
+ m
+ m
+ o
+ n
+ b
+ a
+ s
+ e
+ )
+ 2
+ 0
+ 0
+ 0
+ -
+ 0
+ 1
+ -
+ 0
+ 2
+ a
+ d
+ d
+ M
━
━
━
diff --git a/crates/but/src/command/legacy/status/tui/tests/snapshots/moving_multiple_commits_003.svg b/crates/but/src/command/legacy/status/tui/tests/snapshots/moving_multiple_commits_003.svg
index 161186c7d14..79fb7704194 100644
--- a/crates/but/src/command/legacy/status/tui/tests/snapshots/moving_multiple_commits_003.svg
+++ b/crates/but/src/command/legacy/status/tui/tests/snapshots/moving_multiple_commits_003.svg
@@ -171,13 +171,13 @@
e
>
>
- 9
- 4
- 7
- 7
+ 4
+ 5
+ f
+ 9
a
- e
- 7
+ 8
+ 5
a
d
d
@@ -214,12 +214,12 @@
>
>
d
- 3
- e
- 2
- b
- a
- 3
+ 8
+ 5
+ 6
+ 9
+ d
+ 0
a
d
d
@@ -258,40 +258,68 @@
├
╯
┊
- ┴
- 0
- d
- c
- 3
- 7
+ ┊
+ ●
+ 7
+ 6
+ 0
+ 8
3
- 3
- (
- c
- o
- m
- m
- o
- n
- b
- a
- s
- e
- )
- 2
- 0
- 0
- 0
- -
- 0
- 1
- -
- 0
- 2
- a
- d
- d
- M
+ 2
+ 2
+ (
+ u
+ p
+ s
+ t
+ r
+ e
+ a
+ m
+ )
+ ⏫
+ 1
+ c
+ o
+ m
+ m
+ i
+ t
+ ├
+ ╯
+ 7
+ 6
+ 0
+ 8
+ 3
+ 2
+ 2
+ (
+ c
+ o
+ m
+ m
+ o
+ n
+ b
+ a
+ s
+ e
+ )
+ 2
+ 0
+ 0
+ 0
+ -
+ 0
+ 1
+ -
+ 0
+ 2
+ a
+ d
+ d
+ M
━
━
━
diff --git a/crates/but/src/command/legacy/status/tui/tests/snapshots/moving_multiple_commits_004.svg b/crates/but/src/command/legacy/status/tui/tests/snapshots/moving_multiple_commits_004.svg
index 3eb3d97117c..f42d4325f6e 100644
--- a/crates/but/src/command/legacy/status/tui/tests/snapshots/moving_multiple_commits_004.svg
+++ b/crates/but/src/command/legacy/status/tui/tests/snapshots/moving_multiple_commits_004.svg
@@ -285,13 +285,13 @@
e
>
>
- 9
- 4
- 7
- 7
+ 4
+ 5
+ f
+ 9
a
- e
- 7
+ 8
+ 5
a
d
d
@@ -320,12 +320,12 @@
>
>
d
- 3
- e
- 2
- b
- a
- 3
+ 8
+ 5
+ 6
+ 9
+ d
+ 0
a
d
d
@@ -389,40 +389,69 @@
├
╯
┊
- ┴
- 0
- d
- c
- 3
- 7
+ ┊
+ ●
+ 7
+ 6
+ 0
+ 8
3
- 3
- (
- c
- o
- m
- m
- o
- n
- b
- a
- s
- e
- )
- 2
- 0
- 0
- 0
- -
- 0
- 1
- -
- 0
- 2
- a
- d
- d
- M
+ 2
+ 2
+ (
+ u
+ p
+ s
+ t
+ r
+ e
+ a
+ m
+ )
+ ⏫
+ )
+ 1
+ c
+ o
+ m
+ m
+ i
+ t
+ ├
+ ╯
+ 7
+ 6
+ 0
+ 8
+ 3
+ 2
+ 2
+ (
+ c
+ o
+ m
+ m
+ o
+ n
+ b
+ a
+ s
+ e
+ )
+ 2
+ 0
+ 0
+ 0
+ -
+ 0
+ 1
+ -
+ 0
+ 2
+ a
+ d
+ d
+ M
━
━
━
diff --git a/crates/but/src/command/legacy/status/tui/tests/snapshots/moving_multiple_commits_005.svg b/crates/but/src/command/legacy/status/tui/tests/snapshots/moving_multiple_commits_005.svg
index 152e81596fa..301ba77cf51 100644
--- a/crates/but/src/command/legacy/status/tui/tests/snapshots/moving_multiple_commits_005.svg
+++ b/crates/but/src/command/legacy/status/tui/tests/snapshots/moving_multiple_commits_005.svg
@@ -202,41 +202,41 @@
]
┊
●
- 6
- b
- f
- 8
- 4
- 1
- 2
+ 3
+ d
+ 7
+ e
+ 0
+ 8
+ 7
a
d
d
- B
+ A
┊
●
- 9
- 4
- 7
- 7
- a
- e
- 7
+ a
+ f
+ 6
+ 3
+ 7
+ 8
+ c
a
d
d
- A
+ B
├
╯
┊
┴
- 0
- d
- c
- 3
- 7
- 3
- 3
+ 7
+ 6
+ 0
+ 8
+ 3
+ 2
+ 2
(
c
o
diff --git a/crates/but/tests/but/command/branch/update.rs b/crates/but/tests/but/command/branch/update.rs
index 47096cae256..68445ea0805 100644
--- a/crates/but/tests/but/command/branch/update.rs
+++ b/crates/but/tests/but/command/branch/update.rs
@@ -126,19 +126,51 @@ fn integrate_smart_squash_applies_matching_change_ids() -> anyhow::Result<()> {
let env =
Sandbox::init_scenario_with_target_and_default_settings("branch-integrate-smart-squash");
- insta::assert_snapshot!(env.git_log(), @"
- * 2662ee8 (HEAD -> gitbutler/workspace, A) add only-on-local
+ insta::assert_snapshot!(env.git_log(), @r"
+ * adfadbe (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ |\
+ | * 2662ee8 (A) add only-on-local
+ |/
| * c42227a (origin/A) add only-on-remote
|/
* 0dc3733 (origin/main, origin/HEAD, main) add M
");
- insta::assert_snapshot!(raw_json_status(&env)?, @r"
- status=exit status: 1
+ insta::assert_snapshot!(raw_json_status(&env)?, @r#"
+ status=exit status: 0
stdout:
+ {
+ "uncommittedChanges": [],
+ "stacks": [],
+ "mergeBase": {
+ "cliId": "",
+ "commitId": "0dc37334a458df421bf67ea806103bf5004845dd",
+ "createdAt": "2000-01-01T00:00:00+00:00",
+ "message": "add M\n",
+ "authorName": "author",
+ "authorEmail": "author@example.com",
+ "conflicted": null,
+ "reviewId": null,
+ "changes": null
+ },
+ "upstreamState": {
+ "behind": 0,
+ "latestCommit": {
+ "cliId": "",
+ "commitId": "0dc37334a458df421bf67ea806103bf5004845dd",
+ "createdAt": "2000-01-01T00:00:00+00:00",
+ "message": "add M\n",
+ "authorName": "author",
+ "authorEmail": "author@example.com",
+ "conflicted": null,
+ "reviewId": null,
+ "changes": null
+ },
+ "lastFetched": null
+ }
+ }
stderr:
- Error: GitButler mode exit required: please run `but teardown` to preserve your work.
- ");
+ "#);
env.but("branch update A --strategy smart-squash")
.assert()
@@ -149,19 +181,51 @@ Updated branch A.
"#]]);
- insta::assert_snapshot!(env.git_log(), @"
- * bf02b24 (HEAD -> gitbutler/workspace, A) add only-on-remote
+ insta::assert_snapshot!(env.git_log(), @r"
+ * eeb3d3c (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ |\
+ | * bf02b24 (A) add only-on-remote
+ |/
| * c42227a (origin/A) add only-on-remote
|/
* 0dc3733 (origin/main, origin/HEAD, main, gitbutler/target) add M
");
- insta::assert_snapshot!(raw_json_status(&env)?, @r"
- status=exit status: 1
+ insta::assert_snapshot!(raw_json_status(&env)?, @r#"
+ status=exit status: 0
stdout:
+ {
+ "uncommittedChanges": [],
+ "stacks": [],
+ "mergeBase": {
+ "cliId": "",
+ "commitId": "0dc37334a458df421bf67ea806103bf5004845dd",
+ "createdAt": "2000-01-01T00:00:00+00:00",
+ "message": "add M\n",
+ "authorName": "author",
+ "authorEmail": "author@example.com",
+ "conflicted": null,
+ "reviewId": null,
+ "changes": null
+ },
+ "upstreamState": {
+ "behind": 0,
+ "latestCommit": {
+ "cliId": "",
+ "commitId": "0dc37334a458df421bf67ea806103bf5004845dd",
+ "createdAt": "2000-01-01T00:00:00+00:00",
+ "message": "add M\n",
+ "authorName": "author",
+ "authorEmail": "author@example.com",
+ "conflicted": null,
+ "reviewId": null,
+ "changes": null
+ },
+ "lastFetched": null
+ }
+ }
stderr:
- Error: GitButler mode exit required: please run `but teardown` to preserve your work.
- ");
+ "#);
Ok(())
}
diff --git a/crates/but/tests/but/command/commit.rs b/crates/but/tests/but/command/commit.rs
index 506c2c72ebd..256ce9c47a2 100644
--- a/crates/but/tests/but/command/commit.rs
+++ b/crates/but/tests/but/command/commit.rs
@@ -201,10 +201,10 @@ no need for -a here my friend...
fn commit_with_branch_hint() {
let env = Sandbox::init_scenario_with_target_and_default_settings("two-stacks");
insta::assert_snapshot!(env.git_log(), @r"
- * c128bce (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 8e93f22 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 9477ae7 (A) add A
- * | d3e2ba3 (B) add B
+ | * d3e2ba3 (B) add B
+ * | 9477ae7 (A) add A
|/
* 0dc3733 (origin/main, origin/HEAD, main) add M
");
@@ -231,10 +231,10 @@ fn commit_with_branch_hint() {
fn commit_with_nonexistent_branch_fails() {
let env = Sandbox::init_scenario_with_target_and_default_settings("two-stacks");
insta::assert_snapshot!(env.git_log(), @r"
- * c128bce (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 8e93f22 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 9477ae7 (A) add A
- * | d3e2ba3 (B) add B
+ | * d3e2ba3 (B) add B
+ * | 9477ae7 (A) add A
|/
* 0dc3733 (origin/main, origin/HEAD, main) add M
");
@@ -256,10 +256,10 @@ Error: Branch 'nonexistent' not found
fn commit_with_create_flag_creates_new_branch() {
let env = Sandbox::init_scenario_with_target_and_default_settings("two-stacks");
insta::assert_snapshot!(env.git_log(), @r"
- * c128bce (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 8e93f22 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 9477ae7 (A) add A
- * | d3e2ba3 (B) add B
+ | * d3e2ba3 (B) add B
+ * | 9477ae7 (A) add A
|/
* 0dc3733 (origin/main, origin/HEAD, main) add M
");
diff --git a/crates/but/tests/but/command/move.rs b/crates/but/tests/but/command/move.rs
index 828ea4dfb20..80b96583d85 100644
--- a/crates/but/tests/but/command/move.rs
+++ b/crates/but/tests/but/command/move.rs
@@ -265,21 +265,22 @@ fn move_multiple_commits_from_different_branches() -> anyhow::Result<()> {
╭┄zz [uncommitted] (no changes)
┊
┊╭┄g0 [A]
-┊● add59d2 A: 10 lines on top
+┊● 91048fb A: 10 lines on top
├╯
┊
┊╭┄h0 [B]
-┊● a748762 B: another 10 lines at the bottom
-┊● 62e05ba B: 10 lines at the bottom
+┊● d849bb8 B: another 10 lines at the bottom
+┊● 115df58 B: 10 lines at the bottom
├╯
┊
┊╭┄i0 [C]
-┊● 930563a C: add another 10 lines to new file
-┊● 68a2fc3 C: add 10 lines to new file
-┊● 984fd1c C: new file with 10 lines
+┊● 8d14df7 C: add another 10 lines to new file
+┊● dc232b6 C: add 10 lines to new file
+┊● 34051c8 C: new file with 10 lines
├╯
┊
-┴ 8f0d338 (common base) 2000-01-02 base
+┊● 227db17 (upstream) ⏫ 1 commit
+├╯ 227db17 (common base) 2000-01-02 base
Hint: run `but help` for all commands
@@ -356,10 +357,10 @@ Moved 4 commits → before [..]
b_messages_after,
vec![
"B: another 10 lines at the bottom",
+ "A: 10 lines on top",
"C: add another 10 lines to new file",
"C: add 10 lines to new file",
"C: new file with 10 lines",
- "A: 10 lines on top",
"B: 10 lines at the bottom"
]
);
@@ -375,18 +376,18 @@ Moved 4 commits → before [..]
├╯
┊
┊╭┄h0 [B]
-┊● [..] B: another 10 lines at the bottom
-┊● c4ee0c5 C: add another 10 lines to new file
-┊● 4b3d452 C: add 10 lines to new file
-┊● 90250eb C: new file with 10 lines
-┊● 29bc6a0 A: 10 lines on top
-┊● 62e05ba B: 10 lines at the bottom
+┊● 367af83 B: another 10 lines at the bottom
+┊● b945cdf A: 10 lines on top
+┊● f70b01c C: add another 10 lines to new file
+┊● 7e19d06 C: add 10 lines to new file
+┊● 263e368 C: new file with 10 lines
+┊● 115df58 B: 10 lines at the bottom
├╯
┊
┊╭┄i0 [C] (no commits)
├╯
┊
-┴ 8f0d338 (common base) 2000-01-02 base
+┴ 227db17 (common base) 2000-01-02 base
Hint: run `but help` for all commands
@@ -409,21 +410,22 @@ fn move_multiple_commits_from_different_branches_after() -> anyhow::Result<()> {
╭┄zz [uncommitted] (no changes)
┊
┊╭┄g0 [A]
-┊● add59d2 A: 10 lines on top
+┊● 91048fb A: 10 lines on top
├╯
┊
┊╭┄h0 [B]
-┊● a748762 B: another 10 lines at the bottom
-┊● 62e05ba B: 10 lines at the bottom
+┊● d849bb8 B: another 10 lines at the bottom
+┊● 115df58 B: 10 lines at the bottom
├╯
┊
┊╭┄i0 [C]
-┊● 930563a C: add another 10 lines to new file
-┊● 68a2fc3 C: add 10 lines to new file
-┊● 984fd1c C: new file with 10 lines
+┊● 8d14df7 C: add another 10 lines to new file
+┊● dc232b6 C: add 10 lines to new file
+┊● 34051c8 C: new file with 10 lines
├╯
┊
-┴ 8f0d338 (common base) 2000-01-02 base
+┊● 227db17 (upstream) ⏫ 1 commit
+├╯ 227db17 (common base) 2000-01-02 base
Hint: run `but help` for all commands
@@ -470,7 +472,7 @@ Hint: run `but help` for all commands
.assert()
.success()
.stdout_eq(str![[r#"
-Moved 4 commits → after a748762
+Moved 4 commits → after d849bb8
"#]]);
@@ -499,10 +501,10 @@ Moved 4 commits → after a748762
assert_eq!(
b_messages_after,
vec![
+ "A: 10 lines on top",
"C: add another 10 lines to new file",
"C: add 10 lines to new file",
"C: new file with 10 lines",
- "A: 10 lines on top",
"B: another 10 lines at the bottom",
"B: 10 lines at the bottom"
]
@@ -519,18 +521,18 @@ Moved 4 commits → after a748762
├╯
┊
┊╭┄h0 [B]
-┊● 79860aa C: add another 10 lines to new file
-┊● cb938a1 C: add 10 lines to new file
-┊● 87f25cc C: new file with 10 lines
-┊● d2689e7 A: 10 lines on top
-┊● a748762 B: another 10 lines at the bottom
-┊● 62e05ba B: 10 lines at the bottom
+┊● 5208b29 A: 10 lines on top
+┊● 801cc82 C: add another 10 lines to new file
+┊● 8539b48 C: add 10 lines to new file
+┊● 802ea91 C: new file with 10 lines
+┊● d849bb8 B: another 10 lines at the bottom
+┊● 115df58 B: 10 lines at the bottom
├╯
┊
┊╭┄i0 [C] (no commits)
├╯
┊
-┴ 8f0d338 (common base) 2000-01-02 base
+┴ 227db17 (common base) 2000-01-02 base
Hint: run `but help` for all commands
diff --git a/crates/but/tests/but/command/snapshots/status/classification/status-shows-no-commits-label.stdout.term.svg b/crates/but/tests/but/command/snapshots/status/classification/status-shows-no-commits-label.stdout.term.svg
index f5fd9d3a25c..14c10b33aeb 100644
--- a/crates/but/tests/but/command/snapshots/status/classification/status-shows-no-commits-label.stdout.term.svg
+++ b/crates/but/tests/but/command/snapshots/status/classification/status-shows-no-commits-label.stdout.term.svg
@@ -25,15 +25,15 @@
┊
- ┊╭┄ g0 [ A ]
+ ┊╭┄ g0 [ B ] (no commits)
- ┊● 94 77ae7 add A
+ ├╯
- ├╯
+ ┊
- ┊
+ ┊╭┄ h0 [ A ]
- ┊╭┄ h0 [ B ] (no commits)
+ ┊● 94 77ae7 add A
├╯
diff --git a/crates/but/tests/but/command/snapshots/status/upstream/status-upstream-merge-status-empty.stdout.term.svg b/crates/but/tests/but/command/snapshots/status/upstream/status-upstream-merge-status-empty.stdout.term.svg
index f5fd9d3a25c..14c10b33aeb 100644
--- a/crates/but/tests/but/command/snapshots/status/upstream/status-upstream-merge-status-empty.stdout.term.svg
+++ b/crates/but/tests/but/command/snapshots/status/upstream/status-upstream-merge-status-empty.stdout.term.svg
@@ -25,15 +25,15 @@
┊
- ┊╭┄ g0 [ A ]
+ ┊╭┄ g0 [ B ] (no commits)
- ┊● 94 77ae7 add A
+ ├╯
- ├╯
+ ┊
- ┊
+ ┊╭┄ h0 [ A ]
- ┊╭┄ h0 [ B ] (no commits)
+ ┊● 94 77ae7 add A
├╯
diff --git a/crates/but/tests/but/command/squash.rs b/crates/but/tests/but/command/squash.rs
index e1141707785..cf96a5de642 100644
--- a/crates/but/tests/but/command/squash.rs
+++ b/crates/but/tests/but/command/squash.rs
@@ -623,17 +623,18 @@ fn squash_branch_c_in_three_stacks_keeps_content_and_updates_graph() -> anyhow::
let normalized_log = env.git_log().replace(" \n", "\n");
insta::assert_snapshot!(normalized_log, @r"
- *-. 205e798 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ *-. f8d2152 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\ \
- | | * a748762 (B) B: another 10 lines at the bottom
- | | * 62e05ba B: 10 lines at the bottom
- | * | add59d2 (A) A: 10 lines on top
- | |/
- * | 930563a (C) C: add another 10 lines to new file
- * | 68a2fc3 C: add 10 lines to new file
- * | 984fd1c C: new file with 10 lines
+ | | * 8d14df7 (C) C: add another 10 lines to new file
+ | | * dc232b6 C: add 10 lines to new file
+ | | * 34051c8 C: new file with 10 lines
+ | * | d849bb8 (B) B: another 10 lines at the bottom
+ | * | 115df58 B: 10 lines at the bottom
+ * | | 91048fb (A) A: 10 lines on top
+ |/ /
+ * / 227db17 (tag: base, origin/main, origin/HEAD, main) base
|/
- * 8f0d338 (tag: base, origin/main, origin/HEAD, main) base
+ * 540d27c lower base
");
let working_directory_before = util::working_directory_snapshot(&env)?;
diff --git a/crates/but/tests/but/command/teardown.rs b/crates/but/tests/but/command/teardown.rs
index 9981f0ca054..8324bc6a045 100644
--- a/crates/but/tests/but/command/teardown.rs
+++ b/crates/but/tests/but/command/teardown.rs
@@ -55,10 +55,10 @@ To return to GitButler mode, run:
fn multiple_branches_preserves_state() {
let env = Sandbox::init_scenario_with_target_and_default_settings("two-stacks");
insta::assert_snapshot!(env.git_log(), @r"
- * c128bce (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 8e93f22 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 9477ae7 (A) add A
- * | d3e2ba3 (B) add B
+ | * d3e2ba3 (B) add B
+ * | 9477ae7 (A) add A
|/
* 0dc3733 (origin/main, origin/HEAD, main) add M
");
@@ -270,12 +270,12 @@ fn two_dangling_commits_different_branches() -> anyhow::Result<()> {
Sandbox::init_scenario_with_target_and_default_settings("teardown-two-dangling-commits");
// Initial state: user has made two commits on top of workspace
insta::assert_snapshot!(env.git_log(), @r"
- * fc13bfb (HEAD -> gitbutler/workspace) add FileForB
- * 091c8f9 add FileForA
- * c128bce GitButler Workspace Commit
+ * 56624bb (HEAD -> gitbutler/workspace) add FileForB
+ * e021d42 add FileForA
+ * 8e93f22 GitButler Workspace Commit
|\
- | * 9477ae7 (A) add A
- * | d3e2ba3 (B) add B
+ | * d3e2ba3 (B) add B
+ * | 9477ae7 (A) add A
|/
* 0dc3733 (origin/main, origin/HEAD, main) add M
");
@@ -299,8 +299,8 @@ Exiting GitButler mode...
Attempting to fix workspace stacks...
→ Checking for dangling commits...
-→ Resetting gitbutler/workspace to c128bce
- ✓ gitbutler/workspace reset to c128bce
+→ Resetting gitbutler/workspace to 8e93f22
+ ✓ gitbutler/workspace reset to 8e93f22
⚠ Non-GitButler created commits found.
⚠ Undoing these commits but keeping the changes in your working directory.
diff --git a/crates/but/tests/but/command/worktree.rs b/crates/but/tests/but/command/worktree.rs
index e7f3be6ad70..375f077be0f 100644
--- a/crates/but/tests/but/command/worktree.rs
+++ b/crates/but/tests/but/command/worktree.rs
@@ -8,10 +8,10 @@ use crate::utils::Sandbox;
fn journey_new_list_integrate() -> anyhow::Result<()> {
let env = Sandbox::init_scenario_with_target_and_default_settings("two-stacks");
insta::assert_snapshot!(env.git_log(), @r"
- * c128bce (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 8e93f22 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 9477ae7 (A) add A
- * | d3e2ba3 (B) add B
+ | * d3e2ba3 (B) add B
+ * | 9477ae7 (A) add A
|/
* 0dc3733 (origin/main, origin/HEAD, main) add M
");
diff --git a/crates/but/tests/but/journey.rs b/crates/but/tests/but/journey.rs
index 3368bb42a2a..3f03c8bdb4e 100644
--- a/crates/but/tests/but/journey.rs
+++ b/crates/but/tests/but/journey.rs
@@ -162,10 +162,10 @@ fn from_workspace() {
use crate::utils::CommandExt;
let env = Sandbox::init_scenario_with_target_and_default_settings("two-stacks");
insta::assert_snapshot!(env.git_log(), @r"
- * c128bce (HEAD -> gitbutler/workspace) GitButler Workspace Commit
+ * 8e93f22 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
|\
- | * 9477ae7 (A) add A
- * | d3e2ba3 (B) add B
+ | * d3e2ba3 (B) add B
+ * | 9477ae7 (A) add A
|/
* 0dc3733 (origin/main, origin/HEAD, main) add M
");
diff --git a/crates/but/tests/fixtures/scenario/shared.sh b/crates/but/tests/fixtures/scenario/shared.sh
index 3fed215a547..1588f6659f5 100644
--- a/crates/but/tests/fixtures/scenario/shared.sh
+++ b/crates/but/tests/fixtures/scenario/shared.sh
@@ -74,11 +74,17 @@ function create_workspace_commit_once() {
fi
fi
- git checkout -b gitbutler/workspace
- if [ $# == 1 ] || [ $# == 0 ]; then
- git commit --allow-empty -m "$workspace_commit_subject"
+ if [ $# -gt 1 ]; then
+ # First arg becomes the workspace commit's first parent; the rest merge in order.
+ # NB: `git merge` reports "Already up to date" and makes no commit when an argument is
+ # an ancestor of HEAD, so an empty branch (sitting on the base) must be the FIRST arg —
+ # checked out here — or it is silently dropped from the workspace commit's parents.
+ git checkout "$1"
+ git checkout -b gitbutler/workspace
+ git merge --no-ff -m "$workspace_commit_subject" "${@:2}"
else
- git merge --no-ff -m "$workspace_commit_subject" "${@}"
+ git checkout -b gitbutler/workspace
+ git commit --allow-empty -m "$workspace_commit_subject"
fi
}
diff --git a/crates/but/tests/fixtures/scenario/three-stacks.sh b/crates/but/tests/fixtures/scenario/three-stacks.sh
index 4b5799bbb09..05f52bbdc32 100644
--- a/crates/but/tests/fixtures/scenario/three-stacks.sh
+++ b/crates/but/tests/fixtures/scenario/three-stacks.sh
@@ -10,13 +10,16 @@ source "${BASH_SOURCE[0]%/*}/shared.sh"
# Stack A prepends lines to `file`.
# Stack B appends lines to `file` in two commits.
# Stack C creates `new-file` and extends it in two more commits.
+# A and B branch from the target `base`; C branches one commit lower, so that
+# emptying A and C (e.g. by moving their commits into B) leaves them on DISTINCT
+# workspace-commit parents instead of collapsing onto a single shared base.
git-init-frozen
+echo seed >seed && git add . && git commit -m "lower base"
seq 50 60 >file && git add . && git commit -m "base" && git tag base
setup_target_to_match_main
git branch B
-git branch C
git checkout -b A
{ seq 10; seq 50 60; } >file && git add . && git commit -m "A: 10 lines on top"
@@ -25,9 +28,9 @@ git checkout B
{ seq 50 60; seq 61 70; } >file && git add . && git commit -m "B: 10 lines at the bottom"
{ seq 50 60; seq 61 80; } >file && git add . && git commit -m "B: another 10 lines at the bottom"
-git checkout C
+git checkout -b C base~1
seq 10 >new-file && git add . && git commit -m "C: new file with 10 lines"
seq 20 >new-file && git add . && git commit -m "C: add 10 lines to new file"
seq 30 >new-file && git add . && git commit -m "C: add another 10 lines to new file"
-create_workspace_commit_once A B C
\ No newline at end of file
+create_workspace_commit_once A B C
diff --git a/crates/but/tests/fixtures/scenario/two-stacks-one-empty.sh b/crates/but/tests/fixtures/scenario/two-stacks-one-empty.sh
index 18afda4b727..7eaa1851b0e 100644
--- a/crates/but/tests/fixtures/scenario/two-stacks-one-empty.sh
+++ b/crates/but/tests/fixtures/scenario/two-stacks-one-empty.sh
@@ -16,4 +16,6 @@ git branch B
git checkout -b A
commit-file A
git checkout B
-create_workspace_commit_once A B
+# Empty branch B first so the deterministic helper keeps it as a real ws-commit parent;
+# checking out the non-empty branch first would merge the base as a no-op and drop it.
+create_workspace_commit_once B A
diff --git a/crates/but/tests/fixtures/scenario/two-stacks-second-double.sh b/crates/but/tests/fixtures/scenario/two-stacks-second-double.sh
new file mode 100644
index 00000000000..776a43750e9
--- /dev/null
+++ b/crates/but/tests/fixtures/scenario/two-stacks-second-double.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+
+set -eu -o pipefail
+
+source "${BASH_SOURCE[0]%/*}/shared.sh"
+
+### General Description
+
+# Two single-commit stacks anchored on DIFFERENT bases: A on the merge base M, B one
+# commit back on M's parent Z. The moving_multiple test moves both commits out, emptying
+# both stacks onto distinct bases — so they map to distinct workspace-commit parents and
+# exercise parent-index stack ordering (not the same-base fallback).
+git-init-frozen
+commit-file Z
+commit-file M
+setup_target_to_match_main
+
+git checkout -b A
+ commit-file A
+git checkout -b B main~1
+ commit-file B
+create_workspace_commit_once A B
diff --git a/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs b/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs
index 94869ef282b..8b8e4402191 100644
--- a/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs
+++ b/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs
@@ -1,3 +1,5 @@
+use std::collections::HashSet;
+
use anyhow::{Context as _, Result};
use but_core::{RefMetadata, ref_metadata::StackId};
use but_ctx::access::RepoExclusive;
@@ -54,6 +56,33 @@ impl BranchManager<'_> {
.list_stacks_in_workspace()
.context("failed to read virtual branches")?;
+ // Empties must sit on distinct commits so they map to distinct workspace-commit parents —
+ // sharing one collapses them into a single lane. Collect the commits already taken as
+ // workspace-commit parents (each stack's head). If the target base is already one of them,
+ // walk back through its ancestry to the first commit still free. The new branch then sits
+ // on an older base; we only log a warning about it (not yet surfaced to the client).
+ let repo = self.ctx.repo.get()?;
+ // Best-effort: a stack whose head can't be resolved just doesn't reserve a commit here.
+ let used_parent_commits: HashSet = all_stacks
+ .iter()
+ .filter_map(|s| s.head_oid(self.ctx).ok())
+ .collect();
+ let mut base_oid = target_base_oid;
+ while used_parent_commits.contains(&base_oid) {
+ base_oid = repo
+ .find_commit(base_oid)?
+ .parent_ids()
+ .next()
+ .context("Not enough commits to create a new branch")?
+ .detach();
+ }
+ if base_oid != target_base_oid {
+ tracing::warn!(
+ %base_oid,
+ "target base commit is already a workspace-commit parent; placing the new branch on an older base"
+ );
+ }
+
let stack_names: Vec = all_stacks.iter().map(|b| b.name()).collect();
let stack_name_refs: Vec<&str> = stack_names.iter().map(|s| s.as_str()).collect();
let name = dedup(
@@ -77,7 +106,7 @@ impl BranchManager<'_> {
}
}
- let branch = Stack::new_empty(self.ctx, name, target_base_oid, order)?;
+ let branch = Stack::new_empty(self.ctx, name, base_oid, order)?;
vb_state.set_stack(branch.clone())?;
self.ctx.add_branch_reference(&branch)?;