use crate::crypto::master_key_exists;
use crate::output::{print_info, print_success, print_warning};
use colored::Colorize;
use std::path::Path;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SyncConfig {
pub remote_url: Option<String>,
pub remote_name: String,
pub branch: String,
}
fn config_dir() -> std::path::PathBuf {
dirs::home_dir().unwrap().join(".agentswitch")
}
fn repo_is_initialized() -> bool {
let config_dir = config_dir();
config_dir.join(".git").exists()
}
fn open_repo() -> anyhow::Result<git2::Repository> {
let config_dir = config_dir();
git2::Repository::open(&config_dir).map_err(|e| {
anyhow::anyhow!(
"无法打开 Git 仓库: {}。请先运行 'asw sync init'",
e.message()
)
})
}
fn get_signature(repo: &git2::Repository) -> anyhow::Result<git2::Signature<'static>> {
if let Ok(config) = repo.config() {
if let Ok(name) = config.get_string("user.name") {
if let Ok(email) = config.get_string("user.email") {
return git2::Signature::new(&name, &email, &git2::Time::new(0, 0))
.map_err(|e| anyhow::anyhow!("创建签名失败: {}", e.message()));
}
}
}
if let Ok(config) = git2::Config::open_default() {
if let Ok(name) = config.get_string("user.name") {
if let Ok(email) = config.get_string("user.email") {
return git2::Signature::new(&name, &email, &git2::Time::new(0, 0))
.map_err(|e| anyhow::anyhow!("创建签名失败: {}", e.message()));
}
}
}
git2::Signature::new("AgentSwitch", "agentswitch@local", &git2::Time::new(0, 0))
.map_err(|e| anyhow::anyhow!("创建签名失败: {}", e.message()))
}
fn has_uncommitted_changes(repo: &git2::Repository) -> anyhow::Result<bool> {
let mut status_opts = git2::StatusOptions::new();
status_opts.include_untracked(true);
status_opts.recurse_untracked_dirs(true);
let statuses = repo.statuses(Some(&mut status_opts))?;
Ok(statuses.iter().any(|s| {
let st = s.status();
st.intersects(
git2::Status::INDEX_NEW
| git2::Status::INDEX_MODIFIED
| git2::Status::INDEX_DELETED
| git2::Status::INDEX_RENAMED
| git2::Status::WT_MODIFIED
| git2::Status::WT_DELETED
| git2::Status::WT_RENAMED
| git2::Status::WT_NEW,
)
}))
}
fn current_branch_name(repo: &git2::Repository) -> Option<String> {
repo.head()
.ok()
.and_then(|head| head.shorthand().map(|s| s.to_string()))
}
fn get_remote_url(repo: &git2::Repository, remote_name: &str) -> Option<String> {
repo.find_remote(remote_name)
.ok()
.and_then(|r| r.url().map(|u| u.to_string()))
}
fn list_remotes(repo: &git2::Repository) -> Vec<String> {
repo.remotes()
.map(|names| {
names
.iter()
.filter_map(|n| n.map(|s| s.to_string()))
.collect()
})
.unwrap_or_default()
}
pub fn run_sync_init() -> anyhow::Result<()> {
let config_dir = config_dir();
println!("{}", "初始化 Git 同步".green().bold());
println!("{}", "=".repeat(40).green());
println!();
if repo_is_initialized() {
println!("{} Git 仓库已存在", "✓".green());
print_info("如需重新初始化,请先删除 ~/.agentswitch/.git 目录");
return Ok(());
}
let repo = git2::Repository::init(&config_dir)?;
println!("{} Git 仓库已初始化", "✓".green());
let gitignore_path = config_dir.join(".gitignore");
if !gitignore_path.exists() {
let gitignore_content = "# AgentSwitch Git Sync\n*.key\nwizard_state.toml\n";
std::fs::write(&gitignore_path, gitignore_content)?;
println!("{} 已创建 .gitignore", "✓".green());
} else {
println!("{} .gitignore 已存在", "✓".green());
}
match master_key_exists() {
Ok(true) => {
println!("{} 加密密钥已配置", "✓".green());
}
Ok(false) => {
print_warning("加密密钥未配置,建议运行 'asw crypto keygen' 生成密钥");
}
Err(e) => {
print_warning(&format!("无法检查密钥状态: {}", e));
}
}
let signature = get_signature(&repo)?;
let mut index = repo.index()?;
index.add_path(Path::new(".gitignore"))?;
index.write()?;
let tree_id = index.write_tree()?;
let tree = repo.find_tree(tree_id)?;
repo.commit(
Some("HEAD"),
&signature,
&signature,
"Initial commit: AgentSwitch sync initialization",
&tree,
&[],
)?;
println!("{} 初始提交已创建", "✓".green());
println!();
print_success("Git 同步初始化完成");
println!();
print_info("下一步:");
println!(" 1. 添加远程仓库: asw sync remote add <url>");
println!(" 2. 推送配置: asw sync push");
Ok(())
}
pub fn run_sync_push() -> anyhow::Result<()> {
println!("{}", "推送配置到远程".green().bold());
println!("{}", "=".repeat(40).green());
println!();
let repo = open_repo()?;
let remotes = list_remotes(&repo);
if remotes.is_empty() {
anyhow::bail!("未配置远程仓库。请先运行 'asw sync remote add <url>'");
}
let remote_name = if remotes.contains(&"origin".to_string()) {
"origin"
} else {
&remotes[0]
};
let remote_url = get_remote_url(&repo, remote_name);
println!(
"远程仓库: {} ({})",
remote_name,
remote_url.as_deref().unwrap_or("未知")
);
if has_uncommitted_changes(&repo)? {
println!("{} 检测到未提交的更改,正在提交...", "~".yellow());
let mut index = repo.index()?;
index.add_all(["*"], git2::IndexAddOption::DEFAULT, None)?;
index.write()?;
let tree_id = index.write_tree()?;
let tree = repo.find_tree(tree_id)?;
let head = repo.head()?;
let parent_commit = head.peel_to_commit()?;
let signature = get_signature(&repo)?;
let now = chrono::Local::now();
let message = format!("Update configuration ({})", now.format("%Y-%m-%d %H:%M:%S"));
repo.commit(
Some("HEAD"),
&signature,
&signature,
&message,
&tree,
&[&parent_commit],
)?;
println!("{} 更改已提交: {}", "✓".green(), message);
} else {
println!("{} 没有未提交的更改", "✓".green());
}
let branch = current_branch_name(&repo).unwrap_or_else(|| "master".to_string());
println!("正在推送到 {}/{}...", remote_name, branch);
let mut remote = repo.find_remote(remote_name)?;
let refspec = format!("refs/heads/{}:refs/heads/{}", branch, branch);
let mut push_options = git2::PushOptions::new();
let mut callbacks = git2::RemoteCallbacks::new();
callbacks.push_update_reference(|refname, status| {
if let Some(status) = status {
return Err(git2::Error::from_str(&format!(
"推送 {} 失败: {}",
refname, status
)));
}
Ok(())
});
callbacks.credentials(|_url, username_from_url, _allowed_types| {
git2::Cred::ssh_key_from_agent(username_from_url.unwrap_or("git"))
});
push_options.remote_callbacks(callbacks);
remote.push(&[&refspec], Some(&mut push_options))?;
println!("{} 推送成功", "✓".green());
print_success(&format!("配置已推送到 {}/{}", remote_name, branch));
Ok(())
}
pub fn run_sync_pull() -> anyhow::Result<()> {
println!("{}", "从远程拉取配置".green().bold());
println!("{}", "=".repeat(40).green());
println!();
let mut repo = open_repo()?;
let remotes = list_remotes(&repo);
if remotes.is_empty() {
anyhow::bail!("未配置远程仓库。请先运行 'asw sync remote add <url>'");
}
let remote_name = if remotes.contains(&"origin".to_string()) {
"origin".to_string()
} else {
remotes[0].clone()
};
let remote_url = get_remote_url(&repo, &remote_name);
println!(
"远程仓库: {} ({})",
remote_name,
remote_url.as_deref().unwrap_or("未知")
);
match master_key_exists() {
Ok(true) => println!("{} 加密密钥已配置", "✓".green()),
Ok(false) => print_warning("加密密钥未配置,如需解密请先运行 'asw crypto keygen'"),
Err(_) => {}
}
let branch = current_branch_name(&repo).unwrap_or_else(|| "master".to_string());
println!("正在从 {} 拉取...", remote_name);
{
let refspec = format!(
"refs/heads/{}:refs/remotes/{}/{}",
branch, remote_name, branch
);
let mut fetch_options = git2::FetchOptions::new();
let mut callbacks = git2::RemoteCallbacks::new();
callbacks.credentials(|_url, username_from_url, _allowed_types| {
git2::Cred::ssh_key_from_agent(username_from_url.unwrap_or("git"))
});
fetch_options.remote_callbacks(callbacks);
let mut remote = repo.find_remote(&remote_name)?;
remote.fetch(&[&refspec], Some(&mut fetch_options), None)?;
}
println!("{} 远程更改已获取", "✓".green());
let (fetch_commit_id, head_commit_id) = {
let fetch_head = repo.find_reference("FETCH_HEAD")?;
let fetch_id = fetch_head.peel_to_commit()?.id();
let head_id = repo.head()?.peel_to_commit()?.id();
(fetch_id, head_id)
};
let stash_index = if has_uncommitted_changes(&repo)? {
print_warning("工作区有未提交的更改,正在暂存...");
let signature = get_signature(&repo)?;
repo.stash_save2(
&signature,
Some("auto-stash before pull"),
Some(git2::StashFlags::DEFAULT),
)?;
println!("{} 本地更改已暂存", "✓".green());
Some(0usize)
} else {
None
};
let ancestor_id = repo.merge_base(head_commit_id, fetch_commit_id)?;
if ancestor_id == head_commit_id {
repo.reference("HEAD", fetch_commit_id, true, "Fast-forward pull")?;
repo.checkout_head(Some(git2::build::CheckoutBuilder::new().force()))?;
println!("{} 快进合并完成 (fast-forward)", "✓".green());
} else if ancestor_id == fetch_commit_id {
println!("{} 本地已是最新的", "✓".green());
} else {
{
let fetch_annotated = repo.find_annotated_commit(fetch_commit_id)?;
let mut merge_opts = git2::MergeOptions::new();
merge_opts.file_favor(git2::FileFavor::Normal);
repo.merge(&[&fetch_annotated], Some(&mut merge_opts), None)?;
}
let mut index = repo.index()?;
if index.has_conflicts() {
repo.cleanup_state()?;
anyhow::bail!("合并冲突!请手动解决冲突后运行 'asw sync push'");
}
{
let head_commit = repo.find_commit(head_commit_id)?;
let fetch_commit = repo.find_commit(fetch_commit_id)?;
let signature = get_signature(&repo)?;
let tree_id = index.write_tree()?;
let tree = repo.find_tree(tree_id)?;
repo.commit(
Some("HEAD"),
&signature,
&signature,
&format!("Merge remote-tracking branch '{}/{}'", remote_name, branch),
&tree,
&[&head_commit, &fetch_commit],
)?;
}
println!("{} 自动合并完成", "✓".green());
}
if let Some(idx) = stash_index {
let mut stash_apply_opts = git2::StashApplyOptions::new();
repo.stash_apply(idx, Some(&mut stash_apply_opts))?;
repo.stash_drop(idx)?;
println!("{} 本地更改已恢复", "✓".green());
}
println!();
print_success(&format!("配置已从 {}/{} 拉取完成", remote_name, branch));
Ok(())
}
pub fn run_sync_status() -> anyhow::Result<()> {
println!("{}", "同步状态".green().bold());
println!("{}", "=".repeat(40).green());
println!();
if !repo_is_initialized() {
println!("{:<20} {}", "Git 仓库:", "✗ 未初始化".red());
println!();
print_info("运行 'asw sync init' 初始化 Git 同步");
return Ok(());
}
let repo = open_repo()?;
println!("{:<20} {}", "Git 仓库:", "✓ 已初始化".green());
let branch = current_branch_name(&repo);
match &branch {
Some(b) => println!("{:<20} {}", "当前分支:", b.cyan()),
None => println!("{:<20} {}", "当前分支:", "(空仓库)".yellow()),
}
let remotes = list_remotes(&repo);
if remotes.is_empty() {
println!("{:<20} {}", "远程仓库:", "✗ 未配置".red());
} else {
println!("{:<20}", "远程仓库:");
for remote_name in &remotes {
let url = get_remote_url(&repo, remote_name);
println!(
" {} {}",
format!("{}:", remote_name).cyan(),
url.as_deref().unwrap_or("未知")
);
}
}
let has_changes = has_uncommitted_changes(&repo)?;
if has_changes {
println!("{:<20} {}", "工作区:", "⚠ 有未提交的更改".yellow());
} else {
println!("{:<20} {}", "工作区:", "✓ 干净".green());
}
match master_key_exists() {
Ok(true) => println!("{:<20} {}", "加密密钥:", "✓ 已配置".green()),
Ok(false) => println!("{:<20} {}", "加密密钥:", "✗ 未配置".yellow()),
Err(e) => println!("{:<20} {}", "加密密钥:", format!("? 检查失败: {}", e).red()),
}
if let Ok(head) = repo.head() {
if let Ok(commit) = head.peel_to_commit() {
let time = commit.time();
let seconds = time.seconds();
let offset = time.offset_minutes();
let datetime = chrono::DateTime::from_timestamp(seconds, 0)
.map(|dt: chrono::DateTime<chrono::Utc>| {
dt + chrono::Duration::minutes(offset as i64)
})
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
.unwrap_or_else(|| "未知".to_string());
let message = commit.message().unwrap_or("(无消息)");
let short_msg = if message.len() > 50 {
format!("{}...", &message[..47])
} else {
message.to_string()
};
println!("{:<20} {}", "最后提交:", short_msg);
println!("{:<20} {}", "提交时间:", datetime);
}
}
println!();
if has_changes {
print_info("运行 'asw sync push' 提交并推送更改");
}
Ok(())
}
pub fn run_sync_remote_add(url: &str) -> anyhow::Result<()> {
if !repo_is_initialized() {
anyhow::bail!("Git 仓库未初始化。请先运行 'asw sync init'");
}
let repo = open_repo()?;
if repo.find_remote("origin").is_ok() {
anyhow::bail!(
"远程仓库 'origin' 已存在。使用 'asw sync remote set-url origin <new-url>' 修改"
);
}
repo.remote("origin", url)?;
print_success(&format!("远程仓库已添加: origin -> {}", url));
Ok(())
}
pub fn run_sync_remote_remove(name: &str) -> anyhow::Result<()> {
if !repo_is_initialized() {
anyhow::bail!("Git 仓库未初始化。请先运行 'asw sync init'");
}
let repo = open_repo()?;
repo.remote_delete(name)?;
print_success(&format!("远程仓库 '{}' 已删除", name));
Ok(())
}
pub fn run_sync_remote_list() -> anyhow::Result<()> {
if !repo_is_initialized() {
anyhow::bail!("Git 仓库未初始化。请先运行 'asw sync init'");
}
let repo = open_repo()?;
let remotes = list_remotes(&repo);
if remotes.is_empty() {
println!("没有配置远程仓库");
print_info("使用 'asw sync remote add <url>' 添加远程仓库");
return Ok(());
}
println!("{}", "远程仓库列表:".green().bold());
println!("{}", "-".repeat(60));
println!("{:<15} URL", "名称");
println!("{}", "-".repeat(60));
for name in &remotes {
let url = get_remote_url(&repo, name);
println!("{:<15} {}", name, url.as_deref().unwrap_or("未知"));
}
Ok(())
}
pub fn run_sync_remote_set_url(name: &str, url: &str) -> anyhow::Result<()> {
if !repo_is_initialized() {
anyhow::bail!("Git 仓库未初始化。请先运行 'asw sync init'");
}
let repo = open_repo()?;
let existing_url = get_remote_url(&repo, name);
if existing_url.is_none() {
anyhow::bail!("远程仓库 '{}' 不存在", name);
}
repo.remote_set_url(name, url)?;
print_success(&format!("远程仓库 '{}' 的 URL 已更新: {}", name, url));
Ok(())
}