1use super::StdioProvider;
7use super::transport::{cancel_notification, de};
8use rootle_provider::{
9 BlameRange, Capabilities, CodeMatch, CommitDetail, ErrorKind, GitRef, LogEntry, Provider,
10 ProviderError, ProviderResult, RefInfo, RepoId, RepoInfo, RepoRefs, SearchCodeResult,
11 SearchItem, Sha, TreeNode, TreeResult,
12};
13use serde_json::json;
14use std::io::Write;
15use std::sync::atomic::Ordering;
16
17impl Provider for StdioProvider {
18 fn name(&self) -> &str {
19 &self.name
20 }
21
22 fn icon(&self) -> Option<String> {
23 self.icon.clone()
24 }
25
26 fn capabilities(&self) -> Capabilities {
27 self.capabilities
28 }
29
30 fn search(&self, query: &str) -> ProviderResult<Vec<SearchItem>> {
31 #[derive(serde::Deserialize)]
32 struct RepositorySearchReply {
33 #[serde(default)]
34 items: Vec<Item>,
35 }
36 #[derive(serde::Deserialize)]
37 struct Item {
38 full_name: Option<String>,
39 org: Option<String>,
40 }
41 let reply: RepositorySearchReply =
42 de(self.request("search/repos", json!({ "query": query }))?)?;
43 Ok(reply
44 .items
45 .into_iter()
46 .filter_map(|i| match (i.full_name, i.org) {
47 (Some(r), _) => Some(SearchItem::Repo(r)),
48 (None, Some(o)) => Some(SearchItem::Org(o)),
49 _ => None,
50 })
51 .collect())
52 }
53
54 fn org_repos(&self, org: &str) -> ProviderResult<Vec<RepoInfo>> {
55 #[derive(serde::Deserialize)]
56 struct OrganizationRepositoriesReply {
57 #[serde(default)]
58 repos: Vec<RepoEntry>,
59 }
60 #[derive(serde::Deserialize)]
64 #[serde(untagged)]
65 enum RepoEntry {
66 Name(String),
67 Meta {
68 name: String,
69 description: Option<String>,
70 #[serde(default)]
71 private: bool,
72 #[serde(default)]
73 archived: bool,
74 pushed_at: Option<String>,
75 },
76 }
77 let reply: OrganizationRepositoriesReply =
78 de(self.request("org/repos", json!({ "org": org }))?)?;
79 Ok(reply
80 .repos
81 .into_iter()
82 .map(|e| match e {
83 RepoEntry::Name(name) => RepoInfo::bare(name),
84 RepoEntry::Meta {
85 name,
86 description,
87 private,
88 archived,
89 pushed_at,
90 } => RepoInfo {
91 name,
92 description,
93 private,
94 archived,
95 pushed_at,
96 },
97 })
98 .collect())
99 }
100
101 fn fetch_tree(&self, repo: &RepoId, ref_: Option<&GitRef>) -> ProviderResult<TreeResult> {
102 #[derive(serde::Deserialize)]
103 struct RepositoryTreeReply {
104 #[serde(default)]
105 entries: Vec<Entry>,
106 #[serde(default)]
107 truncated: bool,
108 #[serde(default = "main")]
109 branch: String,
110 }
111 #[derive(serde::Deserialize)]
112 struct Entry {
113 path: String,
114 #[serde(rename = "type")]
115 kind: String, sha: String,
117 size: Option<u64>,
118 }
119 fn main() -> String {
120 "main".into()
121 }
122 let reply: RepositoryTreeReply =
123 de(self.request("repo/tree", json!({ "repo": repo, "ref": ref_ }))?)?;
124 Ok(TreeResult {
125 entries: reply
126 .entries
127 .into_iter()
128 .map(|e| TreeNode {
129 path: e.path,
130 is_dir: e.kind == "tree",
131 sha: e.sha,
132 size: e.size,
133 })
134 .collect(),
135 truncated: reply.truncated,
136 branch: reply.branch,
137 })
138 }
139
140 fn fetch_blob(&self, repo: &RepoId, sha: &Sha) -> ProviderResult<Vec<u8>> {
141 #[derive(serde::Deserialize)]
142 struct BlobReply {
143 bytes_b64: String,
144 }
145 let reply: BlobReply = de(self.request("repo/blob", json!({ "repo": repo, "sha": sha }))?)?;
146 use base64::Engine;
147 base64::engine::general_purpose::STANDARD
148 .decode(reply.bytes_b64)
149 .map_err(|e| ProviderError::other(format!("provider blob base64: {e}")))
150 }
151
152 fn web_url(
153 &self,
154 repo: &RepoId,
155 path: &str,
156 branch: Option<&GitRef>,
157 line: Option<u32>,
158 end: Option<u32>,
159 is_file: bool,
160 ) -> ProviderResult<String> {
161 #[derive(serde::Deserialize)]
162 struct RepositoryUrlReply {
163 url: String,
164 }
165 let reply: RepositoryUrlReply = de(self.request(
166 "repo/web_url",
167 json!({ "repo": repo, "path": path, "branch": branch.map(GitRef::as_str).unwrap_or(""), "line": line, "end_line": end, "is_file": is_file }),
168 )?)?;
169 Ok(reply.url)
170 }
171
172 fn org_url(&self, org: &str) -> ProviderResult<String> {
173 #[derive(serde::Deserialize)]
174 struct OrganizationUrlReply {
175 url: String,
176 }
177 let reply: OrganizationUrlReply = de(self.request("org/url", json!({ "org": org }))?)?;
178 Ok(reply.url)
179 }
180
181 fn clone_url(&self, repo: &RepoId) -> ProviderResult<String> {
182 #[derive(serde::Deserialize)]
183 struct CloneUrlReply {
184 clone_url: String,
185 }
186 let reply: CloneUrlReply = de(self.request("repo/clone_url", json!({ "repo": repo }))?)?;
187 Ok(reply.clone_url)
188 }
189
190 fn search_code(&self, q: &str) -> ProviderResult<SearchCodeResult> {
191 let r: CodeReply = de(self.request(
192 "search/code",
193 json!({ "q": q, "limit": rootle_provider::RENDER_BUDGET }),
194 )?)?;
195 Ok(SearchCodeResult {
196 hits: code_matches(&r.items),
197 truncated: r.truncated,
198 index_as_of: r.index.as_of,
199 })
200 }
201
202 fn search_code_progressive(
207 &self,
208 q: &str,
209 on_hits: &(dyn Fn(&[CodeMatch]) + Send + Sync),
210 ) -> ProviderResult<SearchCodeResult> {
211 let sink = |params: &serde_json::Value| {
212 let items: Vec<WireItem> = params
213 .get("items")
214 .cloned()
215 .and_then(|v| serde_json::from_value(v).ok())
216 .unwrap_or_default();
217 on_hits(&code_matches(&items));
218 };
219 let reply: CodeReply = de(self.exchange_with_partials(
220 "search/code",
221 json!({ "q": q, "partial": true, "limit": rootle_provider::RENDER_BUDGET }),
222 &sink,
223 )?)?;
224 Ok(SearchCodeResult {
225 hits: Vec::new(),
226 truncated: reply.truncated,
227 index_as_of: reply.index.as_of,
228 })
229 }
230
231 fn refs(&self, repo: &RepoId) -> ProviderResult<RepoRefs> {
233 #[derive(serde::Deserialize)]
234 struct RepositoryRefsReply {
235 #[serde(default)]
236 branches: Vec<RefEntry>,
237 #[serde(default)]
238 tags: Vec<RefEntry>,
239 }
240 #[derive(serde::Deserialize)]
241 struct RefEntry {
242 name: String,
243 sha: String,
244 #[serde(default)]
245 default: bool,
246 }
247 let reply: RepositoryRefsReply = de(self.request("repo/refs", json!({ "repo": repo }))?)?;
248 let map = |e: RefEntry| RefInfo {
249 name: e.name,
250 sha: e.sha,
251 is_default: e.default,
252 };
253 Ok(RepoRefs {
254 branches: reply.branches.into_iter().map(map).collect(),
255 tags: reply.tags.into_iter().map(map).collect(),
256 })
257 }
258
259 fn log(
261 &self,
262 repo: &RepoId,
263 path: Option<&str>,
264 ref_: Option<&GitRef>,
265 limit: Option<usize>,
266 ) -> ProviderResult<(Vec<LogEntry>, bool)> {
267 #[derive(serde::Deserialize)]
268 struct CommitLogReply {
269 #[serde(default)]
270 items: Vec<LogEntry>,
271 #[serde(default)]
272 truncated: bool,
273 }
274 let reply: CommitLogReply = de(self.request(
275 "repo/log",
276 json!({ "repo": repo, "path": path, "ref": ref_, "limit": limit }),
277 )?)?;
278 Ok((reply.items, reply.truncated))
279 }
280
281 fn blob_at(
283 &self,
284 repo: &RepoId,
285 path: &str,
286 ref_: Option<&GitRef>,
287 ) -> ProviderResult<(Vec<u8>, Sha)> {
288 #[derive(serde::Deserialize)]
289 struct BlobAtRevisionReply {
290 bytes_b64: String,
291 sha: String,
292 }
293 let reply: BlobAtRevisionReply = de(self.request(
294 "repo/blob_at",
295 json!({ "repo": repo, "path": path, "ref": ref_ }),
296 )?)?;
297 use base64::Engine;
298 let bytes = base64::engine::general_purpose::STANDARD
299 .decode(reply.bytes_b64)
300 .map_err(|e| ProviderError::new(ErrorKind::Provider, format!("bad base64: {e}")))?;
301 Ok((bytes, Sha::from(reply.sha)))
302 }
303
304 fn blame(
306 &self,
307 repo: &RepoId,
308 path: &str,
309 ref_: Option<&GitRef>,
310 ) -> ProviderResult<Vec<BlameRange>> {
311 #[derive(serde::Deserialize)]
312 struct BlameReply {
313 #[serde(default)]
314 ranges: Vec<BlameRange>,
315 }
316 let reply: BlameReply = de(self.request(
317 "repo/blame",
318 json!({ "repo": repo, "path": path, "ref": ref_ }),
319 )?)?;
320 Ok(reply.ranges)
321 }
322
323 fn commit(&self, repo: &RepoId, sha: &Sha) -> ProviderResult<CommitDetail> {
327 de(self.request("repo/commit", json!({ "repo": repo, "sha": sha }))?)
328 }
329
330 fn advise_cancel(&self) {
334 let id = self.current_id.load(Ordering::Acquire);
335 if id == 0 {
336 return;
337 }
338 let line = cancel_notification(id);
339 let mut process = self.process.lock();
340 let _ = writeln!(process.stdin, "{line}");
341 let _ = process.stdin.flush();
342 }
343
344 fn take_notice(&self) -> Option<String> {
346 self.notice.lock().take()
347 }
348
349 fn cache_usage(&self) -> Option<u64> {
352 *self.cache_used.lock()
353 }
354}
355
356#[derive(serde::Deserialize)]
359struct CodeReply {
360 #[serde(default)]
361 items: Vec<WireItem>,
362 #[serde(default)]
364 truncated: bool,
365 #[serde(default)]
367 index: IndexInfo,
368}
369
370#[derive(serde::Deserialize, Default)]
371struct IndexInfo {
372 #[serde(default)]
373 as_of: Option<String>,
374}
375
376#[derive(serde::Deserialize)]
377struct WireItem {
378 repo: String,
379 path: String,
380 #[serde(default)]
381 sha: String,
382 #[serde(default = "main_branch")]
383 branch: String,
384 #[serde(default)]
385 matches: Vec<String>,
386 #[serde(default = "located")]
388 located: bool,
389 #[serde(default)]
391 line: Option<u32>,
392}
393
394fn main_branch() -> String {
395 "main".into()
396}
397
398fn located() -> bool {
399 true
400}
401
402fn code_matches(items: &[WireItem]) -> Vec<CodeMatch> {
403 items
404 .iter()
405 .map(|i| CodeMatch {
406 repo: i.repo.clone(),
407 path: i.path.clone(),
408 sha: i.sha.clone(),
409 branch: i.branch.clone(),
410 matches: i.matches.clone(),
411 located: i.located,
412 line: i.line,
413 })
414 .collect()
415}
416#[cfg(test)]
417mod tests {
418 use rootle_provider::CommitDetail;
419
420 #[test]
423 fn located_defaults_true_and_parses_false() {
424 #[derive(serde::Deserialize)]
425 struct Item {
426 #[serde(default = "located")]
427 located: bool,
428 }
429 fn located() -> bool {
430 true
431 }
432 let absent: Item = serde_json::from_str("{}").unwrap();
433 assert!(absent.located);
434 let stale: Item = serde_json::from_str(r#"{"located":false}"#).unwrap();
435 assert!(!stale.located);
436 }
437
438 #[test]
439 fn commit_unknown_status_and_missing_counts_remain_honest() {
440 let detail: CommitDetail = super::de(serde_json::json!({
441 "sha": "id", "author": "author", "date": "2026-09-06", "message": "message",
442 "files": [
443 {"path": "one", "status": "future-status", "additions": 4294967295u64, "deletions": 0},
444 {"path": "two", "status": "added", "additions": 4294967295u64}
445 ]
446 })).unwrap();
447 assert_eq!(
448 detail.files[0].status,
449 rootle_provider::FileStatus::Modified
450 );
451 assert_eq!(detail.line_stats().additions, Some(8589934590));
452 assert_eq!(detail.line_stats().deletions, None);
453 }
454}