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,812 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}
616
617/// `build_queue_step`: one entry of a merge queue landed, by its method,
618/// on a commit of the target that is not a branch's head: the default
619/// branch, or the state built for the entries ahead. Makes the commits
620/// `land` would make there (`merge`, `squash` and `rebase` new ones; a
621/// `fast_forward` none, its head only if that contains `onto`) and points
622/// `push_branch` of the target at the result, so that landing it later is
623/// a fast-forward to exactly what was tested. The branch it merges into
624/// does not move. Returns `Outcome<QueueStep>`.
625#[derive(Debug, Serialize, Deserialize)]
626#[serde(rename_all = "camelCase")]
627pub struct BuildQueueStepArgs {
628 /// The repository the queue lands on.
629 pub target_id: String,
630 /// The commit to build on.
631 pub onto: String,
632 /// Where the entry's change is: its fork, or the target itself.
633 pub source_id: String,
634 pub branch: String,
635 /// The entry's head as it was queued; refused if the branch moved.
636 pub commit: String,
637 pub method: crate::rules::MergeMethod,
638 /// For `merge` and `squash`: the commit's message. Required for those.
639 #[serde(default, skip_serializing_if = "Option::is_none")]
640 pub message: Option<String>,
641 /// For `squash`: who the commit is by; `actor` when absent.
642 #[serde(default, skip_serializing_if = "Option::is_none")]
643 pub author: Option<User>,
644 #[serde(default)]
645 pub sign_off: bool,
646 /// Whoever queued it: the committer, who must be able to merge.
647 pub actor: User,
648 /// The queue's branch of the target to point at the result.
649 pub push_branch: String,
650}
651
652/// What `build_queue_step` made.
653#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
654#[serde(tag = "kind", rename_all = "snake_case")]
655pub enum QueueStep {
656 /// The state with the entry in it, now at `push_branch`: `onto` itself
657 /// when it held the entry's changes already.
658 Built {
659 commit: String,
660 /// For `rebase`: each commit made, with the one it was made from.
661 #[serde(default)]
662 rebased: Vec<(String, String)>,
663 },
664 /// Its change clashes with `onto` in these files.
665 Conflict { message: String, paths: Vec<String> },
666 /// It cannot be built there, and why: a fast-forward that does not
667 /// contain `onto`, or more than g1t makes itself.
668 Refused { message: String },
669}
670
671/// How a pull request's branch is brought up to date with its base
672/// (`update_pull_branch`).
673#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
674#[serde(rename_all = "snake_case")]
675pub enum UpdateMethod {
676 /// A merge commit on the branch joins the base's new commits.
677 #[default]
678 Merge,
679 /// The branch's commits are made again on the base's head, and the
680 /// branch is moved there (a force push).
681 Rebase,
682}
683
684#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
685#[serde(rename_all = "lowercase")]
686pub enum FileStatus {
687 Added,
688 Modified,
689 Deleted,
690}
691
692#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
693#[serde(rename_all = "lowercase")]
694pub enum LineKind {
695 /// Unchanged, shown for context.
696 Context,
697 Add,
698 Delete,
699}
700
701#[derive(Clone, Debug, Serialize, Deserialize)]
702pub struct DiffLine {
703 pub kind: LineKind,
704 /// Line number in the old file; absent for added lines.
705 pub old: Option<u32>,
706 /// Line number in the new file; absent for deleted lines.
707 pub new: Option<u32>,
708 pub text: String,
709}
710
711/// A run of changed lines with their surrounding context.
712#[derive(Clone, Debug, Serialize, Deserialize)]
713pub struct Hunk {
714 pub lines: Vec<DiffLine>,
715}
716
717#[derive(Clone, Debug, Serialize, Deserialize)]
718pub struct FileDiff {
719 pub path: String,
720 pub status: FileStatus,
721 pub additions: u32,
722 pub deletions: u32,
723 /// True when the file is binary or too large, so no lines are shown.
724 pub binary: bool,
725 pub hunks: Vec<Hunk>,
726 /// True when the comparison reached its limit on lines before this
727 /// file, so it is listed without its lines (and with no counts): ask
728 /// `compare` again with its path in `paths` to have them.
729 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
730 pub omitted: bool,
731}
732
733/// What changed between two commits.
734#[derive(Clone, Debug, Serialize, Deserialize)]
735pub struct Comparison {
736 /// Null when the head has no earlier commit to compare against.
737 pub base: Option<String>,
738 pub head: String,
739 pub files: Vec<FileDiff>,
740 /// True when more files changed than are listed. A listed file whose
741 /// lines were left out says so itself (`FileDiff::omitted`).
742 pub truncated: bool,
743}
744
745/// `compare`: what `head` changes relative to `base`.
746///
747/// `head` is a branch or a commit, and defaults to the default branch.
748/// With no `base`, a fork is compared against the point where it and the
749/// repository it came from last agreed; a branch against the point where it
750/// left the default branch; and the default branch against its head's
751/// parent. Returns `Outcome<Comparison>`.
752#[derive(Debug, Serialize, Deserialize)]
753#[serde(rename_all = "camelCase")]
754pub struct CompareArgs {
755 pub repo_id: String,
756 pub viewer: Viewer,
757 #[serde(default)]
758 pub base: Option<String>,
759 #[serde(default)]
760 pub head: Option<String>,
761 /// With no `base`: the branch whose shared point with the head it is
762 /// compared from, instead of the default branch. A pull request into
763 /// another branch is compared this way.
764 #[serde(default)]
765 pub base_branch: Option<String>,
766 /// Only these files, with their lines: how a file left out of a large
767 /// comparison is read on its own. At most 50.
768 #[serde(default)]
769 pub paths: Option<Vec<String>>,
770 /// With no `base`: a repository `repo_id` is a fork of (directly or
771 /// through other forks) whose branch (`base_branch`, else its default
772 /// branch) the head is compared from, as for a pull request from a
773 /// fork. Both must be readable by the viewer.
774 #[serde(default, skip_serializing_if = "Option::is_none")]
775 pub target_id: Option<String>,
776}
777
778/// Lines `start` to `end` of a file, inclusive and counted from 1, last
779/// changed by `commit`.
780#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
781pub struct BlameRange {
782 pub start: u32,
783 pub end: u32,
784 pub commit: String,
785}
786
787/// Who last changed each line of a file.
788#[derive(Clone, Debug, Serialize, Deserialize)]
789pub struct Blame {
790 /// The commit the file was read at.
791 pub head: String,
792 /// Every line, in order, in runs that share a commit.
793 pub ranges: Vec<BlameRange>,
794 /// The commits the ranges name, each once.
795 pub commits: Vec<Commit>,
796 /// True when the history was too long to read in full (too many
797 /// commits, or too long to read), so the oldest lines are given to the
798 /// oldest commit read that has them.
799 pub partial: bool,
800}
801
802/// `blame`: who last changed each line of `path` as of `ref` (the default
803/// branch if absent). Returns `Outcome<Blame>`; not found when the file is
804/// missing or is not text.
805#[derive(Debug, Serialize, Deserialize)]
806pub struct BlameArgs {
807 pub path: RepoPath,
808 pub viewer: Viewer,
809 #[serde(default, rename = "ref")]
810 pub git_ref: Option<String>,
811 #[serde(rename = "filePath")]
812 pub file_path: String,
813}
814
815/// A branch and the commit it points to.
816#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
817pub struct Branch {
818 pub name: String,
819 pub hash: String,
820}
821
822/// `last_commits`: which commit last changed each entry of a directory at
823/// `ref` (the default branch when absent). Returns `Outcome<LastCommits>`.
824#[derive(Debug, Serialize, Deserialize)]
825#[serde(rename_all = "camelCase")]
826pub struct LastCommitsArgs {
827 pub path: RepoPath,
828 pub viewer: Viewer,
829 #[serde(default, rename = "ref")]
830 pub git_ref: Option<String>,
831 #[serde(default)]
832 pub tree_path: String,
833 /// Answer within this many milliseconds with what was found; absent,
834 /// within 20 seconds. A walk that stops short keeps its progress, and
835 /// the next call goes on from it.
836 #[serde(default)]
837 pub budget_ms: Option<u64>,
838}
839
840/// `path_log`: the commits on the first-parent chain from `ref` (the
841/// default branch when absent) that changed `file_path`, a file or a
842/// directory, newest first, at most `limit`. Returns `Outcome<PathLog>`.
843#[derive(Debug, Serialize, Deserialize)]
844#[serde(rename_all = "camelCase")]
845pub struct PathLogArgs {
846 pub path: RepoPath,
847 pub viewer: Viewer,
848 #[serde(default, rename = "ref")]
849 pub git_ref: Option<String>,
850 pub file_path: String,
851 pub limit: u32,
852 /// Answer within this many milliseconds with what was found.
853 #[serde(default)]
854 pub budget_ms: Option<u64>,
855}
856
857/// A path's history: `complete` is false when the walk ran out of time or
858/// reads before the first commit and before `limit`.
859#[derive(Clone, Debug, Serialize, Deserialize)]
860#[serde(rename_all = "camelCase")]
861pub struct PathLog {
862 pub commits: Vec<Commit>,
863 pub complete: bool,
864}
865
866/// An entry of a directory and the commit that last changed it.
867#[derive(Clone, Debug, Serialize, Deserialize)]
868pub struct LastCommit {
869 pub name: String,
870 pub commit: Commit,
871}
872
873/// The entries' last commits. `complete` is false when the walk stopped
874/// (or the history ran out) before every entry was placed; those entries
875/// are left out, and a later call goes on placing them.
876#[derive(Clone, Debug, Serialize, Deserialize)]
877pub struct LastCommits {
878 pub entries: Vec<LastCommit>,
879 pub complete: bool,
880}
881
882/// `branch_drift`: how far each of `heads` (branch head commits) has moved
883/// from `base` (the default branch's head commit), and each one's head
884/// commit, in one call. Every answer is kept by the pair of hashes: neither
885/// history can change, so neither can it. Returns `Outcome<BranchDrifts>`.
886#[derive(Debug, Serialize, Deserialize)]
887#[serde(rename_all = "camelCase")]
888pub struct BranchDriftArgs {
889 pub path: RepoPath,
890 pub viewer: Viewer,
891 pub base: String,
892 pub heads: Vec<String>,
893}
894
895/// Commits a branch has that the default branch does not (`ahead`), and
896/// the other way round (`behind`), as `git rev-list --left-right --count`.
897#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
898pub struct Drift {
899 pub ahead: u32,
900 pub behind: u32,
901}
902
903/// One branch head's commit and drift. `drift` is absent when the two
904/// histories do not meet within what is read (or could not be read).
905#[derive(Clone, Debug, Serialize, Deserialize)]
906pub struct BranchDrift {
907 pub head: String,
908 pub commit: Option<Commit>,
909 pub drift: Option<Drift>,
910}
911
912/// `base`'s own commit, and each head's answer in the order asked.
913#[derive(Clone, Debug, Serialize, Deserialize)]
914pub struct BranchDrifts {
915 pub base: Option<Commit>,
916 pub branches: Vec<BranchDrift>,
917}
918
919/// `tags`: the repository's tags, newest commit first, each with the
920/// commit it names. Returns `Outcome<Vec<Tag>>`.
921#[derive(Debug, Serialize, Deserialize)]
922pub struct TagsArgs {
923 pub path: RepoPath,
924 pub viewer: Viewer,
925}
926
927/// A tag, and its commit when it could be read.
928#[derive(Clone, Debug, Serialize, Deserialize)]
929pub struct Tag {
930 pub name: String,
931 pub commit: Option<Commit>,
932}
933
934/// `branches`: the repository's branches, default branch first.
935/// Returns `Outcome<Vec<Branch>>`.
936#[derive(Debug, Serialize, Deserialize)]
937pub struct BranchesArgs {
938 pub path: RepoPath,
939 pub viewer: Viewer,
940}
941
942/// `behind`: whether the default branch of the repository a pull request
943/// would merge into has commits its source does not. For services that
944/// have already decided the caller may see the pull request; it reveals
945/// one bit. Returns `bool`.
946#[derive(Debug, Serialize, Deserialize)]
947#[serde(rename_all = "camelCase")]
948pub struct BehindArgs {
949 /// The pull request's fork, or the repository itself for a branch.
950 pub source_id: String,
951 /// The branch of the source. A fork is compared on its default branch.
952 #[serde(default)]
953 pub branch: Option<String>,
954 /// The branch of the target it would merge into; the default branch
955 /// when absent.
956 #[serde(default)]
957 pub target_branch: Option<String>,
958 /// The repository the pull request merges into, when its source is a
959 /// fork of it (`Repo::parent_id`, directly or through other forks)
960 /// rather than a working copy or the repository itself. Refused when
961 /// the source is not in its fork network.
962 #[serde(default, skip_serializing_if = "Option::is_none")]
963 pub target_id: Option<String>,
964}
965
966/// `divergence`: how a pull request's source and the default branch it
967/// would merge into have moved apart since they last agreed: the files each
968/// side changed. Takes `BehindArgs`. For services that have already decided
969/// the caller may see the pull request; it reveals paths, not contents.
970/// Returns `Option<Divergence>`, null when either side has no commits.
971#[derive(Clone, Debug, Default, Serialize, Deserialize)]
972#[serde(rename_all = "camelCase")]
973pub struct Divergence {
974 /// The source's commit.
975 pub head: String,
976 /// The default branch's commit.
977 pub base: String,
978 /// Where they last agreed, if that could be found.
979 pub merge_base: Option<String>,
980 /// Whether the default branch has commits the source does not.
981 pub behind: bool,
982 /// The files the source changed since the merge base.
983 pub ours: Vec<String>,
984 /// The files the default branch changed since the merge base. Empty
985 /// when it is not behind.
986 pub theirs: Vec<String>,
987 /// Whether either list was cut short.
988 pub truncated: bool,
989}
990
991/// `update_pull_branch`: brings a pull request's source up to date with the
992/// default branch it would merge into, without a sandbox, when that can be
993/// done safely: merges the default branch's head into the source's head and
994/// pushes the merge commit to the source's branch, as `actor`, only if the
995/// branch has not moved meanwhile. It applies only when the two sides
996/// changed different files since they last agreed; otherwise the answer is
997/// [`PullBranchUpdate::NeedsAgent`] and nothing is pushed. Refused unless
998/// `actor` may push to the source. Returns `Outcome<PullBranchUpdate>`.
999#[derive(Debug, Serialize, Deserialize)]
1000#[serde(rename_all = "camelCase")]
1001pub struct UpdatePullBranchArgs {
1002 /// The pull request's fork, or the repository itself for a branch.
1003 pub source_id: String,
1004 /// The branch of the source. A fork is updated on its default branch.
1005 #[serde(default)]
1006 pub branch: Option<String>,
1007 /// The pull request's number, to name it in the merge commit's message
1008 /// when its branch has the same name as the default branch.
1009 pub number: u32,
1010 /// Who asked: the merge commit's author and committer, and the pusher.
1011 pub actor: User,
1012 /// The branch of the target to merge in; its default branch when absent.
1013 #[serde(default)]
1014 pub target_branch: Option<String>,
1015 /// The repository the pull request merges into, when its source is a
1016 /// fork of it (`Repo::parent_id`, directly or through other forks)
1017 /// rather than a working copy or the repository itself. Refused when
1018 /// the source is not in its fork network.
1019 #[serde(default, skip_serializing_if = "Option::is_none")]
1020 pub target_id: Option<String>,
1021 /// Merge the base in (the default), or rebase the branch onto it.
1022 #[serde(default)]
1023 pub method: UpdateMethod,
1024 /// Refuse if the branch no longer points here.
1025 #[serde(default, skip_serializing_if = "Option::is_none")]
1026 pub expected_head: Option<String>,
1027}
1028
1029/// Why an update has to be left to a sandbox.
1030#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1031#[serde(rename_all = "snake_case")]
1032pub enum NeedsAgentReason {
1033 /// Both sides changed some of the same files; merging them needs a
1034 /// real merge, which may or may not conflict.
1035 Overlap,
1036 /// Merging is known to conflict.
1037 Conflicting,
1038 /// The update could not be worked out here, such as when the two sides
1039 /// share no history g1t can see, or the change is too large to list.
1040 Unsupported,
1041}
1042
1043/// What came of `update_pull_branch` (or the work service's `catch_up_pull`).
1044#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1045#[serde(tag = "outcome", rename_all = "snake_case")]
1046pub enum PullBranchUpdate {
1047 /// The merge commit was pushed: the branch moved from `previous` to
1048 /// `commit`.
1049 Updated { commit: String, previous: String },
1050 /// The source already holds the default branch's head.
1051 UpToDate { commit: String },
1052 /// Nothing was pushed; a sandbox has to merge it. `paths` are the
1053 /// files both sides changed, or that conflict, when known.
1054 NeedsAgent {
1055 reason: NeedsAgentReason,
1056 detail: String,
1057 paths: Vec<String>,
1058 },
1059}
1060
1061/// `pull_merge_ref`: points `refs/pull/<number>/head` in the repository a
1062/// pull request merges into at its head, fetching the head's objects from
1063/// the fork first when it comes from one, and `refs/pull/<number>/merge`
1064/// at the commit that merges that head into the base branch's tip, made
1065/// without a sandbox: what GitHub's `pull_request` runs build. For the
1066/// actions service, which has already decided the pull request's events
1067/// may start runs. Returns `PullMergeRef`.
1068#[derive(Debug, Serialize, Deserialize)]
1069#[serde(rename_all = "camelCase")]
1070pub struct PullMergeRefArgs {
1071 /// The repository the pull request merges into.
1072 pub repo_id: String,
1073 /// The pull request's fork, or the repository itself for a branch.
1074 pub source_id: String,
1075 /// The pull request's head commit.
1076 pub head: String,
1077 pub number: u32,
1078 /// The branch it merges into; the default branch when absent.
1079 #[serde(default)]
1080 pub target_branch: Option<String>,
1081}
1082
1083/// What came of `pull_merge_ref`.
1084#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1085#[serde(tag = "outcome", rename_all = "snake_case")]
1086pub enum PullMergeRef {
1087 /// `refs/pull/<number>/merge` is `merge`, whose parents are `base`
1088 /// (the base branch's tip) and `head`.
1089 Merged { head: String, base: String, merge: String },
1090 /// The head and the base branch change the same lines, or the same
1091 /// binary file, differently: there is no merge commit, and
1092 /// `refs/pull/<number>/merge` is removed, as on GitHub.
1093 Conflict { head: String, base: String, paths: Vec<String> },
1094 /// g1t could not make the merge commit itself (a file renamed on one
1095 /// side and changed on the other, a very large change, a history it
1096 /// could not follow), or either side has no commits: `reason` says why.
1097 Unavailable { reason: String },
1098}
1099
1100/// The ref a pull request's head is kept at in the repository it merges
1101/// into, as on GitHub.
1102pub fn pull_head_ref(number: u32) -> String {
1103 format!("refs/pull/{number}/head")
1104}
1105
1106/// The ref of a pull request's test merge commit, as on GitHub.
1107pub fn pull_merge_ref(number: u32) -> String {
1108 format!("refs/pull/{number}/merge")
1109}
1110
1111/// `head`: the commit a branch points to, or null. For services reacting
1112/// to a push, which have no viewer; it reveals nothing but a commit hash.
1113/// Returns `Option<String>`.
1114#[derive(Debug, Serialize, Deserialize)]
1115#[serde(rename_all = "camelCase")]
1116pub struct HeadArgs {
1117 pub repo_id: String,
1118 /// Empty for the repository's default branch.
1119 pub branch: String,
1120}
1121
1122/// Where g1t keeps branches of its own in a repository, such as the merge
1123/// queue's tested states. Only these can be removed with `delete_branch`.
1124pub const G1T_BRANCH_PREFIX: &str = "g1t-";
1125
1126/// `delete_branch`: removes a branch g1t made for itself once it is done
1127/// with it, never one of people's: the name must start with
1128/// [`G1T_BRANCH_PREFIX`], or `head` must name the commit it points to (a
1129/// dependency update's branch, which g1t pushed and whose pull request it
1130/// closed). Never the default branch. For services, which have no viewer.
1131/// Returns `Outcome<bool>`: whether there was such a branch.
1132#[derive(Debug, Serialize, Deserialize)]
1133#[serde(rename_all = "camelCase")]
1134pub struct DeleteBranchArgs {
1135 pub repo_id: String,
1136 pub branch: String,
1137 /// The commit the branch must still point to. A branch that moved
1138 /// since (someone pushed to it) is left alone.
1139 #[serde(default, skip_serializing_if = "Option::is_none")]
1140 pub head: Option<String>,
1141}
1142
1143/// `commit_file`: writes one file on a new branch made from the default
1144/// branch's head, as one commit by `actor`, without a sandbox. For a change
1145/// g1t proposes on someone's behalf, such as a starter workflow, which then
1146/// becomes a pull request. Refused unless `actor` may push, when the branch
1147/// already exists, or when the file is already there. Returns
1148/// `Outcome<CommittedFile>`.
1149#[derive(Debug, Serialize, Deserialize)]
1150#[serde(rename_all = "camelCase")]
1151pub struct CommitFileArgs {
1152 pub repo: RepoPath,
1153 pub actor: User,
1154 /// The new branch, which must not exist yet.
1155 pub branch: String,
1156 /// Where the file goes, such as `.g1t/workflows/ci.yml`.
1157 pub path: String,
1158 pub content: String,
1159 pub message: String,
1160 /// Add a `Signed-off-by:` trailer for `actor`: required where the
1161 /// repository asks web commits to be signed off.
1162 #[serde(default)]
1163 pub sign_off: bool,
1164}
1165
1166/// The commit `commit_file` made.
1167#[derive(Clone, Debug, Serialize, Deserialize)]
1168#[serde(rename_all = "camelCase")]
1169pub struct CommittedFile {
1170 pub branch: String,
1171 pub commit: String,
1172}
1173
1174/// `readable`: of these repository ids, the repositories the viewer may
1175/// read, as `get_by_id` decides; forks and unknown ids are left out. For
1176/// services that hold ids and must show only what the viewer could open.
1177/// At most [`MAX_READABLE`] ids are looked at. Returns `Vec<Repo>`.
1178#[derive(Debug, Serialize, Deserialize)]
1179pub struct ReadableArgs {
1180 pub ids: Vec<String>,
1181 pub viewer: Viewer,
1182}
1183
1184/// The most ids one `readable` call looks at.
1185pub const MAX_READABLE: usize = 500;
1186
1187/// `commit_days`: how many commits a person pushed each day (UTC) since
1188/// `since` (`YYYY-MM-DD`), to the default branch or `gh-pages` of
1189/// repositories the viewer may read (as `readable` decides), for the
1190/// contribution calendar. Credited to whoever pushed; at most 50 commits a
1191/// push, counted along the branch's first-parent line. Returns
1192/// `Vec<crate::work::ContributionDay>`, oldest first, days with none left out.
1193#[derive(Debug, Serialize, Deserialize)]
1194pub struct CommitDaysArgs {
1195 pub user_id: String,
1196 pub since: String,
1197 pub viewer: Viewer,
1198}
1199
1200/// `public_namespaces`: the workspaces in which this account made a public
1201/// repository, and so a public project, which anyone can see on its page.
1202/// Returns `Vec<String>` of workspace slugs.
1203#[derive(Debug, Serialize, Deserialize)]
1204#[serde(rename_all = "camelCase")]
1205pub struct PublicNamespacesArgs {
1206 pub owner_id: String,
1207}
1208
1209/// One file on a branch, or one a change touched: its path and blob.
1210#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1211pub struct FileEntry {
1212 pub path: String,
1213 /// The blob it holds now; null when the change deleted it.
1214 pub hash: Option<String>,
1215 /// `blob`, `exec` (executable) or `symlink` (the blob is where it
1216 /// points); absent for a deleted file.
1217 #[serde(default, skip_serializing_if = "Option::is_none")]
1218 pub kind: Option<EntryKind>,
1219}
1220
1221/// Files, and whether there were more than were listed.
1222#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1223#[serde(rename_all = "camelCase")]
1224pub struct FileList {
1225 /// The commit the files were read at; null for an empty repository.
1226 pub commit: Option<String>,
1227 /// When that commit was made (RFC 3339), what an archive dates its
1228 /// files by.
1229 #[serde(default, skip_serializing_if = "Option::is_none")]
1230 pub date: Option<String>,
1231 pub files: Vec<FileEntry>,
1232 pub truncated: bool,
1233}
1234
1235/// `list_files`: every file on a branch (the default branch when absent),
1236/// path order by level, never descending into a directory named in
1237/// `skip_dirs`. For services that index a repository; no viewer, since it
1238/// is only reached by g1t's own services. Returns `FileList`.
1239#[derive(Debug, Default, Serialize, Deserialize)]
1240#[serde(rename_all = "camelCase")]
1241pub struct ListFilesArgs {
1242 pub repo_id: String,
1243 #[serde(default, rename = "ref")]
1244 pub git_ref: Option<String>,
1245 #[serde(default)]
1246 pub skip_dirs: Vec<String>,
1247 /// At most this many files; capped at [`MAX_LISTED_FILES`].
1248 pub limit: u32,
1249 /// Symlinks too, for an archive: by default only files whose text can
1250 /// be read are listed.
1251 #[serde(default)]
1252 pub links: bool,
1253}
1254
1255/// `changed_files`: the files that differ between two commits, as
1256/// `list_files` reads them. With no `base`, every file at `head`. Returns
1257/// `FileList`.
1258#[derive(Debug, Default, Serialize, Deserialize)]
1259#[serde(rename_all = "camelCase")]
1260pub struct ChangedFilesArgs {
1261 pub repo_id: String,
1262 #[serde(default)]
1263 pub base: Option<String>,
1264 pub head: String,
1265 #[serde(default)]
1266 pub skip_dirs: Vec<String>,
1267 pub limit: u32,
1268}
1269
1270/// `archive_link`: a link to download a branch, tag or commit as an
1271/// archive, for someone who can read the repository: the site's
1272/// `/<owner>/<repo>/archive/<commit>.tar.gz` (or `.zip`) with `?token=`,
1273/// good for [`ARCHIVE_LINK_SECONDS`] without a session or an access token.
1274/// The default branch when `ref` is absent. Returns `Outcome<ArchiveLink>`;
1275/// refused as `invalid` where the repos service has no `REPOS_KEY`.
1276#[derive(Debug, Serialize, Deserialize)]
1277#[serde(rename_all = "camelCase")]
1278pub struct ArchiveLinkArgs {
1279 pub path: RepoPath,
1280 pub viewer: Viewer,
1281 #[serde(default, rename = "ref")]
1282 pub git_ref: Option<String>,
1283}
1284
1285/// How long an archive link lasts.
1286pub const ARCHIVE_LINK_SECONDS: u64 = 10 * 60;
1287
1288#[derive(Clone, Debug, Serialize, Deserialize)]
1289#[serde(rename_all = "camelCase")]
1290pub struct ArchiveLink {
1291 pub repo: Repo,
1292 /// The commit the ref named, which the link downloads.
1293 pub commit: String,
1294 /// What the link carries as `?token=`.
1295 pub token: String,
1296 /// RFC 3339.
1297 pub expires_at: String,
1298}
1299
1300/// `open_archive_token`: what an archive link's token lets anyone holding
1301/// it download, when it is for the repository at `path` and has not
1302/// expired. Returns `Option<ArchiveGrant>`. No viewer: the token is the
1303/// permission.
1304#[derive(Debug, Serialize, Deserialize)]
1305#[serde(rename_all = "camelCase")]
1306pub struct OpenArchiveTokenArgs {
1307 pub path: RepoPath,
1308 pub token: String,
1309}
1310
1311#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1312#[serde(rename_all = "camelCase")]
1313pub struct ArchiveGrant {
1314 pub repo_id: String,
1315 /// The repository's workspace and name as they are now.
1316 pub namespace: String,
1317 pub name: String,
1318 pub commit: String,
1319}
1320
1321/// The most files one `list_files` or `changed_files` call lists.
1322pub const MAX_LISTED_FILES: u32 = 10_000;
1323
1324/// `read_blobs`: the text of these blobs of a repository, for services
1325/// that index it. A blob larger than `max_bytes`, or binary, comes back
1326/// with no text. Returns `Vec<BlobText>`, in the order asked.
1327#[derive(Debug, Default, Serialize, Deserialize)]
1328#[serde(rename_all = "camelCase")]
1329pub struct ReadBlobsArgs {
1330 pub repo_id: String,
1331 pub hashes: Vec<String>,
1332 pub max_bytes: u32,
1333}
1334
1335/// The most blobs one `read_blobs` call reads.
1336pub const MAX_READ_BLOBS: usize = 100;
1337
1338/// `refs`: a repository's branches and tags with the commit each points to
1339/// (annotated tags peeled), for services that follow them, such as the
1340/// packages service's Composer registry. No viewer: g1t's own services
1341/// only. Returns `Option<RepoRefs>`, null for a fork or an unknown id.
1342#[derive(Debug, Default, Serialize, Deserialize)]
1343#[serde(rename_all = "camelCase")]
1344pub struct RefsArgs {
1345 pub repo_id: String,
1346}
1347
1348#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1349pub struct GitRefEntry {
1350 /// The full ref: `refs/heads/main`, `refs/tags/v1.0.0`.
1351 pub name: String,
1352 pub commit: String,
1353}
1354
1355#[derive(Clone, Debug, Serialize, Deserialize)]
1356pub struct RepoRefs {
1357 pub repo: Repo,
1358 pub refs: Vec<GitRefEntry>,
1359}
1360
1361/// `raw_file`: one file's bytes at a ref or commit, base64, for g1t's own
1362/// services (no viewer). Returns `Option<RawFile>`: null when the file is
1363/// missing or larger than `max_bytes`.
1364#[derive(Debug, Default, Serialize, Deserialize)]
1365#[serde(rename_all = "camelCase")]
1366pub struct RawFileArgs {
1367 pub repo_id: String,
1368 #[serde(rename = "ref")]
1369 pub git_ref: String,
1370 pub path: String,
1371 pub max_bytes: u32,
1372}
1373
1374#[derive(Clone, Debug, Serialize, Deserialize)]
1375pub struct RawFile {
1376 pub size: u64,
1377 /// Standard base64.
1378 pub data: String,
1379}
1380
1381/// `raw_blobs`: blobs' bytes, base64, in the order asked, at most
1382/// [`MAX_READ_BLOBS`]; `data` is null for one missing or larger than
1383/// `max_bytes`. For g1t's own services. Returns `Vec<RawBlob>`.
1384#[derive(Debug, Default, Serialize, Deserialize)]
1385#[serde(rename_all = "camelCase")]
1386pub struct RawBlobsArgs {
1387 pub repo_id: String,
1388 pub hashes: Vec<String>,
1389 pub max_bytes: u32,
1390}
1391
1392#[derive(Clone, Debug, Serialize, Deserialize)]
1393pub struct RawBlob {
1394 pub hash: String,
1395 pub size: u64,
1396 pub data: Option<String>,
1397}
1398
1399#[derive(Clone, Debug, Serialize, Deserialize)]
1400pub struct BlobText {
1401 pub hash: String,
1402 pub size: u64,
1403 /// Null when the blob is missing, binary or larger than asked.
1404 pub text: Option<String>,
1405}
1406
1407/// `all_ids`: every repository that is not a fork, by id, a page at a
1408/// time, for services that index all of them. Returns `IdPage`.
1409#[derive(Debug, Default, Serialize, Deserialize)]
1410pub struct AllIdsArgs {
1411 /// Ids after this one.
1412 #[serde(default)]
1413 pub after: Option<String>,
1414 pub limit: u32,
1415}
1416
1417#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1418pub struct IdPage {
1419 pub ids: Vec<String>,
1420 /// Where the next page starts; null on the last.
1421 pub next: Option<String>,
1422}
1423
1424/// `repo_creators` takes [`AllIdsArgs`]: every repository that is not a
1425/// fork, with the account that created it, a page at a time, for identity
1426/// giving creators the Admin role. Returns [`CreatorPage`].
1427#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1428pub struct CreatorPage {
1429 pub repos: Vec<RepoCreator>,
1430 /// Where the next page starts; null on the last.
1431 pub next: Option<String>,
1432}
1433
1434#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1435pub struct RepoCreator {
1436 pub id: String,
1437 /// Its workspace's slug.
1438 pub namespace: String,
1439 pub name: String,
1440 /// The account that created it.
1441 pub owner_id: String,
1442}
1443
1444/// `visibility`: which of these repositories (`namespace/name`) are
1445/// private, for billing, which pays for work on public ones from g1t's
1446/// open-source pool. A pull request's working copy answers as the
1447/// repository it is a copy of. Unknown paths are left out. Returns
1448/// `Vec<RepoVisibility>`.
1449#[derive(Debug, Default, Serialize, Deserialize)]
1450pub struct VisibilityArgs {
1451 pub paths: Vec<String>,
1452}
1453
1454#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1455pub struct RepoVisibility {
1456 pub path: String,
1457 pub is_private: bool,
1458}
1459
1460/// `git_operations`: how many git operations (clones, fetches and pushes
1461/// through g1t's git endpoints) each workspace's repositories had in a
1462/// month, for billing's git meter. Cloudflare Artifacts charges per
1463/// operation from 2026-10-14. Pushes from agents' sandboxes go to the
1464/// store directly and are not counted here. Returns
1465/// `Vec<WorkspaceGitOperations>`.
1466#[derive(Debug, Serialize, Deserialize)]
1467pub struct GitOperationsArgs {
1468 /// YYYY-MM.
1469 pub month: String,
1470 /// Count only from this hour on, `YYYY-MM-DDTHH`, such as the day the
1471 /// provider starts charging.
1472 #[serde(default)]
1473 pub since: Option<String>,
1474 /// One workspace only; every workspace with any when absent.
1475 #[serde(default)]
1476 pub namespace: Option<String>,
1477}
1478
1479#[derive(Clone, Debug, Serialize, Deserialize)]
1480pub struct WorkspaceGitOperations {
1481 pub namespace: String,
1482 pub operations: u64,
1483}
1484
1485/// `storage`: what each workspace's private repositories hold, as far as
1486/// g1t can measure it, for billing's daily storage meter. Returns
1487/// `Vec<WorkspaceStorage>`.
1488///
1489/// The git store does not report a repository's size. What is counted is
1490/// the bytes of every pack pushed through g1t's git endpoints to the
1491/// repository or to its pull requests' working copies. Pushes made from
1492/// agents' sandboxes, which go to the store directly, and imports are not
1493/// counted, so it is a lower bound on what is stored.
1494#[derive(Debug, Default, Serialize, Deserialize)]
1495pub struct StorageArgs {}
1496
1497#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1498pub struct WorkspaceStorage {
1499 pub namespace: String,
1500 pub private_bytes: i64,
1501 pub public_bytes: i64,
1502}
1503
1504/// `transfer`: moves a repository to another workspace, keeping its name,
1505/// its id and everything kept under it. The actor must own both
1506/// workspaces. The old path keeps working as a redirect (see
1507/// `resolve_path`) until a repository is made there. Publishes
1508/// `repo.transferred`. Returns `Outcome<Repo>`, the repository at its new
1509/// path.
1510#[derive(Debug, Serialize, Deserialize)]
1511#[serde(rename_all = "camelCase")]
1512pub struct TransferArgs {
1513 pub actor: User,
1514 pub path: RepoPath,
1515 /// The destination workspace's slug.
1516 pub to: String,
1517 /// Where the request came in, for the audit log; g1t.sh when absent.
1518 #[serde(default)]
1519 pub surface: Option<crate::audit::Surface>,
1520}
1521
1522/// `resolve_path`: where a repository that was transferred away from
1523/// `path` is now, while nothing else is there. Returns `Option<RepoPath>`:
1524/// null when `path` is a repository, or never was one that moved. Callers
1525/// check the viewer may see the repository at its new path, as for any
1526/// other.
1527#[derive(Debug, Serialize, Deserialize)]
1528pub struct ResolvePathArgs {
1529 pub path: RepoPath,
1530}
1531
1532/// `namespace_count`: how many repositories (not pull request working
1533/// copies) a workspace holds, private or not, for deciding whether it can
1534/// be deleted. Returns `u32`.
1535#[derive(Debug, Serialize, Deserialize)]
1536pub struct NamespaceCountArgs {
1537 pub namespace: String,
1538}
1539
1540/// `delete`: deletes a repository. Owners of its workspace only, who type
1541/// its full name (`namespace/name`) as `confirm`. It is hidden at once,
1542/// git refuses it, and nothing runs for it; it can be restored for
1543/// [`RESTORE_DAYS`] days, then it is purged, its git data with it. Its
1544/// name stays taken until then, or until it is purged sooner from the
1545/// workspace's Recently deleted list. Publishes `repo.deleted`. Returns
1546/// `Outcome<DeletedRepo>`.
1547#[derive(Debug, Serialize, Deserialize)]
1548#[serde(rename_all = "camelCase")]
1549pub struct DeleteArgs {
1550 pub actor: User,
1551 pub path: RepoPath,
1552 #[serde(default)]
1553 pub confirm: String,
1554 #[serde(default)]
1555 pub surface: Option<crate::audit::Surface>,
1556}
1557
1558/// `deleted`: a workspace's recently deleted repositories, newest first.
1559/// Owners only; empty for anyone else. Returns `Vec<DeletedRepo>`.
1560#[derive(Debug, Serialize, Deserialize)]
1561pub struct DeletedArgs {
1562 pub viewer: Viewer,
1563 pub namespace: String,
1564}
1565
1566/// `restore` and `purge`: a deleted repository, by the path it had.
1567/// `restore` brings it back as it was, at that path (`repo.restored`).
1568/// `purge` removes it for good now, its git data with it, and frees its
1569/// name (`repo.purged`); it takes the full name typed as `confirm`.
1570/// Owners only. Return `Outcome<Repo>` and `Outcome<bool>`.
1571#[derive(Debug, Serialize, Deserialize)]
1572#[serde(rename_all = "camelCase")]
1573pub struct DeletedRepoArgs {
1574 pub actor: User,
1575 pub path: RepoPath,
1576 #[serde(default)]
1577 pub confirm: Option<String>,
1578 #[serde(default)]
1579 pub surface: Option<crate::audit::Surface>,
1580}
1581
1582/// `purge_due`: purges deleted repositories whose time has passed, at
1583/// most `limit` (25 when absent). The service's own schedule runs it.
1584/// Returns `u32`, how many were purged.
1585#[derive(Debug, Default, Serialize, Deserialize)]
1586pub struct PurgeDueArgs {
1587 #[serde(default)]
1588 pub limit: Option<u32>,
1589}
1590
1591/// `rename`: gives a repository a new name in its workspace, keeping its
1592/// id, its git data and everything kept under it. Owners only. The old
1593/// path keeps redirecting, as after a transfer, until a repository is made
1594/// there. Publishes `repo.renamed`. Returns `Outcome<Repo>`.
1595#[derive(Debug, Serialize, Deserialize)]
1596#[serde(rename_all = "camelCase")]
1597pub struct RenameArgs {
1598 pub actor: User,
1599 pub path: RepoPath,
1600 pub name: String,
1601 #[serde(default)]
1602 pub surface: Option<crate::audit::Surface>,
1603}
1604
1605/// `archive`: makes a repository read-only (`archived: true`), or writable
1606/// again. Owners only. While archived, pushes and merges are refused,
1607/// issues and pull requests are locked, and agents and workflows do not
1608/// run; deployments keep serving. Publishes `repo.archived` or
1609/// `repo.unarchived`. Returns `Outcome<Repo>`.
1610#[derive(Debug, Serialize, Deserialize)]
1611#[serde(rename_all = "camelCase")]
1612pub struct ArchiveArgs {
1613 pub actor: User,
1614 pub path: RepoPath,
1615 pub archived: bool,
1616 #[serde(default)]
1617 pub surface: Option<crate::audit::Surface>,
1618}
1619
1620/// `set_visibility`: makes a repository public or private. Owners only,
1621/// who type its full name as `confirm`. A free workspace takes a private
1622/// repository only while its private storage has room. Publishes
1623/// `repo.updated` and `repo.visibility_changed`. Returns `Outcome<Repo>`.
1624#[derive(Debug, Serialize, Deserialize)]
1625#[serde(rename_all = "camelCase")]
1626pub struct SetVisibilityArgs {
1627 pub actor: User,
1628 pub path: RepoPath,
1629 pub is_private: bool,
1630 #[serde(default)]
1631 pub confirm: String,
1632 #[serde(default)]
1633 pub surface: Option<crate::audit::Surface>,
1634}
1635
1636/// `set_default_branch`: makes another existing branch the one everything
1637/// lands on. Members of its workspace. Open pull requests then merge into
1638/// it. Publishes `repo.default_branch_changed`. Returns `Outcome<Repo>`.
1639#[derive(Debug, Serialize, Deserialize)]
1640#[serde(rename_all = "camelCase")]
1641pub struct SetDefaultBranchArgs {
1642 pub actor: User,
1643 pub path: RepoPath,
1644 pub branch: String,
1645 #[serde(default)]
1646 pub surface: Option<crate::audit::Surface>,
1647}
1648
1649/// `rename_branch`: renames a branch. Members of its workspace; only an
1650/// owner renames the default branch, which stays the default. Pull
1651/// requests from it follow, and web addresses naming the old branch
1652/// redirect until a branch of that name is made again. Publishes
1653/// `branch.renamed` (and `repo.default_branch_changed` for the default).
1654/// Returns `Outcome<Repo>`.
1655#[derive(Debug, Serialize, Deserialize)]
1656#[serde(rename_all = "camelCase")]
1657pub struct RenameBranchArgs {
1658 pub actor: User,
1659 pub path: RepoPath,
1660 pub from: String,
1661 pub to: String,
1662 #[serde(default)]
1663 pub surface: Option<crate::audit::Surface>,
1664}
1665
1666/// `create_branch`: makes a branch named `name` at `from`: another
1667/// branch, a tag or a commit (the default branch's head when empty). The
1668/// actor needs to be able to push, and the rules for creating the branch
1669/// hold. Restoring a deleted branch is making it again at the commit
1670/// `remove_branch` answered with. Publishes `git.push`. Returns
1671/// `Outcome<Branch>`.
1672#[derive(Debug, Serialize, Deserialize)]
1673#[serde(rename_all = "camelCase")]
1674pub struct CreateBranchArgs {
1675 pub actor: User,
1676 pub path: RepoPath,
1677 pub name: String,
1678 #[serde(default)]
1679 pub from: String,
1680 #[serde(default)]
1681 pub surface: Option<crate::audit::Surface>,
1682}
1683
1684/// `remove_branch`: deletes a branch for someone who can push, never the
1685/// default branch; the rules for deleting it hold. `head`, when given, is
1686/// the commit it must still point to: a branch that moved since is left
1687/// alone. Publishes `git.deleted`. Returns `Outcome<Branch>`: the branch
1688/// as it was, so it can be made again at the same commit with
1689/// `create_branch`. (`delete_branch` is the services' own, with no actor.)
1690#[derive(Debug, Serialize, Deserialize)]
1691#[serde(rename_all = "camelCase")]
1692pub struct RemoveBranchArgs {
1693 pub actor: User,
1694 pub path: RepoPath,
1695 pub branch: String,
1696 #[serde(default, skip_serializing_if = "Option::is_none")]
1697 pub head: Option<String>,
1698 #[serde(default)]
1699 pub surface: Option<crate::audit::Surface>,
1700}
1701
1702/// `resolve_branch`: what a branch renamed away from `branch` is called
1703/// now, for web addresses that name the old one; null when `branch` was
1704/// never renamed or exists again. Returns `Option<String>`.
1705#[derive(Debug, Serialize, Deserialize)]
1706#[serde(rename_all = "camelCase")]
1707pub struct ResolveBranchArgs {
1708 pub repo_id: String,
1709 pub branch: String,
1710}
1711
1712/// `status_by_id`: whether a repository is archived or deleted, for g1t's
1713/// own services deciding whether to act on it. An unknown id answers as deleted. Returns `RepoStatus`.
1714#[derive(Debug, Serialize, Deserialize)]
1715pub struct StatusByIdArgs {
1716 pub id: String,
1717}
1718
1719#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1720pub struct RepoStatus {
1721 pub archived: bool,
1722 pub deleted: bool,
1723}
1724
1725impl RepoStatus {
1726 /// Whether work may start on it: neither archived nor deleted.
1727 pub fn active(&self) -> bool {
1728 !self.archived && !self.deleted
1729 }
1730}
1731
1732/// What a person is told when something would change an archived
1733/// repository.
1734pub fn archived_message(namespace: &str, name: &str) -> String {
1735 format!(
1736 "{namespace}/{name} is archived, so it is read-only. An owner can unarchive it in its settings."
1737 )
1738}
1739
1740/// The path a repository was transferred from, and when, as `transfer`
1741/// keeps it so old addresses redirect.
1742#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1743#[serde(rename_all = "camelCase")]
1744pub struct RepoRedirect {
1745 pub from: RepoPath,
1746 pub repo_id: String,
1747 /// RFC 3339.
1748 pub created_at: String,
1749}
1750
1751#[cfg(test)]
1752mod topic_tests {
1753 use super::*;
1754
1755 fn topics(list: &[&str]) -> Result<Vec<String>, String> {
1756 clean_topics(&list.iter().map(|t| t.to_string()).collect::<Vec<_>>())
1757 }
1758
1759 #[test]
1760 fn topics_are_tidied() {
1761 assert_eq!(topics(&["Rust", " web_server ", "rust", ""]).unwrap(), vec!["rust", "web-server"]);
1762 }
1763
1764 #[test]
1765 fn websites_are_tidied() {
1766 assert_eq!(clean_website(" example.com ").unwrap().as_deref(), Some("https://example.com"));
1767 assert_eq!(clean_website("http://a.io/x").unwrap().as_deref(), Some("http://a.io/x"));
1768 assert_eq!(clean_website("").unwrap(), None);
1769 assert!(clean_website("ftp://a.io").is_err());
1770 assert!(clean_website("localhost").is_err());
1771 assert!(clean_website("https://a b.io").is_err());
1772 }
1773
1774 #[test]
1775 fn branch_names_follow_git() {
1776 for good in ["main", "trunk", "release/1.2", "feat-x_y"] {
1777 assert!(is_valid_branch_name(good), "{good}");
1778 }
1779 for bad in ["", "-x", "a..b", "a b", "x.lock", "a/", ".hidden", "a/.b", "g1t-queue", "a~1", "a:b", "@"] {
1780 assert!(!is_valid_branch_name(bad), "{bad}");
1781 }
1782 }
1783
1784 #[test]
1785 fn odd_topics_are_refused() {
1786 assert!(topics(&["c++"]).is_err());
1787 assert!(topics(&["-lead"]).is_err());
1788 assert!(topics(&[&"a".repeat(36)]).is_err());
1789 let many: Vec<String> = (0..21).map(|i| format!("t{i}")).collect();
1790 assert!(clean_topics(&many).is_err());
1791 }
1792}
1793
1794#[cfg(test)]
1795mod tests {
1796 use super::*;
1797
1798 #[test]
1799 fn a_pull_branch_update_reads_as_the_web_expects() {
1800 let update = PullBranchUpdate::NeedsAgent {
1801 reason: NeedsAgentReason::Overlap,
1802 detail: "both".into(),
1803 paths: vec!["a.rs".into()],
1804 };
1805 assert_eq!(
1806 serde_json::to_value(&update).unwrap(),
1807 serde_json::json!({ "outcome": "needs_agent", "reason": "overlap", "detail": "both", "paths": ["a.rs"] })
1808 );
1809 let done = PullBranchUpdate::UpToDate { commit: "c".into() };
1810 assert_eq!(serde_json::to_value(&done).unwrap(), serde_json::json!({ "outcome": "up_to_date", "commit": "c" }));
1811 }
1812}