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+ merge_commit_changes:: 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,69 @@ 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`.
122+ /// This operation assumes the provided editor is already normalized and up to
123+ /// date. Callers chaining previous editor mutations should first run
124+ /// `editor.rebase()?.into_editor()` before squashing.
141125///
142- /// After reordering and squashing, the resulting squashed commit has:
143- /// - The tree of the commit that is top-most after reordering.
126+ /// After squashing, the resulting squashed commit has:
127+ /// - The tree produced from the target commit's full tree plus the subject
128+ /// commits' own change ranges.
144129/// - A message determined by `how_to_combine_messages`:
145130/// - `KeepTarget`: target message only.
146131/// - `KeepSubject`: subject messages only.
147132/// - `KeepBoth`: target message followed by subject messages.
148133///
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.
134+ /// Subject messages are appended in the order they are provided, with at least
135+ /// one blank line between non-empty message blocks.
151136///
152137pub fn squash_commits < ' ws , ' meta , M : RefMetadata , S : ToCommitSelector , T : ToCommitSelector > (
153138 editor : Editor < ' ws , ' meta , M > ,
154139 subjects : Vec < S > ,
155140 target_commit : T ,
156141 how_to_combine_messages : MessageCombinationStrategy ,
157142) -> Result < SquashCommitsOutcome < ' ws , ' meta , M > > {
143+ let mut seen_subjects = std:: collections:: HashSet :: with_capacity ( subjects. len ( ) ) ;
144+
158145 if subjects. is_empty ( ) {
159146 bail ! ( "Need at least 2 commits to squash" )
160147 }
161148
162149 let ( target_commit_selector, target_commit_obj) =
163150 editor. find_selectable_commit ( target_commit) ?;
164151
165- let mut all_commits = Vec :: with_capacity ( subjects. len ( ) + 1 ) ;
166- all_commits. push ( target_commit_selector) ;
152+ let mut subject_selectors = Vec :: with_capacity ( subjects. len ( ) ) ;
167153 for subject_commit in subjects {
168154 let ( subject_commit_selector, _) = editor. find_selectable_commit ( subject_commit) ?;
169155 if subject_commit_selector == target_commit_selector {
170156 bail ! ( "Cannot squash a commit into itself" )
171157 }
172- all_commits. push ( subject_commit_selector) ;
158+ if !seen_subjects. insert ( subject_commit_selector) {
159+ continue ;
160+ }
161+ subject_selectors. push ( subject_commit_selector) ;
173162 }
174163
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- }
164+ let subject_commit_ids = subject_selectors
165+ . iter ( )
166+ . map ( |commit_selector| {
167+ let ( _, commit) = editor. find_selectable_commit ( * commit_selector) ?;
168+ Ok ( commit. id )
169+ } )
170+ . collect :: < Result < Vec < _ > > > ( ) ?;
171+ let squashed_tree = editor. merge_commit_changes_to_tree (
172+ target_commit_obj. id ,
173+ subject_commit_ids,
174+ editor. repo ( ) . merge_options_force_ours ( ) ?,
175+ ) ?;
176+ if squashed_tree. conflict . is_some ( ) {
177+ bail ! ( "Cannot squash commits that would result in merge conflicts" ) ;
191178 }
192179
193180 let mut combined_message = Vec :: new ( ) ;
194181 match how_to_combine_messages {
195182 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- {
183+ for source_id in subject_selectors. iter ( ) . copied ( ) {
202184 let ( _, source_commit) = editor. find_selectable_commit ( source_id) ?;
203185 push_message_with_spacing ( & mut combined_message, source_commit. message . as_ref ( ) ) ;
204186 }
@@ -208,37 +190,34 @@ pub fn squash_commits<'ws, 'meta, M: RefMetadata, S: ToCommitSelector, T: ToComm
208190 }
209191 MessageCombinationStrategy :: KeepBoth => {
210192 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- {
193+ for source_id in subject_selectors. iter ( ) . copied ( ) {
217194 let ( _, source_commit) = editor. find_selectable_commit ( source_id) ?;
218195 push_message_with_spacing ( & mut combined_message, source_commit. message . as_ref ( ) ) ;
219196 }
220197 }
221198 }
222199
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 ;
200+ let mut editor = editor;
201+ for commit_selector in subject_selectors {
202+ let delimiter = SegmentDelimiter {
203+ child : commit_selector,
204+ parent : commit_selector,
235205 } ;
206+ editor. disconnect_segment_from ( delimiter, SelectorSet :: All , SelectorSet :: All , false ) ?;
207+ let ( selector, _) = editor. find_selectable_commit ( commit_selector) ?;
236208 editor. replace ( selector, Step :: None ) ?;
237209 }
238210
211+ let ( editor, new_target_selector) = construct_new_squashed_commit (
212+ editor,
213+ squashed_tree,
214+ target_commit_selector,
215+ combined_message,
216+ ) ?;
217+
239218 Ok ( SquashCommitsOutcome {
240219 rebase : editor. rebase ( ) ?,
241- commit_selector : bottom_most_selector ,
220+ commit_selector : new_target_selector ,
242221 } )
243222}
244223
0 commit comments