just playing with tangled
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}