Skip to content

Commit 058d448

Browse files
MrFlynnsgoudham
andcommitted
fix: support multiple URL protocols (#76)
* Loosened validation of submodule URL. These aren't strictly "URLs" in the web sense. They may be a variety of formats and don't strictly conform to HTTP-style URLs, like SSH style URLs. This change aligns the validation with Git's own URL validation. While the validation doesn't necessarily gaurantee that the URL is actually valid, calling the checkout action (which would be necessarily to use this action anyway) would fail before this if it couldn't clone a submodule due to a bad URL. - Switched from `.url()` to `.regex()` string validator using the regex present in url.c[1] in Git's source code. - Added unit test and test fixture to check compatibility with SSH-style URLs. [1]: https://github.com/git/git/blob/77bd3ea9f54f1584147b594abc04c26ca516d987/url.c#L6-L12 * Improved remote name extraction. Since the action no longer accepts HTTP-style URLs, we need to account for more cases. This commit adds a function that attempts a "best guess" remote name extraction. It looks for specific characters in the URL that probably demarcate the remote name. It's not a perfect solution, but it works well in the vast majority of cases. - Added `getRemoteName` function to get remote name. - Added unit tests with a bunch of example cases that cover a gamut of different URL formats. The test cases were taken from this Stack Overflow post: https://stackoverflow.com/questions/31801271/what-are-the-supported-git-url-formats * chore: delete helper function & rename test --------- Co-authored-by: sgoudham <sgoudham@gmail.com>
1 parent 1667558 commit 058d448

File tree

5 files changed

+132
-10
lines changed

5 files changed

+132
-10
lines changed

dist/index.js

Lines changed: 27 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "ports/mdBook"]
2+
path = ports/mdBook
3+
url = git@github.com:catppuccin/mdBook.git

src/__tests__/main.test.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@ import {
99
Submodule,
1010
updateToLatestCommit,
1111
updateToLatestTag,
12+
getRemoteName,
1213
} from "../main";
1314
import { getExecOutput } from "@actions/exec";
14-
import { mdBookSubmodule, nvimSubmodule, vscodeIconsSubmodule } from "./utils";
15+
import {
16+
mdBookSubmodule,
17+
nvimSubmodule,
18+
vscodeIconsSubmodule
19+
} from "./utils";
1520
import { getInput, setOutput } from "@actions/core";
1621

1722
vi.mock("@actions/core", async () => {
@@ -62,6 +67,37 @@ test("parse GitHub Action inputs with no input submodules", async () => {
6267
expect(actual).toEqual(expected);
6368
});
6469

70+
test.each([
71+
["ssh://user@host.xz:port/path/to/repo.git/", "port/path/to/repo"],
72+
["ssh://user@host.xz/path/to/repo.git/", "path/to/repo"],
73+
["ssh://host.xz:port/path/to/repo.git/", "port/path/to/repo"],
74+
["ssh://host.xz/path/to/repo.git/", "path/to/repo"],
75+
["ssh://user@host.xz/path/to/repo.git/", "path/to/repo"],
76+
["ssh://host.xz/path/to/repo.git/", "path/to/repo"],
77+
["ssh://user@host.xz/~user/path/to/repo.git/", "user/path/to/repo"],
78+
["ssh://host.xz/~user/path/to/repo.git/", "user/path/to/repo"],
79+
["ssh://user@host.xz/~/path/to/repo.git", "path/to/repo"],
80+
["ssh://host.xz/~/path/to/repo.git", "path/to/repo"],
81+
["user@host.xz:/path/to/repo.git/", "path/to/repo"],
82+
["host.xz:/path/to/repo.git/", "path/to/repo"],
83+
["user@host.xz:~user/path/to/repo.git/", "user/path/to/repo"],
84+
["host.xz:~user/path/to/repo.git/", "user/path/to/repo"],
85+
["user@host.xz:path/to/repo.git", "path/to/repo"],
86+
["host.xz:path/to/repo.git", "path/to/repo"],
87+
["rsync://host.xz/path/to/repo.git/", "path/to/repo"],
88+
["git://host.xz/path/to/repo.git/", "path/to/repo"],
89+
["git://host.xz/~user/path/to/repo.git/", "user/path/to/repo"],
90+
["http://host.xz/path/to/repo.git/", "path/to/repo"],
91+
["https://host.xz/path/to/repo.git/", "path/to/repo"],
92+
["/path/to/repo.git/", "path/to/repo"],
93+
["path/to/repo.git/", "path/to/repo"],
94+
["~/path/to/repo.git", "path/to/repo"],
95+
["file:///path/to/repo.git/", "path/to/repo"],
96+
["file://~/path/to/repo.git/", "path/to/repo"],
97+
])('getRemoteName(%s) -> %s', (url, expected) => {
98+
expect(getRemoteName(url)).toBe(expected)
99+
})
100+
65101
test("extract single submodule from .gitmodules", async () => {
66102
const input = await readFile("src/__tests__/fixtures/single-gitmodules.ini");
67103
const expected = [mdBookSubmodule()];
@@ -93,6 +129,39 @@ test("extract single submodule from .gitmodules", async () => {
93129
expect(actual).toEqual(expected);
94130
});
95131

132+
test("extract single submodule from .gitmodules with ssh-style url", async () => {
133+
const input = await readFile("src/__tests__/fixtures/ssh-gitmodules.ini");
134+
const submodule = mdBookSubmodule()
135+
submodule.url = "git@github.com:catppuccin/mdBook.git"
136+
const expected = [submodule];
137+
138+
vi.mocked(getExecOutput)
139+
.mockReturnValueOnce(
140+
Promise.resolve({
141+
exitCode: 0,
142+
stdout: `\n${expected[0].previousCommitSha}`,
143+
stderr: "",
144+
})
145+
)
146+
.mockReturnValueOnce(
147+
Promise.resolve({
148+
exitCode: 0,
149+
stdout: `\n${expected[0].previousTag}`,
150+
stderr: "",
151+
})
152+
)
153+
.mockReturnValueOnce(
154+
Promise.resolve({
155+
exitCode: 0,
156+
stdout: `\n${expected[0].previousTag}`,
157+
stderr: "",
158+
})
159+
);
160+
161+
const actual = await parseGitmodules(input);
162+
expect(actual).toEqual(expected);
163+
});
164+
96165
test("extract single submodule from .gitmodules that has no tags", async () => {
97166
const input = await readFile("src/__tests__/fixtures/single-gitmodules.ini");
98167
const expected = [mdBookSubmodule()];

src/main.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ const gitmodulesSchema = z.record(
1414
z.string(),
1515
z.object({
1616
path: z.string(),
17-
url: z.string().url(),
18-
})
17+
url: z.string().regex(/[A-Za-z][A-Za-z0-9+.-]*/),
18+
}),
1919
);
2020

2121
export type Inputs = {
@@ -84,6 +84,34 @@ export const readFile = async (path: string): Promise<string> => {
8484
});
8585
};
8686

87+
export const getRemoteName = (url: string) => {
88+
url = url.replace(/\.git\/?/, "")
89+
90+
let startIndex = url.length - 1;
91+
92+
// Scan backwards to find separator.
93+
while (startIndex >= 0) {
94+
if (url[startIndex] == "~" || url[startIndex] == ":") {
95+
startIndex++;
96+
break;
97+
} else if (url[startIndex] == ".") {
98+
break;
99+
}
100+
101+
startIndex--;
102+
}
103+
104+
// If we broke on a dot, we _probably_ hit a domain label, so
105+
// scan forward until we hit a slash.
106+
if (url[startIndex] == ".") {
107+
while (url[startIndex] != "/") {
108+
startIndex++;
109+
}
110+
}
111+
112+
return url.substring(startIndex).replace(/^\/+/, "");
113+
}
114+
87115
export const parseGitmodules = async (
88116
content: string
89117
): Promise<Submodule[]> => {
@@ -94,8 +122,7 @@ export const parseGitmodules = async (
94122
const name = key.split('"')[1].trim();
95123
const path = values.path.replace(/"/g, "").trim();
96124
const url = values.url.replace(/"/g, "").trim();
97-
const urlParts = url.replace(".git", "").split("/");
98-
const remoteName = `${urlParts[3]}/${urlParts[4]}`;
125+
const remoteName = getRemoteName(url);
99126
const [previousCommitSha, previousShortCommitSha] = await getCommit(path);
100127
const previousCommitShaHasTag = await hasTag(path, previousCommitSha);
101128
const previousTag = await getPreviousTag(path);

0 commit comments

Comments
 (0)