11//! An action to squash multiple commits into a target commit.
22
33use anyhow:: { Result , bail} ;
4- use but_core:: RefMetadata ;
4+ use but_core:: { RefMetadata , RepositoryExt } ;
55use but_rebase:: {
66 commit:: DateMode ,
77 graph_rebase:: {
8- Editor , Selector , Step , SuccessfulRebase , ToCommitSelector , mutate:: InsertSide ,
8+ Editor , LookupStep as _, Selector , Step , SuccessfulRebase , ToCommitSelector ,
9+ commit:: MergeCommitChangesOutcome ,
10+ mutate:: { SegmentDelimiter , SelectorSet } ,
911 } ,
1012} ;
1113
@@ -43,73 +45,56 @@ fn push_message_with_spacing(combined: &mut Vec<u8>, message: &[u8]) {
4345 combined. extend_from_slice ( message) ;
4446}
4547
46- /// Reorder commits around `target_commit` so all selected commits become
47- /// adjacent around the target in parentage order.
48- ///
49- /// Returns the rewritten editor together with the original below/above anchor
50- /// commit IDs used for remapping after the first rebase.
51- fn reorder_commits_around_target < ' ws , ' meta , M : RefMetadata > (
52- mut editor : Editor < ' ws , ' meta , M > ,
53- ordered_all_commits : & [ Selector ] ,
54- target_commit : Selector ,
55- ) -> Result < ( Editor < ' ws , ' meta , M > , Selector , Selector ) > {
56- let target_pos = ordered_all_commits
57- . iter ( )
58- . position ( |id| * id == target_commit)
59- . expect ( "target commit must be in ordered commit list" ) ;
60-
61- let ( below_commits, target_and_above_commits) = ordered_all_commits. split_at ( target_pos) ;
62-
63- let mut below_anchor = target_commit;
64- for source_id in below_commits. iter ( ) . rev ( ) . copied ( ) {
65- editor = crate :: commit:: move_commit_no_rebase (
66- editor,
67- source_id,
68- below_anchor,
69- InsertSide :: Below ,
70- ) ?;
71- below_anchor = source_id;
72- }
73-
74- let mut above_anchor = target_commit;
75- for source_id in target_and_above_commits. iter ( ) . skip ( 1 ) . copied ( ) {
76- editor = crate :: commit:: move_commit_no_rebase (
77- editor,
78- source_id,
79- above_anchor,
80- InsertSide :: Above ,
81- ) ?;
82- above_anchor = source_id;
83- }
84-
85- Ok ( ( editor, below_anchor, above_anchor) )
86- }
87-
88- /// Build the squashed commit from the mapped top/bottom commits and replace the
89- /// bottom selector with the newly created commit.
48+ /// Build the squashed commit and replace the target selector with the newly
49+ /// created commit.
9050///
9151/// Returns the updated editor and the selector that now points to the squashed
9252/// commit.
9353fn construct_new_squashed_commit < ' ws , ' meta , M : RefMetadata > (
9454 mut editor : Editor < ' ws , ' meta , M > ,
95- top_most_commit_id : Selector ,
96- bottom_most_commit_id : Selector ,
55+ squashed_tree : MergeCommitChangesOutcome ,
56+ target_commit_id : Selector ,
9757 combined_message : Vec < u8 > ,
9858) -> Result < ( Editor < ' ws , ' meta , M > , Selector ) > {
99- let ( _, top_most_commit) = editor. find_selectable_commit ( top_most_commit_id) ?;
100- let ( bottom_most_selector, bottom_most_commit) =
101- editor. find_selectable_commit ( bottom_most_commit_id) ?;
59+ let ( target_selector, target_commit) = editor. find_selectable_commit ( target_commit_id) ?;
60+ let target_parent_ids = parent_commit_ids ( & editor, target_selector) ?;
10261
10362 let new_commit = {
104- let mut squashed_commit = bottom_most_commit. clone ( ) ;
105- squashed_commit. tree = top_most_commit. tree ;
63+ let mut squashed_commit = target_commit. clone ( ) ;
64+ squashed_commit. inner . parents = target_parent_ids. into ( ) ;
65+ squashed_commit. tree = squashed_tree. tree_id ;
10666 squashed_commit. message = combined_message. into ( ) ;
10767 editor. new_commit ( squashed_commit, DateMode :: CommitterUpdateAuthorKeep ) ?
10868 } ;
10969
110- editor. replace ( bottom_most_selector , Step :: new_pick ( new_commit) ) ?;
70+ editor. replace ( target_selector , Step :: new_pick ( new_commit) ) ?;
11171
112- Ok ( ( editor, bottom_most_selector) )
72+ Ok ( ( editor, target_selector) )
73+ }
74+
75+ fn parent_commit_ids < M : RefMetadata > (
76+ editor : & Editor < ' _ , ' _ , M > ,
77+ selector : Selector ,
78+ ) -> Result < Vec < gix:: ObjectId > > {
79+ let mut parents = editor. direct_parents ( selector) ?;
80+ parents. sort_by_key ( |( _, order) | * order) ;
81+
82+ parents
83+ . into_iter ( )
84+ . map ( |( parent_selector, _) | match editor. lookup_step ( parent_selector) ? {
85+ Step :: Pick ( _) => {
86+ let ( _, commit) = editor. find_selectable_commit ( parent_selector) ?;
87+ Ok ( commit. id )
88+ }
89+ Step :: Reference { .. } => {
90+ let ( _, commit) = editor. find_reference_target ( parent_selector) ?;
91+ Ok ( commit. id )
92+ }
93+ Step :: None => bail ! (
94+ "BUG: expected parent selector {parent_selector:?} to point to a pick or reference"
95+ ) ,
96+ } )
97+ . collect ( )
11398}
11499
115100/// How to combine messages of commits being squashed.
@@ -133,72 +118,67 @@ but_schemars::register_sdk_type!(MessageCombinationStrategy);
133118
134119/// Squash `subjects` into `target_commit`.
135120///
136- /// `subjects` may be provided in any order. They are ordered by
137- /// parentage internally together with `target_commit` before reordering and
138- /// squashing.
139- ///
140121/// The `target_commit` must not also appear in `subjects`.
141122///
142- /// After reordering and squashing, the resulting squashed commit has:
143- /// - The tree of the commit that is top-most after reordering .
123+ /// After squashing, the resulting squashed commit has:
124+ /// - The tree produced by merging the selected commits together .
144125/// - A message determined by `how_to_combine_messages`:
145126/// - `KeepTarget`: target message only.
146127/// - `KeepSubject`: subject messages only.
147128/// - `KeepBoth`: target message followed by subject messages.
148129///
149- /// Subject messages are appended in squash order (top-most first after
150- /// reordering), with at least one blank line between non-empty message blocks.
130+ /// Subject messages are appended in the order they are provided, with at least
131+ /// one blank line between non-empty message blocks.
151132///
152133pub fn squash_commits < ' ws , ' meta , M : RefMetadata , S : ToCommitSelector , T : ToCommitSelector > (
153134 editor : Editor < ' ws , ' meta , M > ,
154135 subjects : Vec < S > ,
155136 target_commit : T ,
156137 how_to_combine_messages : MessageCombinationStrategy ,
157138) -> Result < SquashCommitsOutcome < ' ws , ' meta , M > > {
139+ let mut seen_subjects = std:: collections:: HashSet :: with_capacity ( subjects. len ( ) ) ;
140+
158141 if subjects. is_empty ( ) {
159142 bail ! ( "Need at least 2 commits to squash" )
160143 }
161144
162145 let ( target_commit_selector, target_commit_obj) =
163146 editor. find_selectable_commit ( target_commit) ?;
164147
165- let mut all_commits = Vec :: with_capacity ( subjects. len ( ) + 1 ) ;
166- all_commits. push ( target_commit_selector) ;
148+ let mut subject_selectors = Vec :: with_capacity ( subjects. len ( ) ) ;
167149 for subject_commit in subjects {
168150 let ( subject_commit_selector, _) = editor. find_selectable_commit ( subject_commit) ?;
169151 if subject_commit_selector == target_commit_selector {
170152 bail ! ( "Cannot squash a commit into itself" )
171153 }
172- all_commits. push ( subject_commit_selector) ;
154+ if !seen_subjects. insert ( subject_commit_selector) {
155+ continue ;
156+ }
157+ subject_selectors. push ( subject_commit_selector) ;
173158 }
174159
175- let ordered_selectors = editor . order_commit_selectors_by_parentage ( all_commits ) ? ;
176-
177- let ( editor , below_anchor , above_anchor ) =
178- reorder_commits_around_target ( editor, & ordered_selectors , target_commit_selector ) ?;
179-
180- let rebase = editor . rebase ( ) ? ;
181- let editor = rebase . into_editor ( ) ;
182-
183- for commit_selector in & ordered_selectors {
184- let ( _ , commit ) = editor . find_selectable_commit ( * commit_selector ) ? ;
185- if commit . clone ( ) . attach ( editor . repo ( ) ) . is_conflicted ( ) {
186- bail ! (
187- "Commit {} became conflicted after reordering. Can't continue with squash." ,
188- commit . id
189- ) ;
190- }
160+ let mut commits_to_merge = Vec :: with_capacity ( subject_selectors . len ( ) + 1 ) ;
161+ commits_to_merge . push ( target_commit_selector ) ;
162+ commits_to_merge . extend ( subject_selectors . iter ( ) . copied ( ) ) ;
163+ let commits_to_merge = editor. order_commit_selectors_by_parentage ( commits_to_merge ) ?;
164+ let squashed_tree = editor . merge_commit_changes_to_tree (
165+ commits_to_merge
166+ . iter ( )
167+ . map ( |commit_selector| {
168+ let ( _ , commit ) = editor . find_selectable_commit ( * commit_selector ) ? ;
169+ Ok ( commit . id )
170+ } )
171+ . collect :: < Result < Vec < _ > > > ( ) ? ,
172+ editor . repo ( ) . merge_options_force_ours ( ) ? ,
173+ ) ? ;
174+ if squashed_tree . conflict . is_some ( ) {
175+ bail ! ( "Cannot squash commits that would result in merge conflicts" ) ;
191176 }
192177
193178 let mut combined_message = Vec :: new ( ) ;
194179 match how_to_combine_messages {
195180 MessageCombinationStrategy :: KeepSubject => {
196- for source_id in ordered_selectors
197- . iter ( )
198- . rev ( )
199- . copied ( )
200- . filter ( |commit_selector| * commit_selector != target_commit_selector)
201- {
181+ for source_id in subject_selectors. iter ( ) . copied ( ) {
202182 let ( _, source_commit) = editor. find_selectable_commit ( source_id) ?;
203183 push_message_with_spacing ( & mut combined_message, source_commit. message . as_ref ( ) ) ;
204184 }
@@ -208,37 +188,34 @@ pub fn squash_commits<'ws, 'meta, M: RefMetadata, S: ToCommitSelector, T: ToComm
208188 }
209189 MessageCombinationStrategy :: KeepBoth => {
210190 push_message_with_spacing ( & mut combined_message, target_commit_obj. message . as_ref ( ) ) ;
211- for source_id in ordered_selectors
212- . iter ( )
213- . rev ( )
214- . copied ( )
215- . filter ( |commit_selector| * commit_selector != target_commit_selector)
216- {
191+ for source_id in subject_selectors. iter ( ) . copied ( ) {
217192 let ( _, source_commit) = editor. find_selectable_commit ( source_id) ?;
218193 push_message_with_spacing ( & mut combined_message, source_commit. message . as_ref ( ) ) ;
219194 }
220195 }
221196 }
222197
223- let ( editor, bottom_most_selector) =
224- construct_new_squashed_commit ( editor, above_anchor, below_anchor, combined_message) ?;
225-
226- let rebase = editor. rebase ( ) ?;
227- let mut editor = rebase. into_editor ( ) ;
228-
229- for commit_selector in ordered_selectors {
230- if commit_selector == below_anchor {
231- continue ;
232- }
233- let Ok ( ( selector, _) ) = editor. find_selectable_commit ( commit_selector) else {
234- continue ;
198+ let mut editor = editor;
199+ for commit_selector in subject_selectors {
200+ let delimiter = SegmentDelimiter {
201+ child : commit_selector,
202+ parent : commit_selector,
235203 } ;
204+ editor. disconnect_segment_from ( delimiter, SelectorSet :: All , SelectorSet :: All , false ) ?;
205+ let ( selector, _) = editor. find_selectable_commit ( commit_selector) ?;
236206 editor. replace ( selector, Step :: None ) ?;
237207 }
238208
209+ let ( editor, new_target_selector) = construct_new_squashed_commit (
210+ editor,
211+ squashed_tree,
212+ target_commit_selector,
213+ combined_message,
214+ ) ?;
215+
239216 Ok ( SquashCommitsOutcome {
240217 rebase : editor. rebase ( ) ?,
241- commit_selector : bottom_most_selector ,
218+ commit_selector : new_target_selector ,
242219 } )
243220}
244221
0 commit comments