* Resolves the flattened, filtered list of repository files for the configured * scope. Repositories are listed once, each is walked via the recursive Items * API, and blobs are filtered by path prefix and extension. The result is cached * on syncContext so offset-based pagination and the maxItem
( accessToken: string, organization: string, project: string, filters: FileFilters, syncContext?: Record<string, unknown> )
| 873 | * stable list across pages. |
| 874 | */ |
| 875 | async function resolveRepoFiles( |
| 876 | accessToken: string, |
| 877 | organization: string, |
| 878 | project: string, |
| 879 | filters: FileFilters, |
| 880 | syncContext?: Record<string, unknown> |
| 881 | ): Promise<RepoFileEntry[]> { |
| 882 | const cached = syncContext?.repoFiles as RepoFileEntry[] | undefined |
| 883 | if (cached) return cached |
| 884 | |
| 885 | const repositories = await resolveRepositories( |
| 886 | accessToken, |
| 887 | organization, |
| 888 | project, |
| 889 | filters.repositoryName, |
| 890 | syncContext |
| 891 | ) |
| 892 | |
| 893 | const normalizedPrefix = |
| 894 | filters.pathPrefix && !filters.pathPrefix.startsWith('/') |
| 895 | ? `/${filters.pathPrefix}` |
| 896 | : filters.pathPrefix |
| 897 | |
| 898 | const entries: RepoFileEntry[] = [] |
| 899 | for (const repo of repositories) { |
| 900 | const branch = filters.branch || stripRefsHeads(repo.defaultBranch ?? '') |
| 901 | if (!branch) { |
| 902 | /** |
| 903 | * No branch override and no resolvable default branch. An empty |
| 904 | * repository (size 0) has nothing to list and nothing previously synced, |
| 905 | * so it is skipped without flagging — flagging here would permanently |
| 906 | * suppress deletion reconciliation for any project containing an empty |
| 907 | * repo. A non-empty repository reaching this branch means content exists |
| 908 | * but its default branch ref is missing/unreadable, so the listing is |
| 909 | * flagged incomplete to protect previously synced files from |
| 910 | * reconciliation deletion. |
| 911 | */ |
| 912 | if ((repo.size ?? 0) > 0 && syncContext) { |
| 913 | syncContext.listingCapped = true |
| 914 | } |
| 915 | logger.warn('Skipping Azure DevOps repository with no default branch', { |
| 916 | repoId: repo.id, |
| 917 | repoName: repo.name, |
| 918 | size: repo.size ?? 0, |
| 919 | }) |
| 920 | continue |
| 921 | } |
| 922 | const blobs = await listRepositoryBlobs( |
| 923 | accessToken, |
| 924 | organization, |
| 925 | project, |
| 926 | repo.id, |
| 927 | branch, |
| 928 | syncContext |
| 929 | ) |
| 930 | for (const item of blobs) { |
| 931 | if (normalizedPrefix && !item.path.startsWith(normalizedPrefix)) continue |
| 932 | if (!matchesExtension(item.path, filters.extensions)) continue |
no test coverage detected