From ecb44f4d7ffe86a6193a6829fe7879cc5660dace Mon Sep 17 00:00:00 2001 From: Grzegorz Uriasz Date: Thu, 13 Apr 2023 11:32:56 +0000 Subject: [PATCH] Support default branch names other than main or master --- app/agent/activities/get-default-branch.ts | 15 +++++++++ app/agent/git/git.service.test.ts | 13 +++++--- app/agent/git/git.service.ts | 31 +++++++++++++------ app/test-utils/constants.ts | 1 + .../migration.sql | 2 ++ prisma/schema.prisma | 11 ++++--- 6 files changed, 55 insertions(+), 18 deletions(-) create mode 100644 prisma/migrations/20230413111932_add_default_branch_to_git_repo/migration.sql diff --git a/app/agent/activities/get-default-branch.ts b/app/agent/activities/get-default-branch.ts index 42c85bbf..fab24446 100644 --- a/app/agent/activities/get-default-branch.ts +++ b/app/agent/activities/get-default-branch.ts @@ -6,6 +6,21 @@ export type GetDefaultBranchActivity = (gitRepositoryId: bigint) => Promise = ({ db }) => async (gitRepositoryId) => { + const repo = await db.gitRepository.findFirstOrThrow({ + where: { + gitRepositoryId, + }, + select: { defaultBranch: true }, + }); + const branch = await db.gitBranch.findFirst({ + where: { + gitRepositoryId, + name: repo.defaultBranch, + }, + }); + if (branch !== null) return branch; + + // If the remote ref is invalid then fallback to main or master const branches = await db.gitBranch.findMany({ where: { gitRepositoryId, diff --git a/app/agent/git/git.service.test.ts b/app/agent/git/git.service.test.ts index 900be8ef..7b89a0fe 100644 --- a/app/agent/git/git.service.test.ts +++ b/app/agent/git/git.service.test.ts @@ -18,20 +18,25 @@ test.concurrent( "getRemotes", provideInjector(async ({ injector }) => { const gitService = injector.resolve(Token.AgentGitService); - const remotesLinux = await gitService.getRemotes( + const linux = await gitService.getRemotes( "git@github.com:torvalds/linux.git", TESTS_PRIVATE_SSH_KEY, ); - const linuxMaster = remotesLinux.find((r) => r.name === "refs/heads/master"); + const linuxMaster = linux.remotes.find((r) => r.name === "refs/heads/master"); expect(linuxMaster).toBeDefined(); + expect(linux.defaultBranch).toEqual("refs/heads/master"); - const remotes = await gitService.getRemotes(TESTS_REPO_URL, TESTS_PRIVATE_SSH_KEY); - const testRemote = remotes.find( + const tests = await gitService.getRemotes(TESTS_REPO_URL, TESTS_PRIVATE_SSH_KEY); + const testRemote = tests.remotes.find( (r) => r.name === "refs/heads/git-service-test" && r.hash === "6aa1af1afb061b67d22e6bcd8a1d8d5bbec64987", ); expect(testRemote).toBeDefined(); + expect(tests.defaultBranch).toEqual("refs/heads/main"); + + const tests2 = await gitService.getRemotes(TESTS2_REPO_URL, TESTS_PRIVATE_SSH_KEY); + expect(tests.defaultBranch).toEqual("refs/heads/custom"); }), ); diff --git a/app/agent/git/git.service.ts b/app/agent/git/git.service.ts index 2d772733..efad30a3 100644 --- a/app/agent/git/git.service.ts +++ b/app/agent/git/git.service.ts @@ -59,10 +59,12 @@ export class AgentGitService { private parseLsRemoteOutput(output: string): { remotes: GitRemoteInfo[]; + defaultBranch: string; errors: ParseError[]; } { const remotes: GitRemoteInfo[] = []; const errors: ParseError[] = []; + let defaultBranch = ""; output .toString() @@ -70,6 +72,12 @@ export class AgentGitService { .filter((line) => line.length > 0) .forEach((line) => { const words = line.split(/\s/); + if (words.length > 0 && words[0] === "ref:") { + if (words.length === 3 && words[2] === "HEAD") { + defaultBranch = words[1]; + } + return; + } const result = RemoteInfoTupleValidator.SafeParse(words); if (result.success) { const value = result.value; @@ -79,7 +87,7 @@ export class AgentGitService { } }); - return { remotes, errors }; + return { remotes, defaultBranch, errors }; } private async writeKey(pathToPrivateKey: string, key: string): Promise { @@ -98,11 +106,14 @@ export class AgentGitService { * be a deploy key for a diffrerent repository. But GitHub must know * that it exists, otherwise it will reject the connection. */ - async getRemotes(repositoryUrl: string, privateKey: string): Promise { + async getRemotes( + repositoryUrl: string, + privateKey: string, + ): Promise<{ defaultBranch: string; remotes: GitRemoteInfo[] }> { const pathToPrivateKey = `/tmp/${uuidv4()}.key`; try { await this.writeKey(pathToPrivateKey, privateKey); - const output = await execCmdWithOpts(["git", "ls-remote", repositoryUrl], { + const output = await execCmdWithOpts(["git", "ls-remote", "--symref", repositoryUrl], { // TODO: allow the user to specify a known_hosts file. env: { GIT_SSH_COMMAND: `ssh -i "${pathToPrivateKey}" -o StrictHostKeyChecking=no` }, }); @@ -112,7 +123,7 @@ export class AgentGitService { `Failed to parse git ls-remote output:\n${error.message}\nOffending value: "${value}"`, ); } - return result.remotes; + return { remotes: result.remotes, defaultBranch: result.defaultBranch }; } finally { await fs.unlink(pathToPrivateKey).catch((err) => { this.logger.warn( @@ -161,10 +172,8 @@ export class AgentGitService { }, }); let gitRepository = await getGitRepo(db); - const newRemotes = await this.getRemotes( - gitRepository.url, - gitRepository.sshKeyPair.privateKey, - ).then((rs) => rs.filter((remote) => this.branchRefRegex.test(remote.name))); + const lsRemotes = await this.getRemotes(gitRepository.url, gitRepository.sshKeyPair.privateKey); + const newRemotes = lsRemotes.remotes.filter((remote) => this.branchRefRegex.test(remote.name)); return await db.$transaction(async (tdb) => { await tdb.$executeRaw`SELECT id FROM "GitRepository" WHERE id = ${gitRepositoryId} FOR UPDATE`; gitRepository = await getGitRepo(tdb); @@ -223,10 +232,14 @@ export class AgentGitService { }), ), ); - if (newGitBranches.length + updatedGitBranches.length > 0) { + if ( + newGitBranches.length + updatedGitBranches.length > 0 || + gitRepository.defaultBranch !== lsRemotes.defaultBranch + ) { await tdb.gitRepository.update({ where: { id: gitRepositoryId }, data: { + defaultBranch: lsRemotes.defaultBranch, lastBranchUpdateAt: new Date(), }, }); diff --git a/app/test-utils/constants.ts b/app/test-utils/constants.ts index 418b0625..9120e4d2 100644 --- a/app/test-utils/constants.ts +++ b/app/test-utils/constants.ts @@ -8,4 +8,5 @@ W71TsDKa/ovAG6nXpmidAAAAF2hvY3VzLXRlc3RzQGV4YW1wbGUuY29tAQIDBAUG -----END OPENSSH PRIVATE KEY----- `; export const TESTS_REPO_URL = "git@github.com:hocus-dev/tests.git"; +export const TESTS2_REPO_URL = "git@github.com:hocus-dev/tests2.git"; export const HOCUS_REPO_URL = "git@github.com:hocus-dev/hocus.git"; diff --git a/prisma/migrations/20230413111932_add_default_branch_to_git_repo/migration.sql b/prisma/migrations/20230413111932_add_default_branch_to_git_repo/migration.sql new file mode 100644 index 00000000..67367847 --- /dev/null +++ b/prisma/migrations/20230413111932_add_default_branch_to_git_repo/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "GitRepository" ADD COLUMN "defaultBranch" TEXT NOT NULL DEFAULT ''; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 02f6dda8..f8f95629 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -216,11 +216,12 @@ model SshKeyPair { } model GitRepository { - id BigInt @id @default(autoincrement()) - url String @unique - sshKeyPair SshKeyPair @relation(fields: [sshKeyPairId], references: [id]) - sshKeyPairId BigInt - gitBranches GitBranch[] + id BigInt @id @default(autoincrement()) + url String @unique + defaultBranch String @default("") + sshKeyPair SshKeyPair @relation(fields: [sshKeyPairId], references: [id]) + sshKeyPairId BigInt + gitBranches GitBranch[] lastBranchUpdateAt DateTime @default(now()) @db.Timestamptz(3) createdAt DateTime @default(now()) @db.Timestamptz(3)