Skip to content

Commit 8269659

Browse files
juliusmarmingeJulius Marminge
andauthored
bitubicket base brnach (#2500)
Co-authored-by: Julius Marminge <julius@macmini.local>
1 parent 623e471 commit 8269659

2 files changed

Lines changed: 164 additions & 14 deletions

File tree

apps/server/src/sourceControl/BitbucketApi.test.ts

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,18 @@ it.effect("expands all-state pull request listing instead of relying on Bitbucke
287287

288288
it.effect("reads repository clone URLs and default branch", () => {
289289
const { layer } = makeLayer({
290-
response: () => Response.json(repositoryJson),
290+
response: (request) =>
291+
Response.json(
292+
request.url.endsWith("/branching-model")
293+
? {
294+
development: {
295+
branch: { name: "main" },
296+
name: "main",
297+
use_mainbranch: true,
298+
},
299+
}
300+
: repositoryJson,
301+
),
291302
});
292303

293304
return Effect.gen(function* () {
@@ -307,6 +318,86 @@ it.effect("reads repository clone URLs and default branch", () => {
307318
}).pipe(Effect.provide(layer));
308319
});
309320

321+
it.effect(
322+
"prefers the Bitbucket branching model development branch as the default PR target",
323+
() => {
324+
const { execute, layer } = makeLayer({
325+
response: (request) =>
326+
Response.json(
327+
request.url.endsWith("/branching-model")
328+
? {
329+
development: {
330+
branch: { name: "develop" },
331+
name: "develop",
332+
use_mainbranch: false,
333+
},
334+
}
335+
: repositoryJson,
336+
),
337+
});
338+
339+
return Effect.gen(function* () {
340+
const bitbucket = yield* BitbucketApi.BitbucketApi;
341+
const defaultBranch = yield* bitbucket.getDefaultBranch({ cwd: "/repo" });
342+
343+
assert.strictEqual(defaultBranch, "develop");
344+
assert.deepStrictEqual(
345+
execute.mock.calls.map((call) => call[0].url).toSorted(),
346+
[
347+
"https://api.test.local/2.0/repositories/pingdotgg/t3code",
348+
"https://api.test.local/2.0/repositories/pingdotgg/t3code/branching-model",
349+
].toSorted(),
350+
);
351+
}).pipe(Effect.provide(layer));
352+
},
353+
);
354+
355+
it.effect(
356+
"falls back to the repository main branch when the Bitbucket development branch is invalid",
357+
() => {
358+
const { layer } = makeLayer({
359+
response: (request) =>
360+
Response.json(
361+
request.url.endsWith("/branching-model")
362+
? {
363+
development: {
364+
name: "develop",
365+
use_mainbranch: false,
366+
is_valid: false,
367+
},
368+
}
369+
: repositoryJson,
370+
),
371+
});
372+
373+
return Effect.gen(function* () {
374+
const bitbucket = yield* BitbucketApi.BitbucketApi;
375+
const defaultBranch = yield* bitbucket.getDefaultBranch({ cwd: "/repo" });
376+
377+
assert.strictEqual(defaultBranch, "main");
378+
}).pipe(Effect.provide(layer));
379+
},
380+
);
381+
382+
it.effect(
383+
"falls back to the repository main branch when the Bitbucket branching model is unavailable",
384+
() => {
385+
const { layer } = makeLayer({
386+
response: (request) =>
387+
request.url.endsWith("/branching-model")
388+
? Response.json({ error: { message: "Not found" } }, { status: 404 })
389+
: Response.json(repositoryJson),
390+
});
391+
392+
return Effect.gen(function* () {
393+
const bitbucket = yield* BitbucketApi.BitbucketApi;
394+
const defaultBranch = yield* bitbucket.getDefaultBranch({ cwd: "/repo" });
395+
396+
assert.strictEqual(defaultBranch, "main");
397+
}).pipe(Effect.provide(layer));
398+
},
399+
);
400+
310401
it.effect("creates repositories through the Bitbucket REST API", () => {
311402
const { execute, layer } = makeLayer({
312403
response: () => Response.json(repositoryJson),

apps/server/src/sourceControl/BitbucketApi.ts

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,23 @@ const RawBitbucketRepositorySchema = Schema.Struct({
6565
),
6666
});
6767

68+
const RawBitbucketBranchingModelSchema = Schema.Struct({
69+
development: Schema.optional(
70+
Schema.Struct({
71+
branch: Schema.optional(
72+
Schema.NullOr(
73+
Schema.Struct({
74+
name: Schema.optional(TrimmedNonEmptyString),
75+
}),
76+
),
77+
),
78+
is_valid: Schema.optional(Schema.Boolean),
79+
name: Schema.optional(Schema.NullOr(Schema.String)),
80+
use_mainbranch: Schema.optional(Schema.Boolean),
81+
}),
82+
),
83+
});
84+
6885
const BitbucketUserSchema = Schema.Struct({
6986
username: Schema.optional(TrimmedNonEmptyString),
7087
display_name: Schema.optional(TrimmedNonEmptyString),
@@ -216,7 +233,7 @@ function parseBitbucketRemoteUrl(remoteUrl: string): BitbucketRepositoryLocator
216233
}
217234

218235
function normalizeRepositoryCloneUrls(
219-
raw: Schema.Schema.Type<typeof RawBitbucketRepositorySchema>,
236+
raw: typeof RawBitbucketRepositorySchema.Type,
220237
): SourceControlRepositoryCloneUrls {
221238
const httpClone =
222239
raw.links.clone?.find((entry) => entry.name.toLowerCase() === "https")?.href ??
@@ -230,6 +247,24 @@ function normalizeRepositoryCloneUrls(
230247
};
231248
}
232249

250+
function defaultChangeRequestTargetBranch(input: {
251+
readonly repository: typeof RawBitbucketRepositorySchema.Type;
252+
readonly branchingModel: typeof RawBitbucketBranchingModelSchema.Type | null;
253+
}): string | null {
254+
const repositoryMainBranch = input.repository.mainbranch?.name ?? null;
255+
const development = input.branchingModel?.development;
256+
if (!development || development.use_mainbranch === true || development.is_valid === false) {
257+
return repositoryMainBranch;
258+
}
259+
260+
const developmentBranch = development.branch?.name?.trim() ?? development.name?.trim() ?? "";
261+
if (developmentBranch.length === 0 || developmentBranch === "null") {
262+
return repositoryMainBranch;
263+
}
264+
265+
return developmentBranch;
266+
}
267+
233268
function shouldPreferSshRemote(originRemoteUrl: string | null): boolean {
234269
const trimmed = originRemoteUrl?.trim() ?? "";
235270
return trimmed.startsWith("git@") || trimmed.startsWith("ssh://");
@@ -430,23 +465,32 @@ export const make = Effect.fn("makeBitbucketApi")(function* () {
430465
});
431466
});
432467

468+
const getRepositoryFromLocator = (repository: BitbucketRepositoryLocator) =>
469+
executeJson(
470+
"getRepository",
471+
HttpClientRequest.get(
472+
apiUrl(
473+
`/repositories/${encodeURIComponent(repository.workspace)}/${encodeURIComponent(repository.repoSlug)}`,
474+
),
475+
),
476+
RawBitbucketRepositorySchema,
477+
);
478+
433479
const getRepository = (input: {
434480
readonly cwd: string;
435481
readonly context?: SourceControlProvider.SourceControlProviderContext;
436482
readonly repository?: string;
437-
}) =>
438-
resolveRepository(input).pipe(
439-
Effect.flatMap((repository) =>
440-
executeJson(
441-
"getRepository",
442-
HttpClientRequest.get(
443-
apiUrl(
444-
`/repositories/${encodeURIComponent(repository.workspace)}/${encodeURIComponent(repository.repoSlug)}`,
445-
),
446-
),
447-
RawBitbucketRepositorySchema,
483+
}) => resolveRepository(input).pipe(Effect.flatMap(getRepositoryFromLocator));
484+
485+
const getBranchingModelFromLocator = (repository: BitbucketRepositoryLocator) =>
486+
executeJson(
487+
"getBranchingModel",
488+
HttpClientRequest.get(
489+
apiUrl(
490+
`/repositories/${encodeURIComponent(repository.workspace)}/${encodeURIComponent(repository.repoSlug)}/branching-model`,
448491
),
449492
),
493+
RawBitbucketBranchingModelSchema,
450494
);
451495

452496
const getRawPullRequestFromRepository = (
@@ -628,7 +672,22 @@ export const make = Effect.fn("makeBitbucketApi")(function* () {
628672
);
629673
}),
630674
getDefaultBranch: (input) =>
631-
getRepository(input).pipe(Effect.map((repository) => repository.mainbranch?.name ?? null)),
675+
resolveRepository(input).pipe(
676+
Effect.flatMap((locator) =>
677+
Effect.all(
678+
{
679+
repository: getRepositoryFromLocator(locator),
680+
branchingModel: getBranchingModelFromLocator(locator).pipe(
681+
Effect.catch(() =>
682+
Effect.succeed<typeof RawBitbucketBranchingModelSchema.Type | null>(null),
683+
),
684+
),
685+
},
686+
{ concurrency: "unbounded" },
687+
),
688+
),
689+
Effect.map(defaultChangeRequestTargetBranch),
690+
),
632691
// Bitbucket Cloud pull requests are Git-backed and Bitbucket does not provide
633692
// an official checkout CLI. This provider-local path uses GitVcsDriver as a
634693
// narrow escape hatch to materialize Bitbucket PR refs. Do not generalize this

0 commit comments

Comments
 (0)