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.

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