Skip to content

Commit e2f13b9

Browse files
fix
1 parent 7c12e77 commit e2f13b9

File tree

5 files changed

+98
-68
lines changed

5 files changed

+98
-68
lines changed

packages/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
"input-otp": "^1.4.2",
146146
"langfuse": "^3.38.4",
147147
"langfuse-vercel": "^3.38.4",
148+
"linguist-languages": "^9.3.1",
148149
"lucide-react": "^0.517.0",
149150
"micromatch": "^4.0.8",
150151
"next": "15.5.9",
Lines changed: 50 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,68 @@
11
import 'server-only';
2-
import { fileNotFound, ServiceError, unexpectedError } from "../../lib/serviceError";
2+
import { fileNotFound, notFound, ServiceError, unexpectedError } from "../../lib/serviceError";
33
import { FileSourceRequest, FileSourceResponse } from "./types";
4-
import { isServiceError } from "../../lib/utils";
5-
import { search } from "./searchApi";
64
import { sew } from "@/actions";
75
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";
1012

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 },
5017
});
51-
52-
if (isServiceError(searchResponse)) {
53-
return searchResponse;
18+
if (!repo) {
19+
return notFound(`Repository "${repoName}" not found.`);
5420
}
5521

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);
6124

62-
const file = files[0];
63-
const source = file.content ?? '';
64-
const language = file.language;
25+
const gitRef = ref ?? 'HEAD';
6526

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;
7039
}
7140

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+
7256
return {
7357
source,
7458
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,
8064
branch: ref,
81-
webUrl: file.webUrl,
82-
externalWebUrl: file.externalWebUrl,
65+
webUrl,
66+
externalWebUrl,
8367
} satisfies FileSourceResponse;
84-
8568
}));
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import * as linguistLanguages from 'linguist-languages';
2+
import path from 'path';
3+
4+
const extensionToLanguage = new Map<string, string>();
5+
6+
for (const [languageName, languageData] of Object.entries(linguistLanguages)) {
7+
if ('extensions' in languageData && languageData.extensions) {
8+
for (const ext of languageData.extensions) {
9+
if (!extensionToLanguage.has(ext)) {
10+
extensionToLanguage.set(ext, languageName);
11+
}
12+
}
13+
}
14+
if ('filenames' in languageData && languageData.filenames) {
15+
for (const filename of languageData.filenames) {
16+
if (!extensionToLanguage.has(filename)) {
17+
extensionToLanguage.set(filename, languageName);
18+
}
19+
}
20+
}
21+
}
22+
23+
export const detectLanguageFromFilename = (filename: string): string => {
24+
const basename = path.basename(filename);
25+
26+
// Check for exact filename match (e.g., Makefile, Dockerfile)
27+
if (extensionToLanguage.has(basename)) {
28+
return extensionToLanguage.get(basename)!;
29+
}
30+
31+
// Check for extension match
32+
const ext = path.extname(filename).toLowerCase();
33+
if (ext && extensionToLanguage.has(ext)) {
34+
return extensionToLanguage.get(ext)!;
35+
}
36+
37+
return '';
38+
};

packages/web/src/lib/serviceError.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export const invalidZoektResponse = async (zoektResponse: Response): Promise<Ser
7272
};
7373
}
7474

75-
export const fileNotFound = async (fileName: string, repository: string): Promise<ServiceError> => {
75+
export const fileNotFound = (fileName: string, repository: string): ServiceError => {
7676
return {
7777
statusCode: StatusCodes.NOT_FOUND,
7878
errorCode: ErrorCode.FILE_NOT_FOUND,

yarn.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8395,6 +8395,7 @@ __metadata:
83958395
jsdom: "npm:^25.0.1"
83968396
langfuse: "npm:^3.38.4"
83978397
langfuse-vercel: "npm:^3.38.4"
8398+
linguist-languages: "npm:^9.3.1"
83988399
lucide-react: "npm:^0.517.0"
83998400
micromatch: "npm:^4.0.8"
84008401
next: "npm:15.5.9"
@@ -15018,6 +15019,13 @@ __metadata:
1501815019
languageName: node
1501915020
linkType: hard
1502015021

15022+
"linguist-languages@npm:^9.3.1":
15023+
version: 9.3.1
15024+
resolution: "linguist-languages@npm:9.3.1"
15025+
checksum: 10c0/41d5c16b9f7095310003598f4568254ac9736fc6f67daa1f62a11ae9aaf6acc847451675dbb8387b70ed8daaef75656dba8c8057ae93e07152304f3c27aa7440
15026+
languageName: node
15027+
linkType: hard
15028+
1502115029
"linkify-it@npm:^5.0.0":
1502215030
version: 5.0.0
1502315031
resolution: "linkify-it@npm:5.0.0"

0 commit comments

Comments
 (0)