use std::fs; use std::io::{self, Read}; use std::path::Path; use cli_table::format::Justify; use cli_table::{print_stderr, Cell, Style, Table}; use color_eyre::eyre::{self, Context}; use color_eyre::{Result, Section as _, SectionExt as _}; use reqwest::blocking::Response; use serde::{Deserialize, Serialize}; use serde_json::json; use time::OffsetDateTime; use url::Url; use crate::types::PullRequest; use crate::util::make_client; use crate::Action; #[derive(Deserialize, Serialize, Debug)] struct Source { access_token: String, owner: String, repo: String, url: Url, } #[derive(Deserialize, Serialize, Debug)] struct Version { prs: String, #[serde(with = "time::serde::iso8601")] timestamp: OffsetDateTime, } impl<'a, I: Iterator> From for Version { fn from(prs: I) -> Self { Self { prs: prs.fold(String::new(), |mut s, pr| { if !s.is_empty() { s.push(','); } s.push_str(&pr.number.to_string()); s }), timestamp: OffsetDateTime::now_utc(), } } } #[derive(Deserialize, Serialize, Debug)] struct Config { /// Resource configuration. /// Passed verbatim from the definition in the pipeline. source: Source, /// For 'check': /// Previous version of this resource. /// Will be empty for the very first request. /// For 'in': /// The version metadata to create this as. version: Option, } fn fetch(src: &Source) -> Result { let client = make_client(&src.access_token).wrap_err("Failed to create HTTP client")?; let url = src .url .join(&format!("api/v1/repos/{}/{}/pulls", src.owner, src.repo)) .wrap_err("Invalid URL")?; client .get(url) .query(&[("sort", "oldest"), ("state", "open")]) .send() .map_err(From::from) } fn action_check(conf: Config) -> Result<()> { let prs: Vec = fetch(&conf.source)?.json()?; let version = Version::from(prs.iter()); let out = if let Some(prev) = conf.version { if prev.prs == version.prs { vec![prev] } else { vec![prev, version] } } else { vec![version] }; let table: Vec<_> = prs .iter() .map(|pr| { vec![ pr.number.cell().justify(Justify::Center), pr.title.clone().cell(), pr.user.login.clone().cell(), pr.base.r#ref.clone().cell(), pr.head.r#ref.clone().cell(), ] }) .collect(); let table = table .table() .title(vec![ "#".cell().bold(true), "Title".cell().bold(true), "User".cell().bold(true), "Base".cell().bold(true), "Head".cell().bold(true), ]) .bold(true); let _ = print_stderr(table); serde_json::to_writer_pretty(io::stdout(), &out) .wrap_err("Failed to write result to stdout")?; Ok(()) } fn action_in(conf: Config, dest: impl AsRef) -> Result<()> { let old_version = if let Some(version) = conf.version { version } else { eyre::bail!("Version missing in 'in' action."); }; let bytes = fetch(&conf.source)?.bytes()?; let prs: Vec = serde_json::from_slice(&bytes)?; let version = Version::from(prs.iter()); { if version.prs != old_version.prs { eyre::bail!("Version to fetch does not match current resource."); } } let path = dest.as_ref().join("prs.json"); let _ = fs::create_dir_all(dest); fs::write(&path, &bytes)?; let out = json!({ "version": version, }); serde_json::to_writer_pretty(io::stdout(), &out)?; Ok(()) } pub(crate) fn run(action: &Action) -> Result<()> { let config: Config = { let mut buf = String::new(); io::stdin() .read_to_string(&mut buf) .wrap_err("Failed to read from stdin")?; if buf.is_empty() { eyre::bail!("No data received on stdin"); } serde_json::from_str(&buf) .wrap_err("Failed to parse config") .with_suggestion(|| "Double-check the `source` and `params` sections in your pipeline") .with_section(|| buf.header("JSON"))? }; match action { Action::Check => action_check(config), Action::In { dest } => action_in(config, dest), Action::Out { src: _ } => Ok(()), } }