@@ -33,13 +33,32 @@ import { assertNever } from "./util/assertNever";
33
33
* `getWantedCommitsWithBranchBoundariesUsingNativeGitRebase` function.
34
34
*
35
35
*/
36
- export async function autosquash ( repo : Git . Repository , extendedCommits : CommitAndBranchBoundary [ ] ) : Promise < void > {
36
+ export async function autosquash (
37
+ repo : Git . Repository , //
38
+ extendedCommits : CommitAndBranchBoundary [ ]
39
+ ) : Promise < CommitAndBranchBoundary [ ] > {
37
40
// type SHA = string;
38
41
// const commitLookupTable: Map<SHA, Git.Commit> = new Map();
42
+
39
43
const autoSquashableSummaryPrefixes = [ "squash!" , "fixup!" ] as const ;
40
44
41
- for ( let i = 0 ; i < extendedCommits . length ; i ++ ) {
42
- const commit = extendedCommits [ i ] ;
45
+ /**
46
+ * we want to re-order the commits,
47
+ * but we do NOT want the branches to follow them.
48
+ *
49
+ * the easiest way to do this is to "un-attach" the branches from the commits,
50
+ * do the re-ordering,
51
+ * and then re-attach the branches to the new commits that are previous to the branch.
52
+ */
53
+ const unattachedCommitsAndBranches : UnAttachedCommitOrBranch [ ] = unAttachBranchesFromCommits ( extendedCommits ) ;
54
+
55
+ for ( let i = 0 ; i < unattachedCommitsAndBranches . length ; i ++ ) {
56
+ const commitOrBranch : UnAttachedCommitOrBranch = unattachedCommitsAndBranches [ i ] ;
57
+
58
+ if ( isBranch ( commitOrBranch ) ) {
59
+ continue ;
60
+ }
61
+ const commit : UnAttachedCommit = commitOrBranch ;
43
62
44
63
const summary : string = commit . commit . summary ( ) ;
45
64
const hasAutoSquashablePrefix = ( prefix : string ) : boolean => summary . startsWith ( prefix ) ;
@@ -75,7 +94,9 @@ export async function autosquash(repo: Git.Repository, extendedCommits: CommitAn
75
94
throw new Termination ( msg ) ;
76
95
}
77
96
78
- const indexOfTargetCommit : number = extendedCommits . findIndex ( ( c ) => ! target . id ( ) . cmp ( c . commit . id ( ) ) ) ;
97
+ const indexOfTargetCommit : number = unattachedCommitsAndBranches . findIndex (
98
+ ( c ) => ! isBranch ( c ) && ! target . id ( ) . cmp ( c . commit . id ( ) )
99
+ ) ;
79
100
const wasNotFound = indexOfTargetCommit === - 1 ;
80
101
81
102
if ( wasNotFound ) {
@@ -117,7 +138,106 @@ export async function autosquash(repo: Git.Repository, extendedCommits: CommitAn
117
138
* TODO optimal implementation with a linked list + a map
118
139
*
119
140
*/
120
- extendedCommits . splice ( i , 1 ) ; // remove 1 element (`commit`)
121
- extendedCommits . splice ( indexOfTargetCommit + 1 , 0 , commit ) ; // insert the `commit` in the new position
141
+ unattachedCommitsAndBranches . splice ( i , 1 ) ; // remove 1 element (`commit`)
142
+ unattachedCommitsAndBranches . splice ( indexOfTargetCommit + 1 , 0 , commit ) ; // insert the `commit` in the new position
122
143
}
144
+
145
+ const reattached : CommitAndBranchBoundary [ ] = reAttachBranchesToCommits ( unattachedCommitsAndBranches ) ;
146
+
147
+ return reattached ;
148
+ }
149
+
150
+ type UnAttachedCommit = Omit < CommitAndBranchBoundary , "branchEnd" > ;
151
+ type UnAttachedBranch = Pick < CommitAndBranchBoundary , "branchEnd" > ;
152
+ type UnAttachedCommitOrBranch = UnAttachedCommit | UnAttachedBranch ;
153
+
154
+ function isBranch ( commitOrBranch : UnAttachedCommitOrBranch ) : commitOrBranch is UnAttachedBranch {
155
+ return "branchEnd" in commitOrBranch ;
156
+ }
157
+
158
+ function unAttachBranchesFromCommits ( attached : CommitAndBranchBoundary [ ] ) : UnAttachedCommitOrBranch [ ] {
159
+ const unattached : UnAttachedCommitOrBranch [ ] = [ ] ;
160
+
161
+ for ( const { branchEnd, ...c } of attached ) {
162
+ unattached . push ( c ) ;
163
+
164
+ if ( branchEnd ?. length ) {
165
+ unattached . push ( { branchEnd } ) ;
166
+ }
167
+ }
168
+
169
+ return unattached ;
170
+ }
171
+
172
+ /**
173
+ * the key to remember here is that commits could've been moved around
174
+ * (that's the whole purpose of unattaching and reattaching the branches)
175
+ * (specifically, commits can only be moved back in history,
176
+ * because you cannot specify a SHA of a commit in the future),
177
+ *
178
+ * and thus multiple `branchEnd` could end up pointing to a single commit,
179
+ * which just needs to be handled.
180
+ *
181
+ */
182
+ function reAttachBranchesToCommits ( unattached : UnAttachedCommitOrBranch [ ] ) : CommitAndBranchBoundary [ ] {
183
+ const reattached : CommitAndBranchBoundary [ ] = [ ] ;
184
+
185
+ let branchEndsForCommit : NonNullable < UnAttachedBranch [ "branchEnd" ] > [ ] = [ ] ;
186
+
187
+ for ( let i = unattached . length - 1 ; i >= 0 ; i -- ) {
188
+ const commitOrBranch = unattached [ i ] ;
189
+
190
+ if ( isBranch ( commitOrBranch ) && commitOrBranch . branchEnd ?. length ) {
191
+ /**
192
+ * it's a branchEnd. remember the above consideration
193
+ * that multiple of them can accumulate for a single commit,
194
+ * thus buffer them, until we reach a commit.
195
+ */
196
+ branchEndsForCommit . push ( commitOrBranch . branchEnd ) ;
197
+ } else {
198
+ /**
199
+ * we reached a commit.
200
+ */
201
+
202
+ let combinedBranchEnds : NonNullable < UnAttachedBranch [ "branchEnd" ] > = [ ] ;
203
+
204
+ /**
205
+ * they are added in reverse order (i--). let's reverse branchEndsForCommit
206
+ */
207
+ for ( let j = branchEndsForCommit . length - 1 ; j >= 0 ; j -- ) {
208
+ const branchEnd : Git . Reference [ ] = branchEndsForCommit [ j ] ;
209
+ combinedBranchEnds = combinedBranchEnds . concat ( branchEnd ) ;
210
+ }
211
+
212
+ const restoredCommitWithBranchEnds : CommitAndBranchBoundary = {
213
+ ...( commitOrBranch as UnAttachedCommit ) , // TODO TS assert
214
+ branchEnd : [ ...combinedBranchEnds ] ,
215
+ } ;
216
+
217
+ reattached . push ( restoredCommitWithBranchEnds ) ;
218
+ branchEndsForCommit = [ ] ;
219
+ }
220
+ }
221
+
222
+ /**
223
+ * we were going backwards - restore correct order.
224
+ * reverses in place.
225
+ */
226
+ reattached . reverse ( ) ;
227
+
228
+ if ( branchEndsForCommit . length ) {
229
+ /**
230
+ * TODO should never happen,
231
+ * or we should assign by default to the 1st commit
232
+ */
233
+
234
+ const msg =
235
+ `\nhave leftover branches without a commit to attach onto:` +
236
+ `\n${ branchEndsForCommit . join ( "\n" ) } ` +
237
+ `\n\n` ;
238
+
239
+ throw new Termination ( msg ) ;
240
+ }
241
+
242
+ return reattached ;
123
243
}
0 commit comments