Skip to content
1,507 linesCodeBlameRaw
1//! The repos service: repository metadata, contents, forks and git access.
2//!
3//! Each `*Args` struct is the argument of the method of the same name,
4//! served at `POST /rpc/<method>`.
5
6use serde::{Deserialize, Serialize};
7
8use crate::{User, Viewer};
9
10#[derive(Clone, Debug, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct Repo {
13 pub id: String,
14 /// The slug of the workspace that owns it: the first URL segment.
15 pub namespace: String,
16 pub name: String,
17 pub description: Option<String>,
18 pub is_private: bool,
19 pub owner_id: String,
20 pub default_branch: String,
21 /// Set when this repo is a pull request's working copy of another repo.
22 pub fork_of: Option<String>,
23 /// Whether the default branch is protected: it changes only by merging
24 /// a pull request, and pushes to it are refused.
25 #[serde(default)]
26 pub protected: bool,
27 /// RFC 3339.
28 pub created_at: String,
29 /// Words that say what it is about, for search and Explore: lowercase
30 /// letters, digits and hyphens. See [`clean_topics`].
31 #[serde(default)]
32 pub topics: Vec<String>,
33 /// Its home page, an http(s) address, shown beside its description.
34 /// See [`clean_website`].
35 #[serde(default)]
36 pub website: Option<String>,
37 /// RFC 3339: when it was archived, made read-only. Null when it is not.
38 #[serde(default)]
39 pub archived_at: Option<String>,
40 /// Set when it mirrors a remote that leads (see [`crate::mirrors`]).
41 /// Unless g1t has taken over, it is read-only.
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub mirror: Option<crate::mirrors::RepoMirror>,
44}
45
46impl Repo {
47 pub fn archived(&self) -> bool {
48 self.archived_at.is_some()
49 }
50
51 /// Whether it is a mirror that does not take writes now.
52 pub fn mirror_read_only(&self) -> bool {
53 self.mirror.as_ref().is_some_and(|mirror| !mirror.writable())
54 }
55
56 /// Why it takes no pushes, merges, issues or agents now: archived, or
57 /// a mirror standing by. `None` when it takes them.
58 pub fn read_only_reason(&self) -> Option<String> {
59 if self.archived() {
60 return Some(archived_message(&self.namespace, &self.name));
61 }
62 self.mirror
63 .as_ref()
64 .filter(|mirror| !mirror.writable())
65 .map(|mirror| crate::mirrors::mirror_message(&self.namespace, &self.name, mirror))
66 }
67}
68
69/// `storage_options` (no arguments, `{}`): what a workspace may choose
70/// about where its repositories are kept. `eu_available`: an EU namespace
71/// is configured and takes new repositories, so a workspace may keep its
72/// data in the EU (`set_workspace_residency` on identity). Returns
73/// `StorageOptions`.
74#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
75#[serde(rename_all = "camelCase")]
76pub struct StorageOptions {
77 pub eu_available: bool,
78}
79
80/// How long a deleted repository can be restored before it is purged.
81pub const RESTORE_DAYS: u64 = 30;
82
83/// A deleted repository, as its workspace's Recently deleted list shows
84/// it: restorable until `purge_after`.
85#[derive(Clone, Debug, Serialize, Deserialize)]
86#[serde(rename_all = "camelCase")]
87pub struct DeletedRepo {
88 pub id: String,
89 pub namespace: String,
90 pub name: String,
91 pub description: Option<String>,
92 pub is_private: bool,
93 /// RFC 3339.
94 pub deleted_at: String,
95 /// The username of who deleted it.
96 pub deleted_by: String,
97 /// RFC 3339: when it is purged, unless restored first.
98 pub purge_after: String,
99}
100
101/// The longest website address a repository keeps.
102pub const MAX_WEBSITE_CHARS: usize = 255;
103
104/// A website as it is kept: an http(s) address, `https://` added when no
105/// scheme is given; empty clears it. Anything else is refused.
106pub fn clean_website(text: &str) -> Result<Option<String>, String> {
107 let text = text.trim();
108 if text.is_empty() {
109 return Ok(None);
110 }
111 let url = if text.starts_with("https://") || text.starts_with("http://") {
112 text.to_owned()
113 } else if text.contains("://") {
114 return Err("A website is an http or https address.".into());
115 } else {
116 format!("https://{text}")
117 };
118 let host = url
119 .split("://")
120 .nth(1)
121 .unwrap_or("")
122 .split(['/', '?', '#'])
123 .next()
124 .unwrap_or("");
125 if url.chars().count() > MAX_WEBSITE_CHARS
126 || host.is_empty()
127 || !host.contains('.')
128 || url.chars().any(char::is_whitespace)
129 {
130 return Err("That is not a website address, such as https://example.com.".into());
131 }
132 Ok(Some(url))
133}
134
135/// Whether `name` can be a branch people name: what `git check-ref-format
136/// --branch` accepts, less the names g1t keeps for itself
137/// ([`G1T_BRANCH_PREFIX`]).
138pub fn is_valid_branch_name(name: &str) -> bool {
139 !name.is_empty()
140 && name.len() <= 200
141 && !name.starts_with('-')
142 && !name.starts_with('/')
143 && !name.ends_with('/')
144 && !name.ends_with('.')
145 && !name.ends_with(".lock")
146 && !name.contains("..")
147 && !name.contains("//")
148 && !name.contains("@{")
149 && name != "@"
150 && !name.starts_with(G1T_BRANCH_PREFIX)
151 && !name.split('/').any(|part| part.starts_with('.'))
152 && name
153 .chars()
154 .all(|c| !c.is_control() && !matches!(c, ' ' | '~' | '^' | ':' | '?' | '*' | '[' | '\\'))
155}
156
157/// The most topics a repository has.
158pub const MAX_TOPICS: usize = 20;
159/// The longest topic.
160pub const MAX_TOPIC_CHARS: usize = 35;
161
162/// Topics as they are kept: lowercase, spaces and underscores made
163/// hyphens, each of letters, digits and hyphens, starting with a letter or
164/// digit, without repeats, at most [`MAX_TOPICS`]. Anything else is the
165/// first topic that could not be read.
166pub fn clean_topics(topics: &[String]) -> Result<Vec<String>, String> {
167 let mut kept: Vec<String> = Vec::new();
168 for topic in topics {
169 let topic: String = topic
170 .trim()
171 .to_lowercase()
172 .chars()
173 .map(|c| if c == ' ' || c == '_' { '-' } else { c })
174 .collect();
175 if topic.is_empty() {
176 continue;
177 }
178 let valid = topic.chars().count() <= MAX_TOPIC_CHARS
179 && topic.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
180 && topic.chars().next().is_some_and(|c| c.is_ascii_alphanumeric());
181 if !valid {
182 return Err(format!(
183 "\"{topic}\" is not a topic: use letters, digits and hyphens, at most {MAX_TOPIC_CHARS} characters."
184 ));
185 }
186 if !kept.contains(&topic) {
187 kept.push(topic);
188 }
189 }
190 if kept.len() > MAX_TOPICS {
191 return Err(format!("A repository has at most {MAX_TOPICS} topics."));
192 }
193 Ok(kept)
194}
195
196#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
197pub struct RepoPath {
198 pub namespace: String,
199 pub name: String,
200}
201
202#[derive(Clone, Debug, Serialize, Deserialize)]
203pub struct Signature {
204 pub name: String,
205 pub email: String,
206}
207
208#[derive(Clone, Debug, Serialize, Deserialize)]
209#[serde(rename_all = "camelCase")]
210pub struct Commit {
211 pub hash: String,
212 pub tree_hash: String,
213 pub message: String,
214 pub author: Signature,
215 pub parents: Vec<String>,
216 /// RFC 3339.
217 pub authored_at: String,
218}
219
220#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
221#[serde(rename_all = "lowercase")]
222pub enum EntryKind {
223 Tree,
224 Blob,
225 Symlink,
226 Gitlink,
227 Exec,
228}
229
230#[derive(Clone, Debug, Serialize, Deserialize)]
231pub struct TreeEntry {
232 pub name: String,
233 pub hash: String,
234 pub kind: EntryKind,
235}
236
237#[derive(Clone, Debug, Serialize, Deserialize)]
238pub struct Readme {
239 pub name: String,
240 /// Null when the file is binary or too large to show.
241 pub text: Option<String>,
242}
243
244#[derive(Clone, Debug, Serialize, Deserialize)]
245pub struct TreeView {
246 pub repo: Repo,
247 #[serde(rename = "ref")]
248 pub git_ref: String,
249 pub path: String,
250 /// Null when the repo has no commits yet.
251 pub head: Option<Commit>,
252 pub entries: Vec<TreeEntry>,
253 pub readme: Option<Readme>,
254}
255
256#[derive(Clone, Debug, Serialize, Deserialize)]
257pub struct BlobView {
258 pub repo: Repo,
259 #[serde(rename = "ref")]
260 pub git_ref: String,
261 pub path: String,
262 pub size: u64,
263 /// Null when the file is binary or too large to show.
264 pub text: Option<String>,
265}
266
267/// A git remote and a short-lived credential for it.
268#[derive(Clone, Debug, Serialize, Deserialize)]
269pub struct GitAccess {
270 pub remote: String,
271 pub token: String,
272}
273
274#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
275pub enum GitService {
276 #[serde(rename = "git-upload-pack")]
277 UploadPack,
278 #[serde(rename = "git-receive-pack")]
279 ReceivePack,
280}
281
282/// The result of landing a pull request.
283#[derive(Clone, Debug, Serialize, Deserialize)]
284pub struct Landed {
285 /// The commit the branch points to now.
286 pub commit: String,
287 /// The commit it pointed to before, if it had one. Comparing against
288 /// this shows what the pull request changed.
289 pub previous: Option<String>,
290}
291
292/// `get`. Returns `Outcome<Repo>`.
293#[derive(Debug, Serialize, Deserialize)]
294pub struct GetArgs {
295 pub path: RepoPath,
296 pub viewer: Viewer,
297}
298
299/// `path_by_id`: where a repository is, whoever may see it. For g1t's own
300/// services, which hold a repository's id from an event and act for its
301/// workspace; nothing outside reaches it. Returns `Option<RepoPath>`, null
302/// for a fork or an unknown id.
303#[derive(Debug, Serialize, Deserialize)]
304pub struct PathByIdArgs {
305 pub id: String,
306}
307
308/// `get_by_id`. Returns `Outcome<Repo>`.
309#[derive(Debug, Serialize, Deserialize)]
310pub struct GetByIdArgs {
311 pub id: String,
312 pub viewer: Viewer,
313}
314
315/// `list`: repos the viewer may see, newest first. Returns `Vec<Repo>`.
316#[derive(Debug, Default, Serialize, Deserialize)]
317#[serde(rename_all = "camelCase")]
318pub struct ListArgs {
319 pub viewer: Viewer,
320 #[serde(default)]
321 pub query: Option<String>,
322 /// Only repos in this workspace.
323 #[serde(default)]
324 pub namespace: Option<String>,
325 /// Only repos in workspaces the viewer belongs to.
326 #[serde(default)]
327 pub member_only: bool,
328}
329
330/// `create`. Returns `Outcome<Repo>`.
331#[derive(Debug, Serialize, Deserialize)]
332#[serde(rename_all = "camelCase")]
333pub struct CreateArgs {
334 /// Who is creating it; they must belong to the workspace.
335 pub owner: User,
336 /// The workspace it is created in.
337 pub namespace: String,
338 pub name: String,
339 #[serde(default)]
340 pub description: Option<String>,
341 #[serde(default)]
342 pub is_private: bool,
343 /// The https address of a public git repository to copy the default
344 /// branch of, such as `https://github.com/owner/repo`.
345 #[serde(default)]
346 pub import_url: Option<String>,
347 /// With `import_url`: a GitHub installation access token that opens it,
348 /// for a private repository. Every branch and tag is then copied, not
349 /// only the default branch. Set only by the integrations service.
350 #[serde(default, skip_serializing_if = "Option::is_none")]
351 pub import_token: Option<String>,
352 /// Set by the integrations service for a mirror: it is read-only from
353 /// the start. See [`crate::mirrors`].
354 #[serde(default, skip_serializing_if = "Option::is_none")]
355 pub mirror: Option<crate::mirrors::RepoMirror>,
356}
357
358/// `mirror`: makes a repository's branches and tags match another git
359/// host's, or pushes its own out to one. Services only. Returns
360/// `Outcome<Mirrored>`.
361#[derive(Debug, Serialize, Deserialize)]
362#[serde(rename_all = "camelCase")]
363pub struct MirrorArgs {
364 pub repo_id: String,
365 /// The other host's https address, such as
366 /// `https://github.com/owner/repo.git`.
367 pub url: String,
368 /// A token for it, such as a GitHub installation access token. Opaque:
369 /// any length.
370 pub token: String,
371 /// The user the token is sent as, by basic authentication.
372 /// `x-access-token` (GitHub's) when absent.
373 #[serde(default, skip_serializing_if = "Option::is_none")]
374 pub username: Option<String>,
375 pub direction: MirrorDirection,
376}
377
378#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
379#[serde(rename_all = "snake_case")]
380pub enum MirrorDirection {
381 /// The repository on g1t follows the other host: its refs are moved,
382 /// and removed, to match.
383 Pull,
384 /// The other host follows g1t: refs g1t has are pushed there; refs only
385 /// the other host has are left alone.
386 Push,
387}
388
389/// What a `mirror` changed.
390#[derive(Clone, Debug, Default, Serialize, Deserialize)]
391#[serde(rename_all = "camelCase")]
392pub struct Mirrored {
393 /// Full ref names created or moved.
394 pub updated: Vec<String>,
395 pub deleted: Vec<String>,
396 /// Refs a pull moved somewhere their old commit is not part of (a
397 /// force-push on the remote), each as the `refs/g1t/replaced/...` ref
398 /// that keeps the old commit.
399 #[serde(default)]
400 pub replaced: Vec<String>,
401}
402
403/// `mirror_refs`: a repository's branches and tags, and another host's (only
404/// the repository's when `url` is empty).
405/// Services only. Returns `Outcome<MirrorRefs>`; when the other host does
406/// not answer, `theirs` is absent and `unreachable` says why. It fails
407/// when the other host answers and refuses.
408#[derive(Debug, Serialize, Deserialize)]
409#[serde(rename_all = "camelCase")]
410pub struct MirrorRefsArgs {
411 pub repo_id: String,
412 pub url: String,
413 pub token: String,
414 #[serde(default, skip_serializing_if = "Option::is_none")]
415 pub username: Option<String>,
416}
417
418#[derive(Clone, Debug, Default, Serialize, Deserialize)]
419#[serde(rename_all = "camelCase")]
420pub struct MirrorRefs {
421 /// Full ref name to commit (or tag) id, on g1t.
422 pub ours: std::collections::BTreeMap<String, String>,
423 /// The same, on the other host; absent when it did not answer.
424 pub theirs: Option<std::collections::BTreeMap<String, String>>,
425 /// Why the other host's refs are absent.
426 #[serde(default)]
427 pub unreachable: Option<String>,
428}
429
430/// One ref moved by `mirror_apply`, from one side to the other.
431#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
432#[serde(rename_all = "camelCase")]
433pub struct RefMove {
434 pub direction: MirrorDirection,
435 /// The ref on the side it comes from.
436 #[serde(rename = "ref")]
437 pub git_ref: String,
438 /// The ref it is written to; the same name when absent.
439 #[serde(default, skip_serializing_if = "Option::is_none")]
440 pub to: Option<String>,
441 /// What the target must hold now for the move to happen; absent when it
442 /// must not exist.
443 #[serde(default)]
444 pub old: Option<String>,
445 /// What it is moved to; absent to delete it.
446 #[serde(default)]
447 pub new: Option<String>,
448}
449
450/// `mirror_apply`: moves the given refs, each only if its target still
451/// holds `old`. Pulled refs that lose their old commit keep it under
452/// `refs/g1t/replaced/`. Services only. Returns `Outcome<MirrorApplied>`.
453#[derive(Debug, Serialize, Deserialize)]
454#[serde(rename_all = "camelCase")]
455pub struct MirrorApplyArgs {
456 pub repo_id: String,
457 pub url: String,
458 pub token: String,
459 #[serde(default, skip_serializing_if = "Option::is_none")]
460 pub username: Option<String>,
461 pub moves: Vec<RefMove>,
462}
463
464#[derive(Clone, Debug, Default, Serialize, Deserialize)]
465#[serde(rename_all = "camelCase")]
466pub struct MirrorApplied {
467 pub moved: Vec<RefMoved>,
468}
469
470#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
471#[serde(rename_all = "camelCase")]
472pub struct RefMoved {
473 #[serde(rename = "ref")]
474 pub git_ref: String,
475 pub direction: MirrorDirection,
476 /// Why it did not move; absent when it did.
477 #[serde(default)]
478 pub problem: Option<String>,
479 /// The `refs/g1t/replaced/...` ref that keeps what it pointed at.
480 #[serde(default)]
481 pub replaced: Option<String>,
482}
483
484/// `set_mirror`: records a repository's [`crate::mirrors::RepoMirror`], or
485/// clears it. Services only (integrations). Returns `Outcome<Repo>`.
486#[derive(Debug, Serialize, Deserialize)]
487#[serde(rename_all = "camelCase")]
488pub struct SetMirrorArgs {
489 pub repo_id: String,
490 pub mirror: Option<crate::mirrors::RepoMirror>,
491}
492
493/// `update`: changes whichever of a repository's details are given.
494/// Members of its workspace only. Returns `Outcome<Repo>`.
495#[derive(Debug, Serialize, Deserialize)]
496#[serde(rename_all = "camelCase")]
497pub struct UpdateArgs {
498 pub actor: User,
499 pub path: RepoPath,
500 /// An empty description clears it.
501 #[serde(default)]
502 pub description: Option<String>,
503 #[serde(default)]
504 pub is_private: Option<bool>,
505 #[serde(default)]
506 pub protected: Option<bool>,
507 /// Replaces its topics; an empty list clears them.
508 #[serde(default)]
509 pub topics: Option<Vec<String>>,
510 /// Its home page; an empty string clears it.
511 #[serde(default)]
512 pub website: Option<String>,
513 /// Where the request came in, for the audit log; g1t.sh when absent.
514 #[serde(default)]
515 pub surface: Option<crate::audit::Surface>,
516}
517
518/// `tree`. Returns `Outcome<TreeView>`.
519#[derive(Debug, Serialize, Deserialize)]
520#[serde(rename_all = "camelCase")]
521pub struct TreeArgs {
522 pub path: RepoPath,
523 pub viewer: Viewer,
524 /// The default branch when absent.
525 #[serde(default, rename = "ref")]
526 pub git_ref: Option<String>,
527 #[serde(default)]
528 pub tree_path: String,
529}
530
531/// `blob`. Returns `Outcome<BlobView>`.
532#[derive(Debug, Serialize, Deserialize)]
533#[serde(rename_all = "camelCase")]
534pub struct BlobArgs {
535 pub path: RepoPath,
536 pub viewer: Viewer,
537 #[serde(rename = "ref")]
538 pub git_ref: String,
539 pub file_path: String,
540}
541
542/// `log`. Returns `Outcome<Vec<Commit>>`.
543#[derive(Debug, Serialize, Deserialize)]
544pub struct LogArgs {
545 pub path: RepoPath,
546 pub viewer: Viewer,
547 #[serde(default, rename = "ref")]
548 pub git_ref: Option<String>,
549 pub limit: u32,
550}
551
552/// `fork_for_pull`: a copy-on-write copy of the source repo, hidden from
553/// listings, for one pull request to be made in. Returns `Outcome<Repo>`.
554#[derive(Debug, Serialize, Deserialize)]
555#[serde(rename_all = "camelCase")]
556pub struct ForkArgs {
557 pub source_id: String,
558 pub pull_id: String,
559 pub actor: User,
560}
561
562/// `git_access`: authorizes a git operation and says where to send it.
563/// Pushing to a repo that does not exist creates it in the pusher's own
564/// namespace. Returns `Outcome<GitAccess>`.
565#[derive(Debug, Serialize, Deserialize)]
566pub struct GitAccessArgs {
567 pub path: RepoPath,
568 pub viewer: Viewer,
569 pub service: GitService,
570}
571
572/// `land`: moves the branch a pull request merges into (the repository's
573/// default branch unless `target_branch` names another) to the head of
574/// its source. Refused with `conflict` when the source is behind, since
575/// that would discard commits. Returns `Outcome<Landed>`.
576#[derive(Debug, Serialize, Deserialize)]
577#[serde(rename_all = "camelCase")]
578pub struct LandArgs {
579 /// The repository holding the commits: a pull request's fork, or the
580 /// target itself when landing one of its own branches.
581 pub source_id: String,
582 /// The branch of the source to land. Required when the source is the
583 /// target; a fork lands its default branch.
584 #[serde(default)]
585 pub branch: Option<String>,
586 pub actor: User,
587 /// The branch of the target to land on; its default branch when absent.
588 #[serde(default)]
589 pub target_branch: Option<String>,
590}
591
592#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
593#[serde(rename_all = "lowercase")]
594pub enum FileStatus {
595 Added,
596 Modified,
597 Deleted,
598}
599
600#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
601#[serde(rename_all = "lowercase")]
602pub enum LineKind {
603 /// Unchanged, shown for context.
604 Context,
605 Add,
606 Delete,
607}
608
609#[derive(Clone, Debug, Serialize, Deserialize)]
610pub struct DiffLine {
611 pub kind: LineKind,
612 /// Line number in the old file; absent for added lines.
613 pub old: Option<u32>,
614 /// Line number in the new file; absent for deleted lines.
615 pub new: Option<u32>,
616 pub text: String,
617}
618
619/// A run of changed lines with their surrounding context.
620#[derive(Clone, Debug, Serialize, Deserialize)]
621pub struct Hunk {
622 pub lines: Vec<DiffLine>,
623}
624
625#[derive(Clone, Debug, Serialize, Deserialize)]
626pub struct FileDiff {
627 pub path: String,
628 pub status: FileStatus,
629 pub additions: u32,
630 pub deletions: u32,
631 /// True when the file is binary or too large, so no lines are shown.
632 pub binary: bool,
633 pub hunks: Vec<Hunk>,
634}
635
636/// What changed between two commits.
637#[derive(Clone, Debug, Serialize, Deserialize)]
638pub struct Comparison {
639 /// Null when the head has no earlier commit to compare against.
640 pub base: Option<String>,
641 pub head: String,
642 pub files: Vec<FileDiff>,
643 /// True when the change was too large to return in full.
644 pub truncated: bool,
645}
646
647/// `compare`: what `head` changes relative to `base`.
648///
649/// `head` is a branch or a commit, and defaults to the default branch.
650/// With no `base`, a fork is compared against the point where it and the
651/// repository it came from last agreed; a branch against the point where it
652/// left the default branch; and the default branch against its head's
653/// parent. Returns `Outcome<Comparison>`.
654#[derive(Debug, Serialize, Deserialize)]
655#[serde(rename_all = "camelCase")]
656pub struct CompareArgs {
657 pub repo_id: String,
658 pub viewer: Viewer,
659 #[serde(default)]
660 pub base: Option<String>,
661 #[serde(default)]
662 pub head: Option<String>,
663 /// With no `base`: the branch whose shared point with the head it is
664 /// compared from, instead of the default branch. A pull request into
665 /// another branch is compared this way.
666 #[serde(default)]
667 pub base_branch: Option<String>,
668}
669
670/// Lines `start` to `end` of a file, inclusive and counted from 1, last
671/// changed by `commit`.
672#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
673pub struct BlameRange {
674 pub start: u32,
675 pub end: u32,
676 pub commit: String,
677}
678
679/// Who last changed each line of a file.
680#[derive(Clone, Debug, Serialize, Deserialize)]
681pub struct Blame {
682 /// The commit the file was read at.
683 pub head: String,
684 /// Every line, in order, in runs that share a commit.
685 pub ranges: Vec<BlameRange>,
686 /// The commits the ranges name, each once.
687 pub commits: Vec<Commit>,
688 /// True when the history was too long to read in full, so the oldest
689 /// lines are given to the oldest commit read.
690 pub partial: bool,
691}
692
693/// `blame`: who last changed each line of `path` as of `ref` (the default
694/// branch if absent). Returns `Outcome<Blame>`; not found when the file is
695/// missing or is not text.
696#[derive(Debug, Serialize, Deserialize)]
697pub struct BlameArgs {
698 pub path: RepoPath,
699 pub viewer: Viewer,
700 #[serde(default, rename = "ref")]
701 pub git_ref: Option<String>,
702 #[serde(rename = "filePath")]
703 pub file_path: String,
704}
705
706/// A branch and the commit it points to.
707#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
708pub struct Branch {
709 pub name: String,
710 pub hash: String,
711}
712
713/// `last_commits`: which commit last changed each entry of a directory at
714/// `ref` (the default branch when absent). Returns `Outcome<LastCommits>`.
715#[derive(Debug, Serialize, Deserialize)]
716#[serde(rename_all = "camelCase")]
717pub struct LastCommitsArgs {
718 pub path: RepoPath,
719 pub viewer: Viewer,
720 #[serde(default, rename = "ref")]
721 pub git_ref: Option<String>,
722 #[serde(default)]
723 pub tree_path: String,
724 /// Answer within this many milliseconds with what was found; absent,
725 /// within 20 seconds. A walk that stops short keeps its progress, and
726 /// the next call goes on from it.
727 #[serde(default)]
728 pub budget_ms: Option<u64>,
729}
730
731/// An entry of a directory and the commit that last changed it.
732#[derive(Clone, Debug, Serialize, Deserialize)]
733pub struct LastCommit {
734 pub name: String,
735 pub commit: Commit,
736}
737
738/// The entries' last commits. `complete` is false when the walk stopped
739/// (or the history ran out) before every entry was placed; those entries
740/// are left out, and a later call goes on placing them.
741#[derive(Clone, Debug, Serialize, Deserialize)]
742pub struct LastCommits {
743 pub entries: Vec<LastCommit>,
744 pub complete: bool,
745}
746
747/// `branch_drift`: how far each of `heads` (branch head commits) has moved
748/// from `base` (the default branch's head commit), and each one's head
749/// commit, in one call. Every answer is kept by the pair of hashes: neither
750/// history can change, so neither can it. Returns `Outcome<BranchDrifts>`.
751#[derive(Debug, Serialize, Deserialize)]
752#[serde(rename_all = "camelCase")]
753pub struct BranchDriftArgs {
754 pub path: RepoPath,
755 pub viewer: Viewer,
756 pub base: String,
757 pub heads: Vec<String>,
758}
759
760/// Commits a branch has that the default branch does not (`ahead`), and
761/// the other way round (`behind`), as `git rev-list --left-right --count`.
762#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
763pub struct Drift {
764 pub ahead: u32,
765 pub behind: u32,
766}
767
768/// One branch head's commit and drift. `drift` is absent when the two
769/// histories do not meet within what is read (or could not be read).
770#[derive(Clone, Debug, Serialize, Deserialize)]
771pub struct BranchDrift {
772 pub head: String,
773 pub commit: Option<Commit>,
774 pub drift: Option<Drift>,
775}
776
777/// `base`'s own commit, and each head's answer in the order asked.
778#[derive(Clone, Debug, Serialize, Deserialize)]
779pub struct BranchDrifts {
780 pub base: Option<Commit>,
781 pub branches: Vec<BranchDrift>,
782}
783
784/// `tags`: the repository's tags, newest commit first, each with the
785/// commit it names. Returns `Outcome<Vec<Tag>>`.
786#[derive(Debug, Serialize, Deserialize)]
787pub struct TagsArgs {
788 pub path: RepoPath,
789 pub viewer: Viewer,
790}
791
792/// A tag, and its commit when it could be read.
793#[derive(Clone, Debug, Serialize, Deserialize)]
794pub struct Tag {
795 pub name: String,
796 pub commit: Option<Commit>,
797}
798
799/// `branches`: the repository's branches, default branch first.
800/// Returns `Outcome<Vec<Branch>>`.
801#[derive(Debug, Serialize, Deserialize)]
802pub struct BranchesArgs {
803 pub path: RepoPath,
804 pub viewer: Viewer,
805}
806
807/// `behind`: whether the default branch of the repository a pull request
808/// would merge into has commits its source does not. For services that
809/// have already decided the caller may see the pull request; it reveals
810/// one bit. Returns `bool`.
811#[derive(Debug, Serialize, Deserialize)]
812#[serde(rename_all = "camelCase")]
813pub struct BehindArgs {
814 /// The pull request's fork, or the repository itself for a branch.
815 pub source_id: String,
816 /// The branch of the source. A fork is compared on its default branch.
817 #[serde(default)]
818 pub branch: Option<String>,
819 /// The branch of the target it would merge into; the default branch
820 /// when absent.
821 #[serde(default)]
822 pub target_branch: Option<String>,
823}
824
825/// `divergence`: how a pull request's source and the default branch it
826/// would merge into have moved apart since they last agreed: the files each
827/// side changed. Takes `BehindArgs`. For services that have already decided
828/// the caller may see the pull request; it reveals paths, not contents.
829/// Returns `Option<Divergence>`, null when either side has no commits.
830#[derive(Clone, Debug, Default, Serialize, Deserialize)]
831#[serde(rename_all = "camelCase")]
832pub struct Divergence {
833 /// The source's commit.
834 pub head: String,
835 /// The default branch's commit.
836 pub base: String,
837 /// Where they last agreed, if that could be found.
838 pub merge_base: Option<String>,
839 /// Whether the default branch has commits the source does not.
840 pub behind: bool,
841 /// The files the source changed since the merge base.
842 pub ours: Vec<String>,
843 /// The files the default branch changed since the merge base. Empty
844 /// when it is not behind.
845 pub theirs: Vec<String>,
846 /// Whether either list was cut short.
847 pub truncated: bool,
848}
849
850/// `update_pull_branch`: brings a pull request's source up to date with the
851/// default branch it would merge into, without a sandbox, when that can be
852/// done safely: merges the default branch's head into the source's head and
853/// pushes the merge commit to the source's branch, as `actor`, only if the
854/// branch has not moved meanwhile. It applies only when the two sides
855/// changed different files since they last agreed; otherwise the answer is
856/// [`PullBranchUpdate::NeedsAgent`] and nothing is pushed. Refused unless
857/// `actor` may push to the source. Returns `Outcome<PullBranchUpdate>`.
858#[derive(Debug, Serialize, Deserialize)]
859#[serde(rename_all = "camelCase")]
860pub struct UpdatePullBranchArgs {
861 /// The pull request's fork, or the repository itself for a branch.
862 pub source_id: String,
863 /// The branch of the source. A fork is updated on its default branch.
864 #[serde(default)]
865 pub branch: Option<String>,
866 /// The pull request's number, to name it in the merge commit's message
867 /// when its branch has the same name as the default branch.
868 pub number: u32,
869 /// Who asked: the merge commit's author and committer, and the pusher.
870 pub actor: User,
871 /// The branch of the target to merge in; its default branch when absent.
872 #[serde(default)]
873 pub target_branch: Option<String>,
874}
875
876/// Why an update has to be left to a sandbox.
877#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
878#[serde(rename_all = "snake_case")]
879pub enum NeedsAgentReason {
880 /// Both sides changed some of the same files; merging them needs a
881 /// real merge, which may or may not conflict.
882 Overlap,
883 /// Merging is known to conflict.
884 Conflicting,
885 /// The update could not be worked out here, such as when the two sides
886 /// share no history g1t can see, or the change is too large to list.
887 Unsupported,
888}
889
890/// What came of `update_pull_branch` (or the work service's `catch_up_pull`).
891#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
892#[serde(tag = "outcome", rename_all = "snake_case")]
893pub enum PullBranchUpdate {
894 /// The merge commit was pushed: the branch moved from `previous` to
895 /// `commit`.
896 Updated { commit: String, previous: String },
897 /// The source already holds the default branch's head.
898 UpToDate { commit: String },
899 /// Nothing was pushed; a sandbox has to merge it. `paths` are the
900 /// files both sides changed, or that conflict, when known.
901 NeedsAgent {
902 reason: NeedsAgentReason,
903 detail: String,
904 paths: Vec<String>,
905 },
906}
907
908/// `head`: the commit a branch points to, or null. For services reacting
909/// to a push, which have no viewer; it reveals nothing but a commit hash.
910/// Returns `Option<String>`.
911#[derive(Debug, Serialize, Deserialize)]
912#[serde(rename_all = "camelCase")]
913pub struct HeadArgs {
914 pub repo_id: String,
915 /// Empty for the repository's default branch.
916 pub branch: String,
917}
918
919/// Where g1t keeps branches of its own in a repository, such as the merge
920/// queue's tested states. Only these can be removed with `delete_branch`.
921pub const G1T_BRANCH_PREFIX: &str = "g1t-";
922
923/// `delete_branch`: removes a branch g1t made for itself once it is done
924/// with it, never one of people's: the name must start with
925/// [`G1T_BRANCH_PREFIX`], or `head` must name the commit it points to (a
926/// dependency update's branch, which g1t pushed and whose pull request it
927/// closed). Never the default branch. For services, which have no viewer.
928/// Returns `Outcome<bool>`: whether there was such a branch.
929#[derive(Debug, Serialize, Deserialize)]
930#[serde(rename_all = "camelCase")]
931pub struct DeleteBranchArgs {
932 pub repo_id: String,
933 pub branch: String,
934 /// The commit the branch must still point to. A branch that moved
935 /// since (someone pushed to it) is left alone.
936 #[serde(default, skip_serializing_if = "Option::is_none")]
937 pub head: Option<String>,
938}
939
940/// `commit_file`: writes one file on a new branch made from the default
941/// branch's head, as one commit by `actor`, without a sandbox. For a change
942/// g1t proposes on someone's behalf, such as a starter workflow, which then
943/// becomes a pull request. Refused unless `actor` may push, when the branch
944/// already exists, or when the file is already there. Returns
945/// `Outcome<CommittedFile>`.
946#[derive(Debug, Serialize, Deserialize)]
947#[serde(rename_all = "camelCase")]
948pub struct CommitFileArgs {
949 pub repo: RepoPath,
950 pub actor: User,
951 /// The new branch, which must not exist yet.
952 pub branch: String,
953 /// Where the file goes, such as `.g1t/workflows/ci.yml`.
954 pub path: String,
955 pub content: String,
956 pub message: String,
957}
958
959/// The commit `commit_file` made.
960#[derive(Clone, Debug, Serialize, Deserialize)]
961#[serde(rename_all = "camelCase")]
962pub struct CommittedFile {
963 pub branch: String,
964 pub commit: String,
965}
966
967/// `readable`: of these repository ids, the repositories the viewer may
968/// read, as `get_by_id` decides; forks and unknown ids are left out. For
969/// services that hold ids and must show only what the viewer could open.
970/// At most [`MAX_READABLE`] ids are looked at. Returns `Vec<Repo>`.
971#[derive(Debug, Serialize, Deserialize)]
972pub struct ReadableArgs {
973 pub ids: Vec<String>,
974 pub viewer: Viewer,
975}
976
977/// The most ids one `readable` call looks at.
978pub const MAX_READABLE: usize = 500;
979
980/// `commit_days`: how many commits a person pushed each day (UTC) since
981/// `since` (`YYYY-MM-DD`), to the default branch or `gh-pages` of
982/// repositories the viewer may read (as `readable` decides), for the
983/// contribution calendar. Credited to whoever pushed; at most 50 commits a
984/// push, counted along the branch's first-parent line. Returns
985/// `Vec<crate::work::ContributionDay>`, oldest first, days with none left out.
986#[derive(Debug, Serialize, Deserialize)]
987pub struct CommitDaysArgs {
988 pub user_id: String,
989 pub since: String,
990 pub viewer: Viewer,
991}
992
993/// `public_namespaces`: the workspaces in which this account made a public
994/// repository, and so a public project, which anyone can see on its page.
995/// Returns `Vec<String>` of workspace slugs.
996#[derive(Debug, Serialize, Deserialize)]
997#[serde(rename_all = "camelCase")]
998pub struct PublicNamespacesArgs {
999 pub owner_id: String,
1000}
1001
1002/// One file on a branch, or one a change touched: its path and blob.
1003#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1004pub struct FileEntry {
1005 pub path: String,
1006 /// The blob it holds now; null when the change deleted it.
1007 pub hash: Option<String>,
1008}
1009
1010/// Files, and whether there were more than were listed.
1011#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1012#[serde(rename_all = "camelCase")]
1013pub struct FileList {
1014 /// The commit the files were read at; null for an empty repository.
1015 pub commit: Option<String>,
1016 pub files: Vec<FileEntry>,
1017 pub truncated: bool,
1018}
1019
1020/// `list_files`: every file on a branch (the default branch when absent),
1021/// path order by level, never descending into a directory named in
1022/// `skip_dirs`. For services that index a repository; no viewer, since it
1023/// is only reached by g1t's own services. Returns `FileList`.
1024#[derive(Debug, Default, Serialize, Deserialize)]
1025#[serde(rename_all = "camelCase")]
1026pub struct ListFilesArgs {
1027 pub repo_id: String,
1028 #[serde(default, rename = "ref")]
1029 pub git_ref: Option<String>,
1030 #[serde(default)]
1031 pub skip_dirs: Vec<String>,
1032 /// At most this many files; capped at [`MAX_LISTED_FILES`].
1033 pub limit: u32,
1034}
1035
1036/// `changed_files`: the files that differ between two commits, as
1037/// `list_files` reads them. With no `base`, every file at `head`. Returns
1038/// `FileList`.
1039#[derive(Debug, Default, Serialize, Deserialize)]
1040#[serde(rename_all = "camelCase")]
1041pub struct ChangedFilesArgs {
1042 pub repo_id: String,
1043 #[serde(default)]
1044 pub base: Option<String>,
1045 pub head: String,
1046 #[serde(default)]
1047 pub skip_dirs: Vec<String>,
1048 pub limit: u32,
1049}
1050
1051/// The most files one `list_files` or `changed_files` call lists.
1052pub const MAX_LISTED_FILES: u32 = 10_000;
1053
1054/// `read_blobs`: the text of these blobs of a repository, for services
1055/// that index it. A blob larger than `max_bytes`, or binary, comes back
1056/// with no text. Returns `Vec<BlobText>`, in the order asked.
1057#[derive(Debug, Default, Serialize, Deserialize)]
1058#[serde(rename_all = "camelCase")]
1059pub struct ReadBlobsArgs {
1060 pub repo_id: String,
1061 pub hashes: Vec<String>,
1062 pub max_bytes: u32,
1063}
1064
1065/// The most blobs one `read_blobs` call reads.
1066pub const MAX_READ_BLOBS: usize = 100;
1067
1068/// `refs`: a repository's branches and tags with the commit each points to
1069/// (annotated tags peeled), for services that follow them, such as the
1070/// packages service's Composer registry. No viewer: g1t's own services
1071/// only. Returns `Option<RepoRefs>`, null for a fork or an unknown id.
1072#[derive(Debug, Default, Serialize, Deserialize)]
1073#[serde(rename_all = "camelCase")]
1074pub struct RefsArgs {
1075 pub repo_id: String,
1076}
1077
1078#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1079pub struct GitRefEntry {
1080 /// The full ref: `refs/heads/main`, `refs/tags/v1.0.0`.
1081 pub name: String,
1082 pub commit: String,
1083}
1084
1085#[derive(Clone, Debug, Serialize, Deserialize)]
1086pub struct RepoRefs {
1087 pub repo: Repo,
1088 pub refs: Vec<GitRefEntry>,
1089}
1090
1091/// `raw_file`: one file's bytes at a ref or commit, base64, for g1t's own
1092/// services (no viewer). Returns `Option<RawFile>`: null when the file is
1093/// missing or larger than `max_bytes`.
1094#[derive(Debug, Default, Serialize, Deserialize)]
1095#[serde(rename_all = "camelCase")]
1096pub struct RawFileArgs {
1097 pub repo_id: String,
1098 #[serde(rename = "ref")]
1099 pub git_ref: String,
1100 pub path: String,
1101 pub max_bytes: u32,
1102}
1103
1104#[derive(Clone, Debug, Serialize, Deserialize)]
1105pub struct RawFile {
1106 pub size: u64,
1107 /// Standard base64.
1108 pub data: String,
1109}
1110
1111/// `raw_blobs`: blobs' bytes, base64, in the order asked, at most
1112/// [`MAX_READ_BLOBS`]; `data` is null for one missing or larger than
1113/// `max_bytes`. For g1t's own services. Returns `Vec<RawBlob>`.
1114#[derive(Debug, Default, Serialize, Deserialize)]
1115#[serde(rename_all = "camelCase")]
1116pub struct RawBlobsArgs {
1117 pub repo_id: String,
1118 pub hashes: Vec<String>,
1119 pub max_bytes: u32,
1120}
1121
1122#[derive(Clone, Debug, Serialize, Deserialize)]
1123pub struct RawBlob {
1124 pub hash: String,
1125 pub size: u64,
1126 pub data: Option<String>,
1127}
1128
1129#[derive(Clone, Debug, Serialize, Deserialize)]
1130pub struct BlobText {
1131 pub hash: String,
1132 pub size: u64,
1133 /// Null when the blob is missing, binary or larger than asked.
1134 pub text: Option<String>,
1135}
1136
1137/// `all_ids`: every repository that is not a fork, by id, a page at a
1138/// time, for services that index all of them. Returns `IdPage`.
1139#[derive(Debug, Default, Serialize, Deserialize)]
1140pub struct AllIdsArgs {
1141 /// Ids after this one.
1142 #[serde(default)]
1143 pub after: Option<String>,
1144 pub limit: u32,
1145}
1146
1147#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1148pub struct IdPage {
1149 pub ids: Vec<String>,
1150 /// Where the next page starts; null on the last.
1151 pub next: Option<String>,
1152}
1153
1154/// `repo_creators` takes [`AllIdsArgs`]: every repository that is not a
1155/// fork, with the account that created it, a page at a time, for identity
1156/// giving creators the Admin role. Returns [`CreatorPage`].
1157#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1158pub struct CreatorPage {
1159 pub repos: Vec<RepoCreator>,
1160 /// Where the next page starts; null on the last.
1161 pub next: Option<String>,
1162}
1163
1164#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1165pub struct RepoCreator {
1166 pub id: String,
1167 /// Its workspace's slug.
1168 pub namespace: String,
1169 pub name: String,
1170 /// The account that created it.
1171 pub owner_id: String,
1172}
1173
1174/// `visibility`: which of these repositories (`namespace/name`) are
1175/// private, for billing, which pays for work on public ones from g1t's
1176/// open-source pool. A pull request's working copy answers as the
1177/// repository it is a copy of. Unknown paths are left out. Returns
1178/// `Vec<RepoVisibility>`.
1179#[derive(Debug, Default, Serialize, Deserialize)]
1180pub struct VisibilityArgs {
1181 pub paths: Vec<String>,
1182}
1183
1184#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1185pub struct RepoVisibility {
1186 pub path: String,
1187 pub is_private: bool,
1188}
1189
1190/// `git_operations`: how many git operations (clones, fetches and pushes
1191/// through g1t's git endpoints) each workspace's repositories had in a
1192/// month, for billing's git meter. Cloudflare Artifacts charges per
1193/// operation from 2026-10-14. Pushes from agents' sandboxes go to the
1194/// store directly and are not counted here. Returns
1195/// `Vec<WorkspaceGitOperations>`.
1196#[derive(Debug, Serialize, Deserialize)]
1197pub struct GitOperationsArgs {
1198 /// YYYY-MM.
1199 pub month: String,
1200 /// Count only from this hour on, `YYYY-MM-DDTHH`, such as the day the
1201 /// provider starts charging.
1202 #[serde(default)]
1203 pub since: Option<String>,
1204 /// One workspace only; every workspace with any when absent.
1205 #[serde(default)]
1206 pub namespace: Option<String>,
1207}
1208
1209#[derive(Clone, Debug, Serialize, Deserialize)]
1210pub struct WorkspaceGitOperations {
1211 pub namespace: String,
1212 pub operations: u64,
1213}
1214
1215/// `storage`: what each workspace's private repositories hold, as far as
1216/// g1t can measure it, for billing's daily storage meter. Returns
1217/// `Vec<WorkspaceStorage>`.
1218///
1219/// The git store does not report a repository's size. What is counted is
1220/// the bytes of every pack pushed through g1t's git endpoints to the
1221/// repository or to its pull requests' working copies. Pushes made from
1222/// agents' sandboxes, which go to the store directly, and imports are not
1223/// counted, so it is a lower bound on what is stored.
1224#[derive(Debug, Default, Serialize, Deserialize)]
1225pub struct StorageArgs {}
1226
1227#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1228pub struct WorkspaceStorage {
1229 pub namespace: String,
1230 pub private_bytes: i64,
1231 pub public_bytes: i64,
1232}
1233
1234/// `transfer`: moves a repository to another workspace, keeping its name,
1235/// its id and everything kept under it. The actor must own both
1236/// workspaces. The old path keeps working as a redirect (see
1237/// `resolve_path`) until a repository is made there. Publishes
1238/// `repo.transferred`. Returns `Outcome<Repo>`, the repository at its new
1239/// path.
1240#[derive(Debug, Serialize, Deserialize)]
1241#[serde(rename_all = "camelCase")]
1242pub struct TransferArgs {
1243 pub actor: User,
1244 pub path: RepoPath,
1245 /// The destination workspace's slug.
1246 pub to: String,
1247 /// Where the request came in, for the audit log; g1t.sh when absent.
1248 #[serde(default)]
1249 pub surface: Option<crate::audit::Surface>,
1250}
1251
1252/// `resolve_path`: where a repository that was transferred away from
1253/// `path` is now, while nothing else is there. Returns `Option<RepoPath>`:
1254/// null when `path` is a repository, or never was one that moved. Callers
1255/// check the viewer may see the repository at its new path, as for any
1256/// other.
1257#[derive(Debug, Serialize, Deserialize)]
1258pub struct ResolvePathArgs {
1259 pub path: RepoPath,
1260}
1261
1262/// `namespace_count`: how many repositories (not pull request working
1263/// copies) a workspace holds, private or not, for deciding whether it can
1264/// be deleted. Returns `u32`.
1265#[derive(Debug, Serialize, Deserialize)]
1266pub struct NamespaceCountArgs {
1267 pub namespace: String,
1268}
1269
1270/// `delete`: deletes a repository. Owners of its workspace only, who type
1271/// its full name (`namespace/name`) as `confirm`. It is hidden at once,
1272/// git refuses it, and nothing runs for it; it can be restored for
1273/// [`RESTORE_DAYS`] days, then it is purged, its git data with it. Its
1274/// name stays taken until then, or until it is purged sooner from the
1275/// workspace's Recently deleted list. Publishes `repo.deleted`. Returns
1276/// `Outcome<DeletedRepo>`.
1277#[derive(Debug, Serialize, Deserialize)]
1278#[serde(rename_all = "camelCase")]
1279pub struct DeleteArgs {
1280 pub actor: User,
1281 pub path: RepoPath,
1282 #[serde(default)]
1283 pub confirm: String,
1284 #[serde(default)]
1285 pub surface: Option<crate::audit::Surface>,
1286}
1287
1288/// `deleted`: a workspace's recently deleted repositories, newest first.
1289/// Owners only; empty for anyone else. Returns `Vec<DeletedRepo>`.
1290#[derive(Debug, Serialize, Deserialize)]
1291pub struct DeletedArgs {
1292 pub viewer: Viewer,
1293 pub namespace: String,
1294}
1295
1296/// `restore` and `purge`: a deleted repository, by the path it had.
1297/// `restore` brings it back as it was, at that path (`repo.restored`).
1298/// `purge` removes it for good now, its git data with it, and frees its
1299/// name (`repo.purged`); it takes the full name typed as `confirm`.
1300/// Owners only. Return `Outcome<Repo>` and `Outcome<bool>`.
1301#[derive(Debug, Serialize, Deserialize)]
1302#[serde(rename_all = "camelCase")]
1303pub struct DeletedRepoArgs {
1304 pub actor: User,
1305 pub path: RepoPath,
1306 #[serde(default)]
1307 pub confirm: Option<String>,
1308 #[serde(default)]
1309 pub surface: Option<crate::audit::Surface>,
1310}
1311
1312/// `purge_due`: purges deleted repositories whose time has passed, at
1313/// most `limit` (25 when absent). The service's own schedule runs it.
1314/// Returns `u32`, how many were purged.
1315#[derive(Debug, Default, Serialize, Deserialize)]
1316pub struct PurgeDueArgs {
1317 #[serde(default)]
1318 pub limit: Option<u32>,
1319}
1320
1321/// `rename`: gives a repository a new name in its workspace, keeping its
1322/// id, its git data and everything kept under it. Owners only. The old
1323/// path keeps redirecting, as after a transfer, until a repository is made
1324/// there. Publishes `repo.renamed`. Returns `Outcome<Repo>`.
1325#[derive(Debug, Serialize, Deserialize)]
1326#[serde(rename_all = "camelCase")]
1327pub struct RenameArgs {
1328 pub actor: User,
1329 pub path: RepoPath,
1330 pub name: String,
1331 #[serde(default)]
1332 pub surface: Option<crate::audit::Surface>,
1333}
1334
1335/// `archive`: makes a repository read-only (`archived: true`), or writable
1336/// again. Owners only. While archived, pushes and merges are refused,
1337/// issues and pull requests are locked, and agents and workflows do not
1338/// run; deployments keep serving. Publishes `repo.archived` or
1339/// `repo.unarchived`. Returns `Outcome<Repo>`.
1340#[derive(Debug, Serialize, Deserialize)]
1341#[serde(rename_all = "camelCase")]
1342pub struct ArchiveArgs {
1343 pub actor: User,
1344 pub path: RepoPath,
1345 pub archived: bool,
1346 #[serde(default)]
1347 pub surface: Option<crate::audit::Surface>,
1348}
1349
1350/// `set_visibility`: makes a repository public or private. Owners only,
1351/// who type its full name as `confirm`. A free workspace takes a private
1352/// repository only while its private storage has room. Publishes
1353/// `repo.updated` and `repo.visibility_changed`. Returns `Outcome<Repo>`.
1354#[derive(Debug, Serialize, Deserialize)]
1355#[serde(rename_all = "camelCase")]
1356pub struct SetVisibilityArgs {
1357 pub actor: User,
1358 pub path: RepoPath,
1359 pub is_private: bool,
1360 #[serde(default)]
1361 pub confirm: String,
1362 #[serde(default)]
1363 pub surface: Option<crate::audit::Surface>,
1364}
1365
1366/// `set_default_branch`: makes another existing branch the one everything
1367/// lands on. Members of its workspace. Open pull requests then merge into
1368/// it. Publishes `repo.default_branch_changed`. Returns `Outcome<Repo>`.
1369#[derive(Debug, Serialize, Deserialize)]
1370#[serde(rename_all = "camelCase")]
1371pub struct SetDefaultBranchArgs {
1372 pub actor: User,
1373 pub path: RepoPath,
1374 pub branch: String,
1375 #[serde(default)]
1376 pub surface: Option<crate::audit::Surface>,
1377}
1378
1379/// `rename_branch`: renames a branch. Members of its workspace; only an
1380/// owner renames the default branch, which stays the default. Pull
1381/// requests from it follow, and web addresses naming the old branch
1382/// redirect until a branch of that name is made again. Publishes
1383/// `branch.renamed` (and `repo.default_branch_changed` for the default).
1384/// Returns `Outcome<Repo>`.
1385#[derive(Debug, Serialize, Deserialize)]
1386#[serde(rename_all = "camelCase")]
1387pub struct RenameBranchArgs {
1388 pub actor: User,
1389 pub path: RepoPath,
1390 pub from: String,
1391 pub to: String,
1392 #[serde(default)]
1393 pub surface: Option<crate::audit::Surface>,
1394}
1395
1396/// `resolve_branch`: what a branch renamed away from `branch` is called
1397/// now, for web addresses that name the old one; null when `branch` was
1398/// never renamed or exists again. Returns `Option<String>`.
1399#[derive(Debug, Serialize, Deserialize)]
1400#[serde(rename_all = "camelCase")]
1401pub struct ResolveBranchArgs {
1402 pub repo_id: String,
1403 pub branch: String,
1404}
1405
1406/// `status_by_id`: whether a repository is archived or deleted, for g1t's
1407/// own services deciding whether to act on it. An unknown id answers as
1408/// deleted. Returns `RepoStatus`.
1409#[derive(Debug, Serialize, Deserialize)]
1410pub struct StatusByIdArgs {
1411 pub id: String,
1412}
1413
1414#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1415pub struct RepoStatus {
1416 pub archived: bool,
1417 pub deleted: bool,
1418}
1419
1420impl RepoStatus {
1421 /// Whether work may start on it: neither archived nor deleted.
1422 pub fn active(&self) -> bool {
1423 !self.archived && !self.deleted
1424 }
1425}
1426
1427/// What a person is told when something would change an archived
1428/// repository.
1429pub fn archived_message(namespace: &str, name: &str) -> String {
1430 format!(
1431 "{namespace}/{name} is archived, so it is read-only. An owner can unarchive it in its settings."
1432 )
1433}
1434
1435/// The path a repository was transferred from, and when, as `transfer`
1436/// keeps it so old addresses redirect.
1437#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1438#[serde(rename_all = "camelCase")]
1439pub struct RepoRedirect {
1440 pub from: RepoPath,
1441 pub repo_id: String,
1442 /// RFC 3339.
1443 pub created_at: String,
1444}
1445
1446#[cfg(test)]
1447mod topic_tests {
1448 use super::*;
1449
1450 fn topics(list: &[&str]) -> Result<Vec<String>, String> {
1451 clean_topics(&list.iter().map(|t| t.to_string()).collect::<Vec<_>>())
1452 }
1453
1454 #[test]
1455 fn topics_are_tidied() {
1456 assert_eq!(topics(&["Rust", " web_server ", "rust", ""]).unwrap(), vec!["rust", "web-server"]);
1457 }
1458
1459 #[test]
1460 fn websites_are_tidied() {
1461 assert_eq!(clean_website(" example.com ").unwrap().as_deref(), Some("https://example.com"));
1462 assert_eq!(clean_website("http://a.io/x").unwrap().as_deref(), Some("http://a.io/x"));
1463 assert_eq!(clean_website("").unwrap(), None);
1464 assert!(clean_website("ftp://a.io").is_err());
1465 assert!(clean_website("localhost").is_err());
1466 assert!(clean_website("https://a b.io").is_err());
1467 }
1468
1469 #[test]
1470 fn branch_names_follow_git() {
1471 for good in ["main", "trunk", "release/1.2", "feat-x_y"] {
1472 assert!(is_valid_branch_name(good), "{good}");
1473 }
1474 for bad in ["", "-x", "a..b", "a b", "x.lock", "a/", ".hidden", "a/.b", "g1t-queue", "a~1", "a:b", "@"] {
1475 assert!(!is_valid_branch_name(bad), "{bad}");
1476 }
1477 }
1478
1479 #[test]
1480 fn odd_topics_are_refused() {
1481 assert!(topics(&["c++"]).is_err());
1482 assert!(topics(&["-lead"]).is_err());
1483 assert!(topics(&[&"a".repeat(36)]).is_err());
1484 let many: Vec<String> = (0..21).map(|i| format!("t{i}")).collect();
1485 assert!(clean_topics(&many).is_err());
1486 }
1487}
1488
1489#[cfg(test)]
1490mod tests {
1491 use super::*;
1492
1493 #[test]
1494 fn a_pull_branch_update_reads_as_the_web_expects() {
1495 let update = PullBranchUpdate::NeedsAgent {
1496 reason: NeedsAgentReason::Overlap,
1497 detail: "both".into(),
1498 paths: vec!["a.rs".into()],
1499 };
1500 assert_eq!(
1501 serde_json::to_value(&update).unwrap(),
1502 serde_json::json!({ "outcome": "needs_agent", "reason": "overlap", "detail": "both", "paths": ["a.rs"] })
1503 );
1504 let done = PullBranchUpdate::UpToDate { commit: "c".into() };
1505 assert_eq!(serde_json::to_value(&done).unwrap(), serde_json::json!({ "outcome": "up_to_date", "commit": "c" }));
1506 }
1507}