First working rescheduling

This commit is contained in:
Rahix 2026-03-01 18:18:49 +01:00
parent adc6b7866a
commit b25857f3af
7 changed files with 85 additions and 20 deletions

26
Cargo.lock generated
View file

@ -217,6 +217,7 @@ dependencies = [
"anyhow", "anyhow",
"env_logger", "env_logger",
"forgejo-api", "forgejo-api",
"jiff",
"log", "log",
"serde", "serde",
"serde_json", "serde_json",
@ -713,28 +714,45 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]] [[package]]
name = "jiff" name = "jiff"
version = "0.2.20" version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" checksum = "819b44bc7c87d9117eb522f14d46e918add69ff12713c475946b0a29363ed1c2"
dependencies = [ dependencies = [
"jiff-static", "jiff-static",
"jiff-tzdb-platform",
"log", "log",
"portable-atomic", "portable-atomic",
"portable-atomic-util", "portable-atomic-util",
"serde_core", "serde_core",
"windows-sys 0.61.2",
] ]
[[package]] [[package]]
name = "jiff-static" name = "jiff-static"
version = "0.2.20" version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" checksum = "470252db18ecc35fd766c0891b1e3ec6cbbcd62507e85276c01bf75d8e94d4a1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
] ]
[[package]]
name = "jiff-tzdb"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68971ebff725b9e2ca27a601c5eb38a4c5d64422c4cbab0c535f248087eda5c2"
[[package]]
name = "jiff-tzdb-platform"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
dependencies = [
"jiff-tzdb",
]
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.87" version = "0.3.87"

View file

@ -16,3 +16,4 @@ serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140" serde_json = "1.0.140"
tokio = { version = "1.45.0", features = ["full"] } tokio = { version = "1.45.0", features = ["full"] }
time = "0.3.41" time = "0.3.41"
jiff = "0.2.22"

View file

@ -1,4 +1,5 @@
use crate::task; use crate::task;
use crate::util;
use anyhow::Context as _; use anyhow::Context as _;
use forgejo_api::structs::Issue; use forgejo_api::structs::Issue;
@ -33,12 +34,12 @@ fn task_from_issue(issue: &Issue) -> anyhow::Result<task::Task> {
fn task_state_from_issue(issue: &Issue) -> anyhow::Result<task::State> { fn task_state_from_issue(issue: &Issue) -> anyhow::Result<task::State> {
match issue.get_state()? { match issue.get_state()? {
forgejo_api::structs::StateType::Open => Ok(task::State::Open { forgejo_api::structs::StateType::Open => Ok(task::State::Open {
due: issue.due_date, due: issue.due_date.map(util::time_to_jiff),
}), }),
forgejo_api::structs::StateType::Closed => Ok(task::State::Completed { forgejo_api::structs::StateType::Closed => Ok(task::State::Completed {
date: issue date: util::time_to_jiff(issue
.closed_at .closed_at
.context("Closed issue without a closed_at date")?, .context("Closed issue without a closed_at date")?),
}), }),
} }
} }
@ -124,19 +125,19 @@ async fn list_all_issues(ctx: &crate::Context) -> anyhow::Result<Vec<Issue>> {
} }
trait IssueExt { trait IssueExt {
fn get_number(&self) -> anyhow::Result<u64>; fn get_number(&self) -> anyhow::Result<u32>;
fn get_title(&self) -> anyhow::Result<String>; fn get_title(&self) -> anyhow::Result<String>;
fn get_state(&self) -> anyhow::Result<forgejo_api::structs::StateType>; fn get_state(&self) -> anyhow::Result<forgejo_api::structs::StateType>;
fn get_label_names(&self) -> anyhow::Result<Vec<String>>; fn get_label_names(&self) -> anyhow::Result<Vec<String>>;
} }
impl IssueExt for Issue { impl IssueExt for Issue {
fn get_number(&self) -> anyhow::Result<u64> { fn get_number(&self) -> anyhow::Result<u32> {
Ok(self Ok(self
.number .number
.context("Missing issue number")? .context("Missing issue number")?
.try_into() .try_into()
.context("Failed converting issue number to u64")?) .context("Failed converting issue number to u32")?)
} }
fn get_title(&self) -> anyhow::Result<String> { fn get_title(&self) -> anyhow::Result<String> {

View file

@ -5,6 +5,7 @@ mod collect;
mod ci_meta; mod ci_meta;
mod context; mod context;
mod scheduler; mod scheduler;
mod util;
use context::Context; use context::Context;

View file

@ -1,4 +1,7 @@
use crate::task; use crate::task;
use crate::util;
use anyhow::Context as _;
pub async fn reschedule_recurring_tasks( pub async fn reschedule_recurring_tasks(
ctx: &crate::Context, ctx: &crate::Context,
@ -17,10 +20,10 @@ pub async fn reschedule_recurring_tasks(
continue; continue;
}; };
let due_date = next_due_date(*completed_date, recurring.interval); let due_date = next_due_date(completed_date, recurring.interval);
log::info!("Rescheduling {task} for {due_date}..."); log::info!("Rescheduling {task} for {due_date}...");
reopen_issue_with_due_date(ctx, task, due_date).await?;
reopen_issue_with_due_date(ctx, task, &due_date).await?;
rescheduled += 1; rescheduled += 1;
} }
@ -31,10 +34,41 @@ pub async fn reschedule_recurring_tasks(
Ok(()) Ok(())
} }
fn next_due_date(completed_date: time::OffsetDateTime, interval: task::RecurringInterval) -> time::OffsetDateTime { fn next_due_date(completed_date: &jiff::Zoned, interval: task::RecurringInterval) -> jiff::Zoned {
todo!() let span = match interval {
task::RecurringInterval::Months(m) => jiff::Span::new().months(m),
task::RecurringInterval::Weeks(w) => jiff::Span::new().weeks(w),
task::RecurringInterval::Days(d) => jiff::Span::new().days(d),
};
completed_date + span
} }
async fn reopen_issue_with_due_date(ctx: &crate::Context, task: &task::Task, due_date: time::OffsetDateTime) -> anyhow::Result<()> { async fn reopen_issue_with_due_date(
todo!() ctx: &crate::Context,
task: &task::Task,
due_date: &jiff::Zoned,
) -> anyhow::Result<()> {
ctx.forgejo
.issue_edit_issue(
&ctx.owner,
&ctx.repo,
task.issue_number.into(),
forgejo_api::structs::EditIssueOption {
due_date: Some(util::jiff_to_time(due_date)),
state: Some("open".to_owned()),
assignee: None,
assignees: None,
body: None,
milestone: None,
r#ref: None,
title: None,
unset_due_date: None,
updated_at: None,
},
)
.await
.context("Failed reopening recurring issue")?;
Ok(())
} }

View file

@ -1,7 +1,7 @@
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Task { pub struct Task {
/// Issue Number for referencing the task /// Issue Number for referencing the task
pub issue_number: u64, pub issue_number: u32,
/// Human-readable summary of the task /// Human-readable summary of the task
pub title: String, pub title: String,
@ -18,10 +18,10 @@ pub enum State {
/// The task is open and pending completion. /// The task is open and pending completion.
/// ///
/// An optional due date may be present. /// An optional due date may be present.
Open { due: Option<time::OffsetDateTime> }, Open { due: Option<jiff::Zoned> },
/// The task has been completed at the specified time. /// The task has been completed at the specified time.
Completed { date: time::OffsetDateTime }, Completed { date: jiff::Zoned },
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

10
src/util.rs Normal file
View file

@ -0,0 +1,10 @@
pub fn time_to_jiff(t: time::OffsetDateTime) -> jiff::Zoned {
let tz = jiff::tz::TimeZone::fixed(jiff::tz::offset(t.offset().whole_hours()));
jiff::Timestamp::new(t.unix_timestamp(), 0).unwrap()
.to_zoned(tz)
}
pub fn jiff_to_time(t: &jiff::Zoned) -> time::OffsetDateTime {
time::OffsetDateTime::from_unix_timestamp(t.timestamp().as_second()).unwrap()
}