From 1ee8ba4f9537ffabf41d5a7dff295194d1b77c4b Mon Sep 17 00:00:00 2001 From: Sam Lanning Date: Sat, 27 Jul 2024 17:59:03 +0100 Subject: [PATCH] Add isomorphic git to project, and allow different bases (#8) * Add isomorphic-git to the project This required the typescript module resolution mode. * Allow for different types of bases Allow for tags, either the same or another branch, or a commit hash to be used as a base. * Add tests for using commits / tags as base * Fix bug using tag as base --- package.json | 3 + pnpm-lock.yaml | 94 ++++++++++++++++++++++++++ src/core.ts | 108 ++++++++++++++++++++++++------ src/fs.ts | 4 ++ src/github/graphql/queries.ts | 5 ++ src/test/integration/fs.test.ts | 4 +- src/test/integration/node.test.ts | 60 ++++++++++++++++- tsconfig.base.json | 4 +- 8 files changed, 256 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index e08ae85..551fc7f 100644 --- a/package.json +++ b/package.json @@ -68,5 +68,8 @@ "packageManager": "pnpm@9.5.0", "publishConfig": { "access": "public" + }, + "dependencies": { + "isomorphic-git": "^1.27.1" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 937cab5..ccf2325 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,10 @@ settings: importers: .: + dependencies: + isomorphic-git: + specifier: ^1.27.1 + version: 1.27.1 devDependencies: '@actions/github': specifier: ^6.0.0 @@ -1492,6 +1496,9 @@ packages: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} + async-lock@1.4.1: + resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} + async@3.2.5: resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} @@ -1660,6 +1667,9 @@ packages: cjs-module-lexer@1.3.1: resolution: {integrity: sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==} + clean-git-ref@2.0.1: + resolution: {integrity: sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==} + clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -1740,6 +1750,11 @@ packages: typescript: optional: true + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + create-jest@29.7.0: resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1784,6 +1799,10 @@ packages: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dedent@1.5.3: resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} peerDependencies: @@ -1821,6 +1840,9 @@ packages: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + diff3@0.0.3: + resolution: {integrity: sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==} + diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} @@ -2331,6 +2353,11 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isomorphic-git@1.27.1: + resolution: {integrity: sha512-X32ph5zIWfT75QAqW2l3JCIqnx9/GWd17bRRehmn3qmWc34OYbSXY6Cxv0o9bIIY+CWugoN4nQFHNA+2uYf2nA==} + engines: {node: '>=12'} + hasBin: true + isomorphic-ws@5.0.0: resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} peerDependencies: @@ -2675,6 +2702,10 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -2693,6 +2724,9 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minimisted@2.0.1: + resolution: {integrity: sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==} + minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} @@ -2811,6 +2845,9 @@ packages: package-json-from-dist@1.0.0: resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + param-case@3.0.4: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} @@ -3103,6 +3140,10 @@ packages: setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + sha.js@2.4.11: + resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} + hasBin: true + shebang-command@1.2.0: resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} engines: {node: '>=0.10.0'} @@ -3132,6 +3173,12 @@ packages: signedsource@1.0.0: resolution: {integrity: sha512-6+eerH9fEnNmi/hyM1DXcRK3pWdoMQtlkQ+ns0ntzunjKqp5i3sKCc80ym8Fib3iaYhdJUOPdhlJWj1tvge2Ww==} + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -5511,6 +5558,8 @@ snapshots: astral-regex@2.0.0: {} + async-lock@1.4.1: {} + async@3.2.5: {} atomic-sleep@1.0.0: {} @@ -5766,6 +5815,8 @@ snapshots: cjs-module-lexer@1.3.1: {} + clean-git-ref@2.0.1: {} + clean-stack@2.2.0: {} cli-cursor@3.1.0: @@ -5836,6 +5887,8 @@ snapshots: optionalDependencies: typescript: 5.5.3 + crc-32@1.2.2: {} + create-jest@29.7.0(@types/node@20.14.10)(ts-node@10.9.2(@types/node@20.14.10)(typescript@5.5.3)): dependencies: '@jest/types': 29.6.3 @@ -5887,6 +5940,10 @@ snapshots: decamelize@1.2.0: {} + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + dedent@1.5.3: {} deep-is@0.1.4: {} @@ -5907,6 +5964,8 @@ snapshots: diff-sequences@29.6.3: {} + diff3@0.0.3: {} + diff@4.0.2: {} dir-glob@3.0.1: @@ -6467,6 +6526,20 @@ snapshots: isexe@2.0.0: {} + isomorphic-git@1.27.1: + dependencies: + async-lock: 1.4.1 + clean-git-ref: 2.0.1 + crc-32: 1.2.2 + diff3: 0.0.3 + ignore: 5.3.1 + minimisted: 2.0.1 + pako: 1.0.11 + pify: 4.0.1 + readable-stream: 3.6.2 + sha.js: 2.4.11 + simple-get: 4.0.1 + isomorphic-ws@5.0.0(ws@8.18.0): dependencies: ws: 8.18.0 @@ -6991,6 +7064,8 @@ snapshots: mimic-fn@2.1.0: {} + mimic-response@3.1.0: {} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -7009,6 +7084,10 @@ snapshots: minimist@1.2.8: {} + minimisted@2.0.1: + dependencies: + minimist: 1.2.8 + minipass@7.1.2: {} mri@1.2.0: {} @@ -7117,6 +7196,8 @@ snapshots: package-json-from-dist@1.0.0: {} + pako@1.0.11: {} + param-case@3.0.4: dependencies: dot-case: 3.0.4 @@ -7416,6 +7497,11 @@ snapshots: setimmediate@1.0.5: {} + sha.js@2.4.11: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + shebang-command@1.2.0: dependencies: shebang-regex: 1.0.0 @@ -7436,6 +7522,14 @@ snapshots: signedsource@1.0.0: {} + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + sisteransi@1.0.5: {} slash@3.0.0: {} diff --git a/src/core.ts b/src/core.ts index 81a352b..d52eace 100644 --- a/src/core.ts +++ b/src/core.ts @@ -8,22 +8,36 @@ import { getRepositoryMetadata, GitHubClient, } from "./github/graphql/queries.js"; -import type { CreateCommitOnBranchMutationVariables } from "./github/graphql/generated/operations.js"; +import type { + CreateCommitOnBranchMutationVariables, + GetRepositoryMetadataQuery, +} from "./github/graphql/generated/operations.js"; import type { Logger } from "./logging.js"; export type CommitFilesResult = { refId: string | null; }; +export type GitBase = + | { + branch: string; + } + | { + tag: string; + } + | { + commit: string; + }; + export type CommitFilesFromBase64Args = { octokit: GitHubClient; owner: string; repository: string; branch: string; /** - * The current commit that the target branch is at + * The current branch, tag or commit that the new branch should be based on. */ - baseBranch: string; + base: GitBase; /** * The commit message */ @@ -32,18 +46,47 @@ export type CommitFilesFromBase64Args = { log?: Logger; }; +const getBaseRef = (base: GitBase): string => { + if ("branch" in base) { + return `refs/heads/${base.branch}`; + } else if ("tag" in base) { + return `refs/tags/${base.tag}`; + } else { + return "HEAD"; + } +}; + +const getOidFromRef = ( + base: GitBase, + ref: (GetRepositoryMetadataQuery["repository"] & Record)["ref"], +) => { + if ("commit" in base) { + return base.commit; + } + + if (!ref?.target) { + throw new Error(`Could not determine oid from ref: ${JSON.stringify(ref)}`); + } + + if ("target" in ref.target) { + return ref.target.target.oid; + } + + return ref.target.oid; +}; + export const commitFilesFromBase64 = async ({ octokit, owner, repository, branch, - baseBranch, + base, message, fileChanges, log, }: CommitFilesFromBase64Args): Promise => { const repositoryNameWithOwner = `${owner}/${repository}`; - const baseRef = `refs/heads/${baseBranch}`; + const baseRef = getBaseRef(base); log?.debug(`Getting repo info ${repositoryNameWithOwner}`); const info = await getRepositoryMetadata(octokit, { @@ -57,36 +100,57 @@ export const commitFilesFromBase64 = async ({ throw new Error(`Repository ${repositoryNameWithOwner} not found`); } - const oid = info.ref?.target?.oid; - - if (!info) { + if (!info.ref) { throw new Error(`Ref ${baseRef} not found`); } - log?.debug(`Creating branch ${branch} from commit ${oid}}`); - const refId = await createRefMutation(octokit, { - input: { - repositoryId: info.id, - name: `refs/heads/${branch}`, - oid, - }, - }); + const repositoryId = info.id; + /** + * The commit oid to base the new commit on. + * + * Used both to create / update the new branch (if necessary), + * and th ensure no changes have been made as we push the new commit. + */ + const baseOid = getOidFromRef(base, info.ref); + + let refId: string; + + if ("branch" in base && base.branch === branch) { + log?.debug(`Committing to the same branch as base: ${branch} (${baseOid})`); + // Get existing branch refId + refId = info.ref.id; + } else { + // Create branch as not committing to same branch + // TODO: detect if branch already exists, and overwrite if so + log?.debug(`Creating branch ${branch} from commit ${baseOid}}`); + const refIdCreation = await createRefMutation(octokit, { + input: { + repositoryId, + name: `refs/heads/${branch}`, + oid: baseOid, + }, + }); + + log?.debug( + `Created branch with refId ${JSON.stringify(refIdCreation, null, 2)}`, + ); - log?.debug(`Created branch with refId ${JSON.stringify(refId, null, 2)}`); + const refIdStr = refIdCreation.createRef?.ref?.id; - const refIdStr = refId.createRef?.ref?.id; + if (!refIdStr) { + throw new Error(`Failed to create branch ${branch}`); + } - if (!refIdStr) { - throw new Error(`Failed to create branch ${branch}`); + refId = refIdStr; } await log?.debug(`Creating commit on branch ${branch}`); const createCommitMutation: CreateCommitOnBranchMutationVariables = { input: { branch: { - id: refIdStr, + id: refId, }, - expectedHeadOid: oid, + expectedHeadOid: baseOid, message, fileChanges, }, diff --git a/src/fs.ts b/src/fs.ts index 1aa136b..13fe462 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -3,6 +3,7 @@ import * as path from "path"; import type { FileAddition } from "./github/graphql/generated/types.js"; import { CommitFilesFromBase64Args, CommitFilesResult } from "./core.js"; import { commitFilesFromBuffers } from "./node.js"; +import git from "isomorphic-git"; export type CommitFilesFromDirectoryArgs = Omit< CommitFilesFromBase64Args, @@ -41,3 +42,6 @@ export const commitFilesFromDirectory = async ({ }, }); }; + +// TODO: remove +export { git }; diff --git a/src/github/graphql/queries.ts b/src/github/graphql/queries.ts index 04caea3..6562f35 100644 --- a/src/github/graphql/queries.ts +++ b/src/github/graphql/queries.ts @@ -21,6 +21,11 @@ const GET_REPOSITORY_METADATA = /* GraphQL */ ` id target { oid + ... on Tag { + target { + oid + } + } } } } diff --git a/src/test/integration/fs.test.ts b/src/test/integration/fs.test.ts index 386392c..34eaef6 100644 --- a/src/test/integration/fs.test.ts +++ b/src/test/integration/fs.test.ts @@ -30,7 +30,9 @@ describe("fs", () => { octokit, ...REPO, branch: TEST_BRANCHES.COMMIT_FILE, - baseBranch: "main", + base: { + branch: "main", + }, message: { headline: "Test commit", body: "This is a test commit", diff --git a/src/test/integration/node.test.ts b/src/test/integration/node.test.ts index 9b0ab87..1c239ea 100644 --- a/src/test/integration/node.test.ts +++ b/src/test/integration/node.test.ts @@ -32,7 +32,9 @@ describe("node", () => { octokit, ...REPO, branch, - baseBranch: "main", + base: { + branch: "main", + }, message: { headline: "Test commit", body: "This is a test commit", @@ -50,6 +52,62 @@ describe("node", () => { }); } }); + + it("can commit using tag as a base", async () => { + const branch = `${TEST_BRANCH_PREFIX}-tag-base`; + branches.push(branch); + const contents = Buffer.alloc(1024, "Hello, world!"); + + await commitFilesFromBuffers({ + octokit, + ...REPO, + branch, + base: { + tag: "v0.1.0", + }, + message: { + headline: "Test commit", + body: "This is a test commit", + }, + fileChanges: { + additions: [ + { + path: `foo.txt`, + contents, + }, + ], + }, + log, + }); + }); + + it("can commit using commit as a base", async () => { + const branch = `${TEST_BRANCH_PREFIX}-commit-base`; + branches.push(branch); + const contents = Buffer.alloc(1024, "Hello, world!"); + + await commitFilesFromBuffers({ + octokit, + ...REPO, + branch, + base: { + commit: "fce2760017eab6d85388ed5cfdfac171559d80b3", + }, + message: { + headline: "Test commit", + body: "This is a test commit", + }, + fileChanges: { + additions: [ + { + path: `foo.txt`, + contents, + }, + ], + }, + log, + }); + }); }); afterAll(async () => { diff --git a/tsconfig.base.json b/tsconfig.base.json index 60d4343..7d30e37 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -8,9 +8,9 @@ "incremental": false, "isolatedModules": true, "lib": ["es2022", "DOM", "DOM.Iterable"], - "module": "NodeNext", + "module": "ESNext", "moduleDetection": "force", - "moduleResolution": "NodeNext", + "moduleResolution": "Bundler", "noUncheckedIndexedAccess": true, "resolveJsonModule": true, "skipLibCheck": true,