From 42b2937d5eb9bcb53714c3897739ce09db487c61 Mon Sep 17 00:00:00 2001 From: Martin von Zweigbergk Date: Mon, 27 Jun 2022 17:36:13 -0700 Subject: [PATCH] cli: add `jj edit` for editing a commit in the wokring copy --- CHANGELOG.md | 4 ++- lib/src/repo.rs | 2 -- src/commands.rs | 28 +++++++++++++++ tests/test_edit_command.rs | 70 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 tests/test_edit_command.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bccbbd12..fe4c6d260 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * The [`$NO_COLOR` environment variable](https://no-color.org/) no longer overrides the `ui.color` configuration if explicitly set. -* `jj edit` has been renamed to `jj touchup`. +* `jj edit` has been renamed to `jj touchup`, and `jj edit` is now a new command + with different behavior. The new `jj edit` lets you edit a commit in the + working copy, even if the specified commit is closed. * `jj git push` no longer aborts if you attempt to push an open commit (but it now aborts if a commit does not have a description). diff --git a/lib/src/repo.rs b/lib/src/repo.rs index d930f1b0c..c3c4cd0dc 100644 --- a/lib/src/repo.rs +++ b/lib/src/repo.rs @@ -597,7 +597,6 @@ impl MutableRepo { } pub fn edit(&mut self, workspace_id: WorkspaceId, commit: &Commit) { - assert!(commit.is_open(), "commit to edit is closed"); self.leave_commit(&workspace_id); self.set_checkout(workspace_id, commit.id().clone()); } @@ -606,7 +605,6 @@ impl MutableRepo { let maybe_current_checkout_id = self.view.borrow().get_checkout(workspace_id).cloned(); if let Some(current_checkout_id) = maybe_current_checkout_id { let current_checkout = self.store().get_commit(¤t_checkout_id).unwrap(); - assert!(current_checkout.is_open(), "current checkout is closed"); if current_checkout.is_empty() && current_checkout.description().is_empty() && self.view().heads().contains(current_checkout.id()) diff --git a/src/commands.rs b/src/commands.rs index b27d86184..37e0de738 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1127,6 +1127,7 @@ enum Commands { Open(OpenArgs), Duplicate(DuplicateArgs), Abandon(AbandonArgs), + Edit(EditArgs), New(NewArgs), Move(MoveArgs), Squash(SquashArgs), @@ -1401,6 +1402,16 @@ struct AbandonArgs { revisions: Vec, } +/// Edit a commit in the working copy +/// +/// Puts the contents of a commit in the working copy for editing. Any changes +/// you make in the working copy will update (amend) the commit. +#[derive(clap::Args, Clone, Debug)] +struct EditArgs { + /// The commit to edit + revision: String, +} + /// Create a new, empty change and check it out /// /// This may be useful if you want to make some changes you're unsure of on top @@ -3439,6 +3450,22 @@ fn cmd_abandon( Ok(()) } +fn cmd_edit(ui: &mut Ui, command: &CommandHelper, args: &EditArgs) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let new_commit = workspace_command.resolve_single_rev(ui, &args.revision)?; + let workspace_id = workspace_command.workspace_id(); + if workspace_command.repo().view().get_checkout(&workspace_id) == Some(new_commit.id()) { + ui.write("Already editing that commit\n")?; + } else { + workspace_command.commit_working_copy(ui)?; + let mut tx = + workspace_command.start_transaction(&format!("edit commit {}", new_commit.id().hex())); + tx.mut_repo().edit(workspace_id, &new_commit); + workspace_command.finish_transaction(ui, tx)?; + } + Ok(()) +} + fn cmd_new(ui: &mut Ui, command: &CommandHelper, args: &NewArgs) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; let parent = workspace_command.resolve_single_rev(ui, &args.revision)?; @@ -5294,6 +5321,7 @@ where Commands::Open(sub_args) => cmd_open(ui, &command_helper, sub_args), Commands::Duplicate(sub_args) => cmd_duplicate(ui, &command_helper, sub_args), Commands::Abandon(sub_args) => cmd_abandon(ui, &command_helper, sub_args), + Commands::Edit(sub_args) => cmd_edit(ui, &command_helper, sub_args), Commands::New(sub_args) => cmd_new(ui, &command_helper, sub_args), Commands::Move(sub_args) => cmd_move(ui, &command_helper, sub_args), Commands::Squash(sub_args) => cmd_squash(ui, &command_helper, sub_args), diff --git a/tests/test_edit_command.rs b/tests/test_edit_command.rs new file mode 100644 index 000000000..dc06640d3 --- /dev/null +++ b/tests/test_edit_command.rs @@ -0,0 +1,70 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::path::Path; + +use crate::common::TestEnvironment; + +pub mod common; + +#[test] +fn test_edit() { + 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"); + std::fs::write(repo_path.join("file1"), "0").unwrap(); + test_env.jj_cmd_success(&repo_path, &["close", "-m", "first"]); + test_env.jj_cmd_success(&repo_path, &["describe", "-m", "second"]); + std::fs::write(repo_path.join("file1"), "1").unwrap(); + + // Errors out without argument + test_env.jj_cmd_cli_error(&repo_path, &["edit"]); + + // Can edit a closed commit + let stdout = test_env.jj_cmd_success(&repo_path, &["edit", "@-"]); + insta::assert_snapshot!(stdout, @r###" + Working copy now at: 5c9d6c787f29 first + Added 0 files, modified 1 files, removed 0 files + "###); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" + o 37ed5225d0fd open second + @ 5c9d6c787f29 closed first + o 000000000000 closed (no description set) + "###); + insta::assert_snapshot!(read_file(&repo_path.join("file1")), @"0"); + + // Changes in the working copy are amended into the commit + std::fs::write(repo_path.join("file2"), "0").unwrap(); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" + Rebased 1 descendant commits onto updated working copy + o 57e61f6b2ce1 open second + @ f1b9706b17d0 closed first + o 000000000000 closed (no description set) + "###); +} + +fn read_file(path: &Path) -> String { + String::from_utf8(std::fs::read(path).unwrap()).unwrap() +} + +fn get_log_output(test_env: &TestEnvironment, cwd: &Path) -> String { + test_env.jj_cmd_success( + cwd, + &[ + "log", + "-T", + r#"commit_id.short() " " if(open, "open", "closed") " " description"#, + ], + ) +}