Skip to content
199 linesCodeBlameRaw

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 incidents1//! 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
10use std::collections::{BTreeSet, HashMap};
11
12use g1t_contracts::access::Capability;
13use g1t_contracts::audit::{AuditActor, NewAuditEntry, Surface};
14use g1t_contracts::credentials::Decision;
15use g1t_contracts::repos::{CommitFileArgs, CommittedFile, EntryKind, TreeEntry, is_valid_branch_name};
16use g1t_contracts::{FailureCode, Outcome};
17use g1t_kit::now_ms;
18use g1t_scan::pack::{ObjectKind, object_id, write_pack};
19use worker::Result;
20
21use crate::catch_up::{Change, Signature, ancestors, commit_object, merge_tree, read_dirs};
22use crate::registry::{can_write, store_key};
23use crate::store::{GitRepo, GitStore, Scope};
24use crate::{Repos, UNVERIFIED, land, not_found};
25
26/// The largest file this writes.
27const 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`.
31pub(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
40impl<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-mode55 if let Some((code, message)) = crate::lifecycle::read_only_refusal(&repo) {
Fast pages, required checks on the branch, self-hosted runners, honest incidents56 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 incidents63 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 table69 // 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 incidents73 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 bar128 // 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 merge132 // 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 incidents152 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 incidents180 Ok(Outcome::Ok(CommittedFile {
181 branch: a.branch,
182 commit: commit_id,
183 }))
184 }
185}
186
187#[cfg(test)]
188mod 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.