Skip to content

Commit 5f3c173

Browse files
fix(worker): include project key in Bitbucket repo identifiers (#904)
* fix(backend): include project key in Bitbucket repo identifiers For Bitbucket Server, repo names now include the project key (PROJECT/repo) instead of just the repo name, preventing collisions across projects. For Bitbucket Cloud, repo names now include the project key (workspace/PROJECT/repo) instead of just workspace/repo. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: update CHANGELOG for #904 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(backend): align cloud exclude.repos matching with new identifier format Update cloudShouldExcludeRepo to match against workspace/PROJECT_KEY/repo instead of full_name (workspace/repo), consistent with the new displayName format. Also update schema examples to reflect the correct formats. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(backend): align cloud exclude.repos matching with new identifier format Update cloudShouldExcludeRepo to match against workspace/PROJECT_KEY/repo instead of full_name (workspace/repo), consistent with the new displayName format. Also update schema examples to reflect the correct formats. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: update CHANGELOG with breaking change note for #904 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(backend): add explicit project key validation for Bitbucket displayName Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * nit * test(backend): add tests for Bitbucket shouldExcludeRepo functions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1d57be3 commit 5f3c173

File tree

11 files changed

+206
-18
lines changed

11 files changed

+206
-18
lines changed

CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Fixed
11+
- Fixed Bitbucket Server and Cloud repo identifiers to include the project key, preventing collisions across projects. **Note:** Bitbucket Cloud users with `exclude.repos` patterns must update them from `workspace/repo` to `workspace/PROJECT_KEY/repo` format. [#904](https://github.com/sourcebot-dev/sourcebot/pull/904)
12+
1013
### Added
1114
- Added optional `visibility` parameter to `/api/chat/blocking` endpoint and MCP `ask_codebase` tool to allow controlling chat session visibility in shared environments. [#903](https://github.com/sourcebot-dev/sourcebot/pull/903)
1215

docs/snippets/schemas/v3/bitbucket.schema.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@
104104
},
105105
"examples": [
106106
[
107-
"cloud_workspace/repo1",
108-
"server_project/repo2"
107+
"cloud_workspace/PROJECT_KEY/repo1",
108+
"SERVER_PROJECT_KEY/repo2"
109109
]
110110
],
111111
"description": "List of specific repos to exclude from syncing."

docs/snippets/schemas/v3/connection.schema.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -781,8 +781,8 @@
781781
},
782782
"examples": [
783783
[
784-
"cloud_workspace/repo1",
785-
"server_project/repo2"
784+
"cloud_workspace/PROJECT_KEY/repo1",
785+
"SERVER_PROJECT_KEY/repo2"
786786
]
787787
],
788788
"description": "List of specific repos to exclude from syncing."

docs/snippets/schemas/v3/index.schema.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,8 +1196,8 @@
11961196
},
11971197
"examples": [
11981198
[
1199-
"cloud_workspace/repo1",
1200-
"server_project/repo2"
1199+
"cloud_workspace/PROJECT_KEY/repo1",
1200+
"SERVER_PROJECT_KEY/repo2"
12011201
]
12021202
],
12031203
"description": "List of specific repos to exclude from syncing."
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { expect, test, describe } from 'vitest';
2+
import { cloudShouldExcludeRepo, serverShouldExcludeRepo, BitbucketRepository } from './bitbucket';
3+
import { BitbucketConnectionConfig } from '@sourcebot/schemas/v3/bitbucket.type';
4+
import { SchemaRepository as CloudRepository } from '@coderabbitai/bitbucket/cloud/openapi';
5+
import { SchemaRestRepository as ServerRepository } from '@coderabbitai/bitbucket/server/openapi';
6+
7+
const makeCloudRepo = (overrides: Partial<CloudRepository> = {}): BitbucketRepository => ({
8+
type: 'repository',
9+
full_name: 'myworkspace/my-repo',
10+
project: { type: 'project', key: 'PROJ' },
11+
is_private: false,
12+
...overrides,
13+
} as CloudRepository);
14+
15+
const makeServerRepo = (overrides: Partial<ServerRepository> = {}): BitbucketRepository => ({
16+
slug: 'my-repo',
17+
project: { key: 'PROJ' },
18+
archived: false,
19+
...overrides,
20+
} as ServerRepository);
21+
22+
const baseConfig: BitbucketConnectionConfig = {
23+
type: 'bitbucket',
24+
deploymentType: 'cloud',
25+
};
26+
27+
describe('cloudShouldExcludeRepo', () => {
28+
test('returns false when no exclusions are configured', () => {
29+
expect(cloudShouldExcludeRepo(makeCloudRepo(), baseConfig)).toBe(false);
30+
});
31+
32+
test('returns false when exclude.repos is empty', () => {
33+
expect(cloudShouldExcludeRepo(makeCloudRepo(), {
34+
...baseConfig,
35+
exclude: { repos: [] },
36+
})).toBe(false);
37+
});
38+
39+
test('returns true when repo matches exclude.repos exactly', () => {
40+
expect(cloudShouldExcludeRepo(makeCloudRepo(), {
41+
...baseConfig,
42+
exclude: { repos: ['myworkspace/PROJ/my-repo'] },
43+
})).toBe(true);
44+
});
45+
46+
test('returns false when exclude.repos does not match', () => {
47+
expect(cloudShouldExcludeRepo(makeCloudRepo(), {
48+
...baseConfig,
49+
exclude: { repos: ['myworkspace/PROJ/other-repo'] },
50+
})).toBe(false);
51+
});
52+
53+
test('returns true when repo matches a glob pattern in exclude.repos', () => {
54+
expect(cloudShouldExcludeRepo(makeCloudRepo(), {
55+
...baseConfig,
56+
exclude: { repos: ['myworkspace/PROJ/*'] },
57+
})).toBe(true);
58+
});
59+
60+
test('returns true when repo matches a workspace-level glob in exclude.repos', () => {
61+
expect(cloudShouldExcludeRepo(makeCloudRepo(), {
62+
...baseConfig,
63+
exclude: { repos: ['myworkspace/**'] },
64+
})).toBe(true);
65+
});
66+
67+
test('returns false when exclude.forks is true but repo is not a fork', () => {
68+
expect(cloudShouldExcludeRepo(makeCloudRepo(), {
69+
...baseConfig,
70+
exclude: { forks: true },
71+
})).toBe(false);
72+
});
73+
74+
test('returns true when exclude.forks is true and repo is a fork', () => {
75+
const forkedRepo = makeCloudRepo({ parent: { type: 'repository' } as CloudRepository });
76+
expect(cloudShouldExcludeRepo(forkedRepo, {
77+
...baseConfig,
78+
exclude: { forks: true },
79+
})).toBe(true);
80+
});
81+
82+
test('returns false when exclude.forks is false and repo is a fork', () => {
83+
const forkedRepo = makeCloudRepo({ parent: { type: 'repository' } as CloudRepository });
84+
expect(cloudShouldExcludeRepo(forkedRepo, {
85+
...baseConfig,
86+
exclude: { forks: false },
87+
})).toBe(false);
88+
});
89+
});
90+
91+
describe('serverShouldExcludeRepo', () => {
92+
const serverConfig: BitbucketConnectionConfig = {
93+
type: 'bitbucket',
94+
deploymentType: 'server',
95+
url: 'https://bitbucket.example.com',
96+
};
97+
98+
test('returns false when no exclusions are configured', () => {
99+
expect(serverShouldExcludeRepo(makeServerRepo(), serverConfig)).toBe(false);
100+
});
101+
102+
test('returns false when exclude.repos is empty', () => {
103+
expect(serverShouldExcludeRepo(makeServerRepo(), {
104+
...serverConfig,
105+
exclude: { repos: [] },
106+
})).toBe(false);
107+
});
108+
109+
test('returns true when repo matches exclude.repos exactly', () => {
110+
expect(serverShouldExcludeRepo(makeServerRepo(), {
111+
...serverConfig,
112+
exclude: { repos: ['PROJ/my-repo'] },
113+
})).toBe(true);
114+
});
115+
116+
test('returns false when exclude.repos does not match', () => {
117+
expect(serverShouldExcludeRepo(makeServerRepo(), {
118+
...serverConfig,
119+
exclude: { repos: ['PROJ/other-repo'] },
120+
})).toBe(false);
121+
});
122+
123+
test('returns true when repo matches a glob pattern in exclude.repos', () => {
124+
expect(serverShouldExcludeRepo(makeServerRepo(), {
125+
...serverConfig,
126+
exclude: { repos: ['PROJ/*'] },
127+
})).toBe(true);
128+
});
129+
130+
test('returns false when exclude.archived is true but repo is not archived', () => {
131+
expect(serverShouldExcludeRepo(makeServerRepo({ archived: false }), {
132+
...serverConfig,
133+
exclude: { archived: true },
134+
})).toBe(false);
135+
});
136+
137+
test('returns true when exclude.archived is true and repo is archived', () => {
138+
expect(serverShouldExcludeRepo(makeServerRepo({ archived: true }), {
139+
...serverConfig,
140+
exclude: { archived: true },
141+
})).toBe(true);
142+
});
143+
144+
test('returns false when exclude.archived is false and repo is archived', () => {
145+
expect(serverShouldExcludeRepo(makeServerRepo({ archived: true }), {
146+
...serverConfig,
147+
exclude: { archived: false },
148+
})).toBe(false);
149+
});
150+
151+
test('returns false when exclude.forks is true but repo is not a fork', () => {
152+
expect(serverShouldExcludeRepo(makeServerRepo(), {
153+
...serverConfig,
154+
exclude: { forks: true },
155+
})).toBe(false);
156+
});
157+
158+
test('returns true when exclude.forks is true and repo is a fork', () => {
159+
const forkedRepo = makeServerRepo({ origin: { slug: 'original-repo' } as ServerRepository });
160+
expect(serverShouldExcludeRepo(forkedRepo, {
161+
...serverConfig,
162+
exclude: { forks: true },
163+
})).toBe(true);
164+
});
165+
166+
test('returns false when exclude.forks is false and repo is a fork', () => {
167+
const forkedRepo = makeServerRepo({ origin: { slug: 'original-repo' } as ServerRepository });
168+
expect(serverShouldExcludeRepo(forkedRepo, {
169+
...serverConfig,
170+
exclude: { forks: false },
171+
})).toBe(false);
172+
});
173+
});

packages/backend/src/bitbucket.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -345,10 +345,11 @@ async function cloudGetRepos(client: BitbucketClient, repoList: string[]): Promi
345345
};
346346
}
347347

348-
function cloudShouldExcludeRepo(repo: BitbucketRepository, config: BitbucketConnectionConfig): boolean {
348+
export function cloudShouldExcludeRepo(repo: BitbucketRepository, config: BitbucketConnectionConfig): boolean {
349349
const cloudRepo = repo as CloudRepository;
350350
let reason = '';
351-
const repoName = cloudRepo.full_name!;
351+
const [workspace, repoSlug] = cloudRepo.full_name!.split('/');
352+
const repoName = `${workspace}/${cloudRepo.project?.key}/${repoSlug}`;
352353

353354
const shouldExclude = (() => {
354355
if (config.exclude?.repos) {
@@ -552,7 +553,7 @@ async function serverGetRepos(client: BitbucketClient, repoList: string[]): Prom
552553
};
553554
}
554555

555-
function serverShouldExcludeRepo(repo: BitbucketRepository, config: BitbucketConnectionConfig): boolean {
556+
export function serverShouldExcludeRepo(repo: BitbucketRepository, config: BitbucketConnectionConfig): boolean {
556557
const serverRepo = repo as ServerRepository;
557558

558559
const projectName = serverRepo.project!.key;

packages/backend/src/repoCompileUtils.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,18 @@ export const compileBitbucketConfig = async (
454454
const repos = bitbucketRepos.map((repo) => {
455455
const isServer = config.deploymentType === 'server';
456456
const codeHostType: CodeHostType = isServer ? 'bitbucketServer' : 'bitbucketCloud';
457-
const displayName = isServer ? (repo as BitbucketServerRepository).name! : (repo as BitbucketCloudRepository).full_name!;
457+
const displayName = (() => {
458+
if (isServer) {
459+
const serverRepo = repo as BitbucketServerRepository;
460+
// Server repos are of the format `project/repo`
461+
return `${serverRepo.project!.key}/${serverRepo.slug!}`;
462+
} else {
463+
const cloudRepo = repo as BitbucketCloudRepository;
464+
// Cloud repos are of the format `workspace/project/repo`
465+
const [workspace, repoSlug] = cloudRepo.full_name!.split('/');
466+
return `${workspace}/${cloudRepo.project?.key!}/${repoSlug}`;
467+
}
468+
})();
458469
const externalId = isServer ? (repo as BitbucketServerRepository).id!.toString() : (repo as BitbucketCloudRepository).uuid!;
459470
const isPublic = isServer ? (repo as BitbucketServerRepository).public : (repo as BitbucketCloudRepository).is_private === false;
460471
const isArchived = isServer ? (repo as BitbucketServerRepository).archived === true : false;

packages/schemas/src/v3/bitbucket.schema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ const schema = {
103103
},
104104
"examples": [
105105
[
106-
"cloud_workspace/repo1",
107-
"server_project/repo2"
106+
"cloud_workspace/PROJECT_KEY/repo1",
107+
"SERVER_PROJECT_KEY/repo2"
108108
]
109109
],
110110
"description": "List of specific repos to exclude from syncing."

packages/schemas/src/v3/connection.schema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -780,8 +780,8 @@ const schema = {
780780
},
781781
"examples": [
782782
[
783-
"cloud_workspace/repo1",
784-
"server_project/repo2"
783+
"cloud_workspace/PROJECT_KEY/repo1",
784+
"SERVER_PROJECT_KEY/repo2"
785785
]
786786
],
787787
"description": "List of specific repos to exclude from syncing."

packages/schemas/src/v3/index.schema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,8 +1195,8 @@ const schema = {
11951195
},
11961196
"examples": [
11971197
[
1198-
"cloud_workspace/repo1",
1199-
"server_project/repo2"
1198+
"cloud_workspace/PROJECT_KEY/repo1",
1199+
"SERVER_PROJECT_KEY/repo2"
12001200
]
12011201
],
12021202
"description": "List of specific repos to exclude from syncing."

0 commit comments

Comments
 (0)