Skip to content

Commit c374b37

Browse files
committed
Fix the classification for independent (virtual) branches in the workspace
1 parent 1de5cd3 commit c374b37

File tree

7 files changed

+212
-6
lines changed

7 files changed

+212
-6
lines changed

crates/but-graph/src/debug.rs

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

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use gix::reference::Category;
1212
use petgraph::Direction;
1313
use petgraph::graph::NodeIndex;
1414
use petgraph::prelude::EdgeRef;
15+
use petgraph::visit::NodeRef;
1516
use std::collections::{BTreeMap, BTreeSet};
1617
use tracing::instrument;
1718

@@ -439,6 +440,27 @@ impl Graph {
439440
.map(|cidx| (sidx, &base_segment.commits[cidx]))
440441
})
441442
})
443+
.chain(
444+
self.inner
445+
.neighbors_directed(ws_sidx, Direction::Outgoing)
446+
.filter_map(|s| {
447+
// This rule means that if there is no target, we'd want to put new independent stacks
448+
// onto segments which then are ambiguous so they get pulled out.
449+
if target.is_some() {
450+
return None;
451+
}
452+
// It's a very specialised filter… will that lead to strange behaviour later?
453+
let segment = &self[s];
454+
if segment.ref_name.is_some() {
455+
return None;
456+
}
457+
segment
458+
.commits
459+
.first()
460+
.filter(|c| !c.refs.is_empty())
461+
.map(|c| (s.id(), c))
462+
}),
463+
)
442464
.collect();
443465
out.sort_by_key(|t| t.1.id);
444466
out.dedup_by_key(|t| t.1.id);
@@ -913,6 +935,37 @@ impl Graph {
913935
}
914936
for (remote_sidx, local_sidx) in links_from_remote_to_local {
915937
self[remote_sidx].sibling_segment_id = Some(local_sidx);
938+
939+
// If both remote and local point to the same commit, make sure that the remote points to the local segment.
940+
if let Some((
941+
(_remote_commit, _owner_of_commit_same_as_local),
942+
(_local_commmit, owner_of_commit_same_as_remote),
943+
)) = self
944+
.first_commit_or_find_along_first_parent(remote_sidx)
945+
.zip(self.first_commit_or_find_along_first_parent(local_sidx))
946+
.filter(|((a, a_sidx), (b, b_sidx))| a.id == b.id && a_sidx == b_sidx)
947+
{
948+
let outgoing: Vec<_> = self
949+
.edges_directed_in_order_of_creation(remote_sidx, Direction::Outgoing)
950+
.into_iter()
951+
.map(EdgeOwned::from)
952+
.collect();
953+
let remote_is_connected_to_local =
954+
outgoing.iter().any(|e| e.target.id() == local_sidx);
955+
if !remote_is_connected_to_local
956+
&& let Some(edge) = outgoing.iter().find(|e| {
957+
outgoing.len() == 1 || e.target.id() == owner_of_commit_same_as_remote
958+
})
959+
{
960+
self.inner.remove_edge(edge.id);
961+
self.inner.add_edge(
962+
remote_sidx,
963+
local_sidx,
964+
edge.weight
965+
.adjusted_for(remote_sidx, local_sidx, &self.inner),
966+
);
967+
}
968+
}
916969
}
917970
Ok(())
918971
}

crates/but-graph/src/lib.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,5 +280,32 @@ pub struct Edge {
280280
dst_id: Option<gix::ObjectId>,
281281
}
282282

283+
impl Edge {
284+
/// Useful when reusing an edge to assure it doesn't list commits that don't exist in `src_idx` and `dst_idx` anymore.
285+
pub(crate) fn adjusted_for(
286+
mut self,
287+
src_sidx: SegmentIndex,
288+
dst_sidx: SegmentIndex,
289+
graph: &init::PetGraph,
290+
) -> Self {
291+
let commits = &graph[src_sidx].commits;
292+
let (id, idx) = commits
293+
.last()
294+
.map(|c| (Some(c.id), Some(commits.len() - 1)))
295+
.unwrap_or_default();
296+
self.src_id = id;
297+
self.src = idx;
298+
299+
let commits = &graph[dst_sidx].commits;
300+
let (id, idx) = commits
301+
.first()
302+
.map(|c| (Some(c.id), Some(0)))
303+
.unwrap_or_default();
304+
self.dst_id = id;
305+
self.dst = idx;
306+
307+
self
308+
}
309+
}
283310
/// An index into the [`Graph`].
284311
pub type SegmentIndex = petgraph::graph::NodeIndex;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -789,7 +789,7 @@ impl Graph {
789789
out
790790
}
791791

792-
/// Return `(commit, start)` if `start` has a commit, or find the first commit downstream along the first parent.
792+
/// Return `(commit, owner_sidx_of_commit)` if `start` has a commit, or find the first commit downstream along the first parent.
793793
pub(crate) fn first_commit_or_find_along_first_parent(
794794
&self,
795795
start: SegmentIndex,

crates/but-graph/tests/fixtures/scenarios.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,5 +1151,17 @@ EOF
11511151
commit A3
11521152
create_workspace_commit_once A
11531153
)
1154+
1155+
1156+
git init no-ws-ref-no-ws-commit-two-branches
1157+
(cd no-ws-ref-no-ws-commit-two-branches
1158+
commit M1
1159+
commit M2
1160+
1161+
git branch A
1162+
git branch B
1163+
1164+
create_workspace_commit_once A B
1165+
)
11541166
)
11551167

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use but_core::RefMetadata;
12
use but_core::ref_metadata::StackId;
23
use but_graph::VirtualBranchesTomlMetadata;
34
use but_graph::virtual_branches_legacy_types::{Stack, StackBranch, Target};
@@ -53,6 +54,18 @@ pub fn add_workspace(meta: &mut VirtualBranchesTomlMetadata) {
5354
);
5455
}
5556

57+
pub fn remove_target(meta: &mut VirtualBranchesTomlMetadata) {
58+
let mut ws_md = meta
59+
.workspace(
60+
"refs/heads/gitbutler/workspace"
61+
.try_into()
62+
.expect("statically known to be valid"),
63+
)
64+
.unwrap();
65+
ws_md.target_ref = None;
66+
meta.set_workspace(&ws_md).unwrap();
67+
}
68+
5669
pub fn add_workspace_without_target(meta: &mut VirtualBranchesTomlMetadata) {
5770
add_workspace(meta);
5871
meta.data_mut().default_target = None;

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

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use crate::init::utils::{add_workspace_without_target, standard_options_with_extra_target};
1+
use crate::init::utils::{
2+
add_workspace_without_target, remove_target, standard_options_with_extra_target,
3+
};
24
use crate::init::{StackState, add_stack_with_segments, add_workspace, id_at, id_by_rev};
35
use crate::init::{read_only_in_memory_scenario, standard_options};
46
use but_graph::Graph;
@@ -2423,7 +2425,7 @@ fn three_branches_one_advanced_ws_commit_advanced_fully_pushed_empty_dependant()
24232425
├── ►:1[0]:origin/main →:2:
24242426
│ └── →:2: (main →:1:)
24252427
└── ►:4[0]:origin/advanced-lane →:6:
2426-
└── →:5: (dependant)
2428+
└── →:6: (advanced-lane →:4:)
24272429
");
24282430

24292431
// When putting the dependent branch on top as empty segment, the frozen state is retained.
@@ -3241,7 +3243,7 @@ fn dependent_branch_insertion() -> anyhow::Result<()> {
32413243
├── ►:1[0]:origin/main →:2:
32423244
│ └── →:2: (main →:1:)
32433245
└── ►:4[0]:origin/advanced-lane →:6:
3244-
└── →:5: (dependant)
3246+
└── →:6: (advanced-lane →:4:)
32453247
");
32463248

32473249
// The dependant branch is empty and on top of the one with the remote
@@ -3960,7 +3962,7 @@ fn without_target_ref_or_managed_commit_ambiguous_with_remotes() -> anyhow::Resu
39603962
├── ►:2[0]:origin/A →:5:
39613963
│ └── →:5: (A →:2:)
39623964
└── ►:3[0]:origin/B →:0:
3963-
└── →:5: (A →:2:)
3965+
└── →:0: (B →:3:)
39643966
");
39653967
insta::assert_snapshot!(graph_workspace(&graph.to_workspace()?), @r"
39663968
📕🏘️⚠️:1:gitbutler/workspace <> ✓!
@@ -5144,6 +5146,105 @@ fn unapplied_branch_on_base() -> anyhow::Result<()> {
51445146
Ok(())
51455147
}
51465148

5149+
#[test]
5150+
fn unapplied_branch_on_base_no_target() -> anyhow::Result<()> {
5151+
let (repo, mut meta) = read_only_in_memory_scenario("ws/unapplied-branch-on-base")?;
5152+
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
5153+
* a26ae77 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
5154+
* fafd9d0 (origin/main, unapplied, main) init
5155+
");
5156+
add_workspace(&mut meta);
5157+
remove_target(&mut meta);
5158+
5159+
let graph = Graph::from_head(&repo, &*meta, standard_options())?.validated()?;
5160+
insta::assert_snapshot!(graph_tree(&graph), @r"
5161+
├── 👉📕►►►:0[0]:gitbutler/workspace
5162+
│ └── ·a26ae77 (⌂|🏘|1)
5163+
│ └── ►:2[1]:main <> origin/main →:1:
5164+
│ └── ·fafd9d0 (⌂|🏘|11) ►unapplied
5165+
└── ►:1[0]:origin/main →:2:
5166+
└── →:2: (main →:1:)
5167+
");
5168+
5169+
// the main branch is disambiguated by its remote reference.
5170+
insta::assert_snapshot!(graph_workspace(&graph.to_workspace()?), @r"
5171+
📕🏘️:0:gitbutler/workspace <> ✓!
5172+
└── ≡:2:main <> origin/main →:1:
5173+
└── :2:main <> origin/main →:1:
5174+
└── ❄️fafd9d0 (🏘️) ►unapplied
5175+
");
5176+
5177+
// The 'unapplied' branch can be added on top of that, and we make clear we want `main` as well.
5178+
add_stack_with_segments(&mut meta, 1, "unapplied", StackState::InWorkspace, &[]);
5179+
add_stack_with_segments(&mut meta, 2, "main", StackState::InWorkspace, &[]);
5180+
5181+
let graph = Graph::from_head(&repo, &*meta, standard_options())?.validated()?;
5182+
insta::assert_snapshot!(graph_tree(&graph), @r"
5183+
├── 👉📕►►►:0[0]:gitbutler/workspace
5184+
│ └── ·a26ae77 (⌂|🏘|1)
5185+
│ ├── 📙►:3[1]:unapplied
5186+
│ │ └── ►:2[2]:anon:
5187+
│ │ └── ·fafd9d0 (⌂|🏘|✓|11)
5188+
│ └── 📙►:4[1]:main <> origin/main →:1:
5189+
│ └── →:2:
5190+
└── ►:1[0]:origin/main →:4:
5191+
└── →:4: (main →:1:)
5192+
");
5193+
5194+
insta::assert_snapshot!(graph_workspace(&graph.to_workspace()?), @r"
5195+
📕🏘️:0:gitbutler/workspace <> ✓refs/remotes/origin/main on fafd9d0
5196+
├── ≡📙:4:main <> origin/main →:1: on fafd9d0
5197+
│ └── 📙:4:main <> origin/main →:1:
5198+
└── ≡📙:3:unapplied on fafd9d0
5199+
└── 📙:3:unapplied
5200+
");
5201+
5202+
// We simulate an unapplied branch on the base by giving it branch metadata, but not listing
5203+
// it in the workspace.
5204+
add_stack_with_segments(&mut meta, 1, "unapplied", StackState::Inactive, &[]);
5205+
let graph = Graph::from_head(&repo, &*meta, standard_options())?.validated()?;
5206+
5207+
// Now only `main` shows up.
5208+
insta::assert_snapshot!(graph_workspace(&graph.to_workspace()?), @r"
5209+
📕🏘️:0:gitbutler/workspace <> ✓refs/remotes/origin/main on fafd9d0
5210+
└── ≡📙:3:main <> origin/main →:1: on fafd9d0
5211+
└── 📙:3:main <> origin/main →:1:
5212+
");
5213+
5214+
Ok(())
5215+
}
5216+
5217+
#[test]
5218+
fn no_ws_commit_two_branches_no_target() -> anyhow::Result<()> {
5219+
let (repo, mut meta) = read_only_in_memory_scenario("ws/no-ws-ref-no-ws-commit-two-branches")?;
5220+
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
5221+
* bce0c5e (HEAD -> gitbutler/workspace, main, B, A) M2
5222+
* 3183e43 M1
5223+
");
5224+
remove_target(&mut meta);
5225+
add_stack_with_segments(&mut meta, 0, "main", StackState::InWorkspace, &[]);
5226+
add_stack_with_segments(&mut meta, 1, "A", StackState::InWorkspace, &[]);
5227+
5228+
let graph = Graph::from_head(&repo, &*meta, standard_options())?.validated()?;
5229+
insta::assert_snapshot!(graph_tree(&graph), @r"
5230+
└── 👉📕►►►:0[0]:gitbutler/workspace
5231+
├── 📙►:2[1]:main
5232+
│ └── ►:1[2]:anon: →:3:
5233+
│ ├── ·bce0c5e (⌂|🏘|1) ►B
5234+
│ └── ·3183e43 (⌂|🏘|1)
5235+
└── 📙►:3[1]:A
5236+
└── →:1:
5237+
");
5238+
insta::assert_snapshot!(graph_workspace(&graph.to_workspace()?), @r"
5239+
📕🏘️⚠️:0:gitbutler/workspace <> ✓! on bce0c5e
5240+
├── ≡📙:3:A on bce0c5e
5241+
│ └── 📙:3:A
5242+
└── ≡📙:2:main on bce0c5e
5243+
└── 📙:2:main
5244+
");
5245+
Ok(())
5246+
}
5247+
51475248
mod edit_commit {
51485249
use crate::init::{add_workspace, id_at, read_only_in_memory_scenario, standard_options};
51495250
use but_graph::Graph;

0 commit comments

Comments
 (0)