Skip to main content

sloc_git/
schedule.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright (C) 2026 Nima Shafie <nimzshafie@gmail.com>
3
4use std::path::Path;
5
6use anyhow::Result;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11// ── enums ─────────────────────────────────────────────────────────────────────
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
14#[serde(rename_all = "snake_case")]
15pub enum ScanScheduleKind {
16    Webhook,
17    Poll,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21pub enum ScanScheduleProvider {
22    #[serde(rename = "github")]
23    GitHub,
24    #[serde(rename = "gitlab")]
25    GitLab,
26    #[serde(rename = "bitbucket")]
27    Bitbucket,
28    #[serde(rename = "any")]
29    Any,
30}
31
32impl ScanScheduleProvider {
33    pub fn display_name(&self) -> &'static str {
34        match self {
35            Self::GitHub => "GitHub",
36            Self::GitLab => "GitLab",
37            Self::Bitbucket => "Bitbucket",
38            Self::Any => "Any / Poll",
39        }
40    }
41}
42
43// ── schedule ──────────────────────────────────────────────────────────────────
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ScanSchedule {
47    pub id: Uuid,
48    pub label: String,
49    pub repo_url: String,
50    pub branch: String,
51    pub kind: ScanScheduleKind,
52    pub provider: ScanScheduleProvider,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub webhook_secret: Option<String>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub interval_secs: Option<u64>,
57    pub last_scan_sha: Option<String>,
58    pub last_scan_at: Option<DateTime<Utc>>,
59    pub last_run_id: Option<String>,
60    pub enabled: bool,
61}
62
63impl ScanSchedule {
64    pub fn new_webhook(
65        repo_url: String,
66        branch: String,
67        provider: ScanScheduleProvider,
68        label: String,
69    ) -> Self {
70        Self {
71            id: Uuid::new_v4(),
72            label,
73            repo_url,
74            branch,
75            kind: ScanScheduleKind::Webhook,
76            provider,
77            webhook_secret: Some(generate_secret()),
78            interval_secs: None,
79            last_scan_sha: None,
80            last_scan_at: None,
81            last_run_id: None,
82            enabled: true,
83        }
84    }
85
86    pub fn new_poll(repo_url: String, branch: String, interval_secs: u64, label: String) -> Self {
87        Self {
88            id: Uuid::new_v4(),
89            label,
90            repo_url,
91            branch,
92            kind: ScanScheduleKind::Poll,
93            provider: ScanScheduleProvider::Any,
94            webhook_secret: None,
95            interval_secs: Some(interval_secs),
96            last_scan_sha: None,
97            last_scan_at: None,
98            last_run_id: None,
99            enabled: true,
100        }
101    }
102}
103
104fn generate_secret() -> String {
105    format!("{}-{}", Uuid::new_v4().simple(), Uuid::new_v4().simple())
106}
107
108// ── store ─────────────────────────────────────────────────────────────────────
109
110#[derive(Debug, Default, Serialize, Deserialize)]
111pub struct ScheduleStore {
112    pub schedules: Vec<ScanSchedule>,
113}
114
115impl ScheduleStore {
116    pub fn load(path: &Path) -> Self {
117        std::fs::read_to_string(path)
118            .ok()
119            .and_then(|s| serde_json::from_str(&s).ok())
120            .unwrap_or_default()
121    }
122
123    pub fn save(&self, path: &Path) -> Result<()> {
124        let json = serde_json::to_string_pretty(self)?;
125        std::fs::write(path, json)?;
126        Ok(())
127    }
128
129    pub fn find_matching<'a>(&'a self, repo_url: &str, branch: &str) -> Vec<&'a ScanSchedule> {
130        self.schedules
131            .iter()
132            .filter(|s| s.enabled && urls_match(&s.repo_url, repo_url) && s.branch == branch)
133            .collect()
134    }
135
136    pub fn by_id_mut(&mut self, id: Uuid) -> Option<&mut ScanSchedule> {
137        self.schedules.iter_mut().find(|s| s.id == id)
138    }
139
140    pub fn remove(&mut self, id: Uuid) {
141        self.schedules.retain(|s| s.id != id);
142    }
143}
144
145fn urls_match(a: &str, b: &str) -> bool {
146    normalize_url(a) == normalize_url(b)
147}
148
149fn normalize_url(url: &str) -> String {
150    url.trim_end_matches('/')
151        .trim_end_matches(".git")
152        .to_lowercase()
153}