ai/
filesystem.rs

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
8/// Error messages for filesystem operations
9const ERR_CURRENT_DIR: &str = "Failed to get current directory";
10
11/// Represents the filesystem structure for git-ai.
12/// Handles paths for hooks and binaries.
13#[derive(Debug, Clone)]
14pub struct Filesystem {
15  git_ai_hook_bin_path: PathBuf,
16  git_hooks_path:       PathBuf
17}
18
19/// Represents a file in the filesystem.
20/// Provides operations for file manipulation.
21#[derive(Debug, Clone)]
22pub struct File {
23  path: PathBuf
24}
25
26impl File {
27  /// Creates a new File instance.
28  ///
29  /// # Arguments
30  /// * `path` - The path to the file
31  pub fn new(path: PathBuf) -> Self {
32    Self { path }
33  }
34
35  /// Checks if the file exists.
36  ///
37  /// # Returns
38  /// * `bool` - true if the file exists, false otherwise
39  pub fn exists(&self) -> bool {
40    self.path.exists()
41  }
42
43  /// Deletes the file from the filesystem.
44  ///
45  /// # Returns
46  /// * `Result<()>` - Success or an error if deletion fails
47  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  /// Creates a symbolic link to the target file.
53  ///
54  /// # Arguments
55  /// * `target` - The file to link to
56  ///
57  /// # Returns
58  /// * `Result<()>` - Success or an error if link creation fails
59  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  /// Gets the relative path from the current directory.
65  ///
66  /// # Returns
67  /// * `Result<Dir>` - The relative path as a Dir or an error
68  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(&current_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  /// Gets the parent directory of the file.
79  ///
80  /// # Returns
81  /// * `Dir` - The parent directory
82  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/// Represents a directory in the filesystem.
107/// Provides operations for directory manipulation.
108#[derive(Debug, Clone)]
109pub struct Dir {
110  path: PathBuf
111}
112
113impl Dir {
114  /// Creates a new Dir instance.
115  ///
116  /// # Arguments
117  /// * `path` - The path to the directory
118  pub fn new(path: PathBuf) -> Self {
119    Self { path }
120  }
121
122  /// Checks if the directory exists.
123  ///
124  /// # Returns
125  /// * `bool` - true if the directory exists, false otherwise
126  pub fn exists(&self) -> bool {
127    self.path.exists()
128  }
129
130  /// Creates the directory and all parent directories if they don't exist.
131  ///
132  /// # Returns
133  /// * `Result<()>` - Success or an error if creation fails
134  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  /// Gets the relative path from the current directory.
140  ///
141  /// # Returns
142  /// * `Result<Self>` - The relative path or an error
143  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(&current_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  /// Creates a new Filesystem instance.
168  /// Initializes paths for git hooks and binaries.
169  ///
170  /// # Returns
171  /// * `Result<Self>` - The initialized filesystem or an error
172  pub fn new() -> Result<Self> {
173    // Get current directory
174    let current_dir = env::current_dir().context(ERR_CURRENT_DIR)?;
175
176    // Get executable path
177    let git_ai_bin_path = env::current_exe().context("Failed to get current executable")?;
178
179    // Open git repository
180    let repo = Repository::open_ext(&current_dir, Flags::empty(), Vec::<&Path>::new())
181      .with_context(|| format!("Failed to open repository at {}", current_dir.display()))?;
182
183    // Get git path and ensure it's absolute
184    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    // Get hook binary path
193    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  /// Gets the path to the git-ai hook binary.
212  ///
213  /// # Returns
214  /// * `Result<File>` - The hook binary path or an error
215  pub fn git_ai_hook_bin_path(&self) -> Result<File> {
216    Ok(File::new(self.git_ai_hook_bin_path.clone()))
217  }
218
219  /// Gets the path to the prepare-commit-msg hook.
220  ///
221  /// # Returns
222  /// * `Result<File>` - The hook path or an error
223  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}