refactor(portability): migrate to git-source; delete github-fetch.ts

Mirrors the skills refactor: company-portability was the second user of
the per-host REST shim (its own parallel parseGitHubSourceUrl + fetch
helpers + raw.githubusercontent URL builder), so importing a company
package from a non-github URL hit the same Gitea 404 the skills path did.

- Extend git-source.ts:
  - parseGitSourceUrl: also recognises query-string shape
    (?ref=...&path=...) used by portability URLs, with precedence over
    path-style segments when both are present.
  - RepoSnapshot: add readBinary (Uint8Array for the company logo
    fetch) and readFileOptional (null on NotFoundError, for the
    COMPANY.md probe + main->master fallback).
- Rewrite resolveSource in company-portability.ts to open a single
  in-memory snapshot per import and serve all reads (COMPANY.md,
  candidate tree, includes, logo) from it. Drops fetchText/fetchJson/
  fetchBinary/fetchOptionalText.
- parseGitHubSourceUrl stays exported with its original return shape
  ({hostname, owner, repo, ref, basePath, companyPath}) so the existing
  test suite passes unchanged. It now delegates URL parsing to
  parseGitSourceUrl and layers companyPath derivation on top.
- Delete server/src/services/github-fetch.ts: zero remaining callers.

Test coverage:
- 7 new git-source tests (query-string parse variants, query-string
  precedence over path style, readBinary, readFileOptional NotFound
  null + non-NotFound rethrow) — 34/34 passing.
- 52 existing company-portability tests still pass via the
  parseGitHubSourceUrl shim contract.
- Smoke-tested end-to-end against https://git.farh.net/.../?ref=main:
  ref resolves, snapshot opens, readFile/readBinary/readFileOptional
  all return expected results.

Note: two pre-existing failures in company-skills-routes.test.ts
("does not expose a skill reference...") exist on dev too and are
unrelated to this change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 10:28:22 -04:00
parent 4317d2a3b4
commit 80f7d8270c
4 changed files with 180 additions and 158 deletions
+41 -2
View File
@@ -25,6 +25,8 @@ export type RepoSnapshot = {
sha: string;
listFiles(): Promise<string[]>;
readFile(repoPath: string): Promise<string>;
readFileOptional(repoPath: string): Promise<string | null>;
readBinary(repoPath: string): Promise<Uint8Array>;
};
const SHA_REGEX = /^[0-9a-f]{40}$/i;
@@ -50,6 +52,25 @@ export function parseGitSourceUrl(rawUrl: string): ParsedGitSource {
const owner = segments[0]!;
const repo = segments[1]!.replace(/\.git$/i, "");
// Query-string shape: /{owner}/{repo}?ref=...&path=...
// Used by company portability URLs. Takes precedence over path-based parsing
// so a URL with both shapes (rare) prefers the explicit query params.
const queryRef = url.searchParams.get("ref")?.trim() ?? null;
const queryPath = url.searchParams.get("path")?.trim() ?? null;
if (queryRef || queryPath) {
const normalizedPath = (queryPath ?? "").replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
return {
cloneUrl: buildCloneUrl(url.hostname, owner, repo),
hostname: url.hostname,
owner,
repo,
ref: queryRef || null,
basePath: normalizedPath,
filePath: null,
explicitRef: Boolean(queryRef),
};
}
let ref: string | null = null;
let basePath = "";
let filePath: string | null = null;
@@ -233,11 +254,29 @@ export async function openRepoSnapshot(
return out;
}
async function readFile(repoPath: string): Promise<string> {
async function readBinary(repoPath: string): Promise<Uint8Array> {
const normalized = repoPath.replace(/^\/+/, "");
const { blob } = await git.readBlob({ fs, dir, oid: sha, filepath: normalized });
return blob;
}
async function readFile(repoPath: string): Promise<string> {
const blob = await readBinary(repoPath);
return new TextDecoder("utf-8").decode(blob);
}
return { sha, listFiles, readFile };
async function readFileOptional(repoPath: string): Promise<string | null> {
try {
return await readFile(repoPath);
} catch (err) {
// isomorphic-git throws NotFoundError when the path is missing from the tree.
const name = (err as { code?: string; name?: string } | null)?.code
?? (err as { name?: string } | null)?.name
?? "";
if (/NotFound/i.test(name)) return null;
throw err;
}
}
return { sha, listFiles, readFile, readFileOptional, readBinary };
}