Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions src/commands/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,20 +110,27 @@ async function fetchRepoInput(spec: string, cwd: string): Promise<FetchResult> {
}

const displayName = `${repoSpec.host}/${repoSpec.owner}/${repoSpec.repo}`;
console.log(
`\nFetching ${repoSpec.owner}/${repoSpec.repo} from ${repoSpec.host}...`,
);
const displayNameWithSubpath = repoSpec.subpath
? `${displayName}/${repoSpec.subpath}`
: displayName;
const logTarget = repoSpec.subpath
? `${repoSpec.owner}/${repoSpec.repo}/${repoSpec.subpath}`
: `${repoSpec.owner}/${repoSpec.repo}`;
console.log(`\nFetching ${logTarget} from ${repoSpec.host}...`);

try {
// Check if already exists with the same ref
if (repoExists(displayName, cwd)) {
const existing = await getRepoInfo(displayName, cwd);
const existing = await getRepoInfo(displayNameWithSubpath, cwd);
if (existing && repoSpec.ref && existing.version === repoSpec.ref) {
console.log(` ✓ Already up to date (${repoSpec.ref})`);
const relativePath = repoSpec.subpath
? `${getRepoRelativePath(displayName)}/${repoSpec.subpath}`
: getRepoRelativePath(displayName);
return {
package: displayName,
package: displayNameWithSubpath,
version: existing.version,
path: getRepoRelativePath(displayName),
path: relativePath,
success: true,
};
} else if (existing) {
Expand All @@ -140,7 +147,10 @@ async function fetchRepoInput(spec: string, cwd: string): Promise<FetchResult> {
console.log(` → Ref: ${resolved.ref}`);

// Fetch the source
console.log(` → Cloning at ${resolved.ref}...`);
const cloneMsg = resolved.subpath
? ` → Sparse cloning ${resolved.subpath} at ${resolved.ref}...`
: ` → Cloning at ${resolved.ref}...`;
console.log(cloneMsg);
const result = await fetchRepoSource(resolved, cwd);

if (result.success) {
Expand All @@ -157,7 +167,7 @@ async function fetchRepoInput(spec: string, cwd: string): Promise<FetchResult> {
const errorMessage = err instanceof Error ? err.message : String(err);
console.log(` ✗ Error: ${errorMessage}`);
return {
package: displayName,
package: displayNameWithSubpath,
version: "",
path: "",
success: false,
Expand Down
61 changes: 51 additions & 10 deletions src/lib/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,35 @@ async function cloneAtRef(
}
}

/**
* Clone with sparse checkout - fetches only the specified subpath
*/
async function cloneAtRefSparse(
git: SimpleGit,
repoUrl: string,
targetPath: string,
ref: string,
subpath: string,
): Promise<{ success: boolean; ref?: string; error?: string }> {
try {
await git.clone(repoUrl, targetPath, [
"--filter=blob:none",
"--sparse",
"--branch",
ref,
"--single-branch",
]);
const repoGit = simpleGit(targetPath);
await repoGit.raw(["sparse-checkout", "set", subpath]);
return { success: true, ref };
} catch (err) {
return {
success: false,
error: `Failed to sparse clone: ${err instanceof Error ? err.message : String(err)}`,
};
}
}

/**
* Fetch source code for a resolved package
*/
Expand Down Expand Up @@ -364,17 +393,22 @@ export async function fetchRepoSource(
await mkdir(parentDir, { recursive: true });
}

// Clone the repository
const cloneResult = await cloneAtRef(
git,
resolved.repoUrl,
repoPath,
resolved.ref,
);
const cloneResult = resolved.subpath
? await cloneAtRefSparse(
git,
resolved.repoUrl,
repoPath,
resolved.ref,
resolved.subpath,
)
: await cloneAtRef(git, resolved.repoUrl, repoPath, resolved.ref);

if (!cloneResult.success) {
const displayName = resolved.subpath
? `${resolved.displayName}/${resolved.subpath}`
: resolved.displayName;
return {
package: resolved.displayName,
package: displayName,
version: resolved.ref,
path: getRepoRelativePath(resolved.displayName),
success: false,
Expand All @@ -388,10 +422,17 @@ export async function fetchRepoSource(
await rm(gitDir, { recursive: true, force: true });
}

const displayName = resolved.subpath
? `${resolved.displayName}/${resolved.subpath}`
: resolved.displayName;
const relativePath = resolved.subpath
? `${getRepoRelativePath(resolved.displayName)}/${resolved.subpath}`
: getRepoRelativePath(resolved.displayName);

return {
package: resolved.displayName,
package: displayName,
version: resolved.ref,
path: getRepoRelativePath(resolved.displayName),
path: relativePath,
success: true,
error: cloneResult.error,
};
Expand Down
4 changes: 4 additions & 0 deletions src/lib/registries/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,5 +232,9 @@ describe("detectInputType", () => {
it("GitLab URL", () => {
expect(detectInputType("https://gitlab.com/owner/repo")).toBe("repo");
});

it("owner/repo/subpath (monorepo subdirectory)", () => {
expect(detectInputType("vercel/ai/packages/ai")).toBe("repo");
});
});
});
103 changes: 103 additions & 0 deletions src/lib/repo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe("parseRepoSpec", () => {
owner: "vercel",
repo: "next.js",
ref: undefined,
subpath: undefined,
});
});

Expand All @@ -25,6 +26,7 @@ describe("parseRepoSpec", () => {
owner: "vercel",
repo: "next.js",
ref: "v14.0.0",
subpath: undefined,
});
});

Expand All @@ -35,6 +37,7 @@ describe("parseRepoSpec", () => {
owner: "vercel",
repo: "next.js",
ref: "main",
subpath: undefined,
});
});
});
Expand All @@ -47,6 +50,7 @@ describe("parseRepoSpec", () => {
owner: "gitlab-org",
repo: "gitlab",
ref: undefined,
subpath: undefined,
});
});

Expand All @@ -57,6 +61,7 @@ describe("parseRepoSpec", () => {
owner: "gitlab-org",
repo: "gitlab",
ref: "v16.0.0",
subpath: undefined,
});
});
});
Expand All @@ -69,6 +74,7 @@ describe("parseRepoSpec", () => {
owner: "atlassian",
repo: "python-bitbucket",
ref: undefined,
subpath: undefined,
});
});
});
Expand All @@ -81,6 +87,7 @@ describe("parseRepoSpec", () => {
owner: "vercel",
repo: "ai",
ref: undefined,
subpath: undefined,
});
});

Expand All @@ -91,6 +98,7 @@ describe("parseRepoSpec", () => {
owner: "vercel",
repo: "ai",
ref: undefined,
subpath: undefined,
});
});

Expand All @@ -101,6 +109,31 @@ describe("parseRepoSpec", () => {
owner: "vercel",
repo: "ai",
ref: "canary",
subpath: undefined,
});
});

it("parses https://github.com/owner/repo/tree/branch/subpath", () => {
const result = parseRepoSpec(
"https://github.com/vercel/ai/tree/main/packages/ai",
);
expect(result).toEqual({
host: "github.com",
owner: "vercel",
repo: "ai",
ref: "main",
subpath: "packages/ai",
});
});

it("parses https://github.com/owner/repo with subpath (no tree)", () => {
const result = parseRepoSpec("https://github.com/vercel/ai/packages/ai");
expect(result).toEqual({
host: "github.com",
owner: "vercel",
repo: "ai",
ref: undefined,
subpath: "packages/ai",
});
});

Expand All @@ -111,6 +144,7 @@ describe("parseRepoSpec", () => {
owner: "gitlab-org",
repo: "gitlab",
ref: undefined,
subpath: undefined,
});
});

Expand All @@ -121,6 +155,7 @@ describe("parseRepoSpec", () => {
owner: "owner",
repo: "repo",
ref: undefined,
subpath: undefined,
});
});
});
Expand All @@ -133,6 +168,18 @@ describe("parseRepoSpec", () => {
owner: "vercel",
repo: "next.js",
ref: undefined,
subpath: undefined,
});
});

it("parses github.com/owner/repo/subpath", () => {
const result = parseRepoSpec("github.com/vercel/ai/packages/ai");
expect(result).toEqual({
host: "github.com",
owner: "vercel",
repo: "ai",
ref: undefined,
subpath: "packages/ai",
});
});

Expand All @@ -143,6 +190,7 @@ describe("parseRepoSpec", () => {
owner: "gitlab-org",
repo: "gitlab",
ref: undefined,
subpath: undefined,
});
});
});
Expand All @@ -155,6 +203,7 @@ describe("parseRepoSpec", () => {
owner: "vercel",
repo: "next.js",
ref: undefined,
subpath: undefined,
});
});

Expand All @@ -165,6 +214,7 @@ describe("parseRepoSpec", () => {
owner: "vercel",
repo: "next.js",
ref: "v14.0.0",
subpath: undefined,
});
});

Expand All @@ -175,6 +225,51 @@ describe("parseRepoSpec", () => {
owner: "vercel",
repo: "ai",
ref: "main",
subpath: undefined,
});
});

it("parses owner/repo/subpath (monorepo subdirectory)", () => {
const result = parseRepoSpec("vercel/ai/packages/ai");
expect(result).toEqual({
host: "github.com",
owner: "vercel",
repo: "ai",
ref: undefined,
subpath: "packages/ai",
});
});

it("parses owner/repo/subpath with ref", () => {
const result = parseRepoSpec("vercel/ai/packages/ai@main");
expect(result).toEqual({
host: "github.com",
owner: "vercel",
repo: "ai",
ref: "main",
subpath: "packages/ai",
});
});

it("parses owner/repo/nested/subpath", () => {
const result = parseRepoSpec("facebook/react/packages/react");
expect(result).toEqual({
host: "github.com",
owner: "facebook",
repo: "react",
ref: undefined,
subpath: "packages/react",
});
});

it("parses github:owner/repo/subpath", () => {
const result = parseRepoSpec("github:vercel/ai/packages/ai");
expect(result).toEqual({
host: "github.com",
owner: "vercel",
repo: "ai",
ref: undefined,
subpath: "packages/ai",
});
});

Expand All @@ -185,6 +280,7 @@ describe("parseRepoSpec", () => {
owner: "vercel",
repo: "next.js",
ref: undefined,
subpath: undefined,
});
});

Expand All @@ -195,6 +291,7 @@ describe("parseRepoSpec", () => {
owner: "facebook",
repo: "react-native",
ref: undefined,
subpath: undefined,
});
});
});
Expand Down Expand Up @@ -224,6 +321,7 @@ describe("parseRepoSpec", () => {
owner: "vercel",
repo: "ai",
ref: undefined,
subpath: undefined,
});
});
});
Expand Down Expand Up @@ -269,6 +367,11 @@ describe("isRepoSpec", () => {
expect(isRepoSpec("vercel/ai@main")).toBe(true);
expect(isRepoSpec("vercel/ai#canary")).toBe(true);
});

it("owner/repo/subpath (monorepo subdirectory)", () => {
expect(isRepoSpec("vercel/ai/packages/ai")).toBe(true);
expect(isRepoSpec("facebook/react/packages/react")).toBe(true);
});
});

describe("returns false for non-repo specs", () => {
Expand Down
Loading