use crate::command_runner::{CommandRunner, SystemCommandRunner};
use crate::config;
use crate::error::{GitcoreError, Result};
use crate::git;
use crate::models::{
Account, GitcoreConfig, Platform, Vault, VaultKey, is_valid_account_name, validate_accounts,
};
use crate::ssh::{self, HostKeyStatus};
use crate::vault::{decrypt_vault, encrypt_vault};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::ExitStatus;
use std::sync::Arc;
mod accounts;
mod audit;
mod backup;
mod repos;
mod ssh_ops;
mod storage;
#[cfg(test)]
mod tests;
#[derive(Debug, Clone)]
pub struct GitcorePaths {
pub config_path: PathBuf,
pub ssh_dir: PathBuf,
}
impl Default for GitcorePaths {
fn default() -> Self {
Self {
config_path: config::default_config_path(),
ssh_dir: ssh::get_ssh_dir(),
}
}
}
#[derive(Clone)]
pub struct Gitcore {
paths: GitcorePaths,
runner: Arc<dyn CommandRunner>,
}
#[derive(Debug, Clone, Default)]
pub struct AddAccountRequest {
pub name: String,
pub platform: Platform,
pub username: String,
pub email: String,
pub gpg_key_id: Option<String>,
pub key_path: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct UpdateAccountRequest {
pub username: Option<String>,
pub email: Option<String>,
pub gpg_key_id: Option<Option<String>>,
}
#[derive(Debug, Clone)]
pub struct RegisteredAccount {
pub account: Account,
pub host_alias: String,
}
#[derive(Debug, Clone)]
pub struct FileAudit {
pub path: PathBuf,
pub exists: bool,
pub permissions: Option<u32>,
pub expected_permissions: u32,
}
#[derive(Debug, Clone)]
pub struct KeyAudit {
pub account: Account,
pub private_key: FileAudit,
pub public_key: FileAudit,
}
#[derive(Debug, Clone)]
pub struct AuditReport {
pub key_audits: Vec<KeyAudit>,
pub ssh_config: FileAudit,
pub config_file: FileAudit,
pub issues: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct BackupReport {
pub output_path: PathBuf,
pub included_keys: Vec<String>,
pub missing_keys: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct RestoreReport {
pub restored_accounts: usize,
pub restored_keys: Vec<String>,
pub source_path: PathBuf,
pub legacy_json: bool,
}
#[derive(Debug, Clone)]
pub struct CloneRequest {
pub account_name: String,
pub repo_url: String,
pub working_dir: PathBuf,
}
#[derive(Debug, Clone)]
pub struct CloneReport {
pub repo_path: PathBuf,
pub remote_url: String,
pub username: String,
pub email: String,
pub reused_existing_repo: bool,
}
#[derive(Debug, Clone)]
pub struct RemoteAddRequest {
pub account_name: String,
pub repo_url: String,
pub repo_path: PathBuf,
}
#[derive(Debug, Clone)]
pub struct RemoteSwitchRequest {
pub account_name: String,
pub repo_path: PathBuf,
}
#[derive(Debug, Clone)]
pub struct RemoteReport {
pub repo_path: PathBuf,
pub remote_url: String,
pub username: String,
pub email: String,
}
#[derive(Debug, Clone)]
pub struct SshTestReport {
pub account: Account,
pub host_status: HostKeyStatus,
pub status: ExitStatus,
pub stderr: String,
pub authenticated: bool,
pub connected_without_shell: bool,
}
#[derive(Debug, Clone)]
pub struct RotationReport {
pub account: Account,
pub deleted_paths: Vec<PathBuf>,
pub public_key: String,
}
#[derive(Debug, Clone)]
pub struct KeyProvisionReport {
pub key_path: String,
pub public_key: String,
}
#[derive(Debug, Clone)]
pub struct KeyDeletionReport {
pub deleted_paths: Vec<PathBuf>,
}
impl Gitcore {
pub fn init_git_repo(&self, path: &Path) -> Result<()> {
git::init_repository_with(self.runner(), path)?;
Ok(())
}
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_paths(paths: GitcorePaths) -> Self {
Self::with_runner(paths, Arc::new(SystemCommandRunner))
}
#[must_use]
pub fn paths(&self) -> &GitcorePaths {
&self.paths
}
pub(crate) fn runner(&self) -> &dyn CommandRunner {
self.runner.as_ref()
}
#[must_use]
pub(crate) fn with_runner(paths: GitcorePaths, runner: Arc<dyn CommandRunner>) -> Self {
Self { paths, runner }
}
pub fn load_config(&self) -> Result<GitcoreConfig> {
config::load_config_from_path(&self.paths.config_path)
}
pub fn save_config(&self, config: &GitcoreConfig) -> Result<()> {
validate_accounts(&config.accounts).map_err(GitcoreError::InvalidConfig)?;
config::save_config_to_path(config, &self.paths.config_path)
}
}
impl Default for Gitcore {
fn default() -> Self {
Self::with_paths(GitcorePaths::default())
}
}
impl std::fmt::Debug for Gitcore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Gitcore")
.field("paths", &self.paths)
.field("runner", &"<command-runner>")
.finish()
}
}
fn audit_file(path: &Path, expected_permissions: u32) -> FileAudit {
FileAudit {
path: path.to_path_buf(),
exists: path.exists(),
permissions: file_permissions(path),
expected_permissions,
}
}
fn file_permissions(path: &Path) -> Option<u32> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::metadata(path)
.ok()
.map(|metadata| metadata.permissions().mode() & 0o777)
}
#[cfg(not(unix))]
{
let _ = path;
None
}
}
fn write_atomic(path: &Path, contents: &[u8]) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let tmp_path = path.with_extension("tmp");
fs::write(&tmp_path, contents)?;
fs::rename(&tmp_path, path)?;
Ok(())
}
fn set_permissions_if_unix(path: &Path, mode: u32) -> std::io::Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(mode))?;
}
#[cfg(not(unix))]
{
let _ = (path, mode);
}
Ok(())
}
fn repository_path(working_dir: &Path, repo_url: &str) -> PathBuf {
working_dir.join(
repo_url
.split('/')
.next_back()
.unwrap_or("repo")
.trim_end_matches(".git"),
)
}