use anyhow::{Context, Result};
use git2::{BranchType, Repository};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::traits::GitOperations;
pub struct GitRepo {
repo: Repository,
}
impl GitRepo {
pub fn open(path: &Path) -> Result<Self> {
let repo = Repository::discover(path).context("Failed to find git repository")?;
Ok(Self { repo })
}
#[must_use]
pub fn get_repo_path(&self) -> &Path {
self.repo.workdir().unwrap_or_else(|| self.repo.path())
}
pub fn branch_exists(&self, branch_name: &str) -> Result<bool> {
match self.repo.find_branch(branch_name, BranchType::Local) {
Ok(_) => Ok(true),
Err(e) if e.code() == git2::ErrorCode::NotFound => Ok(false),
Err(e) => Err(e.into()),
}
}
pub fn create_worktree(
&self,
branch_name: &str,
worktree_path: &Path,
create_branch: bool,
) -> Result<()> {
if create_branch {
let head = self.repo.head()?;
let target_commit = head.peel_to_commit()?;
self.repo.branch(branch_name, &target_commit, false)?;
}
let branch = self
.repo
.find_branch(branch_name, BranchType::Local)
.with_context(|| format!("Failed to find branch '{}'", branch_name))?;
let worktree_name = worktree_path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(branch_name);
let mut opts = git2::WorktreeAddOptions::new();
opts.reference(Some(branch.get()));
self.repo
.worktree(worktree_name, worktree_path, Some(&opts))?;
Ok(())
}
pub fn remove_worktree(&self, worktree_name: &str) -> Result<()> {
let worktree = self.repo.find_worktree(worktree_name)?;
worktree.prune(Some(git2::WorktreePruneOptions::new().valid(true)))?;
Ok(())
}
pub fn list_worktrees(&self) -> Result<Vec<String>> {
let worktree_names = self.repo.worktrees()?;
Ok(worktree_names
.into_iter()
.flatten()
.map(std::string::ToString::to_string)
.collect())
}
pub fn delete_branch(&self, branch_name: &str) -> Result<()> {
let mut branch = self.repo.find_branch(branch_name, BranchType::Local)?;
branch.delete()?;
Ok(())
}
pub fn list_local_branches(&self) -> Result<Vec<String>> {
let branches = self.repo.branches(Some(BranchType::Local))?;
let mut branch_names = Vec::new();
for branch_result in branches {
let (branch, _) = branch_result?;
if let Some(name) = branch.name()? {
branch_names.push(name.to_string());
}
}
Ok(branch_names)
}
pub fn inherit_config(&self, worktree_path: &Path) -> Result<()> {
let mut main_config = self
.repo
.config()
.context("Failed to get repository config")?;
main_config
.set_bool("extensions.worktreeConfig", true)
.context("Failed to enable worktree config extension")?;
let worktree_repo =
Repository::open(worktree_path).context("Failed to open worktree repository")?;
let parent_config = self
.get_effective_config()
.context("Failed to read parent repository config")?;
let mut worktree_config = worktree_repo
.config()
.context("Failed to get worktree config")?;
for (key, config_value) in parent_config {
if should_inherit_config_key(&key) {
match config_value {
ConfigValue::String(s) => {
if let Err(e) = worktree_config.set_str(&key, &s) {
eprintln!("Warning: Failed to set config {}: {}", key, e);
}
}
ConfigValue::Bool(b) => {
if let Err(e) = worktree_config.set_bool(&key, b) {
eprintln!("Warning: Failed to set config {}: {}", key, e);
}
}
ConfigValue::Int(i) => {
if let Err(e) = worktree_config.set_i64(&key, i) {
eprintln!("Warning: Failed to set config {}: {}", key, e);
}
}
}
}
}
Ok(())
}
fn get_effective_config(&self) -> Result<HashMap<String, ConfigValue>> {
let mut config = self
.repo
.config()
.context("Failed to get repository config")?;
let mut config_map = HashMap::new();
let snapshot = config
.snapshot()
.context("Failed to create config snapshot")?;
let mut entries = snapshot
.entries(None)
.context("Failed to get config entries")?;
while let Some(entry_result) = entries.next() {
if let Ok(entry) = entry_result {
if let Some(name) = entry.name() {
let key = name.to_string();
if let Some(value_str) = entry.value() {
let config_value = if let Ok(bool_val) = config.get_bool(&key) {
ConfigValue::Bool(bool_val)
} else if let Ok(int_val) = config.get_i64(&key) {
ConfigValue::Int(int_val)
} else {
ConfigValue::String(value_str.to_string())
};
config_map.insert(key, config_value);
}
}
}
}
Ok(config_map)
}
}
#[derive(Debug, Clone)]
enum ConfigValue {
String(String),
Bool(bool),
Int(i64),
}
fn should_inherit_config_key(key: &str) -> bool {
const EXCLUDED_KEYS: &[&str] = &[
"core.bare",
"core.worktree",
"core.repositoryformatversion",
"extensions.worktreeconfig",
];
const EXCLUDED_PREFIXES: &[&str] = &["branch.", "remote.", "submodule."];
const INCLUDED_PREFIXES: &[&str] = &[
"user.",
"commit.",
"gpg.",
"credential.",
"push.",
"pull.",
"merge.",
"diff.",
"log.",
"color.",
"core.editor",
"core.pager",
"core.autocrlf",
"core.filemode",
"init.defaultbranch",
];
if EXCLUDED_KEYS.contains(&key) {
return false;
}
if EXCLUDED_PREFIXES
.iter()
.any(|prefix| key.starts_with(prefix))
{
return false;
}
if INCLUDED_PREFIXES
.iter()
.any(|prefix| key.starts_with(prefix))
{
return true;
}
if key.starts_with("core.") {
return INCLUDED_PREFIXES
.iter()
.any(|prefix| key == prefix.trim_end_matches('.'));
}
false
}
impl GitOperations for GitRepo {
fn open(path: &Path) -> Result<Box<dyn GitOperations>> {
let git_repo = GitRepo::open(path)?;
Ok(Box::new(git_repo))
}
fn get_repo_path(&self) -> PathBuf {
self.get_repo_path().to_path_buf()
}
fn branch_exists(&self, branch_name: &str) -> Result<bool> {
self.branch_exists(branch_name)
}
fn create_worktree(
&self,
branch_name: &str,
worktree_path: &Path,
create_branch: bool,
) -> Result<()> {
self.create_worktree(branch_name, worktree_path, create_branch)
}
fn remove_worktree(&self, worktree_name: &str) -> Result<()> {
self.remove_worktree(worktree_name)
}
fn list_worktrees(&self) -> Result<Vec<String>> {
self.list_worktrees()
}
fn delete_branch(&self, branch_name: &str) -> Result<()> {
self.delete_branch(branch_name)
}
fn inherit_config(&self, worktree_path: &Path) -> Result<()> {
self.inherit_config(worktree_path)
}
}