Skip to content
Merged
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
1 change: 1 addition & 0 deletions apps/code/src/main/services/folders/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const getFoldersOutput = z.array(registeredFolderWithExistsSchema);

export const addFolderInput = z.object({
folderPath: z.string().min(2, "Folder path must be a valid directory path"),
remoteUrl: z.string().min(1).optional(),
});

export const addFolderOutput = registeredFolderWithExistsSchema;
Expand Down
45 changes: 45 additions & 0 deletions apps/code/src/main/services/folders/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,51 @@ describe("FoldersService", () => {
expect(result.name).toBe("project");
});

it("tags a new folder with the supplied remoteUrl override", async () => {
vi.mocked(isGitRepository).mockResolvedValue(true);
mockRepositoryRepo.findByPath.mockReturnValue(null);
mockRepositoryRepo.create.mockReturnValue({
id: "folder-new",
path: "/home/user/fork",
remoteUrl: "PostHog/posthog",
lastAccessedAt: "2024-01-01T00:00:00.000Z",
createdAt: "2024-01-01T00:00:00.000Z",
updatedAt: "2024-01-01T00:00:00.000Z",
});

await service.addFolder("/home/user/fork", {
remoteUrl: "https://github.com/PostHog/posthog",
});

expect(mockRepositoryRepo.create).toHaveBeenCalledWith({
path: "/home/user/fork",
remoteUrl: "PostHog/posthog",
});
});

it("backfills remoteUrl on an existing folder when override is supplied", async () => {
vi.mocked(isGitRepository).mockResolvedValue(true);
const existing = {
id: "folder-existing",
path: "/home/user/project",
remoteUrl: null,
lastAccessedAt: "2024-01-01T00:00:00.000Z",
createdAt: "2024-01-01T00:00:00.000Z",
updatedAt: "2024-01-01T00:00:00.000Z",
};
mockRepositoryRepo.findByPath.mockReturnValue(existing);
mockRepositoryRepo.findById.mockReturnValue(existing);

await service.addFolder("/home/user/project", {
remoteUrl: "https://github.com/PostHog/posthog",
});

expect(mockRepositoryRepo.updateRemoteUrl).toHaveBeenCalledWith(
"folder-existing",
"PostHog/posthog",
);
});

it("throws error when user cancels git init", async () => {
vi.mocked(isGitRepository).mockResolvedValue(false);
mockDialog.confirm.mockResolvedValue(1);
Expand Down
37 changes: 23 additions & 14 deletions apps/code/src/main/services/folders/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export class FoldersService {

async addFolder(
folderPath: string,
options: { remoteUrl?: string } = {},
): Promise<RegisteredFolder & { exists: boolean }> {
const folderName = path.basename(folderPath);
if (!folderPath || !folderName) {
Expand Down Expand Up @@ -152,6 +153,7 @@ export class FoldersService {
}
}

const repoKey = await this.resolveRepoKey(folderPath, options.remoteUrl);
const existingRepo = this.repositoryRepo.findByPath(folderPath);
let repo: Repository;

Expand All @@ -163,23 +165,17 @@ export class FoldersService {
}
repo = updated;

if (!repo.remoteUrl) {
const remoteUrl = await getRemoteUrl(folderPath);
const repoKey = remoteUrl ? extractRepoKey(remoteUrl) : null;
if (repoKey) {
this.repositoryRepo.updateRemoteUrl(repo.id, repoKey);
const refreshed = this.repositoryRepo.findById(repo.id);
if (!refreshed) {
throw new Error(
`Repository ${repo.id} not found after remote URL update`,
);
}
repo = refreshed;
if (repoKey && repo.remoteUrl !== repoKey) {
this.repositoryRepo.updateRemoteUrl(repo.id, repoKey);
const refreshed = this.repositoryRepo.findById(repo.id);
if (!refreshed) {
throw new Error(
`Repository ${repo.id} not found after remote URL update`,
);
}
repo = refreshed;
}
} else {
const remoteUrl = await getRemoteUrl(folderPath);
const repoKey = remoteUrl ? extractRepoKey(remoteUrl) : null;
repo = this.repositoryRepo.create({
path: folderPath,
remoteUrl: repoKey ?? undefined,
Expand Down Expand Up @@ -248,6 +244,19 @@ export class FoldersService {
await manager.cleanupOrphanedWorktrees(associatedWorktreePaths);
}

private async resolveRepoKey(
folderPath: string,
overrideRemoteUrl: string | undefined,
): Promise<string | null> {
if (overrideRemoteUrl) {
const overrideKey = extractRepoKey(overrideRemoteUrl);
if (overrideKey) return overrideKey;
return normalizeRepoKey(overrideRemoteUrl);
}
const localRemoteUrl = await getRemoteUrl(folderPath);
return localRemoteUrl ? extractRepoKey(localRemoteUrl) : null;
}

getRepositoryByRemoteUrl(
remoteUrl: string,
): { id: string; path: string } | null {
Expand Down
4 changes: 3 additions & 1 deletion apps/code/src/main/trpc/routers/folders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ export const foldersRouter = router({
.input(addFolderInput)
.output(addFolderOutput)
.mutation(({ input }) => {
return getService().addFolder(input.folderPath);
return getService().addFolder(input.folderPath, {
remoteUrl: input.remoteUrl,
});
}),

removeFolder: publicProcedure
Expand Down
3 changes: 2 additions & 1 deletion apps/code/src/renderer/components/HeaderRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ function LocalHandoffButton({ taskId, task }: { taskId: string; task: Task }) {
const workspace = useWorkspace(taskId);
const repoPath = workspace?.folderPath ?? null;
const authStatus = useAuthStateValue((s) => s.status);
const cloudHandoffEnabled = useFeatureFlag(CLOUD_HANDOFF_FLAG);
const cloudHandoffEnabled =
useFeatureFlag(CLOUD_HANDOFF_FLAG) || import.meta.env.DEV;
const { initiateHandoffToCloud } = useSessionCallbacks({
taskId,
task,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ async function resolveRepoPathFromRemote(
return repo?.path ?? null;
}

async function resolveRepoPathFromPicker(): Promise<string | null> {
async function resolveRepoPathFromPicker(
remoteUrl: string | null | undefined,
): Promise<string | null> {
const selectedPath = await trpcClient.os.selectDirectory.query();
if (!selectedPath) return null;

const folders = await trpcClient.folders.getFolders.query();
const folder = folders.find((f) => f.path === selectedPath);
if (!folder) {
await trpcClient.folders.addFolder.mutate({ folderPath: selectedPath });
}
await trpcClient.folders.addFolder.mutate({
folderPath: selectedPath,
remoteUrl: remoteUrl ?? undefined,
});

return selectedPath;
}
Expand Down Expand Up @@ -66,7 +67,7 @@ export class LocalHandoffService {
try {
const targetPath =
(await resolveRepoPathFromRemote(task.repository)) ??
(await resolveRepoPathFromPicker());
(await resolveRepoPathFromPicker(task.repository));

if (!targetPath) return;

Expand Down
25 changes: 25 additions & 0 deletions packages/git/src/handoff.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,31 @@ describe("GitHandoffTracker", () => {
});
}, 15000);

it("keeps shipped index consistent with worktreeTree for staged large files", async () => {
await withRepos(async (repos) => {
const largePath = path.join(repos.cloudRepo, "tracked.txt");
const modified = Buffer.alloc(1024 * 1024 + 1, 9);
await writeFile(largePath, modified);
await repos.cloudGit.add(["tracked.txt"]);

const capture = await captureAndApply(repos);

try {
const restored = await readFile(
path.join(repos.localRepo, "tracked.txt"),
"utf-8",
);
expect(restored).toBe("base\n");

const status = await repos.localGit.raw(["status", "--porcelain"]);
expect(status).not.toMatch(/^M[ M] tracked\.txt/m);
expect(status).not.toMatch(/^MM tracked\.txt/m);
} finally {
await cleanupCapture(capture);
}
});
}, 20000);

it("removes tracked files absent from the checkpoint worktree", async () => {
await withRepos(async (repos) => {
await rm(path.join(repos.cloudRepo, "tracked.txt"));
Expand Down
Loading
Loading