From 69ef13c66db234e13876a78f45709d52d56a039a Mon Sep 17 00:00:00 2001 From: Kipras Melnikovas Date: Thu, 12 May 2022 05:22:53 +0300 Subject: [PATCH 1/3] feat: implement autoSquash! Signed-off-by: Kipras Melnikovas --- apply.ts | 8 +++ autosquash.ts | 123 ++++++++++++++++++++++++++++++++++++++++++ branchSequencer.ts | 17 +++++- configKeys.ts | 1 + forcePush.ts | 8 +++ git-stacked-rebase.ts | 53 ++++++++++++++++-- 6 files changed, 203 insertions(+), 7 deletions(-) create mode 100644 autosquash.ts diff --git a/apply.ts b/apply.ts index 186dae66..88c6878f 100644 --- a/apply.ts +++ b/apply.ts @@ -26,6 +26,14 @@ export const apply: BranchSequencerBase = (args) => delayMsBetweenCheckouts: 0, behaviorOfGetBranchBoundaries: BehaviorOfGetBranchBoundaries["parse-from-not-yet-applied-state"], reverseCheckoutOrder: false, + + /** + * `apply` does not perform the rebase operation + * and thus cannot fully modify local commit history, + * thus `autoSquash` is disabled + * (it would produce incorrect results otherwise). + */ + autoSquash: false, }).then( (ret) => (markThatApplied(args.pathToStackedRebaseDirInsideDotGit), ret) // ); diff --git a/autosquash.ts b/autosquash.ts new file mode 100644 index 00000000..ad30b3bd --- /dev/null +++ b/autosquash.ts @@ -0,0 +1,123 @@ +/* eslint-disable indent */ + +import Git from "nodegit"; + +import { CommitAndBranchBoundary } from "./git-stacked-rebase"; + +import { Termination } from "./util/error"; +import { assertNever } from "./util/assertNever"; + +/** + * the general approach on how to handle autosquashing + * is the following, in order: + * + * 1. collect your commits, + * 2. extend them with branch boundaries, + * 3. re-order the "fixup!" and "squash!" commits, + * 4. convert from objects to strings that are joined + * with a newline and written to the git-rebase-todo file + * + * + * if we were to do (3) before (2) + * (which is what happens if we would use git's native rebase + * to collect the commits), + * then, in a situation where a commit with a "fixup!" or "squash!" subject + * is the latest commit of any branch in the stack, + * that commit will move not only itself, but it's branch as well. + * + * we don't want that obviously - we instead want the branch + * to point to a commit that was before the "fixup!" or "squash!" commit + * (and same applies if there were multiple "fixup!" / "squash!" commits in a row). + * + * see the `--no-autosquash` enforcement/limitation in the + * `getWantedCommitsWithBranchBoundariesUsingNativeGitRebase` function. + * + */ +export async function autosquash(repo: Git.Repository, extendedCommits: CommitAndBranchBoundary[]): Promise { + // type SHA = string; + // const commitLookupTable: Map = new Map(); + const autoSquashableSummaryPrefixes = ["squash!", "fixup!"] as const; + + for (let i = 0; i < extendedCommits.length; i++) { + const commit = extendedCommits[i]; + + const summary: string = commit.commit.summary(); + const hasAutoSquashablePrefix = (prefix: string): boolean => summary.startsWith(prefix); + + const autoSquashCommandIdx: number = autoSquashableSummaryPrefixes.findIndex(hasAutoSquashablePrefix); + const shouldBeAutoSquashed = autoSquashCommandIdx !== -1; + + if (!shouldBeAutoSquashed) { + continue; + } + + const command = autoSquashableSummaryPrefixes[autoSquashCommandIdx]; + const targetedCommittish: string = summary.split(" ")[1]; + + /** + * https://libgit2.org/libgit2/#HEAD/group/revparse + */ + // Git.Revparse.ext(target, ) + const target: Git.Object = await Git.Revparse.single(repo, targetedCommittish); + const targetRev: Git.Object = await target.peel(Git.Object.TYPE.COMMIT); + const targetType: number = await targetRev.type(); + const targetIsCommit: boolean = targetType === Git.Object.TYPE.COMMIT; + + if (!targetIsCommit) { + const msg = + `\ntried to parse auto-squashable commit's target revision, but failed.` + + `\ncommit = ${commit.commit.sha()} (${commit.commit.summary()})` + + `\ncommand = ${command}` + + `\ntarget = ${targetRev.id().tostrS()}` + + `\ntarget type (expected ${Git.Object.TYPE.COMMIT}) = ${targetType}` + + `\n\n`; + + throw new Termination(msg); + } + + const indexOfTargetCommit: number = extendedCommits.findIndex((c) => !target.id().cmp(c.commit.id())); + const wasNotFound = indexOfTargetCommit === -1; + + if (wasNotFound) { + const msg = + `\ntried to re-order an auto-squashable commit, ` + + `but the target commit was not within the commits that are being rebased.` + + `\ncommit = ${commit.commit.sha()} (${commit.commit.summary()})` + + `\ncommand = ${command}` + + `\ntarget = ${targetRev.id().tostrS()}` + + `\n\n`; + + throw new Termination(msg); + } + + commit.commitCommand = + command === "squash!" + ? "squash" // + : command === "fixup!" + ? "fixup" + : assertNever(command); + + /** + * first remove the commit from the array, + * and only then insert it in the array. + * + * this will always work, and the opposite will never work + * because of index mismatch: + * + * you cannot reference commit SHAs that will appear in the future, + * only in the past. + * thus, we know that an auto-squashable commit's target will always be + * earlier in the history than the auto-squashable commit itself. + * + * thus, we first remove the auto-squashable commit, + * so that the index of the target commit stays the same, + * and only then insert the auto-squashable commit. + * + * + * TODO optimal implementation with a linked list + a map + * + */ + extendedCommits.splice(i, 1); // remove 1 element (`commit`) + extendedCommits.splice(indexOfTargetCommit + 1, 0, commit); // insert the `commit` in the new position + } +} diff --git a/branchSequencer.ts b/branchSequencer.ts index 517a0e42..5934630b 100644 --- a/branchSequencer.ts +++ b/branchSequencer.ts @@ -3,7 +3,7 @@ import assert from "assert"; import Git from "nodegit"; -import { getWantedCommitsWithBranchBoundariesOurCustomImpl } from "./git-stacked-rebase"; +import { AutoSquash, getWantedCommitsWithBranchBoundariesOurCustomImpl } from "./git-stacked-rebase"; import { createExecSyncInRepo } from "./util/execSyncInRepo"; import { Termination } from "./util/error"; @@ -23,6 +23,7 @@ export type GetBranchesCtx = BranchRefs & { rootLevelCommandName: string; repo: Git.Repository; pathToStackedRebaseTodoFile: string; + autoSquash: boolean; }; export type SimpleBranchAndCommit = { commitSHA: string | null; @@ -117,7 +118,8 @@ const getBoundariesInclInitialWithSipleBranchTraversal: GetBoundariesInclInitial getWantedCommitsWithBranchBoundariesOurCustomImpl( argsBase.repo, // argsBase.initialBranch, - argsBase.currentBranch + argsBase.currentBranch, + argsBase.autoSquash ).then((boundaries) => boundaries .filter((b) => !!b.branchEnd?.length) @@ -240,6 +242,14 @@ export type BranchSequencerArgs = BranchSequencerArgsBase & { * */ reverseCheckoutOrder: boolean; + + /** + * almost feels like it should default to `false`, + * or even shouldn't be selectable here & always be `false`. + * + * TODO further investigation + */ + autoSquash: AutoSquash; }; export type BranchSequencerBase = (args: BranchSequencerArgsBase) => Promise; @@ -260,6 +270,8 @@ export const branchSequencer: BranchSequencer = async ({ currentBranch, // reverseCheckoutOrder = false, + // + autoSquash, }) => { const execSyncInRepo = createExecSyncInRepo(repo); @@ -276,6 +288,7 @@ export const branchSequencer: BranchSequencer = async ({ rootLevelCommandName, initialBranch, currentBranch, + autoSquash, }) ).map((boundary) => { boundary.branchEndFullName = boundary.branchEndFullName.map((x) => x.replace("refs/heads/", "")); diff --git a/configKeys.ts b/configKeys.ts index f1c2d589..a68856d1 100644 --- a/configKeys.ts +++ b/configKeys.ts @@ -3,4 +3,5 @@ export const configKeyPrefix = "stackedrebase" as const; export const configKeys = { gpgSign: "commit.gpgSign", autoApplyIfNeeded: `${configKeyPrefix}.autoApplyIfNeeded`, + autoSquash: "rebase.autoSquash", } as const; diff --git a/forcePush.ts b/forcePush.ts index 44c5d1a5..b8a8aba5 100644 --- a/forcePush.ts +++ b/forcePush.ts @@ -133,4 +133,12 @@ export const forcePush: BranchSequencerBase = (argsBase) => * would solve this. */ reverseCheckoutOrder: true, + + /** + * `forcePush` does not perform the rebase operation + * and thus cannot fully modify local commit history, + * thus `autoSquash` is disabled + * (it would produce incorrect results otherwise). + */ + autoSquash: false, }); diff --git a/git-stacked-rebase.ts b/git-stacked-rebase.ts index fef1030f..9190e29c 100755 --- a/git-stacked-rebase.ts +++ b/git-stacked-rebase.ts @@ -19,6 +19,7 @@ import { configKeys } from "./configKeys"; import { apply, applyIfNeedsToApply, markThatNeedsToApply as _markThatNeedsToApply } from "./apply"; import { forcePush } from "./forcePush"; import { BehaviorOfGetBranchBoundaries, branchSequencer } from "./branchSequencer"; +import { autosquash } from "./autosquash"; import { createExecSyncInRepo } from "./util/execSyncInRepo"; import { noop } from "./util/noop"; @@ -138,6 +139,7 @@ export const gitStackedRebase = async ( const configValues = { gpgSign: !!(await config.getBool(configKeys.gpgSign).catch(() => 0)), autoApplyIfNeeded: !!(await config.getBool(configKeys.autoApplyIfNeeded).catch(() => 0)), + autoSquash: !!(await config.getBool(configKeys.autoSquash).catch(() => 0)), } as const; console.log({ configValues }); @@ -305,6 +307,17 @@ export const gitStackedRebase = async ( "if-stacked-rebase-in-progress-then-parse-not-applied-state-otherwise-simple-branch-traverse" ], reverseCheckoutOrder: false, + + /** + * `branchSequencer` does not perform the rebase operation + * and thus cannot fully modify local commit history, + * thus `autoSquash` is disabled + * (it would produce incorrect results otherwise). + * + * TODO further investigation + * + */ + autoSquash: false, }); } else { /** @@ -338,7 +351,8 @@ export const gitStackedRebase = async ( initialBranch, currentBranch, // __default__pathToStackedRebaseTodoFile - pathToStackedRebaseTodoFile + pathToStackedRebaseTodoFile, + configValues.autoSquash // () => // getWantedCommitsWithBranchBoundariesUsingNativeGitRebase({ // gitCmd: options.gitCmd, @@ -848,16 +862,31 @@ export function removeUndefinedProperties>( ); } +/** + * should commits with "squash!" and "fixup!" subjects be autosquashed. + * + * if an actual rebase operation is NOT being performed + * (i.e. commits are not being rewritten), + * then SHALL BE set to `false`. + * + * otherwise, should be configured in some way -- most likely + * via the git config, and/or the CLI. + * + */ +export type AutoSquash = boolean; + async function createInitialEditTodoOfGitStackedRebase( repo: Git.Repository, // initialBranch: Git.Reference, currentBranch: Git.Reference, pathToRebaseTodoFile: string, + autoSquash: AutoSquash, getCommitsWithBranchBoundaries: () => Promise = () => getWantedCommitsWithBranchBoundariesOurCustomImpl( repo, // initialBranch, - currentBranch + currentBranch, + autoSquash ) ): Promise { // .catch(logErr); @@ -994,7 +1023,7 @@ function callAll(keyToFunctionMap: KeyToFunctionMap) { ); } -type CommitAndBranchBoundary = { +export type CommitAndBranchBoundary = { commit: Git.Commit; commitCommand: RegularRebaseEitherCommandOrAlias; branchEnd: Git.Reference[] | null; @@ -1004,7 +1033,8 @@ export async function getWantedCommitsWithBranchBoundariesOurCustomImpl( repo: Git.Repository, // /** beginningBranch */ bb: Git.Reference, - currentBranch: Git.Reference + currentBranch: Git.Reference, + autoSquash: boolean ): Promise { /** * BEGIN check e.g. fork & origin/fork @@ -1071,7 +1101,20 @@ export async function getWantedCommitsWithBranchBoundariesOurCustomImpl( ) ); - return extendCommitsWithBranchEnds(repo, bb, currentBranch, wantedCommits); + const extended: CommitAndBranchBoundary[] = await extendCommitsWithBranchEnds( + repo, + bb, + currentBranch, + wantedCommits + ); + + if (!autoSquash) { + return extended; + } + + await autosquash(repo, extended); + + return extended; } noop(getWantedCommitsWithBranchBoundariesUsingNativeGitRebase); From 8e8d9b6a8caf335e1e9dd19d2a47ddedb07da1ca Mon Sep 17 00:00:00 2001 From: Kipras Melnikovas Date: Thu, 12 May 2022 03:14:30 +0300 Subject: [PATCH 2/3] as suspected, autoSquash was meant only for the actual rebase operation see next commit on how we realized this (hint: current impl is still broken) Signed-off-by: Kipras Melnikovas --- apply.ts | 8 ------- branchSequencer.ts | 17 ++------------- forcePush.ts | 8 ------- git-stacked-rebase.ts | 51 +++++++------------------------------------ 4 files changed, 10 insertions(+), 74 deletions(-) diff --git a/apply.ts b/apply.ts index 88c6878f..186dae66 100644 --- a/apply.ts +++ b/apply.ts @@ -26,14 +26,6 @@ export const apply: BranchSequencerBase = (args) => delayMsBetweenCheckouts: 0, behaviorOfGetBranchBoundaries: BehaviorOfGetBranchBoundaries["parse-from-not-yet-applied-state"], reverseCheckoutOrder: false, - - /** - * `apply` does not perform the rebase operation - * and thus cannot fully modify local commit history, - * thus `autoSquash` is disabled - * (it would produce incorrect results otherwise). - */ - autoSquash: false, }).then( (ret) => (markThatApplied(args.pathToStackedRebaseDirInsideDotGit), ret) // ); diff --git a/branchSequencer.ts b/branchSequencer.ts index 5934630b..517a0e42 100644 --- a/branchSequencer.ts +++ b/branchSequencer.ts @@ -3,7 +3,7 @@ import assert from "assert"; import Git from "nodegit"; -import { AutoSquash, getWantedCommitsWithBranchBoundariesOurCustomImpl } from "./git-stacked-rebase"; +import { getWantedCommitsWithBranchBoundariesOurCustomImpl } from "./git-stacked-rebase"; import { createExecSyncInRepo } from "./util/execSyncInRepo"; import { Termination } from "./util/error"; @@ -23,7 +23,6 @@ export type GetBranchesCtx = BranchRefs & { rootLevelCommandName: string; repo: Git.Repository; pathToStackedRebaseTodoFile: string; - autoSquash: boolean; }; export type SimpleBranchAndCommit = { commitSHA: string | null; @@ -118,8 +117,7 @@ const getBoundariesInclInitialWithSipleBranchTraversal: GetBoundariesInclInitial getWantedCommitsWithBranchBoundariesOurCustomImpl( argsBase.repo, // argsBase.initialBranch, - argsBase.currentBranch, - argsBase.autoSquash + argsBase.currentBranch ).then((boundaries) => boundaries .filter((b) => !!b.branchEnd?.length) @@ -242,14 +240,6 @@ export type BranchSequencerArgs = BranchSequencerArgsBase & { * */ reverseCheckoutOrder: boolean; - - /** - * almost feels like it should default to `false`, - * or even shouldn't be selectable here & always be `false`. - * - * TODO further investigation - */ - autoSquash: AutoSquash; }; export type BranchSequencerBase = (args: BranchSequencerArgsBase) => Promise; @@ -270,8 +260,6 @@ export const branchSequencer: BranchSequencer = async ({ currentBranch, // reverseCheckoutOrder = false, - // - autoSquash, }) => { const execSyncInRepo = createExecSyncInRepo(repo); @@ -288,7 +276,6 @@ export const branchSequencer: BranchSequencer = async ({ rootLevelCommandName, initialBranch, currentBranch, - autoSquash, }) ).map((boundary) => { boundary.branchEndFullName = boundary.branchEndFullName.map((x) => x.replace("refs/heads/", "")); diff --git a/forcePush.ts b/forcePush.ts index b8a8aba5..44c5d1a5 100644 --- a/forcePush.ts +++ b/forcePush.ts @@ -133,12 +133,4 @@ export const forcePush: BranchSequencerBase = (argsBase) => * would solve this. */ reverseCheckoutOrder: true, - - /** - * `forcePush` does not perform the rebase operation - * and thus cannot fully modify local commit history, - * thus `autoSquash` is disabled - * (it would produce incorrect results otherwise). - */ - autoSquash: false, }); diff --git a/git-stacked-rebase.ts b/git-stacked-rebase.ts index 9190e29c..062b2178 100755 --- a/git-stacked-rebase.ts +++ b/git-stacked-rebase.ts @@ -307,17 +307,6 @@ export const gitStackedRebase = async ( "if-stacked-rebase-in-progress-then-parse-not-applied-state-otherwise-simple-branch-traverse" ], reverseCheckoutOrder: false, - - /** - * `branchSequencer` does not perform the rebase operation - * and thus cannot fully modify local commit history, - * thus `autoSquash` is disabled - * (it would produce incorrect results otherwise). - * - * TODO further investigation - * - */ - autoSquash: false, }); } else { /** @@ -862,31 +851,17 @@ export function removeUndefinedProperties>( ); } -/** - * should commits with "squash!" and "fixup!" subjects be autosquashed. - * - * if an actual rebase operation is NOT being performed - * (i.e. commits are not being rewritten), - * then SHALL BE set to `false`. - * - * otherwise, should be configured in some way -- most likely - * via the git config, and/or the CLI. - * - */ -export type AutoSquash = boolean; - async function createInitialEditTodoOfGitStackedRebase( repo: Git.Repository, // initialBranch: Git.Reference, currentBranch: Git.Reference, pathToRebaseTodoFile: string, - autoSquash: AutoSquash, + autoSquash: boolean, getCommitsWithBranchBoundaries: () => Promise = () => getWantedCommitsWithBranchBoundariesOurCustomImpl( repo, // initialBranch, - currentBranch, - autoSquash + currentBranch ) ): Promise { // .catch(logErr); @@ -913,6 +888,10 @@ async function createInitialEditTodoOfGitStackedRebase( noop(commitsWithBranchBoundaries); + if (autoSquash) { + await autosquash(repo, commitsWithBranchBoundaries); + } + const rebaseTodo = commitsWithBranchBoundaries .map(({ commit, commitCommand, branchEnd }, i) => { if (i === 0) { @@ -1033,8 +1012,7 @@ export async function getWantedCommitsWithBranchBoundariesOurCustomImpl( repo: Git.Repository, // /** beginningBranch */ bb: Git.Reference, - currentBranch: Git.Reference, - autoSquash: boolean + currentBranch: Git.Reference ): Promise { /** * BEGIN check e.g. fork & origin/fork @@ -1101,20 +1079,7 @@ export async function getWantedCommitsWithBranchBoundariesOurCustomImpl( ) ); - const extended: CommitAndBranchBoundary[] = await extendCommitsWithBranchEnds( - repo, - bb, - currentBranch, - wantedCommits - ); - - if (!autoSquash) { - return extended; - } - - await autosquash(repo, extended); - - return extended; + return extendCommitsWithBranchEnds(repo, bb, currentBranch, wantedCommits); } noop(getWantedCommitsWithBranchBoundariesUsingNativeGitRebase); From 74fda6e3c4c00cac6ea1d211695fd47d56780f16 Mon Sep 17 00:00:00 2001 From: Kipras Melnikovas Date: Thu, 12 May 2022 03:58:09 +0300 Subject: [PATCH 3/3] =?UTF-8?q?implement=20commit=20and=20branch=20un-atta?= =?UTF-8?q?ching=20&=20re-attaching=20=F0=9F=94=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kipras Melnikovas --- autosquash.ts | 132 ++++++++++++++++++++++++++++++++++++++++-- git-stacked-rebase.ts | 4 +- 2 files changed, 128 insertions(+), 8 deletions(-) diff --git a/autosquash.ts b/autosquash.ts index ad30b3bd..f1faaded 100644 --- a/autosquash.ts +++ b/autosquash.ts @@ -33,13 +33,32 @@ import { assertNever } from "./util/assertNever"; * `getWantedCommitsWithBranchBoundariesUsingNativeGitRebase` function. * */ -export async function autosquash(repo: Git.Repository, extendedCommits: CommitAndBranchBoundary[]): Promise { +export async function autosquash( + repo: Git.Repository, // + extendedCommits: CommitAndBranchBoundary[] +): Promise { // type SHA = string; // const commitLookupTable: Map = new Map(); + const autoSquashableSummaryPrefixes = ["squash!", "fixup!"] as const; - for (let i = 0; i < extendedCommits.length; i++) { - const commit = extendedCommits[i]; + /** + * we want to re-order the commits, + * but we do NOT want the branches to follow them. + * + * the easiest way to do this is to "un-attach" the branches from the commits, + * do the re-ordering, + * and then re-attach the branches to the new commits that are previous to the branch. + */ + const unattachedCommitsAndBranches: UnAttachedCommitOrBranch[] = unAttachBranchesFromCommits(extendedCommits); + + for (let i = 0; i < unattachedCommitsAndBranches.length; i++) { + const commitOrBranch: UnAttachedCommitOrBranch = unattachedCommitsAndBranches[i]; + + if (isBranch(commitOrBranch)) { + continue; + } + const commit: UnAttachedCommit = commitOrBranch; const summary: string = commit.commit.summary(); const hasAutoSquashablePrefix = (prefix: string): boolean => summary.startsWith(prefix); @@ -75,7 +94,9 @@ export async function autosquash(repo: Git.Repository, extendedCommits: CommitAn throw new Termination(msg); } - const indexOfTargetCommit: number = extendedCommits.findIndex((c) => !target.id().cmp(c.commit.id())); + const indexOfTargetCommit: number = unattachedCommitsAndBranches.findIndex( + (c) => !isBranch(c) && !target.id().cmp(c.commit.id()) + ); const wasNotFound = indexOfTargetCommit === -1; if (wasNotFound) { @@ -117,7 +138,106 @@ export async function autosquash(repo: Git.Repository, extendedCommits: CommitAn * TODO optimal implementation with a linked list + a map * */ - extendedCommits.splice(i, 1); // remove 1 element (`commit`) - extendedCommits.splice(indexOfTargetCommit + 1, 0, commit); // insert the `commit` in the new position + unattachedCommitsAndBranches.splice(i, 1); // remove 1 element (`commit`) + unattachedCommitsAndBranches.splice(indexOfTargetCommit + 1, 0, commit); // insert the `commit` in the new position } + + const reattached: CommitAndBranchBoundary[] = reAttachBranchesToCommits(unattachedCommitsAndBranches); + + return reattached; +} + +type UnAttachedCommit = Omit; +type UnAttachedBranch = Pick; +type UnAttachedCommitOrBranch = UnAttachedCommit | UnAttachedBranch; + +function isBranch(commitOrBranch: UnAttachedCommitOrBranch): commitOrBranch is UnAttachedBranch { + return "branchEnd" in commitOrBranch; +} + +function unAttachBranchesFromCommits(attached: CommitAndBranchBoundary[]): UnAttachedCommitOrBranch[] { + const unattached: UnAttachedCommitOrBranch[] = []; + + for (const { branchEnd, ...c } of attached) { + unattached.push(c); + + if (branchEnd?.length) { + unattached.push({ branchEnd }); + } + } + + return unattached; +} + +/** + * the key to remember here is that commits could've been moved around + * (that's the whole purpose of unattaching and reattaching the branches) + * (specifically, commits can only be moved back in history, + * because you cannot specify a SHA of a commit in the future), + * + * and thus multiple `branchEnd` could end up pointing to a single commit, + * which just needs to be handled. + * + */ +function reAttachBranchesToCommits(unattached: UnAttachedCommitOrBranch[]): CommitAndBranchBoundary[] { + const reattached: CommitAndBranchBoundary[] = []; + + let branchEndsForCommit: NonNullable[] = []; + + for (let i = unattached.length - 1; i >= 0; i--) { + const commitOrBranch = unattached[i]; + + if (isBranch(commitOrBranch) && commitOrBranch.branchEnd?.length) { + /** + * it's a branchEnd. remember the above consideration + * that multiple of them can accumulate for a single commit, + * thus buffer them, until we reach a commit. + */ + branchEndsForCommit.push(commitOrBranch.branchEnd); + } else { + /** + * we reached a commit. + */ + + let combinedBranchEnds: NonNullable = []; + + /** + * they are added in reverse order (i--). let's reverse branchEndsForCommit + */ + for (let j = branchEndsForCommit.length - 1; j >= 0; j--) { + const branchEnd: Git.Reference[] = branchEndsForCommit[j]; + combinedBranchEnds = combinedBranchEnds.concat(branchEnd); + } + + const restoredCommitWithBranchEnds: CommitAndBranchBoundary = { + ...(commitOrBranch as UnAttachedCommit), // TODO TS assert + branchEnd: [...combinedBranchEnds], + }; + + reattached.push(restoredCommitWithBranchEnds); + branchEndsForCommit = []; + } + } + + /** + * we were going backwards - restore correct order. + * reverses in place. + */ + reattached.reverse(); + + if (branchEndsForCommit.length) { + /** + * TODO should never happen, + * or we should assign by default to the 1st commit + */ + + const msg = + `\nhave leftover branches without a commit to attach onto:` + + `\n${branchEndsForCommit.join("\n")}` + + `\n\n`; + + throw new Termination(msg); + } + + return reattached; } diff --git a/git-stacked-rebase.ts b/git-stacked-rebase.ts index 062b2178..670bb137 100755 --- a/git-stacked-rebase.ts +++ b/git-stacked-rebase.ts @@ -871,7 +871,7 @@ async function createInitialEditTodoOfGitStackedRebase( // return; // } - const commitsWithBranchBoundaries: CommitAndBranchBoundary[] = await getCommitsWithBranchBoundaries(); + let commitsWithBranchBoundaries: CommitAndBranchBoundary[] = await getCommitsWithBranchBoundaries(); // /** // * TODO: FIXME HACK for nodegit rebase @@ -889,7 +889,7 @@ async function createInitialEditTodoOfGitStackedRebase( noop(commitsWithBranchBoundaries); if (autoSquash) { - await autosquash(repo, commitsWithBranchBoundaries); + commitsWithBranchBoundaries = await autosquash(repo, commitsWithBranchBoundaries); } const rebaseTodo = commitsWithBranchBoundaries