Skip to content

Commit 56e25fc

Browse files
authored
Merge pull request #11405 from gitbutlerapp/but-describe
Allow inline message for but describe
2 parents 75bad90 + bed33cc commit 56e25fc

File tree

6 files changed

+171
-15
lines changed

6 files changed

+171
-15
lines changed

.github/copilot-instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,3 +347,4 @@ corepack prepare pnpm@10.17.0 --activate
347347
8. **Test on target platform** if making platform-specific changes
348348
9. **Security**: Check dependencies for vulnerabilities before adding them
349349
10. **Code marked for refactoring**: Be extra careful with crates in the "Code Hitlist" section
350+
11. **but CLI happy path testing only**: CLI tests are expensive and should be limited to what really matters.

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: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
use anyhow::{Result, bail};
22
use but_ctx::Context;
3-
use but_oxidize::{ObjectIdExt, OidExt};
3+
use but_oxidize::{ObjectIdExt as _, OidExt};
44
use gitbutler_project::Project;
5+
use gix::prelude::ObjectIdExt;
56

67
use crate::{legacy::id::CliId, tui, utils::OutputChannel};
78

89
pub(crate) fn describe_target(
910
project: &Project,
1011
out: &mut OutputChannel,
1112
target: &str,
13+
message: Option<&str>,
1214
) -> Result<()> {
1315
let mut ctx = Context::new_from_legacy_project(project.clone())?;
1416

@@ -31,10 +33,10 @@ pub(crate) fn describe_target(
3133

3234
match cli_id {
3335
CliId::Branch { name, .. } => {
34-
edit_branch_name(&ctx, project, name, out)?;
36+
edit_branch_name(&ctx, project, name, out, message)?;
3537
}
3638
CliId::Commit { oid } => {
37-
edit_commit_message_by_id(&ctx, project, *oid, out)?;
39+
edit_commit_message_by_id(&ctx, project, *oid, out, message)?;
3840
}
3941
_ => {
4042
bail!("Target must be a commit ID, not {}", cli_id.kind());
@@ -49,6 +51,7 @@ fn edit_branch_name(
4951
project: &Project,
5052
branch_name: &str,
5153
out: &mut OutputChannel,
54+
message: Option<&str>,
5255
) -> Result<()> {
5356
// Find which stack this branch belongs to
5457
let stacks = but_api::legacy::workspace::stacks(
@@ -62,7 +65,8 @@ fn edit_branch_name(
6265
}
6366

6467
if let Some(sid) = stack_entry.id {
65-
let new_name = get_branch_name_from_editor(branch_name)?;
68+
let new_name = prepare_provided_message(message, "branch name")
69+
.unwrap_or_else(|| get_branch_name_from_editor(branch_name))?;
6670
but_api::legacy::stack::update_branch_name(
6771
project.id,
6872
sid,
@@ -79,11 +83,22 @@ fn edit_branch_name(
7983
bail!("Branch '{}' not found in any stack", branch_name)
8084
}
8185

86+
fn prepare_provided_message(msg: Option<&str>, entity: &str) -> Option<Result<String>> {
87+
msg.map(|msg| {
88+
let trimmed = msg.trim();
89+
if trimmed.is_empty() {
90+
bail!("Aborting due to empty {entity}");
91+
}
92+
Ok(trimmed.to_string())
93+
})
94+
}
95+
8296
fn edit_commit_message_by_id(
8397
ctx: &Context,
8498
project: &Project,
8599
commit_oid: gix::ObjectId,
86100
out: &mut OutputChannel,
101+
message: Option<&str>,
87102
) -> Result<()> {
88103
// Find which stack this commit belongs to
89104
let stacks = but_api::legacy::workspace::stacks(project.id, None)?;
@@ -132,18 +147,23 @@ fn edit_commit_message_by_id(
132147
let stack_id = stack_id
133148
.ok_or_else(|| anyhow::anyhow!("Could not find stack for commit {}", commit_oid))?;
134149

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-
139150
// Get current commit message
140151
let current_message = commit_message.to_string();
141152

142-
// Open editor with current message and file list
143-
let new_message = get_commit_message_from_editor(&current_message, &changed_files)?;
153+
// Get new message from provided argument or editor
154+
let new_message = prepare_provided_message(message, "commit message").unwrap_or_else(|| {
155+
let commit_details = but_api::legacy::diff::commit_details(project.id, commit_oid.into())?;
156+
let changed_files = get_changed_files_from_commit_details(&commit_details);
157+
158+
// Open editor with current message and file list
159+
get_commit_message_from_editor(&current_message, &changed_files)
160+
})?;
144161

145162
if new_message.trim() == current_message.trim() {
146-
bail!("No changes to commit message.");
163+
if let Some(out) = out.for_human() {
164+
writeln!(out, "No changes to commit message - nothing to be done")?;
165+
}
166+
return Ok(());
147167
}
148168

149169
// Use gitbutler_branch_actions::update_commit_message instead of low-level primitives
@@ -156,11 +176,12 @@ fn edit_commit_message_by_id(
156176
)?;
157177

158178
if let Some(out) = out.for_human() {
179+
let repo = ctx.repo.get()?;
159180
writeln!(
160181
out,
161182
"Updated commit message for {} (now {})",
162-
commit_oid.to_hex_with_len(7),
163-
new_commit_oid.to_gix().to_hex_with_len(7)
183+
commit_oid.attach(&repo).shorten_or_id(),
184+
new_commit_oid.to_gix().attach(&repo).shorten_or_id()
164185
)?;
165186
}
166187

@@ -236,3 +257,33 @@ fn get_branch_name_from_editor(current_name: &str) -> Result<String> {
236257

237258
Ok(branch_name)
238259
}
260+
261+
#[cfg(test)]
262+
mod tests {
263+
264+
mod prepare_provided_message {
265+
use super::super::*;
266+
267+
#[test]
268+
fn empty_is_fails() {
269+
assert_eq!(
270+
prepare_provided_message(Some(""), "message")
271+
.unwrap()
272+
.unwrap_err()
273+
.to_string(),
274+
"Aborting due to empty message"
275+
);
276+
}
277+
278+
#[test]
279+
fn empty_is_after_trimming_fails() {
280+
assert_eq!(
281+
prepare_provided_message(Some(" "), "message")
282+
.unwrap()
283+
.unwrap_err()
284+
.to_string(),
285+
"Aborting due to empty message"
286+
);
287+
}
288+
}
289+
}

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: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
use crate::utils::{Sandbox, setup_metadata};
2+
use gitbutler_commit::commit_ext::CommitExt;
3+
use snapbox::str;
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+
insta::assert_snapshot!(env.git_log()?, @r"
26+
* d84f3c4 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
27+
* 2f7c570 (A) Updated commit message
28+
* 0dc3733 (origin/main, origin/HEAD, main, gitbutler/target) add M
29+
");
30+
31+
Ok(())
32+
}
33+
34+
#[test]
35+
fn describe_commit_with_multiline_message() -> anyhow::Result<()> {
36+
let env = Sandbox::init_scenario_with_target_and_default_settings("one-stack")?;
37+
insta::assert_snapshot!(env.git_log()?, @r"
38+
* edd3eb7 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
39+
* 9477ae7 (A) add A
40+
* 0dc3733 (origin/main, origin/HEAD, main) add M
41+
");
42+
43+
setup_metadata(&env, &["A"])?;
44+
45+
// Use describe with multiline message
46+
env.but("describe 9477ae7 -m 'First line\n\n\tSecond paragraph with details'")
47+
.assert()
48+
.success()
49+
.stdout_eq(str![[r#"
50+
Updated commit message for [..] (now [..])
51+
52+
"#]]);
53+
54+
// Verify the commit message was updated with multiline content
55+
insta::assert_snapshot!(env.git_log()?, @r"
56+
* f2c2b50 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
57+
* cdf2c74 (A) First line
58+
* 0dc3733 (origin/main, origin/HEAD, main, gitbutler/target) add M
59+
");
60+
61+
let repo = env.open_repo()?;
62+
assert_eq!(
63+
repo.rev_parse_single(":/First line")?
64+
.object()?
65+
.into_commit()
66+
.message_bstr(),
67+
"First line\n\n\tSecond paragraph with details"
68+
);
69+
70+
Ok(())
71+
}
72+
73+
// Note: Branch rename test is omitted because the test scenario uses single-character
74+
// branch names ("A") which don't meet the 2-character minimum requirement for CLI IDs.
75+
// The branch rename functionality with -m flag is tested manually and works correctly.
76+
77+
#[test]
78+
fn describe_commit_with_same_message_succeeds_as_noop() -> anyhow::Result<()> {
79+
let env = Sandbox::init_scenario_with_target_and_default_settings("one-stack")?;
80+
insta::assert_snapshot!(env.git_log()?, @r"
81+
* edd3eb7 (HEAD -> gitbutler/workspace) GitButler Workspace Commit
82+
* 9477ae7 (A) add A
83+
* 0dc3733 (origin/main, origin/HEAD, main) add M
84+
");
85+
86+
setup_metadata(&env, &["A"])?;
87+
88+
// Try to describe with the same message
89+
env.but("describe 9477ae7 -m 'add A'")
90+
.assert()
91+
.success()
92+
.stdout_eq(str![[r#"
93+
No changes to commit message - nothing to be done
94+
95+
"#]]);
96+
97+
Ok(())
98+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
//! Put command-specific tests here. They should be focused on what's important for each command.
22
//! Ideally they *show* the initial state, and the *post* state, to validate the commands actually do what they claim.
3+
//! **Only** test the *happy path* of a typical user journey, while keeping details in unit tests with private module access.
34
mod branch;
45
#[cfg(feature = "legacy")]
56
mod commit;
7+
#[cfg(feature = "legacy")]
8+
mod describe;
69
mod format;
710
mod help;
811
#[cfg(feature = "legacy")]

0 commit comments

Comments
 (0)