159 lines
5.8 KiB
Rust
159 lines
5.8 KiB
Rust
use crate::task;
|
|
use crate::util;
|
|
|
|
use anyhow::Context as _;
|
|
use forgejo_api::structs::Issue;
|
|
|
|
pub async fn collect_tasks(ctx: &crate::Context) -> anyhow::Result<Vec<task::Task>> {
|
|
let issues = list_all_issues(ctx).await?;
|
|
|
|
let tasks = issues
|
|
.into_iter()
|
|
.map(|issue| {
|
|
task_from_issue(&issue).with_context(|| {
|
|
format!(
|
|
"Error while converting issue #{} to task",
|
|
issue.number.unwrap_or(-1)
|
|
)
|
|
})
|
|
})
|
|
.collect::<anyhow::Result<Vec<_>>>()?;
|
|
|
|
Ok(tasks)
|
|
}
|
|
|
|
fn task_from_issue(issue: &Issue) -> anyhow::Result<task::Task> {
|
|
Ok(task::Task {
|
|
issue_number: issue.get_number()?,
|
|
title: issue.get_title()?,
|
|
state: task_state_from_issue(issue)?,
|
|
recurring: task_recurring_from_issue_labels(issue)?,
|
|
})
|
|
}
|
|
|
|
fn task_state_from_issue(issue: &Issue) -> anyhow::Result<task::State> {
|
|
match issue.get_state()? {
|
|
forgejo_api::structs::StateType::Open => Ok(task::State::Open {
|
|
due: issue.due_date.map(util::time_to_jiff),
|
|
}),
|
|
forgejo_api::structs::StateType::Closed => Ok(task::State::Completed {
|
|
date: util::time_to_jiff(issue
|
|
.closed_at
|
|
.context("Closed issue without a closed_at date")?),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn task_recurring_from_issue_labels(issue: &Issue) -> anyhow::Result<Option<task::Recurring>> {
|
|
let labels = issue.get_label_names()?;
|
|
let Some(recurring_label) = get_recurring_label(&labels)? else {
|
|
return Ok(None)
|
|
};
|
|
|
|
let interval = match recurring_label {
|
|
// Months
|
|
"Every Month" => task::RecurringInterval::Months(1),
|
|
"Every 2 Months" => task::RecurringInterval::Months(2),
|
|
"Every 3 Months" => task::RecurringInterval::Months(3),
|
|
"Every 4 Months" => task::RecurringInterval::Months(4),
|
|
"Every 5 Months" => task::RecurringInterval::Months(5),
|
|
"Every 6 Months" => task::RecurringInterval::Months(6),
|
|
"Every 7 Months" => task::RecurringInterval::Months(7),
|
|
"Every 8 Months" => task::RecurringInterval::Months(8),
|
|
"Every 9 Months" => task::RecurringInterval::Months(9),
|
|
"Every 10 Months" => task::RecurringInterval::Months(10),
|
|
"Every 11 Months" => task::RecurringInterval::Months(11),
|
|
"Every Year" => task::RecurringInterval::Months(12),
|
|
// Weeks
|
|
"Every Week" => task::RecurringInterval::Weeks(1),
|
|
"Every 2 Weeks" => task::RecurringInterval::Weeks(2),
|
|
"Every 3 Weeks" => task::RecurringInterval::Weeks(3),
|
|
"Every 4 Weeks" => task::RecurringInterval::Weeks(4),
|
|
"Every 5 Weeks" => task::RecurringInterval::Weeks(5),
|
|
"Every 6 Weeks" => task::RecurringInterval::Weeks(6),
|
|
"Every 7 Weeks" => task::RecurringInterval::Weeks(7),
|
|
"Every 8 Weeks" => task::RecurringInterval::Weeks(8),
|
|
"Every 9 Weeks" => task::RecurringInterval::Weeks(9),
|
|
"Every 10 Weeks" => task::RecurringInterval::Weeks(10),
|
|
// Days
|
|
"Every Day" => task::RecurringInterval::Days(1),
|
|
"Every 2 Days" => task::RecurringInterval::Days(2),
|
|
"Every 3 Days" => task::RecurringInterval::Days(3),
|
|
"Every 4 Days" => task::RecurringInterval::Days(4),
|
|
"Every 5 Days" => task::RecurringInterval::Days(5),
|
|
"Every 6 Days" => task::RecurringInterval::Days(6),
|
|
// Fallback
|
|
s => anyhow::bail!("Unknown recurring interval: {s:?}"),
|
|
};
|
|
|
|
Ok(Some(task::Recurring { interval }))
|
|
}
|
|
|
|
fn get_recurring_label(labels: &[String]) -> anyhow::Result<Option<&str>> {
|
|
let mut recurring_labels_iter = labels
|
|
.iter()
|
|
.filter_map(|label| label.strip_prefix("Recurring/"));
|
|
let Some(recurring_label) = recurring_labels_iter.next() else {
|
|
// No recurring label means this is not a recurring task
|
|
return Ok(None);
|
|
};
|
|
if recurring_labels_iter.next().is_some() {
|
|
anyhow::bail!("More than one Recurring/ label found on issue");
|
|
}
|
|
Ok(Some(recurring_label))
|
|
}
|
|
|
|
async fn list_all_issues(ctx: &crate::Context) -> anyhow::Result<Vec<Issue>> {
|
|
let issues = 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),
|
|
// Only issues
|
|
r#type: Some(forgejo_api::structs::IssueListIssuesQueryType::Issues),
|
|
..Default::default()
|
|
},
|
|
)
|
|
.all()
|
|
.await
|
|
.with_context(|| format!("Failed fetching issue list"))?;
|
|
|
|
Ok(issues)
|
|
}
|
|
|
|
trait IssueExt {
|
|
fn get_number(&self) -> anyhow::Result<u32>;
|
|
fn get_title(&self) -> anyhow::Result<String>;
|
|
fn get_state(&self) -> anyhow::Result<forgejo_api::structs::StateType>;
|
|
fn get_label_names(&self) -> anyhow::Result<Vec<String>>;
|
|
}
|
|
|
|
impl IssueExt for Issue {
|
|
fn get_number(&self) -> anyhow::Result<u32> {
|
|
Ok(self
|
|
.number
|
|
.context("Missing issue number")?
|
|
.try_into()
|
|
.context("Failed converting issue number to u32")?)
|
|
}
|
|
|
|
fn get_title(&self) -> anyhow::Result<String> {
|
|
Ok(self.title.as_ref().context("Missing issue title")?.clone())
|
|
}
|
|
|
|
fn get_state(&self) -> anyhow::Result<forgejo_api::structs::StateType> {
|
|
Ok(self.state.context("Issue has no state")?)
|
|
}
|
|
|
|
fn get_label_names(&self) -> anyhow::Result<Vec<std::string::String>> {
|
|
let labels = self.labels.as_ref().context("Issue without labels list")?;
|
|
let label_names = labels
|
|
.into_iter()
|
|
.map(|label| label.name.as_ref().context("Label without name").cloned())
|
|
.collect::<anyhow::Result<Vec<_>>>()?;
|
|
Ok(label_names)
|
|
}
|
|
}
|