Skip to content

Commit e4f08de

Browse files
committed
Shows branch PRs on remote
(#4107, #4146)
1 parent f72fbe0 commit e4f08de

File tree

4 files changed

+301
-9
lines changed

4 files changed

+301
-9
lines changed

src/plus/integrations/providers/bitbucket-server.ts

+20-5
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export class BitbucketServerIntegration extends HostingIntegration<
5757
return api.mergePullRequest(this.id, pr, {
5858
accessToken: accessToken,
5959
mergeMethod: options?.mergeMethod,
60+
baseUrl: this.apiBaseUrl,
6061
});
6162
}
6263

@@ -120,15 +121,27 @@ export class BitbucketServerIntegration extends HostingIntegration<
120121
}
121122

122123
protected override async getProviderPullRequestForBranch(
123-
_session: AuthenticationSession,
124-
_repo: BitbucketRepositoryDescriptor,
125-
_branch: string,
124+
{ accessToken }: AuthenticationSession,
125+
repo: BitbucketRepositoryDescriptor,
126+
branch: string,
126127
_options?: {
127128
avatarSize?: number;
128129
include?: PullRequestState[];
129130
},
130131
): Promise<PullRequest | undefined> {
131-
return Promise.resolve(undefined);
132+
const integration = await this.container.integrations.get(this.id);
133+
if (!integration) {
134+
return undefined;
135+
}
136+
return (await this.container.bitbucket)?.getServerPullRequestForBranch(
137+
this,
138+
accessToken,
139+
repo.owner,
140+
repo.name,
141+
branch,
142+
this.apiBaseUrl,
143+
integration,
144+
);
132145
}
133146

134147
protected override async getProviderPullRequestForCommit(
@@ -189,7 +202,9 @@ export class BitbucketServerIntegration extends HostingIntegration<
189202
if (!api || !integration) {
190203
return undefined;
191204
}
192-
const prs = await api.getBitbucketServerPullRequestsForCurrentUser({ accessToken: session.accessToken });
205+
const prs = await api.getBitbucketServerPullRequestsForCurrentUser(this.apiBaseUrl, {
206+
accessToken: session.accessToken,
207+
});
193208
return prs?.map(pr => fromProviderPullRequest(pr, integration));
194209
}
195210

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import { GitPullRequestMergeableState, GitPullRequestReviewState, GitPullRequestState } from '@gitkraken/provider-apis';
2+
import type { ProviderAccount, ProviderPullRequest } from '../models';
3+
4+
export interface BitbucketServerLink {
5+
href: string;
6+
}
7+
8+
export interface NamedBitbucketServerLink<T extends string = string> extends BitbucketServerLink {
9+
name: T;
10+
}
11+
12+
export interface BitbucketServerPagedResponse<T> {
13+
values: T[];
14+
size: number;
15+
limit: number;
16+
isLastPage: boolean;
17+
nextPageStart: number;
18+
start: number;
19+
}
20+
21+
export interface BitbucketServerPullRequestRef {
22+
id: string;
23+
displayId: string;
24+
latestCommit: string;
25+
type: string;
26+
repository: {
27+
slug: string;
28+
id: number;
29+
name: string;
30+
hierarchyId: string;
31+
scmId: string;
32+
state: string;
33+
statusMessage: string;
34+
forkable: boolean;
35+
project: {
36+
key: string;
37+
id: number;
38+
name: string;
39+
public: boolean;
40+
type: string;
41+
links: {
42+
self: BitbucketServerLink[];
43+
};
44+
};
45+
public: boolean;
46+
archived: boolean;
47+
links: {
48+
clone: NamedBitbucketServerLink[];
49+
self: BitbucketServerLink[];
50+
};
51+
};
52+
}
53+
54+
export interface BitbucketServerUser {
55+
name: string;
56+
emailAddress: string;
57+
active: boolean;
58+
displayName: string;
59+
id: number;
60+
slug: string;
61+
type: string;
62+
links: {
63+
self: BitbucketServerLink[];
64+
};
65+
avatarUrl?: string;
66+
}
67+
68+
export interface BitbucketServerPullRequestUser {
69+
user: BitbucketServerUser;
70+
lastReviewedCommit?: string;
71+
role: 'REVIEWER' | 'AUTHOR' | 'PARTICIPANT';
72+
approved: boolean;
73+
status: 'UNAPPROVED' | 'NEEDS_WORK' | 'APPROVED';
74+
}
75+
76+
export interface BitbucketServerPullRequest {
77+
id: number;
78+
version: number;
79+
title: string;
80+
description: string;
81+
state: 'OPEN' | 'MERGED' | 'DECLINED';
82+
open: boolean;
83+
closed: boolean;
84+
createdDate: number;
85+
updatedDate: number;
86+
closedDate: number | null;
87+
fromRef: BitbucketServerPullRequestRef;
88+
toRef: BitbucketServerPullRequestRef;
89+
locked: boolean;
90+
author: BitbucketServerPullRequestUser;
91+
reviewers: BitbucketServerPullRequestUser[];
92+
participants: BitbucketServerPullRequestUser[];
93+
properties: {
94+
mergeResult: {
95+
outcome: string;
96+
current: boolean;
97+
};
98+
resolvedTaskCount: number;
99+
commentCount: number;
100+
openTaskCount: number;
101+
};
102+
links: {
103+
self: BitbucketServerLink[];
104+
};
105+
}
106+
107+
const normalizeUser = (user: BitbucketServerUser): ProviderAccount => ({
108+
name: user.displayName,
109+
email: user.emailAddress,
110+
avatarUrl: user.avatarUrl ?? null,
111+
id: user.id.toString(),
112+
username: user.name,
113+
url: user.links.self[0].href,
114+
});
115+
116+
const reviewDecisionWeightByReviewState = {
117+
[GitPullRequestReviewState.Approved]: 0,
118+
[GitPullRequestReviewState.Commented]: 1,
119+
[GitPullRequestReviewState.ReviewRequested]: 2,
120+
[GitPullRequestReviewState.ChangesRequested]: 3,
121+
};
122+
123+
export const summarizeReviewDecision = (
124+
reviews: { state: GitPullRequestReviewState }[] | null,
125+
): GitPullRequestReviewState | null => {
126+
if (!reviews || reviews.length === 0) {
127+
return null;
128+
}
129+
130+
return reviews.reduce(
131+
(prev: GitPullRequestReviewState, review) =>
132+
reviewDecisionWeightByReviewState[review.state] > reviewDecisionWeightByReviewState[prev]
133+
? review.state
134+
: prev,
135+
GitPullRequestReviewState.Approved,
136+
);
137+
};
138+
139+
export const normalizeBitbucketServerPullRequest = (pr: BitbucketServerPullRequest): ProviderPullRequest => {
140+
const bitbucketStateToGitState = {
141+
OPEN: GitPullRequestState.Open,
142+
MERGED: GitPullRequestState.Merged,
143+
DECLINED: GitPullRequestState.Closed,
144+
};
145+
146+
const reviewerStatusToGitState = {
147+
UNAPPROVED: GitPullRequestReviewState.ReviewRequested,
148+
NEEDS_WORK: GitPullRequestReviewState.ChangesRequested,
149+
APPROVED: GitPullRequestReviewState.Approved,
150+
};
151+
152+
const reviews = pr.reviewers.map(reviewer => ({
153+
reviewer: normalizeUser(reviewer.user),
154+
state: reviewerStatusToGitState[reviewer.status],
155+
}));
156+
157+
const baseSSHUrl = pr.toRef.repository.links.clone.find(link => link.name === 'ssh')?.href ?? null;
158+
let baseHTTPSUrl = pr.toRef.repository.links.clone.find(link => link.name === 'https')?.href ?? null;
159+
if (!baseHTTPSUrl) {
160+
baseHTTPSUrl = pr.toRef.repository.links.clone.find(link => link.name === 'http')?.href ?? null;
161+
}
162+
163+
const headSSHUrl = pr.fromRef.repository.links.clone.find(link => link.name === 'ssh')?.href ?? null;
164+
let headHTTPSUrl = pr.fromRef.repository.links.clone.find(link => link.name === 'https')?.href ?? null;
165+
if (!headHTTPSUrl) {
166+
headHTTPSUrl = pr.fromRef.repository.links.clone.find(link => link.name === 'http')?.href ?? null;
167+
}
168+
169+
return {
170+
id: pr.id.toString(),
171+
number: pr.id,
172+
title: pr.title,
173+
url: pr.links.self[0].href,
174+
state: bitbucketStateToGitState[pr.state],
175+
isDraft: false,
176+
createdDate: new Date(pr.createdDate),
177+
updatedDate: new Date(pr.updatedDate),
178+
closedDate: pr.closedDate ? new Date(pr.closedDate) : null,
179+
mergedDate: pr.state === 'MERGED' && pr.closedDate ? new Date(pr.closedDate) : null,
180+
baseRef: {
181+
name: pr.toRef.displayId,
182+
oid: pr.toRef.latestCommit,
183+
},
184+
headRef: {
185+
name: pr.fromRef.displayId,
186+
oid: pr.fromRef.latestCommit,
187+
},
188+
commentCount: pr.properties.commentCount,
189+
upvoteCount: null,
190+
commitCount: null,
191+
fileCount: null,
192+
additions: null,
193+
deletions: null,
194+
author: normalizeUser(pr.author.user),
195+
assignees: null,
196+
reviews: reviews,
197+
reviewDecision: summarizeReviewDecision(reviews),
198+
repository: {
199+
id: pr.toRef.repository.id.toString(),
200+
name: pr.toRef.repository.name,
201+
owner: {
202+
login: pr.toRef.repository.project.key,
203+
},
204+
remoteInfo:
205+
baseHTTPSUrl && baseSSHUrl
206+
? {
207+
cloneUrlHTTPS: baseHTTPSUrl,
208+
cloneUrlSSH: baseSSHUrl,
209+
}
210+
: null,
211+
},
212+
headRepository: {
213+
id: pr.fromRef.repository.id.toString(),
214+
name: pr.fromRef.repository.name,
215+
owner: {
216+
login: pr.fromRef.repository.project.key,
217+
},
218+
remoteInfo:
219+
headHTTPSUrl && headSSHUrl
220+
? {
221+
cloneUrlHTTPS: headHTTPSUrl,
222+
cloneUrlSSH: headSSHUrl,
223+
}
224+
: null,
225+
},
226+
headCommit: null,
227+
mergeableState: GitPullRequestMergeableState.Unknown,
228+
permissions: null,
229+
version: pr.version,
230+
};
231+
};

src/plus/integrations/providers/bitbucket/bitbucket.ts

+41
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ import { Logger } from '../../../../system/logger';
2525
import type { LogScope } from '../../../../system/logger.scope';
2626
import { getLogScope } from '../../../../system/logger.scope';
2727
import { maybeStopWatch } from '../../../../system/stopwatch';
28+
import type { Integration } from '../../integration';
29+
import type { BitbucketServerPullRequest } from '../bitbucket-server/models';
30+
import { normalizeBitbucketServerPullRequest } from '../bitbucket-server/models';
31+
import { fromProviderPullRequest } from '../models';
2832
import type { BitbucketIssue, BitbucketPullRequest, BitbucketRepository } from './models';
2933
import { bitbucketIssueStateToState, fromBitbucketIssue, fromBitbucketPullRequest } from './models';
3034

@@ -93,6 +97,43 @@ export class BitbucketApi implements Disposable {
9397
return fromBitbucketPullRequest(response.values[0], provider);
9498
}
9599

100+
@debug<BitbucketApi['getServerPullRequestForBranch']>({ args: { 0: p => p.name, 1: '<token>' } })
101+
public async getServerPullRequestForBranch(
102+
provider: Provider,
103+
token: string,
104+
owner: string,
105+
repo: string,
106+
branch: string,
107+
baseUrl: string,
108+
integration: Integration,
109+
): Promise<PullRequest | undefined> {
110+
const scope = getLogScope();
111+
112+
const response = await this.request<{
113+
values: BitbucketServerPullRequest[];
114+
pagelen: number;
115+
size: number;
116+
page: number;
117+
}>(
118+
provider,
119+
token,
120+
baseUrl,
121+
`projects/${owner}/repos/${repo}/pull-requests?at=refs/heads/${branch}&direction=OUTGOING&state=ALL`,
122+
{
123+
method: 'GET',
124+
},
125+
scope,
126+
);
127+
128+
if (!response?.values?.length) {
129+
return undefined;
130+
}
131+
132+
const providersPr = normalizeBitbucketServerPullRequest(response.values[0]);
133+
const gitlensPr = fromProviderPullRequest(providersPr, integration);
134+
return gitlensPr;
135+
}
136+
96137
@debug<BitbucketApi['getUsersIssuesForRepo']>({ args: { 0: p => p.name, 1: '<token>' } })
97138
async getUsersIssuesForRepo(
98139
provider: Provider,

src/plus/integrations/providers/providersApi.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -610,16 +610,21 @@ export class ProvidersApi {
610610
}
611611
}
612612

613-
async getBitbucketServerPullRequestsForCurrentUser(options?: {
614-
accessToken?: string;
615-
}): Promise<ProviderPullRequest[] | undefined> {
613+
async getBitbucketServerPullRequestsForCurrentUser(
614+
baseUrl: string,
615+
options?: {
616+
accessToken?: string;
617+
},
618+
): Promise<ProviderPullRequest[] | undefined> {
616619
const { provider, token } = await this.ensureProviderTokenAndFunction(
617620
SelfHostedIntegrationId.BitbucketServer,
618621
'getBitbucketServerPullRequestsForCurrentUserFn',
619622
options?.accessToken,
620623
);
621624
try {
622-
return (await provider.getBitbucketServerPullRequestsForCurrentUserFn?.({}, { token: token }))?.data;
625+
return (
626+
await provider.getBitbucketServerPullRequestsForCurrentUserFn?.({}, { token: token, baseUrl: baseUrl })
627+
)?.data;
623628
} catch (e) {
624629
return this.handleProviderError(SelfHostedIntegrationId.BitbucketServer, token, e);
625630
}

0 commit comments

Comments
 (0)