Skip to content

Commit 506f89f

Browse files
committed
A sketch for more formalised but CLI output handling.
1 parent 47445cb commit 506f89f

File tree

13 files changed

+425
-236
lines changed

13 files changed

+425
-236
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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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,
@@ -198,6 +203,18 @@ For examples see `but rub --help`."
198203
},
199204
}
200205

206+
/// How to format the output.
207+
#[derive(Debug, Clone, Copy, clap::ValueEnum, Default)]
208+
pub enum OutputFormat {
209+
/// Produce verbose output for human consumption.
210+
#[default]
211+
Human,
212+
/// The output is optimised for variable assignment in shells.
213+
Shell,
214+
/// Output detailed information as JSON for tool consumption.
215+
Json,
216+
}
217+
201218
#[derive(Debug, Clone, Copy, clap::ValueEnum, Default)]
202219
pub enum CommandName {
203220
#[clap(alias = "log")]

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)