wtg_cli/backend/
git_backend.rs1use async_trait::async_trait;
6use chrono::{DateTime, Utc};
7use std::collections::HashMap;
8use std::path::PathBuf;
9
10use super::{Backend, NoticeCallback};
11use crate::error::{WtgError, WtgResult};
12use crate::git::{CommitInfo, FileInfo, GitRepo, TagInfo, looks_like_commit_hash};
13use crate::github::GitHubClient;
14use crate::parse_input::{ParsedQuery, Query};
15
16pub struct GitBackend {
21 repo: GitRepo,
22}
23
24impl GitBackend {
25 #[must_use]
27 pub const fn new(repo: GitRepo) -> Self {
28 Self { repo }
29 }
30
31 pub const fn git_repo(&self) -> &GitRepo {
33 &self.repo
34 }
35
36 pub fn set_notice_callback(&mut self, cb: NoticeCallback) {
38 self.repo.set_notice_callback(cb);
39 }
40
41 fn find_best_tag_for_commit(&self, commit_hash: &str) -> Option<TagInfo> {
43 let candidates = self.repo.tags_containing_commit(commit_hash);
44 if candidates.is_empty() {
45 return None;
46 }
47
48 let timestamps: HashMap<String, i64> = candidates
50 .iter()
51 .map(|tag| {
52 (
53 tag.commit_hash.clone(),
54 self.repo.get_commit_timestamp(&tag.commit_hash),
55 )
56 })
57 .collect();
58
59 Self::pick_best_tag(&candidates, ×tamps)
61 }
62
63 fn pick_best_tag(candidates: &[TagInfo], timestamps: &HashMap<String, i64>) -> Option<TagInfo> {
65 fn select_with_pred<F>(
66 candidates: &[TagInfo],
67 timestamps: &HashMap<String, i64>,
68 predicate: F,
69 ) -> Option<TagInfo>
70 where
71 F: Fn(&TagInfo) -> bool,
72 {
73 candidates
74 .iter()
75 .filter(|tag| predicate(tag))
76 .min_by_key(|tag| {
77 timestamps
78 .get(&tag.commit_hash)
79 .copied()
80 .unwrap_or(i64::MAX)
81 })
82 .cloned()
83 }
84
85 select_with_pred(candidates, timestamps, |t| t.is_release && t.is_semver())
87 .or_else(|| {
88 select_with_pred(candidates, timestamps, |t| !t.is_release && t.is_semver())
89 })
90 .or_else(|| {
91 select_with_pred(candidates, timestamps, |t| t.is_release && !t.is_semver())
92 })
93 .or_else(|| {
94 select_with_pred(candidates, timestamps, |t| !t.is_release && !t.is_semver())
95 })
96 }
97
98 fn disambiguate_input_string(&self, input: &str) -> WtgResult<Query> {
99 if self.repo.get_tags().iter().any(|tag| tag.name == input) {
100 return Ok(Query::Tag(input.to_string()));
101 }
102
103 if self.repo.has_path_at_head(input) {
104 return Ok(Query::FilePath {
105 branch: "HEAD".to_string(),
106 path: PathBuf::from(input),
107 });
108 }
109
110 if looks_like_commit_hash(input) && self.repo.find_commit_local(input).is_some() {
111 return Ok(Query::GitCommit(input.to_string()));
112 }
113
114 Err(WtgError::NotFound(input.to_string()))
115 }
116
117 fn disambiguate_unknown_path(&self, segments: &[String]) -> Option<Query> {
118 let (branch, remainder) = self.repo.find_branch_path_match(segments)?;
119 let mut path = PathBuf::new();
120 for segment in remainder {
121 path.push(segment);
122 }
123 Some(Query::FilePath { branch, path })
124 }
125}
126
127#[async_trait]
128impl Backend for GitBackend {
129 async fn find_commit(&self, hash: &str) -> WtgResult<CommitInfo> {
137 self.repo
139 .find_commit(hash)?
140 .ok_or_else(|| WtgError::NotFound(hash.to_string()))
141 }
142
143 async fn enrich_commit(&self, mut commit: CommitInfo) -> CommitInfo {
144 if commit.commit_url.is_none()
146 && let Some(repo_info) = self.repo.github_remote()
147 {
148 commit.commit_url = Some(GitHubClient::commit_url(&repo_info, &commit.hash));
149 }
150 commit
151 }
152
153 async fn find_file(&self, branch: &str, path: &str) -> WtgResult<FileInfo> {
158 self.repo
159 .find_file_on_branch(branch, path)
160 .ok_or_else(|| WtgError::NotFound(path.to_string()))
161 }
162
163 async fn find_tag(&self, name: &str) -> WtgResult<TagInfo> {
168 self.repo
169 .get_tags()
170 .into_iter()
171 .find(|t| t.name == name)
172 .ok_or_else(|| WtgError::NotFound(name.to_string()))
173 }
174
175 async fn disambiguate_query(&self, query: &ParsedQuery) -> WtgResult<Query> {
176 match query {
177 ParsedQuery::Resolved(resolved) => Ok(resolved.clone()),
178 ParsedQuery::Unknown(input) => self.disambiguate_input_string(input),
179 ParsedQuery::UnknownPath { segments } => self
180 .disambiguate_unknown_path(segments)
181 .ok_or_else(|| WtgError::NotFound(segments.join("/"))),
182 }
183 }
184
185 async fn find_release_for_commit(
186 &self,
187 commit_hash: &str,
188 _commit_date: Option<DateTime<Utc>>,
189 ) -> Option<TagInfo> {
190 self.find_best_tag_for_commit(commit_hash)
191 }
192
193 fn commit_url(&self, hash: &str) -> Option<String> {
198 self.repo
199 .github_remote()
200 .map(|ri| GitHubClient::commit_url(&ri, hash))
201 }
202
203 fn tag_url(&self, tag: &str) -> Option<String> {
204 self.repo
205 .github_remote()
206 .map(|ri| GitHubClient::tag_url(&ri, tag))
207 }
208
209 fn author_url_from_email(&self, email: &str) -> Option<String> {
210 GitHubClient::author_url_from_email(email)
211 }
212}