#![doc = r"
This module provides a unified interface for performing git operations using both `gix` and `git2` libraries.
It implements a gix-first, git2-fallback strategy to ensure robust functionality across different environments and use cases.
The `GitOpsManager` struct manages the operations and automatically falls back to `git2` if a `gix` operation fails,
providing seamless integration for submodule management and configuration tasks.
We prefer Gix, but it's still unstable and several core features are missing, so we use git2 as a fallback for those features and for stability.
"]
pub mod git2_ops;
pub mod gix_ops;
pub mod simple_gix;
pub use git2_ops::Git2Operations;
pub use gix_ops::GixOperations;
use anyhow::{Context, Result};
use bitflags::bitflags;
use std::collections::HashMap;
use std::path::Path;
use crate::config::{SubmoduleAddOptions, SubmoduleEntries, SubmoduleUpdateOptions};
use crate::options::{
ConfigLevel, SerializableBranch, SerializableFetchRecurse, SerializableIgnore,
SerializableUpdate,
};
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GitConfig {
pub entries: HashMap<String, String>,
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SubmoduleStatusFlags: u32 {
const IN_HEAD = 1 << 0;
const IN_INDEX = 1 << 1;
const IN_CONFIG = 1 << 2;
const IN_WD = 1 << 3;
const INDEX_ADDED = 1 << 4;
const INDEX_DELETED = 1 << 5;
const INDEX_MODIFIED = 1 << 6;
const WD_UNINITIALIZED = 1 << 7;
const WD_ADDED = 1 << 8;
const WD_DELETED = 1 << 9;
const WD_MODIFIED = 1 << 10;
const WD_INDEX_MODIFIED = 1 << 11;
const WD_WD_MODIFIED = 1 << 12;
const WD_UNTRACKED = 1 << 13;
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DetailedSubmoduleStatus {
pub path: String,
pub name: String,
pub url: Option<String>,
pub head_oid: Option<String>,
pub index_oid: Option<String>,
pub workdir_oid: Option<String>,
pub status_flags: SubmoduleStatusFlags,
pub ignore_rule: SerializableIgnore,
pub update_rule: SerializableUpdate,
pub fetch_recurse_rule: SerializableFetchRecurse,
pub branch: Option<SerializableBranch>,
pub is_initialized: bool,
pub is_active: bool,
pub has_modifications: bool,
pub sparse_checkout_enabled: bool,
pub sparse_patterns: Vec<String>,
}
pub trait GitOperations {
fn read_gitmodules(&self) -> Result<SubmoduleEntries>;
fn write_gitmodules(&mut self, config: &SubmoduleEntries) -> Result<()>;
#[allow(dead_code)]
fn read_git_config(&self, level: ConfigLevel) -> Result<GitConfig>;
#[allow(dead_code)]
fn write_git_config(&self, config: &GitConfig, level: ConfigLevel) -> Result<()>;
#[allow(dead_code)]
fn set_config_value(&self, key: &str, value: &str, level: ConfigLevel) -> Result<()>;
fn add_submodule(&mut self, opts: &SubmoduleAddOptions) -> Result<()>;
fn init_submodule(&mut self, path: &str) -> Result<()>;
fn update_submodule(&mut self, path: &str, opts: &SubmoduleUpdateOptions) -> Result<()>;
fn delete_submodule(&mut self, path: &str) -> Result<()>;
fn deinit_submodule(&mut self, path: &str, force: bool) -> Result<()>;
#[allow(dead_code)]
fn get_submodule_status(&self, path: &str) -> Result<DetailedSubmoduleStatus>;
fn list_submodules(&self) -> Result<Vec<String>>;
#[allow(dead_code)]
fn fetch_submodule(&self, path: &str) -> Result<()>;
fn reset_submodule(&self, path: &str, hard: bool) -> Result<()>;
fn clean_submodule(&self, path: &str, force: bool, remove_directories: bool) -> Result<()>;
fn stash_submodule(&self, path: &str, include_untracked: bool) -> Result<()>;
fn enable_sparse_checkout(&self, path: &str) -> Result<()>;
fn set_sparse_patterns(&self, path: &str, patterns: &[String]) -> Result<()>;
fn get_sparse_patterns(&self, path: &str) -> Result<Vec<String>>;
fn apply_sparse_checkout(&self, path: &str) -> Result<()>;
}
pub struct GitOpsManager {
gix_ops: Option<GixOperations>,
git2_ops: Git2Operations,
verbose: bool,
}
impl GitOpsManager {
pub fn new(repo_path: Option<&Path>, verbose: bool) -> Result<Self> {
let gix_ops = GixOperations::new(repo_path).ok();
let git2_ops = Git2Operations::new(repo_path)
.with_context(|| "Failed to initialize git2 operations")?;
Ok(Self {
gix_ops,
git2_ops,
verbose,
})
}
pub fn workdir(&self) -> Option<&std::path::Path> {
self.git2_ops.workdir()
}
pub fn reopen(&mut self) -> Result<()> {
let workdir = self
.git2_ops
.workdir()
.ok_or_else(|| anyhow::anyhow!("Cannot reopen repository: no working directory"))?
.to_path_buf();
self.git2_ops = Git2Operations::new(Some(&workdir)).with_context(|| {
format!("Failed to reopen git2 repository at {}", workdir.display())
})?;
match GixOperations::new(Some(&workdir)) {
Ok(new_gix) => {
self.gix_ops = Some(new_gix);
}
Err(e) => {
if self.verbose {
eprintln!(
"Warning: failed to reopen gix repository at {}: {}",
workdir.display(),
e
);
}
}
}
Ok(())
}
fn try_with_fallback<T, F1, F2>(&self, gix_op: F1, git2_op: F2) -> Result<T>
where
F1: FnOnce(&GixOperations) -> Result<T>,
F2: FnOnce(&Git2Operations) -> Result<T>,
{
if let Some(ref gix) = self.gix_ops {
match gix_op(gix) {
Ok(result) => return Ok(result),
Err(e) => {
if self.verbose {
eprintln!("gix operation failed, falling back to git2: {e}");
}
}
}
}
git2_op(&self.git2_ops)
}
fn try_with_fallback_mut<T, F1, F2>(&mut self, gix_op: F1, git2_op: F2) -> Result<T>
where
F1: FnOnce(&mut GixOperations) -> Result<T>,
F2: FnOnce(&mut Git2Operations) -> Result<T>,
{
if let Some(ref mut gix) = self.gix_ops {
match gix_op(gix) {
Ok(result) => return Ok(result),
Err(e) => {
if self.verbose {
eprintln!("gix operation failed, falling back to git2: {e}");
}
}
}
}
git2_op(&mut self.git2_ops)
}
}
impl GitOperations for GitOpsManager {
fn read_gitmodules(&self) -> Result<SubmoduleEntries> {
self.try_with_fallback(GitOperations::read_gitmodules, GitOperations::read_gitmodules)
}
fn write_gitmodules(&mut self, config: &SubmoduleEntries) -> Result<()> {
self.try_with_fallback_mut(
|gix| gix.write_gitmodules(config),
|git2| git2.write_gitmodules(config),
)
}
fn read_git_config(&self, level: ConfigLevel) -> Result<GitConfig> {
self.try_with_fallback(
|gix| gix.read_git_config(level),
|git2| git2.read_git_config(level),
)
}
fn write_git_config(&self, config: &GitConfig, level: ConfigLevel) -> Result<()> {
self.try_with_fallback(
|gix| gix.write_git_config(config, level),
|git2| git2.write_git_config(config, level),
)
}
fn set_config_value(&self, key: &str, value: &str, level: ConfigLevel) -> Result<()> {
self.try_with_fallback(
|gix| gix.set_config_value(key, value, level),
|git2| git2.set_config_value(key, value, level),
)
}
fn add_submodule(&mut self, opts: &SubmoduleAddOptions) -> Result<()> {
self.try_with_fallback_mut(
|gix| gix.add_submodule(opts),
|git2| git2.add_submodule(opts),
)
.or_else(|git2_err| {
let workdir = self
.git2_ops
.workdir()
.ok_or_else(|| anyhow::anyhow!("Repository has no working directory"))?;
let sub_path = workdir.join(&opts.path);
if sub_path.exists() {
let _ = std::fs::remove_dir_all(&sub_path);
}
let gitmodules_path = workdir.join(".gitmodules");
if gitmodules_path.exists() {
if let Ok(content) = std::fs::read_to_string(&gitmodules_path) {
let mut new_content = String::new();
let mut in_target_section = false;
let target_name = format!("\"{}\"", opts.name);
for line in content.lines() {
if line.starts_with("[submodule \"") {
in_target_section = line.contains(&target_name);
}
if !in_target_section {
new_content.push_str(line);
new_content.push('\n');
}
}
let _ = std::fs::write(&gitmodules_path, new_content);
}
}
let gitconfig_path = workdir.join(".git").join("config");
if gitconfig_path.exists() {
let _ = std::process::Command::new("git")
.args([
"config",
"--remove-section",
&format!("submodule.{}", opts.name),
])
.current_dir(workdir)
.output();
let path_key = opts.path.display().to_string();
if path_key != opts.name {
let _ = std::process::Command::new("git")
.args([
"config",
"--remove-section",
&format!("submodule.{path_key}"),
])
.current_dir(workdir)
.output();
}
}
let internal_git_dir = workdir.join(".git").join("modules").join(&opts.name);
if internal_git_dir.exists() {
let _ = std::fs::remove_dir_all(&internal_git_dir);
}
let path_internal_git_dir = workdir.join(".git").join("modules").join(&opts.path);
if path_internal_git_dir.exists() {
let _ = std::fs::remove_dir_all(&path_internal_git_dir);
}
let _ = std::process::Command::new("git")
.args(["rm", "--cached", "-r", "--ignore-unmatch"])
.arg(&opts.path)
.current_dir(workdir)
.output();
let mut cmd = std::process::Command::new("git");
cmd.current_dir(workdir)
.arg("submodule")
.arg("add")
.arg("--name")
.arg(&opts.name);
if let Some(branch) = &opts.branch {
let branch_str = branch.to_string();
if branch_str != "." {
cmd.arg("--branch").arg(&branch_str);
}
}
if opts.shallow {
cmd.arg("--depth").arg("1");
}
cmd.arg("--").arg(&opts.url).arg(&opts.path);
let output = cmd.output().context("Failed to run git submodule add")?;
if output.status.success() {
Ok(())
} else {
Err(anyhow::anyhow!(
"Failed to add submodule (git2 failed with: {}). CLI output: {}",
git2_err,
String::from_utf8_lossy(&output.stderr).trim()
))
}
})
}
fn init_submodule(&mut self, path: &str) -> Result<()> {
self.try_with_fallback_mut(
|gix| gix.init_submodule(path),
|git2| git2.init_submodule(path),
)
}
fn update_submodule(&mut self, path: &str, opts: &SubmoduleUpdateOptions) -> Result<()> {
self.try_with_fallback_mut(
|gix| gix.update_submodule(path, opts),
|git2| git2.update_submodule(path, opts),
)
}
fn delete_submodule(&mut self, path: &str) -> Result<()> {
self.try_with_fallback_mut(
|gix| gix.delete_submodule(path),
|git2| git2.delete_submodule(path),
)
}
fn deinit_submodule(&mut self, path: &str, force: bool) -> Result<()> {
self.try_with_fallback_mut(
|gix| gix.deinit_submodule(path, force),
|git2| git2.deinit_submodule(path, force),
)
}
fn get_submodule_status(&self, path: &str) -> Result<DetailedSubmoduleStatus> {
self.try_with_fallback(
|gix| gix.get_submodule_status(path),
|git2| git2.get_submodule_status(path),
)
}
fn list_submodules(&self) -> Result<Vec<String>> {
self.try_with_fallback(GitOperations::list_submodules, GitOperations::list_submodules)
}
fn fetch_submodule(&self, path: &str) -> Result<()> {
self.try_with_fallback(
|gix| gix.fetch_submodule(path),
|git2| git2.fetch_submodule(path),
)
}
fn reset_submodule(&self, path: &str, hard: bool) -> Result<()> {
self.try_with_fallback(
|gix| gix.reset_submodule(path, hard),
|git2| git2.reset_submodule(path, hard),
)
}
fn clean_submodule(&self, path: &str, force: bool, remove_directories: bool) -> Result<()> {
self.try_with_fallback(
|gix| gix.clean_submodule(path, force, remove_directories),
|git2| git2.clean_submodule(path, force, remove_directories),
)
}
fn stash_submodule(&self, path: &str, include_untracked: bool) -> Result<()> {
self.try_with_fallback(
|gix| gix.stash_submodule(path, include_untracked),
|git2| git2.stash_submodule(path, include_untracked),
)
}
fn enable_sparse_checkout(&self, path: &str) -> Result<()> {
self.try_with_fallback(
|gix| gix.enable_sparse_checkout(path),
|git2| git2.enable_sparse_checkout(path),
)
}
fn set_sparse_patterns(&self, path: &str, patterns: &[String]) -> Result<()> {
self.try_with_fallback(
|gix| gix.set_sparse_patterns(path, patterns),
|git2| git2.set_sparse_patterns(path, patterns),
)
}
fn get_sparse_patterns(&self, path: &str) -> Result<Vec<String>> {
self.try_with_fallback(
|gix| gix.get_sparse_patterns(path),
|git2| git2.get_sparse_patterns(path),
)
}
fn apply_sparse_checkout(&self, path: &str) -> Result<()> {
self.try_with_fallback(
|gix| gix.apply_sparse_checkout(path),
|git2| git2.apply_sparse_checkout(path),
)
.or_else(|_| {
let output = std::process::Command::new("git")
.args(["-C", path, "read-tree", "-mu", "HEAD"])
.output()
.context("Failed to run git read-tree")?;
if output.status.success() {
Ok(())
} else {
Err(anyhow::anyhow!(
"git read-tree failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
))
}
})
}
}