-
Notifications
You must be signed in to change notification settings - Fork 218
chore(web): use git show for fetching file contents
#829
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e2f13b9
2142512
61dae1e
04c9c3d
7f047b8
aea3b36
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| -- AlterTable | ||
| ALTER TABLE "Repo" ADD COLUMN "defaultBranch" TEXT; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,85 +1,70 @@ | ||
| import 'server-only'; | ||
| import { fileNotFound, ServiceError, unexpectedError } from "../../lib/serviceError"; | ||
| import { fileNotFound, notFound, ServiceError, unexpectedError } from "../../lib/serviceError"; | ||
| import { FileSourceRequest, FileSourceResponse } from "./types"; | ||
| import { isServiceError } from "../../lib/utils"; | ||
| import { search } from "./searchApi"; | ||
| import { sew } from "@/actions"; | ||
| import { withOptionalAuthV2 } from "@/withAuthV2"; | ||
| import { QueryIR } from './ir'; | ||
| import escapeStringRegexp from "escape-string-regexp"; | ||
| import { getRepoPath } from '@sourcebot/shared'; | ||
| import { simpleGit } from 'simple-git'; | ||
| import { detectLanguageFromFilename } from "@/lib/languageDetection"; | ||
| import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; | ||
| import { getCodeHostBrowseFileAtBranchUrl } from "@/lib/utils"; | ||
| import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; | ||
|
|
||
| // @todo (bkellam) #574 : We should really be using `git show <hash>:<path>` to fetch file contents here. | ||
| // This will allow us to support permalinks to files at a specific revision that may not be indexed | ||
| // by zoekt. We should also refactor this out of the /search folder. | ||
|
|
||
| export const getFileSource = async ({ path, repo, ref }: FileSourceRequest): Promise<FileSourceResponse | ServiceError> => sew(() => | ||
| withOptionalAuthV2(async () => { | ||
| const query: QueryIR = { | ||
| and: { | ||
| children: [ | ||
| { | ||
| repo: { | ||
| regexp: `^${escapeStringRegexp(repo)}$`, | ||
| }, | ||
| }, | ||
| { | ||
| substring: { | ||
| pattern: path, | ||
| case_sensitive: true, | ||
| file_name: true, | ||
| content: false, | ||
| } | ||
| }, | ||
| ...(ref ? [{ | ||
| branch: { | ||
| pattern: ref, | ||
| exact: true, | ||
| }, | ||
| }]: []) | ||
| ] | ||
| } | ||
| } | ||
|
|
||
| const searchResponse = await search({ | ||
| queryType: 'ir', | ||
| query, | ||
| options: { | ||
| matches: 1, | ||
| whole: true, | ||
| } | ||
| export const getFileSource = async ({ path: filePath, repo: repoName, ref }: FileSourceRequest): Promise<FileSourceResponse | ServiceError> => sew(() => | ||
| withOptionalAuthV2(async ({ org, prisma }) => { | ||
| const repo = await prisma.repo.findFirst({ | ||
| where: { name: repoName, orgId: org.id }, | ||
| }); | ||
|
|
||
| if (isServiceError(searchResponse)) { | ||
| return searchResponse; | ||
| if (!repo) { | ||
| return notFound(`Repository "${repoName}" not found.`); | ||
| } | ||
|
|
||
| const files = searchResponse.files; | ||
|
|
||
| if (!files || files.length === 0) { | ||
| return fileNotFound(path, repo); | ||
| } | ||
| const { path: repoPath } = getRepoPath(repo); | ||
| const git = simpleGit().cwd(repoPath); | ||
|
|
||
| const file = files[0]; | ||
| const source = file.content ?? ''; | ||
| const language = file.language; | ||
| const gitRef = ref ?? | ||
| repo.defaultBranch ?? | ||
| 'HEAD'; | ||
|
|
||
| const repoInfo = searchResponse.repositoryInfo.find((repo) => repo.id === file.repositoryId); | ||
| if (!repoInfo) { | ||
| // This should never happen. | ||
| return unexpectedError("Repository info not found"); | ||
| let source: string; | ||
| try { | ||
| source = await git.raw(['show', `${gitRef}:${filePath}`]); | ||
brendan-kellam marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } catch (error: unknown) { | ||
| const errorMessage = error instanceof Error ? error.message : String(error); | ||
| if (errorMessage.includes('does not exist') || errorMessage.includes('fatal: path')) { | ||
| return fileNotFound(filePath, repoName); | ||
| } | ||
| if (errorMessage.includes('unknown revision') || errorMessage.includes('bad revision') || errorMessage.includes('invalid object name')) { | ||
| return unexpectedError(`Invalid git reference: ${gitRef}`); | ||
| } | ||
| throw error; | ||
| } | ||
|
|
||
| const language = detectLanguageFromFilename(filePath); | ||
| const webUrl = getBrowsePath({ | ||
| repoName: repo.name, | ||
| revisionName: ref, | ||
| path: filePath, | ||
| pathType: 'blob', | ||
| domain: SINGLE_TENANT_ORG_DOMAIN, | ||
| }); | ||
| const externalWebUrl = getCodeHostBrowseFileAtBranchUrl({ | ||
| webUrl: repo.webUrl, | ||
| codeHostType: repo.external_codeHostType, | ||
| branchName: gitRef, | ||
| filePath, | ||
| }); | ||
|
Comment on lines
+51
to
+56
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n packages/web/src/features/search/fileSourceApi.ts | head -80Repository: sourcebot-dev/sourcebot Length of output: 3321 🏁 Script executed: # Find where gitRef is defined and how it defaults to 'HEAD'
rg -n "gitRef" packages/web/src/features/search/fileSourceApi.ts -B5 -A2Repository: sourcebot-dev/sourcebot Length of output: 1251 🏁 Script executed: # Find the getCodeHostBrowseFileAtBranchUrl function definition
fd . packages/web packages/backend -name "*.ts" -type f | xargs grep -l "getCodeHostBrowseFileAtBranchUrl" | head -5Repository: sourcebot-dev/sourcebot Length of output: 297 🏁 Script executed: # Search for getCodeHostBrowseFileAtBranchUrl function definition
rg -n "getCodeHostBrowseFileAtBranchUrl" packages/web packages/backend -t ts -B2 -A15 | grep -A15 "function\|export\|const getCodeHostBrowseFileAtBranchUrl"Repository: sourcebot-dev/sourcebot Length of output: 2341 🏁 Script executed: # Also find the exact file containing this function
fd . packages/web packages/backend -name "*.ts" -type f | xargs grep -l "getCodeHostBrowseFileAtBranchUrl"Repository: sourcebot-dev/sourcebot Length of output: 299 🏁 Script executed: sed -n '376,450p' packages/web/src/lib/utils.tsRepository: sourcebot-dev/sourcebot Length of output: 2283 🏁 Script executed: # Check for tests related to this function
rg -n "getCodeHostBrowseFileAtBranchUrl" packages/ --type ts -g "*.test.*"Repository: sourcebot-dev/sourcebot Length of output: 49 🏁 Script executed: # Check if there's any handling of 'HEAD' in external URL generation
rg -n "'HEAD'" packages/web/src/lib/utils.ts -B2 -A2Repository: sourcebot-dev/sourcebot Length of output: 49 🏁 Script executed: # Check if ref being undefined is actually a valid case (check fileSourceApi usage/tests)
fd . packages/ -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "fileSourceApi\|getFileSource" 2>/dev/null | head -3Repository: sourcebot-dev/sourcebot Length of output: 297 🏁 Script executed: # Search for test files containing fileSourceApi
find packages -type f \( -name "*.test.ts" -o -name "*.spec.ts" \)Repository: sourcebot-dev/sourcebot Length of output: 746 🏁 Script executed: # Check how ref parameter is used in FileSourceRequest type
rg -n "FileSourceRequest" packages/web/src/features/search/types.ts -A10Repository: sourcebot-dev/sourcebot Length of output: 505 🏁 Script executed: # Find the fileSourceRequestSchema definition
rg -n "fileSourceRequestSchema" packages/web/src/features/search/types.ts -B5 -A5Repository: sourcebot-dev/sourcebot Length of output: 585 External URL will be invalid when When Consider either resolving 🤖 Prompt for AI Agents |
||
|
|
||
| return { | ||
| source, | ||
| language, | ||
| path, | ||
| repo, | ||
| repoCodeHostType: repoInfo.codeHostType, | ||
| repoDisplayName: repoInfo.displayName, | ||
| repoExternalWebUrl: repoInfo.webUrl, | ||
| path: filePath, | ||
| repo: repoName, | ||
| repoCodeHostType: repo.external_codeHostType, | ||
| repoDisplayName: repo.displayName ?? undefined, | ||
| repoExternalWebUrl: repo.webUrl ?? undefined, | ||
| branch: ref, | ||
| webUrl: file.webUrl, | ||
| externalWebUrl: file.externalWebUrl, | ||
| webUrl, | ||
| externalWebUrl, | ||
| } satisfies FileSourceResponse; | ||
|
|
||
| })); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import * as linguistLanguages from 'linguist-languages'; | ||
| import path from 'path'; | ||
|
|
||
| const extensionToLanguage = new Map<string, string>(); | ||
|
|
||
| for (const [languageName, languageData] of Object.entries(linguistLanguages)) { | ||
| if ('extensions' in languageData && languageData.extensions) { | ||
| for (const ext of languageData.extensions) { | ||
| const normalizedExt = ext.toLowerCase(); | ||
| if (!extensionToLanguage.has(normalizedExt)) { | ||
| extensionToLanguage.set(normalizedExt, languageName); | ||
| } | ||
| } | ||
| } | ||
| if ('filenames' in languageData && languageData.filenames) { | ||
| for (const filename of languageData.filenames) { | ||
| if (!extensionToLanguage.has(filename)) { | ||
| extensionToLanguage.set(filename, languageName); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export const detectLanguageFromFilename = (filename: string): string => { | ||
| const basename = path.basename(filename); | ||
|
|
||
| // Check for exact filename match (e.g., Makefile, Dockerfile) | ||
| if (extensionToLanguage.has(basename)) { | ||
| return extensionToLanguage.get(basename)!; | ||
| } | ||
|
|
||
| // Check for extension match | ||
| const ext = path.extname(filename).toLowerCase(); | ||
| if (ext && extensionToLanguage.has(ext)) { | ||
| return extensionToLanguage.get(ext)!; | ||
| } | ||
|
|
||
| return ''; | ||
| }; |
Uh oh!
There was an error while loading. Please reload this page.