* Fetches and hydrates a single repository file by its externalId. Re-fetches the * item with content, rebuilds the objectId-based hash identically to the stub, * and skips binary, oversized, or empty files. Returns null for 404 / not found.
( accessToken: string, organization: string, project: string, externalId: string, branchOverride: string, syncContext?: Record<string, unknown> )
| 1021 | * and skips binary, oversized, or empty files. Returns null for 404 / not found. |
| 1022 | */ |
| 1023 | async function getFileDocument( |
| 1024 | accessToken: string, |
| 1025 | organization: string, |
| 1026 | project: string, |
| 1027 | externalId: string, |
| 1028 | branchOverride: string, |
| 1029 | syncContext?: Record<string, unknown> |
| 1030 | ): Promise<ExternalDocument | null> { |
| 1031 | const parsed = parseFileExternalId(externalId) |
| 1032 | if (!parsed) return null |
| 1033 | const { repoId, path } = parsed |
| 1034 | |
| 1035 | const { branch, repo } = await resolveFileBranch( |
| 1036 | accessToken, |
| 1037 | organization, |
| 1038 | project, |
| 1039 | repoId, |
| 1040 | branchOverride, |
| 1041 | syncContext |
| 1042 | ) |
| 1043 | if (!branch) { |
| 1044 | logger.warn('Cannot resolve branch for Azure DevOps file', { externalId }) |
| 1045 | return null |
| 1046 | } |
| 1047 | |
| 1048 | const metadataParams = new URLSearchParams({ |
| 1049 | path, |
| 1050 | 'versionDescriptor.version': branch, |
| 1051 | 'versionDescriptor.versionType': 'Branch', |
| 1052 | includeContentMetadata: 'true', |
| 1053 | $format: 'json', |
| 1054 | 'api-version': GIT_API_VERSION, |
| 1055 | }) |
| 1056 | const metadataUrl = `${ADO_BASE_URL}/${encodeURIComponent(organization)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repoId)}/items?${metadataParams.toString()}` |
| 1057 | const metadataResponse = await fetchWithRetry(metadataUrl, { |
| 1058 | method: 'GET', |
| 1059 | headers: { Accept: 'application/json', Authorization: patAuthHeader(accessToken) }, |
| 1060 | }) |
| 1061 | |
| 1062 | if (!metadataResponse.ok) { |
| 1063 | if (metadataResponse.status === 404) return null |
| 1064 | throw new Error(`Failed to fetch repository file metadata: ${metadataResponse.status}`) |
| 1065 | } |
| 1066 | |
| 1067 | const item = (await metadataResponse.json()) as GitItem |
| 1068 | if (!item.objectId) return null |
| 1069 | if (item.contentMetadata?.isBinary) { |
| 1070 | logger.info('Skipping binary Azure DevOps file', { path }) |
| 1071 | return null |
| 1072 | } |
| 1073 | |
| 1074 | /** |
| 1075 | * Content is fetched as raw bytes (Accept: application/octet-stream) rather |
| 1076 | * than via `includeContent=true` JSON. The JSON `content` field's encoding is |
| 1077 | * ambiguous (the API may deliver base64 or codepage-transcoded text per |
| 1078 | * `ItemContentType`), whereas the octet-stream response is the byte-exact git |
| 1079 | * blob, which is then binary-sniffed and decoded as UTF-8. |
| 1080 | */ |
no test coverage detected