1import { trimOptionalString } from "@t3tools/source-control-core/server/pullRequestUtils";
2import * as DateTime from "effect/DateTime";
3import * as Option from "effect/Option";
4import * as Schema from "effect/Schema";
5import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts";
6
7export interface NormalizedBitbucketPullRequestRecord {
8 readonly number: number;
9 readonly title: string;
10 readonly url: string;
11 readonly baseRefName: string;
12 readonly headRefName: string;
13 readonly headSha?: string;
14 readonly state: "open" | "closed" | "merged";
15 readonly isDraft?: boolean;
16 readonly updatedAt: Option.Option<DateTime.Utc>;
17 readonly isCrossRepository?: boolean;
18 readonly headRepositoryNameWithOwner?: string | null;
19 readonly headRepositoryOwnerLogin?: string | null;
20}
21
22export const BitbucketRepositoryRefSchema = Schema.Struct({
23 full_name: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)),
24 workspace: Schema.optional(
25 Schema.NullOr(
26 Schema.Struct({
27 slug: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)),
28 }),
29 ),
30 ),
31});
32
33export const BitbucketPullRequestBranchSchema = Schema.Struct({
34 repository: Schema.optional(Schema.NullOr(BitbucketRepositoryRefSchema)),
35 branch: Schema.Struct({
36 name: TrimmedNonEmptyString,
37 }),
38 commit: Schema.optional(
39 Schema.NullOr(Schema.Struct({ hash: Schema.optional(Schema.NullOr(Schema.String)) })),
40 ),
41});
42
43export const BitbucketPullRequestSchema = Schema.Struct({
44 id: PositiveInt,
45 title: TrimmedNonEmptyString,
46 state: Schema.optional(Schema.NullOr(Schema.String)),
47 draft: Schema.optional(Schema.Boolean),
48 updated_on: Schema.optional(Schema.OptionFromNullOr(Schema.DateTimeUtcFromString)),
49 links: Schema.Struct({
50 html: Schema.Struct({
51 href: TrimmedNonEmptyString,
52 }),
53 }),
54 source: BitbucketPullRequestBranchSchema,
55 destination: BitbucketPullRequestBranchSchema,
56});
57
58export const BitbucketPullRequestListSchema = Schema.Struct({
59 values: Schema.Array(BitbucketPullRequestSchema),
60 next: Schema.optional(TrimmedNonEmptyString),
61});
62
63function repositoryOwner(repository: Schema.Schema.Type<typeof BitbucketRepositoryRefSchema>) {
64 return (
65 trimOptionalString(repository.workspace?.slug) ??
66 (repository.full_name?.includes("/") ? (repository.full_name.split("/")[0] ?? null) : null)
67 );
68}
69
70function normalizeBitbucketPullRequestState(state: string | null | undefined) {
71 switch (state?.trim().toUpperCase()) {
72 case "MERGED":
73 return "merged" as const;
74 case "DECLINED":
75 case "SUPERSEDED":
76 return "closed" as const;
77 case "OPEN":
78 default:
79 return "open" as const;
80 }
81}
82
83export function normalizeBitbucketPullRequestRecord(
84 raw: Schema.Schema.Type<typeof BitbucketPullRequestSchema>,
85): NormalizedBitbucketPullRequestRecord {
86 const headRepositoryNameWithOwner = trimOptionalString(raw.source.repository?.full_name);
87 const baseRepositoryNameWithOwner = trimOptionalString(raw.destination.repository?.full_name);
88 const headSha = trimOptionalString(raw.source.commit?.hash);
89 const headRepositoryOwnerLogin = raw.source.repository
90 ? repositoryOwner(raw.source.repository)
91 : null;
92 const isCrossRepository =
93 headRepositoryNameWithOwner !== null &&
94 baseRepositoryNameWithOwner !== null &&
95 headRepositoryNameWithOwner !== baseRepositoryNameWithOwner;
96
97 return {
98 number: raw.id,
99 title: raw.title,
100 url: raw.links.html.href,
101 baseRefName: raw.destination.branch.name,
102 headRefName: raw.source.branch.name,
103 ...(headSha ? { headSha } : {}),
104 state: normalizeBitbucketPullRequestState(raw.state),
105 ...(raw.draft === true ? { isDraft: true } : {}),
106 updatedAt: raw.updated_on ?? Option.none(),
107 ...(isCrossRepository ? { isCrossRepository: true } : {}),
108 ...(headRepositoryNameWithOwner ? { headRepositoryNameWithOwner } : {}),
109 ...(headRepositoryOwnerLogin ? { headRepositoryOwnerLogin } : {}),
110 };
111}