fix(installer): handle deep-path URLs in custom module source parser
Rewrite parseSource() from host-specific regex to generic URL-based parser so Azure DevOps _git paths and other multi-segment repo URLs are preserved in cloneUrl and cacheKey. Closes #2268
This commit is contained in:
parent
914c4edd6b
commit
4e5c930318
|
|
@ -73,41 +73,96 @@ class CustomModuleManager {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPS URL: https://host/owner/repo[/tree/branch/subdir][.git]
|
// HTTPS URL: generic handling for any Git host.
|
||||||
const httpsMatch = trimmed.match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/.]+?)(?:\.git)?(\/.*)?$/);
|
// We avoid host-specific parsing — `git clone` will accept whatever URL the
|
||||||
if (httpsMatch) {
|
// user provides. We only need to (a) separate an optional browser-style
|
||||||
const [, host, owner, repo, remainder] = httpsMatch;
|
// subdir suffix from the clone URL, and (b) derive a cache key / display
|
||||||
const cloneUrl = `https://${host}/${owner}/${repo}`;
|
// name from the path.
|
||||||
|
if (/^https?:\/\//i.test(trimmed)) {
|
||||||
|
let url;
|
||||||
|
try {
|
||||||
|
url = new URL(trimmed);
|
||||||
|
} catch {
|
||||||
|
url = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url && url.host) {
|
||||||
|
const host = url.host;
|
||||||
|
let repoPath = url.pathname.replace(/^\/+/, '').replace(/\/+$/, '');
|
||||||
let subdir = null;
|
let subdir = null;
|
||||||
|
|
||||||
if (remainder) {
|
// Detect browser-style deep-path patterns that embed a subdirectory
|
||||||
// Extract subdir from deep path patterns used by various Git hosts
|
// after a ref (branch/tag/commit). These appear across many hosts:
|
||||||
|
// GitHub /<repo>/tree|blob/<ref>/<subdir>
|
||||||
|
// GitLab /<repo>/-/tree|blob/<ref>/<subdir>
|
||||||
|
// Gitea /<repo>/src/<ref>/<subdir>
|
||||||
|
// Gitea /<repo>/src/(branch|commit|tag)/<ref>/<subdir>
|
||||||
|
// Group 1 = repo path prefix, Group 2 = subdir.
|
||||||
|
// Trailing subdir is optional: a URL like /<repo>/tree/<ref> with no
|
||||||
|
// further path still identifies a repo at a ref (no subdir).
|
||||||
const deepPathPatterns = [
|
const deepPathPatterns = [
|
||||||
/^\/(?:-\/)?tree\/[^/]+\/(.+)$/, // GitHub /tree/branch/path, GitLab /-/tree/branch/path
|
/^(.+?)\/(?:-\/)?(?:tree|blob)\/[^/]+(?:\/(.+))?$/,
|
||||||
/^\/(?:-\/)?blob\/[^/]+\/(.+)$/, // /blob/branch/path (treat same as tree)
|
/^(.+?)\/src\/(?:branch\/|commit\/|tag\/)?[^/]+(?:\/(.+))?$/,
|
||||||
/^\/src\/[^/]+\/(.+)$/, // Gitea/Forgejo /src/branch/path
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const pattern of deepPathPatterns) {
|
for (const pattern of deepPathPatterns) {
|
||||||
const match = remainder.match(pattern);
|
const match = repoPath.match(pattern);
|
||||||
if (match) {
|
if (match) {
|
||||||
subdir = match[1].replace(/\/$/, ''); // strip trailing slash
|
repoPath = match[1];
|
||||||
|
if (match[2]) {
|
||||||
|
const cleaned = match[2].replace(/\/+$/, '');
|
||||||
|
if (cleaned) subdir = cleaned;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Some hosts use ?path=/subdir on browse links to point at a file or
|
||||||
|
// directory. Honor it when no deep-path marker matched above.
|
||||||
|
if (!subdir) {
|
||||||
|
const pathParam = url.searchParams.get('path');
|
||||||
|
if (pathParam) {
|
||||||
|
const cleaned = pathParam.replace(/^\/+/, '').replace(/\/+$/, '');
|
||||||
|
if (cleaned) subdir = cleaned;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip a single trailing .git for a stable cacheKey/displayName.
|
||||||
|
const repoPathClean = repoPath.replace(/\.git$/i, '');
|
||||||
|
if (!repoPathClean) {
|
||||||
|
return {
|
||||||
|
type: null,
|
||||||
|
cloneUrl: null,
|
||||||
|
subdir: null,
|
||||||
|
localPath: null,
|
||||||
|
cacheKey: null,
|
||||||
|
displayName: null,
|
||||||
|
isValid: false,
|
||||||
|
error: 'Not a valid Git URL or local path',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const cloneUrl = `${url.protocol}//${host}/${repoPathClean}`;
|
||||||
|
const cacheKey = `${host}/${repoPathClean}`;
|
||||||
|
|
||||||
|
// Display name: prefer "<owner>/<repo>" using the last two meaningful
|
||||||
|
// path segments.
|
||||||
|
const segments = repoPathClean.split('/').filter(Boolean);
|
||||||
|
const repoSeg = segments.at(-1);
|
||||||
|
const ownerSeg = segments.at(-2);
|
||||||
|
const displayName = ownerSeg ? `${ownerSeg}/${repoSeg}` : repoSeg;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'url',
|
type: 'url',
|
||||||
cloneUrl,
|
cloneUrl,
|
||||||
subdir,
|
subdir,
|
||||||
localPath: null,
|
localPath: null,
|
||||||
cacheKey: `${host}/${owner}/${repo}`,
|
cacheKey,
|
||||||
displayName: `${owner}/${repo}`,
|
displayName,
|
||||||
isValid: true,
|
isValid: true,
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: null,
|
type: null,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue