just playing with tangled
at gvimdiff 6.3 kB view raw
1// Copyright 2020 The Jujutsu Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15use std::io::Write as _; 16 17use clap_complete::ArgValueCandidates; 18use itertools::Itertools as _; 19use jj_lib::matchers::EverythingMatcher; 20use jj_lib::object_id::ObjectId as _; 21use jj_lib::rewrite::merge_commit_trees; 22use tracing::instrument; 23 24use crate::cli_util::CommandHelper; 25use crate::cli_util::RevisionArg; 26use crate::command_error::CommandError; 27use crate::complete; 28use crate::ui::Ui; 29 30/// Touch up the content changes in a revision with a diff editor 31/// 32/// With the `-r` option, which is the default, starts a [diff editor] on the 33/// changes in the revision. 34/// 35/// With the `--from` and/or `--to` options, starts a [diff editor] comparing 36/// the "from" revision to the "to" revision. 37/// 38/// [diff editor]: 39/// https://jj-vcs.github.io/jj/latest/config/#editing-diffs 40/// 41/// Edit the right side of the diff until it looks the way you want. Once you 42/// close the editor, the revision specified with `-r` or `--to` will be 43/// updated. Unless `--restore-descendants` is used, descendants will be 44/// rebased on top as usual, which may result in conflicts. 45/// 46/// See `jj restore` if you want to move entire files from one revision to 47/// another. For moving changes between revisions, see `jj squash -i`. 48#[derive(clap::Args, Clone, Debug)] 49pub(crate) struct DiffeditArgs { 50 /// The revision to touch up 51 /// 52 /// Defaults to @ if neither --to nor --from are specified. 53 #[arg( 54 long, 55 short, 56 value_name = "REVSET", 57 add = ArgValueCandidates::new(complete::mutable_revisions) 58 )] 59 revision: Option<RevisionArg>, 60 /// Show changes from this revision 61 /// 62 /// Defaults to @ if --to is specified. 63 #[arg( 64 long, short, 65 conflicts_with = "revision", 66 value_name = "REVSET", 67 add = ArgValueCandidates::new(complete::all_revisions), 68 )] 69 from: Option<RevisionArg>, 70 /// Edit changes in this revision 71 /// 72 /// Defaults to @ if --from is specified. 73 #[arg( 74 long, short, 75 conflicts_with = "revision", 76 value_name = "REVSET", 77 add = ArgValueCandidates::new(complete::mutable_revisions), 78 )] 79 to: Option<RevisionArg>, 80 /// Specify diff editor to be used 81 #[arg(long, value_name = "NAME")] 82 tool: Option<String>, 83 /// Preserve the content (not the diff) when rebasing descendants 84 /// 85 /// When rebasing a descendant on top of the rewritten revision, its diff 86 /// compared to its parent(s) is normally preserved, i.e. the same way that 87 /// descendants are always rebased. This flag makes it so the content/state 88 /// is preserved instead of preserving the diff. 89 #[arg(long)] 90 restore_descendants: bool, 91} 92 93#[instrument(skip_all)] 94pub(crate) fn cmd_diffedit( 95 ui: &mut Ui, 96 command: &CommandHelper, 97 args: &DiffeditArgs, 98) -> Result<(), CommandError> { 99 let mut workspace_command = command.workspace_helper(ui)?; 100 101 let (target_commit, base_commits, diff_description); 102 if args.from.is_some() || args.to.is_some() { 103 target_commit = workspace_command 104 .resolve_single_rev(ui, args.to.as_ref().unwrap_or(&RevisionArg::AT))?; 105 base_commits = vec![workspace_command 106 .resolve_single_rev(ui, args.from.as_ref().unwrap_or(&RevisionArg::AT))?]; 107 diff_description = format!( 108 "The diff initially shows the commit's changes relative to:\n{}", 109 workspace_command.format_commit_summary(&base_commits[0]) 110 ); 111 } else { 112 target_commit = workspace_command 113 .resolve_single_rev(ui, args.revision.as_ref().unwrap_or(&RevisionArg::AT))?; 114 base_commits = target_commit.parents().try_collect()?; 115 diff_description = "The diff initially shows the commit's changes.".to_string(); 116 }; 117 workspace_command.check_rewritable([target_commit.id()])?; 118 119 let diff_editor = workspace_command.diff_editor(ui, args.tool.as_deref())?; 120 let mut tx = workspace_command.start_transaction(); 121 let format_instructions = || { 122 format!( 123 "\ 124You are editing changes in: {} 125 126{diff_description} 127 128Adjust the right side until it shows the contents you want. If you 129don't make any changes, then the operation will be aborted.", 130 tx.format_commit_summary(&target_commit), 131 ) 132 }; 133 let base_tree = merge_commit_trees(tx.repo(), base_commits.as_slice())?; 134 let tree = target_commit.tree()?; 135 let tree_id = diff_editor.edit(&base_tree, &tree, &EverythingMatcher, format_instructions)?; 136 if tree_id == *target_commit.tree_id() { 137 writeln!(ui.status(), "Nothing changed.")?; 138 } else { 139 let new_commit = tx 140 .repo_mut() 141 .rewrite_commit(&target_commit) 142 .set_tree_id(tree_id) 143 .write()?; 144 // rebase_descendants early; otherwise `new_commit` would always have 145 // a conflicted change id at this point. 146 let (num_rebased, extra_msg) = if args.restore_descendants { 147 ( 148 tx.repo_mut().reparent_descendants()?, 149 " (while preserving their content)", 150 ) 151 } else { 152 (tx.repo_mut().rebase_descendants()?, "") 153 }; 154 if let Some(mut formatter) = ui.status_formatter() { 155 write!(formatter, "Created ")?; 156 tx.write_commit_summary(formatter.as_mut(), &new_commit)?; 157 writeln!(formatter)?; 158 if num_rebased > 0 { 159 writeln!( 160 formatter, 161 "Rebased {num_rebased} descendant commits{extra_msg}" 162 )?; 163 } 164 } 165 tx.finish(ui, format!("edit commit {}", target_commit.id().hex()))?; 166 } 167 Ok(()) 168}