diff --git a/src/__tests__/fixtures/ssh-gitmodules.ini b/src/__tests__/fixtures/ssh-gitmodules.ini new file mode 100644 index 0000000..970ee15 --- /dev/null +++ b/src/__tests__/fixtures/ssh-gitmodules.ini @@ -0,0 +1,3 @@ +[submodule "ports/mdBook"] + path = ports/mdBook + url = git@github.com:catppuccin/mdBook.git diff --git a/src/__tests__/main.test.ts b/src/__tests__/main.test.ts index b12dd55..bcaa02e 100644 --- a/src/__tests__/main.test.ts +++ b/src/__tests__/main.test.ts @@ -9,9 +9,14 @@ import { Submodule, updateToLatestCommit, updateToLatestTag, + getRemoteName, } from "../main"; import { getExecOutput } from "@actions/exec"; -import { mdBookSubmodule, nvimSubmodule, vscodeIconsSubmodule } from "./utils"; +import { + mdBookSubmodule, + nvimSubmodule, + vscodeIconsSubmodule +} from "./utils"; import { getInput, setOutput } from "@actions/core"; vi.mock("@actions/core", async () => { @@ -62,6 +67,37 @@ test("parse GitHub Action inputs with no input submodules", async () => { expect(actual).toEqual(expected); }); +test.each([ + ["ssh://user@host.xz:port/path/to/repo.git/", "port/path/to/repo"], + ["ssh://user@host.xz/path/to/repo.git/", "path/to/repo"], + ["ssh://host.xz:port/path/to/repo.git/", "port/path/to/repo"], + ["ssh://host.xz/path/to/repo.git/", "path/to/repo"], + ["ssh://user@host.xz/path/to/repo.git/", "path/to/repo"], + ["ssh://host.xz/path/to/repo.git/", "path/to/repo"], + ["ssh://user@host.xz/~user/path/to/repo.git/", "user/path/to/repo"], + ["ssh://host.xz/~user/path/to/repo.git/", "user/path/to/repo"], + ["ssh://user@host.xz/~/path/to/repo.git", "path/to/repo"], + ["ssh://host.xz/~/path/to/repo.git", "path/to/repo"], + ["user@host.xz:/path/to/repo.git/", "path/to/repo"], + ["host.xz:/path/to/repo.git/", "path/to/repo"], + ["user@host.xz:~user/path/to/repo.git/", "user/path/to/repo"], + ["host.xz:~user/path/to/repo.git/", "user/path/to/repo"], + ["user@host.xz:path/to/repo.git", "path/to/repo"], + ["host.xz:path/to/repo.git", "path/to/repo"], + ["rsync://host.xz/path/to/repo.git/", "path/to/repo"], + ["git://host.xz/path/to/repo.git/", "path/to/repo"], + ["git://host.xz/~user/path/to/repo.git/", "user/path/to/repo"], + ["http://host.xz/path/to/repo.git/", "path/to/repo"], + ["https://host.xz/path/to/repo.git/", "path/to/repo"], + ["/path/to/repo.git/", "path/to/repo"], + ["path/to/repo.git/", "path/to/repo"], + ["~/path/to/repo.git", "path/to/repo"], + ["file:///path/to/repo.git/", "path/to/repo"], + ["file://~/path/to/repo.git/", "path/to/repo"], +])('getRemoteName(%s) -> %s', (url, expected) => { + expect(getRemoteName(url)).toBe(expected) +}) + test("extract single submodule from .gitmodules", async () => { const input = await readFile("src/__tests__/fixtures/single-gitmodules.ini"); const expected = [mdBookSubmodule()]; @@ -93,6 +129,39 @@ test("extract single submodule from .gitmodules", async () => { expect(actual).toEqual(expected); }); +test("extract single submodule from .gitmodules with ssh-style url", async () => { + const input = await readFile("src/__tests__/fixtures/ssh-gitmodules.ini"); + const submodule = mdBookSubmodule() + submodule.url = "git@github.com:catppuccin/mdBook.git" + const expected = [submodule]; + + vi.mocked(getExecOutput) + .mockReturnValueOnce( + Promise.resolve({ + exitCode: 0, + stdout: `\n${expected[0].previousCommitSha}`, + stderr: "", + }) + ) + .mockReturnValueOnce( + Promise.resolve({ + exitCode: 0, + stdout: `\n${expected[0].previousTag}`, + stderr: "", + }) + ) + .mockReturnValueOnce( + Promise.resolve({ + exitCode: 0, + stdout: `\n${expected[0].previousTag}`, + stderr: "", + }) + ); + + const actual = await parseGitmodules(input); + expect(actual).toEqual(expected); +}); + test("extract single submodule from .gitmodules that has no tags", async () => { const input = await readFile("src/__tests__/fixtures/single-gitmodules.ini"); const expected = [mdBookSubmodule()]; diff --git a/src/main.ts b/src/main.ts index d5e200a..c8d46a5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,8 +14,8 @@ const gitmodulesSchema = z.record( z.string(), z.object({ path: z.string(), - url: z.string().url(), - }) + url: z.string().regex(/[A-Za-z][A-Za-z0-9+.-]*/), + }), ); export type Inputs = { @@ -84,6 +84,34 @@ export const readFile = async (path: string): Promise => { }); }; +export const getRemoteName = (url: string) => { + url = url.replace(/\.git\/?/, "") + + let startIndex = url.length - 1; + + // Scan backwards to find separator. + while (startIndex >= 0) { + if (url[startIndex] == "~" || url[startIndex] == ":") { + startIndex++; + break; + } else if (url[startIndex] == ".") { + break; + } + + startIndex--; + } + + // If we broke on a dot, we _probably_ hit a domain label, so + // scan forward until we hit a slash. + if (url[startIndex] == ".") { + while (url[startIndex] != "/") { + startIndex++; + } + } + + return url.substring(startIndex).replace(/^\/+/, ""); +} + export const parseGitmodules = async ( content: string ): Promise => { @@ -94,8 +122,7 @@ export const parseGitmodules = async ( const name = key.split('"')[1].trim(); const path = values.path.replace(/"/g, "").trim(); const url = values.url.replace(/"/g, "").trim(); - const urlParts = url.replace(".git", "").split("/"); - const remoteName = `${urlParts[3]}/${urlParts[4]}`; + const remoteName = getRemoteName(url); const [previousCommitSha, previousShortCommitSha] = await getCommit(path); const previousCommitShaHasTag = await hasTag(path, previousCommitSha); const previousTag = await getPreviousTag(path);