Skip to content

Commit 625a15c

Browse files
authored
Merge pull request #11093 from gitbutlerapp/copilot/replace-print-statements
fix: Replace println!/eprintln! with writeln! to prevent broken pipe panics
2 parents 387efe5 + 22725aa commit 625a15c

File tree

24 files changed

+577
-271
lines changed

24 files changed

+577
-271
lines changed

crates/but/agents.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
### Output
2-
* Usable output goes to `stdout` with `wrinteln!(stdout, "…")`, with `stdout` being a re-used variable filled with `std::io::stdout()`.
3-
- Use `atty::is` to print human output, otherwise print output optimised for use in shell scripts, when single values are returned.
4-
* Error or side-channel information goes to `stderr` with `writeln!(stderr, "…")`, with `stderr` being a re-used variable filled with `std::io::stderr()` as needed.
2+
* Usable output goes to `stdout` with `writeln!(stdout, …).ok()`.
3+
- Obtain `stdout` once per function using `let mut stdout = std::io::stdout();`
4+
- Use `stdout` when writing: `writeln!(stdout, "…").ok();`
5+
- Use `atty::is` to print human output, otherwise print output optimised for use in bash scripts, when single values are returned.
6+
* Error or side-channel information goes to `stderr` with `writeln!(stderr, …).ok()`.
7+
- Obtain `stderr` once per function using `let mut stderr = std::io::stderr();`
8+
- Use `stderr` when writing: `writeln!(stderr, "…").ok();`
9+
* The `.ok()` at the end ignores write errors gracefully (e.g., broken pipe) instead of panicking.
510
* `--json` only outputs the happy path, there are no JSON errors.
611

712
### Testing

crates/but/src/absorb/mod.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::collections::BTreeMap;
2+
use std::io::Write;
23

34
use bstr::{BString, ByteSlice};
45
use but_api::{diff, hex_hash::HexHash, virtual_branches};
@@ -147,8 +148,9 @@ fn absorb_all(
147148
assignments: &[HunkAssignment],
148149
dependencies: &Option<HunkDependencies>,
149150
) -> anyhow::Result<()> {
151+
let mut stdout = std::io::stdout();
150152
if assignments.is_empty() {
151-
println!("No uncommitted changes to absorb");
153+
writeln!(stdout, "No uncommitted changes to absorb").ok();
152154
return Ok(());
153155
}
154156

@@ -311,6 +313,8 @@ fn amend_commit(
311313
commit_id: gix::ObjectId,
312314
diff_specs: Vec<DiffSpec>,
313315
) -> anyhow::Result<()> {
316+
let mut stdout = std::io::stdout();
317+
let mut stderr = std::io::stderr();
314318
// Convert commit_id to HexHash
315319
let hex_hash = HexHash::from(commit_id);
316320

@@ -319,16 +323,20 @@ fn amend_commit(
319323
)?;
320324

321325
if !outcome.paths_to_rejected_changes.is_empty() {
322-
eprintln!(
326+
writeln!(
327+
stderr,
323328
"Warning: Failed to absorb {} file(s)",
324329
outcome.paths_to_rejected_changes.len()
325-
);
330+
)
331+
.ok();
326332
}
327333

328-
println!(
334+
writeln!(
335+
stdout,
329336
"Absorbed changes into commit {}",
330337
&commit_id.to_hex().to_string()[..7]
331-
);
338+
)
339+
.ok();
332340

333341
Ok(())
334342
}

crates/but/src/base.rs

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use gitbutler_branch_actions::upstream_integration::{
55
StackStatuses::{UpToDate, UpdatesRequired},
66
};
77
use gitbutler_project::Project;
8+
use std::io::Write;
89

910
#[derive(Debug, clap::Parser)]
1011
pub struct Platform {
@@ -20,23 +21,27 @@ pub enum Subcommands {
2021
}
2122

2223
pub fn handle(cmd: Subcommands, project: &Project, json: bool) -> anyhow::Result<()> {
24+
let mut stdout = std::io::stdout();
2325
match cmd {
2426
Subcommands::Check => {
2527
if !json {
26-
println!("🔍 Checking base branch status...");
28+
writeln!(stdout, "🔍 Checking base branch status...").ok();
2729
}
2830
let base_branch = but_api::virtual_branches::fetch_from_remotes(
2931
project.id,
3032
Some("auto".to_string()),
3133
)?;
32-
println!("\n📍 Base branch:\t\t{}", base_branch.branch_name);
33-
println!(
34+
writeln!(stdout, "\n📍 Base branch:\t\t{}", base_branch.branch_name).ok();
35+
writeln!(
36+
stdout,
3437
"⏫ Upstream commits:\t{} new commits on {}\n",
3538
base_branch.behind, base_branch.branch_name
36-
);
39+
)
40+
.ok();
3741
let commits = base_branch.recent_commits.iter().take(3);
3842
for commit in commits {
39-
println!(
43+
writeln!(
44+
stdout,
4045
"\t{} {}",
4146
&commit.id[..7],
4247
&commit
@@ -46,29 +51,34 @@ pub fn handle(cmd: Subcommands, project: &Project, json: bool) -> anyhow::Result
4651
.chars()
4752
.take(72)
4853
.collect::<String>()
49-
);
54+
)
55+
.ok();
5056
}
5157
let hidden_commits = base_branch.behind.saturating_sub(3);
5258
if hidden_commits > 0 {
53-
println!("\t... ({hidden_commits} more - run `but base check --all` to see all)");
59+
writeln!(
60+
stdout,
61+
"\t... ({hidden_commits} more - run `but base check --all` to see all)"
62+
)
63+
.ok();
5464
}
5565

5666
let status =
5767
but_api::virtual_branches::upstream_integration_statuses(project.id, None)?;
5868

5969
match status {
60-
UpToDate => println!("\n✅ Everything is up to date"),
70+
UpToDate => _ = writeln!(stdout, "\n✅ Everything is up to date").ok(),
6171
UpdatesRequired {
6272
worktree_conflicts,
6373
statuses,
6474
} => {
6575
if !worktree_conflicts.is_empty() {
66-
println!(
76+
writeln!(stdout,
6777
"\n❗️ There are uncommitted changes in the worktree that may conflict with the updates."
68-
);
78+
).ok();
6979
}
7080
if !statuses.is_empty() {
71-
println!("\n{}", "Active Branch Status".bold());
81+
writeln!(stdout, "\n{}", "Active Branch Status".bold()).ok();
7282
for (_id, status) in statuses {
7383
for bs in status.branch_statuses {
7484
let status_icon = match bs.status {
@@ -95,39 +105,45 @@ pub fn handle(cmd: Subcommands, project: &Project, json: bool) -> anyhow::Result
95105
}
96106
Empty => "Nothing to do".normal(),
97107
};
98-
println!("\n{} {} ({})", status_icon, bs.name, status_text);
108+
writeln!(stdout, "\n{} {} ({})", status_icon, bs.name, status_text)
109+
.ok();
99110
}
100111
}
101112
}
102113
}
103114
}
104-
println!("\nRun `but base update` to update your branches");
115+
writeln!(stdout, "\nRun `but base update` to update your branches").ok();
105116
Ok(())
106117
}
107118
Subcommands::Update => {
108119
let status =
109120
but_api::virtual_branches::upstream_integration_statuses(project.id, None)?;
110121
let resolutions = match status {
111122
UpToDate => {
112-
println!("✅ Everything is up to date");
123+
writeln!(stdout, "✅ Everything is up to date").ok();
113124
None
114125
}
115126
UpdatesRequired {
116127
worktree_conflicts,
117128
statuses,
118129
} => {
119130
if !worktree_conflicts.is_empty() {
120-
println!(
131+
writeln!(stdout,
121132
"❗️ There are uncommitted changes in the worktree that may conflict with
122133
the updates. Please commit or stash them and try again."
123-
);
134+
)
135+
.ok();
124136
None
125137
} else {
126-
println!("🔄 Updating branches...");
138+
writeln!(stdout, "🔄 Updating branches...").ok();
127139
let mut resolutions = vec![];
128140
for (maybe_stack_id, status) in statuses {
129141
let Some(stack_id) = maybe_stack_id else {
130-
println!("No stack ID, assuming we're on single-branch mode...",);
142+
writeln!(
143+
stdout,
144+
"No stack ID, assuming we're on single-branch mode...",
145+
)
146+
.ok();
131147
continue;
132148
};
133149
let approach = if status

crates/but/src/branch/list.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
use std::io::Write;
2+
13
use colored::Colorize;
24
use gitbutler_branch_actions::BranchListingFilter;
35
use gitbutler_project::Project;
46

57
pub async fn list(project: &Project, local: bool) -> Result<(), anyhow::Error> {
8+
let mut stdout = std::io::stdout();
69
let filter = if local {
710
Some(BranchListingFilter {
811
local: Some(true),
@@ -34,7 +37,7 @@ pub async fn list(project: &Project, local: bool) -> Result<(), anyhow::Error> {
3437
&branch_review_map,
3538
);
3639

37-
println!("{}{}", branch.name, reviews);
40+
writeln!(stdout, "{}{}", branch.name, reviews).ok();
3841
}
3942

4043
for branch in remote_only_branches {
@@ -43,7 +46,7 @@ pub async fn list(project: &Project, local: bool) -> Result<(), anyhow::Error> {
4346
&None,
4447
&branch_review_map,
4548
);
46-
println!("{} {}{}", "(remote)".dimmed(), branch.name, reviews);
49+
writeln!(stdout, "{} {}{}", "(remote)".dimmed(), branch.name, reviews).ok();
4750
}
4851
Ok(())
4952
}
@@ -55,6 +58,7 @@ fn print_applied_branches(
5558
Vec<gitbutler_forge::review::ForgeReview>,
5659
>,
5760
) {
61+
let mut stdout = std::io::stdout();
5862
for stack in applied_stacks {
5963
let first_branch = stack.heads.first();
6064
let last_branch = stack.heads.last();
@@ -67,7 +71,7 @@ fn print_applied_branches(
6771
&None,
6872
branch_review_map,
6973
);
70-
println!("{}{}", branch_entry.green(), reviews);
74+
writeln!(stdout, "{}{}", branch_entry.green(), reviews).ok();
7175
continue;
7276
}
7377

@@ -93,7 +97,7 @@ fn print_applied_branches(
9397
branch_review_map,
9498
);
9599

96-
println!("{}{}", branch_entry.green(), reviews);
100+
writeln!(stdout, "{}{}", branch_entry.green(), reviews).ok();
97101
}
98102
}
99103
}

crates/but/src/branch/mod.rs

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ pub enum Subcommands {
6060
}
6161

6262
pub async fn handle(cmd: Option<Subcommands>, project: &Project, json: bool) -> anyhow::Result<()> {
63+
let mut stdout = io::stdout();
6364
match cmd {
6465
None => {
6566
let local = false;
@@ -136,11 +137,11 @@ pub async fn handle(cmd: Option<Subcommands>, project: &Project, json: bool) ->
136137
branch: branch_name,
137138
anchor: anchor_for_json,
138139
};
139-
println!("{}", serde_json::to_string_pretty(&response)?);
140+
writeln!(stdout, "{}", serde_json::to_string_pretty(&response)?).ok();
140141
} else if atty::is(Stream::Stdout) {
141-
println!("Created branch {branch_name}");
142+
writeln!(stdout, "Created branch {branch_name}").ok();
142143
} else {
143-
println!("{branch_name}");
144+
writeln!(stdout, "{branch_name}").ok();
144145
}
145146
Ok(())
146147
}
@@ -162,7 +163,7 @@ pub async fn handle(cmd: Option<Subcommands>, project: &Project, json: bool) ->
162163
}
163164
}
164165

165-
println!("Branch '{}' not found in any stack", branch_name);
166+
writeln!(stdout, "Branch '{}' not found in any stack", branch_name).ok();
166167
Ok(())
167168
}
168169
Some(Subcommands::Unapply { branch_name, force }) => {
@@ -183,7 +184,7 @@ pub async fn handle(cmd: Option<Subcommands>, project: &Project, json: bool) ->
183184
}
184185
}
185186

186-
println!("Branch '{}' not found in any stack", branch_name);
187+
writeln!(stdout, "Branch '{}' not found in any stack", branch_name).ok();
187188
Ok(())
188189
}
189190
}
@@ -195,6 +196,7 @@ fn confirm_unapply_stack(
195196
stack_entry: &StackEntry,
196197
force: bool,
197198
) -> Result<(), anyhow::Error> {
199+
let mut stdout = io::stdout();
198200
let branches = stack_entry
199201
.heads
200202
.iter()
@@ -203,27 +205,31 @@ fn confirm_unapply_stack(
203205
.join(", ");
204206

205207
if !force {
206-
println!(
208+
writeln!(
209+
stdout,
207210
"Are you sure you want to unapply stack with branches '{}'? [y/N]:",
208211
branches
209-
);
212+
)
213+
.ok();
210214

211215
io::stdout().flush()?;
212216
let mut input = String::new();
213217
io::stdin().read_line(&mut input)?;
214218

215219
let input = input.trim().to_lowercase();
216220
if input != "y" && input != "yes" {
217-
println!("Aborted unapply operation.");
221+
writeln!(stdout, "Aborted unapply operation.").ok();
218222
return Ok(());
219223
}
220224
}
221225

222226
but_api::virtual_branches::unapply_stack(project.id, sid)?;
223-
println!(
227+
writeln!(
228+
stdout,
224229
"Unapplied stack with branches '{}' from workspace",
225230
branches
226-
);
231+
)
232+
.ok();
227233
Ok(())
228234
}
229235

@@ -233,24 +239,27 @@ fn confirm_branch_deletion(
233239
branch_name: &str,
234240
force: bool,
235241
) -> Result<(), anyhow::Error> {
242+
let mut stdout = io::stdout();
236243
if !force {
237-
println!(
244+
writeln!(
245+
stdout,
238246
"Are you sure you want to delete branch '{}'? [y/N]:",
239247
branch_name
240-
);
248+
)
249+
.ok();
241250

242251
io::stdout().flush()?;
243252
let mut input = String::new();
244253
io::stdin().read_line(&mut input)?;
245254

246255
let input = input.trim().to_lowercase();
247256
if input != "y" && input != "yes" {
248-
println!("Aborted branch deletion.");
257+
writeln!(stdout, "Aborted branch deletion.").ok();
249258
return Ok(());
250259
}
251260
}
252261

253262
but_api::stack::remove_branch(project.id, sid, branch_name.to_owned())?;
254-
println!("Deleted branch {branch_name}");
263+
writeln!(stdout, "Deleted branch {branch_name}").ok();
255264
Ok(())
256265
}

crates/but/src/command.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::io::Write;
2+
13
use but_action::Source;
24
use but_settings::AppSettings;
35
use gitbutler_command_context::CommandContext;
@@ -46,11 +48,12 @@ pub(crate) fn print<T>(this: &T, json: bool) -> anyhow::Result<()>
4648
where
4749
T: ?Sized + Serialize + std::fmt::Debug,
4850
{
51+
let mut stdout = std::io::stdout();
4952
if json {
5053
let json = serde_json::to_string_pretty(&this)?;
51-
println!("{json}");
54+
writeln!(stdout, "{json}").ok();
5255
} else {
53-
println!("{this:#?}");
56+
writeln!(stdout, "{this:#?}").ok();
5457
}
5558
Ok(())
5659
}

0 commit comments

Comments
 (0)