Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 1 | //! One file, committed on a new branch without a sandbox: how g1t proposes |
| 2 | //! a change on someone's behalf, such as a starter workflow, which then | |
| 3 | //! becomes a pull request they can read, change and merge. | |
| 4 | //! | |
| 5 | //! The new tree is the default branch's head with the file put in place. | |
| 6 | //! Only the trees on the way to it are rewritten (as catching up does), and | |
| 7 | //! the blob, those trees and one commit by the person asking are pushed as | |
| 8 | //! a pack to a branch that must not exist yet. | |
| 9 | ||
| 10 | use std::collections::{BTreeSet, HashMap}; | |
| 11 | ||
| 12 | use g1t_contracts::access::Capability; | |
| 13 | use g1t_contracts::audit::{AuditActor, NewAuditEntry, Surface}; | |
| 14 | use g1t_contracts::credentials::Decision; | |
| 15 | use g1t_contracts::repos::{CommitFileArgs, CommittedFile, EntryKind, TreeEntry, is_valid_branch_name}; | |
| 16 | use g1t_contracts::{FailureCode, Outcome}; | |
| 17 | use g1t_kit::now_ms; | |
| 18 | use g1t_scan::pack::{ObjectKind, object_id, write_pack}; | |
| 19 | use worker::Result; | |
| 20 | ||
| 21 | use crate::catch_up::{Change, Signature, ancestors, commit_object, merge_tree, read_dirs}; | |
| 22 | use crate::registry::{can_write, store_key}; | |
| 23 | use crate::store::{GitRepo, GitStore, Scope}; | |
| 24 | use crate::{Repos, UNVERIFIED, land, not_found}; | |
| 25 | ||
| 26 | /// The largest file this writes. | |
| 27 | const MAX_CONTENT_BYTES: usize = 64 * 1024; | |
| 28 | ||
| 29 | /// Whether `path` is somewhere a file can be written: relative, without | |
| 30 | /// empty, `.` or `..` parts, and not inside `.git`. | |
| 31 | pub(crate) fn valid_path(path: &str) -> bool { | |
| 32 | !path.is_empty() | |
| 33 | && path.len() <= 400 | |
| 34 | && !path.starts_with('/') | |
| 35 | && !path.ends_with('/') | |
| 36 | && path.split('/').all(|part| !part.is_empty() && part != "." && part != ".." && part != ".git") | |
| 37 | && !path.chars().any(|c| c.is_control() || c == '\\') | |
| 38 | } | |
| 39 | ||
| 40 | impl<S: GitStore> Repos<S> { | |
| 41 | pub(crate) async fn commit_file(&self, a: CommitFileArgs) -> Result<Outcome<CommittedFile>> { | |
| 42 | let actor = Some(a.actor.clone()); | |
| 43 | let Some(repo) = self.readable(&a.repo, &actor).await? else { | |
| 44 | return Ok(not_found()); | |
| 45 | }; | |
| 46 | if !can_write(&repo, &actor) { | |
| 47 | return Ok(Outcome::fail( | |
| 48 | FailureCode::Forbidden, | |
| 49 | g1t_contracts::access::needs(Capability::Push, &format!("{}/{}", repo.namespace, repo.name)), | |
| 50 | )); | |
| 51 | } | |
| 52 | if !a.actor.verified { | |
| 53 | return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED)); | |
| 54 | } | |
| Merge branch 'mirroring' into artifacts-mode | 55 | if let Some((code, message)) = crate::lifecycle::read_only_refusal(&repo) { |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 56 | return Ok(Outcome::fail(code, message)); |
| 57 | } | |
| Merge branch 'worktree-agent-a2013627e5ea4ab13' | 58 | // Moving between namespaces: wait for it (moves.rs). |
| 59 | let repo = match self.unpaused(repo).await? { | |
| 60 | Ok(repo) => repo, | |
| 61 | Err((code, message)) => return Ok(Outcome::fail(code, message)), | |
| 62 | }; | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 63 | if !is_valid_branch_name(&a.branch) || a.branch == repo.default_branch { |
| 64 | return Ok(Outcome::fail(FailureCode::Invalid, format!("{} cannot be the new branch's name.", a.branch))); | |
| 65 | } | |
| 66 | if !valid_path(&a.path) { | |
| 67 | return Ok(Outcome::fail(FailureCode::Invalid, format!("{} is not a path a file can be written to.", a.path))); | |
| 68 | } | |
| Workflow files need workflow_files:write from a token; fine-grained permission table | 69 | // A workflow file, written for a token without the scope for it. |
| 70 | if let Some(refused) = g1t_contracts::scopes::decide_workflow_files(a.actor.token.as_deref(), [a.path.as_str()]) { | |
| 71 | return Ok(Outcome::fail(FailureCode::Forbidden, refused.reason.unwrap_or_default())); | |
| 72 | } | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 73 | if a.content.len() > MAX_CONTENT_BYTES { |
| 74 | return Ok(Outcome::fail(FailureCode::Invalid, "The file is too large to write this way.")); | |
| 75 | } | |
| 76 | let message = a.message.trim(); | |
| 77 | if message.is_empty() { | |
| 78 | return Ok(Outcome::fail(FailureCode::Invalid, "A commit needs a message.")); | |
| 79 | } | |
| 80 | ||
| 81 | let git = self.store.open(&store_key(&repo)).await?; | |
| 82 | if git.branches().await?.iter().any(|branch| branch.name == a.branch) { | |
| 83 | return Ok(Outcome::fail(FailureCode::Conflict, format!("A branch named {} already exists.", a.branch))); | |
| 84 | } | |
| 85 | let history = git.log(&repo.default_branch, 1).await?; | |
| 86 | let Some(head) = history.first() else { | |
| 87 | return Ok(Outcome::fail( | |
| 88 | FailureCode::Conflict, | |
| 89 | format!("{} has no commits yet. Push a first commit, then try again.", repo.default_branch), | |
| 90 | )); | |
| 91 | }; | |
| 92 | let dirs: BTreeSet<String> = ancestors(&a.path).map(str::to_owned).collect(); | |
| 93 | let read = read_dirs(&git, &head.tree_hash, &dirs).await?; | |
| 94 | let (parent, name) = a.path.rsplit_once('/').unwrap_or(("", a.path.as_str())); | |
| 95 | let exists = read | |
| 96 | .get(parent) | |
| 97 | .is_some_and(|(_, entries)| entries.iter().any(|entry| entry.name == name)); | |
| 98 | if exists { | |
| 99 | return Ok(Outcome::fail(FailureCode::Conflict, format!("{} already exists on {}.", a.path, repo.default_branch))); | |
| 100 | } | |
| 101 | ||
| 102 | let blob = a.content.clone().into_bytes(); | |
| 103 | let blob_id = object_id(ObjectKind::Blob, &blob); | |
| 104 | let trees: HashMap<String, Vec<TreeEntry>> = read.into_values().collect(); | |
| 105 | let change = Change { | |
| 106 | path: a.path.clone(), | |
| 107 | entry: Some((EntryKind::Blob, blob_id)), | |
| 108 | }; | |
| 109 | let merged = match merge_tree(&head.tree_hash, &trees, &[change]) { | |
| 110 | Ok(merged) => merged, | |
| 111 | Err(why) => { | |
| 112 | return Ok(Outcome::fail(FailureCode::Conflict, format!("g1t could not write {}: {why}.", a.path))); | |
| 113 | } | |
| 114 | }; | |
| 115 | ||
| 116 | let author = self.commit_identity(&a.actor).await; | |
| 117 | let commit = commit_object( | |
| 118 | &merged.tree, | |
| 119 | &[&head.hash], | |
| 120 | &Signature { | |
| 121 | name: &author.name, | |
| 122 | email: &author.email, | |
| 123 | seconds: now_ms() / 1000, | |
| 124 | }, | |
| 125 | message, | |
| 126 | ); | |
| 127 | let commit_id = object_id(ObjectKind::Commit, &commit); | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 128 | // Push protection, as for git: a secret nobody let through stops it. |
| 129 | if let Some(refusal) = self.protect_file(&repo, &a.actor, &a.path, a.content.as_bytes(), &commit_id).await { | |
| 130 | return Ok(Outcome::fail(FailureCode::Forbidden, refusal)); | |
| 131 | } | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 132 | // The rules of the new branch, as for a push of this commit. |
| 133 | let change = g1t_rules::push::RefChange { | |
| 134 | git_ref: format!("refs/heads/{}", a.branch), | |
| 135 | old: None, | |
| 136 | new: Some(commit_id.clone()), | |
| 137 | fast_forward: None, | |
| 138 | commits: vec![crate::rules::made_commit( | |
| 139 | &commit_id, | |
| 140 | message, | |
| 141 | &author.email, | |
| 142 | 1, | |
| 143 | vec![g1t_contracts::rules::FileChange { path: a.path.clone(), size: Some(blob.len() as u64), deleted: false }], | |
| 144 | )], | |
| 145 | complete: true, | |
| 146 | }; | |
| 147 | if let crate::rules::Ruled::Refused { message, .. } = | |
| 148 | self.check_changes(&repo, &a.actor, g1t_contracts::rules::Action::Commit, vec![change]).await? | |
| 149 | { | |
| 150 | return Ok(Outcome::fail(FailureCode::Forbidden, message)); | |
| 151 | } | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 152 | let mut objects: Vec<(ObjectKind, Vec<u8>)> = vec![(ObjectKind::Blob, blob)]; |
| 153 | objects.extend(merged.objects.into_iter().map(|bytes| (ObjectKind::Tree, bytes))); | |
| 154 | objects.push((ObjectKind::Commit, commit)); | |
| 155 | let access = git.access(Scope::Write).await?; | |
| 156 | // Only if the branch is still not there. | |
| 157 | let pushed = land::push_pack(&access, &a.branch, None, &commit_id, write_pack(&objects)).await?; | |
| 158 | self.refs_moved(&repo.id).await; | |
| 159 | ||
| 160 | let git_ref = format!("refs/heads/{}", a.branch); | |
| 161 | let mut target = self.audit_target(&a.repo).await?; | |
| 162 | target.git_ref = Some(git_ref.clone()); | |
| 163 | let mut entry = NewAuditEntry::new( | |
| 164 | AuditActor::of(&a.actor), | |
| 165 | "git.push", | |
| 166 | Surface::Git, | |
| 167 | target, | |
| 168 | &Decision::allow("person"), | |
| 169 | g1t_contracts::new_id("req", now_ms()), | |
| 170 | ); | |
| 171 | if let Err(reason) = pushed { | |
| 172 | entry.result = Some("conflict".to_owned()); | |
| 173 | entry.message = Some(reason.clone()); | |
| 174 | self.record_git(entry).await; | |
| 175 | return Ok(Outcome::fail(FailureCode::Conflict, format!("{} could not be created: {reason}", a.branch))); | |
| 176 | } | |
| 177 | entry.result = Some("ok".to_owned()); | |
| 178 | self.record_git(entry).await; | |
| Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales. | 179 | self.publish_push(&repo, &git_ref, None, &commit_id, Some(&a.actor), Some(1)).await?; |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 180 | Ok(Outcome::Ok(CommittedFile { |
| 181 | branch: a.branch, | |
| 182 | commit: commit_id, | |
| 183 | })) | |
| 184 | } | |
| 185 | } | |
| 186 | ||
| 187 | #[cfg(test)] | |
| 188 | mod tests { | |
| 189 | use super::valid_path; | |
| 190 | ||
| 191 | #[test] | |
| 192 | fn only_plain_relative_paths_are_written() { | |
| 193 | assert!(valid_path(".g1t/workflows/ci.yml")); | |
| 194 | assert!(valid_path("README.md")); | |
| 195 | for path in ["", "/etc/passwd", "a/../b", "a//b", ".git/config", "a/./b", "dir/", "a\\b"] { | |
| 196 | assert!(!valid_path(path), "{path}"); | |
| 197 | } | |
| 198 | } | |
| 199 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.