Skip to content

Commit 6f99b27

Browse files
committed
Incorporate worktree information into graph and workspace.
1 parent 3467de7 commit 6f99b27

File tree

7 files changed

+208
-13
lines changed

7 files changed

+208
-13
lines changed

crates/but-graph/src/api.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ impl Graph {
139139
s.commits.iter().find_map(|c| {
140140
c.refs
141141
.iter()
142-
.any(|rn| rn.as_ref() == name)
142+
.any(|ri| ri.ref_name.as_ref() == name)
143143
.then_some((s, c))
144144
})
145145
}

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

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
//! Utilities for graph-walking specifically.
2-
use std::{collections::BTreeSet, ops::Deref};
3-
4-
use anyhow::{Context, bail};
5-
use but_core::{RefMetadata, ref_metadata};
6-
use gix::{hashtable::hash_map::Entry, reference::Category, traverse::commit::Either};
7-
use petgraph::Direction;
8-
92
use crate::{
103
Commit, CommitFlags, CommitIndex, Edge, Graph, Segment, SegmentIndex, SegmentMetadata,
4+
Worktree,
115
init::{
126
Goals, PetGraph,
137
overlay::{OverlayMetadata, OverlayRepo},
@@ -16,6 +10,14 @@ use crate::{
1610
},
1711
is_workspace_ref_name,
1812
};
13+
use anyhow::{Context, bail};
14+
use bstr::{BStr, BString};
15+
use but_core::{RefMetadata, ref_metadata};
16+
use gix::{hashtable::hash_map::Entry, reference::Category, traverse::commit::Either};
17+
use petgraph::Direction;
18+
use std::collections::BTreeMap;
19+
use std::path::PathBuf;
20+
use std::{collections::BTreeSet, ops::Deref};
1921

2022
pub(crate) type RefsById = gix::hashtable::HashMap<gix::ObjectId, Vec<gix::refs::FullName>>;
2123

@@ -941,3 +943,49 @@ pub fn prune_integrated_tips(graph: &mut Graph, next: &mut Queue) -> anyhow::Res
941943
});
942944
Ok(())
943945
}
946+
947+
type WorktreeByBranch = BTreeMap<gix::refs::FullName, Vec<Worktree>>;
948+
949+
pub fn worktree_branches(repo: &gix::Repository) -> anyhow::Result<WorktreeByBranch> {
950+
fn maybe_insert_head(
951+
head: Option<gix::Head<'_>>,
952+
out: &mut WorktreeByBranch,
953+
) -> anyhow::Result<()> {
954+
let Some((head, wd)) = head.and_then(|head| {
955+
head.repo.worktree().map(|wt| {
956+
(
957+
head,
958+
match wt.id() {
959+
None => Worktree::Main,
960+
Some(id) => Worktree::LinkedId(id.to_owned()),
961+
},
962+
)
963+
})
964+
}) else {
965+
return Ok(());
966+
};
967+
968+
out.entry("HEAD".try_into().expect("valid"))
969+
.or_default()
970+
.push(wd.to_owned());
971+
let mut ref_chain = Vec::new();
972+
let mut cursor = head.try_into_referent();
973+
while let Some(ref_) = cursor {
974+
ref_chain.push(ref_.name().to_owned());
975+
cursor = ref_.follow().transpose()?;
976+
}
977+
for name in ref_chain {
978+
out.entry(name).or_default().push(wd.to_owned());
979+
}
980+
981+
Ok(())
982+
}
983+
984+
let mut map = BTreeMap::new();
985+
maybe_insert_head(repo.head().ok(), &mut map)?;
986+
for proxy in repo.worktrees()? {
987+
let repo = proxy.into_repo_with_possibly_inaccessible_worktree()?;
988+
maybe_insert_head(repo.head().ok(), &mut map)?;
989+
}
990+
Ok(map)
991+
}

crates/but-graph/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@
201201
mod segment;
202202
/// Use this for basic types like [`petgraph::Direction`], and graph algorithms.
203203
pub use petgraph;
204-
pub use segment::{Commit, CommitFlags, Segment, SegmentMetadata};
204+
pub use segment::{Commit, CommitFlags, RefInfo, Segment, SegmentMetadata, Worktree};
205205

206206
mod api;
207207
/// Produce a graph from a Git repository.

crates/but-graph/src/segment.rs

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
use bitflags::bitflags;
2-
31
use crate::{CommitIndex, SegmentIndex};
2+
use bitflags::bitflags;
3+
use bstr::BString;
4+
use std::borrow::Cow;
45

56
/// A commit with must useful information extracted from the Git commit itself.
67
#[derive(Clone, Eq, PartialEq)]
@@ -13,15 +14,57 @@ pub struct Commit {
1314
pub flags: CommitFlags,
1415
/// The references pointing to this commit, even after dereferencing tag objects.
1516
/// These can be names of tags and branches.
16-
pub refs: Vec<gix::refs::FullName>,
17+
pub refs: Vec<RefInfo>,
18+
}
19+
20+
/// A structure to inform about a reference which was present at a commit.
21+
#[derive(Debug, Clone, Eq, PartialEq)]
22+
pub struct RefInfo {
23+
/// The name of the reference.
24+
pub ref_name: gix::refs::FullName,
25+
/// If `Some`, provide information about the worktree that checks out the reference at `ref_name`,
26+
/// i.e. its `HEAD` points to `ref_name` directly or indirectly due to chains of .
27+
///
28+
/// It is `None` if no worktree needs to be updated if this reference is changed.
29+
pub worktree: Option<Worktree>,
30+
}
31+
32+
/// Describes which worktree is checked out.
33+
#[derive(Debug, Clone, Eq, PartialEq)]
34+
pub enum Worktree {
35+
/// The main worktree, i.e. the primary workspace associated with this repository, is checked out.
36+
///
37+
/// It cannot be removed.
38+
Main,
39+
/// The identifier of the worktree, which is always `.git/worktrees/<id>`,
40+
/// indicating that this is a linked worktree that can be removed.
41+
LinkedId(BString),
42+
}
43+
44+
impl RefInfo {
45+
/// Produce a string that identifies this instance concisely, and visually distinguishable.
46+
pub fn debug_string(&self) -> String {
47+
let ws = match self.worktree {
48+
None => String::new(),
49+
Some(ws) => match ws {
50+
Worktree::Main => {
51+
format!("[🌳main]")
52+
}
53+
Worktree::LinkedId(id) => {
54+
format!("[📁{id}]")
55+
}
56+
},
57+
};
58+
format!("►{}{ws}", self.ref_name.shorten())
59+
}
1760
}
1861

1962
impl std::fmt::Debug for Commit {
2063
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2164
let refs = self
2265
.refs
2366
.iter()
24-
.map(|rn| format!("►{}", rn.shorten()))
67+
.map(|ri| ri.debug_string())
2568
.collect::<Vec<_>>()
2669
.join(", ");
2770
write!(

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,13 @@ git init multi-root
120120
git checkout main && git merge --allow-unrelated-histories C
121121
)
122122

123+
git init ambiguous-worktrees
124+
(cd ambiguous-worktrees
125+
commit M
126+
git worktree add ../wt-outside-ambiguous-worktree
127+
git worktree add wt-inside-ambiguous-worktree
128+
)
129+
123130
# A single root that splits up into 4 branches and merges again
124131
git init four-diamond
125132
(cd four-diamond
@@ -205,6 +212,32 @@ git init special-branches
205212

206213
mkdir ws
207214
(cd ws
215+
git init ambiguous-worktrees
216+
(cd ambiguous-worktrees
217+
set -x
218+
commit M1
219+
commit M-base
220+
221+
git branch A
222+
git worktree add -b A-inside wt-A-inside
223+
git worktree add -b A-outside ../wt-A-outside
224+
225+
git checkout -b soon-origin-A main
226+
commit A-remote
227+
git checkout main
228+
commit M-advanced
229+
setup_target_to_match_main
230+
231+
git checkout -b B A
232+
commit B
233+
git checkout A
234+
git worktree add wt-B-inside B
235+
236+
create_workspace_commit_once A B
237+
setup_remote_tracking soon-origin-A A "move"
238+
git worktree add wt-origin-A-inside origin/A
239+
)
240+
208241
git init remote-and-integrated-tracking-linear
209242
(cd remote-and-integrated-tracking-linear
210243
commit M1

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,27 @@ fn special_branch_names_do_not_end_up_in_segment() -> anyhow::Result<()> {
723723
Ok(())
724724
}
725725

726+
#[test]
727+
fn ambiguous_worktrees() -> anyhow::Result<()> {
728+
let (repo, meta) = read_only_in_memory_scenario("ambiguous-worktrees")?;
729+
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @"* 85efbe4 (HEAD -> main, wt-outside-ambiguous-worktree, wt-inside-ambiguous-worktree) M");
730+
731+
let graph = Graph::from_head(&repo, &*meta, standard_options())?.validated()?;
732+
// TODO: show worktree ids at least, make main workspace clear
733+
insta::assert_snapshot!(graph_tree(&graph), @r"
734+
└── 👉►:0[0]:main
735+
└── ·85efbe4 (⌂|1) ►wt-inside-ambiguous-worktree, ►wt-outside-ambiguous-worktree
736+
");
737+
738+
insta::assert_snapshot!(graph_workspace(&graph.to_workspace()?), @r"
739+
⌂:0:main <> ✓!
740+
└── ≡:0:main
741+
└── :0:main
742+
└── ·85efbe4 ►wt-inside-ambiguous-worktree, ►wt-outside-ambiguous-worktree
743+
");
744+
Ok(())
745+
}
746+
726747
mod with_workspace;
727748

728749
mod utils;

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5462,6 +5462,56 @@ fn no_ws_commit_two_branches_no_target() -> anyhow::Result<()> {
54625462
Ok(())
54635463
}
54645464

5465+
#[test]
5466+
fn ambiguous_worktrees() -> anyhow::Result<()> {
5467+
let (repo, mut meta) = read_only_in_memory_scenario("ws/ambiguous-worktrees")?;
5468+
insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @r"
5469+
* a5f94a2 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
5470+
|\
5471+
| * 3e01e28 (B) B
5472+
|/
5473+
| * 8dc508f (origin/main, main) M-advanced
5474+
|/
5475+
| * 197ddce (origin/A) A-remote
5476+
|/
5477+
* 081bae9 (A-outside, A-inside, A) M-base
5478+
* 3183e43 M1
5479+
");
5480+
5481+
add_stack_with_segments(&mut meta, 0, "A", StackState::InWorkspace, &[]);
5482+
let graph = Graph::from_head(&repo, &*meta, standard_options())?.validated()?;
5483+
// TODO: Show worktrees
5484+
insta::assert_snapshot!(graph_tree(&graph), @r"
5485+
├── 👉📕►►►:0[0]:gitbutler/workspace
5486+
│ └── ·a5f94a2 (⌂|🏘|1)
5487+
│ ├── ►:5[1]:B
5488+
│ │ └── ·3e01e28 (⌂|🏘|1)
5489+
│ │ └── ►:3[2]:anon: →:6:
5490+
│ │ ├── ·081bae9 (⌂|🏘|✓|1111) ►A-inside, ►A-outside
5491+
│ │ └── ·3183e43 (⌂|🏘|✓|1111)
5492+
│ └── 📙►:6[1]:A <> origin/A →:4:
5493+
│ └── →:3:
5494+
├── ►:1[0]:origin/main →:2:
5495+
│ └── ►:2[1]:main <> origin/main →:1:
5496+
│ └── ·8dc508f (⌂|✓|10)
5497+
│ └── →:3:
5498+
└── ►:4[0]:origin/A →:6:
5499+
└── 🟣197ddce (0x0|1000)
5500+
└── →:3:
5501+
");
5502+
5503+
insta::assert_snapshot!(graph_workspace(&graph.to_workspace()?), @r"
5504+
📕🏘️:0:gitbutler/workspace <> ✓refs/remotes/origin/main⇣1 on 081bae9
5505+
├── ≡📙:6:A <> origin/A →:4:⇣1 on 081bae9 {0}
5506+
│ └── 📙:6:A <> origin/A →:4:⇣1
5507+
│ └── 🟣197ddce
5508+
└── ≡:5:B on 081bae9
5509+
└── :5:B
5510+
└── ·3e01e28 (🏘️)
5511+
");
5512+
Ok(())
5513+
}
5514+
54655515
mod edit_commit {
54665516
use but_graph::Graph;
54675517
use but_testsupport::{graph_tree, graph_workspace, visualize_commit_graph_all};

0 commit comments

Comments
 (0)