use crate::app::App;
use anyhow::{anyhow, Result};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
pub fn dest_path(git_dir: &Path, tracked: &str, group: &str, prefix_map: &HashMap<String, String>) -> PathBuf {
let filename = Path::new(tracked)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "unknown".into());
if group != "ungrouped" {
git_dir.join(group).join(&filename)
} else if let Some(prefix) = prefix_map.get(&filename) {
git_dir.join(prefix).join(&filename)
} else {
git_dir.join(&filename)
}
}
pub fn build_prefix_map(ungrouped: &[String]) -> HashMap<String, String> {
let mut prefix_count: HashMap<String, usize> = HashMap::new();
let mut file_prefix: HashMap<String, String> = HashMap::new();
for tracked in ungrouped {
let filename = Path::new(tracked)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
let prefix = filename
.split(|c| c == '.' || c == '-' || c == '_')
.next()
.unwrap_or(&filename)
.to_lowercase();
if prefix.len() >= 3 {
*prefix_count.entry(prefix.clone()).or_default() += 1;
file_prefix.insert(filename, prefix);
}
}
file_prefix.retain(|_, prefix| prefix_count.get(prefix).copied().unwrap_or(0) >= 2);
file_prefix
}
pub fn sync(app: &App) -> Result<(usize, usize)> {
let git_dir = PathBuf::from(&app.cfg.git_dir);
fs::create_dir_all(&git_dir)?;
let ungrouped = app.cfg.groups.get("ungrouped").cloned().unwrap_or_default();
let prefix_map = build_prefix_map(&ungrouped);
let blacklist: std::collections::HashSet<&str> = app.cfg.git_blacklist.iter().map(|s| s.as_str()).collect();
let blacklist_groups: std::collections::HashSet<&str> = app.cfg.git_blacklist_groups.iter().map(|s| s.as_str()).collect();
let mut copied = 0usize;
let mut skipped = 0usize;
for (group, files) in &app.cfg.groups {
if blacklist_groups.contains(group.as_str()) {
skipped += files.len();
continue;
}
for tracked in files {
if blacklist.contains(tracked.as_str()) {
skipped += 1;
continue;
}
let local = app.local_path(tracked);
if !local.exists() {
skipped += 1;
continue;
}
let dest = dest_path(&git_dir, tracked, group, &prefix_map);
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(&local, &dest)
.map_err(|e| anyhow!("failed to copy {} → {}: {e}", local.display(), dest.display()))?;
copied += 1;
}
}
Ok((copied, skipped))
}
fn git_in(git_dir: &Path, args: &[&str]) -> Result<String> {
let out = Command::new("git")
.args(args)
.current_dir(git_dir)
.output()
.map_err(|_| anyhow!("git not found, is it installed?"))?;
if out.status.success() {
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
} else {
Err(anyhow!("{}", String::from_utf8_lossy(&out.stderr).trim()))
}
}
pub fn is_git_repo(git_dir: &Path) -> bool {
git_dir.join(".git").exists()
}
pub fn has_commits(git_dir: &Path) -> bool {
git_in(git_dir, &["rev-parse", "HEAD"]).is_ok()
}
pub fn git_add_all(git_dir: &Path) -> Result<()> {
git_in(git_dir, &["add", "."])?;
Ok(())
}
pub fn git_commit(git_dir: &Path, message: &str) -> Result<()> {
git_in(git_dir, &["commit", "-m", message])?;
Ok(())
}
pub fn git_push(git_dir: &Path) -> Result<String> {
let status = Command::new("git")
.args(["push"])
.current_dir(git_dir)
.status()
.map_err(|_| anyhow!("git not found"))?;
if status.success() {
Ok("pushed!".into())
} else {
Err(anyhow!("git push failed! check your remote config"))
}
}
pub fn git_has_staged(git_dir: &Path) -> bool {
Command::new("git")
.args(["diff", "--cached", "--quiet"])
.current_dir(git_dir)
.status()
.map(|s| !s.success())
.unwrap_or(false)
}
pub fn blacklist_file(app: &mut App, tracked: &str) {
if !app.cfg.git_blacklist.contains(&tracked.to_string()) {
app.cfg.git_blacklist.push(tracked.into());
app.save();
}
}
pub fn unblacklist_file(app: &mut App, tracked: &str) {
app.cfg.git_blacklist.retain(|f| f != tracked);
app.save();
}
pub fn blacklist_group(app: &mut App, group: &str) {
if !app.cfg.git_blacklist_groups.contains(&group.to_string()) {
app.cfg.git_blacklist_groups.push(group.into());
app.save();
}
}
pub fn unblacklist_group(app: &mut App, group: &str) {
app.cfg.git_blacklist_groups.retain(|g| g != group);
app.save();
}
pub fn is_blacklisted(app: &App, tracked: &str) -> bool {
app.cfg.git_blacklist.iter().any(|f| f == tracked)
}
pub fn is_group_blacklisted(app: &App, group: &str) -> bool {
app.cfg.git_blacklist_groups.iter().any(|g| g == group)
}