* Lists every blob in a repository at the given branch via the non-paginated * Items list API (recursionLevel=Full). Returns an empty list on 401/403/404 so * a single inaccessible or empty repo does not abort the sync.
( accessToken: string, organization: string, project: string, repoId: string, branch: string, syncContext?: Record<string, unknown> )
| 756 | * a single inaccessible or empty repo does not abort the sync. |
| 757 | */ |
| 758 | async function listRepositoryBlobs( |
| 759 | accessToken: string, |
| 760 | organization: string, |
| 761 | project: string, |
| 762 | repoId: string, |
| 763 | branch: string, |
| 764 | syncContext?: Record<string, unknown> |
| 765 | ): Promise<GitItem[]> { |
| 766 | const params = new URLSearchParams({ |
| 767 | recursionLevel: 'Full', |
| 768 | 'versionDescriptor.version': branch, |
| 769 | 'versionDescriptor.versionType': 'Branch', |
| 770 | 'api-version': GIT_API_VERSION, |
| 771 | }) |
| 772 | const url = `${ADO_BASE_URL}/${encodeURIComponent(organization)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repoId)}/items?${params.toString()}` |
| 773 | const response = await fetchWithRetry(url, { |
| 774 | method: 'GET', |
| 775 | headers: { Accept: 'application/json', Authorization: patAuthHeader(accessToken) }, |
| 776 | }) |
| 777 | if (!response.ok) { |
| 778 | if (response.status === 401 || response.status === 403 || response.status === 404) { |
| 779 | /** |
| 780 | * 401/403 mean the repository's files still exist but this PAT cannot |
| 781 | * read them right now — flag the listing as incomplete so reconciliation |
| 782 | * does not delete previously synced files. A 404 means the branch/repo |
| 783 | * content is genuinely absent (empty repo, deleted branch), so |
| 784 | * reconciliation stays enabled. |
| 785 | */ |
| 786 | if ((response.status === 401 || response.status === 403) && syncContext) { |
| 787 | syncContext.listingCapped = true |
| 788 | } |
| 789 | logger.warn('Azure DevOps repository items unavailable; skipping repository', { |
| 790 | repoId, |
| 791 | branch, |
| 792 | status: response.status, |
| 793 | }) |
| 794 | return [] |
| 795 | } |
| 796 | const errorText = await response.text().catch(() => '') |
| 797 | logger.error('Failed to list Azure DevOps repository items', { |
| 798 | repoId, |
| 799 | branch, |
| 800 | status: response.status, |
| 801 | error: errorText, |
| 802 | }) |
| 803 | throw new Error(`Failed to list repository items: ${response.status}`) |
| 804 | } |
| 805 | /** |
| 806 | * The Items list API documents no pagination, but very large trees may emit |
| 807 | * an `x-ms-continuationtoken` response header. No request parameter exists |
| 808 | * to follow it, so when it appears the tree is treated as incomplete: the |
| 809 | * listing is flagged so deletion reconciliation cannot remove files that |
| 810 | * were never returned. |
| 811 | */ |
| 812 | if (response.headers.get('x-ms-continuationtoken')) { |
| 813 | if (syncContext) syncContext.listingCapped = true |
| 814 | logger.warn( |
| 815 | 'Azure DevOps repository tree listing returned a continuation token; partial tree', |
no test coverage detected