Stub rescheduling algo

This commit is contained in:
Rahix 2026-03-01 16:42:18 +01:00
parent b3e0270e38
commit adc6b7866a
4 changed files with 186 additions and 12 deletions

View file

@ -1,6 +1,7 @@
use crate::task; use crate::task;
use anyhow::Context as _; use anyhow::Context as _;
use forgejo_api::structs::Issue;
pub async fn collect_tasks(ctx: &crate::Context) -> anyhow::Result<Vec<task::Task>> { pub async fn collect_tasks(ctx: &crate::Context) -> anyhow::Result<Vec<task::Task>> {
let issues = list_all_issues(ctx).await?; let issues = list_all_issues(ctx).await?;
@ -20,18 +21,88 @@ pub async fn collect_tasks(ctx: &crate::Context) -> anyhow::Result<Vec<task::Tas
Ok(tasks) Ok(tasks)
} }
fn task_from_issue(issue: &forgejo_api::structs::Issue) -> anyhow::Result<task::Task> { fn task_from_issue(issue: &Issue) -> anyhow::Result<task::Task> {
Ok(task::Task { Ok(task::Task {
issue_number: issue issue_number: issue.get_number()?,
.number title: issue.get_title()?,
.context("Missing issue number")? state: task_state_from_issue(issue)?,
.try_into() recurring: task_recurring_from_issue_labels(issue)?,
.context("Failed converting issue number")?,
title: issue.title.as_ref().context("Missing issue title")?.clone(),
}) })
} }
async fn list_all_issues(ctx: &crate::Context) -> anyhow::Result<Vec<forgejo_api::structs::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,
}),
forgejo_api::structs::StateType::Closed => Ok(task::State::Completed {
date: 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 let issues = ctx
.forgejo .forgejo
.issue_list_issues( .issue_list_issues(
@ -51,3 +122,37 @@ async fn list_all_issues(ctx: &crate::Context) -> anyhow::Result<Vec<forgejo_api
Ok(issues) Ok(issues)
} }
trait IssueExt {
fn get_number(&self) -> anyhow::Result<u64>;
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<u64> {
Ok(self
.number
.context("Missing issue number")?
.try_into()
.context("Failed converting issue number to u64")?)
}
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)
}
}

View file

@ -13,7 +13,7 @@ async fn run() -> anyhow::Result<()> {
let tasks = collect::collect_tasks(&ctx).await?; let tasks = collect::collect_tasks(&ctx).await?;
scheduler::schedule_recurring_tasks(&ctx, &tasks).await?; scheduler::reschedule_recurring_tasks(&ctx, &tasks).await?;
Ok(()) Ok(())
} }

View file

@ -1,9 +1,40 @@
use crate::task; use crate::task;
pub async fn schedule_recurring_tasks( pub async fn reschedule_recurring_tasks(
ctx: &crate::Context, ctx: &crate::Context,
tasks: &[task::Task], tasks: &[task::Task],
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
dbg!(tasks); let mut rescheduled = 0;
for task in tasks {
let task::State::Completed {
date: completed_date,
} = &task.state
else {
continue;
};
let Some(recurring) = &task.recurring else {
continue;
};
let due_date = next_due_date(*completed_date, recurring.interval);
log::info!("Rescheduling {task} for {due_date}...");
reopen_issue_with_due_date(ctx, task, due_date).await?;
rescheduled += 1;
}
if rescheduled == 0 {
log::info!("No tasks need rescheduling.");
}
Ok(())
}
fn next_due_date(completed_date: time::OffsetDateTime, interval: task::RecurringInterval) -> time::OffsetDateTime {
todo!()
}
async fn reopen_issue_with_due_date(ctx: &crate::Context, task: &task::Task, due_date: time::OffsetDateTime) -> anyhow::Result<()> {
todo!() todo!()
} }

View file

@ -1,5 +1,43 @@
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone)]
pub struct Task { pub struct Task {
/// Issue Number for referencing the task
pub issue_number: u64, pub issue_number: u64,
/// Human-readable summary of the task
pub title: String, pub title: String,
/// Whether the task is open or has been completed
pub state: State,
/// Whether the task is a recurring one and metadata for rescheduling
pub recurring: Option<Recurring>,
}
#[derive(Debug, Clone)]
pub enum State {
/// The task is open and pending completion.
///
/// An optional due date may be present.
Open { due: Option<time::OffsetDateTime> },
/// The task has been completed at the specified time.
Completed { date: time::OffsetDateTime },
}
#[derive(Debug, Clone)]
pub struct Recurring {
pub interval: RecurringInterval,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RecurringInterval {
Months(u32),
Weeks(u32),
Days(u32),
}
impl std::fmt::Display for Task {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "#{} — \"{}\"", self.issue_number, self.title)
}
} }