diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..de8958ed --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,54 @@ +name: "tests" + +on: [push, workflow_dispatch] + +jobs: + test: + runs-on: ${{ matrix.os }} + + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-preventing-a-specific-failing-matrix-job-from-failing-a-workflow-run + continue-on-error: ${{ matrix.experimental }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + node: [12, 14] + experimental: [false] + exclude: + - os: macos-latest + node: 12 + include: + # test macos - for some reason, internet conn fails @ github actions + - os: macos-latest + node: 12 + experimental: true + + # - os: ubuntu-latest + # node: 10 + # experimental: true + # - os: ubuntu-latest + # # v16 should work fine; there're some issues in CI with `krb5-config` + # node: 16 + # experimental: true + # - os: ubuntu-latest + # node: 18 + # experimental: true + + # # test windows, w/ wanted versions + # - os: windows-latest + # node: 12 + # experimental: true + # - os: windows-latest + # node: 14 + # experimental: true + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + cache: 'yarn' + cache-dependency-path: '**/yarn.lock' + - run: yarn --frozen-lockfile + - run: yarn test + \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..196629e7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ts-node tests", + "type": "node", + "request": "launch", + "args": [ + // "${relativeFile}" // + "test/run.ts" + ], + "runtimeArgs": [ + "-r", // + "ts-node/register" + ], + "cwd": "${workspaceRoot}", + "protocol": "inspector", + "internalConsoleOptions": "openOnSessionStart", + "env": { + "DEBUG": "gsr:*" + } + } + ] +} diff --git a/package.json b/package.json index 9d1a5dcd..06c6e78a 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,11 @@ "git-stacked-rebase": "./dist/git-stacked-rebase.js" }, "scripts": { - "prebuild": "node ./script/prebuild.js", - "build": "yarn tsc -b", - "postbuild": "node ./script/postbuild.js", + "prebuild:lean": "node ./script/prebuild.js", + "test": "ts-node-dev ./test/run.ts", + "build": "yarn test && yarn build:lean", + "build:lean": "yarn tsc -b", + "postbuild:lean": "node ./script/postbuild.js", "prepack": "yarn build" }, "devDependencies": { diff --git a/parse-todo-of-stacked-rebase/validator.ts b/parse-todo-of-stacked-rebase/validator.ts index 3a542211..26b903f4 100644 --- a/parse-todo-of-stacked-rebase/validator.ts +++ b/parse-todo-of-stacked-rebase/validator.ts @@ -143,7 +143,7 @@ export const regularRebaseCommands = { // m: standardCommand, } as const; -type RegularRebaseCommand = keyof typeof regularRebaseCommands; +export type RegularRebaseCommand = keyof typeof regularRebaseCommands; /** * TODO: assert each value is `RegularRebaseCommand`, @@ -219,7 +219,7 @@ export const stackedRebaseCommands = { }), } as const; -type StackedRebaseCommand = keyof typeof stackedRebaseCommands; +export type StackedRebaseCommand = keyof typeof stackedRebaseCommands; // const allowedCommandAliasesFromGitStackedRebase: { [key: string]: AllowedGitStackedRebaseCommand } = { const stackedRebaseCommandAliases = { @@ -229,10 +229,10 @@ const stackedRebaseCommandAliases = { type StackedRebaseCommandAlias = keyof typeof stackedRebaseCommandAliases; -type EitherRebaseCommand = RegularRebaseCommand | StackedRebaseCommand; -type EitherRebaseCommandAlias = RegularRebaseCommandAlias | StackedRebaseCommandAlias; +export type EitherRebaseCommand = RegularRebaseCommand | StackedRebaseCommand; +export type EitherRebaseCommandAlias = RegularRebaseCommandAlias | StackedRebaseCommandAlias; -type EitherRebaseEitherCommandOrAlias = EitherRebaseCommand | EitherRebaseCommandAlias; +export type EitherRebaseEitherCommandOrAlias = EitherRebaseCommand | EitherRebaseCommandAlias; type MapOfAllowedRebaseCommands = { [key in EitherRebaseCommand]: Command; diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 00000000..5deb4c5a --- /dev/null +++ b/test/.gitignore @@ -0,0 +1,2 @@ +folders-to-delete +.tmp diff --git a/test/experiment.spec.ts b/test/experiment.spec.ts new file mode 100755 index 00000000..81406ecb --- /dev/null +++ b/test/experiment.spec.ts @@ -0,0 +1,248 @@ +#!/usr/bin/env ts-node-dev + +import fs from "fs"; +import path from "path"; +import assert from "assert"; + +import Git from "nodegit"; + +import { gitStackedRebase, defaultGitCmd } from "../git-stacked-rebase"; + +import { RegularRebaseCommand } from "../parse-todo-of-stacked-rebase/validator"; +import { createExecSyncInRepo } from "../util/execSyncInRepo"; +import { configKeys } from "../configKeys"; + +export async function testCase() { + const { + repo, // + config, + sig, + dir, + } = await setupRepo(); + + const commitOidsInInitial: Git.Oid[] = []; + const initialBranch: Git.Reference = await appendCommitsTo(commitOidsInInitial, 3, repo, sig); + + const latestStackedBranch: Git.Reference = await Git.Branch.create( + repo, + "stack-latest", + await repo.getHeadCommit(), + 0 + ); + await repo.checkoutBranch(latestStackedBranch); + + const execSyncInRepo = createExecSyncInRepo(repo); + + // const read = () => execSyncInRepo("read"); + const read = () => void 0; + + read(); + + const commitOidsInLatestStacked: Git.Oid[] = []; + await appendCommitsTo(commitOidsInLatestStacked, 12, repo, sig); + + const newPartialBranches = [ + ["partial-1", 4], + ["partial-2", 6], + ["partial-3", 8], + ] as const; + + console.log("launching 1st rebase to create partial branches"); + await gitStackedRebase(initialBranch.shorthand(), { + gitDir: dir, + getGitConfig: () => config, + editor: async ({ filePath }) => { + console.log("filePath %s", filePath); + + for (const [newPartial, nthCommit] of newPartialBranches) { + await humanOpAppendLineAfterNthCommit( + filePath, + commitOidsInLatestStacked[nthCommit].tostrS(), + `branch-end-new ${newPartial}` + ); + } + + console.log("finished editor"); + + read(); + }, + }); + + console.log("looking up branches to make sure they were created successfully"); + read(); + for (const [newPartial] of newPartialBranches) { + /** + * will throw if branch does not exist + * TODO "properly" expect to not throw + */ + await Git.Branch.lookup(repo, newPartial, Git.Branch.BRANCH.LOCAL); + } + + /** + * + */ + console.log("launching 2nd rebase to change command of nth commit"); + read(); + + const nthCommit2ndRebase = 5; + + await gitStackedRebase(initialBranch.shorthand(), { + gitDir: dir, + getGitConfig: () => config, + editor: async ({ filePath }) => { + const SHA = commitOidsInLatestStacked[nthCommit2ndRebase].tostrS(); + + humanOpChangeCommandOfNthCommitInto("edit", SHA, filePath); + }, + }); + /** + * rebase will now exit because of the "edit" command, + * and so will our stacked rebase, + * allowing us to edit. + */ + + fs.writeFileSync(nthCommit2ndRebase.toString(), "new data from 2nd rebase\n"); + + execSyncInRepo(`${defaultGitCmd} add .`); + execSyncInRepo(`${defaultGitCmd} -c commit.gpgSign=false commit --amend --no-edit`); + + execSyncInRepo(`${defaultGitCmd} rebase --continue`); + + execSyncInRepo(`${defaultGitCmd} status`); + read(); + + /** + * now some partial branches will be "gone" from the POV of the latestBranch<->initialBranch. + * + * TODO verify they are gone (history got modified successfully) + */ + + // TODO + + /** + * TODO continue with --apply + * TODO and then verify that partial branches are "back" in our POV properly. + */ + + console.log("attempting early 3rd rebase to --apply"); + read(); + + await gitStackedRebase(initialBranch.shorthand(), { + gitDir: dir, + getGitConfig: () => config, + apply: true, + }); +} + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export async function setupRepo() { + const dir: string = path.join(__dirname, ".tmp"); + if (fs.existsSync(dir)) { + fs.rmdirSync(dir, { recursive: true }); + } + fs.mkdirSync(dir); + console.log("tmpdir path %s", dir); + + const foldersToDeletePath: string = path.join(__dirname, "folders-to-delete"); + fs.appendFileSync(foldersToDeletePath, dir + "\n", { encoding: "utf-8" }); + + process.chdir(dir); + console.log("chdir to tmpdir"); + + const isBare = 0; + const repo: Git.Repository = await Git.Repository.init(dir, isBare); + + const config: Git.Config = await repo.config(); + + await config.setBool(configKeys.autoApplyIfNeeded, Git.Config.MAP.FALSE); + await config.setString("user.email", "tester@test.com"); + await config.setString("user.name", "tester"); + + /** + * fixups / not implemented in libgit2. + * though, would be better if received empty/minimal config by default.. + */ + await config.setString("merge.conflictStyle", "diff3"); // zdiff3 + + const sig: Git.Signature = await Git.Signature.default(repo); + console.log("sig %s", sig); + + const inicialCommitId = "Initial commit (from setupRepo)"; + const initialCommit: Git.Oid = await fs.promises + .writeFile(inicialCommitId, inicialCommitId) // + .then(() => repo.createCommitOnHead([inicialCommitId], sig, sig, inicialCommitId)); + + return { + dir, + repo, + config, + sig, + initialCommit, + } as const; +} + +async function appendCommitsTo( + alreadyExistingCommits: Git.Oid[], + n: number, + repo: Git.Repository, // + sig: Git.Signature +): Promise { + assert(n > 0, "cannot append <= 0 commits"); + + const commits: string[] = new Array(n) + .fill(0) // + .map((_, i) => "a".charCodeAt(0) + i + alreadyExistingCommits.length) + .map((ascii) => String.fromCharCode(ascii)); + + for (const c of commits) { + const branchName: string = repo.isEmpty() ? "" : (await repo.getCurrentBranch()).shorthand(); + const cInBranch: string = c + " in " + branchName; + + const oid: Git.Oid = await fs.promises + .writeFile(c, cInBranch) // + .then(() => repo.createCommitOnHead([c], sig, sig, cInBranch)); + + alreadyExistingCommits.push(oid); + + console.log(`oid of commit "%s" in branch "%s": %s`, c, branchName, oid); + } + + return repo.getCurrentBranch(); +} + +/** + * TODO general "HumanOp" for `appendLineAfterNthCommit` & similar utils + */ +async function humanOpAppendLineAfterNthCommit( + filePath: string, // + commitSHA: string, + newLine: string +): Promise { + const file = await fs.promises.readFile(filePath, { encoding: "utf-8" }); + const lines = file.split("\n"); + const lineIdx: number = lines.findIndex((line) => line.startsWith(`pick ${commitSHA}`)); + + console.log("commitSHA: %s, lineIdx: %s, newLine: %s", commitSHA, lineIdx, newLine); + + lines.splice(lineIdx, 0, newLine); + + await fs.promises.writeFile(filePath, lines.join("\n")); +} + +function humanOpChangeCommandOfNthCommitInto( + newCommand: RegularRebaseCommand, // + commitSHA: string, + filePath: string +): void { + const file = fs.readFileSync(filePath, { encoding: "utf-8" }); + const lines = file.split("\n"); + const lineIdx: number = lines.findIndex((line) => line.startsWith(`pick ${commitSHA}`)); + + console.log("commitSHA: %s, lineIdx: %s, newCommand: %s", commitSHA, lineIdx, newCommand); + + const parts = lines[lineIdx].split(" "); + parts[0] = newCommand; + lines[lineIdx] = parts.join(" "); + + fs.writeFileSync(filePath, lines.join("\n")); +} diff --git a/test/run.ts b/test/run.ts new file mode 100644 index 00000000..7ffd3882 --- /dev/null +++ b/test/run.ts @@ -0,0 +1,13 @@ +#!/usr/bin/env ts-node-dev + +import { testCase } from "./experiment.spec"; + +main(); +function main() { + testCase() + .then(() => process.stdout.write("\nsuccess\n\n")) + .catch((e) => { + process.stderr.write("\nfailure: " + e + "\n\n"); + process.exit(1); + }); +}