1/**
2 * The `repos` group: repo CRUD, fork, import, and compaction
3 * (DESIGN.md §5). Every route is an Effect `HttpApiEndpoint`. Who may
4 * call it is decided by the middleware applied to its route layer.
5 */
6import * as HttpApiEndpoint from "effect/http-api/HttpApiEndpoint";
7import * as Schema from "effect/Schema";
8import * as HttpApiGroup from "effect/http-api/HttpApiGroup";
9import * as HttpApiSchema from "effect/http-api/HttpApiSchema";
10import {
11 ImportFailed,
12 OwnerName,
13 Paginated,
14 RefNotFound,
15 Repo,
16 RepoAlreadyExists,
17 RepoCreated,
18 RepoName,
19 RepoNotFound,
20 RepoNotReady,
21 RepoPath,
22 ValidationError,
23} from "./Schema.ts";
24
25/** Creates a repo owned by `owner`. */
26export const CreateRepo = HttpApiEndpoint.post("create", "/repos", {
27 payload: Schema.Struct({
28 owner: OwnerName,
29 name: RepoName,
30 /** Default branch short name. @default "main" */
31 defaultBranch: Schema.optional(Schema.String),
32 description: Schema.optional(Schema.String),
33 /**
34 * Anyone can read/clone without a credential when `true`.
35 * @default false
36 */
37 public: Schema.optional(Schema.Boolean),
38 /** @default false */
39 readOnly: Schema.optional(Schema.Boolean),
40 }),
41 success: RepoCreated,
42 error: [RepoAlreadyExists, ValidationError],
43});
44
45/** Reads one repo (poll `status` for async fork/import/delete progress). */
46export const GetRepo = HttpApiEndpoint.get("get", "/repos/:owner/:repo", {
47 params: RepoPath,
48 success: Repo,
49 error: [RepoNotFound],
50});
51
52/** Patches description / default branch / readOnly / public. */
53export const UpdateRepo = HttpApiEndpoint.patch(
54 "update",
55 "/repos/:owner/:repo",
56 {
57 params: RepoPath,
58 payload: Schema.Struct({
59 description: Schema.optional(Schema.NullOr(Schema.String)),
60 /** Must resolve to an existing branch. */
61 defaultBranch: Schema.optional(Schema.String),
62 readOnly: Schema.optional(Schema.Boolean),
63 /** Anyone can read/clone without a credential when `true`. */
64 public: Schema.optional(Schema.Boolean),
65 }),
66 success: Repo,
67 error: [RepoNotFound, RefNotFound],
68 },
69);
70
71/**
72 * Lists repos, optionally filtered by owner. Lists everything the
73 * Registry holds; `public: true` narrows it to public repositories.
74 */
75export const ListRepos = HttpApiEndpoint.get("list", "/repos", {
76 query: Schema.Struct({
77 owner: Schema.optional(OwnerName),
78 /** Only public repositories when `true`. */
79 public: Schema.optional(Schema.Boolean),
80 cursor: Schema.optional(Schema.String),
81 limit: Schema.optional(
82 Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100 })),
83 ),
84 }),
85 success: Paginated(Repo),
86});
87
88/**
89 * Deletes a repo. Async purge: responds 204 immediately, repo `status`
90 * flips to `'deleting'`, the name frees after the purge alarm completes
91 * (then 404).
92 */
93export const DeleteRepo = HttpApiEndpoint.delete(
94 "delete",
95 "/repos/:owner/:repo",
96 {
97 params: RepoPath,
98 success: HttpApiSchema.NoContent,
99 error: [RepoNotFound],
100 },
101);
102
103/**
104 * Forks a repo. The fork starts in `status: 'forking'`; poll
105 * `GET /repos/:owner/:repo` until `'ready'`.
106 */
107export const ForkRepo = HttpApiEndpoint.post(
108 "fork",
109 "/repos/:owner/:repo/fork",
110 {
111 params: RepoPath,
112 payload: Schema.Struct({
113 targetOwner: OwnerName,
114 targetName: RepoName,
115 }),
116 success: RepoCreated,
117 error: [RepoNotFound, RepoAlreadyExists, RepoNotReady],
118 },
119);
120
121/**
122 * Imports from an external smart-HTTP source. The repo starts in
123 * `status: 'importing'`; poll `GET /repos/:owner/:repo` until `'ready'`.
124 */
125export const ImportRepo = HttpApiEndpoint.post("import", "/repos/import", {
126 payload: Schema.Struct({
127 owner: OwnerName,
128 name: RepoName,
129 source: Schema.Struct({
130 /** Smart-HTTP URL of the source repository. */
131 url: Schema.String,
132 /** Restrict the import to a single ref. */
133 ref: Schema.optional(Schema.String),
134 /** Depth-limit the imported history. */
135 depth: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))),
136 }),
137 }),
138 success: RepoCreated,
139 error: [RepoAlreadyExists, ImportFailed],
140});
141
142/**
143 * Forces a compaction run now (`Maintain`): moves loose object bytes into
144 * an immutable pack. Normally armed automatically by size thresholds after
145 * a push — this is the operator/benchmark handle. Returns immediately; poll
146 * `GET /repos/:owner/:repo` and watch `objects.loose` fall to zero.
147 */
148export const CompactRepo = HttpApiEndpoint.post(
149 "compact",
150 "/repos/:owner/:repo/compact",
151 {
152 params: RepoPath,
153 success: HttpApiSchema.NoContent,
154 error: [RepoNotFound],
155 },
156);
157
158/** The `repos` group, mounted at `/api/v1`. */
159export class Repos extends HttpApiGroup.make("repos")
160 .add(
161 CreateRepo,
162 GetRepo,
163 UpdateRepo,
164 ListRepos,
165 DeleteRepo,
166 ForkRepo,
167 ImportRepo,
168 CompactRepo,
169 )
170 .prefix("/api/v1") {}