Skip to content

Commit

Permalink
describe: allow updating the description of multiple commits
Browse files Browse the repository at this point in the history
If multiple commits are provided, the description of each commit
will be opened for editing in a new editor.
  • Loading branch information
bnjmnt4n committed Jun 4, 2024
1 parent fcc0b86 commit 088ea1a
Show file tree
Hide file tree
Showing 4 changed files with 317 additions and 39 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* Upgraded `scm-record` from v0.2.0 to v0.3.0. See release notes at https://github.com/arxanas/scm-record/releases/tag/v0.3.0

* `jj describe` can now update the description of multiple commits.

### Fixed bugs

* Previously, `jj git push` only made sure that the branch is in the expected
Expand Down
129 changes: 100 additions & 29 deletions cli/src/commands/describe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::io::{self, Read, Write};
use std::io::{self, Read};

use indexmap::IndexMap;
use itertools::Itertools;
use jj_lib::backend::CommitId;
use jj_lib::commit::CommitIteratorExt;
use jj_lib::object_id::ObjectId;
use tracing::instrument;

use crate::cli_util::{CommandHelper, RevisionArg};
use crate::command_error::CommandError;
use crate::command_error::{user_error, CommandError};
use crate::description_util::{
description_template_for_describe, edit_description, join_message_paragraphs,
};
Expand All @@ -31,16 +35,22 @@ use crate::ui::Ui;
#[derive(clap::Args, Clone, Debug)]
#[command(visible_aliases = &["desc"])]
pub(crate) struct DescribeArgs {
/// The revision whose description to edit
/// The revision(s) whose description to edit
#[arg(default_value = "@")]
revision: RevisionArg,
revisions: Vec<RevisionArg>,
/// Ignored (but lets you pass `-r` for consistency with other commands)
#[arg(short = 'r', hide = true)]
unused_revision: bool,
#[arg(short = 'r', hide = true, action = clap::ArgAction::Count)]
unused_revision: u8,
/// The change description to use (don't open editor)
///
/// If multiple revisions are specified, the same description will be used
/// for all of them.
#[arg(long = "message", short, value_name = "MESSAGE")]
message_paragraphs: Vec<String>,
/// Read the change description from stdin
///
/// If multiple revisions are specified, the same description will be used
/// for all of them.
#[arg(long)]
stdin: bool,
/// Don't open an editor
Expand All @@ -67,35 +77,96 @@ pub(crate) fn cmd_describe(
args: &DescribeArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let commit = workspace_command.resolve_single_rev(&args.revision)?;
workspace_command.check_rewritable([commit.id()])?;
let description = if args.stdin {
let commits = workspace_command
.resolve_some_revsets_default_single(&args.revisions)?
.into_iter()
.collect_vec();
if commits.is_empty() {
return Err(user_error("Empty revision set"));
}
workspace_command.check_rewritable(commits.iter().ids())?;
let shared_description = if args.stdin {
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer).unwrap();
buffer
Some(buffer)
} else if !args.message_paragraphs.is_empty() {
join_message_paragraphs(&args.message_paragraphs)
} else if args.no_edit {
commit.description().to_owned()
Some(join_message_paragraphs(&args.message_paragraphs))
} else {
let template =
description_template_for_describe(ui, command.settings(), &workspace_command, &commit)?;
edit_description(workspace_command.repo(), &template, command.settings())?
None
};
if description == *commit.description() && !args.reset_author {
writeln!(ui.status(), "Nothing changed.")?;

// TODO: Allow editing description of multiple commits in a single editor?
let commit_descriptions: IndexMap<_, _> = commits
.iter()
.rev()
.map(
|commit| -> Result<Option<(&CommitId, String)>, CommandError> {
let original_description = commit.description();
let new_description = if args.no_edit {
commit.description().to_owned()
} else if let Some(shared_description) = &shared_description {
shared_description.to_owned()
} else {
let template = description_template_for_describe(
ui,
command.settings(),
&workspace_command,
commit,
)?;
edit_description(workspace_command.repo(), &template, command.settings())?
};
if new_description == *original_description && !args.reset_author {
Ok(None)
} else {
Ok(Some((commit.id(), new_description.to_owned())))
}
},
)
.filter_map_ok(|x| x)
.try_collect()?;

let mut tx = workspace_command.start_transaction();
let tx_description = if commits.len() == 1 {
format!("describe commit {}", commits[0].id().hex())
} else {
let mut tx = workspace_command.start_transaction();
let mut commit_builder = tx
.mut_repo()
.rewrite_commit(command.settings(), &commit)
.set_description(description);
if args.reset_author {
let new_author = commit_builder.committer().clone();
commit_builder = commit_builder.set_author(new_author);
}
commit_builder.write()?;
tx.finish(ui, format!("describe commit {}", commit.id().hex()))?;
format!(
"describe commit {} and {} more",
commits[0].id().hex(),
commits.len() - 1
)
};

let mut num_described = 0;
let mut num_rebased = 0;
tx.mut_repo().transform_descendants(
command.settings(),
commit_descriptions
.keys()
.map(|&id| id.clone())
.collect_vec(),
|rewriter| {
let old_commit_id = rewriter.old_commit().id().clone();
let mut commit_builder = rewriter.rebase(command.settings())?;
if let Some(description) = commit_descriptions.get(&old_commit_id) {
commit_builder = commit_builder.set_description(description);
if args.reset_author {
let new_author = commit_builder.committer().clone();
commit_builder = commit_builder.set_author(new_author);
}
num_described += 1;
} else {
num_rebased += 1;
}
commit_builder.write()?;
Ok(())
},
)?;
if num_described > 1 {
writeln!(ui.status(), "Updated {} commits", num_described)?;
}
if num_rebased > 0 {
writeln!(ui.status(), "Rebased {} descendant commits", num_rebased)?;
}
tx.finish(ui, tx_description)?;
Ok(())
}
7 changes: 2 additions & 5 deletions cli/tests/[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -600,20 +600,17 @@ Update the change description or other metadata
Starts an editor to let you edit the description of a change. The editor will be $EDITOR, or `pico` if that's not defined (`Notepad` on Windows).
**Usage:** `jj describe [OPTIONS] [REVISION]`
**Usage:** `jj describe [OPTIONS] [REVISIONS]...`
###### **Arguments:**
* `<REVISION>` — The revision whose description to edit
* `<REVISIONS>` — The revision(s) whose description to edit
Default value: `@`
###### **Options:**
* `-r` — Ignored (but lets you pass `-r` for consistency with other commands)
Possible values: `true`, `false`
* `-m`, `--message <MESSAGE>` — The change description to use (don't open editor)
* `--stdin` — Read the change description from stdin
Expand Down
Loading

0 comments on commit 088ea1a

Please sign in to comment.