use crate::error::{GitMultiError, Result};
use git2::Repository;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::{self};
const CONFIG_DIR: &str = ".gitmulti";
const CONFIG_FILE: &str = "config.toml";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RemoteConfig {
pub url: String,
#[serde(default)]
pub push_url: Option<String>,
#[serde(default)]
pub fetch_refspecs: Vec<String>,
#[serde(default)]
pub push_refspecs: Vec<String>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub is_primary: bool,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Config {
#[serde(default)]
pub remotes: HashMap<String, RemoteConfig>,
#[serde(default)]
pub default_remote: Option<String>,
#[serde(default)]
pub sync_preferences: SyncPreferences,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SyncPreferences {
#[serde(default = "default_sync_strategy")]
pub default_strategy: String,
#[serde(default)]
pub auto_fetch: bool,
#[serde(default)]
pub auto_push: bool,
}
impl Default for SyncPreferences {
fn default() -> Self {
Self {
default_strategy: default_sync_strategy(),
auto_fetch: true,
auto_push: false,
}
}
}
fn default_sync_strategy() -> String {
"cherry-pick".to_string()
}
impl Config {
pub fn load(repo: &Repository) -> Result<Self> {
let config_path = Self::get_config_path(repo)?;
if !config_path.exists() {
let mut config = Self::default();
config.reconcile_with_git(repo)?;
return Ok(config);
}
let config_content = fs::read_to_string(&config_path)?;
let mut config: Config = toml::from_str(&config_content)
.map_err(GitMultiError::TomlDeserializeError)?;
config.reconcile_with_git(repo)?;
Ok(config)
}
pub fn reconcile_with_git(&mut self, repo: &Repository) -> Result<()> {
let git_remotes = repo.remotes()?;
let git_names: Vec<String> = git_remotes
.iter()
.flatten()
.map(|s| s.to_string())
.collect();
for name in &git_names {
if !self.remotes.contains_key(name) {
let url = repo
.find_remote(name)
.ok()
.and_then(|r| r.url().map(|u| u.to_string()))
.unwrap_or_default();
self.remotes.insert(
name.clone(),
RemoteConfig {
url,
push_url: None,
fetch_refspecs: vec!["+refs/heads/*:refs/remotes/{}/*".to_string()],
push_refspecs: vec!["refs/heads/*:refs/heads/*".to_string()],
tags: vec![".*".to_string()],
is_primary: self.remotes.is_empty(),
},
);
}
}
let stale: Vec<String> = self
.remotes
.keys()
.filter(|n| !git_names.iter().any(|g| g == *n))
.cloned()
.collect();
for name in stale {
self.remotes.remove(&name);
}
if self.default_remote.as_deref().map_or(true, |d| !git_names.iter().any(|g| g == d)) {
self.default_remote = git_names.first().cloned();
}
Ok(())
}
pub fn save(&self, repo: &Repository) -> Result<()> {
let config_path = Self::get_config_path(repo)?;
if let Some(dir) = config_path.parent() {
fs::create_dir_all(dir)?;
}
let config_content = toml::to_string(self)
.map_err(GitMultiError::TomlSerializeError)?;
fs::write(&config_path, config_content)?;
Ok(())
}
pub fn get_config_path(repo: &Repository) -> Result<std::path::PathBuf> {
let root = repo.workdir()
.unwrap_or_else(|| repo.path());
Ok(root.join(CONFIG_DIR).join(CONFIG_FILE))
}
pub fn add_remote(&mut self, name: String, url: String) -> Result<()> {
if self.remotes.contains_key(&name) {
return Err(GitMultiError::RemoteAlreadyExists(name));
}
let config = RemoteConfig {
url,
push_url: None,
fetch_refspecs: vec!["+refs/heads/*:refs/remotes/{}/*".to_string()],
push_refspecs: vec!["refs/heads/*:refs/heads/*".to_string()],
tags: vec![".*".to_string()],
is_primary: self.remotes.is_empty(),
};
self.remotes.insert(name.clone(), config);
if self.default_remote.is_none() {
self.default_remote = Some(name);
}
Ok(())
}
pub fn remove_remote(&mut self, name: &str) -> Result<()> {
if !self.remotes.contains_key(name) {
return Err(GitMultiError::RemoteNotFound(name.to_string()));
}
self.remotes.remove(name);
if self.default_remote.as_deref() == Some(name) {
self.default_remote = self.remotes.keys().next().cloned();
}
Ok(())
}
pub fn get_remote(&self, name: &str) -> Result<&RemoteConfig> {
self.remotes.get(name)
.ok_or_else(|| GitMultiError::RemoteNotFound(name.to_string()))
}
pub fn get_remote_names(&self) -> Vec<&String> {
self.remotes.keys().collect()
}
pub fn set_default_remote(&mut self, name: String) -> Result<()> {
if !self.remotes.contains_key(&name) {
return Err(GitMultiError::RemoteNotFound(name));
}
self.default_remote = Some(name);
Ok(())
}
pub fn get_default_remote(&self) -> Option<&String> {
self.default_remote.as_ref()
}
pub fn set_primary_remote(&mut self, name: &str) -> Result<()> {
if !self.remotes.contains_key(name) {
return Err(GitMultiError::RemoteNotFound(name.to_string()));
}
for config in self.remotes.values_mut() {
config.is_primary = false;
}
if let Some(config) = self.remotes.get_mut(name) {
config.is_primary = true;
}
Ok(())
}
pub fn get_primary_remote(&self) -> Option<(&String, &RemoteConfig)> {
self.remotes.iter()
.find(|(_, config)| config.is_primary)
}
}
pub fn init_config(repo: &Repository) -> Result<Config> {
let mut config = Config::default();
config.reconcile_with_git(repo)?;
config.save(repo)?;
Ok(config)
}