g1t/services/repos/src/store.rs

232 lines7,483 bytesCodeBlame

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.

Rust repos service with shipping; pull requests kept in the model1//! The storage that actually holds git repositories.
2//!
3//! The service depends on the [`GitStore`] and [`GitRepo`] ports;
4//! [`ArtifactsStore`] is the adapter for Cloudflare Artifacts.
5
Pull requests from branches6use g1t_contracts::repos::{Branch, Commit, EntryKind, GitAccess, Signature, TreeEntry};
RFC 3339 timestamps in identity and repos7use g1t_contracts::time::rfc3339;
Rust repos service with shipping; pull requests kept in the model8use g1t_kit::js;
9use serde::Deserialize;
10use worker::js_sys::{Reflect, Uint8Array};
11use worker::wasm_bindgen::{JsCast, JsValue};
12use worker::{Env, Result};
13
14/// How long a credential handed to git stays valid.
15const TOKEN_TTL_SECONDS: u32 = 300;
16
17#[derive(Clone, Copy)]
18pub enum Scope {
19 Read,
20 Write,
21}
22
23/// A place repositories live. `key` is the store's own name for a repo.
24#[allow(async_fn_in_trait)]
25pub trait GitStore {
26 type Repo: GitRepo;
27
28 /// Creates an empty repository. Succeeds if it already exists.
29 async fn create(
30 &self,
31 key: &str,
32 description: Option<&str>,
33 default_branch: &str,
34 ) -> Result<()>;
35 async fn open(&self, key: &str) -> Result<Self::Repo>;
36}
37
38/// One open repository.
39#[allow(async_fn_in_trait)]
40pub trait GitRepo {
41 /// A remote URL and short-lived credential for git itself.
42 async fn access(&self, scope: Scope) -> Result<GitAccess>;
Pull requests from branches43 /// Every branch and the commit it points to.
44 async fn branches(&self) -> Result<Vec<Branch>>;
Rust repos service with shipping; pull requests kept in the model45 /// Newest first along the first-parent chain; empty for an unknown ref.
46 async fn log(&self, git_ref: &str, limit: u32) -> Result<Vec<Commit>>;
47 /// The parents of a commit, or `None` if the commit does not exist.
48 async fn parents(&self, commit_hash: &str) -> Result<Option<Vec<String>>>;
49 async fn read_tree(&self, tree_hash: &str) -> Result<Option<Vec<TreeEntry>>>;
50 async fn read_blob(&self, blob_hash: &str) -> Result<Option<Vec<u8>>>;
51 /// `None` when the ref or path does not resolve to a file.
52 async fn read_file(&self, git_ref: &str, path: &str) -> Result<Option<Vec<u8>>>;
53 /// Makes a copy-on-write copy of this repository under `target_key`.
54 async fn fork(&self, target_key: &str) -> Result<()>;
55}
56
57pub struct ArtifactsStore {
58 binding: JsValue,
59}
60
61impl ArtifactsStore {
62 pub fn new(env: &Env) -> Result<Self> {
63 Ok(Self {
64 binding: js::binding(env, "ARTIFACTS")?,
65 })
66 }
67}
68
69impl GitStore for ArtifactsStore {
70 type Repo = ArtifactsRepo;
71
72 async fn create(
73 &self,
74 key: &str,
75 description: Option<&str>,
76 default_branch: &str,
77 ) -> Result<()> {
78 let options = js::to_js(&serde_json::json!({
79 "description": description,
80 "setDefaultBranch": default_branch,
81 }))?;
82 match js::call(&self.binding, "create", &[key.into(), options]).await {
83 // Left behind by an earlier failed attempt; adopt it.
84 Err(thrown) if !thrown.is("ALREADY_EXISTS") => Err(thrown.into()),
85 _ => Ok(()),
86 }
87 }
88
89 async fn open(&self, key: &str) -> Result<ArtifactsRepo> {
90 Ok(ArtifactsRepo {
91 handle: js::call(&self.binding, "get", &[key.into()]).await?,
92 })
93 }
94}
95
96/// A handle to one Artifacts repository. It is an RPC stub, so it is
97/// released when dropped.
98pub struct ArtifactsRepo {
99 handle: JsValue,
100}
101
102impl Drop for ArtifactsRepo {
103 fn drop(&mut self) {
104 let symbol = js::get(&worker::js_sys::global(), "Symbol");
105 let dispose = js::get(&symbol, "dispose");
106 if let Ok(function) = Reflect::get(&self.handle, &dispose)
107 .and_then(|value| value.dyn_into::<worker::js_sys::Function>())
108 {
109 let _ = function.call0(&self.handle);
110 }
111 }
112}
113
114#[derive(Deserialize)]
115#[serde(rename_all = "camelCase")]
116struct RawCommit {
117 hash: String,
118 tree_hash: String,
119 message: String,
120 author: Signature,
121 parents: Vec<String>,
122 /// Seconds since the epoch.
123 authored_at: u64,
124}
125
126#[derive(Deserialize)]
127struct RawEntry {
128 name: String,
129 hash: String,
130 #[serde(rename = "type")]
131 kind: EntryKind,
132}
133
134#[derive(Deserialize)]
135struct RawInfo {
136 remote: String,
137}
138
139#[derive(Deserialize)]
140struct RawToken {
141 plaintext: String,
142}
143
144/// The bytes of a `Blob`, or `None` for null.
145async fn blob_bytes(blob: JsValue) -> Result<Option<Vec<u8>>> {
146 if blob.is_null() || blob.is_undefined() {
147 return Ok(None);
148 }
149 let buffer = js::call(&blob, "arrayBuffer", &[]).await?;
150 Ok(Some(Uint8Array::new(&buffer).to_vec()))
151}
152
153impl GitRepo for ArtifactsRepo {
154 async fn access(&self, scope: Scope) -> Result<GitAccess> {
155 let scope = match scope {
156 Scope::Read => "read",
157 Scope::Write => "write",
158 };
159 let info: RawInfo = js::from_js(&js::call(&self.handle, "info", &[]).await?)?;
160 let token: RawToken = js::from_js(
161 &js::call(
162 &self.handle,
163 "createToken",
164 &[scope.into(), TOKEN_TTL_SECONDS.into()],
165 )
166 .await?,
167 )?;
168 Ok(GitAccess {
169 remote: info.remote,
170 token: token.plaintext,
171 })
172 }
173
Pull requests from branches174 async fn branches(&self) -> Result<Vec<Branch>> {
175 crate::refs::branches(&self.access(Scope::Read).await?).await
176 }
177
Rust repos service with shipping; pull requests kept in the model178 async fn log(&self, git_ref: &str, limit: u32) -> Result<Vec<Commit>> {
179 let options = js::to_js(&serde_json::json!({ "ref": git_ref, "limit": limit }))?;
180 let commits: Vec<RawCommit> =
181 js::from_js(&js::call(&self.handle, "log", &[options]).await?)?;
182 Ok(commits
183 .into_iter()
184 .map(|commit| Commit {
185 hash: commit.hash,
186 tree_hash: commit.tree_hash,
187 message: commit.message,
188 author: commit.author,
189 parents: commit.parents,
RFC 3339 timestamps in identity and repos190 authored_at: rfc3339(commit.authored_at * 1000),
Rust repos service with shipping; pull requests kept in the model191 })
192 .collect())
193 }
194
195 async fn parents(&self, commit_hash: &str) -> Result<Option<Vec<String>>> {
196 let commit: Option<RawCommit> =
197 js::from_js(&js::call(&self.handle, "readCommit", &[commit_hash.into()]).await?)?;
198 Ok(commit.map(|commit| commit.parents))
199 }
200
201 async fn read_tree(&self, tree_hash: &str) -> Result<Option<Vec<TreeEntry>>> {
202 let entries: Option<Vec<RawEntry>> =
203 js::from_js(&js::call(&self.handle, "readTree", &[tree_hash.into()]).await?)?;
204 Ok(entries.map(|entries| {
205 entries
206 .into_iter()
207 .map(|entry| TreeEntry {
208 name: entry.name,
209 hash: entry.hash,
210 kind: entry.kind,
211 })
212 .collect()
213 }))
214 }
215
216 async fn read_blob(&self, blob_hash: &str) -> Result<Option<Vec<u8>>> {
217 blob_bytes(js::call(&self.handle, "readBlob", &[blob_hash.into()]).await?).await
218 }
219
220 async fn read_file(&self, git_ref: &str, path: &str) -> Result<Option<Vec<u8>>> {
221 let args = js::to_js(&serde_json::json!({ "ref": git_ref, "path": path }))?;
222 blob_bytes(js::call(&self.handle, "readFile", &[args]).await?).await
223 }
224
225 async fn fork(&self, target_key: &str) -> Result<()> {
226 let options = js::to_js(&serde_json::json!({ "defaultBranchOnly": true }))?;
227 match js::call(&self.handle, "fork", &[target_key.into(), options]).await {
228 Err(thrown) if !thrown.is("ALREADY_EXISTS") => Err(thrown.into()),
229 _ => Ok(()),
230 }
231 }
232}