Skip to content

Commit c71d539

Browse files
committed
implement commit and branch un-attaching & re-attaching 🔥
Signed-off-by: Kipras Melnikovas <[email protected]>
1 parent 5fae678 commit c71d539

File tree

2 files changed

+128
-8
lines changed

2 files changed

+128
-8
lines changed

Diff for: ‎autosquash.ts

+126-6
Original file line numberDiff line numberDiff line change
@@ -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
}

Diff for: ‎git-stacked-rebase.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -876,7 +876,7 @@ async function createInitialEditTodoOfGitStackedRebase(
876876
// return;
877877
// }
878878

879-
const commitsWithBranchBoundaries: CommitAndBranchBoundary[] = await getCommitsWithBranchBoundaries();
879+
let commitsWithBranchBoundaries: CommitAndBranchBoundary[] = await getCommitsWithBranchBoundaries();
880880

881881
// /**
882882
// * TODO: FIXME HACK for nodegit rebase
@@ -894,7 +894,7 @@ async function createInitialEditTodoOfGitStackedRebase(
894894
noop(commitsWithBranchBoundaries);
895895

896896
if (autoSquash) {
897-
await autosquash(repo, commitsWithBranchBoundaries);
897+
commitsWithBranchBoundaries = await autosquash(repo, commitsWithBranchBoundaries);
898898
}
899899

900900
const rebaseTodo = commitsWithBranchBoundaries

0 commit comments

Comments
 (0)