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