@@ -33,13 +33,32 @@ import { assertNever } from "./util/assertNever";
3333 * `getWantedCommitsWithBranchBoundariesUsingNativeGitRebase` function.
3434 *
3535 */
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 [ ] > {
3740 // type SHA = string;
3841 // const commitLookupTable: Map<SHA, Git.Commit> = new Map();
42+
3943 const autoSquashableSummaryPrefixes = [ "squash!" , "fixup!" ] as const ;
4044
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 ;
4362
4463 const summary : string = commit . commit . summary ( ) ;
4564 const hasAutoSquashablePrefix = ( prefix : string ) : boolean => summary . startsWith ( prefix ) ;
@@ -75,7 +94,9 @@ export async function autosquash(repo: Git.Repository, extendedCommits: CommitAn
7594 throw new Termination ( msg ) ;
7695 }
7796
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+ ) ;
79100 const wasNotFound = indexOfTargetCommit === - 1 ;
80101
81102 if ( wasNotFound ) {
@@ -117,7 +138,106 @@ export async function autosquash(repo: Git.Repository, extendedCommits: CommitAn
117138 * TODO optimal implementation with a linked list + a map
118139 *
119140 */
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
122143 }
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 ;
123243}
0 commit comments