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::collections::HashSet;
16use std::io::Write as _;
17
18use clap_complete::ArgValueCandidates;
19use itertools::Itertools as _;
20use jj_lib::backend::CommitId;
21use jj_lib::repo::Repo as _;
22use jj_lib::rewrite::merge_commit_trees;
23use jj_lib::rewrite::rebase_commit;
24use tracing::instrument;
25
26use crate::cli_util::compute_commit_location;
27use crate::cli_util::CommandHelper;
28use crate::cli_util::RevisionArg;
29use crate::command_error::CommandError;
30use crate::complete;
31use crate::description_util::join_message_paragraphs;
32use crate::ui::Ui;
33
34/// Create a new, empty change and (by default) edit it in the working copy
35///
36/// By default, `jj` will edit the new change, making the [working copy]
37/// represent the new commit. This can be avoided with `--no-edit`.
38///
39/// Note that you can create a merge commit by specifying multiple revisions as
40/// argument. For example, `jj new @ main` will create a new commit with the
41/// working copy and the `main` bookmark as parents.
42///
43/// [working copy]:
44/// https://jj-vcs.github.io/jj/latest/working-copy/
45#[derive(clap::Args, Clone, Debug)]
46pub(crate) struct NewArgs {
47 /// Parent(s) of the new change
48 #[arg(
49 default_value = "@",
50 value_name = "REVSETS",
51 add = ArgValueCandidates::new(complete::all_revisions)
52 )]
53 revisions: Option<Vec<RevisionArg>>,
54 /// Ignored (but lets you pass `-d`/`-r` for consistency with other
55 /// commands)
56 #[arg(short = 'd', hide = true, short_alias = 'r', action = clap::ArgAction::Count)]
57 unused_destination: u8,
58 /// The change description to use
59 #[arg(long = "message", short, value_name = "MESSAGE")]
60 message_paragraphs: Vec<String>,
61 /// Do not edit the newly created change
62 #[arg(long, conflicts_with = "_edit")]
63 no_edit: bool,
64 /// No-op flag to pair with --no-edit
65 #[arg(long, hide = true)]
66 _edit: bool,
67 /// Insert the new change after the given commit(s)
68 #[arg(
69 long,
70 short = 'A',
71 visible_alias = "after",
72 conflicts_with = "revisions",
73 value_name = "REVSETS",
74 add = ArgValueCandidates::new(complete::all_revisions),
75 )]
76 insert_after: Option<Vec<RevisionArg>>,
77 /// Insert the new change before the given commit(s)
78 #[arg(
79 long,
80 short = 'B',
81 visible_alias = "before",
82 conflicts_with = "revisions",
83 value_name = "REVSETS",
84 add = ArgValueCandidates::new(complete::mutable_revisions),
85 )]
86 insert_before: Option<Vec<RevisionArg>>,
87}
88
89#[instrument(skip_all)]
90pub(crate) fn cmd_new(
91 ui: &mut Ui,
92 command: &CommandHelper,
93 args: &NewArgs,
94) -> Result<(), CommandError> {
95 let mut workspace_command = command.workspace_helper(ui)?;
96
97 let (parent_commit_ids, child_commit_ids) = compute_commit_location(
98 ui,
99 &workspace_command,
100 // HACK: `args.revisions` will always have a value due to the `default_value`, however
101 // `compute_commit_location` requires that the `destination` argument is mutually exclusive
102 // to `insert_after` and `insert_before` arguments.
103 if args.insert_before.is_some() || args.insert_after.is_some() {
104 None
105 } else {
106 args.revisions.as_deref()
107 },
108 args.insert_after.as_deref(),
109 args.insert_before.as_deref(),
110 "new commit",
111 )?;
112 let parent_commits: Vec<_> = parent_commit_ids
113 .iter()
114 .map(|commit_id| workspace_command.repo().store().get_commit(commit_id))
115 .try_collect()?;
116 let mut advance_bookmarks_target = None;
117 let mut advanceable_bookmarks = vec![];
118
119 if args.insert_before.is_none() && args.insert_after.is_none() {
120 let should_advance_bookmarks = parent_commits.len() == 1;
121 if should_advance_bookmarks {
122 advance_bookmarks_target = Some(parent_commit_ids[0].clone());
123 advanceable_bookmarks =
124 workspace_command.get_advanceable_bookmarks(parent_commits[0].parent_ids())?;
125 }
126 };
127
128 let parent_commit_ids_set: HashSet<CommitId> = parent_commit_ids.iter().cloned().collect();
129
130 let mut tx = workspace_command.start_transaction();
131 let merged_tree = merge_commit_trees(tx.repo(), &parent_commits)?;
132 let new_commit = tx
133 .repo_mut()
134 .new_commit(parent_commit_ids, merged_tree.id())
135 .set_description(join_message_paragraphs(&args.message_paragraphs))
136 .write()?;
137
138 let child_commits: Vec<_> = child_commit_ids
139 .iter()
140 .map(|commit_id| tx.repo().store().get_commit(commit_id))
141 .try_collect()?;
142 let mut num_rebased = 0;
143 for child_commit in child_commits {
144 let new_parent_ids = child_commit
145 .parent_ids()
146 .iter()
147 .filter(|id| !parent_commit_ids_set.contains(id))
148 .cloned()
149 .chain(std::iter::once(new_commit.id().clone()))
150 .collect_vec();
151 rebase_commit(tx.repo_mut(), child_commit, new_parent_ids)?;
152 num_rebased += 1;
153 }
154 num_rebased += tx.repo_mut().rebase_descendants()?;
155
156 if args.no_edit {
157 if let Some(mut formatter) = ui.status_formatter() {
158 write!(formatter, "Created new commit ")?;
159 tx.write_commit_summary(formatter.as_mut(), &new_commit)?;
160 writeln!(formatter)?;
161 }
162 } else {
163 tx.edit(&new_commit)?;
164 // The description of the new commit will be printed by tx.finish()
165 }
166 if num_rebased > 0 {
167 writeln!(ui.status(), "Rebased {num_rebased} descendant commits")?;
168 }
169
170 // Does nothing if there's no bookmarks to advance.
171 if let Some(target) = advance_bookmarks_target {
172 tx.advance_bookmarks(advanceable_bookmarks, &target);
173 }
174
175 tx.finish(ui, "new empty commit")?;
176 Ok(())
177}