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 clap_complete::ArgValueCompleter;
16use jj_lib::backend::Signature;
17use jj_lib::object_id::ObjectId as _;
18use jj_lib::repo::Repo as _;
19use tracing::instrument;
20
21use crate::cli_util::CommandHelper;
22use crate::command_error::user_error;
23use crate::command_error::CommandError;
24use crate::complete;
25use crate::description_util::description_template;
26use crate::description_util::edit_description;
27use crate::description_util::join_message_paragraphs;
28use crate::text_util::parse_author;
29use crate::ui::Ui;
30
31/// Update the description and create a new change on top.
32#[derive(clap::Args, Clone, Debug)]
33pub(crate) struct CommitArgs {
34 /// Interactively choose which changes to include in the first commit
35 #[arg(short, long)]
36 interactive: bool,
37 /// Specify diff editor to be used (implies --interactive)
38 #[arg(long, value_name = "NAME")]
39 tool: Option<String>,
40 /// The change description to use (don't open editor)
41 #[arg(long = "message", short, value_name = "MESSAGE")]
42 message_paragraphs: Vec<String>,
43 /// Put these paths in the first commit
44 #[arg(
45 value_name = "FILESETS",
46 value_hint = clap::ValueHint::AnyPath,
47 add = ArgValueCompleter::new(complete::modified_files),
48 )]
49 paths: Vec<String>,
50 /// Reset the author to the configured user
51 ///
52 /// This resets the author name, email, and timestamp.
53 ///
54 /// You can use it in combination with the JJ_USER and JJ_EMAIL
55 /// environment variables to set a different author:
56 ///
57 /// $ JJ_USER='Foo Bar' JJ_EMAIL=foo@bar.com jj commit --reset-author
58 #[arg(long)]
59 reset_author: bool,
60 /// Set author to the provided string
61 ///
62 /// This changes author name and email while retaining author
63 /// timestamp for non-discardable commits.
64 #[arg(
65 long,
66 conflicts_with = "reset_author",
67 value_parser = parse_author
68 )]
69 author: Option<(String, String)>,
70}
71
72#[instrument(skip_all)]
73pub(crate) fn cmd_commit(
74 ui: &mut Ui,
75 command: &CommandHelper,
76 args: &CommitArgs,
77) -> Result<(), CommandError> {
78 let mut workspace_command = command.workspace_helper(ui)?;
79
80 let commit_id = workspace_command
81 .get_wc_commit_id()
82 .ok_or_else(|| user_error("This command requires a working copy"))?;
83 let commit = workspace_command.repo().store().get_commit(commit_id)?;
84 let matcher = workspace_command
85 .parse_file_patterns(ui, &args.paths)?
86 .to_matcher();
87 let advanceable_bookmarks = workspace_command.get_advanceable_bookmarks(commit.parent_ids())?;
88 let diff_selector =
89 workspace_command.diff_selector(ui, args.tool.as_deref(), args.interactive)?;
90 let text_editor = workspace_command.text_editor()?;
91 let mut tx = workspace_command.start_transaction();
92 let base_tree = commit.parent_tree(tx.repo())?;
93 let format_instructions = || {
94 format!(
95 "\
96You are splitting the working-copy commit: {}
97
98The diff initially shows all changes. Adjust the right side until it shows the
99contents you want for the first commit. The remainder will be included in the
100new working-copy commit.
101",
102 tx.format_commit_summary(&commit)
103 )
104 };
105 let tree_id = diff_selector.select(
106 &base_tree,
107 &commit.tree()?,
108 matcher.as_ref(),
109 format_instructions,
110 )?;
111 if !args.paths.is_empty() && tree_id == base_tree.id() {
112 writeln!(
113 ui.warning_default(),
114 "The given paths do not match any file: {}",
115 args.paths.join(" ")
116 )?;
117 }
118
119 let mut commit_builder = tx.repo_mut().rewrite_commit(&commit).detach();
120 commit_builder.set_tree_id(tree_id);
121 if args.reset_author {
122 commit_builder.set_author(commit_builder.committer().clone());
123 }
124 if let Some((name, email)) = args.author.clone() {
125 let new_author = Signature {
126 name,
127 email,
128 timestamp: commit_builder.author().timestamp,
129 };
130 commit_builder.set_author(new_author);
131 }
132
133 let description = if !args.message_paragraphs.is_empty() {
134 join_message_paragraphs(&args.message_paragraphs)
135 } else {
136 if commit_builder.description().is_empty() {
137 commit_builder.set_description(tx.settings().get_string("ui.default-description")?);
138 }
139 let temp_commit = commit_builder.write_hidden()?;
140 let template = description_template(ui, &tx, "", &temp_commit)?;
141 edit_description(&text_editor, &template)?
142 };
143 commit_builder.set_description(description);
144 let new_commit = commit_builder.write(tx.repo_mut())?;
145
146 let workspace_names = tx.repo().view().workspaces_for_wc_commit_id(commit.id());
147 if !workspace_names.is_empty() {
148 let new_wc_commit = tx
149 .repo_mut()
150 .new_commit(vec![new_commit.id().clone()], commit.tree_id().clone())
151 .write()?;
152
153 // Does nothing if there's no bookmarks to advance.
154 tx.advance_bookmarks(advanceable_bookmarks, new_commit.id());
155
156 for name in workspace_names {
157 tx.repo_mut().edit(name, &new_wc_commit).unwrap();
158 }
159 }
160 tx.finish(ui, format!("commit {}", commit.id().hex()))?;
161 Ok(())
162}