@superblocksteam/shared 0.9569.3 → 0.9570.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/utils/git-url.d.ts +38 -0
- package/dist/utils/git-url.d.ts.map +1 -0
- package/dist/utils/git-url.js +153 -0
- package/dist/utils/git-url.js.map +1 -0
- package/dist/utils/git-url.test.d.ts +2 -0
- package/dist/utils/git-url.test.d.ts.map +1 -0
- package/dist/utils/git-url.test.js +78 -0
- package/dist/utils/git-url.test.js.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist-esm/utils/git-url.d.ts +38 -0
- package/dist-esm/utils/git-url.d.ts.map +1 -0
- package/dist-esm/utils/git-url.js +143 -0
- package/dist-esm/utils/git-url.js.map +1 -0
- package/dist-esm/utils/git-url.test.d.ts +2 -0
- package/dist-esm/utils/git-url.test.d.ts.map +1 -0
- package/dist-esm/utils/git-url.test.js +76 -0
- package/dist-esm/utils/git-url.test.js.map +1 -0
- package/dist-esm/utils/index.d.ts +1 -0
- package/dist-esm/utils/index.d.ts.map +1 -1
- package/dist-esm/utils/index.js +1 -0
- package/dist-esm/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/utils/git-url.test.ts +101 -0
- package/src/utils/git-url.ts +175 -0
- package/src/utils/index.ts +1 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git remote URL utilities — GitHub-focused.
|
|
3
|
+
*
|
|
4
|
+
* Parsing, validation, and compare-URL generation for git remote URLs.
|
|
5
|
+
* Used by both the UI and the vite-plugin-file-sync AI agent tooling.
|
|
6
|
+
*/
|
|
7
|
+
export declare const GITHUB_PAT_SETTINGS_URL = "https://github.com/settings/tokens";
|
|
8
|
+
export declare function isValidGitUrl(url: string): boolean;
|
|
9
|
+
export declare function isGitHubUrl(url: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Converts a git remote URL (HTTPS or SSH) to a browser-friendly HTTPS URL.
|
|
12
|
+
* Returns `null` when the URL cannot be converted.
|
|
13
|
+
*
|
|
14
|
+
* https://github.com/org/repo.git → https://github.com/org/repo
|
|
15
|
+
* git@github.com:org/repo.git → https://github.com/org/repo
|
|
16
|
+
*/
|
|
17
|
+
export declare function gitRemoteUrlToWeb(url: string): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* Returns a URL that opens the "new pull request" compare screen on GitHub.
|
|
20
|
+
* Returns `null` when the remote URL is not a GitHub URL or cannot be parsed.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getGitHubCompareUrl({ gitRemoteUrl, fromBranch, toBranch, title, body }: {
|
|
23
|
+
gitRemoteUrl: string;
|
|
24
|
+
fromBranch: string;
|
|
25
|
+
toBranch: string;
|
|
26
|
+
title?: string;
|
|
27
|
+
body?: string;
|
|
28
|
+
}): string | null;
|
|
29
|
+
export declare function isGitCommitHash(value: string): boolean;
|
|
30
|
+
export declare function getCommitUrlForHash(params: {
|
|
31
|
+
hash: string;
|
|
32
|
+
repositoryWebUrl: string;
|
|
33
|
+
}): string | null;
|
|
34
|
+
export declare function isCommitUrlForHash(params: {
|
|
35
|
+
hash: string;
|
|
36
|
+
url: string;
|
|
37
|
+
}): boolean;
|
|
38
|
+
//# sourceMappingURL=git-url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-url.d.ts","sourceRoot":"","sources":["../../src/utils/git-url.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,eAAO,MAAM,uBAAuB,uCAAuC,CAAC;AAE5E,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGlD;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAchD;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkB5D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,KAAK,EACL,IAAI,EACL,EAAE;IACD,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,GAAG,IAAI,CAchB;AA2CD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,GAAG,IAAI,CAerG;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAoBjF"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Git remote URL utilities — GitHub-focused.
|
|
4
|
+
*
|
|
5
|
+
* Parsing, validation, and compare-URL generation for git remote URLs.
|
|
6
|
+
* Used by both the UI and the vite-plugin-file-sync AI agent tooling.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.GITHUB_PAT_SETTINGS_URL = void 0;
|
|
10
|
+
exports.isValidGitUrl = isValidGitUrl;
|
|
11
|
+
exports.isGitHubUrl = isGitHubUrl;
|
|
12
|
+
exports.gitRemoteUrlToWeb = gitRemoteUrlToWeb;
|
|
13
|
+
exports.getGitHubCompareUrl = getGitHubCompareUrl;
|
|
14
|
+
exports.isGitCommitHash = isGitCommitHash;
|
|
15
|
+
exports.getCommitUrlForHash = getCommitUrlForHash;
|
|
16
|
+
exports.isCommitUrlForHash = isCommitUrlForHash;
|
|
17
|
+
const HTTPS_GIT_URL_RE = /^https?:\/\/[^\s/$.?#]+\.[^\s/$.?#]+\/[^\s/$.?#]+(\/[^\s/$.?#]+)+(\.git)?\/?$/i;
|
|
18
|
+
const SSH_GIT_URL_RE = /^[\w-]+@[\w.-]+:[\w./-]+(\.git)?$/;
|
|
19
|
+
exports.GITHUB_PAT_SETTINGS_URL = 'https://github.com/settings/tokens';
|
|
20
|
+
function isValidGitUrl(url) {
|
|
21
|
+
const trimmed = url.trim();
|
|
22
|
+
return HTTPS_GIT_URL_RE.test(trimmed) || SSH_GIT_URL_RE.test(trimmed);
|
|
23
|
+
}
|
|
24
|
+
function isGitHubUrl(url) {
|
|
25
|
+
const trimmed = url.trim();
|
|
26
|
+
try {
|
|
27
|
+
const parsed = new URL(trimmed);
|
|
28
|
+
if (parsed.hostname.toLowerCase() === 'github.com')
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Not a valid URL — try SSH format
|
|
33
|
+
}
|
|
34
|
+
const sshMatch = trimmed.match(/^[\w-]+@([\w.-]+):/);
|
|
35
|
+
if (sshMatch?.[1]?.toLowerCase() === 'github.com')
|
|
36
|
+
return true;
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Converts a git remote URL (HTTPS or SSH) to a browser-friendly HTTPS URL.
|
|
41
|
+
* Returns `null` when the URL cannot be converted.
|
|
42
|
+
*
|
|
43
|
+
* https://github.com/org/repo.git → https://github.com/org/repo
|
|
44
|
+
* git@github.com:org/repo.git → https://github.com/org/repo
|
|
45
|
+
*/
|
|
46
|
+
function gitRemoteUrlToWeb(url) {
|
|
47
|
+
const trimmed = url.trim();
|
|
48
|
+
try {
|
|
49
|
+
const parsed = new URL(trimmed);
|
|
50
|
+
if (parsed.protocol === 'https:' || parsed.protocol === 'http:') {
|
|
51
|
+
return trimmed.replace(/\.git\s*$/, '');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Not a standard URL — try SSH
|
|
56
|
+
}
|
|
57
|
+
const sshMatch = trimmed.match(/^[\w-]+@([\w.-]+):(.+?)(?:\.git)?\s*$/);
|
|
58
|
+
if (sshMatch?.[1] && sshMatch[2]) {
|
|
59
|
+
return `https://${sshMatch[1]}/${sshMatch[2]}`;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Returns a URL that opens the "new pull request" compare screen on GitHub.
|
|
65
|
+
* Returns `null` when the remote URL is not a GitHub URL or cannot be parsed.
|
|
66
|
+
*/
|
|
67
|
+
function getGitHubCompareUrl({ gitRemoteUrl, fromBranch, toBranch, title, body }) {
|
|
68
|
+
if (!isGitHubUrl(gitRemoteUrl))
|
|
69
|
+
return null;
|
|
70
|
+
const webUrl = gitRemoteUrlToWeb(gitRemoteUrl);
|
|
71
|
+
if (!webUrl)
|
|
72
|
+
return null;
|
|
73
|
+
const base = `${webUrl}/compare/${encodeURIComponent(toBranch)}...${encodeURIComponent(fromBranch)}`;
|
|
74
|
+
if (!title && !body)
|
|
75
|
+
return base;
|
|
76
|
+
const params = new URLSearchParams();
|
|
77
|
+
params.set('expand', '1');
|
|
78
|
+
if (title)
|
|
79
|
+
params.set('title', title);
|
|
80
|
+
if (body)
|
|
81
|
+
params.set('body', body);
|
|
82
|
+
return `${base}?${params.toString()}`;
|
|
83
|
+
}
|
|
84
|
+
const HOST_TO_PROVIDER = {
|
|
85
|
+
'bitbucket.org': 'bitbucket',
|
|
86
|
+
'github.com': 'github',
|
|
87
|
+
'gitlab.com': 'gitlab'
|
|
88
|
+
};
|
|
89
|
+
function detectGitProvider(url) {
|
|
90
|
+
const trimmed = url.trim();
|
|
91
|
+
try {
|
|
92
|
+
const parsed = new URL(trimmed);
|
|
93
|
+
return HOST_TO_PROVIDER[parsed.hostname.toLowerCase()] ?? 'unknown';
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Not a standard URL — try SSH format.
|
|
97
|
+
}
|
|
98
|
+
const sshMatch = trimmed.match(/^[\w-]+@([\w.-]+):/);
|
|
99
|
+
if (sshMatch?.[1]) {
|
|
100
|
+
return HOST_TO_PROVIDER[sshMatch[1].toLowerCase()] ?? 'unknown';
|
|
101
|
+
}
|
|
102
|
+
return 'unknown';
|
|
103
|
+
}
|
|
104
|
+
function getCommitPathForProvider(params) {
|
|
105
|
+
const { hash, provider } = params;
|
|
106
|
+
switch (provider) {
|
|
107
|
+
case 'bitbucket':
|
|
108
|
+
return `/commits/${hash}`;
|
|
109
|
+
case 'gitlab':
|
|
110
|
+
return `/-/commit/${hash}`;
|
|
111
|
+
case 'github':
|
|
112
|
+
case 'unknown':
|
|
113
|
+
return `/commit/${hash}`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const GIT_COMMIT_HASH_REGEX = /^[0-9a-f]{7,40}$/i;
|
|
117
|
+
function isGitCommitHash(value) {
|
|
118
|
+
return GIT_COMMIT_HASH_REGEX.test(value.trim());
|
|
119
|
+
}
|
|
120
|
+
function getCommitUrlForHash(params) {
|
|
121
|
+
const { hash, repositoryWebUrl } = params;
|
|
122
|
+
const trimmedHash = hash.trim();
|
|
123
|
+
if (!isGitCommitHash(trimmedHash)) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const trimmedRepositoryUrl = repositoryWebUrl.trim();
|
|
127
|
+
const webUrl = gitRemoteUrlToWeb(trimmedRepositoryUrl) ?? trimmedRepositoryUrl;
|
|
128
|
+
const provider = detectGitProvider(trimmedRepositoryUrl);
|
|
129
|
+
return `${webUrl.replace(/\/+$/, '')}${getCommitPathForProvider({
|
|
130
|
+
hash: trimmedHash,
|
|
131
|
+
provider
|
|
132
|
+
})}`;
|
|
133
|
+
}
|
|
134
|
+
function isCommitUrlForHash(params) {
|
|
135
|
+
const { hash, url } = params;
|
|
136
|
+
const trimmedHash = hash.trim();
|
|
137
|
+
const trimmedUrl = url.trim();
|
|
138
|
+
if (!isGitCommitHash(trimmedHash)) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const parsed = new URL(trimmedUrl);
|
|
143
|
+
const normalizedPathname = parsed.pathname.replace(/\/+$/, '');
|
|
144
|
+
return normalizedPathname.endsWith(getCommitPathForProvider({
|
|
145
|
+
hash: trimmedHash,
|
|
146
|
+
provider: detectGitProvider(trimmedUrl)
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=git-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-url.js","sourceRoot":"","sources":["../../src/utils/git-url.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-url.test.d.ts","sourceRoot":"","sources":["../../src/utils/git-url.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const git_url_js_1 = require("./git-url.js");
|
|
5
|
+
(0, vitest_1.describe)('isValidGitUrl', () => {
|
|
6
|
+
vitest_1.it.each([
|
|
7
|
+
'https://github.com/org/repo.git',
|
|
8
|
+
'https://github.com/org/repo',
|
|
9
|
+
'http://gitlab.com/org/repo.git',
|
|
10
|
+
'git@github.com:org/repo.git',
|
|
11
|
+
'git@gitlab.com:org/repo'
|
|
12
|
+
])('returns true for %s', (url) => {
|
|
13
|
+
(0, vitest_1.expect)((0, git_url_js_1.isValidGitUrl)(url)).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
vitest_1.it.each(['', 'not-a-url', 'ftp://github.com/org/repo', 'https://'])('returns false for %j', (url) => {
|
|
16
|
+
(0, vitest_1.expect)((0, git_url_js_1.isValidGitUrl)(url)).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
(0, vitest_1.it)('trims whitespace', () => {
|
|
19
|
+
(0, vitest_1.expect)((0, git_url_js_1.isValidGitUrl)(' https://github.com/org/repo ')).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
(0, vitest_1.describe)('isGitHubUrl', () => {
|
|
23
|
+
vitest_1.it.each(['https://github.com/org/repo.git', 'https://github.com/org/repo', 'git@github.com:org/repo.git'])('returns true for %s', (url) => {
|
|
24
|
+
(0, vitest_1.expect)((0, git_url_js_1.isGitHubUrl)(url)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
vitest_1.it.each(['https://gitlab.com/org/repo', 'git@bitbucket.org:org/repo.git', 'https://example.com/org/repo', 'not-a-url'])('returns false for %s', (url) => {
|
|
27
|
+
(0, vitest_1.expect)((0, git_url_js_1.isGitHubUrl)(url)).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
(0, vitest_1.it)('is case-insensitive on the host', () => {
|
|
30
|
+
(0, vitest_1.expect)((0, git_url_js_1.isGitHubUrl)('https://GitHub.COM/org/repo')).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
(0, vitest_1.describe)('gitRemoteUrlToWeb', () => {
|
|
34
|
+
vitest_1.it.each([
|
|
35
|
+
['https://github.com/org/repo.git', 'https://github.com/org/repo'],
|
|
36
|
+
['https://github.com/org/repo', 'https://github.com/org/repo'],
|
|
37
|
+
['http://gitlab.com/org/repo.git', 'http://gitlab.com/org/repo'],
|
|
38
|
+
['git@github.com:org/repo.git', 'https://github.com/org/repo'],
|
|
39
|
+
['git@gitlab.com:group/sub/repo.git', 'https://gitlab.com/group/sub/repo']
|
|
40
|
+
])('converts %s → %s', (input, expected) => {
|
|
41
|
+
(0, vitest_1.expect)((0, git_url_js_1.gitRemoteUrlToWeb)(input)).toBe(expected);
|
|
42
|
+
});
|
|
43
|
+
(0, vitest_1.it)('returns null for unconvertible URLs', () => {
|
|
44
|
+
(0, vitest_1.expect)((0, git_url_js_1.gitRemoteUrlToWeb)('not-a-url')).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
(0, vitest_1.describe)('getGitHubCompareUrl', () => {
|
|
48
|
+
const base = { fromBranch: 'feature/cool', toBranch: 'main' };
|
|
49
|
+
(0, vitest_1.it)('builds a GitHub compare URL', () => {
|
|
50
|
+
(0, vitest_1.expect)((0, git_url_js_1.getGitHubCompareUrl)({
|
|
51
|
+
gitRemoteUrl: 'https://github.com/org/repo.git',
|
|
52
|
+
...base
|
|
53
|
+
})).toBe('https://github.com/org/repo/compare/main...feature%2Fcool');
|
|
54
|
+
});
|
|
55
|
+
(0, vitest_1.it)('appends title and body', () => {
|
|
56
|
+
(0, vitest_1.expect)((0, git_url_js_1.getGitHubCompareUrl)({
|
|
57
|
+
gitRemoteUrl: 'https://github.com/org/repo.git',
|
|
58
|
+
...base,
|
|
59
|
+
title: 'My PR',
|
|
60
|
+
body: '## Summary'
|
|
61
|
+
})).toBe('https://github.com/org/repo/compare/main...feature%2Fcool?expand=1&title=My+PR&body=%23%23+Summary');
|
|
62
|
+
});
|
|
63
|
+
(0, vitest_1.it)('returns null for non-GitHub URLs', () => {
|
|
64
|
+
(0, vitest_1.expect)((0, git_url_js_1.getGitHubCompareUrl)({
|
|
65
|
+
gitRemoteUrl: 'https://gitlab.com/org/repo',
|
|
66
|
+
...base
|
|
67
|
+
})).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
(0, vitest_1.it)('returns null for unconvertible URLs', () => {
|
|
70
|
+
(0, vitest_1.expect)((0, git_url_js_1.getGitHubCompareUrl)({ gitRemoteUrl: 'not-a-url', ...base })).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
(0, vitest_1.describe)('GITHUB_PAT_SETTINGS_URL', () => {
|
|
74
|
+
(0, vitest_1.it)('is a valid URL', () => {
|
|
75
|
+
(0, vitest_1.expect)(git_url_js_1.GITHUB_PAT_SETTINGS_URL).toBe('https://github.com/settings/tokens');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
//# sourceMappingURL=git-url.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-url.test.js","sourceRoot":"","sources":["../../src/utils/git-url.test.ts"],"names":[],"mappings":"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"}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,UAAU,CAAC;AACzB,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AAEvC,OAAO,MAAM,MAAM,2BAA2B,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,UAAU,CAAC;AACzB,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AAEvC,OAAO,MAAM,MAAM,2BAA2B,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
package/dist/utils/index.js
CHANGED
|
@@ -22,6 +22,7 @@ __exportStar(require("./configuration.js"), exports);
|
|
|
22
22
|
__exportStar(require("./attachment-upload.js"), exports);
|
|
23
23
|
__exportStar(require("./env.js"), exports);
|
|
24
24
|
__exportStar(require("./environment.js"), exports);
|
|
25
|
+
__exportStar(require("./git-url.js"), exports);
|
|
25
26
|
__exportStar(require("./string.js"), exports);
|
|
26
27
|
__exportStar(require("./time.js"), exports);
|
|
27
28
|
__exportStar(require("./truncate.js"), exports);
|
package/dist/utils/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA,qDAAmC;AACnC,yDAAuC;AACvC,2CAAyB;AACzB,mDAAiC;AACjC,8CAA4B;AAC5B,4CAA0B;AAC1B,gDAA8B;AAC9B,2CAAyB;AACzB,kDAAgC;AAChC,6CAA2B;AAC3B,4CAA0B;AAC1B,iDAA+B;AAC/B,yDAAuC;AAEvC,wEAA+C;AACtC,iBADF,iBAAM,CACE"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA,qDAAmC;AACnC,yDAAuC;AACvC,2CAAyB;AACzB,mDAAiC;AACjC,+CAA6B;AAC7B,8CAA4B;AAC5B,4CAA0B;AAC1B,gDAA8B;AAC9B,2CAAyB;AACzB,kDAAgC;AAChC,6CAA2B;AAC3B,4CAA0B;AAC1B,iDAA+B;AAC/B,yDAAuC;AAEvC,wEAA+C;AACtC,iBADF,iBAAM,CACE"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git remote URL utilities — GitHub-focused.
|
|
3
|
+
*
|
|
4
|
+
* Parsing, validation, and compare-URL generation for git remote URLs.
|
|
5
|
+
* Used by both the UI and the vite-plugin-file-sync AI agent tooling.
|
|
6
|
+
*/
|
|
7
|
+
export declare const GITHUB_PAT_SETTINGS_URL = "https://github.com/settings/tokens";
|
|
8
|
+
export declare function isValidGitUrl(url: string): boolean;
|
|
9
|
+
export declare function isGitHubUrl(url: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Converts a git remote URL (HTTPS or SSH) to a browser-friendly HTTPS URL.
|
|
12
|
+
* Returns `null` when the URL cannot be converted.
|
|
13
|
+
*
|
|
14
|
+
* https://github.com/org/repo.git → https://github.com/org/repo
|
|
15
|
+
* git@github.com:org/repo.git → https://github.com/org/repo
|
|
16
|
+
*/
|
|
17
|
+
export declare function gitRemoteUrlToWeb(url: string): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* Returns a URL that opens the "new pull request" compare screen on GitHub.
|
|
20
|
+
* Returns `null` when the remote URL is not a GitHub URL or cannot be parsed.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getGitHubCompareUrl({ gitRemoteUrl, fromBranch, toBranch, title, body }: {
|
|
23
|
+
gitRemoteUrl: string;
|
|
24
|
+
fromBranch: string;
|
|
25
|
+
toBranch: string;
|
|
26
|
+
title?: string;
|
|
27
|
+
body?: string;
|
|
28
|
+
}): string | null;
|
|
29
|
+
export declare function isGitCommitHash(value: string): boolean;
|
|
30
|
+
export declare function getCommitUrlForHash(params: {
|
|
31
|
+
hash: string;
|
|
32
|
+
repositoryWebUrl: string;
|
|
33
|
+
}): string | null;
|
|
34
|
+
export declare function isCommitUrlForHash(params: {
|
|
35
|
+
hash: string;
|
|
36
|
+
url: string;
|
|
37
|
+
}): boolean;
|
|
38
|
+
//# sourceMappingURL=git-url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-url.d.ts","sourceRoot":"","sources":["../../src/utils/git-url.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,eAAO,MAAM,uBAAuB,uCAAuC,CAAC;AAE5E,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGlD;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAchD;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkB5D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,KAAK,EACL,IAAI,EACL,EAAE;IACD,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,GAAG,IAAI,CAchB;AA2CD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,GAAG,IAAI,CAerG;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAoBjF"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git remote URL utilities — GitHub-focused.
|
|
3
|
+
*
|
|
4
|
+
* Parsing, validation, and compare-URL generation for git remote URLs.
|
|
5
|
+
* Used by both the UI and the vite-plugin-file-sync AI agent tooling.
|
|
6
|
+
*/
|
|
7
|
+
const HTTPS_GIT_URL_RE = /^https?:\/\/[^\s/$.?#]+\.[^\s/$.?#]+\/[^\s/$.?#]+(\/[^\s/$.?#]+)+(\.git)?\/?$/i;
|
|
8
|
+
const SSH_GIT_URL_RE = /^[\w-]+@[\w.-]+:[\w./-]+(\.git)?$/;
|
|
9
|
+
export const GITHUB_PAT_SETTINGS_URL = 'https://github.com/settings/tokens';
|
|
10
|
+
export function isValidGitUrl(url) {
|
|
11
|
+
const trimmed = url.trim();
|
|
12
|
+
return HTTPS_GIT_URL_RE.test(trimmed) || SSH_GIT_URL_RE.test(trimmed);
|
|
13
|
+
}
|
|
14
|
+
export function isGitHubUrl(url) {
|
|
15
|
+
const trimmed = url.trim();
|
|
16
|
+
try {
|
|
17
|
+
const parsed = new URL(trimmed);
|
|
18
|
+
if (parsed.hostname.toLowerCase() === 'github.com')
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Not a valid URL — try SSH format
|
|
23
|
+
}
|
|
24
|
+
const sshMatch = trimmed.match(/^[\w-]+@([\w.-]+):/);
|
|
25
|
+
if (sshMatch?.[1]?.toLowerCase() === 'github.com')
|
|
26
|
+
return true;
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Converts a git remote URL (HTTPS or SSH) to a browser-friendly HTTPS URL.
|
|
31
|
+
* Returns `null` when the URL cannot be converted.
|
|
32
|
+
*
|
|
33
|
+
* https://github.com/org/repo.git → https://github.com/org/repo
|
|
34
|
+
* git@github.com:org/repo.git → https://github.com/org/repo
|
|
35
|
+
*/
|
|
36
|
+
export function gitRemoteUrlToWeb(url) {
|
|
37
|
+
const trimmed = url.trim();
|
|
38
|
+
try {
|
|
39
|
+
const parsed = new URL(trimmed);
|
|
40
|
+
if (parsed.protocol === 'https:' || parsed.protocol === 'http:') {
|
|
41
|
+
return trimmed.replace(/\.git\s*$/, '');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Not a standard URL — try SSH
|
|
46
|
+
}
|
|
47
|
+
const sshMatch = trimmed.match(/^[\w-]+@([\w.-]+):(.+?)(?:\.git)?\s*$/);
|
|
48
|
+
if (sshMatch?.[1] && sshMatch[2]) {
|
|
49
|
+
return `https://${sshMatch[1]}/${sshMatch[2]}`;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Returns a URL that opens the "new pull request" compare screen on GitHub.
|
|
55
|
+
* Returns `null` when the remote URL is not a GitHub URL or cannot be parsed.
|
|
56
|
+
*/
|
|
57
|
+
export function getGitHubCompareUrl({ gitRemoteUrl, fromBranch, toBranch, title, body }) {
|
|
58
|
+
if (!isGitHubUrl(gitRemoteUrl))
|
|
59
|
+
return null;
|
|
60
|
+
const webUrl = gitRemoteUrlToWeb(gitRemoteUrl);
|
|
61
|
+
if (!webUrl)
|
|
62
|
+
return null;
|
|
63
|
+
const base = `${webUrl}/compare/${encodeURIComponent(toBranch)}...${encodeURIComponent(fromBranch)}`;
|
|
64
|
+
if (!title && !body)
|
|
65
|
+
return base;
|
|
66
|
+
const params = new URLSearchParams();
|
|
67
|
+
params.set('expand', '1');
|
|
68
|
+
if (title)
|
|
69
|
+
params.set('title', title);
|
|
70
|
+
if (body)
|
|
71
|
+
params.set('body', body);
|
|
72
|
+
return `${base}?${params.toString()}`;
|
|
73
|
+
}
|
|
74
|
+
const HOST_TO_PROVIDER = {
|
|
75
|
+
'bitbucket.org': 'bitbucket',
|
|
76
|
+
'github.com': 'github',
|
|
77
|
+
'gitlab.com': 'gitlab'
|
|
78
|
+
};
|
|
79
|
+
function detectGitProvider(url) {
|
|
80
|
+
const trimmed = url.trim();
|
|
81
|
+
try {
|
|
82
|
+
const parsed = new URL(trimmed);
|
|
83
|
+
return HOST_TO_PROVIDER[parsed.hostname.toLowerCase()] ?? 'unknown';
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Not a standard URL — try SSH format.
|
|
87
|
+
}
|
|
88
|
+
const sshMatch = trimmed.match(/^[\w-]+@([\w.-]+):/);
|
|
89
|
+
if (sshMatch?.[1]) {
|
|
90
|
+
return HOST_TO_PROVIDER[sshMatch[1].toLowerCase()] ?? 'unknown';
|
|
91
|
+
}
|
|
92
|
+
return 'unknown';
|
|
93
|
+
}
|
|
94
|
+
function getCommitPathForProvider(params) {
|
|
95
|
+
const { hash, provider } = params;
|
|
96
|
+
switch (provider) {
|
|
97
|
+
case 'bitbucket':
|
|
98
|
+
return `/commits/${hash}`;
|
|
99
|
+
case 'gitlab':
|
|
100
|
+
return `/-/commit/${hash}`;
|
|
101
|
+
case 'github':
|
|
102
|
+
case 'unknown':
|
|
103
|
+
return `/commit/${hash}`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const GIT_COMMIT_HASH_REGEX = /^[0-9a-f]{7,40}$/i;
|
|
107
|
+
export function isGitCommitHash(value) {
|
|
108
|
+
return GIT_COMMIT_HASH_REGEX.test(value.trim());
|
|
109
|
+
}
|
|
110
|
+
export function getCommitUrlForHash(params) {
|
|
111
|
+
const { hash, repositoryWebUrl } = params;
|
|
112
|
+
const trimmedHash = hash.trim();
|
|
113
|
+
if (!isGitCommitHash(trimmedHash)) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const trimmedRepositoryUrl = repositoryWebUrl.trim();
|
|
117
|
+
const webUrl = gitRemoteUrlToWeb(trimmedRepositoryUrl) ?? trimmedRepositoryUrl;
|
|
118
|
+
const provider = detectGitProvider(trimmedRepositoryUrl);
|
|
119
|
+
return `${webUrl.replace(/\/+$/, '')}${getCommitPathForProvider({
|
|
120
|
+
hash: trimmedHash,
|
|
121
|
+
provider
|
|
122
|
+
})}`;
|
|
123
|
+
}
|
|
124
|
+
export function isCommitUrlForHash(params) {
|
|
125
|
+
const { hash, url } = params;
|
|
126
|
+
const trimmedHash = hash.trim();
|
|
127
|
+
const trimmedUrl = url.trim();
|
|
128
|
+
if (!isGitCommitHash(trimmedHash)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const parsed = new URL(trimmedUrl);
|
|
133
|
+
const normalizedPathname = parsed.pathname.replace(/\/+$/, '');
|
|
134
|
+
return normalizedPathname.endsWith(getCommitPathForProvider({
|
|
135
|
+
hash: trimmedHash,
|
|
136
|
+
provider: detectGitProvider(trimmedUrl)
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=git-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-url.js","sourceRoot":"","sources":["../../src/utils/git-url.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-url.test.d.ts","sourceRoot":"","sources":["../../src/utils/git-url.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { GITHUB_PAT_SETTINGS_URL, getGitHubCompareUrl, gitRemoteUrlToWeb, isGitHubUrl, isValidGitUrl } from './git-url.js';
|
|
3
|
+
describe('isValidGitUrl', () => {
|
|
4
|
+
it.each([
|
|
5
|
+
'https://github.com/org/repo.git',
|
|
6
|
+
'https://github.com/org/repo',
|
|
7
|
+
'http://gitlab.com/org/repo.git',
|
|
8
|
+
'git@github.com:org/repo.git',
|
|
9
|
+
'git@gitlab.com:org/repo'
|
|
10
|
+
])('returns true for %s', (url) => {
|
|
11
|
+
expect(isValidGitUrl(url)).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
it.each(['', 'not-a-url', 'ftp://github.com/org/repo', 'https://'])('returns false for %j', (url) => {
|
|
14
|
+
expect(isValidGitUrl(url)).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
it('trims whitespace', () => {
|
|
17
|
+
expect(isValidGitUrl(' https://github.com/org/repo ')).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe('isGitHubUrl', () => {
|
|
21
|
+
it.each(['https://github.com/org/repo.git', 'https://github.com/org/repo', 'git@github.com:org/repo.git'])('returns true for %s', (url) => {
|
|
22
|
+
expect(isGitHubUrl(url)).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
it.each(['https://gitlab.com/org/repo', 'git@bitbucket.org:org/repo.git', 'https://example.com/org/repo', 'not-a-url'])('returns false for %s', (url) => {
|
|
25
|
+
expect(isGitHubUrl(url)).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
it('is case-insensitive on the host', () => {
|
|
28
|
+
expect(isGitHubUrl('https://GitHub.COM/org/repo')).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe('gitRemoteUrlToWeb', () => {
|
|
32
|
+
it.each([
|
|
33
|
+
['https://github.com/org/repo.git', 'https://github.com/org/repo'],
|
|
34
|
+
['https://github.com/org/repo', 'https://github.com/org/repo'],
|
|
35
|
+
['http://gitlab.com/org/repo.git', 'http://gitlab.com/org/repo'],
|
|
36
|
+
['git@github.com:org/repo.git', 'https://github.com/org/repo'],
|
|
37
|
+
['git@gitlab.com:group/sub/repo.git', 'https://gitlab.com/group/sub/repo']
|
|
38
|
+
])('converts %s → %s', (input, expected) => {
|
|
39
|
+
expect(gitRemoteUrlToWeb(input)).toBe(expected);
|
|
40
|
+
});
|
|
41
|
+
it('returns null for unconvertible URLs', () => {
|
|
42
|
+
expect(gitRemoteUrlToWeb('not-a-url')).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe('getGitHubCompareUrl', () => {
|
|
46
|
+
const base = { fromBranch: 'feature/cool', toBranch: 'main' };
|
|
47
|
+
it('builds a GitHub compare URL', () => {
|
|
48
|
+
expect(getGitHubCompareUrl({
|
|
49
|
+
gitRemoteUrl: 'https://github.com/org/repo.git',
|
|
50
|
+
...base
|
|
51
|
+
})).toBe('https://github.com/org/repo/compare/main...feature%2Fcool');
|
|
52
|
+
});
|
|
53
|
+
it('appends title and body', () => {
|
|
54
|
+
expect(getGitHubCompareUrl({
|
|
55
|
+
gitRemoteUrl: 'https://github.com/org/repo.git',
|
|
56
|
+
...base,
|
|
57
|
+
title: 'My PR',
|
|
58
|
+
body: '## Summary'
|
|
59
|
+
})).toBe('https://github.com/org/repo/compare/main...feature%2Fcool?expand=1&title=My+PR&body=%23%23+Summary');
|
|
60
|
+
});
|
|
61
|
+
it('returns null for non-GitHub URLs', () => {
|
|
62
|
+
expect(getGitHubCompareUrl({
|
|
63
|
+
gitRemoteUrl: 'https://gitlab.com/org/repo',
|
|
64
|
+
...base
|
|
65
|
+
})).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
it('returns null for unconvertible URLs', () => {
|
|
68
|
+
expect(getGitHubCompareUrl({ gitRemoteUrl: 'not-a-url', ...base })).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('GITHUB_PAT_SETTINGS_URL', () => {
|
|
72
|
+
it('is a valid URL', () => {
|
|
73
|
+
expect(GITHUB_PAT_SETTINGS_URL).toBe('https://github.com/settings/tokens');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
//# sourceMappingURL=git-url.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-url.test.js","sourceRoot":"","sources":["../../src/utils/git-url.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,UAAU,CAAC;AACzB,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AAEvC,OAAO,MAAM,MAAM,2BAA2B,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,UAAU,CAAC;AACzB,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AAEvC,OAAO,MAAM,MAAM,2BAA2B,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
package/dist-esm/utils/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,UAAU,CAAC;AACzB,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AAEvC,OAAO,MAAM,MAAM,2BAA2B,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,UAAU,CAAC;AACzB,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AAEvC,OAAO,MAAM,MAAM,2BAA2B,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { GITHUB_PAT_SETTINGS_URL, getGitHubCompareUrl, gitRemoteUrlToWeb, isGitHubUrl, isValidGitUrl } from './git-url.js';
|
|
3
|
+
|
|
4
|
+
describe('isValidGitUrl', () => {
|
|
5
|
+
it.each([
|
|
6
|
+
'https://github.com/org/repo.git',
|
|
7
|
+
'https://github.com/org/repo',
|
|
8
|
+
'http://gitlab.com/org/repo.git',
|
|
9
|
+
'git@github.com:org/repo.git',
|
|
10
|
+
'git@gitlab.com:org/repo'
|
|
11
|
+
])('returns true for %s', (url) => {
|
|
12
|
+
expect(isValidGitUrl(url)).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it.each(['', 'not-a-url', 'ftp://github.com/org/repo', 'https://'])('returns false for %j', (url) => {
|
|
16
|
+
expect(isValidGitUrl(url)).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('trims whitespace', () => {
|
|
20
|
+
expect(isValidGitUrl(' https://github.com/org/repo ')).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('isGitHubUrl', () => {
|
|
25
|
+
it.each(['https://github.com/org/repo.git', 'https://github.com/org/repo', 'git@github.com:org/repo.git'])(
|
|
26
|
+
'returns true for %s',
|
|
27
|
+
(url) => {
|
|
28
|
+
expect(isGitHubUrl(url)).toBe(true);
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
it.each(['https://gitlab.com/org/repo', 'git@bitbucket.org:org/repo.git', 'https://example.com/org/repo', 'not-a-url'])(
|
|
33
|
+
'returns false for %s',
|
|
34
|
+
(url) => {
|
|
35
|
+
expect(isGitHubUrl(url)).toBe(false);
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
it('is case-insensitive on the host', () => {
|
|
40
|
+
expect(isGitHubUrl('https://GitHub.COM/org/repo')).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('gitRemoteUrlToWeb', () => {
|
|
45
|
+
it.each<[string, string]>([
|
|
46
|
+
['https://github.com/org/repo.git', 'https://github.com/org/repo'],
|
|
47
|
+
['https://github.com/org/repo', 'https://github.com/org/repo'],
|
|
48
|
+
['http://gitlab.com/org/repo.git', 'http://gitlab.com/org/repo'],
|
|
49
|
+
['git@github.com:org/repo.git', 'https://github.com/org/repo'],
|
|
50
|
+
['git@gitlab.com:group/sub/repo.git', 'https://gitlab.com/group/sub/repo']
|
|
51
|
+
])('converts %s → %s', (input, expected) => {
|
|
52
|
+
expect(gitRemoteUrlToWeb(input)).toBe(expected);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('returns null for unconvertible URLs', () => {
|
|
56
|
+
expect(gitRemoteUrlToWeb('not-a-url')).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('getGitHubCompareUrl', () => {
|
|
61
|
+
const base = { fromBranch: 'feature/cool', toBranch: 'main' };
|
|
62
|
+
|
|
63
|
+
it('builds a GitHub compare URL', () => {
|
|
64
|
+
expect(
|
|
65
|
+
getGitHubCompareUrl({
|
|
66
|
+
gitRemoteUrl: 'https://github.com/org/repo.git',
|
|
67
|
+
...base
|
|
68
|
+
})
|
|
69
|
+
).toBe('https://github.com/org/repo/compare/main...feature%2Fcool');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('appends title and body', () => {
|
|
73
|
+
expect(
|
|
74
|
+
getGitHubCompareUrl({
|
|
75
|
+
gitRemoteUrl: 'https://github.com/org/repo.git',
|
|
76
|
+
...base,
|
|
77
|
+
title: 'My PR',
|
|
78
|
+
body: '## Summary'
|
|
79
|
+
})
|
|
80
|
+
).toBe('https://github.com/org/repo/compare/main...feature%2Fcool?expand=1&title=My+PR&body=%23%23+Summary');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns null for non-GitHub URLs', () => {
|
|
84
|
+
expect(
|
|
85
|
+
getGitHubCompareUrl({
|
|
86
|
+
gitRemoteUrl: 'https://gitlab.com/org/repo',
|
|
87
|
+
...base
|
|
88
|
+
})
|
|
89
|
+
).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('returns null for unconvertible URLs', () => {
|
|
93
|
+
expect(getGitHubCompareUrl({ gitRemoteUrl: 'not-a-url', ...base })).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('GITHUB_PAT_SETTINGS_URL', () => {
|
|
98
|
+
it('is a valid URL', () => {
|
|
99
|
+
expect(GITHUB_PAT_SETTINGS_URL).toBe('https://github.com/settings/tokens');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git remote URL utilities — GitHub-focused.
|
|
3
|
+
*
|
|
4
|
+
* Parsing, validation, and compare-URL generation for git remote URLs.
|
|
5
|
+
* Used by both the UI and the vite-plugin-file-sync AI agent tooling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const HTTPS_GIT_URL_RE = /^https?:\/\/[^\s/$.?#]+\.[^\s/$.?#]+\/[^\s/$.?#]+(\/[^\s/$.?#]+)+(\.git)?\/?$/i;
|
|
9
|
+
const SSH_GIT_URL_RE = /^[\w-]+@[\w.-]+:[\w./-]+(\.git)?$/;
|
|
10
|
+
|
|
11
|
+
export const GITHUB_PAT_SETTINGS_URL = 'https://github.com/settings/tokens';
|
|
12
|
+
|
|
13
|
+
export function isValidGitUrl(url: string): boolean {
|
|
14
|
+
const trimmed = url.trim();
|
|
15
|
+
return HTTPS_GIT_URL_RE.test(trimmed) || SSH_GIT_URL_RE.test(trimmed);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isGitHubUrl(url: string): boolean {
|
|
19
|
+
const trimmed = url.trim();
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const parsed = new URL(trimmed);
|
|
23
|
+
if (parsed.hostname.toLowerCase() === 'github.com') return true;
|
|
24
|
+
} catch {
|
|
25
|
+
// Not a valid URL — try SSH format
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const sshMatch = trimmed.match(/^[\w-]+@([\w.-]+):/);
|
|
29
|
+
if (sshMatch?.[1]?.toLowerCase() === 'github.com') return true;
|
|
30
|
+
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Converts a git remote URL (HTTPS or SSH) to a browser-friendly HTTPS URL.
|
|
36
|
+
* Returns `null` when the URL cannot be converted.
|
|
37
|
+
*
|
|
38
|
+
* https://github.com/org/repo.git → https://github.com/org/repo
|
|
39
|
+
* git@github.com:org/repo.git → https://github.com/org/repo
|
|
40
|
+
*/
|
|
41
|
+
export function gitRemoteUrlToWeb(url: string): string | null {
|
|
42
|
+
const trimmed = url.trim();
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const parsed = new URL(trimmed);
|
|
46
|
+
if (parsed.protocol === 'https:' || parsed.protocol === 'http:') {
|
|
47
|
+
return trimmed.replace(/\.git\s*$/, '');
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// Not a standard URL — try SSH
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const sshMatch = trimmed.match(/^[\w-]+@([\w.-]+):(.+?)(?:\.git)?\s*$/);
|
|
54
|
+
if (sshMatch?.[1] && sshMatch[2]) {
|
|
55
|
+
return `https://${sshMatch[1]}/${sshMatch[2]}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Returns a URL that opens the "new pull request" compare screen on GitHub.
|
|
63
|
+
* Returns `null` when the remote URL is not a GitHub URL or cannot be parsed.
|
|
64
|
+
*/
|
|
65
|
+
export function getGitHubCompareUrl({
|
|
66
|
+
gitRemoteUrl,
|
|
67
|
+
fromBranch,
|
|
68
|
+
toBranch,
|
|
69
|
+
title,
|
|
70
|
+
body
|
|
71
|
+
}: {
|
|
72
|
+
gitRemoteUrl: string;
|
|
73
|
+
fromBranch: string;
|
|
74
|
+
toBranch: string;
|
|
75
|
+
title?: string;
|
|
76
|
+
body?: string;
|
|
77
|
+
}): string | null {
|
|
78
|
+
if (!isGitHubUrl(gitRemoteUrl)) return null;
|
|
79
|
+
|
|
80
|
+
const webUrl = gitRemoteUrlToWeb(gitRemoteUrl);
|
|
81
|
+
if (!webUrl) return null;
|
|
82
|
+
|
|
83
|
+
const base = `${webUrl}/compare/${encodeURIComponent(toBranch)}...${encodeURIComponent(fromBranch)}`;
|
|
84
|
+
if (!title && !body) return base;
|
|
85
|
+
|
|
86
|
+
const params = new URLSearchParams();
|
|
87
|
+
params.set('expand', '1');
|
|
88
|
+
if (title) params.set('title', title);
|
|
89
|
+
if (body) params.set('body', body);
|
|
90
|
+
return `${base}?${params.toString()}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type GitProvider = 'github' | 'gitlab' | 'bitbucket' | 'unknown';
|
|
94
|
+
|
|
95
|
+
const HOST_TO_PROVIDER: Record<string, GitProvider> = {
|
|
96
|
+
'bitbucket.org': 'bitbucket',
|
|
97
|
+
'github.com': 'github',
|
|
98
|
+
'gitlab.com': 'gitlab'
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function detectGitProvider(url: string): GitProvider {
|
|
102
|
+
const trimmed = url.trim();
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const parsed = new URL(trimmed);
|
|
106
|
+
return HOST_TO_PROVIDER[parsed.hostname.toLowerCase()] ?? 'unknown';
|
|
107
|
+
} catch {
|
|
108
|
+
// Not a standard URL — try SSH format.
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const sshMatch = trimmed.match(/^[\w-]+@([\w.-]+):/);
|
|
112
|
+
if (sshMatch?.[1]) {
|
|
113
|
+
return HOST_TO_PROVIDER[sshMatch[1].toLowerCase()] ?? 'unknown';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return 'unknown';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getCommitPathForProvider(params: { hash: string; provider: GitProvider }): string {
|
|
120
|
+
const { hash, provider } = params;
|
|
121
|
+
switch (provider) {
|
|
122
|
+
case 'bitbucket':
|
|
123
|
+
return `/commits/${hash}`;
|
|
124
|
+
case 'gitlab':
|
|
125
|
+
return `/-/commit/${hash}`;
|
|
126
|
+
case 'github':
|
|
127
|
+
case 'unknown':
|
|
128
|
+
return `/commit/${hash}`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const GIT_COMMIT_HASH_REGEX = /^[0-9a-f]{7,40}$/i;
|
|
133
|
+
|
|
134
|
+
export function isGitCommitHash(value: string): boolean {
|
|
135
|
+
return GIT_COMMIT_HASH_REGEX.test(value.trim());
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function getCommitUrlForHash(params: { hash: string; repositoryWebUrl: string }): string | null {
|
|
139
|
+
const { hash, repositoryWebUrl } = params;
|
|
140
|
+
const trimmedHash = hash.trim();
|
|
141
|
+
if (!isGitCommitHash(trimmedHash)) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const trimmedRepositoryUrl = repositoryWebUrl.trim();
|
|
146
|
+
const webUrl = gitRemoteUrlToWeb(trimmedRepositoryUrl) ?? trimmedRepositoryUrl;
|
|
147
|
+
const provider = detectGitProvider(trimmedRepositoryUrl);
|
|
148
|
+
|
|
149
|
+
return `${webUrl.replace(/\/+$/, '')}${getCommitPathForProvider({
|
|
150
|
+
hash: trimmedHash,
|
|
151
|
+
provider
|
|
152
|
+
})}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function isCommitUrlForHash(params: { hash: string; url: string }): boolean {
|
|
156
|
+
const { hash, url } = params;
|
|
157
|
+
const trimmedHash = hash.trim();
|
|
158
|
+
const trimmedUrl = url.trim();
|
|
159
|
+
if (!isGitCommitHash(trimmedHash)) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const parsed = new URL(trimmedUrl);
|
|
165
|
+
const normalizedPathname = parsed.pathname.replace(/\/+$/, '');
|
|
166
|
+
return normalizedPathname.endsWith(
|
|
167
|
+
getCommitPathForProvider({
|
|
168
|
+
hash: trimmedHash,
|
|
169
|
+
provider: detectGitProvider(trimmedUrl)
|
|
170
|
+
})
|
|
171
|
+
);
|
|
172
|
+
} catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
package/src/utils/index.ts
CHANGED