forked from mirrors/jj
cli: Allow repeated -m
options for multi-paragraph descriptions
Emulates git's behavior: https://www.git-scm.com/docs/git-commit#Documentation/git-commit.txt--mltmsggt
This commit is contained in:
parent
c572c1a331
commit
062f7a252b
4 changed files with 107 additions and 48 deletions
|
@ -55,6 +55,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
* Added the `ui.paginate` option to enable/disable pager usage in commands
|
||||
|
||||
* `jj checkout`/`jj describe`/`jj commit`/`jj new`/`jj squash` can take repeated
|
||||
`-m/--message` arguments. Each passed message will be combined into paragraphs
|
||||
(separated by a blank line)
|
||||
|
||||
### Fixed bugs
|
||||
|
||||
* SSH authentication could hang when ssh-agent couldn't be reached
|
||||
|
|
|
@ -2292,35 +2292,17 @@ pub struct EarlyArgs {
|
|||
pub config_toml: Vec<String>,
|
||||
}
|
||||
|
||||
/// `-m/--message` argument which should be terminated with `\n`.
|
||||
/// Create a description from a list of paragraphs.
|
||||
///
|
||||
/// Based on the Git CLI behavior. See `opt_parse_m()` and `cleanup_mode` in
|
||||
/// `git/builtin/commit.c`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct DescriptionArg(String);
|
||||
|
||||
impl DescriptionArg {
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for DescriptionArg {
|
||||
fn from(s: String) -> Self {
|
||||
DescriptionArg(text_util::complete_newline(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&DescriptionArg> for String {
|
||||
fn from(arg: &DescriptionArg) -> Self {
|
||||
arg.0.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for DescriptionArg {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
pub fn join_message_paragraphs(paragraphs: &[String]) -> String {
|
||||
// Ensure each paragraph ends with a newline, then add another newline between
|
||||
// paragraphs.
|
||||
paragraphs
|
||||
.iter()
|
||||
.map(|p| text_util::complete_newline(p.as_str()))
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
|
@ -56,11 +56,11 @@ use maplit::{hashmap, hashset};
|
|||
use tracing::instrument;
|
||||
|
||||
use crate::cli_util::{
|
||||
check_stale_working_copy, get_new_config_file_path, print_checkout_stats,
|
||||
self, check_stale_working_copy, get_new_config_file_path, print_checkout_stats,
|
||||
resolve_multiple_nonempty_revsets, resolve_multiple_nonempty_revsets_default_single,
|
||||
run_ui_editor, serialize_config_value, short_commit_hash, user_error, user_error_with_hint,
|
||||
write_config_value_to_file, Args, CommandError, CommandHelper, DescriptionArg,
|
||||
LogContentFormat, RevisionArg, WorkspaceCommandHelper,
|
||||
write_config_value_to_file, Args, CommandError, CommandHelper, LogContentFormat, RevisionArg,
|
||||
WorkspaceCommandHelper,
|
||||
};
|
||||
use crate::config::{AnnotatedValue, ConfigSource};
|
||||
use crate::diff_util::{self, DiffFormat, DiffFormatArgs};
|
||||
|
@ -263,8 +263,8 @@ struct CheckoutArgs {
|
|||
#[arg(short = 'r', hide = true)]
|
||||
unused_revision: bool,
|
||||
/// The change description to use
|
||||
#[arg(long, short, default_value = "")]
|
||||
message: DescriptionArg,
|
||||
#[arg(long = "message", short, value_name = "MESSAGE")]
|
||||
message_paragraphs: Vec<String>,
|
||||
}
|
||||
|
||||
/// Stop tracking specified paths in the working copy
|
||||
|
@ -448,8 +448,8 @@ struct DescribeArgs {
|
|||
#[arg(short = 'r', hide = true)]
|
||||
unused_revision: bool,
|
||||
/// The change description to use (don't open editor)
|
||||
#[arg(long, short)]
|
||||
message: Option<DescriptionArg>,
|
||||
#[arg(long = "message", short, value_name = "MESSAGE")]
|
||||
message_paragraphs: Vec<String>,
|
||||
/// Read the change description from stdin
|
||||
#[arg(long)]
|
||||
stdin: bool,
|
||||
|
@ -470,8 +470,8 @@ struct DescribeArgs {
|
|||
#[command(visible_aliases=&["ci"])]
|
||||
struct CommitArgs {
|
||||
/// The change description to use (don't open editor)
|
||||
#[arg(long, short)]
|
||||
message: Option<DescriptionArg>,
|
||||
#[arg(long = "message", short, value_name = "MESSAGE")]
|
||||
message_paragraphs: Vec<String>,
|
||||
}
|
||||
|
||||
/// Create a new change with the same content as an existing one
|
||||
|
@ -534,8 +534,8 @@ struct NewArgs {
|
|||
#[arg(short = 'r', hide = true)]
|
||||
unused_revision: bool,
|
||||
/// The change description to use
|
||||
#[arg(long, short, default_value = "")]
|
||||
message: DescriptionArg,
|
||||
#[arg(long = "message", short, value_name = "MESSAGE")]
|
||||
message_paragraphs: Vec<String>,
|
||||
/// Deprecated. Please prefix the revset with `all:` instead.
|
||||
#[arg(long, short = 'L', hide = true)]
|
||||
allow_large_revsets: bool,
|
||||
|
@ -592,8 +592,8 @@ struct SquashArgs {
|
|||
#[arg(long, short, default_value = "@")]
|
||||
revision: RevisionArg,
|
||||
/// The description to use for squashed revision (don't open editor)
|
||||
#[arg(long, short)]
|
||||
message: Option<DescriptionArg>,
|
||||
#[arg(long = "message", short, value_name = "MESSAGE")]
|
||||
message_paragraphs: Vec<String>,
|
||||
/// Interactively choose which parts to squash
|
||||
#[arg(long, short)]
|
||||
interactive: bool,
|
||||
|
@ -1302,7 +1302,7 @@ fn cmd_checkout(
|
|||
vec![target.id().clone()],
|
||||
target.tree_id().clone(),
|
||||
)
|
||||
.set_description(&args.message);
|
||||
.set_description(cli_util::join_message_paragraphs(&args.message_paragraphs));
|
||||
let new_commit = commit_builder.write()?;
|
||||
tx.edit(&new_commit).unwrap();
|
||||
tx.finish(ui)?;
|
||||
|
@ -2011,8 +2011,8 @@ fn cmd_describe(
|
|||
let mut buffer = String::new();
|
||||
io::stdin().read_to_string(&mut buffer).unwrap();
|
||||
buffer
|
||||
} else if let Some(message) = &args.message {
|
||||
message.into()
|
||||
} else if !args.message_paragraphs.is_empty() {
|
||||
cli_util::join_message_paragraphs(&args.message_paragraphs)
|
||||
} else if args.no_edit {
|
||||
commit.description().to_owned()
|
||||
} else {
|
||||
|
@ -2046,8 +2046,8 @@ fn cmd_commit(ui: &mut Ui, command: &CommandHelper, args: &CommitArgs) -> Result
|
|||
.get_wc_commit_id()
|
||||
.ok_or_else(|| user_error("This command requires a working copy"))?;
|
||||
let commit = workspace_command.repo().store().get_commit(commit_id)?;
|
||||
let description = if let Some(message) = &args.message {
|
||||
message.into()
|
||||
let description = if !args.message_paragraphs.is_empty() {
|
||||
cli_util::join_message_paragraphs(&args.message_paragraphs)
|
||||
} else {
|
||||
let template = description_template_for_commit(ui, &workspace_command, &commit)?;
|
||||
edit_description(workspace_command.repo(), &template, command.settings())?
|
||||
|
@ -2287,7 +2287,7 @@ Please use `jj new 'all:x|y'` instead of `jj new --allow-large-revsets x y`.",
|
|||
new_parents_commit_id,
|
||||
merged_tree.id().clone(),
|
||||
)
|
||||
.set_description(&args.message)
|
||||
.set_description(cli_util::join_message_paragraphs(&args.message_paragraphs))
|
||||
.write()?;
|
||||
num_rebased = target_ids.len();
|
||||
for child_commit in target_commits {
|
||||
|
@ -2307,7 +2307,7 @@ Please use `jj new 'all:x|y'` instead of `jj new --allow-large-revsets x y`.",
|
|||
target_ids.clone(),
|
||||
merged_tree.id().clone(),
|
||||
)
|
||||
.set_description(&args.message)
|
||||
.set_description(cli_util::join_message_paragraphs(&args.message_paragraphs))
|
||||
.write()?;
|
||||
if args.insert_after {
|
||||
// Each child of the targets will be rebased: its set of parents will be updated
|
||||
|
@ -2525,8 +2525,8 @@ from the source will be moved into the parent.
|
|||
// Abandon the child if the parent now has all the content from the child
|
||||
// (always the case in the non-interactive case).
|
||||
let abandon_child = &new_parent_tree_id == commit.tree_id();
|
||||
let description = if let Some(m) = &args.message {
|
||||
m.into()
|
||||
let description = if !args.message_paragraphs.is_empty() {
|
||||
cli_util::join_message_paragraphs(&args.message_paragraphs)
|
||||
} else {
|
||||
combine_messages(
|
||||
tx.base_repo(),
|
||||
|
|
|
@ -147,6 +147,79 @@ fn test_describe() {
|
|||
assert!(get_stderr_string(&assert).contains("bad-jj-editor-from-jj-editor-env"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_message_args() {
|
||||
let test_env = TestEnvironment::default();
|
||||
test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
|
||||
let repo_path = test_env.env_root().join("repo");
|
||||
|
||||
// Set a description using `-m` flag
|
||||
let stdout = test_env.jj_cmd_success(
|
||||
&repo_path,
|
||||
&[
|
||||
"describe",
|
||||
"-m",
|
||||
"First Paragraph from CLI",
|
||||
"-m",
|
||||
"Second Paragraph from CLI",
|
||||
],
|
||||
);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
Working copy now at: qpvuntsm bdee9366 (empty) First Paragraph from CLI
|
||||
Parent commit : zzzzzzzz 00000000 (empty) (no description set)
|
||||
"###);
|
||||
|
||||
let stdout =
|
||||
test_env.jj_cmd_success(&repo_path, &["log", "--no-graph", "-r@", "-Tdescription"]);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
First Paragraph from CLI
|
||||
|
||||
Second Paragraph from CLI
|
||||
"###);
|
||||
|
||||
// Set the same description, with existing newlines
|
||||
let stdout = test_env.jj_cmd_success(
|
||||
&repo_path,
|
||||
&[
|
||||
"describe",
|
||||
"-m",
|
||||
"First Paragraph from CLI\n",
|
||||
"-m",
|
||||
"Second Paragraph from CLI\n",
|
||||
],
|
||||
);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
Nothing changed.
|
||||
"###);
|
||||
|
||||
// Use an empty -m flag between paragraphs to insert an extra blank line
|
||||
let stdout = test_env.jj_cmd_success(
|
||||
&repo_path,
|
||||
&[
|
||||
"describe",
|
||||
"-m",
|
||||
"First Paragraph from CLI\n",
|
||||
"--message",
|
||||
"",
|
||||
"-m",
|
||||
"Second Paragraph from CLI",
|
||||
],
|
||||
);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
Working copy now at: qpvuntsm a7506fe0 (empty) First Paragraph from CLI
|
||||
Parent commit : zzzzzzzz 00000000 (empty) (no description set)
|
||||
"###);
|
||||
|
||||
let stdout =
|
||||
test_env.jj_cmd_success(&repo_path, &["log", "--no-graph", "-r@", "-Tdescription"]);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
First Paragraph from CLI
|
||||
|
||||
|
||||
Second Paragraph from CLI
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_describe_author() {
|
||||
let test_env = TestEnvironment::default();
|
||||
|
|
Loading…
Reference in a new issue