Skip to content

Commit 9f9ce5f

Browse files
wip
1 parent fd9973a commit 9f9ce5f

11 files changed

Lines changed: 220 additions & 18 deletions

File tree

packages/backend/src/api.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export class Api {
116116
const record = createGitHubRepoRecord({
117117
repo: response.data,
118118
hostUrl: 'https://github.com',
119+
isAutoCleanupDisabled: true,
119120
});
120121

121122
const repo = await this.prisma.repo.create({
@@ -124,7 +125,7 @@ export class Api {
124125

125126
const [jobId ] = await this.repoIndexManager.createJobs([repo], RepoIndexingJobType.INDEX);
126127

127-
res.status(200).json({ jobId });
128+
res.status(200).json({ jobId, repoId: repo.id });
128129
}
129130

130131
public async dispose() {

packages/backend/src/repoCompileUtils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,13 @@ export const createGitHubRepoRecord = ({
9292
hostUrl,
9393
branches,
9494
tags,
95+
isAutoCleanupDisabled,
9596
}: {
9697
repo: OctokitRepository,
9798
hostUrl: string,
9899
branches?: string[],
99100
tags?: string[],
101+
isAutoCleanupDisabled?: boolean,
100102
}) => {
101103
const repoNameRoot = new URL(hostUrl)
102104
.toString()
@@ -121,6 +123,7 @@ export const createGitHubRepoRecord = ({
121123
isFork: repo.fork,
122124
isArchived: !!repo.archived,
123125
isPublic: isPublic,
126+
isAutoCleanupDisabled,
124127
org: {
125128
connect: {
126129
id: SINGLE_TENANT_ORG_ID,

packages/backend/src/repoIndexManager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ export class RepoIndexManager {
160160
connections: {
161161
none: {}
162162
},
163+
isAutoCleanupDisabled: false,
163164
OR: [
164165
{ indexedAt: null },
165166
{ indexedAt: { lt: gcGracePeriodMs } },
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "Repo" ADD COLUMN "isAutoCleanupDisabled" BOOLEAN NOT NULL DEFAULT false;

packages/db/prisma/schema.prisma

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,20 @@ enum CodeHostType {
4545
}
4646

4747
model Repo {
48-
id Int @id @default(autoincrement())
49-
name String /// Full repo name, including the vcs hostname (ex. github.com/sourcebot-dev/sourcebot)
50-
displayName String? /// Display name of the repo for UI (ex. sourcebot-dev/sourcebot)
51-
createdAt DateTime @default(now())
52-
updatedAt DateTime @updatedAt
53-
isFork Boolean
54-
isArchived Boolean
55-
isPublic Boolean @default(false)
56-
metadata Json /// For schema see repoMetadataSchema in packages/shared/src/types.ts
57-
cloneUrl String
58-
webUrl String?
59-
connections RepoToConnection[]
60-
imageUrl String?
48+
id Int @id @default(autoincrement())
49+
name String /// Full repo name, including the vcs hostname (ex. github.com/sourcebot-dev/sourcebot)
50+
displayName String? /// Display name of the repo for UI (ex. sourcebot-dev/sourcebot)
51+
createdAt DateTime @default(now())
52+
updatedAt DateTime @updatedAt
53+
isFork Boolean
54+
isArchived Boolean
55+
isPublic Boolean @default(false)
56+
isAutoCleanupDisabled Boolean @default(false) /// If true, automatic cleanup of this repo when it becomes orphaned will be disabled.
57+
metadata Json /// For schema see repoMetadataSchema in packages/shared/src/types.ts
58+
cloneUrl String
59+
webUrl String?
60+
connections RepoToConnection[]
61+
imageUrl String?
6162
6263
permittedAccounts AccountToRepoPermission[]
6364
permissionSyncJobs RepoPermissionSyncJob[]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { getRepoInfo } from "@/app/askgh/[owner]/[repo]/api";
2+
import { serviceErrorResponse } from "@/lib/serviceError";
3+
import { isServiceError } from "@/lib/utils";
4+
import { NextRequest } from "next/server";
5+
6+
export async function GET(
7+
_request: NextRequest,
8+
props: { params: Promise<{ repoId: string }> }
9+
) {
10+
const params = await props.params;
11+
const { repoId } = params;
12+
const repoIdNum = parseInt(repoId);
13+
14+
if (isNaN(repoIdNum)) {
15+
return new Response("Invalid repo ID", { status: 400 });
16+
}
17+
18+
const result = await getRepoInfo(repoIdNum);
19+
20+
if (isServiceError(result)) {
21+
return serviceErrorResponse(result);
22+
}
23+
24+
return Response.json(result);
25+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import 'server-only';
2+
3+
import { sew } from '@/actions';
4+
import { notFound, ServiceError } from '@/lib/serviceError';
5+
import { withOptionalAuthV2 } from '@/withAuthV2';
6+
import { RepoInfo } from './types';
7+
8+
export const getRepoInfo = async (repoId: number): Promise<RepoInfo | ServiceError> => sew(() =>
9+
withOptionalAuthV2(async ({ prisma }) => {
10+
const repo = await prisma.repo.findUnique({
11+
where: { id: repoId },
12+
include: {
13+
jobs: {
14+
orderBy: {
15+
createdAt: 'desc',
16+
},
17+
take: 1,
18+
},
19+
},
20+
});
21+
22+
if (!repo) {
23+
return notFound();
24+
}
25+
26+
return {
27+
id: repo.id,
28+
name: repo.name,
29+
displayName: repo.displayName,
30+
isIndexed: repo.indexedAt !== null,
31+
};
32+
})
33+
)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
'use client';
2+
3+
import { ServiceError } from "@/lib/serviceError";
4+
import { unwrapServiceError } from "@/lib/utils";
5+
import { useQuery } from "@tanstack/react-query";
6+
import { Loader2 } from "lucide-react";
7+
import { RepoInfo } from "../types";
8+
import { SearchBar } from "@/app/[domain]/components/searchBar";
9+
import { SyntaxGuideProvider } from "@/app/[domain]/components/syntaxGuideProvider";
10+
11+
const REINDEX_INTERVAL_MS = 2000;
12+
13+
interface Props {
14+
initialRepoInfo: RepoInfo;
15+
}
16+
17+
export function RepoStatusDisplay({ initialRepoInfo }: Props) {
18+
const { data: repoInfo, isError } = useQuery({
19+
queryKey: ['repo-status', initialRepoInfo.id],
20+
queryFn: () => unwrapServiceError(getRepoStatus(initialRepoInfo.id)),
21+
initialData: initialRepoInfo,
22+
refetchInterval: (query) => {
23+
const repo = query.state.data;
24+
25+
// If repo has been indexed before (indexedAt is not null), stop polling
26+
if (repo?.isIndexed) {
27+
return false;
28+
}
29+
30+
return REINDEX_INTERVAL_MS;
31+
},
32+
});
33+
34+
if (isError) {
35+
// todo
36+
return null;
37+
}
38+
39+
if (!repoInfo.isIndexed) {
40+
// Loading spinner only for first-time indexing (indexedAt is null)
41+
return (
42+
<div className="flex flex-col items-center justify-center min-h-[400px] p-4">
43+
<Loader2 className="w-12 h-12 animate-spin text-primary mb-4" />
44+
<h2 className="text-2xl font-semibold mb-2">
45+
Indexing in progress...
46+
</h2>
47+
<p className="text-muted-foreground text-center">
48+
This may take a few minutes. The page will update automatically.
49+
</p>
50+
</div>
51+
);
52+
}
53+
54+
return (
55+
<div>
56+
<pre className="p-4">
57+
{JSON.stringify({
58+
status: 'indexed',
59+
repo: {
60+
id: repoInfo.id,
61+
name: repoInfo.name,
62+
displayName: repoInfo.displayName,
63+
}
64+
}, null, 2)}
65+
</pre>
66+
<SyntaxGuideProvider>
67+
<SearchBar
68+
size="sm"
69+
defaults={{
70+
query: `repo:^${repoInfo.name}$`,
71+
}}
72+
autoFocus
73+
/>
74+
</SyntaxGuideProvider>
75+
</div>
76+
);
77+
}
78+
79+
const getRepoStatus = async (repoId: number): Promise<RepoInfo | ServiceError> => {
80+
const result = await fetch(
81+
`/api/repo-status/${repoId}`,
82+
{
83+
method: 'GET',
84+
headers: {
85+
'Content-Type': 'application/json',
86+
},
87+
}
88+
).then(response => response.json());
89+
return result as RepoInfo | ServiceError;
90+
}
Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
import { addGithubRepo } from "@/features/workerApi/actions";
2+
import { RepoStatusDisplay } from "./components/repoStatusDisplay";
3+
import { isServiceError, unwrapServiceError } from "@/lib/utils";
4+
import { ServiceErrorException } from "@/lib/serviceError";
5+
import { prisma } from "@/prisma";
6+
import { SINGLE_TENANT_ORG_ID } from "@/lib/constants";
7+
import { getRepoInfo } from "./api";
28

39
interface PageProps {
410
params: Promise<{ owner: string; repo: string }>;
@@ -8,9 +14,33 @@ export default async function GitHubRepoPage(props: PageProps) {
814
const params = await props.params;
915
const { owner, repo } = params;
1016

11-
const response = await addGithubRepo(owner, repo);
17+
const repoId = await (async () => {
18+
// 1. Look up repo by owner/repo
19+
const displayName = `${owner}/${repo}`;
20+
const existingRepo = await prisma.repo.findFirst({
21+
where: {
22+
orgId: SINGLE_TENANT_ORG_ID,
23+
displayName: displayName,
24+
external_codeHostType: 'github',
25+
external_codeHostUrl: 'https://github.com',
26+
},
27+
});
1228

13-
return <p>{JSON.stringify(response, null, 2)}</p>;
14-
}
29+
if (existingRepo) {
30+
return existingRepo.id;
31+
}
32+
33+
// 2. If it doesn't exist, attempt to create it
34+
const response = await addGithubRepo(owner, repo);
35+
36+
if (isServiceError(response)) {
37+
throw new ServiceErrorException(response);
38+
}
1539

40+
return response.repoId;
41+
})();
1642

43+
const repoInfo = await unwrapServiceError(getRepoInfo(repoId));
44+
45+
return <RepoStatusDisplay initialRepoInfo={repoInfo} />;
46+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
3+
export type RepoInfo = {
4+
id: number;
5+
name: string;
6+
displayName: string | null;
7+
isIndexed: boolean;
8+
};

0 commit comments

Comments
 (0)