| 1 | import { execFile } from 'node:child_process'; |
| 2 | import { readdir } |
| 3 | import { promisify } |
| 4 | |
| 5 | import { runGit } |
| 6 | import { |
| 7 | CodeSearchUnavailableError, |
| 8 | InvalidSearchQueryError, |
| 9 | } from './code-search.errors.js'; |
| 10 | |
| 11 | const execFileAsync = promisify |
| 12 | |
| 13 | const SEARCH_TIMEOUT_MS = 10_000 |
| 14 | |
| 15 | export interface CodeSearchHit |
| 16 | repositoryId: string; |
| 17 | /** The commit the shard was built from, which is what the line numbers refer to. */ |
| 18 | commit: string; |
| 19 | path: string; |
| 20 | language: string; |
| 21 | lines: { |
| 22 | lineNumber: number |
| 23 | line: string |
| 24 | /** Half-open [start, end) offsets that slice `line` as a JavaScript string. */ |
| 25 | ranges: { |
| 26 | }[]; |
| 27 | } |
| 28 | |
| 29 | /** The fields of zoekt's `FileMatch` read here. `[]byte` fields arrive base64 encoded. */ |
| 30 | interface ZoektFile { |
| 31 | FileName: string; |
| 32 | Repository: string; |
| 33 | Version: string; |
| 34 | Language: string; |
| 35 | LineMatches?: { |
| 36 | Line: string |
| 37 | LineNumber: number |
| 38 | FileName: boolean |
| 39 | LineFragments: { |
| 40 | }[]; |
| 41 | } |
| 42 | |
| 43 | /** Shards are named `<name>_v16.00000.zoekt`, with further numbered shards for a large repository. Returns their file names. */ |
| 44 | export async function indexRepository |
| 45 | indexDir, |
| 46 | repoDirectory, |
| 47 | name, |
| 48 | isPublic, |
| 49 | }: { |
| 50 | indexDir: string |
| 51 | repoDirectory: string |
| 52 | name: string |
| 53 | isPublic: boolean |
| 54 | }) { |
| 55 | // zoekt copies the `zoekt` config section into the shard, which is what `public:yes` matches, but only for a repository that has `zoekt.name` or an origin remote; the API's cache has no origin. |
| 56 | for (const [key |
| 57 | ['zoekt.name', name |
| 58 | ['zoekt.public', isPublic |
| 59 | ]) { |
| 60 | await runGit({ |
| 61 | } |
| 62 | await execFileAsync('zoekt-git-index |
| 63 | '-index', |
| 64 | indexDir, |
| 65 | '-branches', |
| 66 | 'HEAD', |
| 67 | '-submodules=false', |
| 68 | '-disable_ctags', |
| 69 | repoDirectory, |
| 70 | ]); |
| 71 | return (await readdir |
| 72 | } |
| 73 | |
| 74 | /** Matching lines only: a file that matched by its path alone comes back with none. */ |
| 75 | export async function searchIndex |
| 76 | url, |
| 77 | query, |
| 78 | limit, |
| 79 | }: { |
| 80 | url: string |
| 81 | query: string |
| 82 | limit: number |
| 83 | }): Promise<CodeSearchHit |
| 84 | const response = |
| 85 | method: 'POST', |
| 86 | headers: { 'Content-Type': |
| 87 | body: JSON.stringify |
| 88 | signal: AbortSignal.timeout |
| 89 | }).catch((cause |
| 90 | throw new CodeSearchUnavailableError( |
| 91 | }); |
| 92 | |
| 93 | if (response.status |
| 94 | const { Error: |
| 95 | throw new InvalidSearchQueryError( |
| 96 | } |
| 97 | if (!response.ok |
| 98 | throw new CodeSearchUnavailableError( |
| 99 | cause: new Error |
| 100 | `zoekt answered ${response.status |
| 101 | ), |
| 102 | }); |
| 103 | } |
| 104 | |
| 105 | const { Result |
| 106 | Result: { |
| 107 | }; |
| 108 | |
| 109 | return (Result.Files |
| 110 | repositoryId: file.Repository |
| 111 | commit: file.Version |
| 112 | path: file.FileName |
| 113 | language: file.Language |
| 114 | lines: (file.LineMatches |
| 115 | .filter((match) |
| 116 | .map((match) |
| 117 | const line = |
| 118 | return { |
| 119 | lineNumber: match.LineNumber |
| 120 | line: line.toString |
| 121 | ranges: match.LineFragments |
| 122 | start: charOffset(line |
| 123 | end: charOffset(line |
| 124 | })), |
| 125 | }; |
| 126 | }), |
| 127 | })); |
| 128 | } |
| 129 | |
| 130 | // zoekt reports byte offsets; a browser slices UTF-16 strings. |
| 131 | function charOffset(line: |
| 132 | return line.subarray |
| 133 | } |
| 134 | |
| 135 | /** A zoekt `r:` clause matching exactly these repositories, or nothing to add when unscoped. */ |
| 136 | export function repositoryScope(repositoryIds |
| 137 | return repositoryIds |
| 138 | ? ` r:^(${repositoryIds. |
| 139 | : ''; |
| 140 | } |
| 141 | |
| 142 | function escapeRegExp(value: |
| 143 | return value.replace |
| 144 | } |