Skip to content

Commit 6c36c14

Browse files
committed
Show non-main worktrees in but status
Note that this is just a first take, and more integrations are likely needed to get it right. For one, legacy APIs shouldn't be used.
1 parent a771d39 commit 6c36c14

File tree

10 files changed

+253
-17
lines changed

10 files changed

+253
-17
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,6 @@ mkdir ws
214214
(cd ws
215215
git init ambiguous-worktrees
216216
(cd ambiguous-worktrees
217-
set -x
218217
commit M1
219218
commit M-base
220219

crates/but-workspace/src/branch_details.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ pub fn branch_details(
8686

8787
Ok(ui::BranchDetails {
8888
name: branch_name.into(),
89+
linked_worktree_id: None, /* not implemented in legacy mode */
8990
remote_tracking_branch: upstream
9091
.as_ref()
9192
.and_then(|upstream| upstream.get().name())
@@ -228,6 +229,7 @@ pub fn branch_details_v3(
228229

229230
Ok(ui::BranchDetails {
230231
name: name.as_bstr().into(),
232+
linked_worktree_id: None, /* probably not needed here */
231233
remote_tracking_branch: remote_tracking_branch.map(|b| b.name().as_bstr().to_owned()),
232234
description: meta.description.clone(),
233235
pr_number: meta.review.pull_request,

crates/but-workspace/src/stacks.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@ pub fn stack_details(
391391

392392
branch_details.push(ui::BranchDetails {
393393
name: branch.name().to_owned().into(),
394+
linked_worktree_id: None, /* not implemented in legacy mode */
394395
remote_tracking_branch: upstream_reference.map(Into::into),
395396
description: branch.description.clone(),
396397
pr_number: branch.pr_number,
@@ -549,10 +550,9 @@ impl ui::BranchDetails {
549550
base,
550551
}: &Segment,
551552
) -> anyhow::Result<Self> {
552-
let ref_name = ref_info
553+
let ref_info = ref_info
553554
.clone()
554-
.context("Can't handle a stack yet whose tip isn't pointed to by a ref")?
555-
.ref_name;
555+
.context("Can't handle a stack yet whose tip isn't pointed to by a ref")?;
556556
let (description, updated_at, review_id, pr_number) = metadata
557557
.clone()
558558
.map(|meta| {
@@ -566,10 +566,15 @@ impl ui::BranchDetails {
566566
.unwrap_or_default();
567567
let base_commit = base.unwrap_or(gix::hash::Kind::Sha1.null());
568568
Ok(ui::BranchDetails {
569-
is_remote_head: ref_name
569+
is_remote_head: ref_info
570+
.ref_name
570571
.category()
571572
.is_some_and(|c| matches!(c, gix::refs::Category::RemoteBranch)),
572-
name: ref_name.shorten().into(),
573+
name: ref_info.ref_name.shorten().into(),
574+
linked_worktree_id: ref_info.worktree.and_then(|ws| match ws {
575+
but_graph::Worktree::Main => None,
576+
but_graph::Worktree::LinkedId(id) => Some(id),
577+
}),
573578
remote_tracking_branch: remote_tracking_ref_name
574579
.as_ref()
575580
.map(|full_name| full_name.as_bstr().into()),

crates/but-workspace/src/ui/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,10 @@ pub struct BranchDetails {
362362
/// The name of the branch.
363363
#[serde(with = "gitbutler_serde::bstring_lossy")]
364364
pub name: BString,
365+
/// The id of the linked worktree that has the reference of `name` checked out.
366+
/// Note that we don't list the main worktree here.
367+
#[serde(with = "gitbutler_serde::bstring_opt_lossy")]
368+
pub linked_worktree_id: Option<BString>,
365369
/// Upstream reference, e.g. `refs/remotes/origin/base-branch-improvements`
366370
#[serde(with = "gitbutler_serde::bstring_opt_lossy")]
367371
pub remote_tracking_branch: Option<BString>,

crates/but/src/status/mod.rs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ pub fn print_group(
216216
review_map: &std::collections::HashMap<String, Vec<gitbutler_forge::review::ForgeReview>>,
217217
) -> anyhow::Result<()> {
218218
let mut stdout = std::io::stdout();
219+
let repo = project.open_isolated()?;
219220
if let Some(group) = &group {
220221
let mut first = true;
221222
for branch in &group.branch_details {
@@ -242,15 +243,24 @@ pub fn print_group(
242243
review_map,
243244
);
244245

246+
let workspace = branch
247+
.linked_worktree_id
248+
.as_ref()
249+
.and_then(|id| {
250+
let ws = repo.worktree_proxy_by_id(id.as_bstr())?;
251+
let base = ws.base().ok()?;
252+
let git_dir = gix::path::realpath(repo.git_dir()).ok();
253+
let base = git_dir
254+
.and_then(|git_dir| base.strip_prefix(git_dir).ok())
255+
.unwrap_or_else(|| &base);
256+
format!(" 📁 {base}", base = base.display()).into()
257+
})
258+
.unwrap_or_default();
245259
writeln!(
246260
stdout,
247-
"┊{}┄{} [{}]{} {} {}",
248-
notch,
249-
id,
250-
branch.name.to_string().green().bold(),
251-
reviews,
252-
no_commits,
253-
stack_mark.clone().unwrap_or_default()
261+
"┊{notch}┄{id} [{branch}{workspace}]{reviews} {no_commits} {stack_mark}",
262+
stack_mark = stack_mark.clone().unwrap_or_default(),
263+
branch = branch.name.to_string().green().bold(),
254264
)
255265
.ok();
256266
*stack_mark = None; // Only show the stack mark for the first branch
Lines changed: 59 additions & 0 deletions
Loading
Lines changed: 53 additions & 0 deletions
Loading

crates/but/tests/but/status.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,46 @@
11
use crate::utils::Sandbox;
22
use crate::utils::setup_metadata;
33

4+
#[test]
5+
fn worktrees() -> anyhow::Result<()> {
6+
let env = Sandbox::init_scenario_with_target_slow("two-worktrees")?;
7+
insta::assert_snapshot!(env.git_log()?, @r"
8+
* 063d8c1 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
9+
|\
10+
| * 3e01e28 (B) B
11+
* | 4c4624e (A) A
12+
|/
13+
| * 8dc508f (origin/main, origin/HEAD, main) M-advanced
14+
|/
15+
| * 197ddce (origin/A) A-remote
16+
|/
17+
* 081bae9 M-base
18+
* 3183e43 M1
19+
");
20+
21+
// Must set metadata to match the scenario, or else the old APIs used here won't deliver.
22+
setup_metadata(&env, &["A", "B"])?;
23+
24+
env.but("status")
25+
.with_assert(env.assert_with_uuid_and_timestamp_redactions())
26+
.assert()
27+
.success()
28+
.stderr_eq(snapbox::str![])
29+
.stdout_eq(snapbox::file![
30+
"snapshots/two-worktrees/status-with-worktrees.stdout.term.svg"
31+
]);
32+
33+
env.but("status --verbose")
34+
.with_assert(env.assert_with_uuid_and_timestamp_redactions())
35+
.assert()
36+
.success()
37+
.stderr_eq(snapbox::str![])
38+
.stdout_eq(snapbox::file![
39+
"snapshots/two-worktrees/status-with-worktrees-verbose.stdout.term.svg"
40+
]);
41+
Ok(())
42+
}
43+
444
#[test]
545
fn json_shows_paths_as_strings() -> anyhow::Result<()> {
646
let env = Sandbox::init_scenario_with_target("two-stacks")?;
@@ -72,6 +112,7 @@ fn json_shows_paths_as_strings() -> anyhow::Result<()> {
72112
"branchDetails": [
73113
{
74114
"name": "A",
115+
"linkedWorktreeId": null,
75116
"remoteTrackingBranch": null,
76117
"description": null,
77118
"prNumber": null,
@@ -126,6 +167,7 @@ fn json_shows_paths_as_strings() -> anyhow::Result<()> {
126167
"branchDetails": [
127168
{
128169
"name": "B",
170+
"linkedWorktreeId": null,
129171
"remoteTrackingBranch": null,
130172
"description": null,
131173
"prNumber": null,

crates/but/tests/but/utils.rs

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use but_settings::app_settings::{
66
Claude, ExtraCsp, FeatureFlags, Fetch, GitHubOAuthAppSettings, Reviews, TelemetrySettings,
77
UiSettings,
88
};
9-
use but_testsupport::gix_testtools::tempfile;
9+
use but_testsupport::gix_testtools::{Creation, tempfile};
1010
use but_workspace::StackId;
1111
use snapbox::{Assert, Redactions};
1212
use std::env;
@@ -49,9 +49,24 @@ impl Sandbox {
4949
/// TODO: we shouldn't have to add the project for interaction - it's only useful for listing.
5050
/// TODO: there should be no need for the target.
5151
pub fn init_scenario_with_target(name: &str) -> anyhow::Result<Sandbox> {
52-
let project = but_testsupport::gix_testtools::scripted_fixture_writable(format!(
53-
"scenario/{name}.sh"
54-
))
52+
Self::init_scenario_with_target_inner(name, Creation::CopyFromReadOnly)
53+
}
54+
55+
/// Like [`Self::init_scenario_with_target`], Execute the script at `name` instead of
56+
/// copying it - necessary if Git places absolute paths.
57+
pub fn init_scenario_with_target_slow(name: &str) -> anyhow::Result<Sandbox> {
58+
Self::init_scenario_with_target_inner(name, Creation::ExecuteScript)
59+
}
60+
61+
fn init_scenario_with_target_inner(
62+
name: &str,
63+
script_creation: Creation,
64+
) -> anyhow::Result<Sandbox> {
65+
let project = but_testsupport::gix_testtools::scripted_fixture_writable_with_args(
66+
format!("scenario/{name}.sh"),
67+
None::<String>,
68+
script_creation,
69+
)
5570
.map_err(anyhow::Error::from_boxed)?;
5671
let sandbox = Sandbox {
5772
projects_root: Some(project),
@@ -148,6 +163,20 @@ impl Sandbox {
148163
)
149164
}
150165

166+
/// return the graph at `HEAD`, along with the `(graph, repo, meta)` repository and metadata used to create it.
167+
pub fn graph_at_head(
168+
&self,
169+
) -> anyhow::Result<(
170+
but_graph::Graph,
171+
gix::Repository,
172+
impl but_core::RefMetadata,
173+
)> {
174+
let repo = self.repo()?;
175+
let meta = self.meta()?;
176+
let graph = but_graph::Graph::from_head(&repo, &meta, Default::default())?;
177+
Ok((graph, repo, meta))
178+
}
179+
151180
/// Show a git log for all refs.
152181
pub fn git_log(&self) -> anyhow::Result<String> {
153182
let repo = self.repo()?;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env bash
2+
3+
set -eu -o pipefail
4+
5+
source "${BASH_SOURCE[0]%/*}/shared.sh"
6+
7+
### General Description
8+
9+
# A ws-ref points to a workspace commit, with two stacks inside, each with their own commit.
10+
git init
11+
commit M1
12+
commit M-base
13+
14+
git checkout -b A
15+
commit A
16+
17+
git checkout -b soon-origin-A main
18+
commit A-remote
19+
20+
git checkout -b B main
21+
commit B
22+
23+
git checkout main
24+
commit M-advanced
25+
setup_target_to_match_main
26+
27+
git checkout A
28+
create_workspace_commit_once A B
29+
setup_remote_tracking soon-origin-A A "move"
30+
31+
git worktree add .git/gitbutler/worktrees/A A
32+
git worktree add .git/gitbutler/worktrees/B B
33+

0 commit comments

Comments
 (0)