pub mod repo;
pub mod hooks;
pub mod attributes;
pub mod storage;
pub mod team;
pub mod ignore;
pub mod config;
pub use repo::{GitRepo, GitRepoError, GitRepoResult};
pub use hooks::{GitHooks, HookType, HookConfig, SecretDetectionHook};
pub use attributes::{GitAttributes, EncryptionPattern, AttributeConfig};
pub use storage::{EncryptedStorage, GitObjectStorage, StorageRef};
pub use team::{TeamKeySharing, TeamMember, KeyShareConfig};
pub use ignore::{GitIgnoreManager, IgnorePattern, IgnoreConfig};
pub use config::{GitCryptConfig, RepositorySetup, IntegrationMode};
use crate::crypto::{CryptoEngine, EncryptedSecret};
use git2::{Repository, Signature};
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum GitError {
#[error("Git repository error: {0}")]
Repository(#[from] git2::Error),
#[error("Not a git repository or no git repository found in parent directories")]
NotGitRepository,
#[error("Failed to initialize git repository: {0}")]
InitializationFailed(String),
#[error("Git hook operation failed: {0}")]
HookFailed(String),
#[error("Git attributes configuration failed: {0}")]
AttributesFailed(String),
#[error("Team key sharing failed: {0}")]
TeamSharingFailed(String),
#[error("Encrypted storage operation failed: {0}")]
StorageFailed(String),
#[error("Invalid git object: {0}")]
InvalidObject(String),
#[error("Crypto operation failed: {0}")]
Crypto(#[from] crate::crypto::CryptoError),
#[error("IO operation failed: {0}")]
Io(#[from] std::io::Error),
#[error("Serialization failed: {0}")]
SerializationFailed(String),
#[error("Git repo operation failed: {0}")]
Repo(#[from] GitRepoError),
}
pub type GitResult<T> = Result<T, GitError>;
pub struct GitIntegration {
repo: GitRepo,
crypto: CryptoEngine,
config: GitCryptConfig,
}
impl GitIntegration {
pub async fn new() -> GitResult<Self> {
let repo = GitRepo::find_or_create().await?;
let crypto = CryptoEngine::new();
let config = GitCryptConfig::load_or_default(&repo).await?;
Ok(Self {
repo,
crypto,
config,
})
}
pub async fn new_in_dir<P: AsRef<Path>>(path: P) -> GitResult<Self> {
let repo = GitRepo::open_or_create(path).await?;
let crypto = CryptoEngine::new();
let config = GitCryptConfig::load_or_default(&repo).await?;
Ok(Self {
repo,
crypto,
config,
})
}
pub async fn setup_repository(&mut self) -> GitResult<()> {
self.setup_gitignore().await?;
self.setup_attributes().await?;
self.setup_hooks().await?;
self.setup_storage().await?;
self.setup_team_sharing().await?;
Ok(())
}
async fn setup_gitignore(&self) -> GitResult<()> {
let mut ignore_manager = GitIgnoreManager::new(&self.repo)?;
ignore_manager.add_pattern("*.cargocrypt").await?;
ignore_manager.add_pattern("*.enc").await?;
ignore_manager.add_pattern(".cargocrypt/").await?;
ignore_manager.add_pattern("!.cargocrypt/config.toml").await?; ignore_manager.add_pattern("!.cargocrypt/team/").await?;
ignore_manager.save().await?;
Ok(())
}
async fn setup_attributes(&self) -> GitResult<()> {
let mut attributes = GitAttributes::new(&self.repo)?;
attributes.add_pattern("*.secret", "cargocrypt-encrypt").await?;
attributes.add_pattern("*.key", "cargocrypt-encrypt").await?;
attributes.add_pattern("secrets/*", "cargocrypt-encrypt").await?;
attributes.add_pattern("config/secrets.*", "cargocrypt-encrypt").await?;
attributes.configure_filters(&self.config).await?;
attributes.save().await?;
Ok(())
}
async fn setup_hooks(&self) -> GitResult<()> {
let hooks = GitHooks::new(&self.repo)?;
let detection_hook = SecretDetectionHook::new(&self.crypto)?;
hooks.install_hook(HookType::PreCommit, Box::new(detection_hook)).await?;
hooks.install_encryption_validation_hook().await?;
Ok(())
}
async fn setup_storage(&self) -> GitResult<()> {
let storage = EncryptedStorage::new(&self.repo, &self.crypto)?;
storage.initialize().await?;
Ok(())
}
async fn setup_team_sharing(&self) -> GitResult<()> {
let team_sharing = TeamKeySharing::new(&self.repo, &self.crypto)?;
team_sharing.initialize().await?;
Ok(())
}
pub async fn is_configured(&self) -> bool {
self.repo.has_cargocrypt_config() &&
self.has_gitignore_patterns().await &&
self.has_git_attributes().await &&
self.has_git_hooks().await
}
async fn has_gitignore_patterns(&self) -> bool {
GitIgnoreManager::new(&self.repo)
.map(|manager| manager.has_cargocrypt_patterns())
.unwrap_or(false)
}
async fn has_git_attributes(&self) -> bool {
GitAttributes::new(&self.repo)
.map(|attrs| attrs.has_cargocrypt_patterns())
.unwrap_or(false)
}
async fn has_git_hooks(&self) -> bool {
GitHooks::new(&self.repo)
.map(|hooks| hooks.are_installed())
.unwrap_or(false)
}
pub async fn encrypt_and_stage<P: AsRef<Path>>(&self, path: P, password: &str) -> GitResult<PathBuf> {
let path = path.as_ref();
let content = tokio::fs::read(path).await
.map_err(|e| GitError::Io(e))?;
let encrypted = self.crypto.encrypt_data(&content, password).await
.map_err(GitError::Crypto)?;
let encrypted_path = path.with_extension(format!("{}.enc",
path.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("")
));
let encrypted_bytes = bincode::serialize(&encrypted)
.map_err(|e| GitError::SerializationFailed(format!("Failed to serialize: {}", e)))?;
tokio::fs::write(&encrypted_path, encrypted_bytes).await
.map_err(|e| GitError::Io(e))?;
self.repo.stage_file(&encrypted_path).await?;
if self.repo.is_staged(path).await? {
self.repo.unstage_file(path).await?;
}
Ok(encrypted_path)
}
pub async fn decrypt_from_git<P: AsRef<Path>>(&self, encrypted_path: P, password: &str) -> GitResult<PathBuf> {
let encrypted_path = encrypted_path.as_ref();
let encrypted_data = tokio::fs::read(encrypted_path).await
.map_err(|e| GitError::Io(e))?;
let encrypted: EncryptedSecret = bincode::deserialize(&encrypted_data)
.map_err(|e| GitError::SerializationFailed(format!("Failed to deserialize: {}", e)))?;
let decrypted_data = self.crypto.decrypt_data(&encrypted, password)
.map_err(GitError::Crypto)?;
let decrypted_path = if let Some(stem) = encrypted_path.file_stem() {
encrypted_path.with_file_name(stem)
} else {
encrypted_path.with_extension("decrypted")
};
tokio::fs::write(&decrypted_path, decrypted_data).await
.map_err(|e| GitError::Io(e))?;
Ok(decrypted_path)
}
pub async fn add_team_member(&self, member: TeamMember) -> GitResult<()> {
let team_sharing = TeamKeySharing::new(&self.repo, &self.crypto)?;
team_sharing.add_member(member).await?;
Ok(())
}
pub async fn rotate_team_keys(&self) -> GitResult<()> {
let team_sharing = TeamKeySharing::new(&self.repo, &self.crypto)?;
team_sharing.rotate_keys().await?;
Ok(())
}
pub fn repo(&self) -> &GitRepo {
&self.repo
}
pub fn crypto(&self) -> &CryptoEngine {
&self.crypto
}
pub fn config(&self) -> &GitCryptConfig {
&self.config
}
}
pub mod utils {
use super::*;
pub fn is_git_repository() -> bool {
Repository::discover(".").is_ok()
}
pub fn find_git_root() -> GitResult<PathBuf> {
Repository::discover(".")
.map(|repo| repo.workdir().unwrap_or(repo.path()).to_path_buf())
.map_err(|_| GitError::NotGitRepository)
}
pub fn should_encrypt<P: AsRef<Path>>(repo_path: P, _file_path: P) -> GitResult<bool> {
let _repo = Repository::open(repo_path)?;
Ok(false) }
pub fn get_signature(repo: &Repository) -> GitResult<Signature> {
repo.signature()
.or_else(|_| {
Signature::now("CargoCrypt", "cargocrypt@localhost")
})
.map_err(GitError::Repository)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn test_git_integration_creation() {
let temp_dir = TempDir::new().unwrap();
let git_integration = GitIntegration::new_in_dir(temp_dir.path()).await;
assert!(git_integration.is_ok());
}
#[test]
fn test_utils_git_detection() {
let is_git = utils::is_git_repository();
println!("Is git repository: {}", is_git);
if is_git {
let root = utils::find_git_root();
assert!(root.is_ok());
}
}
}