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}