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 @@
1+import { Cause, DateTime, Exit, Option, Result, Schema } from "effect";
2+import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts";
3+import { decodeJsonResult, formatSchemaError } from "@t3tools/shared/schemaJson";
4+
5+export 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+
18+const 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+
32+const 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+
46+function trimOptionalString(value: string | null | undefined): string | null {
47+ const trimmed = value?.trim() ?? "";
48+ return trimmed.length > 0 ? trimmed : null;
49+}
50+
51+function 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+
64+function 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+
81+function ownerLoginFromPathWithNamespace(pathWithNamespace: string | null): string | null {
82+ const [owner] = pathWithNamespace?.split("/") ?? [];
83+ return trimOptionalString(owner);
84+}
85+
86+function 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+
113+const decodeGitLabMergeRequestList = decodeJsonResult(Schema.Array(Schema.Unknown));
114+const decodeGitLabMergeRequest = decodeJsonResult(GitLabMergeRequestSchema);
115+const decodeGitLabMergeRequestEntry = Schema.decodeUnknownExit(GitLabMergeRequestSchema);
116+
117+export const formatGitLabJsonDecodeError = formatSchemaError;
118+
119+export 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+
140+export 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+}