Skip to content

Commit 0e06cda

Browse files
authored
Merge pull request #11232 from Byron/next2
Single-branch support for `but branch apply`
2 parents 122676b + dbda345 commit 0e06cda

File tree

14 files changed

+546
-339
lines changed

14 files changed

+546
-339
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/but-testsupport/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,13 @@ pub fn git_at_dir(dir: impl AsRef<Path>) -> std::process::Command {
5151
/// Run the given `script` in bash, with the `cwd` set to the `repo` worktree.
5252
/// Panic if the script fails.
5353
pub fn invoke_bash(script: &str, repo: &gix::Repository) {
54+
invoke_bash_at_dir(script, repo.workdir().unwrap_or(repo.git_dir()))
55+
}
56+
/// Run the given `script` in bash, with the `cwd` set to `dir`.
57+
/// Panic if the script fails.
58+
pub fn invoke_bash_at_dir(script: &str, dir: &Path) {
5459
let mut cmd = std::process::Command::new("bash");
55-
cmd.current_dir(repo.workdir().unwrap_or(repo.git_dir()));
60+
cmd.current_dir(dir);
5661
isolate_env_std_cmd(&mut cmd);
5762
cmd.stdin(std::process::Stdio::piped())
5863
.stdout(std::process::Stdio::piped())

crates/but/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ cli-prompts = "0.1.0"
8080

8181
[dev-dependencies]
8282
but-core = { workspace = true, features = ["testing"] }
83+
serde_json = { workspace = true, features = ["preserve_order"] }
8384
but-testsupport = { workspace = true, features = ["snapbox"] }
8485
snapbox = { workspace = true, features = ["term-svg", "regex"] }
8586
shell-words = "1.1.0"

crates/but/src/args.rs

Lines changed: 82 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
use crate::forge::review;
2+
use crate::{base, branch, forge};
13
use std::path::PathBuf;
24

3-
use crate::forge;
4-
55
#[derive(Debug, clap::Parser)]
66
#[clap(name = "but", about = "A GitButler CLI tool", version = option_env!("GIX_VERSION"))]
77
pub struct Args {
@@ -11,6 +11,11 @@ pub struct Args {
1111
/// Run as if gitbutler-cli was started in PATH instead of the current working directory.
1212
#[clap(short = 'C', long, default_value = ".", value_name = "PATH")]
1313
pub current_dir: PathBuf,
14+
/// Explicitly control how output should be formatted.
15+
///
16+
/// If unset and from a terminal, it defaults to human output, when redirected it's for shells.
17+
#[clap(long, short = 'f', env = "BUT_OUTPUT_FORMAT", conflicts_with = "json")]
18+
pub format: Option<OutputFormat>,
1419
/// Whether to use JSON output format.
1520
#[clap(long, short = 'j', global = true)]
1621
pub json: bool,
@@ -84,9 +89,9 @@ For examples see `but rub --help`."
8489
repo: bool,
8590
},
8691
/// Commands for managing the base.
87-
Base(crate::base::Platform),
92+
Base(base::Platform),
8893
/// Commands for managing branches.
89-
Branch(crate::branch::Platform),
94+
Branch(branch::Platform),
9095
/// Commands for managing worktrees.
9196
#[clap(hide = true)]
9297
Worktree(crate::worktree::Platform),
@@ -198,35 +203,82 @@ For examples see `but rub --help`."
198203
},
199204
}
200205

206+
impl Subcommands {
207+
pub fn to_metrics_command(&self) -> CommandName {
208+
use CommandName::*;
209+
match self {
210+
Subcommands::Log => Log,
211+
Subcommands::Status { .. } => Status,
212+
Subcommands::Stf { .. } => Stf,
213+
Subcommands::Rub { .. } => Rub,
214+
Subcommands::Base(base::Platform { cmd }) => match cmd {
215+
base::Subcommands::Update => BaseUpdate,
216+
base::Subcommands::Check => BaseCheck,
217+
},
218+
Subcommands::Branch(branch::Platform { cmd }) => match cmd {
219+
None | Some(branch::Subcommands::List { .. }) => BranchList,
220+
Some(branch::Subcommands::New { .. }) => BranchNew,
221+
Some(branch::Subcommands::Delete { .. }) => BranchDelete,
222+
Some(branch::Subcommands::Unapply { .. }) => BranchUnapply,
223+
Some(branch::Subcommands::Apply { .. }) => BranchApply,
224+
},
225+
Subcommands::Worktree(crate::worktree::Platform { cmd: _ }) => Worktree,
226+
Subcommands::Mark { .. } => Mark,
227+
Subcommands::Unmark => Unmark,
228+
Subcommands::Gui => Gui,
229+
Subcommands::Commit { .. } => Commit,
230+
Subcommands::Push(_) => Push,
231+
Subcommands::New { .. } => New,
232+
Subcommands::Describe { .. } => Describe,
233+
Subcommands::Oplog { .. } => Oplog,
234+
Subcommands::Restore { .. } => Restore,
235+
Subcommands::Undo => Undo,
236+
Subcommands::Snapshot { .. } => Snapshot,
237+
Subcommands::Claude(claude::Platform { cmd }) => match cmd {
238+
claude::Subcommands::PreTool => ClaudePreTool,
239+
claude::Subcommands::PostTool => ClaudePostTool,
240+
claude::Subcommands::Stop => ClaudeStop,
241+
claude::Subcommands::Last { .. }
242+
| claude::Subcommands::PermissionPromptMcp { .. } => Unknown,
243+
},
244+
Subcommands::Cursor(cursor::Platform { cmd }) => match cmd {
245+
cursor::Subcommands::AfterEdit => CursorAfterEdit,
246+
cursor::Subcommands::Stop { .. } => CursorStop,
247+
},
248+
Subcommands::Forge(forge::integration::Platform { cmd }) => match cmd {
249+
forge::integration::Subcommands::Auth => ForgeAuth,
250+
forge::integration::Subcommands::Forget { .. } => ForgeForget,
251+
forge::integration::Subcommands::ListUsers => ForgeListUsers,
252+
},
253+
Subcommands::Review(review::Platform { cmd }) => match cmd {
254+
review::Subcommands::Publish { .. } => PublishReview,
255+
review::Subcommands::Template { .. } => ReviewTemplate,
256+
},
257+
Subcommands::Completions { .. } => Completions,
258+
Subcommands::Absorb { .. } => Absorb,
259+
Subcommands::Metrics { .. }
260+
| Subcommands::Actions(_)
261+
| Subcommands::Mcp { .. }
262+
| Subcommands::Init { .. } => Unknown,
263+
}
264+
}
265+
}
266+
201267
#[derive(Debug, Clone, Copy, clap::ValueEnum, Default)]
202268
pub enum CommandName {
203-
#[clap(alias = "log")]
204269
Log,
205-
#[clap(alias = "absorb")]
206270
Absorb,
207-
#[clap(alias = "st")]
208271
Status,
209-
#[clap(alias = "stf", hide = true)]
210272
Stf,
211-
#[clap(alias = "rub")]
212273
Rub,
213-
#[clap(alias = "commit")]
214274
Commit,
215-
#[clap(alias = "push")]
216275
Push,
217-
#[clap(alias = "new")]
218276
New,
219-
#[clap(alias = "describe")]
220277
Describe,
221-
#[clap(alias = "oplog")]
222278
Oplog,
223-
#[clap(alias = "restore")]
224279
Restore,
225-
#[clap(alias = "undo")]
226280
Undo,
227-
#[clap(alias = "snapshot")]
228281
Snapshot,
229-
#[clap(alias = "gui")]
230282
Gui,
231283
BaseCheck,
232284
BaseUpdate,
@@ -235,40 +287,10 @@ pub enum CommandName {
235287
BranchList,
236288
BranchUnapply,
237289
BranchApply,
238-
#[clap(
239-
alias = "claude-pre-tool",
240-
alias = "claudepretool",
241-
alias = "claudePreTool",
242-
alias = "ClaudePreTool"
243-
)]
244290
ClaudePreTool,
245-
#[clap(
246-
alias = "claude-post-tool",
247-
alias = "claudeposttool",
248-
alias = "claudePostTool",
249-
alias = "ClaudePostTool"
250-
)]
251291
ClaudePostTool,
252-
#[clap(
253-
alias = "claude-stop",
254-
alias = "claudestop",
255-
alias = "claudeStop",
256-
alias = "ClaudeStop"
257-
)]
258292
ClaudeStop,
259-
#[clap(
260-
alias = "cursor-after-edit",
261-
alias = "cursorafteredit",
262-
alias = "cursorAfterEdit",
263-
alias = "CursorAfterEdit"
264-
)]
265293
CursorAfterEdit,
266-
#[clap(
267-
alias = "cursor-stop",
268-
alias = "cursorstop",
269-
alias = "cursorStop",
270-
alias = "CursorStop"
271-
)]
272294
CursorStop,
273295
Worktree,
274296
Mark,
@@ -283,6 +305,18 @@ pub enum CommandName {
283305
Unknown,
284306
}
285307

308+
/// How to format the output.
309+
#[derive(Debug, Clone, Copy, clap::ValueEnum, Default)]
310+
pub enum OutputFormat {
311+
/// Produce verbose output for human consumption.
312+
#[default]
313+
Human,
314+
/// The output is optimised for variable assignment in shells.
315+
Shell,
316+
/// Output detailed information as JSON for tool consumption.
317+
Json,
318+
}
319+
286320
pub mod actions {
287321
#[derive(Debug, clap::Parser)]
288322
pub struct Platform {

crates/but/src/branch/apply.rs

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
use crate::utils::{Output, OutputFormat};
12
use anyhow::bail;
23
use bstr::ByteSlice;
34
use gitbutler_reference::RemoteRefname;
5+
use gix::reference::Category;
6+
use std::io::Write;
47
use std::{ops::Deref, str::FromStr};
58

69
/// Apply a branch to the workspace, and return the full ref name to it.
@@ -9,7 +12,7 @@ use std::{ops::Deref, str::FromStr};
912
pub fn apply(
1013
ctx: &but_ctx::Context,
1114
branch_name: &str,
12-
json: bool,
15+
out: &mut Output,
1316
) -> anyhow::Result<but_api::json::Reference> {
1417
let legacy_project = &ctx.legacy_project;
1518
let ctx = ctx.legacy_ctx()?;
@@ -32,9 +35,6 @@ pub fn apply(
3235
remote_ref_name,
3336
None,
3437
)?;
35-
if !json {
36-
println!("Applied branch '{branch_name}' to workspace");
37-
}
3838
reference
3939
} else if let Some((remote_ref, reference)) = find_remote_reference(&repo, branch_name)? {
4040
let remote = remote_ref.remote();
@@ -48,14 +48,30 @@ pub fn apply(
4848
Some(remote_ref.clone()),
4949
None,
5050
)?;
51-
if !json {
52-
println!("Applied remote branch '{branch_name}' to workspace");
53-
}
5451
reference
5552
} else {
5653
bail!("Could not find branch '{branch_name}' in local repository");
5754
};
5855

56+
match out.format {
57+
OutputFormat::Human => {
58+
let short_name = reference.name().shorten();
59+
let is_remote_reference = reference
60+
.name()
61+
.category()
62+
.is_some_and(|c| c == Category::RemoteBranch);
63+
if is_remote_reference {
64+
writeln!(out, "Applied remote branch '{short_name}' to workspace")
65+
} else {
66+
writeln!(out, "Applied branch '{short_name}' to workspace")
67+
}
68+
.ok();
69+
}
70+
OutputFormat::Shell => {
71+
writeln!(out, "{reference_name}", reference_name = reference.name())?;
72+
}
73+
}
74+
5975
Ok(reference.inner.into())
6076
}
6177

crates/but/src/branch/list.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::io::Write;
22

3-
use crate::we_need_proper_json_output_here;
3+
use crate::utils::we_need_proper_json_output_here;
44
use colored::Colorize;
55
use gitbutler_branch_actions::BranchListingFilter;
66
use gitbutler_project::Project;

crates/but/src/branch/mod.rs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use crate::{LegacyProject, into_json_value, we_need_proper_json_output_here};
1+
use crate::LegacyProject;
2+
use crate::utils::{Output, OutputFormat, into_json_value, we_need_proper_json_output_here};
23
use anyhow::bail;
3-
use atty::Stream;
44
use but_core::ref_metadata::StackId;
55
use but_settings::AppSettings;
66
use but_workspace::legacy::ui::StackEntry;
@@ -70,9 +70,8 @@ pub enum Subcommands {
7070
pub async fn handle(
7171
cmd: Option<Subcommands>,
7272
ctx: &but_ctx::Context,
73-
json: bool,
73+
out: &mut Output,
7474
) -> anyhow::Result<serde_json::Value> {
75-
let mut stdout = io::stdout();
7675
let legacy_project = &ctx.legacy_project;
7776
match cmd {
7877
None => {
@@ -147,18 +146,19 @@ pub async fn handle(
147146
},
148147
)?;
149148

150-
if json {
151-
let response = json::BranchNewOutput {
152-
branch: branch_name,
153-
anchor: anchor_for_json,
154-
};
155-
writeln!(stdout, "{}", serde_json::to_string_pretty(&response)?)?;
156-
} else if atty::is(Stream::Stdout) {
157-
writeln!(stdout, "Created branch {branch_name}").ok();
158-
} else {
159-
writeln!(stdout, "{branch_name}").ok();
149+
let json = json::BranchNewOutput {
150+
branch: branch_name.clone(),
151+
anchor: anchor_for_json,
152+
};
153+
match out.format {
154+
OutputFormat::Human => {
155+
writeln!(out, "Created branch {branch_name}").ok();
156+
}
157+
OutputFormat::Shell => {
158+
writeln!(out, "{branch_name}").ok();
159+
}
160160
}
161-
Ok(we_need_proper_json_output_here())
161+
Ok(into_json_value(json))
162162
}
163163
Some(Subcommands::Delete { branch_name, force }) => {
164164
let stacks = but_api::workspace::stacks(
@@ -178,11 +178,11 @@ pub async fn handle(
178178
}
179179
}
180180

181-
writeln!(stdout, "Branch '{}' not found in any stack", branch_name).ok();
181+
writeln!(out, "Branch '{}' not found in any stack", branch_name).ok();
182182
Ok(we_need_proper_json_output_here())
183183
}
184184
Some(Subcommands::Apply { branch_name }) => {
185-
apply::apply(ctx, &branch_name, json).map(into_json_value)
185+
apply::apply(ctx, &branch_name, out).map(into_json_value)
186186
}
187187
Some(Subcommands::Unapply { branch_name, force }) => {
188188
let stacks = but_api::workspace::stacks(
@@ -202,7 +202,7 @@ pub async fn handle(
202202
}
203203
}
204204

205-
writeln!(stdout, "Branch '{}' not found in any stack", branch_name).ok();
205+
writeln!(out, "Branch '{}' not found in any stack", branch_name).ok();
206206
Ok(we_need_proper_json_output_here())
207207
}
208208
}

0 commit comments

Comments
 (0)