use anyhow::Context as _; /// Read all issues to generate the full techtree pub async fn collect_tree(ctx: &crate::Context) -> anyhow::Result { 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("".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 { 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, }) }