git_rune/git/
repo.rs

1use crate::error::Result;
2use git2::{Repository, StatusOptions};
3use std::{
4    fs,
5    path::{Path, PathBuf},
6};
7
8pub struct GitRepo {
9    pub(crate) repo: Repository,
10    pub(crate) root_path: PathBuf,
11}
12
13impl GitRepo {
14    pub fn open() -> Result<Self> {
15        let repo = Repository::discover(".")?;
16        let root_path = repo.path().parent().unwrap().to_path_buf();
17        Ok(Self { repo, root_path })
18    }
19
20    pub fn open_from(path: &Path) -> Result<Self> {
21        let repo = Repository::discover(path)?;
22        let root_path = repo.path().parent().unwrap().to_path_buf();
23        Ok(Self { repo, root_path })
24    }
25
26    pub fn root_path(&self) -> &Path {
27        &self.root_path
28    }
29
30    pub fn get_tracked_files(&self, target_path: &Path) -> Result<Vec<PathBuf>> {
31        let mut opts = StatusOptions::new();
32        opts.include_untracked(true)
33            .include_ignored(false)
34            .include_unmodified(true);
35
36        let statuses = self.repo.statuses(Some(&mut opts))?;
37        let root = self.root_path();
38
39        let files: Vec<PathBuf> = statuses
40            .iter()
41            .filter_map(|entry| {
42                let status = entry.status();
43                if status.is_wt_deleted() || status.is_index_deleted() {
44                    return None;
45                }
46                let path = entry.path()?;
47                let full_path = root.join(path);
48                if full_path.starts_with(target_path) {
49                    Some(full_path)
50                } else {
51                    None
52                }
53            })
54            .collect();
55
56        Ok(files)
57    }
58
59    pub fn get_repo_name(&self) -> Result<String> {
60        if let Ok(remote) = self.repo.find_remote("origin") {
61            if let Some(url) = remote.url() {
62                return Ok(url
63                    .split(['/', ':'].as_ref())
64                    .last()
65                    .unwrap_or("unknown")
66                    .trim()
67                    .trim_end_matches(".git")
68                    .to_string());
69            }
70        }
71
72        Ok(self
73            .root_path
74            .file_name()
75            .and_then(|n| n.to_str())
76            .unwrap_or("unknown")
77            .to_string())
78    }
79
80    pub fn setup_commit_template(&self) -> Result<()> {
81        let template_path = self.root_path.join(".git/commit-template");
82        fs::write(&template_path, "")?;
83
84        let mut config = self.repo.config()?;
85        config.set_str("commit.template", ".git/commit-template")?;
86
87        Ok(())
88    }
89
90    pub fn write_commit_template(&self, message: &str) -> Result<()> {
91        let template_path = self.root_path.join(".git/commit-template");
92        Ok(fs::write(template_path, message)?)
93    }
94
95    pub fn install_hook(&self, hook_name: &str, script: &str) -> Result<()> {
96        let hooks_dir = self.root_path.join(".git/hooks");
97        fs::create_dir_all(&hooks_dir)?;
98
99        let hook_path = hooks_dir.join(hook_name);
100        fs::write(&hook_path, script)?;
101
102        #[cfg(unix)]
103        {
104            use std::os::unix::fs::PermissionsExt;
105            fs::set_permissions(&hook_path, fs::Permissions::from_mode(0o755))?;
106        }
107
108        Ok(())
109    }
110
111    pub fn has_staged_changes(&self) -> Result<bool> {
112        let mut status_opts = StatusOptions::new();
113        status_opts.include_untracked(true).include_ignored(false);
114
115        let statuses = self.repo.statuses(Some(&mut status_opts))?;
116
117        Ok(statuses.iter().any(|entry| {
118            let status = entry.status();
119            status.is_index_new()
120                || status.is_index_modified()
121                || status.is_index_deleted()
122                || status.is_index_renamed()
123                || status.is_index_typechange()
124        }))
125    }
126}