use std::collections::hash_map::DefaultHasher;
use std::fs::{File, OpenOptions};
use std::hash::{Hash, Hasher};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use ossctl_core::ports::{
Clock, CommandOutput, CommandRunner, Fs, GitRepo, IdGen, JournalLock, JournalStore,
RegistryQuery, Tagger,
};
pub struct RealCommandRunner;
impl CommandRunner for RealCommandRunner {
fn run(&self, program: &str, args: &[&str], cwd: &Path) -> io::Result<CommandOutput> {
let out = Command::new(program)
.args(args)
.current_dir(cwd)
.stdin(Stdio::null())
.env("GIT_TERMINAL_PROMPT", "0")
.env("GIT_ASKPASS", "true")
.env("GH_PROMPT_DISABLED", "1")
.env("GH_NO_UPDATE_NOTIFIER", "1")
.output()?;
Ok(CommandOutput {
status: out.status.code(),
stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
})
}
}
pub struct RealFs;
impl Fs for RealFs {
fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
std::fs::read(path)
}
fn exists(&self, path: &Path) -> bool {
path.exists()
}
fn is_dir(&self, path: &Path) -> bool {
path.is_dir()
}
fn is_file(&self, path: &Path) -> bool {
path.is_file()
}
fn read_dir(&self, dir: &Path) -> io::Result<Vec<String>> {
let mut names = Vec::new();
for entry in std::fs::read_dir(dir)? {
names.push(entry?.file_name().to_string_lossy().into_owned());
}
names.sort();
Ok(names)
}
}
pub struct RealGitRepo {
root: PathBuf,
}
impl RealGitRepo {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
fn git(&self, args: &[&str]) -> io::Result<std::process::Output> {
Command::new("git")
.arg("-C")
.arg(&self.root)
.args(args)
.stdin(Stdio::null())
.env("GIT_TERMINAL_PROMPT", "0")
.env("GIT_ASKPASS", "true")
.env("GIT_OPTIONAL_LOCKS", "0")
.output()
}
fn git_stdout(&self, args: &[&str]) -> io::Result<String> {
let out = self.git(args)?;
if out.status.success() {
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
} else {
Err(io::Error::other(format!(
"git {} exited {:?}",
args.join(" "),
out.status.code()
)))
}
}
}
impl GitRepo for RealGitRepo {
fn head_commit(&self) -> io::Result<String> {
let head = self.git_stdout(&["rev-parse", "HEAD"])?.trim().to_string();
if head.is_empty() {
return Err(io::Error::other("git rev-parse HEAD produced no output"));
}
Ok(head)
}
fn is_work_tree(&self) -> bool {
self.git(&["rev-parse", "--is-inside-work-tree"])
.is_ok_and(|o| o.status.success())
}
fn shortlog(&self, since: Option<&str>) -> io::Result<String> {
let mut args: Vec<String> = vec!["shortlog".into(), "-sne".into(), "--all".into()];
if let Some(s) = since {
args.push(format!("--since={s}"));
}
args.push("HEAD".into());
let borrowed: Vec<&str> = args.iter().map(String::as_str).collect();
self.git_stdout(&borrowed)
}
fn tags(&self) -> io::Result<Vec<String>> {
Ok(self
.git_stdout(&["tag", "--list"])?
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.map(str::to_string)
.collect())
}
fn git_common_dir(&self) -> io::Result<PathBuf> {
let raw = self.git_stdout(&["rev-parse", "--git-common-dir"])?;
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(io::Error::other(
"git rev-parse --git-common-dir produced no output",
));
}
let path = Path::new(trimmed);
Ok(if path.is_absolute() {
path.to_path_buf()
} else {
self.root.join(path)
})
}
}
pub struct RealClock;
impl Clock for RealClock {
fn now_unix(&self) -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs())
}
}
pub struct RealIdGen;
static ID_COUNTER: AtomicU64 = AtomicU64::new(0);
impl IdGen for RealIdGen {
fn new_id(&self) -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let ms = u64::try_from(now.as_millis()).unwrap_or(u64::MAX) & ((1 << 48) - 1);
let counter = ID_COUNTER.fetch_add(1, Ordering::Relaxed);
let anchor = 0u8;
let addr = std::ptr::addr_of!(anchor) as usize;
let pid = std::process::id();
let mut h1 = DefaultHasher::new();
(now.subsec_nanos(), counter, ms, addr, pid).hash(&mut h1);
let mut h2 = DefaultHasher::new();
(counter, h1.finish(), addr, ms, pid).hash(&mut h2);
let rand80 = (u128::from(h1.finish()) << 16) | u128::from(h2.finish() & 0xffff);
let value = (u128::from(ms) << 80) | (rand80 & ((1 << 80) - 1));
crockford_u128(value)
}
}
fn crockford_u128(mut value: u128) -> String {
const ALPHABET: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
let mut buf = [0u8; 26];
for slot in buf.iter_mut().rev() {
*slot = ALPHABET[(value & 0x1f) as usize];
value >>= 5;
}
String::from_utf8(buf.to_vec()).expect("crockford alphabet is ASCII")
}
pub struct RealRegistryQuery;
impl RealRegistryQuery {
fn npm_versions(package: &str) -> io::Result<Vec<String>> {
if package.starts_with('-') {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"refusing to query npm for a package name that looks like a flag: {package:?}"
),
));
}
let out = Command::new("npm")
.args(["view", "--json", package, "versions"])
.stdin(Stdio::null())
.env("NO_UPDATE_NOTIFIER", "1")
.env("NPM_CONFIG_FUND", "false")
.output()?;
if !out.status.success() {
return Err(io::Error::other(format!(
"npm view {package} versions exited {:?}",
out.status.code()
)));
}
let stdout = String::from_utf8_lossy(&out.stdout);
match serde_json::from_str::<serde_json::Value>(stdout.trim()) {
Ok(serde_json::Value::Array(items)) => items
.into_iter()
.map(|v| {
v.as_str()
.map(str::to_string)
.ok_or_else(|| io::Error::other("npm returned a non-string version entry"))
})
.collect(),
Ok(serde_json::Value::String(v)) => Ok(vec![v]),
_ => Err(io::Error::other(format!(
"could not parse `npm view --json {package} versions` output"
))),
}
}
}
impl RegistryQuery for RealRegistryQuery {
fn published_versions(&self, ecosystem: &str, package: &str) -> io::Result<Vec<String>> {
match ecosystem {
"node" => Self::npm_versions(package),
other => Err(io::Error::other(format!(
"no registry query wired for ecosystem '{other}' yet"
))),
}
}
}
pub struct ReadOnlyJournalStore;
impl ReadOnlyJournalStore {
fn read_only(op: &str) -> io::Error {
io::Error::new(
io::ErrorKind::Unsupported,
format!("{op} is not available on the read-only journal store"),
)
}
}
impl JournalStore for ReadOnlyJournalStore {
fn lock_exclusive(&self, _lock_path: &Path) -> io::Result<Box<dyn JournalLock>> {
Err(Self::read_only("lock_exclusive"))
}
fn append_line(&self, _path: &Path, _line: &str) -> io::Result<()> {
Err(Self::read_only("append_line"))
}
fn read_lines(&self, path: &Path) -> io::Result<Vec<String>> {
match std::fs::read_to_string(path) {
Ok(contents) => {
let end = contents.rfind('\n').map_or(0, |i| i + 1);
Ok(contents[..end].lines().map(str::to_string).collect())
}
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(Vec::new()),
Err(e) => Err(e),
}
}
fn read(&self, path: &Path) -> io::Result<Option<Vec<u8>>> {
match std::fs::read(path) {
Ok(bytes) => Ok(Some(bytes)),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
fn write_atomic(&self, _path: &Path, _bytes: &[u8]) -> io::Result<()> {
Err(Self::read_only("write_atomic"))
}
fn list_dir(&self, dir: &Path) -> io::Result<Vec<String>> {
let mut names = Vec::new();
match std::fs::read_dir(dir) {
Ok(entries) => {
for entry in entries {
names.push(entry?.file_name().to_string_lossy().into_owned());
}
}
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(e),
}
names.sort();
Ok(names)
}
}
pub struct RealTagger {
root: PathBuf,
}
impl RealTagger {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
fn run(&self, program: &str, args: &[&str]) -> io::Result<std::process::Output> {
Command::new(program)
.args(args)
.current_dir(&self.root)
.stdin(Stdio::null())
.env("GIT_TERMINAL_PROMPT", "0")
.env("GIT_ASKPASS", "true")
.env("GH_PROMPT_DISABLED", "1")
.env("GH_NO_UPDATE_NOTIFIER", "1")
.output()
}
fn check(out: std::process::Output, what: &str) -> io::Result<std::process::Output> {
if out.status.success() {
return Ok(out);
}
let stderr = String::from_utf8_lossy(&out.stderr);
let detail = if stderr.trim().is_empty() {
String::from_utf8_lossy(&out.stdout).into_owned()
} else {
stderr.into_owned()
};
Err(io::Error::other(format!(
"{what} exited {:?}: {}",
out.status.code(),
detail.trim()
)))
}
fn tag_commit(&self, tag: &str) -> Option<String> {
let out = self
.run(
"git",
&["rev-parse", "--verify", "-q", &format!("{tag}^{{commit}}")],
)
.ok()?;
if !out.status.success() {
return None;
}
let sha = String::from_utf8_lossy(&out.stdout).trim().to_string();
(!sha.is_empty()).then_some(sha)
}
}
impl Tagger for RealTagger {
fn create_tag(&self, tag: &str, commit: &str, message: &str) -> io::Result<()> {
if let Some(existing) = self.tag_commit(tag) {
if existing == commit || existing.starts_with(commit) || commit.starts_with(&existing) {
return Ok(());
}
return Err(io::Error::other(format!(
"tag `{tag}` already exists at {existing}, not the sealed commit {commit}"
)));
}
let out = self.run("git", &["tag", "-a", tag, commit, "-m", message])?;
Self::check(out, "git tag")?;
Ok(())
}
fn push_tag(&self, tag: &str) -> io::Result<()> {
let refspec = format!("refs/tags/{tag}:refs/tags/{tag}");
let out = self.run("git", &["push", "origin", &refspec])?;
Self::check(out, "git push")?;
Ok(())
}
fn create_github_release(&self, tag: &str, title: &str) -> io::Result<Option<String>> {
let view = self.run(
"gh",
&["release", "view", tag, "--json", "url", "-q", ".url"],
)?;
if view.status.success() {
let url = String::from_utf8_lossy(&view.stdout).trim().to_string();
return Ok((!url.is_empty()).then_some(url));
}
let out = self.run(
"gh",
&[
"release",
"create",
tag,
"--title",
title,
"--generate-notes",
],
)?;
let out = Self::check(out, "gh release create")?;
let url = String::from_utf8_lossy(&out.stdout).trim().to_string();
Ok((!url.is_empty()).then_some(url))
}
}
pub struct RealJournalStore;
struct RealJournalLock {
path: PathBuf,
}
impl JournalLock for RealJournalLock {}
impl Drop for RealJournalLock {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
fn fsync_dir(dir: &Path) {
let _ = File::open(dir).and_then(|f| f.sync_all());
}
impl JournalStore for RealJournalStore {
fn lock_exclusive(&self, lock_path: &Path) -> io::Result<Box<dyn JournalLock>> {
if let Some(parent) = lock_path.parent() {
std::fs::create_dir_all(parent)?;
}
match OpenOptions::new()
.write(true)
.create_new(true)
.open(lock_path)
{
Ok(mut f) => {
let _ = writeln!(f, "{}", std::process::id());
let _ = f.sync_all();
Ok(Box::new(RealJournalLock {
path: lock_path.to_path_buf(),
}))
}
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Err(io::Error::new(
io::ErrorKind::WouldBlock,
"another release cut/resume holds the single-active-cut lock",
)),
Err(e) => Err(e),
}
}
fn append_line(&self, path: &Path, line: &str) -> io::Result<()> {
let created_parent = match path.parent() {
Some(p) if !p.exists() => {
std::fs::create_dir_all(p)?;
true
}
_ => false,
};
let mut f = OpenOptions::new().create(true).append(true).open(path)?;
f.write_all(line.as_bytes())?;
f.write_all(b"\n")?;
f.sync_all()?;
if created_parent {
if let Some(p) = path.parent() {
fsync_dir(p);
}
}
Ok(())
}
fn read_lines(&self, path: &Path) -> io::Result<Vec<String>> {
match std::fs::read_to_string(path) {
Ok(s) => Ok(s.lines().map(str::to_string).collect()),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(Vec::new()),
Err(e) => Err(e),
}
}
fn read(&self, path: &Path) -> io::Result<Option<Vec<u8>>> {
match std::fs::read(path) {
Ok(b) => Ok(Some(b)),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
fn write_atomic(&self, path: &Path, bytes: &[u8]) -> io::Result<()> {
let parent = path.parent().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "manifest path has no parent")
})?;
std::fs::create_dir_all(parent)?;
let file_name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("manifest.json");
let counter = ID_COUNTER.fetch_add(1, Ordering::Relaxed);
let tmp = parent.join(format!(".{file_name}.{}.{counter}.tmp", std::process::id()));
{
let mut f = File::create(&tmp)?;
f.write_all(bytes)?;
f.sync_all()?;
}
std::fs::rename(&tmp, path)?;
fsync_dir(parent);
Ok(())
}
fn list_dir(&self, dir: &Path) -> io::Result<Vec<String>> {
match std::fs::read_dir(dir) {
Ok(entries) => {
let mut names = Vec::new();
for entry in entries {
names.push(entry?.file_name().to_string_lossy().into_owned());
}
names.sort();
Ok(names)
}
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(Vec::new()),
Err(e) => Err(e),
}
}
}