|
| 1 | +/* eslint-disable indent */ |
| 2 | + |
| 3 | +import Git from "nodegit"; |
| 4 | + |
| 5 | +import { CommitAndBranchBoundary } from "./git-stacked-rebase"; |
| 6 | + |
| 7 | +import { Termination } from "./util/error"; |
| 8 | +import { assertNever } from "./util/assertNever"; |
| 9 | + |
| 10 | +/** |
| 11 | + * the general approach on how to handle autosquashing |
| 12 | + * is the following, in order: |
| 13 | + * |
| 14 | + * 1. collect your commits, |
| 15 | + * 2. extend them with branch boundaries, |
| 16 | + * 3. re-order the "fixup!" and "squash!" commits, |
| 17 | + * 4. convert from objects to strings that are joined |
| 18 | + * with a newline and written to the git-rebase-todo file |
| 19 | + * |
| 20 | + * |
| 21 | + * if we were to do (3) before (2) |
| 22 | + * (which is what happens if we would use git's native rebase |
| 23 | + * to collect the commits), |
| 24 | + * then, in a situation where a commit with a "fixup!" or "squash!" subject |
| 25 | + * is the latest commit of any branch in the stack, |
| 26 | + * that commit will move not only itself, but it's branch as well. |
| 27 | + * |
| 28 | + * we don't want that obviously - we instead want the branch |
| 29 | + * to point to a commit that was before the "fixup!" or "squash!" commit |
| 30 | + * (and same applies if there were multiple "fixup!" / "squash!" commits in a row). |
| 31 | + * |
| 32 | + * see the `--no-autosquash` enforcement/limitation in the |
| 33 | + * `getWantedCommitsWithBranchBoundariesUsingNativeGitRebase` function. |
| 34 | + * |
| 35 | + */ |
| 36 | +export async function autosquash(repo: Git.Repository, extendedCommits: CommitAndBranchBoundary[]): Promise<void> { |
| 37 | + // type SHA = string; |
| 38 | + // const commitLookupTable: Map<SHA, Git.Commit> = new Map(); |
| 39 | + const autoSquashableSummaryPrefixes = ["squash!", "fixup!"] as const; |
| 40 | + |
| 41 | + for (let i = 0; i < extendedCommits.length; i++) { |
| 42 | + const commit = extendedCommits[i]; |
| 43 | + |
| 44 | + const summary: string = commit.commit.summary(); |
| 45 | + const hasAutoSquashablePrefix = (prefix: string): boolean => summary.startsWith(prefix); |
| 46 | + |
| 47 | + const autoSquashCommandIdx: number = autoSquashableSummaryPrefixes.findIndex(hasAutoSquashablePrefix); |
| 48 | + const shouldBeAutoSquashed = autoSquashCommandIdx !== -1; |
| 49 | + |
| 50 | + if (!shouldBeAutoSquashed) { |
| 51 | + continue; |
| 52 | + } |
| 53 | + |
| 54 | + const command = autoSquashableSummaryPrefixes[autoSquashCommandIdx]; |
| 55 | + const targetedCommittish: string = summary.split(" ")[1]; |
| 56 | + |
| 57 | + /** |
| 58 | + * https://libgit2.org/libgit2/#HEAD/group/revparse |
| 59 | + */ |
| 60 | + // Git.Revparse.ext(target, ) |
| 61 | + const target: Git.Object = await Git.Revparse.single(repo, targetedCommittish); |
| 62 | + const targetRev: Git.Object = await target.peel(Git.Object.TYPE.COMMIT); |
| 63 | + const targetType: number = await targetRev.type(); |
| 64 | + const targetIsCommit: boolean = targetType === Git.Object.TYPE.COMMIT; |
| 65 | + |
| 66 | + if (!targetIsCommit) { |
| 67 | + const msg = |
| 68 | + `\ntried to parse auto-squashable commit's target revision, but failed.` + |
| 69 | + `\ncommit = ${commit.commit.sha()} (${commit.commit.summary()})` + |
| 70 | + `\ncommand = ${command}` + |
| 71 | + `\ntarget = ${targetRev.id().tostrS()}` + |
| 72 | + `\ntarget type (expected ${Git.Object.TYPE.COMMIT}) = ${targetType}` + |
| 73 | + `\n\n`; |
| 74 | + |
| 75 | + throw new Termination(msg); |
| 76 | + } |
| 77 | + |
| 78 | + const indexOfTargetCommit: number = extendedCommits.findIndex((c) => !target.id().cmp(c.commit.id())); |
| 79 | + const wasNotFound = indexOfTargetCommit === -1; |
| 80 | + |
| 81 | + if (wasNotFound) { |
| 82 | + const msg = |
| 83 | + `\ntried to re-order an auto-squashable commit, ` + |
| 84 | + `but the target commit was not within the commits that are being rebased.` + |
| 85 | + `\ncommit = ${commit.commit.sha()} (${commit.commit.summary()})` + |
| 86 | + `\ncommand = ${command}` + |
| 87 | + `\ntarget = ${targetRev.id().tostrS()}` + |
| 88 | + `\n\n`; |
| 89 | + |
| 90 | + throw new Termination(msg); |
| 91 | + } |
| 92 | + |
| 93 | + commit.commitCommand = |
| 94 | + command === "squash!" |
| 95 | + ? "squash" // |
| 96 | + : command === "fixup!" |
| 97 | + ? "fixup" |
| 98 | + : assertNever(command); |
| 99 | + |
| 100 | + /** |
| 101 | + * first remove the commit from the array, |
| 102 | + * and only then insert it in the array. |
| 103 | + * |
| 104 | + * this will always work, and the opposite will never work |
| 105 | + * because of index mismatch: |
| 106 | + * |
| 107 | + * you cannot reference commit SHAs that will appear in the future, |
| 108 | + * only in the past. |
| 109 | + * thus, we know that an auto-squashable commit's target will always be |
| 110 | + * earlier in the history than the auto-squashable commit itself. |
| 111 | + * |
| 112 | + * thus, we first remove the auto-squashable commit, |
| 113 | + * so that the index of the target commit stays the same, |
| 114 | + * and only then insert the auto-squashable commit. |
| 115 | + * |
| 116 | + * |
| 117 | + * TODO optimal implementation with a linked list + a map |
| 118 | + * |
| 119 | + */ |
| 120 | + extendedCommits.splice(i, 1); // remove 1 element (`commit`) |
| 121 | + extendedCommits.splice(indexOfTargetCommit + 1, 0, commit); // insert the `commit` in the new position |
| 122 | + } |
| 123 | +} |
0 commit comments