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