Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: add info on how it works + (perhaps too thorough?) info on the potential issues #3

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,102 @@ cd repo/
# then:

git-stacked-rebase --help
```
<!--
## What problems do we solve

whatever `git-stacked-rebase` is doing, you could do manually,

i.e. checking out separate branches, keeping them up to date -
constantly rebasing the first branch onto master, and subsequent branches
on the previous one, all while having to drop now-empty/duplicate commits.

but, it doesn't have to be like this.

<++>
-->

## how it works, the tricky parts, & things to be aware of

instead of rebasing one partial branch on top of another,
we always use the latest branch and rebase it onto the initial branch,
and then reset the partial branches to point to new commits.

the last part is the hardest, and likely still has many bugs yet unsolved.

the tricky part comes from the power of git-rebase.
as long as a rebase finishes without exiting, we are golden.
but, as soon as a user indicates that they want to do some further
operations, by e.g. changing a command from `pick` to `edit` or `break`,
then git-rebase, when it reaches that point, has to exit (!) to let the user
do whatever actions they want to do, and of course allows the user to continue
the rebase via `git rebase --continue`.

<!--
this ability to pause the execution of the rebase,
and then continue later on, is very powerful & useful,
and the fact that it makes our job harder is not a bad thing;
rather, it's a natural occurence <++>
-->

why this matters is because we need to recover the branches
to point to the new commits.

git, upon finishing (?) the rebase, writes out a file
inside `.git/rebase-{merge|apply}/` -- `rewritten-list`.
this file contains the rewritten list of commits -
either `1 -> 1` mapping, or, if multiple commits got merged into 1
(e.g. `squash` / `fixup` / manually with `edit`, or similar),
`N -> 1` mapping (?).
it's very simple - old commit SHA, space, new commit SHA, newline.

again, this is how we understand the new positions that the branches
need to be poiting to.

and no, we cannot simply use the `git-rebase-todo` file (the one you edit
when launching `git rebase -i` / `git-stacked-rebase`),
because of commands like `break`, `edit`, etc., whom, again, allow you
to modify the history, and we cannot infer of what will happen
by simply using the `git-rebase-todo` file.

<!-- now, issue(s) come up when -->
let's first do a scenario without the issue(s) that we'll describe soon -
you ran `git-stacked-rebase`, you did some `fixup`s, `squash`es, `reword`s,
etc. - stuff that _does not_ make git-rebase exit.

all went well, git-rebase wrote out the `rewritten-list` file,
we snagged it up with a `post-rewrite` script that we placed
inside `.git/hooks/`, we combined it with the now outdated
`git-rebase-todo` file (old SHAs etc.) to figure out where the
branch boundaries (partial branches) should end up at in the
new history, and we reset them successfully. all good and well.

now, the scenario w/ a potential issue (?) -- you did
all the same as above, but you also had an `edit` command,
and when git-rebase progressed to it, you `git reset HEAD~`
your commit and instead create 3 new commits, and then continued
& the rebase via `git rebase --continue`.

since `git-rebase` exited before it was done,
we, `git-stacked-rebase`, were __not__ able to reset the branches
to the new history, because of course, if git-rebase exits,
but the rebase is still in progress, we know you are _not_ done,
thus we exit as well.

so when you finish, your changes are applied to your latest branch,
__but__ the partial branches are now out of date.

what you need to do in this case is run `git-stacked-rebase <branch> --apply`,
which, in the 1st scenario, happened automatically.

__the real issue arises when you forget to do the `--apply` part in time__,
i.e. if you do any other history rewritting, including `git commit --amend`,
before you `--apply`ied, `git-stacked-rebase` can no longer (?) properly figure out
where the partial branches should point to, and you're stuck in having to do it
manually.

in the future, we might consider storing the list of partial branches
and if they end up gone, we could e.g. place them at the end of the
`git-rebase-todo` file the next time you invoke `git-stacked-rebase`,
thus you'd be able to drag them into their proper places yourself.
but until then, beware this issue exists (?).
15 changes: 9 additions & 6 deletions branchSequencer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Git from "nodegit";
import { filenames } from "./filenames";

import { createExecSyncInRepo } from "./util/execSyncInRepo";
import { EitherExitFinal, fail } from "./util/Exitable";
import { Termination } from "./util/error";

import { parseNewGoodCommands } from "./parse-todo-of-stacked-rebase/parseNewGoodCommands";
import { GoodCommand } from "./parse-todo-of-stacked-rebase/validator";
Expand Down Expand Up @@ -57,8 +57,8 @@ export type BranchSequencerArgs = BranchSequencerArgsBase & {
rewrittenListFile?: typeof filenames.rewrittenList | typeof filenames.rewrittenListApplied;
};

export type BranchSequencerBase = (args: BranchSequencerArgsBase) => Promise<EitherExitFinal>;
export type BranchSequencer = (args: BranchSequencerArgs) => Promise<EitherExitFinal>;
export type BranchSequencerBase = (args: BranchSequencerArgsBase) => Promise<void>;
export type BranchSequencer = (args: BranchSequencerArgs) => Promise<void>;

export const branchSequencer: BranchSequencer = async ({
pathToStackedRebaseDirInsideDotGit, //
Expand All @@ -73,11 +73,14 @@ export const branchSequencer: BranchSequencer = async ({
rewrittenListFile = filenames.rewrittenList,
}) => {
if (!fs.existsSync(pathToStackedRebaseDirInsideDotGit)) {
return fail(`\n\nno stacked-rebase in progress? (nothing to ${rootLevelCommandName})\n\n`);
throw new Termination(`\n\nno stacked-rebase in progress? (nothing to ${rootLevelCommandName})\n\n`);
}

const [exit, stackedRebaseCommandsNew] = parseNewGoodCommands(repo, pathToStackedRebaseTodoFile, rewrittenListFile);
if (!stackedRebaseCommandsNew) return fail(exit);
const stackedRebaseCommandsNew: GoodCommand[] = parseNewGoodCommands(
repo,
pathToStackedRebaseTodoFile,
rewrittenListFile
);

// const remotes: Git.Remote[] = await repo.getRemotes();
// const remote: Git.Remote | undefined = remotes.find((r) =>
Expand Down
42 changes: 26 additions & 16 deletions git-stacked-rebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import { createExecSyncInRepo } from "./util/execSyncInRepo";
import { noop } from "./util/noop";
import { uniq } from "./util/uniq";
import { parseTodoOfStackedRebase } from "./parse-todo-of-stacked-rebase/parseTodoOfStackedRebase";
import { processWriteAndOrExit, fail, EitherExitFinal } from "./util/Exitable";
import { namesOfRebaseCommandsThatMakeRebaseExitToPause } from "./parse-todo-of-stacked-rebase/validator";
import { Termination } from "./util/error";
import { GoodCommand, namesOfRebaseCommandsThatMakeRebaseExitToPause } from "./parse-todo-of-stacked-rebase/validator";

// console.log = () => {};

Expand Down Expand Up @@ -84,7 +84,7 @@ function areOptionsIncompetible(
export const gitStackedRebase = async (
nameOfInitialBranch: string,
specifiedOptions: SomeOptionsForGitStackedRebase = {}
): Promise<EitherExitFinal> => {
): Promise<void> => {
try {
const options: OptionsForGitStackedRebase = {
...getDefaultOptions(), //
Expand All @@ -95,7 +95,7 @@ export const gitStackedRebase = async (
const reasonsWhatWhyIncompatible: string[] = [];

if (areOptionsIncompetible(options, reasonsWhatWhyIncompatible)) {
return fail(
throw new Termination(
"\n" +
bullets(
"error - incompatible options:", //
Expand Down Expand Up @@ -195,7 +195,7 @@ export const gitStackedRebase = async (

if (options.push) {
if (!options.forcePush) {
return fail("\npush without --force will fail (since git rebase overrides history).\n\n");
throw new Termination("\npush without --force will fail (since git rebase overrides history).\n\n");
}

return await forcePush({
Expand Down Expand Up @@ -225,7 +225,7 @@ export const gitStackedRebase = async (
* to branchSequencer later.
*/

return fail("\n--branch-sequencer (without --exec) - nothing to do?\n\n");
throw new Termination("\n--branch-sequencer (without --exec) - nothing to do?\n\n");
}
}

Expand Down Expand Up @@ -273,8 +273,7 @@ export const gitStackedRebase = async (

const regularRebaseTodoLines: string[] = [];

const [exit, goodCommands] = parseTodoOfStackedRebase(pathToStackedRebaseTodoFile);
if (!goodCommands) return fail(exit);
const goodCommands: GoodCommand[] = parseTodoOfStackedRebase(pathToStackedRebaseTodoFile);

const proms: Promise<void>[] = goodCommands.map(async (cmd) => {
if (cmd.rebaseKind === "regular") {
Expand Down Expand Up @@ -646,8 +645,7 @@ cat "$REWRITTEN_LIST_FILE_PATH" > "$REWRITTEN_LIST_BACKUP_FILE_PATH"

return;
} catch (e) {
console.error(e);
return fail(e);
throw e; // TODO FIXME - no try/catch at all?
}
};

Expand Down Expand Up @@ -949,7 +947,7 @@ async function getCommitOfBranch(repo: Git.Repository, branchReference: Git.Refe
/**
* the CLI
*/
export async function git_stacked_rebase(): Promise<EitherExitFinal> {
export async function git_stacked_rebase(): Promise<void> {
const pkgFromSrc = path.join(__dirname, "package.json");
const pkgFromDist = path.join(__dirname, "../", "package.json");
let pkg;
Expand Down Expand Up @@ -1058,7 +1056,9 @@ git-stacked-rebase ${gitStackedRebaseVersionStr}
console.log({ "process.argv after non-positional": process.argv });

const nameOfInitialBranch: string | undefined = eatNextArg();
if (!nameOfInitialBranch) return fail(helpMsg);
if (!nameOfInitialBranch) {
throw new Termination(helpMsg);
}

/**
* TODO: improve arg parsing, lmao
Expand Down Expand Up @@ -1112,17 +1112,19 @@ git-stacked-rebase ${gitStackedRebaseVersionStr}
const fourth = eatNextArg();
branchSequencerExec = fourth ? fourth : false;
} else {
return fail(`\n--branch-sequencer can only (for now) be followed by ${execNames.join("|")}\n\n`);
throw new Termination(
`\n--branch-sequencer can only (for now) be followed by ${execNames.join("|")}\n\n`
);
}
}

if (!isForcePush && !branchSequencerExec) {
return fail(`\nunrecognized 3th option (got "${third}")\n\n`);
throw new Termination(`\nunrecognized 3th option (got "${third}")\n\n`);
}
}

if (process.argv.length) {
return fail(
throw new Termination(
"" + //
"\n" +
bullets("\nerror - leftover arguments: ", process.argv, " ") +
Expand All @@ -1146,5 +1148,13 @@ git-stacked-rebase ${gitStackedRebaseVersionStr}

if (!module.parent) {
git_stacked_rebase() //
.then(processWriteAndOrExit);
.then(() => process.exit(0))
.catch((e) => {
if (e instanceof Termination) {
process.stderr.write(e.message);
process.exit(1);
} else {
throw e;
}
});
}
8 changes: 3 additions & 5 deletions parse-todo-of-stacked-rebase/parseNewGoodCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import Git from "nodegit";
import { array } from "nice-comment";

import { filenames } from "../filenames";
import { EitherExit, fail, succ } from "../util/Exitable";

import { parseTodoOfStackedRebase } from "./parseTodoOfStackedRebase";
import { GoodCommand, stackedRebaseCommands } from "./validator";
Expand All @@ -17,9 +16,8 @@ export function parseNewGoodCommands(
repo: Git.Repository,
pathToStackedRebaseTodoFile: string, //
rewrittenListFile: typeof filenames.rewrittenList | typeof filenames.rewrittenListApplied
): EitherExit<GoodCommand[]> {
const [exit, goodCommands] = parseTodoOfStackedRebase(pathToStackedRebaseTodoFile);
if (!goodCommands) return fail(exit);
): GoodCommand[] {
const goodCommands: GoodCommand[] = parseTodoOfStackedRebase(pathToStackedRebaseTodoFile);

logGoodCmds(goodCommands);

Expand Down Expand Up @@ -164,7 +162,7 @@ export function parseNewGoodCommands(

assert(stackedRebaseCommandsOld.length === stackedRebaseCommandsNew.length);

return succ(stackedRebaseCommandsNew);
return stackedRebaseCommandsNew;
}

const logGoodCmds = (goodCommands: GoodCommand[]): void => {
Expand Down
3 changes: 1 addition & 2 deletions parse-todo-of-stacked-rebase/parseTodoOfStackedRebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@

import fs from "fs";

import { EitherExit } from "../util/Exitable";
// import path from "path";

import { GoodCommand, validate } from "./validator";

export function parseTodoOfStackedRebase(
pathToStackedRebaseTodoFile: string //
// goodCommands: GoodCommand[]
): EitherExit<GoodCommand[]> {
): GoodCommand[] {
const editedRebaseTodo: string = fs.readFileSync(pathToStackedRebaseTodoFile, { encoding: "utf-8" });
const linesOfEditedRebaseTodo: string[] = editedRebaseTodo.split("\n").filter((line) => !!line);

Expand Down
10 changes: 5 additions & 5 deletions parse-todo-of-stacked-rebase/validator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/consistent-type-assertions */
/* eslint-disable indent */

import { assert } from "console";
import assert from "assert";
import { bullets, joinWith, tick } from "nice-comment";
import { EitherExit, fail, succ } from "../util/Exitable";
import { Termination } from "../util/error";

/**
* if invalid, should fill the array with reasons why not valid.
Expand Down Expand Up @@ -292,7 +292,7 @@ export type GoodCommand = {
}
);

export function validate(linesOfEditedRebaseTodo: string[]): EitherExit<GoodCommand[]> {
export function validate(linesOfEditedRebaseTodo: string[]): GoodCommand[] {
const badCommands: BadCommand[] = [];
const goodCommands: GoodCommand[] = [];

Expand Down Expand Up @@ -418,7 +418,7 @@ export function validate(linesOfEditedRebaseTodo: string[]): EitherExit<GoodComm
});

if (badCommands.length) {
return fail(
throw new Termination(
"\n" +
joinWith("\n\n")([
"found errors in rebase commands:",
Expand All @@ -432,5 +432,5 @@ export function validate(linesOfEditedRebaseTodo: string[]): EitherExit<GoodComm
);
}

return succ(goodCommands);
return goodCommands;
}
Loading