use std::path::{Path, PathBuf};
mod common;
use common::{run_bin_with_envs, unique_dir};
fn home_envs(home: &Path) -> Vec<(&'static str, String)> {
vec![
("HOME", home.to_string_lossy().into_owned()),
("USERPROFILE", home.to_string_lossy().into_owned()),
("APPDATA", home.to_string_lossy().into_owned()),
]
}
fn init_git_repo(dir: &Path) -> PathBuf {
let git = git2::Repository::init(dir).expect("git init 失败");
let mut cfg = git.config().unwrap();
cfg.set_str("user.name", "test").unwrap();
cfg.set_str("user.email", "test@test.com").unwrap();
dir.join(".git").join("hooks")
}
fn parse_hook_command(content: &str) -> String {
content
.lines()
.find(|l| l.starts_with("code-repo-wiki "))
.expect("hook 应包含 code-repo-wiki 命令行")
.split(" 2>>")
.next()
.unwrap()
.trim()
.to_string()
}
#[test]
fn test_install_writes_git_hooks() {
let work_dir = unique_dir("writes_hooks");
let _ = std::fs::remove_dir_all(&work_dir);
std::fs::create_dir_all(&work_dir).unwrap();
let hooks_dir = init_git_repo(&work_dir);
let home = unique_dir("writes_hooks_home");
let _ = std::fs::remove_dir_all(&home);
std::fs::create_dir_all(&home).unwrap();
let envs: Vec<(&str, String)> = home_envs(&home);
let envs_ref: Vec<(&str, &str)> = envs.iter().map(|(k, v)| (*k, v.as_str())).collect();
let out = run_bin_with_envs(&work_dir, &["install"], &envs_ref);
assert!(
out.status.success(),
"install 应成功,stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
for hook_name in ["post-commit", "post-merge"] {
let hook_path = hooks_dir.join(hook_name);
let content = std::fs::read_to_string(&hook_path)
.unwrap_or_else(|e| panic!("{} hook 应写入 {}: {}", hook_name, hook_path.display(), e));
assert!(
content.contains("code-repo-wiki update"),
"{hook_name} 应含 update 命令,实际: {content}"
);
assert!(
!content.contains("--quiet"),
"{hook_name} 不应含已废弃的 --quiet(clap 会报错被 || true 吞掉),实际: {content}"
);
assert!(
content.contains("command -v code-repo-wiki"),
"{hook_name} 应含 PATH 探测,实际: {content}"
);
}
let _ = std::fs::remove_dir_all(&work_dir);
let _ = std::fs::remove_dir_all(&home);
}
#[test]
fn test_hook_command_succeeds() {
let work_dir = unique_dir("cmd_valid");
let _ = std::fs::remove_dir_all(&work_dir);
std::fs::create_dir_all(&work_dir).unwrap();
let hooks_dir = init_git_repo(&work_dir);
let home = unique_dir("cmd_valid_home");
let _ = std::fs::remove_dir_all(&home);
std::fs::create_dir_all(&home).unwrap();
let envs: Vec<(&str, String)> = home_envs(&home);
let envs_ref: Vec<(&str, &str)> = envs.iter().map(|(k, v)| (*k, v.as_str())).collect();
let out = run_bin_with_envs(&work_dir, &["install"], &envs_ref);
assert!(out.status.success(), "install 应成功");
let content = std::fs::read_to_string(hooks_dir.join("post-commit")).unwrap();
assert_eq!(
parse_hook_command(&content),
"code-repo-wiki update",
"hook 命令应为 code-repo-wiki update(无 --quiet),实际 hook:\n{content}"
);
let _ = std::fs::remove_dir_all(&work_dir);
let _ = std::fs::remove_dir_all(&home);
}
#[test]
fn test_install_non_git_repo_prints_hint() {
let work_dir = unique_dir("no_git");
let _ = std::fs::remove_dir_all(&work_dir);
std::fs::create_dir_all(&work_dir).unwrap();
let home = unique_dir("no_git_home");
let _ = std::fs::remove_dir_all(&home);
std::fs::create_dir_all(&home).unwrap();
let envs: Vec<(&str, String)> = home_envs(&home);
let envs_ref: Vec<(&str, &str)> = envs.iter().map(|(k, v)| (*k, v.as_str())).collect();
let out = run_bin_with_envs(&work_dir, &["install"], &envs_ref);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
out.status.success(),
"非 git 仓库 install 应成功退出,stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
stdout.contains("未检测到 .git 目录"),
"应打印跳过提示,实际 stdout: {stdout}"
);
let _ = std::fs::remove_dir_all(&work_dir);
let _ = std::fs::remove_dir_all(&home);
}
#[test]
fn test_uninstall_removes_only_own_hooks() {
let work_dir = unique_dir("rm_own");
let _ = std::fs::remove_dir_all(&work_dir);
std::fs::create_dir_all(&work_dir).unwrap();
let hooks_dir = init_git_repo(&work_dir);
std::fs::write(
hooks_dir.join("post-commit"),
"#!/bin/sh\n# code-repo-wiki: auto-update wiki on commit\ncode-repo-wiki update 2>/dev/null || true\n",
)
.unwrap();
std::fs::write(
hooks_dir.join("post-merge"),
"#!/bin/sh\necho '人工合并后处理'\n",
)
.unwrap();
let home = unique_dir("rm_own_home");
let _ = std::fs::remove_dir_all(&home);
std::fs::create_dir_all(&home).unwrap();
let envs: Vec<(&str, String)> = home_envs(&home);
let envs_ref: Vec<(&str, &str)> = envs.iter().map(|(k, v)| (*k, v.as_str())).collect();
let out = run_bin_with_envs(&work_dir, &["uninstall", "--force"], &envs_ref);
assert!(
out.status.success(),
"uninstall --force 应成功,stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
!hooks_dir.join("post-commit").exists(),
"含 code-repo-wiki 标记的 post-commit 应被删除"
);
assert!(
hooks_dir.join("post-merge").exists(),
"人工 post-merge(无 code-repo-wiki 标记)应保留"
);
assert_eq!(
std::fs::read_to_string(hooks_dir.join("post-merge")).unwrap(),
"#!/bin/sh\necho '人工合并后处理'\n",
"保留的 hook 内容不应被改动"
);
let _ = std::fs::remove_dir_all(&work_dir);
let _ = std::fs::remove_dir_all(&home);
}
#[test]
fn test_install_upgrades_legacy_hooks() {
let work_dir = unique_dir("upgrade_hooks");
let _ = std::fs::remove_dir_all(&work_dir);
std::fs::create_dir_all(&work_dir).unwrap();
let hooks_dir = init_git_repo(&work_dir);
std::fs::write(
hooks_dir.join("post-commit"),
"#!/bin/sh\n# code-repo-wiki: auto-update wiki on commit\ncode-repo-wiki update 2>/dev/null || true\n",
)
.unwrap();
std::fs::write(
hooks_dir.join("post-merge"),
"#!/bin/sh\necho '人工 hook'\n",
)
.unwrap();
let home = unique_dir("upgrade_hooks_home");
let _ = std::fs::remove_dir_all(&home);
std::fs::create_dir_all(&home).unwrap();
let envs: Vec<(&str, String)> = home_envs(&home);
let envs_ref: Vec<(&str, &str)> = envs.iter().map(|(k, v)| (*k, v.as_str())).collect();
let out = run_bin_with_envs(&work_dir, &["install"], &envs_ref);
assert!(
out.status.success(),
"install 应成功,stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let upgraded = std::fs::read_to_string(hooks_dir.join("post-commit")).unwrap();
assert!(
upgraded.contains("# code-repo-wiki: append-begin"),
"旧模板应升级为含当前标记,实际: {upgraded}"
);
assert!(
upgraded.contains("code-repo-wiki update"),
"升级后仍应含 update 命令"
);
let merged = std::fs::read_to_string(hooks_dir.join("post-merge")).unwrap();
assert!(
merged.contains("echo '人工 hook'"),
"人工 hook 原内容应保留,实际: {merged}"
);
assert!(
merged.contains("# code-repo-wiki: append-begin"),
"人工 hook 应追加 code-repo-wiki 块,实际: {merged}"
);
assert!(
merged.contains("code-repo-wiki update"),
"追加块应含 update 命令,实际: {merged}"
);
let _ = std::fs::remove_dir_all(&work_dir);
let _ = std::fs::remove_dir_all(&home);
}
#[test]
fn test_install_appends_idempotent() {
let work_dir = unique_dir("append_idem");
let _ = std::fs::remove_dir_all(&work_dir);
std::fs::create_dir_all(&work_dir).unwrap();
let hooks_dir = init_git_repo(&work_dir);
std::fs::write(
hooks_dir.join("post-commit"),
"#!/bin/sh\necho '人工 hook'\n",
)
.unwrap();
let home = unique_dir("append_idem_home");
let _ = std::fs::remove_dir_all(&home);
std::fs::create_dir_all(&home).unwrap();
let envs: Vec<(&str, String)> = home_envs(&home);
let envs_ref: Vec<(&str, &str)> = envs.iter().map(|(k, v)| (*k, v.as_str())).collect();
let out = run_bin_with_envs(&work_dir, &["install"], &envs_ref);
assert!(out.status.success(), "install 应成功");
let once = std::fs::read_to_string(hooks_dir.join("post-commit")).unwrap();
let out = run_bin_with_envs(&work_dir, &["install"], &envs_ref);
assert!(out.status.success(), "第二次 install 应成功");
let twice = std::fs::read_to_string(hooks_dir.join("post-commit")).unwrap();
assert_eq!(once, twice, "幂等:第二次 install 不得改变 hook 内容");
assert_eq!(
twice.matches("# code-repo-wiki: append-begin").count(),
1,
"追加块只应出现一次,实际: {twice}"
);
assert!(
twice.contains("echo '人工 hook'"),
"人工 hook 原内容应保留,实际: {twice}"
);
let _ = std::fs::remove_dir_all(&work_dir);
let _ = std::fs::remove_dir_all(&home);
}
#[test]
fn test_install_upgrades_appended_block_keeps_user_content() {
let work_dir = unique_dir("append_upgrade");
let _ = std::fs::remove_dir_all(&work_dir);
std::fs::create_dir_all(&work_dir).unwrap();
let hooks_dir = init_git_repo(&work_dir);
std::fs::write(
hooks_dir.join("post-commit"),
"#!/bin/sh\necho '人工 hook'\n# code-repo-wiki: append-begin\ncode-repo-wiki update 2>/dev/null || true\n# code-repo-wiki: append-end\n",
)
.unwrap();
let home = unique_dir("append_upgrade_home");
let _ = std::fs::remove_dir_all(&home);
std::fs::create_dir_all(&home).unwrap();
let envs: Vec<(&str, String)> = home_envs(&home);
let envs_ref: Vec<(&str, &str)> = envs.iter().map(|(k, v)| (*k, v.as_str())).collect();
let out = run_bin_with_envs(&work_dir, &["install"], &envs_ref);
assert!(out.status.success(), "install 应成功");
let content = std::fs::read_to_string(hooks_dir.join("post-commit")).unwrap();
assert!(
content.contains("echo '人工 hook'"),
"人工内容应保留,实际: {content}"
);
assert!(
content.contains("2>>.code-repo-wiki/update-error.log"),
"块应升级为当前模板(v36 D2 错误日志特性),实际: {content}"
);
assert_eq!(
content.matches("# code-repo-wiki: append-begin").count(),
1,
"块只应有一个,实际: {content}"
);
let _ = std::fs::remove_dir_all(&work_dir);
let _ = std::fs::remove_dir_all(&home);
}
#[test]
fn test_uninstall_strips_appended_block() {
let work_dir = unique_dir("strip_block");
let _ = std::fs::remove_dir_all(&work_dir);
std::fs::create_dir_all(&work_dir).unwrap();
let hooks_dir = init_git_repo(&work_dir);
std::fs::write(
hooks_dir.join("post-commit"),
"#!/bin/sh\necho '人工 hook'\n# code-repo-wiki: append-begin\n# 自动更新 wiki\ncode-repo-wiki update 2>>.code-repo-wiki/update-error.log || echo x >&2\n# code-repo-wiki: append-end\n",
)
.unwrap();
let home = unique_dir("strip_block_home");
let _ = std::fs::remove_dir_all(&home);
std::fs::create_dir_all(&home).unwrap();
let envs: Vec<(&str, String)> = home_envs(&home);
let envs_ref: Vec<(&str, &str)> = envs.iter().map(|(k, v)| (*k, v.as_str())).collect();
let out = run_bin_with_envs(&work_dir, &["uninstall", "--force"], &envs_ref);
assert!(
out.status.success(),
"uninstall --force 应成功,stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let remaining = std::fs::read_to_string(hooks_dir.join("post-commit")).unwrap();
assert!(
remaining.contains("echo '人工 hook'"),
"人工内容应保留,实际: {remaining}"
);
assert!(
!remaining.contains("code-repo-wiki"),
"块应被剥离,实际: {remaining}"
);
let _ = std::fs::remove_dir_all(&work_dir);
let _ = std::fs::remove_dir_all(&home);
}