Skip to content

Commit 67acbae

Browse files
committed
Allow inline message for but describe
Add an optional -m/--message flag to the describe command so users can supply a new commit message or branch name on the command line instead of opening an editor. This change threads the optional message through the describe handlers and uses it to validate and apply the supplied value, when absent it falls back to the existing editor-based flow.
1 parent ae8c87d commit 67acbae

File tree

5 files changed

+193
-11
lines changed

5 files changed

+193
-11
lines changed

crates/but/src/args/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,9 @@ pub enum Subcommands {
345345
Describe {
346346
/// Commit ID to edit the message for, or branch ID to rename
347347
target: String,
348+
/// The new commit message or branch name. If not provided, opens an editor.
349+
#[clap(short = 'm', long = "message")]
350+
message: Option<String>,
348351
},
349352

350353
/// Show operation history.

crates/but/src/command/legacy/describe.rs

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub(crate) fn describe_target(
99
project: &Project,
1010
out: &mut OutputChannel,
1111
target: &str,
12+
message: Option<&str>,
1213
) -> Result<()> {
1314
let mut ctx = Context::new_from_legacy_project(project.clone())?;
1415

@@ -31,10 +32,10 @@ pub(crate) fn describe_target(
3132

3233
match cli_id {
3334
CliId::Branch { name, .. } => {
34-
edit_branch_name(&ctx, project, name, out)?;
35+
edit_branch_name(&ctx, project, name, out, message)?;
3536
}
3637
CliId::Commit { oid } => {
37-
edit_commit_message_by_id(&ctx, project, *oid, out)?;
38+
edit_commit_message_by_id(&ctx, project, *oid, out, message)?;
3839
}
3940
_ => {
4041
bail!("Target must be a commit ID, not {}", cli_id.kind());
@@ -49,6 +50,7 @@ fn edit_branch_name(
4950
project: &Project,
5051
branch_name: &str,
5152
out: &mut OutputChannel,
53+
message: Option<&str>,
5254
) -> Result<()> {
5355
// Find which stack this branch belongs to
5456
let stacks = but_api::legacy::workspace::stacks(
@@ -62,7 +64,17 @@ fn edit_branch_name(
6264
}
6365

6466
if let Some(sid) = stack_entry.id {
65-
let new_name = get_branch_name_from_editor(branch_name)?;
67+
let new_name = if let Some(msg) = message {
68+
// Use provided message, trim and validate
69+
let trimmed = msg.trim();
70+
if trimmed.is_empty() {
71+
bail!("Aborting due to empty branch name");
72+
}
73+
trimmed.to_string()
74+
} else {
75+
// Fall back to editor
76+
get_branch_name_from_editor(branch_name)?
77+
};
6678
but_api::legacy::stack::update_branch_name(
6779
project.id,
6880
sid,
@@ -84,6 +96,7 @@ fn edit_commit_message_by_id(
8496
project: &Project,
8597
commit_oid: gix::ObjectId,
8698
out: &mut OutputChannel,
99+
message: Option<&str>,
87100
) -> Result<()> {
88101
// Find which stack this commit belongs to
89102
let stacks = but_api::legacy::workspace::stacks(project.id, None)?;
@@ -132,15 +145,25 @@ fn edit_commit_message_by_id(
132145
let stack_id = stack_id
133146
.ok_or_else(|| anyhow::anyhow!("Could not find stack for commit {}", commit_oid))?;
134147

135-
// Get the files changed in this commit using but_api
136-
let commit_details = but_api::legacy::diff::commit_details(project.id, commit_oid.into())?;
137-
let changed_files = get_changed_files_from_commit_details(&commit_details);
138-
139148
// Get current commit message
140149
let current_message = commit_message.to_string();
141150

142-
// Open editor with current message and file list
143-
let new_message = get_commit_message_from_editor(&current_message, &changed_files)?;
151+
// Get new message from provided argument or editor
152+
let new_message = if let Some(msg) = message {
153+
// Use provided message, trim and validate
154+
let trimmed = msg.trim();
155+
if trimmed.is_empty() {
156+
bail!("Aborting due to empty commit message");
157+
}
158+
trimmed.to_string()
159+
} else {
160+
// Get the files changed in this commit using but_api
161+
let commit_details = but_api::legacy::diff::commit_details(project.id, commit_oid.into())?;
162+
let changed_files = get_changed_files_from_commit_details(&commit_details);
163+
164+
// Open editor with current message and file list
165+
get_commit_message_from_editor(&current_message, &changed_files)?
166+
};
144167

145168
if new_message.trim() == current_message.trim() {
146169
bail!("No changes to commit message.");

crates/but/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,9 +364,9 @@ async fn match_subcommand(
364364
.emit_metrics(metrics_ctx)
365365
}
366366
#[cfg(feature = "legacy")]
367-
Subcommands::Describe { target } => {
367+
Subcommands::Describe { target, message } => {
368368
let project = legacy::get_or_init_non_bare_project(&args)?;
369-
command::legacy::describe::describe_target(&project, out, &target)
369+
command::legacy::describe::describe_target(&project, out, &target, message.as_deref())
370370
.emit_metrics(metrics_ctx)
371371
}
372372
#[cfg(feature = "legacy")]
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
use snapbox::str;
2+
3+
use crate::utils::{Sandbox, setup_metadata};
4+
5+
#[test]
6+
fn describe_commit_with_message_flag() -> anyhow::Result<()> {
7+
let env = Sandbox::init_scenario_with_target_and_default_settings("one-stack")?;
8+
insta::assert_snapshot!(env.git_log()?, @r"
9+
* edd3eb7 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
10+
* 9477ae7 (A) add A
11+
* 0dc3733 (origin/main, origin/HEAD, main) add M
12+
");
13+
14+
setup_metadata(&env, &["A"])?;
15+
16+
// Use describe with -m flag to change commit message (using commit ID)
17+
env.but("describe 9477ae7 -m 'Updated commit message'")
18+
.assert()
19+
.success()
20+
.stdout_eq(str![[r#"
21+
Updated commit message for [..] (now [..])
22+
23+
"#]]);
24+
25+
// Verify the commit message was updated
26+
let log = env.git_log()?;
27+
assert!(log.contains("Updated commit message"));
28+
assert!(!log.contains("add A"));
29+
30+
Ok(())
31+
}
32+
33+
// Note: Branch rename test is omitted because the test scenario uses single-character
34+
// branch names ("A") which don't meet the 2-character minimum requirement for CLI IDs.
35+
// The branch rename functionality with -m flag is tested manually and works correctly.
36+
37+
#[test]
38+
fn describe_with_empty_message_fails() -> anyhow::Result<()> {
39+
let env = Sandbox::init_scenario_with_target_and_default_settings("one-stack")?;
40+
insta::assert_snapshot!(env.git_log()?, @r"
41+
* edd3eb7 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
42+
* 9477ae7 (A) add A
43+
* 0dc3733 (origin/main, origin/HEAD, main) add M
44+
");
45+
46+
setup_metadata(&env, &["A"])?;
47+
48+
// Try to describe commit with empty message
49+
env.but("describe 9477ae7 -m ''")
50+
.assert()
51+
.failure()
52+
.stderr_eq(str![[r#"
53+
Error: Aborting due to empty commit message
54+
55+
"#]]);
56+
57+
Ok(())
58+
}
59+
60+
#[test]
61+
fn describe_with_whitespace_only_message_fails() -> anyhow::Result<()> {
62+
let env = Sandbox::init_scenario_with_target_and_default_settings("one-stack")?;
63+
insta::assert_snapshot!(env.git_log()?, @r"
64+
* edd3eb7 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
65+
* 9477ae7 (A) add A
66+
* 0dc3733 (origin/main, origin/HEAD, main) add M
67+
");
68+
69+
setup_metadata(&env, &["A"])?;
70+
71+
// Try to describe commit with whitespace-only message
72+
env.but("describe 9477ae7 -m ' '")
73+
.assert()
74+
.failure()
75+
.stderr_eq(str![[r#"
76+
Error: Aborting due to empty commit message
77+
78+
"#]]);
79+
80+
Ok(())
81+
}
82+
83+
#[test]
84+
fn describe_commit_with_same_message_fails() -> anyhow::Result<()> {
85+
let env = Sandbox::init_scenario_with_target_and_default_settings("one-stack")?;
86+
insta::assert_snapshot!(env.git_log()?, @r"
87+
* edd3eb7 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
88+
* 9477ae7 (A) add A
89+
* 0dc3733 (origin/main, origin/HEAD, main) add M
90+
");
91+
92+
setup_metadata(&env, &["A"])?;
93+
94+
// Try to describe with the same message
95+
env.but("describe 9477ae7 -m 'add A'")
96+
.assert()
97+
.failure()
98+
.stderr_eq(str![[r#"
99+
Error: No changes to commit message.
100+
101+
"#]]);
102+
103+
Ok(())
104+
}
105+
106+
#[test]
107+
fn describe_nonexistent_target_fails() -> anyhow::Result<()> {
108+
let env = Sandbox::init_scenario_with_target_and_default_settings("one-stack")?;
109+
insta::assert_snapshot!(env.git_log()?, @r"
110+
* edd3eb7 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
111+
* 9477ae7 (A) add A
112+
* 0dc3733 (origin/main, origin/HEAD, main) add M
113+
");
114+
115+
setup_metadata(&env, &["A"])?;
116+
117+
// Try to describe a nonexistent target
118+
env.but("describe nonexistent -m 'new message'")
119+
.assert()
120+
.failure()
121+
.stderr_eq(str![[r#"
122+
Error: ID 'nonexistent' not found
123+
124+
"#]]);
125+
126+
Ok(())
127+
}
128+
129+
#[test]
130+
fn describe_commit_with_multiline_message() -> anyhow::Result<()> {
131+
let env = Sandbox::init_scenario_with_target_and_default_settings("one-stack")?;
132+
insta::assert_snapshot!(env.git_log()?, @r"
133+
* edd3eb7 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
134+
* 9477ae7 (A) add A
135+
* 0dc3733 (origin/main, origin/HEAD, main) add M
136+
");
137+
138+
setup_metadata(&env, &["A"])?;
139+
140+
// Use describe with multiline message
141+
env.but("describe 9477ae7 -m 'First line\n\nSecond paragraph with details'")
142+
.assert()
143+
.success()
144+
.stdout_eq(str![[r#"
145+
Updated commit message for [..] (now [..])
146+
147+
"#]]);
148+
149+
// Verify the commit message was updated with multiline content
150+
let log = env.git_log()?;
151+
assert!(log.contains("First line"));
152+
153+
Ok(())
154+
}

crates/but/tests/but/command/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
mod branch;
44
#[cfg(feature = "legacy")]
55
mod commit;
6+
#[cfg(feature = "legacy")]
7+
mod describe;
68
mod format;
79
mod help;
810
#[cfg(feature = "legacy")]

0 commit comments

Comments
 (0)