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) - ┊● 9477ae7 add A + ├╯ - ├╯ + - + ┊╭┄h0 [A] - ┊╭┄h0 [B] (no commits) + ┊● 9477ae7 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) - ┊● 9477ae7 add A + ├╯ - ├╯ + - + ┊╭┄h0 [A] - ┊╭┄h0 [B] (no commits) + ┊● 9477ae7 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)?;