Skip to main content

git_commit_helper_cli/
install.rs

1use anyhow::{Context, Result};
2use dialoguer::{Confirm, Select};
3use std::fs;
4use std::path::{Path, PathBuf};
5use std::process::Command;
6
7pub fn install_git_hook(repo_path: Option<PathBuf>, force: bool) -> Result<()> {
8    let repo_path = repo_path.unwrap_or_else(|| PathBuf::from("."));
9    let git_dir = find_git_dir(&repo_path)?;
10    let hooks_dir = git_dir.join("hooks");
11    let commit_msg_hook = hooks_dir.join("commit-msg");
12
13    // 处理已存在的 hook
14    let (use_backup, run_before) = if commit_msg_hook.exists() {
15        if force {
16            let (keep_old, run_before) = handle_existing_hook(&commit_msg_hook)?;
17            (keep_old, if keep_old { Some(run_before) } else { None })
18        } else {
19            return Err(anyhow::anyhow!(
20                "Hook 文件已存在: {}。使用 --force 选项进行处理。",
21                commit_msg_hook.display()
22            ));
23        }
24    } else {
25        (false, None)
26    };
27
28    // 获取当前二进制的路径
29    let current_exe = std::env::current_exe()?;
30    let binary_path = current_exe.canonicalize()?;
31
32    // 创建新的 hook 内容
33    let hook_content = create_hook_content(&binary_path, use_backup, run_before)?;
34
35    // 确保 hooks 目录存在
36    fs::create_dir_all(&hooks_dir)?;
37
38    // 写入 hook 脚本
39    fs::write(&commit_msg_hook, hook_content)?;
40
41    // 设置可执行权限
42    #[cfg(unix)]
43    {
44        use std::os::unix::fs::PermissionsExt;
45        fs::set_permissions(&commit_msg_hook, fs::Permissions::from_mode(0o755))?;
46    }
47
48    println!("Git hook 已安装到: {}", commit_msg_hook.display());
49    Ok(())
50}
51
52fn handle_existing_hook(hook_path: &Path) -> Result<(bool, bool)> {
53    println!("检测到已存在的 commit-msg hook");
54
55    let keep_old = Confirm::with_theme(&dialoguer::theme::ColorfulTheme::default())
56        .with_prompt("是否保留已存在的 hook 功能?")
57        .default(true)
58        .interact()?;
59
60    if !keep_old {
61        fs::remove_file(hook_path)?;
62        return Ok((false, false));
63    }
64
65    // 备份原有 hook 到同目录下
66    let hooks_dir = hook_path.parent().unwrap();
67    let backup_path = hooks_dir.join("commit-msg.old");
68    fs::rename(hook_path, &backup_path)?;
69    println!("已存在的 hook 已备份到: {}", backup_path.display());
70
71    let options = vec![
72        "先执行翻译程序,再执行原 hook",
73        "先执行原 hook,再执行翻译程序"
74    ];
75    let selection = Select::with_theme(&dialoguer::theme::ColorfulTheme::default())
76        .with_prompt("请选择执行顺序")
77        .items(&options)
78        .default(0)
79        .interact()?;
80
81    Ok((true, selection == 0))
82}
83
84fn create_hook_content(binary_path: &Path, use_backup: bool, run_before: Option<bool>) -> Result<String> {
85    if !use_backup {
86        return Ok(format!(
87            r#"#!/bin/sh
88# 检查是否在命令行中使用了 --no-review 选项
89if git config --bool git-commit-helper.disable-review >/dev/null 2>&1; then
90    exec "{}" --no-review "$1"
91else
92    exec "{}" "$1"
93fi
94"#,
95            binary_path.display(),
96            binary_path.display()
97        ));
98    }
99
100    let git_hooks_dir = binary_path.parent().unwrap().parent().unwrap().join("hooks");
101    let backup_path = git_hooks_dir.join("commit-msg.old");
102
103    match run_before {
104        Some(true) => Ok(format!(
105            r#"#!/bin/sh
106# 检查是否在命令行中使用了 --no-review 选项
107if git config --bool git-commit-helper.disable-review >/dev/null 2>&1; then
108    # 先运行当前程序,如果失败则中止
109    "{}" --no-review "$1" || exit $?
110else
111    # 先运行当前程序,如果失败则中止
112    "{}" "$1" || exit $?
113fi
114
115# 如果存在旧的 hook,则运行它
116if [ -x "{}" ]; then
117    exec "{}" "$1"
118fi
119"#,
120            binary_path.display(),
121            binary_path.display(),
122            backup_path.display(),
123            backup_path.display()
124        )),
125        Some(false) => Ok(format!(
126            r#"#!/bin/sh
127# 如果存在旧的 hook,先运行它
128if [ -x "{}" ]; then
129    "{}" "$1" || exit $?
130fi
131
132# 检查是否在命令行中使用了 --no-review 选项
133if git config --bool git-commit-helper.disable-review >/dev/null 2>&1; then
134    # 运行当前程序
135    exec "{}" --no-review "$1"
136else
137    # 运行当前程序
138    exec "{}" "$1"
139fi
140"#,
141            backup_path.display(),
142            backup_path.display(),
143            binary_path.display(),
144            binary_path.display()
145        )),
146        None => unreachable!("使用备份时必须指定运行顺序"),
147    }
148}
149
150fn find_git_dir(start_path: &Path) -> Result<PathBuf> {
151    let output = Command::new("git")
152        .arg("rev-parse")
153        .arg("--git-dir")
154        .current_dir(start_path)
155        .output()
156        .context("执行 git 命令失败")?;
157
158    if !output.status.success() {
159        return Err(anyhow::anyhow!("当前目录不是 git 仓库"));
160    }
161
162    let git_dir = String::from_utf8(output.stdout)?;
163    Ok(PathBuf::from(git_dir.trim()))
164}