techtree/techtree-manager/src/collect.rs
Rahix b66e8c8d9c Replace dirty unwrap() with expect("TODO")
This makes it obvious where better error handling is still needed.
2025-12-09 15:29:21 +01:00

160 lines
5.2 KiB
Rust

use anyhow::Context as _;
/// Read all issues to generate the full techtree
pub async fn collect_tree(ctx: &crate::Context) -> anyhow::Result<crate::tree::Tree> {
let mut issues = vec![];
for page in 1.. {
let new = ctx
.forgejo
.issue_list_issues(
&ctx.owner,
&ctx.repo,
forgejo_api::structs::IssueListIssuesQuery {
// We also want the closed issues
state: Some(forgejo_api::structs::IssueListIssuesQueryState::All),
// We cannot turn off pagination entirely, but let's set the limit as high as
// Forgejo lets us.
limit: Some(10000),
page: Some(page),
// Only issues
r#type: Some(forgejo_api::structs::IssueListIssuesQueryType::Issues),
..Default::default()
},
)
.await
.with_context(|| format!("Failed fetching page {page} of the issue list"))?;
if new.len() == 0 {
break;
}
issues.extend(new);
}
let mut tree = crate::tree::Tree::new();
let mut issue_numbers = Vec::new();
for issue in issues.iter() {
let element = match element_from_issue(issue) {
Ok(el) => el,
Err(e) => {
let maybe_number = issue
.number
.map(|n| n.to_string())
.unwrap_or("<unknown>".to_owned());
log::warn!("Failed processing issue #{maybe_number}: {e:?}");
continue;
}
};
issue_numbers.push(element.issue_number);
tree.add_element(element);
}
for issue in issue_numbers.into_iter() {
let dependencies = ctx
.forgejo
.issue_list_issue_dependencies(
&ctx.owner,
&ctx.repo,
// Why the hell is the issue number a string here?
&issue.to_string(),
forgejo_api::structs::IssueListIssueDependenciesQuery {
limit: Some(10000),
..Default::default()
},
)
.await
.with_context(|| format!("Failed to fetch issue dependencies for #{issue}",))?;
for dep in dependencies {
// Check that the dependency is actually an issue in the techtree and not external
let dep_repo = dep
.repository
.context("Dependency issue without repository info")?;
if dep_repo.owner.as_ref() != Some(&ctx.owner)
|| dep_repo.name.as_ref() != Some(&ctx.repo)
{
log::warn!(
"Issue #{issue} depends on external {}#{}, ignoring.",
dep_repo.full_name.as_deref().unwrap_or("unknown?"),
dep.number.unwrap_or(9999)
);
continue;
}
let dep_number = dep.number.context("Missing issue number in dependency")?;
if !tree.find_element_by_issue_number(dep_number).is_some() {
log::warn!("Found dependency from #{issue} on non-tracked issue #{dep_number}!");
} else {
tree.add_dependency_by_issue_number(issue, dep_number);
}
}
}
Ok(tree)
}
fn element_from_issue(issue: &forgejo_api::structs::Issue) -> anyhow::Result<crate::tree::Element> {
let issue_number = issue.number.context("Missing issue number")?;
let description = issue
.title
.as_deref()
.context("Issue is missing a title")?
.to_owned();
let labels = issue
.labels
.as_ref()
.context("Issue does not have any labels")?;
let ty_labels: Vec<_> = labels
.iter()
.filter_map(|l| l.name.as_deref())
.filter(|l| l.starts_with("Type/"))
.collect();
if ty_labels.len() == 0 {
anyhow::bail!("Issue #{issue_number} has no type label!");
}
if ty_labels.len() > 1 {
anyhow::bail!("Issue #{issue_number} has more than one type label!");
}
let ty = ty_labels
.first()
.expect("TODO")
.strip_prefix("Type/")
.expect("TODO")
.to_owned();
let has_completed_label = labels
.iter()
.any(|l| l.name.as_deref() == Some("Completed"));
let status = match issue.state.context("Missing issue state")? {
forgejo_api::structs::StateType::Open => {
if has_completed_label {
crate::tree::ElementStatus::Completed
} else if issue.assignee.is_some()
|| issue
.assignees
.as_ref()
.map(|v| v.len() > 0)
.unwrap_or(false)
{
crate::tree::ElementStatus::Assigned
} else {
crate::tree::ElementStatus::Missing
}
}
forgejo_api::structs::StateType::Closed => anyhow::bail!("Ignoring closed issue!"),
};
Ok(crate::tree::Element {
issue_number,
description,
ty,
status,
})
}