g1t/services/repos/src/store.rs

226 lines7,241 bytesCodeBlame
1//! 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
6use g1t_contracts::repos::{Commit, EntryKind, GitAccess, Signature, TreeEntry};
7use g1t_contracts::time::rfc3339;
8use 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>;
43 /// Newest first along the first-parent chain; empty for an unknown ref.
44 async fn log(&self, git_ref: &str, limit: u32) -> Result<Vec<Commit>>;
45 /// The parents of a commit, or `None` if the commit does not exist.
46 async fn parents(&self, commit_hash: &str) -> Result<Option<Vec<String>>>;
47 async fn read_tree(&self, tree_hash: &str) -> Result<Option<Vec<TreeEntry>>>;
48 async fn read_blob(&self, blob_hash: &str) -> Result<Option<Vec<u8>>>;
49 /// `None` when the ref or path does not resolve to a file.
50 async fn read_file(&self, git_ref: &str, path: &str) -> Result<Option<Vec<u8>>>;
51 /// Makes a copy-on-write copy of this repository under `target_key`.
52 async fn fork(&self, target_key: &str) -> Result<()>;
53}
54
55pub struct ArtifactsStore {
56 binding: JsValue,
57}
58
59impl ArtifactsStore {
60 pub fn new(env: &Env) -> Result<Self> {
61 Ok(Self {
62 binding: js::binding(env, "ARTIFACTS")?,
63 })
64 }
65}
66
67impl GitStore for ArtifactsStore {
68 type Repo = ArtifactsRepo;
69
70 async fn create(
71 &self,
72 key: &str,
73 description: Option<&str>,
74 default_branch: &str,
75 ) -> Result<()> {
76 let options = js::to_js(&serde_json::json!({
77 "description": description,
78 "setDefaultBranch": default_branch,
79 }))?;
80 match js::call(&self.binding, "create", &[key.into(), options]).await {
81 // Left behind by an earlier failed attempt; adopt it.
82 Err(thrown) if !thrown.is("ALREADY_EXISTS") => Err(thrown.into()),
83 _ => Ok(()),
84 }
85 }
86
87 async fn open(&self, key: &str) -> Result<ArtifactsRepo> {
88 Ok(ArtifactsRepo {
89 handle: js::call(&self.binding, "get", &[key.into()]).await?,
90 })
91 }
92}
93
94/// A handle to one Artifacts repository. It is an RPC stub, so it is
95/// released when dropped.
96pub struct ArtifactsRepo {
97 handle: JsValue,
98}
99
100impl Drop for ArtifactsRepo {
101 fn drop(&mut self) {
102 let symbol = js::get(&worker::js_sys::global(), "Symbol");
103 let dispose = js::get(&symbol, "dispose");
104 if let Ok(function) = Reflect::get(&self.handle, &dispose)
105 .and_then(|value| value.dyn_into::<worker::js_sys::Function>())
106 {
107 let _ = function.call0(&self.handle);
108 }
109 }
110}
111
112#[derive(Deserialize)]
113#[serde(rename_all = "camelCase")]
114struct RawCommit {
115 hash: String,
116 tree_hash: String,
117 message: String,
118 author: Signature,
119 parents: Vec<String>,
120 /// Seconds since the epoch.
121 authored_at: u64,
122}
123
124#[derive(Deserialize)]
125struct RawEntry {
126 name: String,
127 hash: String,
128 #[serde(rename = "type")]
129 kind: EntryKind,
130}
131
132#[derive(Deserialize)]
133struct RawInfo {
134 remote: String,
135}
136
137#[derive(Deserialize)]
138struct RawToken {
139 plaintext: String,
140}
141
142/// The bytes of a `Blob`, or `None` for null.
143async fn blob_bytes(blob: JsValue) -> Result<Option<Vec<u8>>> {
144 if blob.is_null() || blob.is_undefined() {
145 return Ok(None);
146 }
147 let buffer = js::call(&blob, "arrayBuffer", &[]).await?;
148 Ok(Some(Uint8Array::new(&buffer).to_vec()))
149}
150
151impl GitRepo for ArtifactsRepo {
152 async fn access(&self, scope: Scope) -> Result<GitAccess> {
153 let scope = match scope {
154 Scope::Read => "read",
155 Scope::Write => "write",
156 };
157 let info: RawInfo = js::from_js(&js::call(&self.handle, "info", &[]).await?)?;
158 let token: RawToken = js::from_js(
159 &js::call(
160 &self.handle,
161 "createToken",
162 &[scope.into(), TOKEN_TTL_SECONDS.into()],
163 )
164 .await?,
165 )?;
166 Ok(GitAccess {
167 remote: info.remote,
168 token: token.plaintext,
169 })
170 }
171
172 async fn log(&self, git_ref: &str, limit: u32) -> Result<Vec<Commit>> {
173 let options = js::to_js(&serde_json::json!({ "ref": git_ref, "limit": limit }))?;
174 let commits: Vec<RawCommit> =
175 js::from_js(&js::call(&self.handle, "log", &[options]).await?)?;
176 Ok(commits
177 .into_iter()
178 .map(|commit| Commit {
179 hash: commit.hash,
180 tree_hash: commit.tree_hash,
181 message: commit.message,
182 author: commit.author,
183 parents: commit.parents,
184 authored_at: rfc3339(commit.authored_at * 1000),
185 })
186 .collect())
187 }
188
189 async fn parents(&self, commit_hash: &str) -> Result<Option<Vec<String>>> {
190 let commit: Option<RawCommit> =
191 js::from_js(&js::call(&self.handle, "readCommit", &[commit_hash.into()]).await?)?;
192 Ok(commit.map(|commit| commit.parents))
193 }
194
195 async fn read_tree(&self, tree_hash: &str) -> Result<Option<Vec<TreeEntry>>> {
196 let entries: Option<Vec<RawEntry>> =
197 js::from_js(&js::call(&self.handle, "readTree", &[tree_hash.into()]).await?)?;
198 Ok(entries.map(|entries| {
199 entries
200 .into_iter()
201 .map(|entry| TreeEntry {
202 name: entry.name,
203 hash: entry.hash,
204 kind: entry.kind,
205 })
206 .collect()
207 }))
208 }
209
210 async fn read_blob(&self, blob_hash: &str) -> Result<Option<Vec<u8>>> {
211 blob_bytes(js::call(&self.handle, "readBlob", &[blob_hash.into()]).await?).await
212 }
213
214 async fn read_file(&self, git_ref: &str, path: &str) -> Result<Option<Vec<u8>>> {
215 let args = js::to_js(&serde_json::json!({ "ref": git_ref, "path": path }))?;
216 blob_bytes(js::call(&self.handle, "readFile", &[args]).await?).await
217 }
218
219 async fn fork(&self, target_key: &str) -> Result<()> {
220 let options = js::to_js(&serde_json::json!({ "defaultBranchOnly": true }))?;
221 match js::call(&self.handle, "fork", &[target_key.into(), options]).await {
222 Err(thrown) if !thrown.is("ALREADY_EXISTS") => Err(thrown.into()),
223 _ => Ok(()),
224 }
225 }
226}