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.
| g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other. | 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 | ||
| 10 | use g1t_contracts::repos::{BlobArgs, BlobView, EntryKind, GetArgs, Repo, RepoPath, TreeArgs, TreeView}; | |
| 11 | use g1t_contracts::{FailureCode, Outcome, Viewer}; | |
| 12 | use serde_json::{Value, json}; | |
| 13 | use worker::Result; | |
| 14 | ||
| 15 | use crate::operations::Services; | |
| 16 | ||
| 17 | /// The most of a file's text one answer carries; more is cut with a note. | |
| 18 | pub const MAX_TEXT_CHARS: usize = 200_000; | |
| 19 | /// The most entries of a directory one answer lists. | |
| 20 | pub const MAX_ENTRIES: usize = 1_000; | |
| 21 | ||
| 22 | pub 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 | ||
| 26 | pub 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 | ||
| 38 | fn 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 | ||
| 47 | fn full_name(repo: &Repo) -> String { | |
| 48 | format!("{}/{}", repo.namespace, repo.name) | |
| 49 | } | |
| 50 | ||
| 51 | /// A file's answer. | |
| 52 | pub 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. | |
| 76 | pub 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 | ||
| 105 | pub 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)] | |
| 150 | mod 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 | } |