1use crate::backend::Backend;
8use crate::changelog;
9use crate::error::{WtgError, WtgResult};
10use crate::git::{CommitInfo, FileInfo, TagInfo};
11use crate::github::{ExtendedIssueInfo, PullRequestInfo};
12use crate::parse_input::Query;
13use crate::release_filter::ReleaseFilter;
14
15#[derive(Debug, Clone)]
21pub enum EntryPoint {
22 Commit(String), IssueNumber(u64), PullRequestNumber(u64), FilePath { branch: String, path: String }, Tag(String), }
28
29#[derive(Debug, Clone)]
31pub struct IssueInfo {
32 pub number: u64,
33 pub title: String,
34 pub body: Option<String>,
35 pub state: octocrab::models::IssueState,
36 pub url: String,
37 pub author: Option<String>,
38 pub author_url: Option<String>,
39}
40
41impl From<&ExtendedIssueInfo> for IssueInfo {
42 fn from(ext_info: &ExtendedIssueInfo) -> Self {
43 Self {
44 number: ext_info.number,
45 title: ext_info.title.clone(),
46 body: ext_info.body.clone(),
47 state: ext_info.state.clone(),
48 url: ext_info.url.clone(),
49 author: ext_info.author.clone(),
50 author_url: ext_info.author_url.clone(),
51 }
52 }
53}
54
55#[derive(Debug, Clone)]
57pub struct EnrichedInfo {
58 pub entry_point: EntryPoint,
59
60 pub commit: Option<CommitInfo>,
62
63 pub pr: Option<PullRequestInfo>,
65
66 pub issue: Option<IssueInfo>,
68
69 pub release: Option<TagInfo>,
71}
72
73#[derive(Debug, Clone)]
75pub struct FileResult {
76 pub file_info: FileInfo,
77 pub commit_url: Option<String>,
78 pub author_urls: Vec<Option<String>>,
79 pub release: Option<TagInfo>,
80}
81
82#[derive(Debug, Clone)]
84pub enum ChangesSource {
85 GitHubRelease,
87 Changelog,
89 Commits { previous_tag: String },
91}
92
93#[derive(Debug, Clone)]
95pub struct TagResult {
96 pub tag_info: TagInfo,
97 pub github_url: Option<String>,
98 pub changes: Option<String>,
100 pub changes_source: Option<ChangesSource>,
102 pub truncated_lines: usize,
104 pub commits: Vec<CommitInfo>,
106}
107
108#[derive(Debug, Clone)]
109pub enum IdentifiedThing {
110 Enriched(Box<EnrichedInfo>),
111 File(Box<FileResult>),
112 Tag(Box<TagResult>),
113}
114
115pub async fn resolve(
124 backend: &dyn Backend,
125 query: &Query,
126 filter: &ReleaseFilter,
127) -> WtgResult<IdentifiedThing> {
128 if let Some(tag_name) = filter.specific_tag()
130 && backend.find_tag(tag_name).await.is_err()
131 {
132 return Err(WtgError::TagNotFound(tag_name.to_string()));
133 }
134
135 match query {
136 Query::GitCommit(hash) => resolve_commit(backend, hash, filter).await,
137 Query::Pr(number) => resolve_pr(backend, *number, filter).await,
138 Query::Issue(number) => resolve_issue(backend, *number, filter).await,
139 Query::IssueOrPr(number) => {
140 if let Ok(result) = resolve_pr(backend, *number, filter).await {
142 return Ok(result);
143 }
144 if let Ok(result) = resolve_issue(backend, *number, filter).await {
145 return Ok(result);
146 }
147 Err(WtgError::NotFound(format!("#{number}")))
148 }
149 Query::FilePath { branch, path } => {
150 resolve_file(backend, branch, &path.to_string_lossy(), filter).await
151 }
152 Query::Tag(tag) => resolve_tag(backend, tag).await,
153 }
154}
155
156async fn resolve_commit(
158 backend: &dyn Backend,
159 hash: &str,
160 filter: &ReleaseFilter,
161) -> WtgResult<IdentifiedThing> {
162 let commit = backend.find_commit(hash).await?;
163 let commit = backend.enrich_commit(commit).await;
164 let release = backend
165 .find_release_for_commit(&commit.hash, Some(commit.date), filter)
166 .await;
167
168 Ok(IdentifiedThing::Enriched(Box::new(EnrichedInfo {
169 entry_point: EntryPoint::Commit(hash.to_string()),
170 commit: Some(commit),
171 pr: None,
172 issue: None,
173 release,
174 })))
175}
176
177async fn resolve_pr(
179 backend: &dyn Backend,
180 number: u64,
181 filter: &ReleaseFilter,
182) -> WtgResult<IdentifiedThing> {
183 let pr = backend.fetch_pr(number).await?;
184
185 let commit = backend.find_commit_for_pr(&pr).await.ok();
186 let commit = match commit {
187 Some(c) => Some(backend.enrich_commit(c).await),
188 None => None,
189 };
190
191 let release = if let Some(ref c) = commit {
192 backend
193 .find_release_for_commit(&c.hash, Some(c.date), filter)
194 .await
195 } else {
196 None
197 };
198
199 Ok(IdentifiedThing::Enriched(Box::new(EnrichedInfo {
200 entry_point: EntryPoint::PullRequestNumber(number),
201 commit,
202 pr: Some(pr),
203 issue: None,
204 release,
205 })))
206}
207
208async fn resolve_issue(
212 backend: &dyn Backend,
213 number: u64,
214 filter: &ReleaseFilter,
215) -> WtgResult<IdentifiedThing> {
216 let ext_issue = backend.fetch_issue(number).await?;
217 let display_issue = (&ext_issue).into();
218
219 let closing_pr = ext_issue.closing_prs.into_iter().next();
221
222 let (commit, release) = if let Some(ref pr) = closing_pr {
223 if let Some(merge_sha) = &pr.merge_commit_sha {
224 let cross_backend = backend.backend_for_pr(pr).await;
226 let effective_backend: &dyn Backend =
227 cross_backend.as_ref().map_or(backend, |b| b.as_ref());
228
229 let commit = effective_backend.find_commit(merge_sha).await.ok();
230 let commit = match commit {
231 Some(c) => Some(effective_backend.enrich_commit(c).await),
232 None => None,
233 };
234
235 let release = if let Some(ref c) = commit {
236 let hash = &c.hash;
237 let date = Some(c.date);
238 if cross_backend.is_some() {
240 match backend.find_release_for_commit(hash, date, filter).await {
241 Some(r) => Some(r),
242 None => {
243 effective_backend
244 .find_release_for_commit(hash, date, filter)
245 .await
246 }
247 }
248 } else {
249 backend.find_release_for_commit(hash, date, filter).await
250 }
251 } else {
252 None
253 };
254
255 (commit, release)
256 } else {
257 (None, None)
258 }
259 } else {
260 (None, None)
261 };
262
263 Ok(IdentifiedThing::Enriched(Box::new(EnrichedInfo {
264 entry_point: EntryPoint::IssueNumber(number),
265 commit,
266 pr: closing_pr,
267 issue: Some(display_issue),
268 release,
269 })))
270}
271
272async fn resolve_file(
274 backend: &dyn Backend,
275 branch: &str,
276 path: &str,
277 filter: &ReleaseFilter,
278) -> WtgResult<IdentifiedThing> {
279 let file_info = backend.find_file(branch, path).await?;
280 let commit_url = backend.commit_url(&file_info.last_commit.hash);
281
282 let author_urls: Vec<Option<String>> = file_info
284 .previous_authors
285 .iter()
286 .map(|(_, _, email)| backend.author_url_from_email(email))
287 .collect();
288
289 let release = backend
290 .find_release_for_commit(
291 &file_info.last_commit.hash,
292 Some(file_info.last_commit.date),
293 filter,
294 )
295 .await;
296
297 Ok(IdentifiedThing::File(Box::new(FileResult {
298 file_info,
299 commit_url,
300 author_urls,
301 release,
302 })))
303}
304
305async fn select_best_changes(
307 backend: &dyn Backend,
308 tag_name: &str,
309 release_body: Option<&str>,
310 changelog_content: Option<&str>,
311) -> (
312 Option<String>,
313 Option<ChangesSource>,
314 usize,
315 Vec<CommitInfo>,
316) {
317 let best_source = match (release_body, changelog_content) {
319 (Some(release), Some(changelog)) => {
321 if release.trim().len() >= changelog.trim().len() {
322 Some((release.to_string(), ChangesSource::GitHubRelease))
323 } else {
324 Some((changelog.to_string(), ChangesSource::Changelog))
325 }
326 }
327 (Some(release), None) if !release.trim().is_empty() => {
329 Some((release.to_string(), ChangesSource::GitHubRelease))
330 }
331 (None, Some(changelog)) if !changelog.trim().is_empty() => {
333 Some((changelog.to_string(), ChangesSource::Changelog))
334 }
335 _ => None,
337 };
338
339 if let Some((content, source)) = best_source {
340 let (truncated_content, remaining) = changelog::truncate_content(&content);
341 return (
342 Some(truncated_content.to_string()),
343 Some(source),
344 remaining,
345 Vec::new(),
346 );
347 }
348
349 if let Ok(Some(prev_tag)) = backend.find_previous_tag(tag_name).await
351 && let Ok(commits) = backend
352 .commits_between_tags(&prev_tag.name, tag_name, 5)
353 .await
354 && !commits.is_empty()
355 {
356 return (
357 None,
358 Some(ChangesSource::Commits {
359 previous_tag: prev_tag.name,
360 }),
361 0,
362 commits,
363 );
364 }
365
366 (None, None, 0, Vec::new())
368}
369
370async fn resolve_tag(backend: &dyn Backend, name: &str) -> WtgResult<IdentifiedThing> {
372 let tag = backend.find_tag(name).await?;
373
374 let changelog_content = backend.changelog_for_version(name).await;
376
377 let release_body = if tag.is_release {
379 backend.fetch_release_body(name).await
380 } else {
381 None
382 };
383
384 let (changes, source, truncated, commits) = select_best_changes(
386 backend,
387 name,
388 release_body.as_deref(),
389 changelog_content.as_deref(),
390 )
391 .await;
392
393 Ok(IdentifiedThing::Tag(Box::new(TagResult {
394 tag_info: tag.clone(),
395 github_url: tag.tag_url,
396 changes,
397 changes_source: source,
398 truncated_lines: truncated,
399 commits,
400 })))
401}