Skip to content

Commit e073890

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

File tree

2 files changed

+127
-8
lines changed

2 files changed

+127
-8
lines changed

Diff for: ‎autosquash.ts

+125-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,105 @@ 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+
let combinedBranchEnds: NonNullable<UnAttachedBranch["branchEnd"]> = [];
202+
203+
/**
204+
* they are added in reverse order (i--). let's reverse.branchEndsForCommit
205+
*/
206+
for (let j = branchEndsForCommit.length - 1; j >= 0; j--) {
207+
const branchEnd: Git.Reference[] = branchEndsForCommit[j];
208+
combinedBranchEnds = combinedBranchEnds.concat(branchEnd);
209+
}
210+
211+
const restoredCommitWithBranchEnds: CommitAndBranchBoundary = {
212+
...(commitOrBranch as UnAttachedCommit), // TODO TS assert
213+
branchEnd: [...combinedBranchEnds],
214+
};
215+
216+
reattached.push(restoredCommitWithBranchEnds);
217+
branchEndsForCommit = [];
218+
}
219+
}
220+
221+
/**
222+
* we were going backwards - restore correct order.
223+
* reverses in place.
224+
*/
225+
reattached.reverse();
226+
227+
if (branchEndsForCommit.length) {
228+
/**
229+
* TODO should never happen,
230+
* or we should assign by default to the 1st commit
231+
*/
232+
233+
const msg =
234+
`\nhave leftover branches without a commit to attach onto:` +
235+
`\n${branchEndsForCommit.join("\n")}` +
236+
`\n\n`;
237+
238+
throw new Termination(msg);
239+
}
240+
241+
return reattached;
123242
}

Diff for: ‎git-stacked-rebase.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -967,7 +967,7 @@ async function createInitialEditTodoOfGitStackedRebase(
967967
// return;
968968
// }
969969

970-
const commitsWithBranchBoundaries: CommitAndBranchBoundary[] = await getCommitsWithBranchBoundaries();
970+
let commitsWithBranchBoundaries: CommitAndBranchBoundary[] = await getCommitsWithBranchBoundaries();
971971

972972
// /**
973973
// * TODO: FIXME HACK for nodegit rebase
@@ -985,7 +985,7 @@ async function createInitialEditTodoOfGitStackedRebase(
985985
noop(commitsWithBranchBoundaries);
986986

987987
if (autoSquash) {
988-
await autosquash(repo, commitsWithBranchBoundaries);
988+
commitsWithBranchBoundaries = await autosquash(repo, commitsWithBranchBoundaries);
989989
}
990990

991991
const rebaseTodo = commitsWithBranchBoundaries

0 commit comments

Comments
 (0)