diff --git a/apply.ts b/apply.ts index 03bc8785..c37f976b 100644 --- a/apply.ts +++ b/apply.ts @@ -109,7 +109,7 @@ const getPaths = ( appliedPath: path.join(pathToStackedRebaseDirInsideDotGit, filenames.applied), } as const); -const markThatNeedsToApply = ( +export const markThatNeedsToApply = ( pathToStackedRebaseDirInsideDotGit: string // ): void => [getPaths(pathToStackedRebaseDirInsideDotGit)].map( diff --git a/filenames.ts b/filenames.ts index ccc32479..518504d4 100644 --- a/filenames.ts +++ b/filenames.ts @@ -3,6 +3,7 @@ */ export const filenames = { rewrittenList: "rewritten-list", + willNeedToApply: "will-need-to-apply", needsToApply: "needs-to-apply", applied: "applied", // diff --git a/git-stacked-rebase.ts b/git-stacked-rebase.ts index 0b042cc7..a3e73d43 100755 --- a/git-stacked-rebase.ts +++ b/git-stacked-rebase.ts @@ -11,7 +11,7 @@ import { bullets } from "nice-comment"; import { filenames } from "./filenames"; import { configKeys } from "./configKeys"; -import { apply, applyIfNeedsToApply } from "./apply"; +import { apply, applyIfNeedsToApply, markThatNeedsToApply as _markThatNeedsToApply } from "./apply"; import { forcePush } from "./forcePush"; import { branchSequencer } from "./branchSequencer"; @@ -26,6 +26,7 @@ import { GoodCommand, namesOfRebaseCommandsThatMakeRebaseExitToPause } from "./p export type OptionsForGitStackedRebase = { gitDir: string; + getGitConfig: (ctx: { GitConfig: typeof Git.Config; repo: Git.Repository }) => Promise | Git.Config; /** * editor name, or a function that opens the file inside some editor. @@ -49,10 +50,15 @@ export type OptionsForGitStackedRebase = { export type SomeOptionsForGitStackedRebase = Partial; -const getDefaultOptions = (): OptionsForGitStackedRebase => ({ +export const defaultEditor = "vi" as const; +export const defaultGitCmd = "/usr/bin/env git" as const; + +export const getDefaultOptions = (): OptionsForGitStackedRebase => ({ gitDir: ".", // - editor: process.env.EDITOR ?? "vi", - gitCmd: process.env.GIT_CMD ?? "/usr/bin/env git", + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + getGitConfig: ({ GitConfig }) => GitConfig.openDefault(), + editor: process.env.EDITOR ?? defaultEditor, + gitCmd: process.env.GIT_CMD ?? defaultGitCmd, viewTodoOnly: false, apply: false, push: false, @@ -81,6 +87,115 @@ function areOptionsIncompetible( return reasons.length > 0; } +/** + * some notes: + * + * 1. re: "the different ways to launch git rebase": + * + * 1.1 initially, we started probably the hardest way: + * we tried to replicate the behavior of git rebase, by: + * + * 1) finding which commits we need (manually by ourselves), + * 2) letting the user edit our own git-rebase-todo file + * (already containing branch boundaries generated by us), + * 3) creating & writing to files inside `.git/rebase-{merge|apply}/`, + * just like git rebase would, to represent the current state [1]. + * 4) and then exec'ing a `git rebase --continue`, + * so that the user gets launched into the rebase. + * + * + * 1.2 later, we started using 2 rebases - 1st for finding the commits, 2nd for the actual rebase. + * + * important switch that i didn't think had significance until 1.3 -- + * using `editorScript`s (providing an env var - a string of a path to a bash script + * that will move the user-edited & our-processed git-rebase-todo file + * into the proper place (.git/rebase-{merge|apply}/git-rebase-todo) + * right when the rebase starts, thus no longer having to `--continue` (!), + * which is what broke things that were discovered + * and fixed in 1.3 by (fabricately) introducing `--continue` back) + * + * + * 1.3 but soon after, things started breaking. + * + * i didn't understand why. + * + * but after playing w/ it for a while, realized that it's stuff like `reword` + * that breaks, as long as we launch the rebase with an `editorScript`. + * e.g. git would ask to identify yourself - config wouldn't be detected, + * and our `--apply` was completely broken as well [2]. + * + * thus we started manually adding a first command `break` + * into the proper `git-rebase-todo` file [3] + * so that we can launch `--continue` again, + * after the `editorScript` had finished, + * and that somehow fixed everything & we're back to normal. + * + * + * + * i am still not sure what the best approach is, + * though i think i know which one's aren't. + * + * 1.1 seems bad because imitating the full behavior is hard, + * + * e.g. respecting commit.gpgSign required creating a file `gpg_sign_opt` + * with content `-S` - there's probably a ton of stuff like this i didn't even realize + * that git has, and it's possible more will be added in the future. + * you can obviously see the problems that stem from this. + * + * 1.2 seems bad because it, apart from being broken until 1.3, + * also used 2 rebases instead of 1, and that is kinda hacky. + * stuff like git hooks exists, e.g. post-write, + * that even we ourselves utilize, + * and launching 2 rebases means producing side-effects + * like this, and we're potentially breaking end users' workflows. + * + * thus, 1.3 seems like a clear winner, at least for now. + * especially up until we investigate the break + continue thingie - + * ideally we wouldn't need it. + * + * + * + * --- + * + * [1] + * on representing the _state_ -- git rebase, + * specifically the --interactive one (which is what we have here as well), + * is _not_ a continuous command that goes through and is 100% done when it exits. + * + * there are some cases where it intentionally exits, to allow the user + * to do further interactions, and later --continue the rebase + * (e.g. `edit`, `break` etc.). + * + * thus, some state needs to be saved, so that the user, once they're done, + * can tell git to --continue. + * + * turns out, git does this very simply - by saving files inside `.git/`, + * and `.git/rebase-{merge|apply}/` folders. + * this simple design is a big part in allowing tools like us, + * git-stacked-rebase, to work, or rather - to expand upon the existing stuff, + * w/o having to re-implement everything ourselves. + * + * [2] + * on the --apply being broken - it's the logic of `parseNewGoodCommands` that seems broken. + * + * right before realizing 1.3 and writing this comment, + * i wrote a lengthy comment there as well, with thoughts of what's likely broken. + * + * but now, after discovering 1.3, i do not understand yet how the + * `--continue` exec fixes things, and am not sure if i want to mess + * with the `parseNewGoodCommands` logic before i do. + * + * [3] + * the user would never see the `break` command, since just like in 1.1 2), + * we give the user to edit our own git-rebase-todo file, which contains branch boundaries, + * and then extracting the normal git rebase commands and creating + * the git-rebase-todo file for git rebase to run on. + * + * --- + * + * + * + */ export const gitStackedRebase = async ( nameOfInitialBranch: string, specifiedOptions: SomeOptionsForGitStackedRebase = {} @@ -107,7 +222,7 @@ export const gitStackedRebase = async ( } const repo = await Git.Repository.open(options.gitDir); - const config = await Git.Config.openDefault(); + const config: Git.Config = await options.getGitConfig({ GitConfig: Git.Config, repo }); const configValues = { gpgSign: !!(await config.getBool(configKeys.gpgSign).catch(() => 0)), @@ -169,6 +284,10 @@ export const gitStackedRebase = async ( const pathToStackedRebaseDirInsideDotGit: string = parsed.pathToStackedRebaseDirInsideDotGit; const pathToStackedRebaseTodoFile: string = parsed.pathToStackedRebaseTodoFile; + if (fs.existsSync(path.join(pathToStackedRebaseDirInsideDotGit, filenames.willNeedToApply))) { + _markThatNeedsToApply(pathToStackedRebaseDirInsideDotGit); + } + if (options.apply) { return await apply({ repo, @@ -243,16 +362,29 @@ export const gitStackedRebase = async ( console.log({ wasRegularRebaseInProgress }); - if (!wasRegularRebaseInProgress) { - await createInitialEditTodoOfGitStackedRebase( - repo, // - initialBranch, - currentBranch, - // __default__pathToStackedRebaseTodoFile - pathToStackedRebaseTodoFile - ); + if (wasRegularRebaseInProgress) { + throw new Termination("regular rebase already in progress"); } + await createInitialEditTodoOfGitStackedRebase( + repo, // + initialBranch, + currentBranch, + // __default__pathToStackedRebaseTodoFile + pathToStackedRebaseTodoFile + // () => + // getWantedCommitsWithBranchBoundariesUsingNativeGitRebase({ + // gitCmd: options.gitCmd, + // repo, + // initialBranch, + // currentBranch, + // dotGitDirPath, + // pathToRegularRebaseTodoFile, + // pathToStackedRebaseTodoFile, + // pathToRegularRebaseDirInsideDotGit, + // }) + ); + if (!wasRegularRebaseInProgress || options.viewTodoOnly) { if (options.editor instanceof Function) { await options.editor({ filePath: pathToStackedRebaseTodoFile }); @@ -273,9 +405,18 @@ export const gitStackedRebase = async ( const regularRebaseTodoLines: string[] = []; + /** + * part 1 of "the different ways to launch git rebase" + */ + regularRebaseTodoLines.push("break"); + const goodCommands: GoodCommand[] = parseTodoOfStackedRebase(pathToStackedRebaseTodoFile); const proms: Promise[] = goodCommands.map(async (cmd) => { + // if (cmd.commandName === "pick") { + + // } + if (cmd.rebaseKind === "regular") { regularRebaseTodoLines.push(cmd.fullLine); } else if (cmd.rebaseKind === "stacked") { @@ -332,80 +473,80 @@ export const gitStackedRebase = async ( pathToRegularRebaseTodoFile, }); - fs.mkdirSync(pathToRegularRebaseDirInsideDotGit, { recursive: true }); + // fs.mkdirSync(pathToRegularRebaseDirInsideDotGit, { recursive: true }); - fs.writeFileSync(pathToRegularRebaseTodoFile, regularRebaseTodo); - fs.writeFileSync(pathToRegularRebaseTodoFile + ".backup", regularRebaseTodo); + // fs.writeFileSync(pathToRegularRebaseTodoFile, regularRebaseTodo); + // fs.writeFileSync(pathToRegularRebaseTodoFile + ".backup", regularRebaseTodo); - /** - * writing the rebase todo is not enough. - * follow https://github.com/git/git/blob/abe6bb3905392d5eb6b01fa6e54d7e784e0522aa/sequencer.c#L53-L170 - */ + // /** + // * writing the rebase todo is not enough. + // * follow https://github.com/git/git/blob/abe6bb3905392d5eb6b01fa6e54d7e784e0522aa/sequencer.c#L53-L170 + // */ - // (await initialBranch.peel(Git.Object.TYPE.COMMIT)) - const commitShaOfInitialBranch: string = (await (await getCommitOfBranch(repo, initialBranch)).sha()) + "\n"; + // // (await initialBranch.peel(Git.Object.TYPE.COMMIT)) + // const commitShaOfInitialBranch: string = (await (await getCommitOfBranch(repo, initialBranch)).sha()) + "\n"; const getCurrentCommit = (): Promise => repo.getHeadCommit().then((c) => c.sha()); const commitShaOfCurrentCommit: string = await getCurrentCommit(); - console.log({ commitShaOfInitialBranch }); - - await repo.checkoutRef(initialBranch); /** TODO wtf */ - // repo.rebaseBranches() + // console.log({ commitShaOfInitialBranch }); - // const headName: string = (await (await repo.getHeadCommit()).sha()) + "\n"; - // const headName: string = initialBranch.name() + "\n"; - const headName: string = currentBranch.name() + "\n"; - fs.writeFileSync(path.join(pathToRegularRebaseDirInsideDotGit, "head-name"), headName); + // await repo.checkoutRef(initialBranch); + // // repo.rebaseBranches() - fs.writeFileSync(path.join(pathToRegularRebaseDirInsideDotGit, "orig-name"), commitShaOfInitialBranch); + // // const headName: string = (await (await repo.getHeadCommit()).sha()) + "\n"; + // // const headName: string = initialBranch.name() + "\n"; + // const headName: string = currentBranch.name() + "\n"; + // fs.writeFileSync(path.join(pathToRegularRebaseDirInsideDotGit, "head-name"), headName); - fs.writeFileSync( - path.join(pathToRegularRebaseDirInsideDotGit, "onto"), // - commitShaOfInitialBranch - ); + // fs.writeFileSync(path.join(pathToRegularRebaseDirInsideDotGit, "orig-name"), commitShaOfInitialBranch); - fs.writeFileSync( - path.join(pathToRegularRebaseDirInsideDotGit, "onto"), // - commitShaOfInitialBranch - ); - /** - * TODO - is this even needed? seems only a nodegit thing - */ - fs.writeFileSync( - path.join(pathToRegularRebaseDirInsideDotGit, "onto_name"), // - initialBranch.name() + "\n" - ); - fs.writeFileSync( - path.join(pathToRegularRebaseDirInsideDotGit, "cmt.1"), // - commitShaOfInitialBranch - ); + // fs.writeFileSync( + // path.join(pathToRegularRebaseDirInsideDotGit, "onto"), // + // commitShaOfInitialBranch + // ); - fs.writeFileSync( - // path.join(dotGitDirPath, "HEAD"), // - path.join(pathToRegularRebaseDirInsideDotGit, "head"), - commitShaOfInitialBranch - ); + // // fs.writeFileSync( + // // path.join(pathToRegularRebaseDirInsideDotGit, "onto"), // + // // commitShaOfInitialBranch + // // ); + // /** + // * TODO - is this even needed? seems only a nodegit thing + // */ + // // fs.writeFileSync( + // // path.join(pathToRegularRebaseDirInsideDotGit, "onto_name"), // + // // initialBranch.name() + "\n" + // // ); + // // fs.writeFileSync( + // // path.join(pathToRegularRebaseDirInsideDotGit, "cmt.1"), // + // // commitShaOfInitialBranch + // // ); + + // fs.writeFileSync( + // // path.join(dotGitDirPath, "HEAD"), // + // path.join(pathToRegularRebaseDirInsideDotGit, "head"), + // commitShaOfInitialBranch + // ); - // fs.writeFileSync(path.join(dotGitDirPath, "ORIG_HEAD"), commitShaOfInitialBranch); - fs.writeFileSync(path.join(pathToRegularRebaseDirInsideDotGit, "orig-head"), commitShaOfCurrentCommit + "\n"); + // // fs.writeFileSync(path.join(dotGitDirPath, "ORIG_HEAD"), commitShaOfInitialBranch); + // fs.writeFileSync(path.join(pathToRegularRebaseDirInsideDotGit, "orig-head"), commitShaOfCurrentCommit + "\n"); - fs.writeFileSync(path.join(pathToRegularRebaseDirInsideDotGit, "interactive"), ""); + // fs.writeFileSync(path.join(pathToRegularRebaseDirInsideDotGit, "interactive"), ""); - fs.writeFileSync(path.join(pathToRegularRebaseDirInsideDotGit, "done"), ""); + // fs.writeFileSync(path.join(pathToRegularRebaseDirInsideDotGit, "done"), ""); - fs.writeFileSync( - path.join(pathToRegularRebaseDirInsideDotGit, "end"), // - (regularRebaseTodoLines.length + 1).toString() + "\n" - ); + // fs.writeFileSync( + // path.join(pathToRegularRebaseDirInsideDotGit, "end"), // + // (regularRebaseTodoLines.length + 1).toString() + "\n" + // ); - fs.writeFileSync(path.join(pathToRegularRebaseDirInsideDotGit, "msgnum"), "1"); + // fs.writeFileSync(path.join(pathToRegularRebaseDirInsideDotGit, "msgnum"), "1"); - if (configValues.gpgSign) { - const gpgSignOpt = "-S" as const; - fs.writeFileSync(path.join(pathToRegularRebaseDirInsideDotGit, "gpg_sign_opt"), gpgSignOpt); - } + // if (configValues.gpgSign) { + // const gpgSignOpt = "-S" as const; + // fs.writeFileSync(path.join(pathToRegularRebaseDirInsideDotGit, "gpg_sign_opt"), gpgSignOpt); + // } /** * end rebase initial setup. @@ -502,16 +643,75 @@ cp "$REWRITTEN_LIST_FILE_PATH" "$REWRITTEN_LIST_BACKUP_FILE_PATH" process.stdout.write("\nwarning - overwrote post-rewrite script in .git/hooks/, saved backup.\n\n"); } + // /** + // * too bad libgit2 is limited. oh well, i've seen worse. + // * + // * this passes it off to the user. + // * + // * they'll come back to us once they're done, + // * with --apply or whatever. + // * + // */ + // execSyncInRepo(`${options.gitCmd} rebase --continue`); + + const preparedRegularRebaseTodoFile = path.join( + pathToStackedRebaseDirInsideDotGit, + filenames.gitRebaseTodo + ".ready" + ); + fs.writeFileSync(preparedRegularRebaseTodoFile, regularRebaseTodo); + + const editorScript = `\ +#!/usr/bin/env bash + +cat "${preparedRegularRebaseTodoFile}" + +mv -f "${preparedRegularRebaseTodoFile}" "${pathToRegularRebaseTodoFile}" + + `; + const editorScriptPath: string = path.join(dotGitDirPath, "editorScript.doActualRebase.sh"); + fs.writeFileSync(editorScriptPath, editorScript, { mode: "777" }); + + const referenceToOid = (ref: Git.Reference): Promise => + ref.peel(Git.Object.TYPE.COMMIT).then((x) => x.id()); + + const commitOfInitialBranch: Git.Oid = await referenceToOid(initialBranch); // bb + const commitOfCurrentBranch: Git.Oid = await referenceToOid(currentBranch); + + // https://stackoverflow.com/a/1549155/9285308 + const latestCommitOfOursThatInitialBranchAlreadyHas: Git.Oid = await Git.Merge.base( + repo, // + commitOfInitialBranch, + commitOfCurrentBranch + ); + + execSyncInRepo( + [ + options.gitCmd, // + "rebase", + "--interactive", + latestCommitOfOursThatInitialBranchAlreadyHas.tostrS(), + "--onto", + initialBranch.name(), + ].join(" "), + { + env: { + // https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt-sequenceeditor + GIT_SEQUENCE_EDITOR: editorScriptPath, + }, + } + ); + console.log("big buns - the proper rebase returned"); + /** - * too bad libgit2 is limited. oh well, i've seen worse. - * - * this passes it off to the user. - * - * they'll come back to us once they're done, - * with --apply or whatever. - * + * will need to apply, unless proven otherwise + */ + fs.writeFileSync(path.join(pathToStackedRebaseDirInsideDotGit, filenames.willNeedToApply), ""); + + /** + * part 2 of "the different ways to launch git rebase" */ execSyncInRepo(`${options.gitCmd} rebase --continue`); + /** * if the rebase finishes and ONLY THEN EXITS, * it's fine and we continue. @@ -579,8 +779,14 @@ cp "$REWRITTEN_LIST_FILE_PATH" "$REWRITTEN_LIST_BACKUP_FILE_PATH" }); console.log(""); + fs.unlinkSync(path.join(pathToStackedRebaseDirInsideDotGit, filenames.willNeedToApply)); if (rebaseChangedLocalHistory) { markThatNeedsToApply(); + } else { + // /** + // * TODO `unmarkThatNeedsToApply` (NOT the same as `markThatApplied`!) + // */ + // // unmarkThatNeedsToApply(); } /** @@ -676,7 +882,13 @@ async function createInitialEditTodoOfGitStackedRebase( repo: Git.Repository, // initialBranch: Git.Reference, currentBranch: Git.Reference, - pathToRebaseTodoFile: string + pathToRebaseTodoFile: string, + getCommitsWithBranchBoundaries: () => Promise = () => + getWantedCommitsWithBranchBoundariesOurCustomImpl( + repo, // + initialBranch, + currentBranch + ) ): Promise { // .catch(logErr); @@ -685,13 +897,7 @@ async function createInitialEditTodoOfGitStackedRebase( // return; // } - const commitsWithBranchBoundaries: CommitAndBranchBoundary[] = ( - await getWantedCommitsWithBranchBoundaries( - repo, // - initialBranch, - currentBranch - ) - ).reverse(); + const commitsWithBranchBoundaries: CommitAndBranchBoundary[] = await getCommitsWithBranchBoundaries(); // /** // * TODO: FIXME HACK for nodegit rebase @@ -825,15 +1031,12 @@ type CommitAndBranchBoundary = { branchEnd: Git.Reference | null; }; -async function getWantedCommitsWithBranchBoundaries( +async function getWantedCommitsWithBranchBoundariesOurCustomImpl( repo: Git.Repository, // /** beginningBranch */ bb: Git.Reference, currentBranch: Git.Reference ): Promise { - const refNames: string[] = await Git.Reference.list(repo); - const refs: Git.Reference[] = await Promise.all(refNames.map((ref) => Git.Reference.lookup(repo, ref))); - /** * BEGIN check e.g. fork & origin/fork */ @@ -898,56 +1101,215 @@ async function getWantedCommitsWithBranchBoundaries( * TODO FIXME - this is done later, but probably should be done directly * in the underlying function to avoid confusion. */ - commits + commits.reverse() ) ); + return extendCommitsWithBranchEnds(repo, bb, wantedCommits); +} + +noop(getWantedCommitsWithBranchBoundariesUsingNativeGitRebase); +async function getWantedCommitsWithBranchBoundariesUsingNativeGitRebase({ + gitCmd, + repo, + initialBranch, + currentBranch, + dotGitDirPath, + pathToRegularRebaseTodoFile, + pathToStackedRebaseTodoFile, + pathToRegularRebaseDirInsideDotGit, +}: { + gitCmd: string; + repo: Git.Repository; // + initialBranch: Git.Reference; + currentBranch: Git.Reference; + dotGitDirPath: string; + pathToRegularRebaseTodoFile: string; + pathToStackedRebaseTodoFile: string; + pathToRegularRebaseDirInsideDotGit: string; +}) { + const referenceToOid = (ref: Git.Reference): Promise => + ref.peel(Git.Object.TYPE.COMMIT).then((x) => x.id()); + + // const commitOfInitialBranch: Git.Oid = await referenceToOid(bb); + const commitOfInitialBranch: Git.Oid = await referenceToOid(initialBranch); + const commitOfCurrentBranch: Git.Oid = await referenceToOid(currentBranch); + + // https://stackoverflow.com/a/1549155/9285308 + const latestCommitOfOursThatInitialBranchAlreadyHas: Git.Oid = await Git.Merge.base( + repo, // + commitOfInitialBranch, + commitOfCurrentBranch + ); + + const regularRebaseDirBackupPath: string = pathToRegularRebaseDirInsideDotGit + ".backup-from-1st"; + + /** BEGIN COPY-PASTA */ + + const editorScript = `\ +#!/usr/bin/env bash + +printf "yes sir\n\n" + +pushd "${dotGitDirPath}" + +printf "pwd: $(pwd)\n" + +# cat rebase-merge/git-rebase-todo +cat ${pathToRegularRebaseTodoFile} + +# cat ${pathToRegularRebaseTodoFile} > ${pathToStackedRebaseTodoFile}.regular +cp -r ${pathToRegularRebaseDirInsideDotGit} ${regularRebaseDirBackupPath} + +# abort the rebase before even starting it +exit 1 + `; + const editorScriptPath: string = path.join(dotGitDirPath, "editorScript.sh"); + fs.writeFileSync(editorScriptPath, editorScript, { mode: "777" }); + console.log("wrote editorScript"); + + try { + const execSyncInRepo = createExecSyncInRepo(repo); + + const cmd = [ + gitCmd, + /** + * we need the full SHA. + * + * https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt-rebaseinstructionFormat + * https://git-scm.com/docs/git-log#Documentation/git-log.txt-emHem + * + */ + "-c rebase.instructionFormat='%H'", + "rebase", + "--interactive", + latestCommitOfOursThatInitialBranchAlreadyHas.tostrS() + + "~" /** include self (needed for initialBranch's boundary) */, + "--onto", + initialBranch.name(), + ">/dev/null 2>&1", + ].join(" "); + + console.log("launching internal rebase with editorScript to create initial todo:\n%s", cmd); + + execSyncInRepo(cmd, { + env: { + // https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt-sequenceeditor + GIT_SEQUENCE_EDITOR: editorScriptPath, + }, + }); + } catch (e) { + // as expected. do nothing. + // TODO verify that it actually came from our script exiting intentionally + } + + console.log("rebase -i exited"); + + /** END COPY-PASTA */ + + const goodRegularCommands = parseTodoOfStackedRebase( + path.join(regularRebaseDirBackupPath, filenames.gitRebaseTodo) + ); + + if (fs.existsSync(regularRebaseDirBackupPath)) { + fs.rmdirSync(regularRebaseDirBackupPath, { recursive: true }); + } + + console.log("parsedRegular: %O", goodRegularCommands); + + /** + * TODO - would now have to use the logic from `getWantedCommitsWithBranchBoundaries` + * & subsequent utils, though adapted differently - we already have the commits, + * now we gotta add the branch boundaries & then continue like regular. + * + */ + const wantedCommitSHAs: string[] = goodRegularCommands.map((cmd) => { + assert(cmd.commandName === "pick"); + /** + * 1st is the command name + * 2nd is the short commit SHA + * 3rd is the long commit SHA, because of our custom `-c rebase.instructionFormat='%H'` + */ + const commitSHAFull: string = cmd.fullLine.split(" ")?.[2] || ""; + assert(commitSHAFull); + return commitSHAFull; + }); + + console.log("wantedCommitSHAs %O", wantedCommitSHAs); + + // const commits: Git.Commit[] = await Promise.all( + // wantedCommitSHAs.map((sha) => (console.log("sha %s", sha), Git.Commit.lookup(repo, sha))) + // ); + const commits = []; + for (const sha of wantedCommitSHAs) { + console.log("sha %s", sha); + // const oid = await Git.Oid.fromString(sha); + const c = await Git.Commit.lookup(repo, sha); + commits.push(c); + } + + const commitsWithBranchBoundaries: CommitAndBranchBoundary[] = await extendCommitsWithBranchEnds( + repo, + initialBranch, + commits + ); + + console.log("commitsWithBranchBoundaries %O", commitsWithBranchBoundaries); + + return commitsWithBranchBoundaries; +} + +async function extendCommitsWithBranchEnds( + repo: Git.Repository, + initialBranch: Git.Reference, + commits: Git.Commit[] +): Promise { + const refNames: string[] = await Git.Reference.list(repo); + const refs: Git.Reference[] = await Promise.all(refNames.map((ref) => Git.Reference.lookup(repo, ref))); + let matchedRefs: Git.Reference[]; - const wantedCommitsWithBranchEnds: CommitAndBranchBoundary[] = await Promise.all( - wantedCommits.map( - (c: Git.Commit) => ( - (matchedRefs = refs.filter((ref) => !!ref.target()?.equal(c.id()))), - assert( - matchedRefs.length <= 1 || - /** - * if it's more than 1, - * it's only allowed all of the branches are the same ones, - * just on different remotes. - */ - (matchedRefs.length > 1 && - uniq( - matchedRefs.map((r) => - r - ?.name() - .replace(/^refs\/heads\//, "") - .replace(/^refs\/remotes\/[^/]*\//, "") - ) - ).length === 1), - "" + - "2 (or more) branches for the same commit, both in the same path - cannot continue" + - "(until explicit branch specifying is implemented)" + - "\n\n" + - "matchedRefs = " + - matchedRefs.map((mr) => mr?.name()) + - "\n" - ), - matchedRefs.length > 1 && - (matchedRefs = matchedRefs.some((r) => r?.name() === bb.name()) - ? [bb] - : matchedRefs.filter((r) => !r?.isRemote() /* r?.name().includes("refs/heads/") */)), - assert(matchedRefs.length <= 1, "refs/heads/ and refs/remotes/*/ replacement went wrong"), - { - commit: c, - branchEnd: !matchedRefs.length ? null : matchedRefs[0], - } - ) - ) + const extend = (c: Git.Commit) => ( + (matchedRefs = refs.filter((ref) => !!ref.target()?.equal(c.id()))), + assert( + matchedRefs.length <= 1 || + /** + * if it's more than 1, + * it's only allowed all of the branches are the same ones, + * just on different remotes. + */ + (matchedRefs.length > 1 && + uniq( + matchedRefs.map((r) => + r + ?.name() + .replace(/^refs\/heads\//, "") + .replace(/^refs\/remotes\/[^/]*\//, "") + ) + ).length === 1), + "" + + "2 (or more) branches for the same commit, both in the same path - cannot continue" + + "(until explicit branch specifying is implemented)" + + "\n\n" + + "matchedRefs = " + + matchedRefs.map((mr) => mr?.name()) + + "\n" + ), + matchedRefs.length > 1 && + (matchedRefs = matchedRefs.some((r) => r?.name() === initialBranch.name()) + ? [initialBranch] + : matchedRefs.filter((r) => !r?.isRemote() /* r?.name().includes("refs/heads/") */)), + assert(matchedRefs.length <= 1, "refs/heads/ and refs/remotes/*/ replacement went wrong"), + { + commit: c, + branchEnd: !matchedRefs.length ? null : matchedRefs[0], + } ); - return wantedCommitsWithBranchEnds; + return commits.map(extend); } +noop(getCommitOfBranch); async function getCommitOfBranch(repo: Git.Repository, branchReference: Git.Reference) { const branchOid: Git.Oid = await (await branchReference.peel(Git.Object.TYPE.COMMIT)).id(); return await Git.Commit.lookup(repo, branchOid); diff --git a/parse-todo-of-stacked-rebase/validator.ts b/parse-todo-of-stacked-rebase/validator.ts index 70a12633..3a542211 100644 --- a/parse-todo-of-stacked-rebase/validator.ts +++ b/parse-todo-of-stacked-rebase/validator.ts @@ -292,7 +292,10 @@ export type GoodCommand = { } ); -export function validate(linesOfEditedRebaseTodo: string[]): GoodCommand[] { +export function validate( + linesOfEditedRebaseTodo: string[], // + { enforceRequirementsSpecificToStackedRebase = false } = {} +): GoodCommand[] { const badCommands: BadCommand[] = []; const goodCommands: GoodCommand[] = []; @@ -319,6 +322,13 @@ export function validate(linesOfEditedRebaseTodo: string[]): GoodCommand[] { * we're not processing command-by-command, we're processing line-by-line. */ linesOfEditedRebaseTodo.forEach((fullLine, index) => { + if (fullLine.startsWith("#")) { + /** + * ignore comments + */ + return; + } + const [commandOrAliasName, ..._rest] = fullLine.split(" "); const rest = _rest.join(" "); @@ -346,14 +356,16 @@ export function validate(linesOfEditedRebaseTodo: string[]): GoodCommand[] { const reasonsIfBad: string[] = []; - if (index === 0) { - if (commandName !== "branch-end-initial") { - reasonsIfBad.push("initial command must be `branch-end-initial`"); + if (enforceRequirementsSpecificToStackedRebase) { + if (index === 0) { + if (commandName !== "branch-end-initial") { + reasonsIfBad.push("initial command must be `branch-end-initial`"); + } } - } - if (index === linesOfEditedRebaseTodo.length - 1) { - if (commandName !== "branch-end-last") { - reasonsIfBad.push("last command must be `branch-end-last`"); + if (index === linesOfEditedRebaseTodo.length - 1) { + if (commandName !== "branch-end-last") { + reasonsIfBad.push("last command must be `branch-end-last`"); + } } }