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,540 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 /// Refuse if the source no longer names the commit that was checked.
437 #[serde(default)]
438 pub expected_commit: Option<String>,
439}
440
441#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
442#[serde(rename_all = "lowercase")]
443pub enum FileStatus {
444 Added,
445 Modified,
446 Deleted,
447}
448
449#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
450#[serde(rename_all = "lowercase")]
451pub enum LineKind {
452 /// Unchanged, shown for context.
453 Context,
454 Add,
455 Delete,
456}
457
458#[derive(Clone, Debug, Serialize, Deserialize)]
459pub struct DiffLine {
460 pub kind: LineKind,
461 /// Line number in the old file; absent for added lines.
462 pub old: Option<u32>,
463 /// Line number in the new file; absent for deleted lines.
464 pub new: Option<u32>,
465 pub text: String,
466}
467
468/// A run of changed lines with their surrounding context.
469#[derive(Clone, Debug, Serialize, Deserialize)]
470pub struct Hunk {
471 pub lines: Vec<DiffLine>,
472}
473
474#[derive(Clone, Debug, Serialize, Deserialize)]
475pub struct FileDiff {
476 pub path: String,
477 pub status: FileStatus,
478 pub additions: u32,
479 pub deletions: u32,
480 /// True when the file is binary or too large, so no lines are shown.
481 pub binary: bool,
482 pub hunks: Vec<Hunk>,
483 /// True when the comparison reached its limit on lines before this
484 /// file, so it is listed without its lines (and with no counts): ask
485 /// `compare` again with its path in `paths` to have them.
486 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
487 pub omitted: bool,
488}
489
490/// What changed between two commits.
491#[derive(Clone, Debug, Serialize, Deserialize)]
492pub struct Comparison {
493 /// Null when the head has no earlier commit to compare against.
494 pub base: Option<String>,
495 pub head: String,
496 pub files: Vec<FileDiff>,
497 /// True when more files changed than are listed. A listed file whose
498 /// lines were left out says so itself (`FileDiff::omitted`).
499 pub truncated: bool,
500}
501
502/// `compare`: what `head` changes relative to `base`.
503///
504/// `head` is a branch or a commit, and defaults to the default branch.
505/// With no `base`, a fork is compared against the point where it and the
506/// repository it came from last agreed; a branch against the point where it
507/// left the default branch; and the default branch against its head's
508/// parent. Returns `Outcome<Comparison>`.
509#[derive(Debug, Serialize, Deserialize)]
510#[serde(rename_all = "camelCase")]
511pub struct CompareArgs {
512 pub repo_id: String,
513 pub viewer: Viewer,
514 #[serde(default)]
515 pub base: Option<String>,
516 #[serde(default)]
517 pub head: Option<String>,
518 /// With no `base`: the branch whose shared point with the head it is
519 /// compared from, instead of the default branch. A pull request into
520 /// another branch is compared this way.
521 #[serde(default)]
522 pub base_branch: Option<String>,
523 /// Only these files, with their lines: how a file left out of a large
524 /// comparison is read on its own. At most 50.
525 #[serde(default)]
526 pub paths: Option<Vec<String>>,
527}
528
529/// Lines `start` to `end` of a file, inclusive and counted from 1, last
530/// changed by `commit`.
531#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
532pub struct BlameRange {
533 pub start: u32,
534 pub end: u32,
535 pub commit: String,
536}
537
538/// Who last changed each line of a file.
539#[derive(Clone, Debug, Serialize, Deserialize)]
540pub struct Blame {
541 /// The commit the file was read at.
542 pub head: String,
543 /// Every line, in order, in runs that share a commit.
544 pub ranges: Vec<BlameRange>,
545 /// The commits the ranges name, each once.
546 pub commits: Vec<Commit>,
547 /// True when the history was too long to read in full, so the oldest
548 /// lines are given to the oldest commit read.
549 pub partial: bool,
550}
551
552/// `blame`: who last changed each line of `path` as of `ref` (the default
553/// branch if absent). Returns `Outcome<Blame>`; not found when the file is
554/// missing or is not text.
555#[derive(Debug, Serialize, Deserialize)]
556pub struct BlameArgs {
557 pub path: RepoPath,
558 pub viewer: Viewer,
559 #[serde(default, rename = "ref")]
560 pub git_ref: Option<String>,
561 #[serde(rename = "filePath")]
562 pub file_path: String,
563}
564
565/// A branch and the commit it points to.
566#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
567pub struct Branch {
568 pub name: String,
569 pub hash: String,
570}
571
572/// `last_commits`: which commit last changed each entry of a directory at
573/// `ref` (the default branch when absent). Returns `Outcome<LastCommits>`.
574#[derive(Debug, Serialize, Deserialize)]
575#[serde(rename_all = "camelCase")]
576pub struct LastCommitsArgs {
577 pub path: RepoPath,
578 pub viewer: Viewer,
579 #[serde(default, rename = "ref")]
580 pub git_ref: Option<String>,
581 #[serde(default)]
582 pub tree_path: String,
583 /// Answer within this many milliseconds with what was found; absent,
584 /// within 20 seconds. A walk that stops short keeps its progress, and
585 /// the next call goes on from it.
586 #[serde(default)]
587 pub budget_ms: Option<u64>,
588}
589
590/// `path_log`: the commits on the first-parent chain from `ref` (the
591/// default branch when absent) that changed `file_path`, a file or a
592/// directory, newest first, at most `limit`. Returns `Outcome<PathLog>`.
593#[derive(Debug, Serialize, Deserialize)]
594#[serde(rename_all = "camelCase")]
595pub struct PathLogArgs {
596 pub path: RepoPath,
597 pub viewer: Viewer,
598 #[serde(default, rename = "ref")]
599 pub git_ref: Option<String>,
600 pub file_path: String,
601 pub limit: u32,
602 /// Answer within this many milliseconds with what was found.
603 #[serde(default)]
604 pub budget_ms: Option<u64>,
605}
606
607/// A path's history: `complete` is false when the walk ran out of time or
608/// reads before the first commit and before `limit`.
609#[derive(Clone, Debug, Serialize, Deserialize)]
610#[serde(rename_all = "camelCase")]
611pub struct PathLog {
612 pub commits: Vec<Commit>,
613 pub complete: bool,
614}
615
616/// An entry of a directory and the commit that last changed it.
617#[derive(Clone, Debug, Serialize, Deserialize)]
618pub struct LastCommit {
619 pub name: String,
620 pub commit: Commit,
621}
622
623/// The entries' last commits. `complete` is false when the walk stopped
624/// (or the history ran out) before every entry was placed; those entries
625/// are left out, and a later call goes on placing them.
626#[derive(Clone, Debug, Serialize, Deserialize)]
627pub struct LastCommits {
628 pub entries: Vec<LastCommit>,
629 pub complete: bool,
630}
631
632/// `branch_drift`: how far each of `heads` (branch head commits) has moved
633/// from `base` (the default branch's head commit), and each one's head
634/// commit, in one call. Every answer is kept by the pair of hashes: neither
635/// history can change, so neither can it. Returns `Outcome<BranchDrifts>`.
636#[derive(Debug, Serialize, Deserialize)]
637#[serde(rename_all = "camelCase")]
638pub struct BranchDriftArgs {
639 pub path: RepoPath,
640 pub viewer: Viewer,
641 pub base: String,
642 pub heads: Vec<String>,
643}
644
645/// Commits a branch has that the default branch does not (`ahead`), and
646/// the other way round (`behind`), as `git rev-list --left-right --count`.
647#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
648pub struct Drift {
649 pub ahead: u32,
650 pub behind: u32,
651}
652
653/// One branch head's commit and drift. `drift` is absent when the two
654/// histories do not meet within what is read (or could not be read).
655#[derive(Clone, Debug, Serialize, Deserialize)]
656pub struct BranchDrift {
657 pub head: String,
658 pub commit: Option<Commit>,
659 pub drift: Option<Drift>,
660}
661
662/// `base`'s own commit, and each head's answer in the order asked.
663#[derive(Clone, Debug, Serialize, Deserialize)]
664pub struct BranchDrifts {
665 pub base: Option<Commit>,
666 pub branches: Vec<BranchDrift>,
667}
668
669/// `tags`: the repository's tags, newest commit first, each with the
670/// commit it names. Returns `Outcome<Vec<Tag>>`.
671#[derive(Debug, Serialize, Deserialize)]
672pub struct TagsArgs {
673 pub path: RepoPath,
674 pub viewer: Viewer,
675}
676
677/// A tag, and its commit when it could be read.
678#[derive(Clone, Debug, Serialize, Deserialize)]
679pub struct Tag {
680 pub name: String,
681 pub commit: Option<Commit>,
682}
683
684/// `branches`: the repository's branches, default branch first.
685/// Returns `Outcome<Vec<Branch>>`.
686#[derive(Debug, Serialize, Deserialize)]
687pub struct BranchesArgs {
688 pub path: RepoPath,
689 pub viewer: Viewer,
690}
691
692/// `behind`: whether the default branch of the repository a pull request
693/// would merge into has commits its source does not. For services that
694/// have already decided the caller may see the pull request; it reveals
695/// one bit. Returns `bool`.
696#[derive(Debug, Serialize, Deserialize)]
697#[serde(rename_all = "camelCase")]
698pub struct BehindArgs {
699 /// The pull request's fork, or the repository itself for a branch.
700 pub source_id: String,
701 /// The branch of the source. A fork is compared on its default branch.
702 #[serde(default)]
703 pub branch: Option<String>,
704 /// The branch of the target it would merge into; the default branch
705 /// when absent.
706 #[serde(default)]
707 pub target_branch: Option<String>,
708}
709
710/// `divergence`: how a pull request's source and the default branch it
711/// would merge into have moved apart since they last agreed: the files each
712/// side changed. Takes `BehindArgs`. For services that have already decided
713/// the caller may see the pull request; it reveals paths, not contents.
714/// Returns `Option<Divergence>`, null when either side has no commits.
715#[derive(Clone, Debug, Default, Serialize, Deserialize)]
716#[serde(rename_all = "camelCase")]
717pub struct Divergence {
718 /// The source's commit.
719 pub head: String,
720 /// The default branch's commit.
721 pub base: String,
722 /// Where they last agreed, if that could be found.
723 pub merge_base: Option<String>,
724 /// Whether the default branch has commits the source does not.
725 pub behind: bool,
726 /// The files the source changed since the merge base.
727 pub ours: Vec<String>,
728 /// The files the default branch changed since the merge base. Empty
729 /// when it is not behind.
730 pub theirs: Vec<String>,
731 /// Whether either list was cut short.
732 pub truncated: bool,
733}
734
735/// `update_pull_branch`: brings a pull request's source up to date with the
736/// default branch it would merge into, without a sandbox, when that can be
737/// done safely: merges the default branch's head into the source's head and
738/// pushes the merge commit to the source's branch, as `actor`, only if the
739/// branch has not moved meanwhile. It applies only when the two sides
740/// changed different files since they last agreed; otherwise the answer is
741/// [`PullBranchUpdate::NeedsAgent`] and nothing is pushed. Refused unless
742/// `actor` may push to the source. Returns `Outcome<PullBranchUpdate>`.
743#[derive(Debug, Serialize, Deserialize)]
744#[serde(rename_all = "camelCase")]
745pub struct UpdatePullBranchArgs {
746 /// The pull request's fork, or the repository itself for a branch.
747 pub source_id: String,
748 /// The branch of the source. A fork is updated on its default branch.
749 #[serde(default)]
750 pub branch: Option<String>,
751 /// The pull request's number, to name it in the merge commit's message
752 /// when its branch has the same name as the default branch.
753 pub number: u32,
754 /// Who asked: the merge commit's author and committer, and the pusher.
755 pub actor: User,
756 /// The branch of the target to merge in; its default branch when absent.
757 #[serde(default)]
758 pub target_branch: Option<String>,
759}
760
761/// Why an update has to be left to a sandbox.
762#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
763#[serde(rename_all = "snake_case")]
764pub enum NeedsAgentReason {
765 /// Both sides changed some of the same files; merging them needs a
766 /// real merge, which may or may not conflict.
767 Overlap,
768 /// Merging is known to conflict.
769 Conflicting,
770 /// The update could not be worked out here, such as when the two sides
771 /// share no history g1t can see, or the change is too large to list.
772 Unsupported,
773}
774
775/// What came of `update_pull_branch` (or the work service's `catch_up_pull`).
776#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
777#[serde(tag = "outcome", rename_all = "snake_case")]
778pub enum PullBranchUpdate {
779 /// The merge commit was pushed: the branch moved from `previous` to
780 /// `commit`.
781 Updated { commit: String, previous: String },
782 /// The source already holds the default branch's head.
783 UpToDate { commit: String },
784 /// Nothing was pushed; a sandbox has to merge it. `paths` are the
785 /// files both sides changed, or that conflict, when known.
786 NeedsAgent {
787 reason: NeedsAgentReason,
788 detail: String,
789 paths: Vec<String>,
790 },
791}
792
793/// `pull_merge_ref`: points `refs/pull/<number>/head` in the repository a
794/// pull request merges into at its head, fetching the head's objects from
795/// the fork first when it comes from one, and `refs/pull/<number>/merge`
796/// at the commit that merges that head into the base branch's tip, made
797/// without a sandbox: what GitHub's `pull_request` runs build. For the
798/// actions service, which has already decided the pull request's events
799/// may start runs. Returns `PullMergeRef`.
800#[derive(Debug, Serialize, Deserialize)]
801#[serde(rename_all = "camelCase")]
802pub struct PullMergeRefArgs {
803 /// The repository the pull request merges into.
804 pub repo_id: String,
805 /// The pull request's fork, or the repository itself for a branch.
806 pub source_id: String,
807 /// The pull request's head commit.
808 pub head: String,
809 pub number: u32,
810 /// The branch it merges into; the default branch when absent.
811 #[serde(default)]
812 pub target_branch: Option<String>,
813}
814
815/// What came of `pull_merge_ref`.
816#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
817#[serde(tag = "outcome", rename_all = "snake_case")]
818pub enum PullMergeRef {
819 /// `refs/pull/<number>/merge` is `merge`, whose parents are `base`
820 /// (the base branch's tip) and `head`.
821 Merged { head: String, base: String, merge: String },
822 /// The head and the base branch change the same lines, or the same
823 /// binary file, differently: there is no merge commit, and
824 /// `refs/pull/<number>/merge` is removed, as on GitHub.
825 Conflict { head: String, base: String, paths: Vec<String> },
826 /// g1t could not make the merge commit itself (a file renamed on one
827 /// side and changed on the other, a very large change, a history it
828 /// could not follow), or either side has no commits: `reason` says why.
829 Unavailable { reason: String },
830}
831
832/// The ref a pull request's head is kept at in the repository it merges
833/// into, as on GitHub.
834pub fn pull_head_ref(number: u32) -> String {
835 format!("refs/pull/{number}/head")
836}
837
838/// The ref of a pull request's test merge commit, as on GitHub.
839pub fn pull_merge_ref(number: u32) -> String {
840 format!("refs/pull/{number}/merge")
841}
842
843/// `head`: the commit a branch points to, or null. For services reacting
844/// to a push, which have no viewer; it reveals nothing but a commit hash.
845/// Returns `Option<String>`.
846#[derive(Debug, Serialize, Deserialize)]
847#[serde(rename_all = "camelCase")]
848pub struct HeadArgs {
849 pub repo_id: String,
850 /// Empty for the repository's default branch.
851 pub branch: String,
852}
853
854/// Where g1t keeps branches of its own in a repository, such as the merge
855/// queue's tested states. Only these can be removed with `delete_branch`.
856pub const G1T_BRANCH_PREFIX: &str = "g1t-";
857
858/// `delete_branch`: removes a branch g1t made for itself once it is done
859/// with it, never one of people's: the name must start with
860/// [`G1T_BRANCH_PREFIX`], or `head` must name the commit it points to (a
861/// dependency update's branch, which g1t pushed and whose pull request it
862/// closed). Never the default branch. For services, which have no viewer.
863/// Returns `Outcome<bool>`: whether there was such a branch.
864#[derive(Debug, Serialize, Deserialize)]
865#[serde(rename_all = "camelCase")]
866pub struct DeleteBranchArgs {
867 pub repo_id: String,
868 pub branch: String,
869 /// The commit the branch must still point to. A branch that moved
870 /// since (someone pushed to it) is left alone.
871 #[serde(default, skip_serializing_if = "Option::is_none")]
872 pub head: Option<String>,
873}
874
875/// `commit_file`: writes one file on a new branch made from the default
876/// branch's head, as one commit by `actor`, without a sandbox. For a change
877/// g1t proposes on someone's behalf, such as a starter workflow, which then
878/// becomes a pull request. Refused unless `actor` may push, when the branch
879/// already exists, or when the file is already there. Returns
880/// `Outcome<CommittedFile>`.
881#[derive(Debug, Serialize, Deserialize)]
882#[serde(rename_all = "camelCase")]
883pub struct CommitFileArgs {
884 pub repo: RepoPath,
885 pub actor: User,
886 /// The new branch, which must not exist yet.
887 pub branch: String,
888 /// Where the file goes, such as `.g1t/workflows/ci.yml`.
889 pub path: String,
890 pub content: String,
891 pub message: String,
892}
893
894/// The commit `commit_file` made.
895#[derive(Clone, Debug, Serialize, Deserialize)]
896#[serde(rename_all = "camelCase")]
897pub struct CommittedFile {
898 pub branch: String,
899 pub commit: String,
900}
901
902/// `readable`: of these repository ids, the repositories the viewer may
903/// read, as `get_by_id` decides; forks and unknown ids are left out. For
904/// services that hold ids and must show only what the viewer could open.
905/// At most [`MAX_READABLE`] ids are looked at. Returns `Vec<Repo>`.
906#[derive(Debug, Serialize, Deserialize)]
907pub struct ReadableArgs {
908 pub ids: Vec<String>,
909 pub viewer: Viewer,
910}
911
912/// The most ids one `readable` call looks at.
913pub const MAX_READABLE: usize = 500;
914
915/// `commit_days`: how many commits a person pushed each day (UTC) since
916/// `since` (`YYYY-MM-DD`), to the default branch or `gh-pages` of
917/// repositories the viewer may read (as `readable` decides), for the
918/// contribution calendar. Credited to whoever pushed; at most 50 commits a
919/// push, counted along the branch's first-parent line. Returns
920/// `Vec<crate::work::ContributionDay>`, oldest first, days with none left out.
921#[derive(Debug, Serialize, Deserialize)]
922pub struct CommitDaysArgs {
923 pub user_id: String,
924 pub since: String,
925 pub viewer: Viewer,
926}
927
928/// `public_namespaces`: the workspaces in which this account made a public
929/// repository, and so a public project, which anyone can see on its page.
930/// Returns `Vec<String>` of workspace slugs.
931#[derive(Debug, Serialize, Deserialize)]
932#[serde(rename_all = "camelCase")]
933pub struct PublicNamespacesArgs {
934 pub owner_id: String,
935}
936
937/// One file on a branch, or one a change touched: its path and blob.
938#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
939pub struct FileEntry {
940 pub path: String,
941 /// The blob it holds now; null when the change deleted it.
942 pub hash: Option<String>,
943 /// `blob`, `exec` (executable) or `symlink` (the blob is where it
944 /// points); absent for a deleted file.
945 #[serde(default, skip_serializing_if = "Option::is_none")]
946 pub kind: Option<EntryKind>,
947}
948
949/// Files, and whether there were more than were listed.
950#[derive(Clone, Debug, Default, Serialize, Deserialize)]
951#[serde(rename_all = "camelCase")]
952pub struct FileList {
953 /// The commit the files were read at; null for an empty repository.
954 pub commit: Option<String>,
955 /// When that commit was made (RFC 3339), what an archive dates its
956 /// files by.
957 #[serde(default, skip_serializing_if = "Option::is_none")]
958 pub date: Option<String>,
959 pub files: Vec<FileEntry>,
960 pub truncated: bool,
961}
962
963/// `list_files`: every file on a branch (the default branch when absent),
964/// path order by level, never descending into a directory named in
965/// `skip_dirs`. For services that index a repository; no viewer, since it
966/// is only reached by g1t's own services. Returns `FileList`.
967#[derive(Debug, Default, Serialize, Deserialize)]
968#[serde(rename_all = "camelCase")]
969pub struct ListFilesArgs {
970 pub repo_id: String,
971 #[serde(default, rename = "ref")]
972 pub git_ref: Option<String>,
973 #[serde(default)]
974 pub skip_dirs: Vec<String>,
975 /// At most this many files; capped at [`MAX_LISTED_FILES`].
976 pub limit: u32,
977 /// Symlinks too, for an archive: by default only files whose text can
978 /// be read are listed.
979 #[serde(default)]
980 pub links: bool,
981}
982
983/// `changed_files`: the files that differ between two commits, as
984/// `list_files` reads them. With no `base`, every file at `head`. Returns
985/// `FileList`.
986#[derive(Debug, Default, Serialize, Deserialize)]
987#[serde(rename_all = "camelCase")]
988pub struct ChangedFilesArgs {
989 pub repo_id: String,
990 #[serde(default)]
991 pub base: Option<String>,
992 pub head: String,
993 #[serde(default)]
994 pub skip_dirs: Vec<String>,
995 pub limit: u32,
996}
997
998/// `archive_link`: a link to download a branch, tag or commit as an
999/// archive, for someone who can read the repository: the site's
1000/// `/<owner>/<repo>/archive/<commit>.tar.gz` (or `.zip`) with `?token=`,
1001/// good for [`ARCHIVE_LINK_SECONDS`] without a session or an access token.
1002/// The default branch when `ref` is absent. Returns `Outcome<ArchiveLink>`;
1003/// refused as `invalid` where the repos service has no `REPOS_KEY`.
1004#[derive(Debug, Serialize, Deserialize)]
1005#[serde(rename_all = "camelCase")]
1006pub struct ArchiveLinkArgs {
1007 pub path: RepoPath,
1008 pub viewer: Viewer,
1009 #[serde(default, rename = "ref")]
1010 pub git_ref: Option<String>,
1011}
1012
1013/// How long an archive link lasts.
1014pub const ARCHIVE_LINK_SECONDS: u64 = 10 * 60;
1015
1016#[derive(Clone, Debug, Serialize, Deserialize)]
1017#[serde(rename_all = "camelCase")]
1018pub struct ArchiveLink {
1019 pub repo: Repo,
1020 /// The commit the ref named, which the link downloads.
1021 pub commit: String,
1022 /// What the link carries as `?token=`.
1023 pub token: String,
1024 /// RFC 3339.
1025 pub expires_at: String,
1026}
1027
1028/// `open_archive_token`: what an archive link's token lets anyone holding
1029/// it download, when it is for the repository at `path` and has not
1030/// expired. Returns `Option<ArchiveGrant>`. No viewer: the token is the
1031/// permission.
1032#[derive(Debug, Serialize, Deserialize)]
1033#[serde(rename_all = "camelCase")]
1034pub struct OpenArchiveTokenArgs {
1035 pub path: RepoPath,
1036 pub token: String,
1037}
1038
1039#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1040#[serde(rename_all = "camelCase")]
1041pub struct ArchiveGrant {
1042 pub repo_id: String,
1043 /// The repository's workspace and name as they are now.
1044 pub namespace: String,
1045 pub name: String,
1046 pub commit: String,
1047}
1048
1049/// The most files one `list_files` or `changed_files` call lists.
1050pub const MAX_LISTED_FILES: u32 = 10_000;
1051
1052/// `read_blobs`: the text of these blobs of a repository, for services
1053/// that index it. A blob larger than `max_bytes`, or binary, comes back
1054/// with no text. Returns `Vec<BlobText>`, in the order asked.
1055#[derive(Debug, Default, Serialize, Deserialize)]
1056#[serde(rename_all = "camelCase")]
1057pub struct ReadBlobsArgs {
1058 pub repo_id: String,
1059 pub hashes: Vec<String>,
1060 pub max_bytes: u32,
1061}
1062
1063/// The most blobs one `read_blobs` call reads.
1064pub const MAX_READ_BLOBS: usize = 100;
1065
1066/// `refs`: a repository's branches and tags with the commit each points to
1067/// (annotated tags peeled), for services that follow them, such as the
1068/// packages service's Composer registry. No viewer: g1t's own services
1069/// only. Returns `Option<RepoRefs>`, null for a fork or an unknown id.
1070#[derive(Debug, Default, Serialize, Deserialize)]
1071#[serde(rename_all = "camelCase")]
1072pub struct RefsArgs {
1073 pub repo_id: String,
1074}
1075
1076#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1077pub struct GitRefEntry {
1078 /// The full ref: `refs/heads/main`, `refs/tags/v1.0.0`.
1079 pub name: String,
1080 pub commit: String,
1081}
1082
1083#[derive(Clone, Debug, Serialize, Deserialize)]
1084pub struct RepoRefs {
1085 pub repo: Repo,
1086 pub refs: Vec<GitRefEntry>,
1087}
1088
1089/// `raw_file`: one file's bytes at a ref or commit, base64, for g1t's own
1090/// services (no viewer). Returns `Option<RawFile>`: null when the file is
1091/// missing or larger than `max_bytes`.
1092#[derive(Debug, Default, Serialize, Deserialize)]
1093#[serde(rename_all = "camelCase")]
1094pub struct RawFileArgs {
1095 pub repo_id: String,
1096 #[serde(rename = "ref")]
1097 pub git_ref: String,
1098 pub path: String,
1099 pub max_bytes: u32,
1100}
1101
1102#[derive(Clone, Debug, Serialize, Deserialize)]
1103pub struct RawFile {
1104 pub size: u64,
1105 /// Standard base64.
1106 pub data: String,
1107}
1108
1109/// `raw_blobs`: blobs' bytes, base64, in the order asked, at most
1110/// [`MAX_READ_BLOBS`]; `data` is null for one missing or larger than
1111/// `max_bytes`. For g1t's own services. Returns `Vec<RawBlob>`.
1112#[derive(Debug, Default, Serialize, Deserialize)]
1113#[serde(rename_all = "camelCase")]
1114pub struct RawBlobsArgs {
1115 pub repo_id: String,
1116 pub hashes: Vec<String>,
1117 pub max_bytes: u32,
1118}
1119
1120#[derive(Clone, Debug, Serialize, Deserialize)]
1121pub struct RawBlob {
1122 pub hash: String,
1123 pub size: u64,
1124 pub data: Option<String>,
1125}
1126
1127#[derive(Clone, Debug, Serialize, Deserialize)]
1128pub struct BlobText {
1129 pub hash: String,
1130 pub size: u64,
1131 /// Null when the blob is missing, binary or larger than asked.
1132 pub text: Option<String>,
1133}
1134
1135/// `all_ids`: every repository that is not a fork, by id, a page at a
1136/// time, for services that index all of them. Returns `IdPage`.
1137#[derive(Debug, Default, Serialize, Deserialize)]
1138pub struct AllIdsArgs {
1139 /// Ids after this one.
1140 #[serde(default)]
1141 pub after: Option<String>,
1142 pub limit: u32,
1143}
1144
1145#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1146pub struct IdPage {
1147 pub ids: Vec<String>,
1148 /// Where the next page starts; null on the last.
1149 pub next: Option<String>,
1150}
1151
1152/// `repo_creators` takes [`AllIdsArgs`]: every repository that is not a
1153/// fork, with the account that created it, a page at a time, for identity
1154/// giving creators the Admin role. Returns [`CreatorPage`].
1155#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1156pub struct CreatorPage {
1157 pub repos: Vec<RepoCreator>,
1158 /// Where the next page starts; null on the last.
1159 pub next: Option<String>,
1160}
1161
1162#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1163pub struct RepoCreator {
1164 pub id: String,
1165 /// Its workspace's slug.
1166 pub namespace: String,
1167 pub name: String,
1168 /// The account that created it.
1169 pub owner_id: String,
1170}
1171
1172/// `visibility`: which of these repositories (`namespace/name`) are
1173/// private, for billing, which pays for work on public ones from g1t's
1174/// open-source pool. A pull request's working copy answers as the
1175/// repository it is a copy of. Unknown paths are left out. Returns
1176/// `Vec<RepoVisibility>`.
1177#[derive(Debug, Default, Serialize, Deserialize)]
1178pub struct VisibilityArgs {
1179 pub paths: Vec<String>,
1180}
1181
1182#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1183pub struct RepoVisibility {
1184 pub path: String,
1185 pub is_private: bool,
1186}
1187
1188/// `git_operations`: how many git operations (clones, fetches and pushes
1189/// through g1t's git endpoints) each workspace's repositories had in a
1190/// month, for billing's git meter. Cloudflare Artifacts charges per
1191/// operation from 2026-10-14. Pushes from agents' sandboxes go to the
1192/// store directly and are not counted here. Returns
1193/// `Vec<WorkspaceGitOperations>`.
1194#[derive(Debug, Serialize, Deserialize)]
1195pub struct GitOperationsArgs {
1196 /// YYYY-MM.
1197 pub month: String,
1198 /// Count only from this hour on, `YYYY-MM-DDTHH`, such as the day the
1199 /// provider starts charging.
1200 #[serde(default)]
1201 pub since: Option<String>,
1202 /// One workspace only; every workspace with any when absent.
1203 #[serde(default)]
1204 pub namespace: Option<String>,
1205}
1206
1207#[derive(Clone, Debug, Serialize, Deserialize)]
1208pub struct WorkspaceGitOperations {
1209 pub namespace: String,
1210 pub operations: u64,
1211}
1212
1213/// `storage`: what each workspace's private repositories hold, as far as
1214/// g1t can measure it, for billing's daily storage meter. Returns
1215/// `Vec<WorkspaceStorage>`.
1216///
1217/// The git store does not report a repository's size. What is counted is
1218/// the bytes of every pack pushed through g1t's git endpoints to the
1219/// repository or to its pull requests' working copies. Pushes made from
1220/// agents' sandboxes, which go to the store directly, and imports are not
1221/// counted, so it is a lower bound on what is stored.
1222#[derive(Debug, Default, Serialize, Deserialize)]
1223pub struct StorageArgs {}
1224
1225#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1226pub struct WorkspaceStorage {
1227 pub namespace: String,
1228 pub private_bytes: i64,
1229 pub public_bytes: i64,
1230}
1231
1232/// `transfer`: moves a repository to another workspace, keeping its name,
1233/// its id and everything kept under it. The actor must own both
1234/// workspaces. The old path keeps working as a redirect (see
1235/// `resolve_path`) until a repository is made there. Publishes
1236/// `repo.transferred`. Returns `Outcome<Repo>`, the repository at its new
1237/// path.
1238#[derive(Debug, Serialize, Deserialize)]
1239#[serde(rename_all = "camelCase")]
1240pub struct TransferArgs {
1241 pub actor: User,
1242 pub path: RepoPath,
1243 /// The destination workspace's slug.
1244 pub to: String,
1245 /// Where the request came in, for the audit log; g1t.sh when absent.
1246 #[serde(default)]
1247 pub surface: Option<crate::audit::Surface>,
1248}
1249
1250/// `resolve_path`: where a repository that was transferred away from
1251/// `path` is now, while nothing else is there. Returns `Option<RepoPath>`:
1252/// null when `path` is a repository, or never was one that moved. Callers
1253/// check the viewer may see the repository at its new path, as for any
1254/// other.
1255#[derive(Debug, Serialize, Deserialize)]
1256pub struct ResolvePathArgs {
1257 pub path: RepoPath,
1258}
1259
1260/// `namespace_count`: how many repositories (not pull request working
1261/// copies) a workspace holds, private or not, for deciding whether it can
1262/// be deleted. Returns `u32`.
1263#[derive(Debug, Serialize, Deserialize)]
1264pub struct NamespaceCountArgs {
1265 pub namespace: String,
1266}
1267
1268/// `delete`: deletes a repository. Owners of its workspace only, who type
1269/// its full name (`namespace/name`) as `confirm`. It is hidden at once,
1270/// git refuses it, and nothing runs for it; it can be restored for
1271/// [`RESTORE_DAYS`] days, then it is purged, its git data with it. Its
1272/// name stays taken until then, or until it is purged sooner from the
1273/// workspace's Recently deleted list. Publishes `repo.deleted`. Returns
1274/// `Outcome<DeletedRepo>`.
1275#[derive(Debug, Serialize, Deserialize)]
1276#[serde(rename_all = "camelCase")]
1277pub struct DeleteArgs {
1278 pub actor: User,
1279 pub path: RepoPath,
1280 #[serde(default)]
1281 pub confirm: String,
1282 #[serde(default)]
1283 pub surface: Option<crate::audit::Surface>,
1284}
1285
1286/// `deleted`: a workspace's recently deleted repositories, newest first.
1287/// Owners only; empty for anyone else. Returns `Vec<DeletedRepo>`.
1288#[derive(Debug, Serialize, Deserialize)]
1289pub struct DeletedArgs {
1290 pub viewer: Viewer,
1291 pub namespace: String,
1292}
1293
1294/// `restore` and `purge`: a deleted repository, by the path it had.
1295/// `restore` brings it back as it was, at that path (`repo.restored`).
1296/// `purge` removes it for good now, its git data with it, and frees its
1297/// name (`repo.purged`); it takes the full name typed as `confirm`.
1298/// Owners only. Return `Outcome<Repo>` and `Outcome<bool>`.
1299#[derive(Debug, Serialize, Deserialize)]
1300#[serde(rename_all = "camelCase")]
1301pub struct DeletedRepoArgs {
1302 pub actor: User,
1303 pub path: RepoPath,
1304 #[serde(default)]
1305 pub confirm: Option<String>,
1306 #[serde(default)]
1307 pub surface: Option<crate::audit::Surface>,
1308}
1309
1310/// `purge_due`: purges deleted repositories whose time has passed, at
1311/// most `limit` (25 when absent). The service's own schedule runs it.
1312/// Returns `u32`, how many were purged.
1313#[derive(Debug, Default, Serialize, Deserialize)]
1314pub struct PurgeDueArgs {
1315 #[serde(default)]
1316 pub limit: Option<u32>,
1317}
1318
1319/// `rename`: gives a repository a new name in its workspace, keeping its
1320/// id, its git data and everything kept under it. Owners only. The old
1321/// path keeps redirecting, as after a transfer, until a repository is made
1322/// there. Publishes `repo.renamed`. Returns `Outcome<Repo>`.
1323#[derive(Debug, Serialize, Deserialize)]
1324#[serde(rename_all = "camelCase")]
1325pub struct RenameArgs {
1326 pub actor: User,
1327 pub path: RepoPath,
1328 pub name: String,
1329 #[serde(default)]
1330 pub surface: Option<crate::audit::Surface>,
1331}
1332
1333/// `archive`: makes a repository read-only (`archived: true`), or writable
1334/// again. Owners only. While archived, pushes and merges are refused,
1335/// issues and pull requests are locked, and agents and workflows do not
1336/// run; deployments keep serving. Publishes `repo.archived` or
1337/// `repo.unarchived`. Returns `Outcome<Repo>`.
1338#[derive(Debug, Serialize, Deserialize)]
1339#[serde(rename_all = "camelCase")]
1340pub struct ArchiveArgs {
1341 pub actor: User,
1342 pub path: RepoPath,
1343 pub archived: bool,
1344 #[serde(default)]
1345 pub surface: Option<crate::audit::Surface>,
1346}
1347
1348/// `set_visibility`: makes a repository public or private. Owners only,
1349/// who type its full name as `confirm`. A free workspace takes a private
1350/// repository only while its private storage has room. Publishes
1351/// `repo.updated` and `repo.visibility_changed`. Returns `Outcome<Repo>`.
1352#[derive(Debug, Serialize, Deserialize)]
1353#[serde(rename_all = "camelCase")]
1354pub struct SetVisibilityArgs {
1355 pub actor: User,
1356 pub path: RepoPath,
1357 pub is_private: bool,
1358 #[serde(default)]
1359 pub confirm: String,
1360 #[serde(default)]
1361 pub surface: Option<crate::audit::Surface>,
1362}
1363
1364/// `set_default_branch`: makes another existing branch the one everything
1365/// lands on. Members of its workspace. Open pull requests then merge into
1366/// it. Publishes `repo.default_branch_changed`. Returns `Outcome<Repo>`.
1367#[derive(Debug, Serialize, Deserialize)]
1368#[serde(rename_all = "camelCase")]
1369pub struct SetDefaultBranchArgs {
1370 pub actor: User,
1371 pub path: RepoPath,
1372 pub branch: String,
1373 #[serde(default)]
1374 pub surface: Option<crate::audit::Surface>,
1375}
1376
1377/// `rename_branch`: renames a branch. Members of its workspace; only an
1378/// owner renames the default branch, which stays the default. Pull
1379/// requests from it follow, and web addresses naming the old branch
1380/// redirect until a branch of that name is made again. Publishes
1381/// `branch.renamed` (and `repo.default_branch_changed` for the default).
1382/// Returns `Outcome<Repo>`.
1383#[derive(Debug, Serialize, Deserialize)]
1384#[serde(rename_all = "camelCase")]
1385pub struct RenameBranchArgs {
1386 pub actor: User,
1387 pub path: RepoPath,
1388 pub from: String,
1389 pub to: String,
1390 #[serde(default)]
1391 pub surface: Option<crate::audit::Surface>,
1392}
1393
1394/// `create_branch`: makes a branch named `name` at `from`: another
1395/// branch, a tag or a commit (the default branch's head when empty). The
1396/// actor needs to be able to push, and the rules for creating the branch
1397/// hold. Restoring a deleted branch is making it again at the commit
1398/// `remove_branch` answered with. Publishes `git.push`. Returns
1399/// `Outcome<Branch>`.
1400#[derive(Debug, Serialize, Deserialize)]
1401#[serde(rename_all = "camelCase")]
1402pub struct CreateBranchArgs {
1403 pub actor: User,
1404 pub path: RepoPath,
1405 pub name: String,
1406 #[serde(default)]
1407 pub from: String,
1408 #[serde(default)]
1409 pub surface: Option<crate::audit::Surface>,
1410}
1411
1412/// `remove_branch`: deletes a branch for someone who can push, never the
1413/// default branch; the rules for deleting it hold. `head`, when given, is
1414/// the commit it must still point to: a branch that moved since is left
1415/// alone. Publishes `git.deleted`. Returns `Outcome<Branch>`: the branch
1416/// as it was, so it can be made again at the same commit with
1417/// `create_branch`. (`delete_branch` is the services' own, with no actor.)
1418#[derive(Debug, Serialize, Deserialize)]
1419#[serde(rename_all = "camelCase")]
1420pub struct RemoveBranchArgs {
1421 pub actor: User,
1422 pub path: RepoPath,
1423 pub branch: String,
1424 #[serde(default, skip_serializing_if = "Option::is_none")]
1425 pub head: Option<String>,
1426 #[serde(default)]
1427 pub surface: Option<crate::audit::Surface>,
1428}
1429
1430/// `resolve_branch`: what a branch renamed away from `branch` is called
1431/// now, for web addresses that name the old one; null when `branch` was
1432/// never renamed or exists again. Returns `Option<String>`.
1433#[derive(Debug, Serialize, Deserialize)]
1434#[serde(rename_all = "camelCase")]
1435pub struct ResolveBranchArgs {
1436 pub repo_id: String,
1437 pub branch: String,
1438}
1439
1440/// `status_by_id`: whether a repository is archived or deleted, for g1t's
1441/// own services deciding whether to act on it. An unknown id answers as deleted. Returns `RepoStatus`.
1442#[derive(Debug, Serialize, Deserialize)]
1443pub struct StatusByIdArgs {
1444 pub id: String,
1445}
1446
1447#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1448pub struct RepoStatus {
1449 pub archived: bool,
1450 pub deleted: bool,
1451}
1452
1453impl RepoStatus {
1454 /// Whether work may start on it: neither archived nor deleted.
1455 pub fn active(&self) -> bool {
1456 !self.archived && !self.deleted
1457 }
1458}
1459
1460/// What a person is told when something would change an archived
1461/// repository.
1462pub fn archived_message(namespace: &str, name: &str) -> String {
1463 format!(
1464 "{namespace}/{name} is archived, so it is read-only. An owner can unarchive it in its settings."
1465 )
1466}
1467
1468/// The path a repository was transferred from, and when, as `transfer`
1469/// keeps it so old addresses redirect.
1470#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1471#[serde(rename_all = "camelCase")]
1472pub struct RepoRedirect {
1473 pub from: RepoPath,
1474 pub repo_id: String,
1475 /// RFC 3339.
1476 pub created_at: String,
1477}
1478
1479#[cfg(test)]
1480mod topic_tests {
1481 use super::*;
1482
1483 fn topics(list: &[&str]) -> Result<Vec<String>, String> {
1484 clean_topics(&list.iter().map(|t| t.to_string()).collect::<Vec<_>>())
1485 }
1486
1487 #[test]
1488 fn topics_are_tidied() {
1489 assert_eq!(topics(&["Rust", " web_server ", "rust", ""]).unwrap(), vec!["rust", "web-server"]);
1490 }
1491
1492 #[test]
1493 fn websites_are_tidied() {
1494 assert_eq!(clean_website(" example.com ").unwrap().as_deref(), Some("https://example.com"));
1495 assert_eq!(clean_website("http://a.io/x").unwrap().as_deref(), Some("http://a.io/x"));
1496 assert_eq!(clean_website("").unwrap(), None);
1497 assert!(clean_website("ftp://a.io").is_err());
1498 assert!(clean_website("localhost").is_err());
1499 assert!(clean_website("https://a b.io").is_err());
1500 }
1501
1502 #[test]
1503 fn branch_names_follow_git() {
1504 for good in ["main", "trunk", "release/1.2", "feat-x_y"] {
1505 assert!(is_valid_branch_name(good), "{good}");
1506 }
1507 for bad in ["", "-x", "a..b", "a b", "x.lock", "a/", ".hidden", "a/.b", "g1t-queue", "a~1", "a:b", "@"] {
1508 assert!(!is_valid_branch_name(bad), "{bad}");
1509 }
1510 }
1511
1512 #[test]
1513 fn odd_topics_are_refused() {
1514 assert!(topics(&["c++"]).is_err());
1515 assert!(topics(&["-lead"]).is_err());
1516 assert!(topics(&[&"a".repeat(36)]).is_err());
1517 let many: Vec<String> = (0..21).map(|i| format!("t{i}")).collect();
1518 assert!(clean_topics(&many).is_err());
1519 }
1520}
1521
1522#[cfg(test)]
1523mod tests {
1524 use super::*;
1525
1526 #[test]
1527 fn a_pull_branch_update_reads_as_the_web_expects() {
1528 let update = PullBranchUpdate::NeedsAgent {
1529 reason: NeedsAgentReason::Overlap,
1530 detail: "both".into(),
1531 paths: vec!["a.rs".into()],
1532 };
1533 assert_eq!(
1534 serde_json::to_value(&update).unwrap(),
1535 serde_json::json!({ "outcome": "needs_agent", "reason": "overlap", "detail": "both", "paths": ["a.rs"] })
1536 );
1537 let done = PullBranchUpdate::UpToDate { commit: "c".into() };
1538 assert_eq!(serde_json::to_value(&done).unwrap(), serde_json::json!({ "outcome": "up_to_date", "commit": "c" }));
1539 }
1540}