use std::fs;
use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use crate::CliError;
static PROFILE: OnceLock<String> = OnceLock::new();
pub fn set_profile(profile: String) {
let _ = PROFILE.set(profile);
}
pub fn profile() -> &'static str {
PROFILE.get().map_or("default", String::as_str)
}
pub fn profile_name(value: &str) -> Result<String, String> {
if value.is_empty()
|| value.len() > 64
|| !value
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'_' | b'-'))
{
return Err("profile names are 1 to 64 lowercase letters, digits, `_` or `-`".into());
}
Ok(value.to_owned())
}
pub fn default_home() -> PathBuf {
std::env::var_os("SILICON_HOME")
.filter(|v| !v.is_empty())
.or_else(|| std::env::var_os("HOME").filter(|v| !v.is_empty()))
.map_or_else(|| PathBuf::from("."), PathBuf::from)
}
pub fn home_pointer() -> PathBuf {
default_home().join(".commit").join("home_dir")
}
pub fn configured_home() -> Option<PathBuf> {
let text = fs::read_to_string(home_pointer()).ok()?;
let path = PathBuf::from(text.trim());
(!path.as_os_str().is_empty()).then_some(path)
}
pub fn home() -> PathBuf {
configured_home().unwrap_or_else(default_home)
}
pub fn directory() -> PathBuf {
home().join(".commit")
}
pub fn profile_directory() -> PathBuf {
if profile() == "default" {
directory()
} else {
directory().join("profiles").join(profile())
}
}
pub fn set_home(location: &Path) -> Result<PathBuf, CliError> {
let location = expand_tilde(location);
if !location.is_dir() {
return Err(CliError::new(
"not_a_directory",
format!("not a directory: {}", location.display()),
)
.hint("Create the directory first, or pass an existing one; Commit keeps its state in <LOCATION>/.commit."));
}
let location = fs::canonicalize(&location)
.map_err(|e| CliError::io(format!("could not resolve {}", location.display()), &e))?;
private_write(&home_pointer(), location.to_string_lossy().as_bytes())?;
Ok(location)
}
fn expand_tilde(location: &Path) -> PathBuf {
match location.to_str() {
Some("~") => default_home(),
Some(text) if text.starts_with("~/") => default_home().join(&text[2..]),
_ => location.to_path_buf(),
}
}
pub fn telemetry_enabled() -> bool {
std::env::var("COMMIT_TELEMETRY")
.ok()
.or_else(|| fs::read_to_string(directory().join("telemetry")).ok())
.is_none_or(|v| !matches!(v.trim(), "off" | "false" | "0"))
}
pub fn private_directory(parent: &Path) -> Result<(), CliError> {
fs::create_dir_all(parent)
.map_err(|e| CliError::io(format!("could not create {}", parent.display()), &e))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
fs::set_permissions(parent, fs::Permissions::from_mode(0o700))
.map_err(|e| CliError::io(format!("could not protect {}", parent.display()), &e))?;
}
Ok(())
}
pub fn private_write(path: &Path, bytes: &[u8]) -> Result<(), CliError> {
let parent = path.parent().ok_or_else(|| {
CliError::new(
"invalid_state_path",
format!("{} has no parent directory", path.display()),
)
})?;
private_directory(parent)?;
let file_name = path
.file_name()
.map_or_else(|| "state".into(), |n| n.to_string_lossy());
let temporary = parent.join(format!(
".{file_name}.tmp-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
));
let result = (|| -> std::io::Result<()> {
let mut options = fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt as _;
options.mode(0o600);
}
let mut file = options.open(&temporary)?;
file.write_all(bytes)?;
file.sync_all()?;
fs::rename(&temporary, path)
})();
if let Err(error) = result {
let _ = fs::remove_file(&temporary);
return Err(CliError::io(
format!("could not write {}", path.display()),
&error,
));
}
Ok(())
}