1use std::path::{Path, PathBuf};
2use std::{env, fs};
3use std::os::unix::fs::symlink as symlink_unix;
4
5use anyhow::{bail, Context, Result};
6use git2::{Repository, RepositoryOpenFlags as Flags};
7
8const ERR_CURRENT_DIR: &str = "Failed to get current directory";
10
11#[derive(Debug, Clone)]
14pub struct Filesystem {
15 git_ai_hook_bin_path: PathBuf,
16 git_hooks_path: PathBuf
17}
18
19#[derive(Debug, Clone)]
22pub struct File {
23 path: PathBuf
24}
25
26impl File {
27 pub fn new(path: PathBuf) -> Self {
32 Self { path }
33 }
34
35 pub fn exists(&self) -> bool {
40 self.path.exists()
41 }
42
43 pub fn delete(&self) -> Result<()> {
48 log::debug!("Removing file at {self}");
49 fs::remove_file(&self.path).with_context(|| format!("Failed to remove file at {self}"))
50 }
51
52 pub fn symlink(&self, target: &File) -> Result<()> {
60 log::debug!("Symlinking {target} to {self}");
61 symlink_unix(&target.path, &self.path).with_context(|| format!("Failed to symlink {target} to {self}"))
62 }
63
64 pub fn relative_path(&self) -> Result<Dir> {
69 let current_dir = env::current_dir().context(ERR_CURRENT_DIR)?;
70 let relative = self
71 .path
72 .strip_prefix(¤t_dir)
73 .with_context(|| format!("Failed to strip prefix from {}", self.path.display()))?;
74
75 Ok(Dir::new(relative.to_path_buf()))
76 }
77
78 pub fn parent(&self) -> Dir {
83 Dir::new(self.path.parent().unwrap_or(Path::new("")).to_path_buf())
84 }
85}
86
87impl From<&File> for Dir {
88 fn from(file: &File) -> Self {
89 file.parent()
90 }
91}
92
93impl std::fmt::Display for File {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 let path = self.relative_path().unwrap_or_else(|_| self.into());
96 write!(f, "{}", path.path.display())
97 }
98}
99
100impl From<File> for Result<File> {
101 fn from(file: File) -> Result<File> {
102 Ok(file)
103 }
104}
105
106#[derive(Debug, Clone)]
109pub struct Dir {
110 path: PathBuf
111}
112
113impl Dir {
114 pub fn new(path: PathBuf) -> Self {
119 Self { path }
120 }
121
122 pub fn exists(&self) -> bool {
127 self.path.exists()
128 }
129
130 pub fn create_dir_all(&self) -> Result<()> {
135 log::debug!("Creating directory at {self}");
136 fs::create_dir_all(&self.path).with_context(|| format!("Failed to create directory at {self}"))
137 }
138
139 pub fn relative_path(&self) -> Result<Self> {
144 let current_dir = env::current_dir().context(ERR_CURRENT_DIR)?;
145 let relative = self
146 .path
147 .strip_prefix(¤t_dir)
148 .with_context(|| format!("Failed to strip prefix from {}", self.path.display()))?;
149
150 Ok(Self::new(relative.to_path_buf()))
151 }
152}
153
154impl std::fmt::Display for Dir {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 write!(f, "{}", self.path.display())
157 }
158}
159
160impl From<Dir> for Result<Dir> {
161 fn from(dir: Dir) -> Result<Dir> {
162 Ok(dir)
163 }
164}
165
166impl Filesystem {
167 pub fn new() -> Result<Self> {
173 let current_dir = env::current_dir().context(ERR_CURRENT_DIR)?;
175
176 let git_ai_bin_path = env::current_exe().context("Failed to get current executable")?;
178
179 let repo = Repository::open_ext(¤t_dir, Flags::empty(), Vec::<&Path>::new())
181 .with_context(|| format!("Failed to open repository at {}", current_dir.display()))?;
182
183 let git_path = {
185 let mut path = repo.path().to_path_buf();
186 if path.is_relative() {
187 path = current_dir.join(path);
188 }
189 path
190 };
191
192 let git_ai_hook_bin_path = {
194 let hook_path = git_ai_bin_path
195 .parent()
196 .with_context(|| format!("Failed to get parent directory of {}", git_ai_bin_path.display()))?
197 .join("git-ai-hook");
198
199 if !hook_path.exists() {
200 bail!("Hook binary not found at {}", hook_path.display());
201 }
202 hook_path
203 };
204
205 Ok(Self {
206 git_ai_hook_bin_path,
207 git_hooks_path: git_path.join("hooks")
208 })
209 }
210
211 pub fn git_ai_hook_bin_path(&self) -> Result<File> {
216 Ok(File::new(self.git_ai_hook_bin_path.clone()))
217 }
218
219 pub fn prepare_commit_msg_path(&self) -> Result<File> {
224 if !self.git_hooks_path.exists() {
225 bail!("Hooks directory not found at {}", self.git_hooks_path.display());
226 }
227
228 Ok(File::new(self.git_hooks_path.join("prepare-commit-msg")))
229 }
230}