Skip to content

Commit 0200881

Browse files
committed
feat(bitbucket): integrate Bitbucket support with repository search and team management
1 parent 4d8b965 commit 0200881

File tree

6 files changed

+194
-36
lines changed

6 files changed

+194
-36
lines changed

backend/analytics_server/mhq/api/request_utils.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from stringcase import snakecase
88
from voluptuous import Invalid
99
from werkzeug.exceptions import BadRequest
10+
from mhq.utils.log import LOG
1011
from mhq.store.models.code.repository import TeamRepos
1112
from mhq.service.code.models.org_repo import RawTeamOrgRepo
1213
from mhq.store.models.code import WorkflowFilter, CodeProvider
@@ -82,20 +83,24 @@ def coerce_workflow_filter(filter_data: str) -> WorkflowFilter:
8283

8384

8485
def coerce_org_repo(repo: Dict[str, str]) -> RawTeamOrgRepo:
85-
return RawTeamOrgRepo(
86-
team_id=repo.get("team_id"),
87-
provider=CodeProvider(repo.get("provider")),
88-
name=repo.get("name"),
89-
org_name=repo.get("org"),
90-
slug=repo.get("slug"),
91-
idempotency_key=repo.get("idempotency_key"),
92-
default_branch=repo.get("default_branch"),
93-
deployment_type=(
94-
TeamReposDeploymentType(repo.get("deployment_type"))
95-
if repo.get("deployment_type")
96-
else TeamReposDeploymentType.PR_MERGE
97-
),
98-
)
86+
try:
87+
return RawTeamOrgRepo(
88+
team_id=repo.get("team_id"),
89+
provider=CodeProvider(repo.get("provider")),
90+
name=repo.get("name"),
91+
org_name=repo.get("org"),
92+
slug=repo.get("slug"),
93+
idempotency_key=repo.get("idempotency_key"),
94+
default_branch=repo.get("default_branch"),
95+
deployment_type=(
96+
TeamReposDeploymentType(repo.get("deployment_type"))
97+
if repo.get("deployment_type")
98+
else TeamReposDeploymentType.PR_MERGE
99+
),
100+
)
101+
except Exception as e:
102+
LOG.error(f"Error creating RawTeamOrgRepo with data: {repo}. Error: {str(e)}")
103+
raise
99104

100105

101106
def coerce_org_repos(repos: List[Dict[str, str]]) -> List[RawTeamOrgRepo]:

backend/analytics_server/mhq/store/models/code/enums.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
class CodeProvider(Enum):
55
GITHUB = "github"
66
GITLAB = "gitlab"
7+
BITBUCKET = "bitbucket"
78

89

910
class CodeBookmarkType(Enum):

web-server/pages/api/internal/[org_id]/git_provider_org.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as yup from 'yup';
22

3-
import { gitlabSearch, searchGithubRepos } from '@/api/internal/[org_id]/utils';
3+
import { gitlabSearch, searchGithubRepos, bitbucketSearch } from '@/api/internal/[org_id]/utils';
44
import { Endpoint } from '@/api-helpers/global';
55
import { Integration } from '@/constants/integrations';
66
import { dec } from '@/utils/auth-supplementary';
@@ -68,6 +68,18 @@ const getGitlabToken = async (org_id: ID) => {
6868
.then((r) => dec(r.access_token_enc_chunks));
6969
};
7070

71+
const getBitbucketToken = async (org_id: ID) => {
72+
return await db('Integration')
73+
.select()
74+
.where({
75+
org_id,
76+
name: Integration.BITBUCKET
77+
})
78+
.returning('*')
79+
.then(getFirstRow)
80+
.then((r) => dec(r.access_token_enc_chunks));
81+
};
82+
7183
const fetchMap = [
7284
{
7385
provider: Integration.GITHUB,
@@ -78,5 +90,10 @@ const fetchMap = [
7890
provider: Integration.GITLAB,
7991
search: gitlabSearch,
8092
getToken: getGitlabToken
93+
},
94+
{
95+
provider: Integration.BITBUCKET,
96+
search: bitbucketSearch,
97+
getToken: getBitbucketToken
8198
}
8299
];

web-server/pages/api/internal/[org_id]/utils.ts

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,134 @@ export const gitlabSearch = async (pat: string, searchString: string) => {
269269
return searchGitlabRepos(pat, search);
270270
};
271271

272+
// Bitbucket functions
273+
274+
type BitbucketRepo = {
275+
uuid: string;
276+
name: string;
277+
full_name: string;
278+
description?: string;
279+
language?: string;
280+
mainbranch?: {
281+
name: string;
282+
};
283+
links: {
284+
html: {
285+
href: string;
286+
};
287+
};
288+
owner: {
289+
username: string;
290+
};
291+
};
292+
293+
type BitbucketResponse = {
294+
values: BitbucketRepo[];
295+
next?: string;
296+
};
297+
298+
const BITBUCKET_API_URL = 'https://api.bitbucket.org/2.0';
299+
300+
export const searchBitbucketRepos = async (
301+
credentials: string,
302+
searchString: string
303+
): Promise<BaseRepo[]> => {
304+
let urlString = convertUrlToQuery(searchString);
305+
if (urlString !== searchString && urlString.includes('/')) {
306+
try {
307+
return await searchBitbucketRepoWithURL(credentials, urlString);
308+
} catch (e) {
309+
return await searchBitbucketReposWithNames(credentials, urlString);
310+
}
311+
}
312+
return await searchBitbucketReposWithNames(credentials, urlString);
313+
};
314+
315+
const searchBitbucketRepoWithURL = async (
316+
credentials: string,
317+
searchString: string
318+
): Promise<BaseRepo[]> => {
319+
const apiUrl = `${BITBUCKET_API_URL}/repositories/${searchString}`;
320+
321+
const response = await fetch(apiUrl, {
322+
method: 'GET',
323+
headers: {
324+
Authorization: `Basic ${credentials}`,
325+
'Content-Type': 'application/json'
326+
}
327+
});
328+
329+
if (!response.ok) {
330+
throw new Error(`Bitbucket API error: ${response.statusText}`);
331+
}
332+
333+
const repo = (await response.json()) as BitbucketRepo;
334+
335+
return [
336+
{
337+
id: repo.uuid.replace(/[{}]/g, ''),
338+
name: repo.name,
339+
desc: repo.description,
340+
slug: repo.name,
341+
parent: repo.owner.username,
342+
web_url: repo.links.html.href,
343+
branch: repo.mainbranch?.name,
344+
language: repo.language,
345+
provider: Integration.BITBUCKET
346+
}
347+
] as BaseRepo[];
348+
};
349+
350+
const searchBitbucketReposWithNames = async (
351+
credentials: string,
352+
searchString: string
353+
): Promise<BaseRepo[]> => {
354+
const apiUrl = `${BITBUCKET_API_URL}/repositories`;
355+
const params = new URLSearchParams({
356+
q: `name~"${searchString}"`,
357+
role: 'member',
358+
pagelen: '50'
359+
});
360+
361+
const response = await fetch(`${apiUrl}?${params}`, {
362+
method: 'GET',
363+
headers: {
364+
Authorization: `Basic ${credentials}`,
365+
'Content-Type': 'application/json'
366+
}
367+
});
368+
369+
if (!response.ok) {
370+
throw new Error(`Bitbucket API error: ${response.statusText}`);
371+
}
372+
373+
const responseBody = (await response.json()) as BitbucketResponse;
374+
const repositories = responseBody.values || [];
375+
376+
return repositories.map(
377+
(repo) =>
378+
({
379+
id: repo.uuid.replace(/[{}]/g, ''),
380+
name: repo.name,
381+
desc: repo.description,
382+
slug: repo.name,
383+
parent: repo.owner.username,
384+
web_url: repo.links.html.href,
385+
branch: repo.mainbranch?.name,
386+
language: repo.language || null,
387+
provider: Integration.BITBUCKET
388+
}) as BaseRepo
389+
);
390+
};
391+
392+
export const bitbucketSearch = async (
393+
credentials: string,
394+
searchString: string
395+
): Promise<BaseRepo[]> => {
396+
let search = convertUrlToQuery(searchString);
397+
return searchBitbucketRepos(credentials, search);
398+
};
399+
272400
const convertUrlToQuery = (url: string) => {
273401
let query = url;
274402
try {
@@ -280,6 +408,7 @@ const convertUrlToQuery = (url: string) => {
280408
query = query.replace('http://', '');
281409
query = query.replace('github.com/', '');
282410
query = query.replace('gitlab.com/', '');
411+
query = query.replace('bitbucket.org/', '');
283412
query = query.startsWith('www.') ? query.slice(4) : query;
284413
query = query.endsWith('/') ? query.slice(0, -1) : query;
285414
}
@@ -303,4 +432,4 @@ const replaceURL = async (url: string): Promise<string> => {
303432
}
304433

305434
return url;
306-
};
435+
};

web-server/pages/api/resources/orgs/[org_id]/teams/v2.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ endpoint.handle.GET(getSchema, async (req, res) => {
9191
org_id,
9292
providers?.length
9393
? (providers as Integration[])
94-
: [Integration.GITHUB, Integration.GITLAB]
94+
: [Integration.GITHUB, Integration.GITLAB,Integration.BITBUCKET]
9595
);
9696

9797
res.send({
@@ -116,6 +116,7 @@ endpoint.handle.POST(postSchema, async (req, res) => {
116116
} as any as ReqRepoWithProvider);
117117
});
118118
}, org_repos);
119+
console.log('orgReposList in POST', orgReposList);
119120
const [team, onboardingState] = await Promise.all([
120121
createTeam(org_id, name, []),
121122
getOnBoardingState(org_id)
@@ -165,7 +166,7 @@ endpoint.handle.PATCH(patchSchema, async (req, res) => {
165166
} as any as ReqRepoWithProvider);
166167
});
167168
}, org_repos);
168-
169+
console.log('orgReposList in Patch', orgReposList);
169170
const [team] = await Promise.all([
170171
updateTeam(id, name, []),
171172
handleRequest<(Row<'TeamRepos'> & Row<'OrgRepo'>)[]>(`/teams/${id}/repos`, {
@@ -301,7 +302,7 @@ const updateReposWorkflows = async (
301302
.whereIn('name', reposForWorkflows)
302303
.where('org_id', org_id)
303304
.andWhere('is_active', true)
304-
.and.whereIn('provider', [Integration.GITHUB, Integration.GITLAB]);
305+
.and.whereIn('provider', [Integration.GITHUB, Integration.GITLAB, Integration.BITBUCKET]);
305306

306307
const groupedRepos = groupBy(dbReposForWorkflows, 'name');
307308

web-server/src/components/Teams/CreateTeams.tsx

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { DeploymentWorkflowSelector } from '@/components/WorkflowSelector';
3131
import { Integration } from '@/constants/integrations';
3232
import { useBoolState, useEasyState } from '@/hooks/useEasyState';
3333
import GitlabIcon from '@/mocks/icons/gitlab.svg';
34+
import BitbucketIcon from '@/mocks/icons/bitbucket.svg'
3435
import { BaseRepo, DeploymentSources } from '@/types/resources';
3536
import { trimWithEllipsis } from '@/utils/stringFormatting';
3637

@@ -257,7 +258,7 @@ const TeamRepos: FC = () => {
257258
)}
258259
renderOption={(props, option, { selected }) => (
259260
<li {...props}>
260-
<FlexBox
261+
<FlexBox
261262
gap={2}
262263
justifyBetween
263264
fullWidth
@@ -266,34 +267,36 @@ const TeamRepos: FC = () => {
266267
textOverflow: 'ellipsis',
267268
overflow: 'hidden'
268269
}}
269-
>
270+
>
270271
<FlexBox
271272
col
272273
sx={{ maxWidth: '200px', overflow: 'hidden' }}
273274
tooltipPlacement="right"
274275
title={
275-
checkOverflow(option) ? (
276-
<OverFlowTooltip
277-
parent={option.parent}
278-
name={option.name}
279-
/>
280-
) : undefined
276+
checkOverflow(option) ? (
277+
<OverFlowTooltip
278+
parent={option.parent}
279+
name={option.name}
280+
/>
281+
) : undefined
281282
}
282283
>
283284
<FlexBox gap={1 / 2} alignCenter>
284-
{option.provider === Integration.GITHUB ? (
285-
<GitHub sx={{ fontSize: '14px' }} />
286-
) : (
287-
<GitlabIcon height={12} width={12} />
288-
)}
289-
<Line tiny>
290-
{addEllipsis(option.parent, MAX_LENGTH_PARENT_NAME)}
291-
</Line>
285+
{option.provider === Integration.GITHUB ? (
286+
<GitHub sx={{ fontSize: '14px' }} />
287+
) : option.provider === Integration.BITBUCKET ? (
288+
<BitbucketIcon height={12} width={12} />
289+
) : (
290+
<GitlabIcon height={12} width={12} />
291+
)}
292+
<Line tiny>
293+
{addEllipsis(option.parent, MAX_LENGTH_PARENT_NAME)}
294+
</Line>
292295
</FlexBox>
293296
<Line>{addEllipsis(option.name, MAX_LENGTH_REPO_NAME)}</Line>
294297
</FlexBox>
295298
{selected ? <Close fontSize="small" /> : null}
296-
</FlexBox>
299+
</FlexBox>
297300
</li>
298301
)}
299302
renderTags={() => null}
@@ -398,6 +401,8 @@ const DisplayRepos: FC = () => {
398401
>
399402
{repo.provider === Integration.GITHUB ? (
400403
<GitHub sx={{ fontSize: '16px' }} />
404+
) : repo.provider === Integration.BITBUCKET ? (
405+
<BitbucketIcon height={14} width={14} />
401406
) : (
402407
<GitlabIcon height={14} width={14} />
403408
)}

0 commit comments

Comments
 (0)