#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
#![allow(clippy::multiple_crate_versions)]
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Context, Result};
use git_sshripped_repository_models::{
GithubSourceRegistry, RepositoryLocalConfig, RepositoryManifest,
};
#[must_use]
pub fn metadata_dir(repo_root: &Path) -> PathBuf {
repo_root.join(".git-sshripped")
}
#[must_use]
pub fn manifest_file(repo_root: &Path) -> PathBuf {
metadata_dir(repo_root).join("manifest.toml")
}
#[must_use]
pub fn github_sources_file(repo_root: &Path) -> PathBuf {
metadata_dir(repo_root).join("github-sources.toml")
}
#[must_use]
pub fn local_config_file(repo_root: &Path) -> PathBuf {
metadata_dir(repo_root).join("config.toml")
}
pub fn write_manifest(repo_root: &Path, manifest: &RepositoryManifest) -> Result<()> {
let dir = metadata_dir(repo_root);
fs::create_dir_all(&dir)
.with_context(|| format!("failed to create metadata directory {}", dir.display()))?;
let text = toml::to_string_pretty(manifest).context("failed to serialize manifest")?;
let file = manifest_file(repo_root);
fs::write(&file, text).with_context(|| format!("failed to write {}", file.display()))?;
Ok(())
}
pub fn read_manifest(repo_root: &Path) -> Result<RepositoryManifest> {
let file = manifest_file(repo_root);
let text = fs::read_to_string(&file)
.with_context(|| format!("failed to read manifest {}", file.display()))?;
toml::from_str(&text).context("failed to parse repository manifest")
}
pub fn read_github_sources(repo_root: &Path) -> Result<GithubSourceRegistry> {
let file = github_sources_file(repo_root);
if !file.exists() {
return Ok(GithubSourceRegistry::default());
}
let text = fs::read_to_string(&file)
.with_context(|| format!("failed to read github source registry {}", file.display()))?;
toml::from_str(&text).context("failed to parse github source registry")
}
pub fn write_github_sources(repo_root: &Path, registry: &GithubSourceRegistry) -> Result<()> {
let dir = metadata_dir(repo_root);
fs::create_dir_all(&dir)
.with_context(|| format!("failed to create metadata directory {}", dir.display()))?;
let text = toml::to_string_pretty(registry).context("failed to serialize github sources")?;
let file = github_sources_file(repo_root);
fs::write(&file, text)
.with_context(|| format!("failed to write github source registry {}", file.display()))?;
Ok(())
}
pub fn read_local_config(repo_root: &Path) -> Result<RepositoryLocalConfig> {
let file = local_config_file(repo_root);
if !file.exists() {
return Ok(RepositoryLocalConfig::default());
}
let text = fs::read_to_string(&file)
.with_context(|| format!("failed to read repository config {}", file.display()))?;
toml::from_str(&text).context("failed to parse repository local config")
}
pub fn write_local_config(repo_root: &Path, config: &RepositoryLocalConfig) -> Result<()> {
let dir = metadata_dir(repo_root);
fs::create_dir_all(&dir)
.with_context(|| format!("failed to create metadata directory {}", dir.display()))?;
let text = toml::to_string_pretty(config).context("failed to serialize local config")?;
let file = local_config_file(repo_root);
fs::write(&file, text)
.with_context(|| format!("failed to write local config {}", file.display()))?;
Ok(())
}
pub fn install_gitattributes(repo_root: &Path, patterns: &[String]) -> Result<()> {
let path = repo_root.join(".gitattributes");
let mut existing = if path.exists() {
fs::read_to_string(&path)
.with_context(|| format!("failed to read gitattributes {}", path.display()))?
} else {
String::new()
};
for pattern in patterns {
let line = pattern.strip_prefix('!').map_or_else(
|| format!("{pattern} filter=git-sshripped diff=git-sshripped"),
|negated| format!("{negated} !filter !diff"),
);
if !existing.lines().any(|item| item.trim() == line) {
if !existing.ends_with('\n') && !existing.is_empty() {
existing.push('\n');
}
existing.push_str(&line);
existing.push('\n');
}
}
fs::write(&path, existing)
.with_context(|| format!("failed to write gitattributes {}", path.display()))?;
Ok(())
}
fn shell_quote(s: &str) -> String {
if !s.contains(|c: char| {
c.is_whitespace()
|| matches!(
c,
'\'' | '"' | '\\' | '(' | ')' | '&' | ';' | '|' | '<' | '>' | '`' | '$' | '!' | '#'
)
}) {
return s.to_string();
}
format!("'{}'", s.replace('\'', "'\\''"))
}
pub fn install_git_filters(repo_root: &Path, bin: &str, linked_worktree: bool) -> Result<()> {
if linked_worktree {
let ext_status = Command::new("git")
.args(["config", "--local", "extensions.worktreeConfig", "true"])
.current_dir(repo_root)
.status()
.context("failed to enable extensions.worktreeConfig")?;
if !ext_status.success() {
anyhow::bail!("git config failed for key 'extensions.worktreeConfig'");
}
}
let scope = if linked_worktree {
"--worktree"
} else {
"--local"
};
let quoted = shell_quote(bin);
let pairs = [
(
"filter.git-sshripped.process".to_string(),
format!("{quoted} filter-process"),
),
(
"filter.git-sshripped.clean".to_string(),
format!("{quoted} clean --path %f"),
),
(
"filter.git-sshripped.smudge".to_string(),
format!("{quoted} smudge --path %f"),
),
(
"filter.git-sshripped.required".to_string(),
"true".to_string(),
),
(
"diff.git-sshripped.textconv".to_string(),
format!("{quoted} diff --path %f"),
),
];
for (key, value) in &pairs {
let status = Command::new("git")
.args(["config", scope, key.as_str(), value.as_str()])
.current_dir(repo_root)
.status()
.with_context(|| format!("failed to set git config {key}"))?;
if !status.success() {
anyhow::bail!("git config failed for key '{key}'");
}
}
Ok(())
}
#[must_use]
pub fn agent_wrap_dir(common_dir: &Path) -> PathBuf {
common_dir.join("git-sshripped-agent-wrap")
}
#[must_use]
pub fn agent_wrap_file(common_dir: &Path, fingerprint: &str) -> PathBuf {
agent_wrap_dir(common_dir).join(format!("{fingerprint}.toml"))
}
pub fn read_agent_wrap(
common_dir: &Path,
fingerprint: &str,
) -> Result<Option<git_sshripped_ssh_agent_models::AgentWrappedKey>> {
let file = agent_wrap_file(common_dir, fingerprint);
if !file.exists() {
return Ok(None);
}
let text = fs::read_to_string(&file)
.with_context(|| format!("failed to read agent-wrap file {}", file.display()))?;
let key: git_sshripped_ssh_agent_models::AgentWrappedKey =
toml::from_str(&text).context("failed to parse agent-wrap file")?;
Ok(Some(key))
}
pub fn write_agent_wrap(
common_dir: &Path,
wrapped: &git_sshripped_ssh_agent_models::AgentWrappedKey,
) -> Result<()> {
let dir = agent_wrap_dir(common_dir);
fs::create_dir_all(&dir)
.with_context(|| format!("failed to create agent-wrap directory {}", dir.display()))?;
let file = agent_wrap_file(common_dir, &wrapped.fingerprint);
let text = toml::to_string_pretty(wrapped).context("failed to serialize agent-wrap key")?;
fs::write(&file, text)
.with_context(|| format!("failed to write agent-wrap file {}", file.display()))?;
Ok(())
}
pub fn list_agent_wrap_files(common_dir: &Path) -> Result<Vec<PathBuf>> {
let dir = agent_wrap_dir(common_dir);
if !dir.exists() {
return Ok(Vec::new());
}
let mut files = Vec::new();
for entry in fs::read_dir(&dir).with_context(|| format!("failed to read {}", dir.display()))? {
let entry = entry?;
let path = entry.path();
if path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("toml"))
{
files.push(path);
}
}
Ok(files)
}
pub fn parse_agent_wrap(text: &str) -> Result<git_sshripped_ssh_agent_models::AgentWrappedKey> {
toml::from_str(text).context("failed to parse agent-wrap TOML")
}