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
204 linesCodeBlameRaw
1//! `get_contents`: what a repository holds at a path, at a branch, tag or
2//! commit: a file's text, or a directory's entries with its README. What
3//! an agent (or Ask g1t) reads to learn how a project builds and tests,
4//! what a config says, or what a file does, without cloning it.
5//!
6//! The repos service reads it, as the caller may see it: `blob` for a
7//! file, `tree` for a directory. A path that is not a file is tried as a
8//! directory. The shaping is [`file`] and [`directory`], plain functions.
9
10use g1t_contracts::repos::{BlobArgs, BlobView, EntryKind, GetArgs, Repo, RepoPath, TreeArgs, TreeView};
11use g1t_contracts::{FailureCode, Outcome, Viewer};
12use serde_json::{Value, json};
13use worker::Result;
14
15use crate::operations::Services;
16
17/// The most of a file's text one answer carries; more is cut with a note.
18pub const MAX_TEXT_CHARS: usize = 200_000;
19/// The most entries of a directory one answer lists.
20pub const MAX_ENTRIES: usize = 1_000;
21
22pub fn description() -> &'static str {
23 "What a repository holds at a path, at `ref` (a branch, tag or commit; the default branch when left out). A file: its `text` (null for a binary file or one too large to show; at most 200,000 characters, the rest cut with a note), `size` in bytes and `html_url`. A directory (or the root, with no `path`): its `entries` (each `name`, `path` and `type`: dir, file, symlink or submodule) and its README's `text` when it has one. Read the root first to find README, CONTRIBUTING, package.json, Cargo.toml or a workflow under .g1t/workflows, then the files you need."
24}
25
26pub fn input() -> Value {
27 json!({
28 "type": "object",
29 "properties": {
30 "repo": { "type": "string", "description": "Repository as \"owner/name\", e.g. \"flagon-io/hello\"." },
31 "path": { "type": "string", "description": "A file or directory, such as README.md or src/lib; the root when left out." },
32 "ref": { "type": "string", "description": "A branch, tag or commit; the default branch when left out." },
33 },
34 "required": ["repo"],
35 })
36}
37
38fn kind(entry: EntryKind) -> &'static str {
39 match entry {
40 EntryKind::Tree => "dir",
41 EntryKind::Blob | EntryKind::Exec => "file",
42 EntryKind::Symlink => "symlink",
43 EntryKind::Gitlink => "submodule",
44 }
45}
46
47fn full_name(repo: &Repo) -> String {
48 format!("{}/{}", repo.namespace, repo.name)
49}
50
51/// A file's answer.
52pub fn file(site: &str, view: &BlobView) -> Value {
53 let site = site.trim_end_matches('/');
54 let text = view.text.as_ref().map(|text| {
55 let total = text.chars().count();
56 if total <= MAX_TEXT_CHARS {
57 text.clone()
58 } else {
59 let kept: String = text.chars().take(MAX_TEXT_CHARS).collect();
60 format!("{kept}\n[Cut: {} more characters.]", total - MAX_TEXT_CHARS)
61 }
62 });
63 json!({
64 "type": "file",
65 "repo": full_name(&view.repo),
66 "ref": view.git_ref,
67 "path": view.path,
68 "size": view.size,
69 "binary": text.is_none(),
70 "text": text,
71 "html_url": format!("{site}/{}/blob/{}/{}", full_name(&view.repo), view.git_ref, view.path.trim_start_matches('/')),
72 })
73}
74
75/// A directory's answer.
76pub fn directory(site: &str, view: &TreeView) -> Value {
77 let site = site.trim_end_matches('/');
78 let base = view.path.trim_matches('/');
79 let entries: Vec<Value> = view
80 .entries
81 .iter()
82 .take(MAX_ENTRIES)
83 .map(|entry| {
84 let path = if base.is_empty() { entry.name.clone() } else { format!("{base}/{}", entry.name) };
85 json!({ "name": entry.name, "path": path, "type": kind(entry.kind) })
86 })
87 .collect();
88 let html_url = if base.is_empty() {
89 format!("{site}/{}", full_name(&view.repo))
90 } else {
91 format!("{site}/{}/tree/{}/{base}", full_name(&view.repo), view.git_ref)
92 };
93 json!({
94 "type": "dir",
95 "repo": full_name(&view.repo),
96 "ref": view.git_ref,
97 "path": base,
98 "entries": entries,
99 "truncated": view.entries.len() > MAX_ENTRIES,
100 "readme": view.readme.as_ref().map(|readme| json!({ "name": readme.name, "text": readme.text })),
101 "html_url": html_url,
102 })
103}
104
105pub async fn run(services: &Services, viewer: &Viewer, repo: &RepoPath, input: &Value) -> Result<Outcome<Value>> {
106 let site = services.addresses.site.clone();
107 let path = input["path"].as_str().unwrap_or_default().trim().trim_matches('/').to_owned();
108 let git_ref = input["ref"].as_str().map(str::trim).filter(|git_ref| !git_ref.is_empty()).map(str::to_owned);
109 let tree = |tree_path: String, git_ref: Option<String>| TreeArgs { path: repo.clone(), viewer: viewer.clone(), git_ref, tree_path };
110 if !path.is_empty() {
111 // A file needs a ref: the default branch, unless one was given.
112 let git_ref = match &git_ref {
113 Some(git_ref) => git_ref.clone(),
114 None => {
115 let found: Outcome<Repo> =
116 g1t_kit::call(&services.repos, "get", &GetArgs { path: repo.clone(), viewer: viewer.clone() }).await?;
117 match found {
118 Outcome::Ok(found) => found.default_branch,
119 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
120 }
121 }
122 };
123 let blob: Outcome<BlobView> = g1t_kit::call(
124 &services.repos,
125 "blob",
126 &BlobArgs { path: repo.clone(), viewer: viewer.clone(), git_ref: git_ref.clone(), file_path: path.clone() },
127 )
128 .await?;
129 if let Outcome::Ok(view) = blob {
130 return Ok(Outcome::Ok(file(&site, &view)));
131 }
132 // Not a file: a directory, or nothing.
133 let listed: Outcome<TreeView> = g1t_kit::call(&services.repos, "tree", &tree(path.clone(), Some(git_ref))).await?;
134 return Ok(match listed {
135 Outcome::Ok(view) => Outcome::Ok(directory(&site, &view)),
136 Outcome::Fail(failure) if failure.code == FailureCode::NotFound => {
137 Outcome::fail(FailureCode::NotFound, format!("{}/{} has no file or directory {path}.", repo.namespace, repo.name))
138 }
139 Outcome::Fail(failure) => Outcome::Fail(failure),
140 });
141 }
142 let listed: Outcome<TreeView> = g1t_kit::call(&services.repos, "tree", &tree(String::new(), git_ref)).await?;
143 Ok(match listed {
144 Outcome::Ok(view) => Outcome::Ok(directory(&site, &view)),
145 Outcome::Fail(failure) => Outcome::Fail(failure),
146 })
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use g1t_contracts::repos::{Readme, TreeEntry};
153
154 fn repo() -> Repo {
155 serde_json::from_value(json!({
156 "id": "rep_1", "namespace": "acme", "name": "web", "description": null, "isPrivate": false,
157 "ownerId": "wsp_1", "defaultBranch": "main", "forkOf": null, "createdAt": "2026-01-01T00:00:00Z",
158 }))
159 .unwrap()
160 }
161
162 #[test]
163 fn a_file_is_its_text_and_where_it_is() {
164 let view = BlobView { repo: repo(), git_ref: "main".into(), path: "CONTRIBUTING.md".into(), size: 12, text: Some("npm test\n".into()) };
165 let shown = file("https://g1t.sh/", &view);
166 assert_eq!(shown["type"], "file");
167 assert_eq!(shown["text"], "npm test\n");
168 assert_eq!(shown["binary"], false);
169 assert_eq!(shown["html_url"], "https://g1t.sh/acme/web/blob/main/CONTRIBUTING.md");
170 let binary = file("https://g1t.sh", &BlobView { text: None, ..view });
171 assert_eq!(binary["binary"], true);
172 assert!(binary["text"].is_null());
173 }
174
175 #[test]
176 fn a_huge_file_is_cut_with_a_note() {
177 let text = "x".repeat(MAX_TEXT_CHARS + 10);
178 let view = BlobView { repo: repo(), git_ref: "main".into(), path: "big.txt".into(), size: text.len() as u64, text: Some(text) };
179 let shown = file("https://g1t.sh", &view);
180 assert!(shown["text"].as_str().unwrap().ends_with("[Cut: 10 more characters.]"));
181 }
182
183 #[test]
184 fn a_directory_lists_its_entries_and_readme() {
185 let view = TreeView {
186 repo: repo(),
187 git_ref: "main".into(),
188 path: "".into(),
189 head: None,
190 entries: vec![
191 TreeEntry { name: "src".into(), hash: "a".into(), kind: EntryKind::Tree },
192 TreeEntry { name: "README.md".into(), hash: "b".into(), kind: EntryKind::Blob },
193 ],
194 readme: Some(Readme { name: "README.md".into(), text: Some("# Web".into()) }),
195 };
196 let shown = directory("https://g1t.sh", &view);
197 assert_eq!(shown["entries"], json!([{ "name": "src", "path": "src", "type": "dir" }, { "name": "README.md", "path": "README.md", "type": "file" }]));
198 assert_eq!(shown["readme"]["text"], "# Web");
199 assert_eq!(shown["html_url"], "https://g1t.sh/acme/web");
200 let nested = directory("https://g1t.sh", &TreeView { path: "src".into(), readme: None, ..view });
201 assert_eq!(nested["entries"][0]["path"], "src/src");
202 assert_eq!(nested["html_url"], "https://g1t.sh/acme/web/tree/main/src");
203 }
204}