From 7cbda320cb3cda92c0334f68454152ac02f551d6 Mon Sep 17 00:00:00 2001 From: Fede Sanchez Date: Mon, 15 Sep 2025 23:43:50 -0300 Subject: [PATCH] fix(bitbucket): Bitbucket Cloud pagination not working beyond first page (#502) --- CHANGELOG.md | 3 +++ packages/backend/src/bitbucket.ts | 41 ++++++++++++++++++++++--------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21fefa0b..ec5963a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- Fixed Bitbucket Cloud pagination not working beyond first page. [#295](https://github.com/sourcebot-dev/sourcebot/issues/295) + ## [4.6.7] - 2025-09-08 ### Added diff --git a/packages/backend/src/bitbucket.ts b/packages/backend/src/bitbucket.ts index e204850c..cfa591cc 100644 --- a/packages/backend/src/bitbucket.ts +++ b/packages/backend/src/bitbucket.ts @@ -148,13 +148,14 @@ function cloudClient(user: string | undefined, token: string | undefined): Bitbu **/ const getPaginatedCloud = async ( path: CloudGetRequestPath, - get: (url: CloudGetRequestPath) => Promise> + get: (path: CloudGetRequestPath, query?: Record) => Promise> ): Promise => { const results: T[] = []; - let url = path; + let nextPath = path; + let nextQuery = undefined; while (true) { - const response = await get(url); + const response = await get(nextPath, nextQuery); if (!response.values || response.values.length === 0) { break; @@ -166,25 +167,38 @@ const getPaginatedCloud = async ( break; } - url = response.next as CloudGetRequestPath; + const parsedUrl = parseUrl(response.next); + nextPath = parsedUrl.path as CloudGetRequestPath; + nextQuery = parsedUrl.query; } return results; } - + +/** + * Parse the url into a path and query parameters to be used with the api client (openapi-fetch) + */ +function parseUrl(url: string): { path: string; query: Record; } { + const fullUrl = new URL(url); + const path = fullUrl.pathname.replace(/^\/\d+(\.\d+)*/, ''); // remove version number in the beginning of the path + const query = Object.fromEntries(fullUrl.searchParams); + logger.debug(`Parsed url ${url} into path ${path} and query ${JSON.stringify(query)}`); + return { path, query }; +} + async function cloudGetReposForWorkspace(client: BitbucketClient, workspaces: string[]): Promise<{validRepos: CloudRepository[], notFoundWorkspaces: string[]}> { const results = await Promise.allSettled(workspaces.map(async (workspace) => { try { logger.debug(`Fetching all repos for workspace ${workspace}...`); - const path = `/repositories/${workspace}` as CloudGetRequestPath; const { durationMs, data } = await measure(async () => { - const fetchFn = () => getPaginatedCloud(path, async (url) => { - const response = await client.apiClient.GET(url, { + const fetchFn = () => getPaginatedCloud(`/repositories/${workspace}` as CloudGetRequestPath, async (path, query) => { + const response = await client.apiClient.GET(path, { params: { path: { workspace, - } + }, + query: query, } }); const { data, error } = response; @@ -238,11 +252,14 @@ async function cloudGetReposForProjects(client: BitbucketClient, projects: strin logger.debug(`Fetching all repos for project ${project} for workspace ${workspace}...`); try { - const path = `/repositories/${workspace}` as CloudGetRequestPath; - const repos = await getPaginatedCloud(path, async (url) => { - const response = await client.apiClient.GET(url, { + const repos = await getPaginatedCloud(`/repositories/${workspace}` as CloudGetRequestPath, async (path, query) => { + const response = await client.apiClient.GET(path, { params: { + path: { + workspace, + }, query: { + ...query, q: `project.key="${project_name}"` } }