use super::iso_time::format_iso_milliseconds;
use super::stale_ref_lock_found::{Found, Outcome, PidFile};
use super::stale_ref_lock_judge::{Proof, Verdict};
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
const LINE_PREFIX: &str = "cli-git: ";
fn is_plain(byte: u8) -> bool {
return byte.is_ascii_alphanumeric() || b"_@%+=:,./-".contains(&byte);
}
pub fn shell_quote(path: &Path) -> String {
let bytes: &[u8] = path.as_os_str().as_bytes();
if !bytes.is_empty() && bytes.iter().all(|byte| { return is_plain(*byte); }) {
return String::from_utf8_lossy(bytes).into_owned();
}
let printable: bool = std::str::from_utf8(bytes).is_ok_and(|text| { return text.chars().all(|character| { return !character.is_control(); }); });
if printable {
return format!("'{}'", String::from_utf8_lossy(bytes).replace('\'', "'\\''"));
}
let mut quoted: String = String::from("$'");
for byte in bytes {
match *byte {
b'\'' => quoted.push_str("\\'"),
b'\\' => quoted.push_str("\\\\"),
0x20..=0x7e => quoted.push(char::from(*byte)),
other => quoted.push_str(&format!("\\x{other:02x}")),
}
}
quoted.push('\'');
return quoted;
}
fn age_text(age: Duration) -> String {
let seconds: u64 = age.as_secs();
return match seconds {
0..=119 => format!("{seconds} seconds"),
120..=7199 => format!("{} minutes", seconds / 60),
7200..=172_799 => format!("{} hours", seconds / 3600),
_ => format!("{} days", seconds / 86_400),
};
}
fn born_text(born: Option<SystemTime>) -> String {
let Some(at) = born else {
return String::new();
};
let milliseconds: i64 = at.duration_since(SystemTime::UNIX_EPOCH).ok().and_then(|elapsed| { return i64::try_from(elapsed.as_millis()).ok(); }).unwrap_or(0);
let age: Duration = SystemTime::now().duration_since(at).unwrap_or_default();
return format!(", made {} ({} ago)", format_iso_milliseconds(milliseconds), age_text(age));
}
fn proof_text(proof: &Proof) -> String {
return match proof {
Proof::PidGone(pid) => format!("process {pid}, named by the pid file beside it, has ended"),
Proof::PidReused { pid, command } => format!("process {pid}, named by the pid file beside it, has ended and the number now belongs to `{command}`"),
Proof::OrphanStaging => String::from("Git makes this staging file only while it holds `packed-refs.lock`, and that lock is gone"),
Proof::BeforeBoot => String::from("it was made before this boot of the machine, so no process that exists now can hold it"),
};
}
fn clearing_words(found: &Found) -> String {
let mut paths: Vec<String> = vec![shell_quote(found.lock.as_path())];
paths.extend(found.companions.iter().map(|path| { return shell_quote(path.as_path()); }));
return paths.join(" ");
}
impl Outcome {
pub fn notice(&self) -> String {
let mut lines: Vec<String> = Vec::new();
for found in &self.cleared {
if let Verdict::Stale(proof) = &found.verdict {
let what: &str = if matches!(proof, Proof::OrphanStaging) { "the packed-references staging file" } else { "the reference lock" };
lines.push(format!("{LINE_PREFIX}removed {what} {}, left by a Git that is gone: {}.", shell_quote(found.lock.as_path()), proof_text(proof)));
}
}
let mut hint: bool = false;
for found in &self.left {
match &found.verdict {
Verdict::Held(text) => lines.push(format!("{LINE_PREFIX}the reference lock {} is in use: {text}; it was left alone.", shell_quote(found.lock.as_path()))),
Verdict::Unproven(reason) => {
lines.push(format!("{LINE_PREFIX}left the reference lock {} alone: {reason}.", shell_quote(found.lock.as_path())));
lines.push(format!("{LINE_PREFIX}it is {} bytes{}. If no Git process is running in this repository, clear it with:", found.size, born_text(found.born)));
lines.push(format!("{LINE_PREFIX} rm -- {}", clearing_words(found)));
hint = hint || found.pid_file == PidFile::Absent;
}
Verdict::TooNew | Verdict::Stale(_) => {}
}
}
for path in &self.displaced {
lines.push(format!("{LINE_PREFIX}a lock file was set aside while a new lock took its place, and is left at {}; look at it before removing it.", shell_quote(path.as_path())));
}
if hint {
lines.push(format!("{LINE_PREFIX}Git writes a process number beside its locks only when `core.lockfilePid` is on. The wrapper turns it on for the Git it starts; `git config --global core.lockfilePid true` does it for the rest, so the next lock can be proven."));
}
if lines.is_empty() {
return String::new();
}
return format!("{}\n", lines.join("\n"));
}
}
pub fn path_list(paths: &[PathBuf]) -> String {
return paths.iter().map(|path| { return shell_quote(path.as_path()); }).collect::<Vec<String>>().join(" ");
}
#[cfg(test)]
#[path = "stale_ref_lock_message_tests.rs"]
mod tests;