use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use argonauts_conductor::gh::RepoName;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IssueLink {
pub repo: RepoName,
pub number: u64,
pub title: String,
pub url: String,
pub branch: String,
pub default_branch: String,
pub login: String,
pub request: String,
#[serde(default)]
pub writes_allowed: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub as_read: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub push_remote: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fork_account: Option<String>,
}
impl IssueLink {
pub fn mark(title: &str, body: &str) -> String {
argonauts_core::hash::sha256_hex(format!("{title}\n{body}").as_bytes())
}
pub fn dir(state_dir: &Path) -> PathBuf {
state_dir.join("issues")
}
fn path(&self, state_dir: &Path) -> PathBuf {
Self::dir(state_dir).join(format!("{}.json", self.number))
}
pub fn save(&self, state_dir: &Path) -> io::Result<()> {
fs::create_dir_all(Self::dir(state_dir))?;
let text = serde_json::to_string_pretty(self).map_err(io::Error::other)?;
let path = self.path(state_dir);
let partial = path.with_extension("json.partial");
fs::write(&partial, text)?;
fs::rename(partial, path)
}
pub fn for_request(state_dir: &Path, request: &str) -> Option<Self> {
let entries = fs::read_dir(Self::dir(state_dir)).ok()?;
entries
.filter_map(Result::ok)
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "json"))
.filter_map(|entry| fs::read_to_string(entry.path()).ok())
.filter_map(|text| serde_json::from_str::<Self>(&text).ok())
.find(|link| link.request == request)
}
pub fn closing_line(&self, base: &str) -> String {
if base == self.default_branch {
format!("Fixes #{}", self.number)
} else {
format!("Refs #{}", self.number)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn link(number: u64, request: &str) -> IssueLink {
IssueLink {
repo: RepoName::parse("test-user/limiter").unwrap(),
number,
title: "Rate limiter shares one bucket across API keys".to_owned(),
url: format!("https://github.com/test-user/limiter/issues/{number}"),
branch: format!("issue-{number}-rate-limiter-shares"),
default_branch: "main".to_owned(),
login: "test-user".to_owned(),
request: request.to_owned(),
writes_allowed: false,
as_read: None,
push_remote: None,
fork_account: None,
}
}
#[test]
fn a_run_finds_its_link_by_its_request() {
let state = tempfile::tempdir().unwrap();
link(42, "work on 42").save(state.path()).unwrap();
link(45, "work on 45").save(state.path()).unwrap();
let found = IssueLink::for_request(state.path(), "work on 45").unwrap();
assert_eq!(found.number, 45);
assert!(IssueLink::for_request(state.path(), "something else").is_none());
link(42, "work on 42 again").save(state.path()).unwrap();
assert!(IssueLink::for_request(state.path(), "work on 42").is_none());
}
#[test]
fn the_closing_line_names_the_issue_as_github_reads_it() {
let link = link(42, "");
assert_eq!(link.closing_line("main"), "Fixes #42");
assert_eq!(link.closing_line("release"), "Refs #42");
}
}