Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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