1mod combined_backend;
10mod git_backend;
11mod github_backend;
12
13pub(crate) use combined_backend::CombinedBackend;
14pub use git_backend::GitBackend;
15pub(crate) use github_backend::GitHubBackend;
16
17use std::collections::HashSet;
18
19use async_trait::async_trait;
20use chrono::{DateTime, Utc};
21
22use crate::error::{WtgError, WtgResult};
23use crate::git::{CommitInfo, FileInfo, GitRepo, TagInfo};
24use crate::github::{ExtendedIssueInfo, PullRequestInfo};
25use crate::notice::{Notice, NoticeCallback, no_notices};
26use crate::parse_input::{ParsedInput, ParsedQuery, Query};
27use crate::remote::{RemoteHost, RemoteInfo};
28
29#[async_trait]
34pub trait Backend: Send + Sync {
35 async fn backend_for_pr(&self, _pr: &PullRequestInfo) -> Option<Box<dyn Backend>> {
42 None
43 }
44
45 async fn find_commit(&self, _hash: &str) -> WtgResult<CommitInfo> {
51 Err(WtgError::Unsupported("commit lookup".into()))
52 }
53
54 async fn enrich_commit(&self, commit: CommitInfo) -> CommitInfo {
56 commit
57 }
58
59 async fn find_commit_for_pr(&self, pr: &PullRequestInfo) -> WtgResult<CommitInfo> {
61 if let Some(ref sha) = pr.merge_commit_sha {
62 self.find_commit(sha).await
63 } else {
64 Err(WtgError::NotFound("PR has no merge commit".into()))
65 }
66 }
67
68 async fn find_file(&self, _branch: &str, _path: &str) -> WtgResult<FileInfo> {
74 Err(WtgError::Unsupported("file lookup".into()))
75 }
76
77 async fn find_tag(&self, _name: &str) -> WtgResult<TagInfo> {
83 Err(WtgError::Unsupported("tag lookup".into()))
84 }
85
86 async fn disambiguate_query(&self, query: &ParsedQuery) -> WtgResult<Query> {
88 match query {
89 ParsedQuery::Resolved(resolved) => Ok(resolved.clone()),
90 ParsedQuery::Unknown(input) => Err(WtgError::NotFound(input.clone())),
91 ParsedQuery::UnknownPath { segments } => Err(WtgError::NotFound(segments.join("/"))),
92 }
93 }
94
95 async fn find_release_for_commit(
97 &self,
98 _commit_hash: &str,
99 _commit_date: Option<DateTime<Utc>>,
100 ) -> Option<TagInfo> {
101 None
102 }
103
104 async fn fetch_issue(&self, _number: u64) -> WtgResult<ExtendedIssueInfo> {
110 Err(WtgError::Unsupported("issue lookup".into()))
111 }
112
113 async fn fetch_pr(&self, _number: u64) -> WtgResult<PullRequestInfo> {
119 Err(WtgError::Unsupported("PR lookup".into()))
120 }
121
122 fn commit_url(&self, _hash: &str) -> Option<String> {
128 None
129 }
130
131 fn tag_url(&self, _tag: &str) -> Option<String> {
133 None
134 }
135
136 fn author_url_from_email(&self, _email: &str) -> Option<String> {
138 None
139 }
140}
141
142pub fn resolve_backend(
158 parsed_input: &ParsedInput,
159 allow_user_repo_fetch: bool,
160) -> WtgResult<Box<dyn Backend>> {
161 resolve_backend_with_notices(parsed_input, allow_user_repo_fetch, no_notices())
162}
163
164pub fn resolve_backend_with_notices(
166 parsed_input: &ParsedInput,
167 allow_user_repo_fetch: bool,
168 notice_cb: NoticeCallback,
169) -> WtgResult<Box<dyn Backend>> {
170 if let Some(repo_info) = parsed_input.gh_repo_info() {
172 let github = GitHubBackend::new(repo_info.clone()).ok_or(WtgError::GitHubClientFailed)?;
174
175 if let Ok(git_repo) = GitRepo::remote_with_notices(repo_info.clone(), notice_cb.clone()) {
177 let git = GitBackend::new(git_repo);
178 let mut combined = CombinedBackend::new(git, github);
179 combined.set_notice_callback(notice_cb);
180 Ok(Box::new(combined))
181 } else {
182 notice_cb(Notice::ApiOnly);
184 Ok(Box::new(github))
185 }
186 } else {
187 resolve_local_backend_with_notices(allow_user_repo_fetch, notice_cb)
189 }
190}
191
192fn resolve_local_backend_with_notices(
193 allow_user_repo_fetch: bool,
194 notice_cb: NoticeCallback,
195) -> WtgResult<Box<dyn Backend>> {
196 let mut git_repo = GitRepo::open()?;
197 if allow_user_repo_fetch {
198 git_repo.set_allow_fetch(true);
199 }
200 git_repo.set_notice_callback(notice_cb.clone());
201
202 let mut remotes: Vec<RemoteInfo> = git_repo.remotes().collect();
204 remotes.sort_by_key(RemoteInfo::priority);
205
206 if remotes.is_empty() {
208 notice_cb(Notice::NoRemotes);
209 let git = GitBackend::new(git_repo);
210 return Ok(Box::new(git));
211 }
212
213 let github_remote = remotes
215 .iter()
216 .find(|r| r.host == Some(RemoteHost::GitHub))
217 .cloned();
218
219 if let Some(github_remote) = github_remote {
220 if let Some(repo_info) = git_repo.github_remote() {
222 let git = GitBackend::new(git_repo);
223
224 if let Some(github) = GitHubBackend::new(repo_info) {
225 let mut combined = CombinedBackend::new(git, github);
227 combined.set_notice_callback(notice_cb);
228 return Ok(Box::new(combined));
229 }
230
231 notice_cb(Notice::UnreachableGitHub {
233 remote: github_remote,
234 });
235 return Ok(Box::new(git));
236 }
237 }
238
239 let git = GitBackend::new(git_repo);
241 let unique_hosts: HashSet<Option<RemoteHost>> = remotes.iter().map(|r| r.host).collect();
242
243 let known_hosts: Vec<RemoteHost> = unique_hosts.iter().filter_map(|h| *h).collect();
245
246 if known_hosts.len() > 1 {
247 notice_cb(Notice::MixedRemotes {
249 hosts: known_hosts,
250 count: remotes.len(),
251 });
252 return Ok(Box::new(git));
253 }
254
255 let best_remote = remotes.into_iter().next().unwrap();
257 notice_cb(Notice::UnsupportedHost { best_remote });
258 Ok(Box::new(git))
259}