History

feat(scm): Gitlab (#2462)

Julius Marminge committed 6 months agov0.0.22-nightly.20260503.186

Commit 0ce7e561 parent 350d76e24 files · +1606 −185Browse files

Showing the changes to apps/server/src/sourceControl/gitLabMergeRequests.ts (1 of 24 files). Show all

Unified · Split

apps/server/src/sourceControl/gitLabMergeRequests.ts+148−0
@@ -0,0 +1,148 @@
1import { Cause, DateTime, Exit, Option, Result, Schema } from "effect";
2import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts";
3import { decodeJsonResult, formatSchemaError } from "@t3tools/shared/schemaJson";
4
5export interface NormalizedGitLabMergeRequestRecord {
6 readonly number: number;
7 readonly title: string;
8 readonly url: string;
9 readonly baseRefName: string;
10 readonly headRefName: string;
11 readonly state: "open" | "closed" | "merged";
12 readonly updatedAt: Option.Option<DateTime.Utc>;
13 readonly isCrossRepository?: boolean;
14 readonly headRepositoryNameWithOwner?: string | null;
15 readonly headRepositoryOwnerLogin?: string | null;
16}
17
18const GitLabProjectReferenceSchema = Schema.Struct({
19 path_with_namespace: Schema.optional(Schema.String),
20 pathWithNamespace: Schema.optional(Schema.String),
21 namespace: Schema.optional(
22 Schema.NullOr(
23 Schema.Struct({
24 path: Schema.optional(Schema.String),
25 full_path: Schema.optional(Schema.String),
26 fullPath: Schema.optional(Schema.String),
27 }),
28 ),
29 ),
30});
31
32const GitLabMergeRequestSchema = Schema.Struct({
33 iid: PositiveInt,
34 title: TrimmedNonEmptyString,
35 web_url: TrimmedNonEmptyString,
36 source_branch: TrimmedNonEmptyString,
37 target_branch: TrimmedNonEmptyString,
38 state: Schema.optional(Schema.NullOr(Schema.String)),
39 updated_at: Schema.optional(Schema.OptionFromNullOr(Schema.DateTimeUtcFromString)),
40 source_project_id: Schema.optional(Schema.NullOr(Schema.Number)),
41 target_project_id: Schema.optional(Schema.NullOr(Schema.Number)),
42 source_project: Schema.optional(Schema.NullOr(GitLabProjectReferenceSchema)),
43 target_project: Schema.optional(Schema.NullOr(GitLabProjectReferenceSchema)),
44});
45
46function trimOptionalString(value: string | null | undefined): string | null {
47 const trimmed = value?.trim() ?? "";
48 return trimmed.length > 0 ? trimmed : null;
49}
50
51function normalizeGitLabMergeRequestState(
52 state: string | null | undefined,
53): "open" | "closed" | "merged" {
54 const normalized = state?.trim().toLowerCase();
55 if (normalized === "merged") {
56 return "merged";
57 }
58 if (normalized === "closed") {
59 return "closed";
60 }
61 return "open";
62}
63
64function projectPathWithNamespace(
65 project: Schema.Schema.Type<typeof GitLabProjectReferenceSchema> | null | undefined,
66): string | null {
67 const explicit =
68 trimOptionalString(project?.path_with_namespace) ??
69 trimOptionalString(project?.pathWithNamespace);
70 if (explicit) {
71 return explicit;
72 }
73
74 const namespacePath =
75 trimOptionalString(project?.namespace?.full_path) ??
76 trimOptionalString(project?.namespace?.fullPath) ??
77 trimOptionalString(project?.namespace?.path);
78 return namespacePath;
79}
80
81function ownerLoginFromPathWithNamespace(pathWithNamespace: string | null): string | null {
82 const [owner] = pathWithNamespace?.split("/") ?? [];
83 return trimOptionalString(owner);
84}
85
86function normalizeGitLabMergeRequestRecord(
87 raw: Schema.Schema.Type<typeof GitLabMergeRequestSchema>,
88): NormalizedGitLabMergeRequestRecord {
89 const sourceProjectPath = projectPathWithNamespace(raw.source_project);
90 const targetProjectPath = projectPathWithNamespace(raw.target_project);
91 const isCrossRepository =
92 typeof raw.source_project_id === "number" && typeof raw.target_project_id === "number"
93 ? raw.source_project_id !== raw.target_project_id
94 : sourceProjectPath !== null && targetProjectPath !== null
95 ? sourceProjectPath.toLowerCase() !== targetProjectPath.toLowerCase()
96 : undefined;
97 const headRepositoryOwnerLogin = ownerLoginFromPathWithNamespace(sourceProjectPath);
98
99 return {
100 number: raw.iid,
101 title: raw.title,
102 url: raw.web_url,
103 baseRefName: raw.target_branch,
104 headRefName: raw.source_branch,
105 state: normalizeGitLabMergeRequestState(raw.state),
106 updatedAt: raw.updated_at ?? Option.none(),
107 ...(typeof isCrossRepository === "boolean" ? { isCrossRepository } : {}),
108 ...(sourceProjectPath ? { headRepositoryNameWithOwner: sourceProjectPath } : {}),
109 ...(headRepositoryOwnerLogin ? { headRepositoryOwnerLogin } : {}),
110 };
111}
112
113const decodeGitLabMergeRequestList = decodeJsonResult(Schema.Array(Schema.Unknown));
114const decodeGitLabMergeRequest = decodeJsonResult(GitLabMergeRequestSchema);
115const decodeGitLabMergeRequestEntry = Schema.decodeUnknownExit(GitLabMergeRequestSchema);
116
117export const formatGitLabJsonDecodeError = formatSchemaError;
118
119export function decodeGitLabMergeRequestListJson(
120 raw: string,
121): Result.Result<
122 ReadonlyArray<NormalizedGitLabMergeRequestRecord>,
123 Cause.Cause<Schema.SchemaError>
124> {
125 const result = decodeGitLabMergeRequestList(raw);
126 if (Result.isSuccess(result)) {
127 const mergeRequests: NormalizedGitLabMergeRequestRecord[] = [];
128 for (const entry of result.success) {
129 const decodedEntry = decodeGitLabMergeRequestEntry(entry);
130 if (Exit.isFailure(decodedEntry)) {
131 continue;
132 }
133 mergeRequests.push(normalizeGitLabMergeRequestRecord(decodedEntry.value));
134 }
135 return Result.succeed(mergeRequests);
136 }
137 return Result.fail(result.failure);
138}
139
140export function decodeGitLabMergeRequestJson(
141 raw: string,
142): Result.Result<NormalizedGitLabMergeRequestRecord, Cause.Cause<Schema.SchemaError>> {
143 const result = decodeGitLabMergeRequest(raw);
144 if (Result.isSuccess(result)) {
145 return Result.succeed(normalizeGitLabMergeRequestRecord(result.success));
146 }
147 return Result.fail(result.failure);
148}