12
12
// See the License for the specific language governing permissions and
13
13
// limitations under the License.
14
14
15
+ use std:: collections:: HashMap ;
16
+
15
17
use clap_complete:: ArgValueCandidates ;
16
18
use clap_complete:: ArgValueCompleter ;
17
19
use indoc:: formatdoc;
@@ -23,12 +25,15 @@ use jj_lib::object_id::ObjectId as _;
23
25
use jj_lib:: repo:: Repo as _;
24
26
use jj_lib:: rewrite;
25
27
use jj_lib:: rewrite:: CommitWithSelection ;
28
+ use jj_lib:: rewrite:: merge_commit_trees;
29
+ use pollster:: FutureExt as _;
26
30
use tracing:: instrument;
27
31
28
32
use crate :: cli_util:: CommandHelper ;
29
33
use crate :: cli_util:: DiffSelector ;
30
34
use crate :: cli_util:: RevisionArg ;
31
35
use crate :: cli_util:: WorkspaceCommandTransaction ;
36
+ use crate :: cli_util:: compute_commit_location;
32
37
use crate :: command_error:: CommandError ;
33
38
use crate :: command_error:: user_error;
34
39
use crate :: command_error:: user_error_with_hint;
@@ -72,6 +77,7 @@ pub(crate) struct SquashArgs {
72
77
add = ArgValueCompleter :: new( complete:: revset_expression_mutable) ,
73
78
) ]
74
79
revision : Option < RevisionArg > ,
80
+
75
81
/// Revision(s) to squash from (default: @)
76
82
#[ arg(
77
83
long, short,
@@ -80,6 +86,7 @@ pub(crate) struct SquashArgs {
80
86
add = ArgValueCompleter :: new( complete:: revset_expression_mutable) ,
81
87
) ]
82
88
from : Vec < RevisionArg > ,
89
+
83
90
/// Revision to squash into (default: @)
84
91
#[ arg(
85
92
long, short = 't' ,
@@ -89,30 +96,76 @@ pub(crate) struct SquashArgs {
89
96
add = ArgValueCompleter :: new( complete:: revset_expression_mutable) ,
90
97
) ]
91
98
into : Option < RevisionArg > ,
99
+
100
+ /// The revision(s) to use as parent for the new commit (can be repeated
101
+ /// to create a merge commit)
102
+ #[ arg(
103
+ long,
104
+ short,
105
+ conflicts_with = "into" ,
106
+ conflicts_with = "revision" ,
107
+ value_name = "REVSETS" ,
108
+ add = ArgValueCompleter :: new( complete:: revset_expression_all) ,
109
+ ) ]
110
+ destination : Option < Vec < RevisionArg > > ,
111
+
112
+ /// The revision(s) to insert the new commit after (can be repeated to
113
+ /// create a merge commit)
114
+ #[ arg(
115
+ long,
116
+ short = 'A' ,
117
+ visible_alias = "after" ,
118
+ conflicts_with = "destination" ,
119
+ conflicts_with = "into" ,
120
+ conflicts_with = "revision" ,
121
+ value_name = "REVSETS" ,
122
+ add = ArgValueCompleter :: new( complete:: revset_expression_all) ,
123
+ ) ]
124
+ insert_after : Option < Vec < RevisionArg > > ,
125
+
126
+ /// The revision(s) to insert the new commit before (can be repeated to
127
+ /// create a merge commit)
128
+ #[ arg(
129
+ long,
130
+ short = 'B' ,
131
+ visible_alias = "before" ,
132
+ conflicts_with = "destination" ,
133
+ conflicts_with = "into" ,
134
+ conflicts_with = "revision" ,
135
+ value_name = "REVSETS" ,
136
+ add = ArgValueCompleter :: new( complete:: revset_expression_mutable) ,
137
+ ) ]
138
+ insert_before : Option < Vec < RevisionArg > > ,
139
+
92
140
/// The description to use for squashed revision (don't open editor)
93
141
#[ arg( long = "message" , short, value_name = "MESSAGE" ) ]
94
142
message_paragraphs : Vec < String > ,
143
+
95
144
/// Use the description of the destination revision and discard the
96
145
/// description(s) of the source revision(s)
97
146
#[ arg( long, short, conflicts_with = "message_paragraphs" ) ]
98
147
use_destination_message : bool ,
148
+
99
149
/// Interactively choose which parts to squash
100
150
#[ arg( long, short) ]
101
151
interactive : bool ,
152
+
102
153
/// Specify diff editor to be used (implies --interactive)
103
154
#[ arg(
104
155
long,
105
156
value_name = "NAME" ,
106
157
add = ArgValueCandidates :: new( complete:: diff_editors) ,
107
158
) ]
108
159
tool : Option < String > ,
160
+
109
161
/// Move only changes to these paths (instead of all paths)
110
162
#[ arg(
111
163
value_name = "FILESETS" ,
112
164
value_hint = clap:: ValueHint :: AnyPath ,
113
165
add = ArgValueCompleter :: new( complete:: squash_revision_files) ,
114
166
) ]
115
167
paths : Vec < String > ,
168
+
116
169
/// The source revision will not be abandoned
117
170
#[ arg( long, short) ]
118
171
keep_emptied : bool ,
@@ -124,22 +177,31 @@ pub(crate) fn cmd_squash(
124
177
command : & CommandHelper ,
125
178
args : & SquashArgs ,
126
179
) -> Result < ( ) , CommandError > {
180
+ let insert_destination_commit =
181
+ args. destination . is_some ( ) || args. insert_after . is_some ( ) || args. insert_before . is_some ( ) ;
182
+
127
183
let mut workspace_command = command. workspace_helper ( ui) ?;
128
184
129
185
let mut sources: Vec < Commit > ;
130
- let destination;
131
- if !args. from . is_empty ( ) || args. into . is_some ( ) {
186
+ let pre_existing_destination;
187
+
188
+ if !args. from . is_empty ( ) || args. into . is_some ( ) || insert_destination_commit {
132
189
sources = if args. from . is_empty ( ) {
133
190
workspace_command. parse_revset ( ui, & RevisionArg :: AT ) ?
134
191
} else {
135
192
workspace_command. parse_union_revsets ( ui, & args. from ) ?
136
193
}
137
194
. evaluate_to_commits ( ) ?
138
195
. try_collect ( ) ?;
139
- destination = workspace_command
140
- . resolve_single_rev ( ui, args. into . as_ref ( ) . unwrap_or ( & RevisionArg :: AT ) ) ?;
141
- // remove the destination from the sources
142
- sources. retain ( |source| source. id ( ) != destination. id ( ) ) ;
196
+ if insert_destination_commit {
197
+ pre_existing_destination = None ;
198
+ } else {
199
+ let destination = workspace_command
200
+ . resolve_single_rev ( ui, args. into . as_ref ( ) . unwrap_or ( & RevisionArg :: AT ) ) ?;
201
+ // remove the destination from the sources
202
+ sources. retain ( |source| source. id ( ) != destination. id ( ) ) ;
203
+ pre_existing_destination = Some ( destination) ;
204
+ }
143
205
// Reverse the set so we apply the oldest commits first. It shouldn't affect the
144
206
// result, but it avoids creating transient conflicts and is therefore probably
145
207
// a little faster.
@@ -155,21 +217,83 @@ pub(crate) fn cmd_squash(
155
217
) ) ;
156
218
}
157
219
sources = vec ! [ source] ;
158
- destination = parents. pop ( ) . unwrap ( ) ;
159
- }
220
+ pre_existing_destination = Some ( parents. pop ( ) . unwrap ( ) ) ;
221
+ } ;
160
222
161
- let matcher = workspace_command
223
+ workspace_command. check_rewritable ( sources. iter ( ) . chain ( & pre_existing_destination) . ids ( ) ) ?;
224
+
225
+ // prepare the tx description before possibly rebasing the source commits
226
+ let source_ids: Vec < _ > = sources. iter ( ) . ids ( ) . collect ( ) ;
227
+ let tx_description = if let Some ( destination) = & pre_existing_destination {
228
+ format ! ( "squash commits into {}" , destination. id( ) . hex( ) )
229
+ } else {
230
+ match & source_ids[ ..] {
231
+ [ ] => format ! ( "squash {} commits" , source_ids. len( ) ) ,
232
+ [ id] => format ! ( "squash commit {}" , id. hex( ) ) ,
233
+ [ first, others @ ..] => {
234
+ format ! ( "squash commit {} and {} more" , first. hex( ) , others. len( ) )
235
+ }
236
+ }
237
+ } ;
238
+
239
+ let mut tx = workspace_command. start_transaction ( ) ;
240
+ let mut num_rebased = 0 ;
241
+ let destination = if let Some ( commit) = pre_existing_destination {
242
+ commit
243
+ } else {
244
+ // create the new destination commit
245
+ let ( parent_ids, child_ids) = compute_commit_location (
246
+ ui,
247
+ tx. base_workspace_helper ( ) ,
248
+ args. destination . as_deref ( ) ,
249
+ args. insert_after . as_deref ( ) ,
250
+ args. insert_before . as_deref ( ) ,
251
+ "squashed commit" ,
252
+ ) ?;
253
+ let parent_commits: Vec < _ > = parent_ids
254
+ . iter ( )
255
+ . map ( |commit_id| {
256
+ tx. base_workspace_helper ( )
257
+ . repo ( )
258
+ . store ( )
259
+ . get_commit ( commit_id)
260
+ } )
261
+ . try_collect ( ) ?;
262
+ let merged_tree = merge_commit_trees ( tx. repo ( ) , & parent_commits) . block_on ( ) ?;
263
+ let commit = tx
264
+ . repo_mut ( )
265
+ . new_commit ( parent_ids. clone ( ) , merged_tree. id ( ) )
266
+ . write ( ) ?;
267
+ let mut rewritten = HashMap :: new ( ) ;
268
+ tx. repo_mut ( )
269
+ . transform_descendants ( child_ids, async |mut rewriter| {
270
+ let old_commit_id = rewriter. old_commit ( ) . id ( ) . clone ( ) ;
271
+ for parent_id in & parent_ids {
272
+ rewriter. replace_parent ( parent_id, [ commit. id ( ) ] ) ;
273
+ }
274
+ let new_commit = rewriter. rebase ( ) . await ?. write ( ) ?;
275
+ rewritten. insert ( old_commit_id, new_commit) ;
276
+ num_rebased += 1 ;
277
+ Ok ( ( ) )
278
+ } ) ?;
279
+ for source in & mut * sources {
280
+ if let Some ( rewritten_source) = rewritten. remove ( source. id ( ) ) {
281
+ * source = rewritten_source;
282
+ }
283
+ }
284
+ commit
285
+ } ;
286
+
287
+ let matcher = tx
288
+ . base_workspace_helper ( )
162
289
. parse_file_patterns ( ui, & args. paths ) ?
163
290
. to_matcher ( ) ;
164
291
let diff_selector =
165
- workspace_command. diff_selector ( ui, args. tool . as_deref ( ) , args. interactive ) ?;
166
- let text_editor = workspace_command. text_editor ( ) ?;
292
+ tx. base_workspace_helper ( )
293
+ . diff_selector ( ui, args. tool . as_deref ( ) , args. interactive ) ?;
294
+ let text_editor = tx. base_workspace_helper ( ) . text_editor ( ) ?;
167
295
let description = SquashedDescription :: from_args ( args) ;
168
- workspace_command
169
- . check_rewritable ( sources. iter ( ) . chain ( std:: iter:: once ( & destination) ) . ids ( ) ) ?;
170
296
171
- let mut tx = workspace_command. start_transaction ( ) ;
172
- let tx_description = format ! ( "squash commits into {}" , destination. id( ) . hex( ) ) ;
173
297
let source_commits = select_diff ( & tx, & sources, & destination, & matcher, & diff_selector) ?;
174
298
if let Some ( squashed) = rewrite:: squash_commits (
175
299
tx. repo_mut ( ) ,
@@ -209,7 +333,7 @@ pub(crate) fn cmd_squash(
209
333
ui,
210
334
& tx,
211
335
abandoned_commits,
212
- & destination,
336
+ ( !insert_destination_commit ) . then_some ( & destination) ,
213
337
& commit_builder,
214
338
) ?;
215
339
// It's weird that commit.description() contains "JJ: " lines, but works.
@@ -222,12 +346,45 @@ pub(crate) fn cmd_squash(
222
346
}
223
347
} ;
224
348
commit_builder. set_description ( new_description) ;
225
- commit_builder. write ( tx. repo_mut ( ) ) ?;
349
+ if insert_destination_commit {
350
+ // forget about the intermediate commit
351
+ commit_builder. set_predecessors (
352
+ commit_builder
353
+ . predecessors ( )
354
+ . iter ( )
355
+ . filter ( |p| p != & destination. id ( ) )
356
+ . cloned ( )
357
+ . collect ( ) ,
358
+ ) ;
359
+ }
360
+ let commit = commit_builder. write ( tx. repo_mut ( ) ) ?;
361
+ let num_rebased = tx. repo_mut ( ) . rebase_descendants ( ) ?;
362
+ if let Some ( mut formatter) = ui. status_formatter ( ) {
363
+ if insert_destination_commit {
364
+ write ! ( formatter, "Created new commit " ) ?;
365
+ tx. write_commit_summary ( formatter. as_mut ( ) , & commit) ?;
366
+ writeln ! ( formatter) ?;
367
+ }
368
+ if num_rebased > 0 {
369
+ writeln ! ( formatter, "Rebased {num_rebased} descendant commits" ) ?;
370
+ }
371
+ }
226
372
} else {
227
373
if diff_selector. is_interactive ( ) {
228
374
return Err ( user_error ( "No changes selected" ) ) ;
229
375
}
230
376
377
+ if let Some ( mut formatter) = ui. status_formatter ( ) {
378
+ if insert_destination_commit {
379
+ write ! ( formatter, "Created new commit " ) ?;
380
+ tx. write_commit_summary ( formatter. as_mut ( ) , & destination) ?;
381
+ writeln ! ( formatter) ?;
382
+ }
383
+ if num_rebased > 0 {
384
+ writeln ! ( formatter, "Rebased {num_rebased} descendant commits" ) ?;
385
+ }
386
+ }
387
+
231
388
if let [ only_path] = & * args. paths {
232
389
let no_rev_arg = args. revision . is_none ( ) && args. from . is_empty ( ) && args. into . is_none ( ) ;
233
390
if no_rev_arg
0 commit comments