Skip to content

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

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