|
1 | 1 | import 'server-only'; |
2 | | -import { fileNotFound, ServiceError, unexpectedError } from "../../lib/serviceError"; |
| 2 | +import { fileNotFound, notFound, ServiceError, unexpectedError } from "../../lib/serviceError"; |
3 | 3 | import { FileSourceRequest, FileSourceResponse } from "./types"; |
4 | | -import { isServiceError } from "../../lib/utils"; |
5 | | -import { search } from "./searchApi"; |
6 | 4 | import { sew } from "@/actions"; |
7 | 5 | import { withOptionalAuthV2 } from "@/withAuthV2"; |
8 | | -import { QueryIR } from './ir'; |
9 | | -import escapeStringRegexp from "escape-string-regexp"; |
| 6 | +import { getRepoPath } from '@sourcebot/shared'; |
| 7 | +import { simpleGit } from 'simple-git'; |
| 8 | +import { detectLanguageFromFilename } from "@/lib/languageDetection"; |
| 9 | +import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; |
| 10 | +import { getCodeHostBrowseFileAtBranchUrl } from "@/lib/utils"; |
| 11 | +import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; |
10 | 12 |
|
11 | | -// @todo (bkellam) #574 : We should really be using `git show <hash>:<path>` to fetch file contents here. |
12 | | -// This will allow us to support permalinks to files at a specific revision that may not be indexed |
13 | | -// by zoekt. We should also refactor this out of the /search folder. |
14 | | - |
15 | | -export const getFileSource = async ({ path, repo, ref }: FileSourceRequest): Promise<FileSourceResponse | ServiceError> => sew(() => |
16 | | - withOptionalAuthV2(async () => { |
17 | | - const query: QueryIR = { |
18 | | - and: { |
19 | | - children: [ |
20 | | - { |
21 | | - repo: { |
22 | | - regexp: `^${escapeStringRegexp(repo)}$`, |
23 | | - }, |
24 | | - }, |
25 | | - { |
26 | | - substring: { |
27 | | - pattern: path, |
28 | | - case_sensitive: true, |
29 | | - file_name: true, |
30 | | - content: false, |
31 | | - } |
32 | | - }, |
33 | | - ...(ref ? [{ |
34 | | - branch: { |
35 | | - pattern: ref, |
36 | | - exact: true, |
37 | | - }, |
38 | | - }]: []) |
39 | | - ] |
40 | | - } |
41 | | - } |
42 | | - |
43 | | - const searchResponse = await search({ |
44 | | - queryType: 'ir', |
45 | | - query, |
46 | | - options: { |
47 | | - matches: 1, |
48 | | - whole: true, |
49 | | - } |
| 13 | +export const getFileSource = async ({ path: filePath, repo: repoName, ref }: FileSourceRequest): Promise<FileSourceResponse | ServiceError> => sew(() => |
| 14 | + withOptionalAuthV2(async ({ org, prisma }) => { |
| 15 | + const repo = await prisma.repo.findFirst({ |
| 16 | + where: { name: repoName, orgId: org.id }, |
50 | 17 | }); |
51 | | - |
52 | | - if (isServiceError(searchResponse)) { |
53 | | - return searchResponse; |
| 18 | + if (!repo) { |
| 19 | + return notFound(`Repository "${repoName}" not found.`); |
54 | 20 | } |
55 | 21 |
|
56 | | - const files = searchResponse.files; |
57 | | - |
58 | | - if (!files || files.length === 0) { |
59 | | - return fileNotFound(path, repo); |
60 | | - } |
| 22 | + const { path: repoPath } = getRepoPath(repo); |
| 23 | + const git = simpleGit().cwd(repoPath); |
61 | 24 |
|
62 | | - const file = files[0]; |
63 | | - const source = file.content ?? ''; |
64 | | - const language = file.language; |
| 25 | + const gitRef = ref ?? 'HEAD'; |
65 | 26 |
|
66 | | - const repoInfo = searchResponse.repositoryInfo.find((repo) => repo.id === file.repositoryId); |
67 | | - if (!repoInfo) { |
68 | | - // This should never happen. |
69 | | - return unexpectedError("Repository info not found"); |
| 27 | + let source: string; |
| 28 | + try { |
| 29 | + source = await git.raw(['show', `${gitRef}:${filePath}`]); |
| 30 | + } catch (error: unknown) { |
| 31 | + const errorMessage = error instanceof Error ? error.message : String(error); |
| 32 | + if (errorMessage.includes('does not exist') || errorMessage.includes('fatal: path')) { |
| 33 | + return fileNotFound(filePath, repoName); |
| 34 | + } |
| 35 | + if (errorMessage.includes('unknown revision') || errorMessage.includes('bad revision')) { |
| 36 | + return unexpectedError(`Invalid git reference: ${gitRef}`); |
| 37 | + } |
| 38 | + throw error; |
70 | 39 | } |
71 | 40 |
|
| 41 | + const language = detectLanguageFromFilename(filePath); |
| 42 | + const webUrl = getBrowsePath({ |
| 43 | + repoName: repo.name, |
| 44 | + revisionName: ref, |
| 45 | + path: filePath, |
| 46 | + pathType: 'blob', |
| 47 | + domain: SINGLE_TENANT_ORG_DOMAIN, |
| 48 | + }); |
| 49 | + const externalWebUrl = getCodeHostBrowseFileAtBranchUrl({ |
| 50 | + webUrl: repo.webUrl, |
| 51 | + codeHostType: repo.external_codeHostType, |
| 52 | + branchName: gitRef, |
| 53 | + filePath, |
| 54 | + }); |
| 55 | + |
72 | 56 | return { |
73 | 57 | source, |
74 | 58 | language, |
75 | | - path, |
76 | | - repo, |
77 | | - repoCodeHostType: repoInfo.codeHostType, |
78 | | - repoDisplayName: repoInfo.displayName, |
79 | | - repoExternalWebUrl: repoInfo.webUrl, |
| 59 | + path: filePath, |
| 60 | + repo: repoName, |
| 61 | + repoCodeHostType: repo.external_codeHostType, |
| 62 | + repoDisplayName: repo.displayName ?? undefined, |
| 63 | + repoExternalWebUrl: repo.webUrl ?? undefined, |
80 | 64 | branch: ref, |
81 | | - webUrl: file.webUrl, |
82 | | - externalWebUrl: file.externalWebUrl, |
| 65 | + webUrl, |
| 66 | + externalWebUrl, |
83 | 67 | } satisfies FileSourceResponse; |
84 | | - |
85 | 68 | })); |
0 commit comments