Skip to content

Commit

Permalink
backout: accept multiple revisions to back out
Browse files Browse the repository at this point in the history
Closes #3339.
  • Loading branch information
bnjmnt4n committed Jul 7, 2024
1 parent 4b0cd02 commit cd41bc3
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 30 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

* `jj rebase --skip-empty` has been renamed to `jj rebase --skip-emptied`

* `jj backout --revision` has been renamed to `jj backout --revisions`.
The short alias `-r` is still supported.

### Deprecations

### New features
Expand All @@ -33,6 +36,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
* `jj backout` now includes the backed out commit's subject in the new commit
message.

* `jj backout` can now back out multiple commits at once.

### Fixed bugs

## [0.19.0] - 2024-07-03
Expand Down
71 changes: 45 additions & 26 deletions cli/src/commands/backout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use itertools::Itertools as _;
use jj_lib::object_id::ObjectId;
use jj_lib::rewrite::merge_commit_trees;
use tracing::instrument;
Expand All @@ -23,9 +24,9 @@ use crate::ui::Ui;
/// Apply the reverse of a revision on top of another revision
#[derive(clap::Args, Clone, Debug)]
pub(crate) struct BackoutArgs {
/// The revision to apply the reverse of
/// The revision(s) to apply the reverse of
#[arg(long, short, default_value = "@")]
revision: RevisionArg,
revisions: Vec<RevisionArg>,
/// The revision to apply the reverse changes on top of
// TODO: It seems better to default this to `@-`. Maybe the working
// copy should be rebased on top?
Expand All @@ -40,36 +41,54 @@ pub(crate) fn cmd_backout(
args: &BackoutArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let commit_to_back_out = workspace_command.resolve_single_rev(&args.revision)?;
let to_back_out: Vec<_> = workspace_command
.parse_union_revsets(&args.revisions)?
.evaluate_to_commits()?
.try_collect()?; // in reverse topological order
if to_back_out.is_empty() {
writeln!(ui.status(), "No revisions to back out.")?;
return Ok(());
}
let mut parents = vec![];
for revision_str in &args.destination {
let destination = workspace_command.resolve_single_rev(revision_str)?;
parents.push(destination);
}
let mut tx = workspace_command.start_transaction();
let commit_to_back_out_subject = commit_to_back_out
.description()
.lines()
.next()
.unwrap_or_default();
let new_commit_description = format!(
"Back out \"{}\"\n\nThis backs out commit {}.\n",
commit_to_back_out_subject,
&commit_to_back_out.id().hex()
);
let old_base_tree = commit_to_back_out.parent_tree(tx.mut_repo())?;
let new_base_tree = merge_commit_trees(tx.mut_repo(), &parents)?;
let old_tree = commit_to_back_out.tree()?;
let new_tree = new_base_tree.merge(&old_tree, &old_base_tree)?;
let new_parent_ids = parents.iter().map(|commit| commit.id().clone()).collect();
tx.mut_repo()
.new_commit(command.settings(), new_parent_ids, new_tree.id())
.set_description(new_commit_description)
.write()?;
tx.finish(
ui,
format!("back out commit {}", commit_to_back_out.id().hex()),
)?;
let transaction_description = if to_back_out.len() == 1 {
format!("back out commit {}", to_back_out[0].id().hex())
} else {
format!(
"back out commit {} and {} more",
to_back_out[0].id().hex(),
to_back_out.len() - 1
)
};
let mut new_base_tree = merge_commit_trees(tx.mut_repo(), &parents)?;
for commit_to_back_out in to_back_out {
let commit_to_back_out_subject = commit_to_back_out
.description()
.lines()
.next()
.unwrap_or_default();
let new_commit_description = format!(
"Back out \"{}\"\n\nThis backs out commit {}.\n",
commit_to_back_out_subject,
&commit_to_back_out.id().hex()
);
let old_base_tree = commit_to_back_out.parent_tree(tx.mut_repo())?;
let old_tree = commit_to_back_out.tree()?;
let new_tree = new_base_tree.merge(&old_tree, &old_base_tree)?;
let new_parent_ids = parents.iter().map(|commit| commit.id().clone()).collect();
let new_commit = tx
.mut_repo()
.new_commit(command.settings(), new_parent_ids, new_tree.id())
.set_description(new_commit_description)
.write()?;
parents = vec![new_commit];
new_base_tree = new_tree;
}
tx.finish(ui, transaction_description)?;

Ok(())
}
2 changes: 1 addition & 1 deletion cli/tests/[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ Apply the reverse of a revision on top of another revision
###### **Options:**
* `-r`, `--revision <REVISION>` — The revision to apply the reverse of
* `-r`, `--revisions <REVISIONS>` — The revision(s) to apply the reverse of
Default value: `@`
* `-d`, `--destination <DESTINATION>` — The revision to apply the reverse changes on top of
Expand Down
112 changes: 109 additions & 3 deletions cli/tests/test_backout_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,23 @@ use std::path::Path;

use crate::common::TestEnvironment;

fn create_commit(test_env: &TestEnvironment, repo_path: &Path, name: &str, parents: &[&str]) {
fn create_commit(
test_env: &TestEnvironment,
repo_path: &Path,
name: &str,
parents: &[&str],
files: &[(&str, &str)],
) {
if parents.is_empty() {
test_env.jj_cmd_ok(repo_path, &["new", "root()", "-m", name]);
} else {
let mut args = vec!["new", "-m", name];
args.extend(parents);
test_env.jj_cmd_ok(repo_path, &args);
}
std::fs::write(repo_path.join(name), format!("{name}\n")).unwrap();
for (name, contents) in files {
std::fs::write(repo_path.join(name), contents).unwrap();
}
test_env.jj_cmd_ok(repo_path, &["branch", "create", name]);
}

Expand All @@ -34,7 +42,7 @@ fn test_backout() {
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");

create_commit(&test_env, &repo_path, "a", &[]);
create_commit(&test_env, &repo_path, "a", &[], &[("a", "a\n")]);
// Test the setup
insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
@ 2443ea76b0b1 a
Expand Down Expand Up @@ -82,6 +90,104 @@ fn test_backout() {
"###);
}

#[test]
fn test_backout_multiple() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");

create_commit(&test_env, &repo_path, "a", &[], &[("a", "a\n")]);
create_commit(&test_env, &repo_path, "b", &["a"], &[("a", "a\nb\n")]);
create_commit(
&test_env,
&repo_path,
"c",
&["b"],
&[("a", "a\nb\n"), ("b", "b\n")],
);
create_commit(&test_env, &repo_path, "d", &["c"], &[]);
create_commit(&test_env, &repo_path, "e", &["d"], &[("a", "a\nb\nc\n")]);

// Test the setup
insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
@ 208f8612074a e
◉ ceeec03be46b d
◉ 413337bbd11f c
◉ 46cc97af6802 b
◉ 2443ea76b0b1 a
◉ 000000000000
"###);

// Backout multiple commits
let (stdout, stderr) =
test_env.jj_cmd_ok(&repo_path, &["backout", "-r", "b", "-r", "c", "-r", "e"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @"");
insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
◉ 6504c4ded177 Back out "b"
│ This backs out commit 46cc97af6802301d8db381386e8485ff3ff24ae6.
◉ d31d42e0267f Back out "c"
│ This backs out commit 413337bbd11f7a6636c010d9e196acf801d8df2f.
◉ 8ff3fbc2ccb0 Back out "e"
│ This backs out commit 208f8612074af4c219d06568a8e1f04f2e80dc25.
@ 208f8612074a e
◉ ceeec03be46b d
◉ 413337bbd11f c
◉ 46cc97af6802 b
◉ 2443ea76b0b1 a
◉ 000000000000
"###);
// View the output of each backed out commit
let stdout = test_env.jj_cmd_success(&repo_path, &["show", "@+"]);
insta::assert_snapshot!(stdout, @r###"
Commit ID: 8ff3fbc2ccb0d66985f558c461d1643cebb4c7d6
Change ID: wqnwkozpkustnxypnnntnykwrqrkrpvv
Author: Test User <[email protected]> (2001-02-03 08:05:19)
Committer: Test User <[email protected]> (2001-02-03 08:05:19)
Back out "e"
This backs out commit 208f8612074af4c219d06568a8e1f04f2e80dc25.
Modified regular file a:
1 1: a
2 2: b
3 : c
"###);
let stdout = test_env.jj_cmd_success(&repo_path, &["show", "@++"]);
insta::assert_snapshot!(stdout, @r###"
Commit ID: d31d42e0267f6524d445348b1dd00926c62a6b57
Change ID: mouksmquosnpvwqrpsvvxtxpywpnxlss
Author: Test User <[email protected]> (2001-02-03 08:05:19)
Committer: Test User <[email protected]> (2001-02-03 08:05:19)
Back out "c"
This backs out commit 413337bbd11f7a6636c010d9e196acf801d8df2f.
Removed regular file b:
1 : b
"###);
let stdout = test_env.jj_cmd_success(&repo_path, &["show", "@+++"]);
insta::assert_snapshot!(stdout, @r###"
Commit ID: 6504c4ded177fba2334f76683d1aa643700d5073
Change ID: tqvpomtpwrqsylrpsxknultrymmqxmxv
Author: Test User <[email protected]> (2001-02-03 08:05:19)
Committer: Test User <[email protected]> (2001-02-03 08:05:19)
Back out "b"
This backs out commit 46cc97af6802301d8db381386e8485ff3ff24ae6.
Modified regular file a:
1 1: a
2 : b
"###);
}

fn get_log_output(test_env: &TestEnvironment, cwd: &Path) -> String {
let template = r#"commit_id.short() ++ " " ++ description"#;
test_env.jj_cmd_success(cwd, &["log", "-T", template])
Expand Down

0 comments on commit cd41bc3

Please sign in to comment.