Skip to content

Commit

Permalink
feat: Enhance File System Operations (#33)
Browse files Browse the repository at this point in the history
* feat: File System Operations changes

* Update git-ai version in Cargo.lock and refactor install.rs

Based only on the changes visible in the diff, this commit:

- Updates the git-ai version from 0.2.56 to 0.2.57 in Cargo.lock
- Removes the reference to 'ai::filesystem::Filesystem' and adds a reference to 'crate::filesystem::Filesystem' in 'install.rs'
- Modifies the 'run' function to an 'install' function, taking a 'Filesystem' reference instead of creating a 'Filesystem' instance
- Adjusts the 'bail' function call for an existing hook error, formatting the hook file path as italic
- Alters how the hook file is symlinked to the hook binary file in 'install.rs'

* Refactor filesystem usage and paths in install and reinstall modules

Based solely on the changes visible in the diff, this commit:
- Replaces `Filesystem::new()` instances with `fs = Filesystem::new()` in `reinstall.rs` and `install.rs`.
- Changes function calls to `git_ai_hook_bin_path()` and `prepare_commit_msg_path()` instead of `hook_bin()` and `hook_file()`.
- Modifies the import statement in `install.rs` from `crate::filesystem::Filesystem` to `ai::filesystem::Filesystem`.
- Adds a new code block in `install.rs` to create a git hooks path if it doesn't exist and run the install operation.
- Adjusts the symlink creation in `reinstall.rs`, using `&hook_bin` instead of `hook_bin`.

---------

Co-authored-by: Git AI Test <[email protected]>
  • Loading branch information
oleander and Git AI Test authored Feb 8, 2025
1 parent 862b5a7 commit d2cf454
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 85 deletions.
201 changes: 137 additions & 64 deletions src/filesystem.rs
Original file line number Diff line number Diff line change
@@ -1,51 +1,86 @@
#![allow(dead_code)]

use std::path::{Path, PathBuf};
use std::{env, fs};
use std::os::unix::fs::symlink as symlink_unix;

use anyhow::{bail, Context, Result};
use git2::{Repository, RepositoryOpenFlags as Flags};

/// Error messages for filesystem operations
const ERR_CURRENT_DIR: &str = "Failed to get current directory";

/// Represents the filesystem structure for git-ai.
/// Handles paths for hooks and binaries.
#[derive(Debug, Clone)]
pub struct Filesystem {
git_ai_hook_bin_path: PathBuf,
git_hooks_path: PathBuf
}

/// Represents a file in the filesystem.
/// Provides operations for file manipulation.
#[derive(Debug, Clone)]
pub struct File {
path: PathBuf
}

impl File {
/// Creates a new File instance.
///
/// # Arguments
/// * `path` - The path to the file
pub fn new(path: PathBuf) -> Self {
Self { path }
}

/// Checks if the file exists.
///
/// # Returns
/// * `bool` - true if the file exists, false otherwise
pub fn exists(&self) -> bool {
self.path.exists()
}

/// Deletes the file from the filesystem.
///
/// # Returns
/// * `Result<()>` - Success or an error if deletion fails
pub fn delete(&self) -> Result<()> {
log::debug!("Removing file at {}", self);
fs::remove_file(&self.path).context(format!("Failed to remove file at {}", self))
fs::remove_file(&self.path).with_context(|| format!("Failed to remove file at {}", self))
}

pub fn symlink(&self, target: File) -> Result<()> {
/// Creates a symbolic link to the target file.
///
/// # Arguments
/// * `target` - The file to link to
///
/// # Returns
/// * `Result<()>` - Success or an error if link creation fails
pub fn symlink(&self, target: &File) -> Result<()> {
log::debug!("Symlinking {} to {}", target, self);
symlink_unix(&target.path, &self.path).context(format!("Failed to symlink {} to {}", target, self))
symlink_unix(&target.path, &self.path).with_context(|| format!("Failed to symlink {} to {}", target, self))
}

/// Gets the relative path from the current directory.
///
/// # Returns
/// * `Result<Dir>` - The relative path as a Dir or an error
pub fn relative_path(&self) -> Result<Dir> {
Dir::new(
self
.path
.strip_prefix(env::current_dir().context("Failed to get current directory")?)
.context(format!("Failed to strip prefix from {}", self.path.display()))?
.to_path_buf()
)
.into()
let current_dir = env::current_dir().context(ERR_CURRENT_DIR)?;
let relative = self
.path
.strip_prefix(&current_dir)
.with_context(|| format!("Failed to strip prefix from {}", self.path.display()))?;

Ok(Dir::new(relative.to_path_buf()))
}

/// Gets the parent directory of the file.
///
/// # Returns
/// * `Dir` - The parent directory
pub fn parent(&self) -> Dir {
Dir::new(self.path.parent().unwrap_or(Path::new("")).to_path_buf())
}
Expand All @@ -59,7 +94,8 @@ impl From<&File> for Dir {

impl std::fmt::Display for File {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.relative_path().unwrap_or(self.into()).path.display())
let path = self.relative_path().unwrap_or_else(|_| self.into());
write!(f, "{}", path.path.display())
}
}

Expand All @@ -69,99 +105,136 @@ impl From<File> for Result<File> {
}
}

impl From<Dir> for Result<Dir> {
fn from(dir: Dir) -> Result<Dir> {
Ok(dir)
}
}

/// Represents a directory in the filesystem.
/// Provides operations for directory manipulation.
#[derive(Debug, Clone)]
pub struct Dir {
path: PathBuf
}

impl std::fmt::Display for Dir {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.path.display())
}
}

impl From<Filesystem> for Result<Filesystem> {
fn from(filesystem: Filesystem) -> Result<Filesystem> {
Ok(filesystem)
}
}

impl Dir {
/// Creates a new Dir instance.
///
/// # Arguments
/// * `path` - The path to the directory
pub fn new(path: PathBuf) -> Self {
Self { path }
}

/// Checks if the directory exists.
///
/// # Returns
/// * `bool` - true if the directory exists, false otherwise
pub fn exists(&self) -> bool {
self.path.exists()
}

/// Creates the directory and all parent directories if they don't exist.
///
/// # Returns
/// * `Result<()>` - Success or an error if creation fails
pub fn create_dir_all(&self) -> Result<()> {
log::debug!("Creating directory at {}", self);
fs::create_dir_all(&self.path).context(format!("Failed to create directory at {}", self))
fs::create_dir_all(&self.path).with_context(|| format!("Failed to create directory at {}", self))
}

/// Gets the relative path from the current directory.
///
/// # Returns
/// * `Result<Self>` - The relative path or an error
pub fn relative_path(&self) -> Result<Self> {
Self::new(
self
.path
.strip_prefix(env::current_dir().context("Failed to get current directory")?)
.context(format!("Failed to strip prefix from {}", self.path.display()))?
.to_path_buf()
)
.into()
let current_dir = env::current_dir().context(ERR_CURRENT_DIR)?;
let relative = self
.path
.strip_prefix(&current_dir)
.with_context(|| format!("Failed to strip prefix from {}", self.path.display()))?;

Ok(Self::new(relative.to_path_buf()))
}
}

impl Filesystem {
pub fn new() -> Result<Self> {
let current_dir = env::current_dir().context("Failed to get current directory")?;
let git_ai_bin_path = env::current_exe().context("Failed to get current executable")?;

let repo = Repository::open_ext(current_dir.clone(), Flags::empty(), Vec::<&Path>::new())
.context(format!("Failed to open repository at {}", current_dir.clone().display()))?;
impl std::fmt::Display for Dir {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.path.display())
}
}

let mut git_path = repo.path().to_path_buf();
// if relative, make it absolute
if git_path.is_relative() {
// make git_path absolute using the current folder as the base
git_path = current_dir.join(git_path);
}
impl From<Dir> for Result<Dir> {
fn from(dir: Dir) -> Result<Dir> {
Ok(dir)
}
}

let git_ai_hook_bin_path = git_ai_bin_path
.parent()
.context(format!("Failed to get parent directory of {}", git_ai_bin_path.display()))?
.join("git-ai-hook");
impl Filesystem {
/// Creates a new Filesystem instance.
/// Initializes paths for git hooks and binaries.
///
/// # Returns
/// * `Result<Self>` - The initialized filesystem or an error
pub fn new() -> Result<Self> {
// Get current directory
let current_dir = env::current_dir().context(ERR_CURRENT_DIR)?;

if !git_ai_hook_bin_path.exists() {
bail!("Hook binary not found at {}", git_ai_hook_bin_path.display());
}
// Get executable path
let git_ai_bin_path = env::current_exe().context("Failed to get current executable")?;

Self {
// Open git repository
let repo = Repository::open_ext(&current_dir, Flags::empty(), Vec::<&Path>::new())
.with_context(|| format!("Failed to open repository at {}", current_dir.display()))?;

// Get git path and ensure it's absolute
let git_path = {
let mut path = repo.path().to_path_buf();
if path.is_relative() {
path = current_dir.join(path);
}
path
};

// Get hook binary path
let git_ai_hook_bin_path = {
let hook_path = git_ai_bin_path
.parent()
.with_context(|| format!("Failed to get parent directory of {}", git_ai_bin_path.display()))?
.join("git-ai-hook");

if !hook_path.exists() {
bail!("Hook binary not found at {}", hook_path.display());
}
hook_path
};

Ok(Self {
git_ai_hook_bin_path,
git_hooks_path: git_path.join("hooks")
}
.into()
})
}

/// Gets the path to the git-ai hook binary.
///
/// # Returns
/// * `Result<File>` - The hook binary path or an error
pub fn git_ai_hook_bin_path(&self) -> Result<File> {
File::new(self.git_ai_hook_bin_path.clone()).into()
Ok(File::new(self.git_ai_hook_bin_path.clone()))
}

/// Gets the path to the git hooks directory.
///
/// # Returns
/// * `Dir` - The hooks directory path
pub fn git_hooks_path(&self) -> Dir {
Dir::new(self.git_hooks_path.clone())
}

/// Gets the path to the prepare-commit-msg hook.
///
/// # Returns
/// * `Result<File>` - The hook path or an error
pub fn prepare_commit_msg_path(&self) -> Result<File> {
if !self.git_hooks_path.exists() {
bail!("Hooks directory not found at {}", self.git_hooks_path.display());
}

File::new(self.git_hooks_path.join("prepare-commit-msg")).into()
Ok(File::new(self.git_hooks_path.join("prepare-commit-msg")))
}
}
23 changes: 15 additions & 8 deletions src/install.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
use anyhow::{bail, Result};
use ai::filesystem::Filesystem;
use colored::Colorize;
use console::Emoji;
use ai::filesystem::Filesystem;

const EMOJI: Emoji<'_, '_> = Emoji("🔗", "");

pub fn run() -> Result<()> {
let filesystem = Filesystem::new()?;
let fs = Filesystem::new()?;

if !filesystem.git_hooks_path().exists() {
filesystem.git_hooks_path().create_dir_all()?;
if !fs.git_hooks_path().exists() {
fs.git_hooks_path().create_dir_all()?;
}

let hook_file = filesystem.prepare_commit_msg_path()?;
let hook_bin = filesystem.git_ai_hook_bin_path()?;
install(&fs)
}

pub fn install(fs: &Filesystem) -> Result<()> {
let hook_bin = fs.git_ai_hook_bin_path()?;
let hook_file = fs.prepare_commit_msg_path()?;

if hook_file.exists() {
bail!("Hook already exists at {}, please run 'git ai hook reinstall'", hook_file);
bail!(
"Hook already exists at {}, please run 'git ai hook reinstall'",
hook_file.to_string().italic()
);
}

hook_file.symlink(hook_bin)?;
hook_file.symlink(&hook_bin)?;

println!("{EMOJI} Hook symlinked successfully to {}", hook_file.to_string().italic());

Expand Down
13 changes: 4 additions & 9 deletions src/reinstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,16 @@ use colored::*;
const EMOJI: Emoji<'_, '_> = Emoji("🔗", "");

pub fn run() -> Result<()> {
let filesystem = Filesystem::new()?;

if !filesystem.git_hooks_path().exists() {
filesystem.git_hooks_path().create_dir_all()?;
}

let hook_file = filesystem.prepare_commit_msg_path()?;
let hook_bin = filesystem.git_ai_hook_bin_path()?;
let fs = Filesystem::new()?;
let hook_bin = fs.git_ai_hook_bin_path()?;
let hook_file = fs.prepare_commit_msg_path()?;

if hook_file.exists() {
log::debug!("Removing existing hook file: {}", hook_file);
hook_file.delete()?;
}

hook_file.symlink(hook_bin)?;
hook_file.symlink(&hook_bin)?;

println!(
"{EMOJI} Hook symlinked successfully to {}",
Expand Down
6 changes: 2 additions & 4 deletions src/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ pub trait Styled {

impl Styled for PathBuf {
fn relative_path(&self) -> PathBuf {
let current_dir = env::current_dir().unwrap();
let relative_path = self
.strip_prefix(&current_dir)
.unwrap_or(current_dir.as_path());
let current_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let relative_path = self.strip_prefix(&current_dir).unwrap_or(self.as_path());
relative_path.to_path_buf()
}
}

0 comments on commit d2cf454

Please sign in to comment.