mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 09:01:14 +00:00
fix(coding-agent): fallback parse git URLs for unknown hosts
This commit is contained in:
parent
898ad73d8a
commit
5a30e16305
2 changed files with 143 additions and 41 deletions
|
|
@ -18,54 +18,131 @@ export type GitSource = {
|
|||
pinned: boolean;
|
||||
};
|
||||
|
||||
function splitRef(url: string): { repo: string; ref?: string } {
|
||||
const lastAt = url.lastIndexOf("@");
|
||||
if (lastAt <= 0) {
|
||||
return { repo: url };
|
||||
}
|
||||
|
||||
const lastSlash = url.lastIndexOf("/");
|
||||
const hasScheme = url.includes("://");
|
||||
const scpLikeMatch = url.match(/^[^@]+@[^:]+:/);
|
||||
if (scpLikeMatch) {
|
||||
const separatorIndex = scpLikeMatch[0].length - 1;
|
||||
if (lastAt <= separatorIndex || lastAt <= lastSlash) {
|
||||
return { repo: url };
|
||||
}
|
||||
} else if (hasScheme) {
|
||||
const schemeIndex = url.indexOf("://");
|
||||
const pathStart = url.indexOf("/", schemeIndex + 3);
|
||||
if (pathStart < 0 || lastAt <= pathStart || lastAt <= lastSlash) {
|
||||
return { repo: url };
|
||||
}
|
||||
} else if (lastAt <= lastSlash) {
|
||||
return { repo: url };
|
||||
}
|
||||
|
||||
return {
|
||||
repo: url.slice(0, lastAt),
|
||||
ref: url.slice(lastAt + 1),
|
||||
};
|
||||
}
|
||||
|
||||
function parseGenericGitUrl(url: string): GitSource | null {
|
||||
const { repo: repoWithoutRef, ref } = splitRef(url);
|
||||
let repo = repoWithoutRef;
|
||||
let host = "";
|
||||
let path = "";
|
||||
|
||||
const scpLikeMatch = repoWithoutRef.match(/^git@([^:]+):(.+)$/);
|
||||
if (scpLikeMatch) {
|
||||
host = scpLikeMatch[1] ?? "";
|
||||
path = scpLikeMatch[2] ?? "";
|
||||
} else if (
|
||||
repoWithoutRef.startsWith("https://") ||
|
||||
repoWithoutRef.startsWith("http://") ||
|
||||
repoWithoutRef.startsWith("ssh://")
|
||||
) {
|
||||
try {
|
||||
const parsed = new URL(repoWithoutRef);
|
||||
host = parsed.hostname;
|
||||
path = parsed.pathname.replace(/^\/+/, "");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
const slashIndex = repoWithoutRef.indexOf("/");
|
||||
if (slashIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
host = repoWithoutRef.slice(0, slashIndex);
|
||||
path = repoWithoutRef.slice(slashIndex + 1);
|
||||
if (!host.includes(".") && host !== "localhost") {
|
||||
return null;
|
||||
}
|
||||
repo = `https://${repoWithoutRef}`;
|
||||
}
|
||||
|
||||
const normalizedPath = path.replace(/\.git$/, "").replace(/^\/+/, "");
|
||||
if (!host || !normalizedPath || normalizedPath.split("/").length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
type: "git",
|
||||
repo,
|
||||
host,
|
||||
path: normalizedPath,
|
||||
ref,
|
||||
pinned: Boolean(ref),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse any git URL (SSH or HTTPS) into a GitSource.
|
||||
*/
|
||||
export function parseGitUrl(source: string): GitSource | null {
|
||||
let url = source.startsWith("git:") ? source.slice(4).trim() : source;
|
||||
const url = source.startsWith("git:") ? source.slice(4).trim() : source;
|
||||
const split = splitRef(url);
|
||||
|
||||
// Try hosted-git-info, converting @ref to #ref if needed
|
||||
let info = hostedGitInfo.fromUrl(url);
|
||||
const lastAt = url.lastIndexOf("@");
|
||||
if ((info?.project?.includes("@") || !info) && lastAt > 0) {
|
||||
info = hostedGitInfo.fromUrl(`${url.slice(0, lastAt)}#${url.slice(lastAt + 1)}`) ?? info;
|
||||
url = url.slice(0, lastAt); // strip ref from url for repo field
|
||||
const hostedCandidates = [url, split.ref ? `${split.repo}#${split.ref}` : undefined].filter(
|
||||
(value): value is string => Boolean(value),
|
||||
);
|
||||
for (const candidate of hostedCandidates) {
|
||||
const info = hostedGitInfo.fromUrl(candidate);
|
||||
if (info) {
|
||||
const useHttpsPrefix =
|
||||
!split.repo.startsWith("http://") &&
|
||||
!split.repo.startsWith("https://") &&
|
||||
!split.repo.startsWith("ssh://") &&
|
||||
!split.repo.startsWith("git@");
|
||||
return {
|
||||
type: "git",
|
||||
repo: useHttpsPrefix ? `https://${split.repo}` : split.repo,
|
||||
host: info.domain || "",
|
||||
path: `${info.user}/${info.project}`,
|
||||
ref: info.committish || split.ref || undefined,
|
||||
pinned: Boolean(info.committish || split.ref),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Try with https:// prefix for shorthand URLs
|
||||
if (!info) {
|
||||
info = hostedGitInfo.fromUrl(`https://${url}`);
|
||||
if (info) url = `https://${url}`; // make repo a valid clone URL
|
||||
const httpsCandidates = [`https://${url}`, split.ref ? `https://${split.repo}#${split.ref}` : undefined].filter(
|
||||
(value): value is string => Boolean(value),
|
||||
);
|
||||
for (const candidate of httpsCandidates) {
|
||||
const info = hostedGitInfo.fromUrl(candidate);
|
||||
if (info) {
|
||||
return {
|
||||
type: "git",
|
||||
repo: `https://${split.repo}`,
|
||||
host: info.domain || "",
|
||||
path: `${info.user}/${info.project}`,
|
||||
ref: info.committish || split.ref || undefined,
|
||||
pinned: Boolean(info.committish || split.ref),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (info) {
|
||||
return {
|
||||
type: "git",
|
||||
repo: url,
|
||||
host: info.domain || "",
|
||||
path: `${info.user}/${info.project}`,
|
||||
ref: info.committish || undefined,
|
||||
pinned: Boolean(info.committish),
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback for codeberg (not in hosted-git-info)
|
||||
const normalized = url.replace(/^https?:\/\//, "").replace(/@[^/]*$/, "");
|
||||
const codebergHost = "codeberg.org";
|
||||
if (normalized.startsWith(`${codebergHost}/`)) {
|
||||
const ref = url.match(/@([^/]+)$/)?.[1];
|
||||
const repoUrl = ref ? url.slice(0, url.lastIndexOf("@")) : url;
|
||||
// Ensure repo is a valid clone URL
|
||||
const cloneableRepo = repoUrl.startsWith("http") ? repoUrl : `https://${repoUrl}`;
|
||||
return {
|
||||
type: "git",
|
||||
repo: cloneableRepo,
|
||||
host: codebergHost,
|
||||
path: normalized.slice(codebergHost.length + 1).replace(/\.git$/, ""),
|
||||
ref,
|
||||
pinned: Boolean(ref),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
return parseGenericGitUrl(url);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,5 +146,30 @@ describe("Package Manager SSH URL Support", () => {
|
|||
expect(parsed.path).toBe("team/repo");
|
||||
expect(parsed.repo).toBe("git@bitbucket.org:team/repo");
|
||||
});
|
||||
|
||||
it("should parse unknown enterprise host shorthand", () => {
|
||||
const parsed = (packageManager as any).parseSource("git:github.tools.sap/agent-dev/sap-pie");
|
||||
expect(parsed.type).toBe("git");
|
||||
expect(parsed.host).toBe("github.tools.sap");
|
||||
expect(parsed.path).toBe("agent-dev/sap-pie");
|
||||
expect(parsed.repo).toBe("https://github.tools.sap/agent-dev/sap-pie");
|
||||
});
|
||||
|
||||
it("should parse unknown enterprise host with ref", () => {
|
||||
const parsed = (packageManager as any).parseSource("git:github.tools.sap/agent-dev/sap-pie@v1");
|
||||
expect(parsed.type).toBe("git");
|
||||
expect(parsed.host).toBe("github.tools.sap");
|
||||
expect(parsed.path).toBe("agent-dev/sap-pie");
|
||||
expect(parsed.ref).toBe("v1");
|
||||
expect(parsed.repo).toBe("https://github.tools.sap/agent-dev/sap-pie");
|
||||
expect(parsed.pinned).toBe(true);
|
||||
});
|
||||
|
||||
it("should normalize unknown enterprise host identities", () => {
|
||||
const withPrefix = (packageManager as any).getPackageIdentity("git:github.tools.sap/agent-dev/sap-pie");
|
||||
const withHttps = (packageManager as any).getPackageIdentity("https://github.tools.sap/agent-dev/sap-pie");
|
||||
expect(withPrefix).toBe("git:github.tools.sap/agent-dev/sap-pie");
|
||||
expect(withPrefix).toBe(withHttps);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue