Skip to content

Commit fb53723

Browse files
authored
Merge branch 'sourcebot-dev:main' into zoekt-pipe-fix
2 parents 331de9a + 83a8d30 commit fb53723

File tree

31 files changed

+1157
-180
lines changed

31 files changed

+1157
-180
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [4.6.8] - 2025-09-15
11+
12+
### Fixed
13+
- Fixed Bitbucket Cloud pagination not working beyond first page. [#295](https://github.com/sourcebot-dev/sourcebot/issues/295)
14+
- Fixed search bar line wrapping. [#501](https://github.com/sourcebot-dev/sourcebot/pull/501)
15+
- Fixed carousel perf issues. [#507](https://github.com/sourcebot-dev/sourcebot/pull/507)
16+
1017
## [4.6.7] - 2025-09-08
1118

1219
### Added

packages/backend/src/bitbucket.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,14 @@ function cloudClient(user: string | undefined, token: string | undefined): Bitbu
148148
**/
149149
const getPaginatedCloud = async <T>(
150150
path: CloudGetRequestPath,
151-
get: (url: CloudGetRequestPath) => Promise<CloudPaginatedResponse<T>>
151+
get: (path: CloudGetRequestPath, query?: Record<string, string>) => Promise<CloudPaginatedResponse<T>>
152152
): Promise<T[]> => {
153153
const results: T[] = [];
154-
let url = path;
154+
let nextPath = path;
155+
let nextQuery = undefined;
155156

156157
while (true) {
157-
const response = await get(url);
158+
const response = await get(nextPath, nextQuery);
158159

159160
if (!response.values || response.values.length === 0) {
160161
break;
@@ -166,25 +167,38 @@ const getPaginatedCloud = async <T>(
166167
break;
167168
}
168169

169-
url = response.next as CloudGetRequestPath;
170+
const parsedUrl = parseUrl(response.next);
171+
nextPath = parsedUrl.path as CloudGetRequestPath;
172+
nextQuery = parsedUrl.query;
170173
}
171174
return results;
172175
}
173-
176+
177+
/**
178+
* Parse the url into a path and query parameters to be used with the api client (openapi-fetch)
179+
*/
180+
function parseUrl(url: string): { path: string; query: Record<string, string>; } {
181+
const fullUrl = new URL(url);
182+
const path = fullUrl.pathname.replace(/^\/\d+(\.\d+)*/, ''); // remove version number in the beginning of the path
183+
const query = Object.fromEntries(fullUrl.searchParams);
184+
logger.debug(`Parsed url ${url} into path ${path} and query ${JSON.stringify(query)}`);
185+
return { path, query };
186+
}
187+
174188

175189
async function cloudGetReposForWorkspace(client: BitbucketClient, workspaces: string[]): Promise<{validRepos: CloudRepository[], notFoundWorkspaces: string[]}> {
176190
const results = await Promise.allSettled(workspaces.map(async (workspace) => {
177191
try {
178192
logger.debug(`Fetching all repos for workspace ${workspace}...`);
179193

180-
const path = `/repositories/${workspace}` as CloudGetRequestPath;
181194
const { durationMs, data } = await measure(async () => {
182-
const fetchFn = () => getPaginatedCloud<CloudRepository>(path, async (url) => {
183-
const response = await client.apiClient.GET(url, {
195+
const fetchFn = () => getPaginatedCloud<CloudRepository>(`/repositories/${workspace}` as CloudGetRequestPath, async (path, query) => {
196+
const response = await client.apiClient.GET(path, {
184197
params: {
185198
path: {
186199
workspace,
187-
}
200+
},
201+
query: query,
188202
}
189203
});
190204
const { data, error } = response;
@@ -238,11 +252,14 @@ async function cloudGetReposForProjects(client: BitbucketClient, projects: strin
238252

239253
logger.debug(`Fetching all repos for project ${project} for workspace ${workspace}...`);
240254
try {
241-
const path = `/repositories/${workspace}` as CloudGetRequestPath;
242-
const repos = await getPaginatedCloud<CloudRepository>(path, async (url) => {
243-
const response = await client.apiClient.GET(url, {
255+
const repos = await getPaginatedCloud<CloudRepository>(`/repositories/${workspace}` as CloudGetRequestPath, async (path, query) => {
256+
const response = await client.apiClient.GET(path, {
244257
params: {
258+
path: {
259+
workspace,
260+
},
245261
query: {
262+
...query,
246263
q: `project.key="${project_name}"`
247264
}
248265
}

packages/mcp/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [1.0.5] - 2025-09-15
11+
12+
### Changed
13+
- Updated API client to match the latest Sourcebot release. [#356](https://github.com/sourcebot-dev/sourcebot/pull/356)
14+
1015
## [1.0.4] - 2025-08-04
1116

1217
### Fixed

packages/mcp/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@sourcebot/mcp",
3-
"version": "1.0.4",
3+
"version": "1.0.5",
44
"type": "module",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

packages/mcp/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,10 @@ server.tool(
161161
};
162162
}
163163

164-
const content: TextContent[] = response.repos.map(repo => {
164+
const content: TextContent[] = response.map(repo => {
165165
return {
166166
type: "text",
167-
text: `id: ${repo.name}\nurl: ${repo.webUrl}`,
167+
text: `id: ${repo.repoName}\nurl: ${repo.webUrl}`,
168168
}
169169
});
170170

packages/mcp/src/schemas.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,16 +92,34 @@ export const searchResponseSchema = z.object({
9292
isBranchFilteringEnabled: z.boolean(),
9393
});
9494

95-
export const repositorySchema = z.object({
96-
name: z.string(),
97-
branches: z.array(z.string()),
95+
enum RepoIndexingStatus {
96+
NEW = 'NEW',
97+
IN_INDEX_QUEUE = 'IN_INDEX_QUEUE',
98+
INDEXING = 'INDEXING',
99+
INDEXED = 'INDEXED',
100+
FAILED = 'FAILED',
101+
IN_GC_QUEUE = 'IN_GC_QUEUE',
102+
GARBAGE_COLLECTING = 'GARBAGE_COLLECTING',
103+
GARBAGE_COLLECTION_FAILED = 'GARBAGE_COLLECTION_FAILED'
104+
}
105+
106+
export const repositoryQuerySchema = z.object({
107+
codeHostType: z.string(),
108+
repoId: z.number(),
109+
repoName: z.string(),
110+
repoDisplayName: z.string().optional(),
111+
repoCloneUrl: z.string(),
98112
webUrl: z.string().optional(),
99-
rawConfig: z.record(z.string(), z.string()).optional(),
113+
linkedConnections: z.array(z.object({
114+
id: z.number(),
115+
name: z.string(),
116+
})),
117+
imageUrl: z.string().optional(),
118+
indexedAt: z.coerce.date().optional(),
119+
repoIndexingStatus: z.nativeEnum(RepoIndexingStatus),
100120
});
101121

102-
export const listRepositoriesResponseSchema = z.object({
103-
repos: z.array(repositorySchema),
104-
});
122+
export const listRepositoriesResponseSchema = repositoryQuerySchema.array();
105123

106124
export const fileSourceRequestSchema = z.object({
107125
fileName: z.string(),

packages/mcp/src/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ export type SearchResultChunk = SearchResultFile["chunks"][number];
2222
export type SearchSymbol = z.infer<typeof symbolSchema>;
2323

2424
export type ListRepositoriesResponse = z.infer<typeof listRepositoriesResponseSchema>;
25-
export type Repository = ListRepositoriesResponse["repos"][number];
2625

2726
export type FileSourceRequest = z.infer<typeof fileSourceRequestSchema>;
2827
export type FileSourceResponse = z.infer<typeof fileSourceResponseSchema>;

packages/web/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"build": "cross-env SKIP_ENV_VALIDATION=1 next build",
88
"start": "next start",
99
"lint": "cross-env SKIP_ENV_VALIDATION=1 eslint .",
10-
"test": "vitest",
10+
"test": "cross-env SKIP_ENV_VALIDATION=1 vitest",
1111
"dev:emails": "email dev --dir ./src/emails",
1212
"stripe:listen": "stripe listen --forward-to http://localhost:3000/api/stripe"
1313
},
@@ -212,7 +212,8 @@
212212
"tsx": "^4.19.2",
213213
"typescript": "^5",
214214
"vite-tsconfig-paths": "^5.1.3",
215-
"vitest": "^2.1.5"
215+
"vitest": "^2.1.5",
216+
"vitest-mock-extended": "^3.1.0"
216217
},
217218
"resolutions": {
218219
"@types/react": "19.1.10",
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { SINGLE_TENANT_ORG_DOMAIN, SINGLE_TENANT_ORG_ID, SINGLE_TENANT_ORG_NAME } from '@/lib/constants';
2+
import { ApiKey, Org, PrismaClient, User } from '@prisma/client';
3+
import { beforeEach } from 'vitest';
4+
import { mockDeep, mockReset } from 'vitest-mock-extended';
5+
6+
beforeEach(() => {
7+
mockReset(prisma);
8+
});
9+
10+
export const prisma = mockDeep<PrismaClient>();
11+
12+
export const MOCK_ORG: Org = {
13+
id: SINGLE_TENANT_ORG_ID,
14+
name: SINGLE_TENANT_ORG_NAME,
15+
domain: SINGLE_TENANT_ORG_DOMAIN,
16+
createdAt: new Date(),
17+
updatedAt: new Date(),
18+
isOnboarded: true,
19+
imageUrl: null,
20+
metadata: null,
21+
memberApprovalRequired: false,
22+
stripeCustomerId: null,
23+
stripeSubscriptionStatus: null,
24+
stripeLastUpdatedAt: null,
25+
inviteLinkEnabled: false,
26+
inviteLinkId: null
27+
}
28+
29+
export const MOCK_API_KEY: ApiKey = {
30+
name: 'Test API Key',
31+
hash: 'apikey',
32+
createdAt: new Date(),
33+
lastUsedAt: new Date(),
34+
orgId: 1,
35+
createdById: '1',
36+
}
37+
38+
export const MOCK_USER: User = {
39+
id: '1',
40+
name: 'Test User',
41+
email: 'test@test.com',
42+
createdAt: new Date(),
43+
updatedAt: new Date(),
44+
hashedPassword: null,
45+
emailVerified: null,
46+
image: null
47+
}
48+

packages/web/src/actions.ts

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { getAuditService } from "@/ee/features/audit/factory";
4040
import { addUserToOrganization, orgHasAvailability } from "@/lib/authUtils";
4141
import { getOrgMetadata } from "@/lib/utils";
4242
import { getOrgFromDomain } from "./data/org";
43+
import { withOptionalAuthV2 } from "./withAuthV2";
4344

4445
const ajv = new Ajv({
4546
validateFormats: false,
@@ -637,49 +638,47 @@ export const getConnectionInfo = async (connectionId: number, domain: string) =>
637638
}
638639
})));
639640

640-
export const getRepos = async (domain: string, filter: { status?: RepoIndexingStatus[], connectionId?: number } = {}) => sew(() =>
641-
withAuth((userId) =>
642-
withOrgMembership(userId, domain, async ({ org }) => {
643-
const repos = await prisma.repo.findMany({
644-
where: {
645-
orgId: org.id,
646-
...(filter.status ? {
647-
repoIndexingStatus: { in: filter.status }
648-
} : {}),
649-
...(filter.connectionId ? {
650-
connections: {
651-
some: {
652-
connectionId: filter.connectionId
653-
}
654-
}
655-
} : {}),
656-
},
657-
include: {
641+
export const getRepos = async (filter: { status?: RepoIndexingStatus[], connectionId?: number } = {}) => sew(() =>
642+
withOptionalAuthV2(async ({ org }) => {
643+
const repos = await prisma.repo.findMany({
644+
where: {
645+
orgId: org.id,
646+
...(filter.status ? {
647+
repoIndexingStatus: { in: filter.status }
648+
} : {}),
649+
...(filter.connectionId ? {
658650
connections: {
659-
include: {
660-
connection: true,
651+
some: {
652+
connectionId: filter.connectionId
661653
}
662654
}
655+
} : {}),
656+
},
657+
include: {
658+
connections: {
659+
include: {
660+
connection: true,
661+
}
663662
}
664-
});
663+
}
664+
});
665665

666-
return repos.map((repo) => repositoryQuerySchema.parse({
667-
codeHostType: repo.external_codeHostType,
668-
repoId: repo.id,
669-
repoName: repo.name,
670-
repoDisplayName: repo.displayName ?? undefined,
671-
repoCloneUrl: repo.cloneUrl,
672-
webUrl: repo.webUrl ?? undefined,
673-
linkedConnections: repo.connections.map(({ connection }) => ({
674-
id: connection.id,
675-
name: connection.name,
676-
})),
677-
imageUrl: repo.imageUrl ?? undefined,
678-
indexedAt: repo.indexedAt ?? undefined,
679-
repoIndexingStatus: repo.repoIndexingStatus,
680-
}));
681-
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowAnonymousAccess = */ true
682-
));
666+
return repos.map((repo) => repositoryQuerySchema.parse({
667+
codeHostType: repo.external_codeHostType,
668+
repoId: repo.id,
669+
repoName: repo.name,
670+
repoDisplayName: repo.displayName ?? undefined,
671+
repoCloneUrl: repo.cloneUrl,
672+
webUrl: repo.webUrl ?? undefined,
673+
linkedConnections: repo.connections.map(({ connection }) => ({
674+
id: connection.id,
675+
name: connection.name,
676+
})),
677+
imageUrl: repo.imageUrl ?? undefined,
678+
indexedAt: repo.indexedAt ?? undefined,
679+
repoIndexingStatus: repo.repoIndexingStatus,
680+
}))
681+
}));
683682

684683
export const getRepoInfoByName = async (repoName: string, domain: string) => sew(() =>
685684
withAuth((userId) =>

0 commit comments

Comments
 (0)