Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 29 additions & 12 deletions packages/backend/src/bitbucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,14 @@ function cloudClient(user: string | undefined, token: string | undefined): Bitbu
**/
const getPaginatedCloud = async <T>(
path: CloudGetRequestPath,
get: (url: CloudGetRequestPath) => Promise<CloudPaginatedResponse<T>>
get: (path: CloudGetRequestPath, query?: Record<string, string>) => Promise<CloudPaginatedResponse<T>>
): Promise<T[]> => {
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;
Expand All @@ -166,25 +167,38 @@ const getPaginatedCloud = async <T>(
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<string, string>; } {
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<CloudRepository>(path, async (url) => {
const response = await client.apiClient.GET(url, {
const fetchFn = () => getPaginatedCloud<CloudRepository>(`/repositories/${workspace}` as CloudGetRequestPath, async (path, query) => {
const response = await client.apiClient.GET(path, {
params: {
path: {
workspace,
}
},
query: query,
}
});
const { data, error } = response;
Expand Down Expand Up @@ -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<CloudRepository>(path, async (url) => {
const response = await client.apiClient.GET(url, {
const repos = await getPaginatedCloud<CloudRepository>(`/repositories/${workspace}` as CloudGetRequestPath, async (path, query) => {
const response = await client.apiClient.GET(path, {
params: {
path: {
workspace,
},
query: {
...query,
q: `project.key="${project_name}"`
}
}
Expand Down
Loading