capsula_capture_git_repo/
lib.rs

1mod config;
2mod error;
3
4use crate::error::GitHookError;
5
6use crate::config::{GitHookConfig, GitHookFactory};
7use capsula_core::captured::Captured;
8use capsula_core::error::CapsulaResult;
9use capsula_core::hook::{Hook, HookFactory, RuntimeParams};
10use git2::Repository;
11use serde_json::json;
12use std::path::PathBuf;
13
14pub const KEY: &str = "capture-git-repo";
15
16#[derive(Debug)]
17pub struct GitHook {
18    pub config: GitHookConfig,
19    pub name: String,
20    pub working_dir: PathBuf,
21    pub allow_dirty: bool,
22}
23
24#[derive(Debug)]
25pub struct GitCaptured {
26    pub name: String,
27    pub working_dir: PathBuf,
28    pub sha: String, // TODO: Use more suitable type
29    pub is_dirty: bool,
30    pub abort_on_dirty: bool,
31}
32
33impl Captured for GitCaptured {
34    fn to_json(&self) -> serde_json::Value {
35        json!({
36            "working_dir": self.working_dir.to_string_lossy(),
37            "sha": self.sha,
38            "is_dirty": self.is_dirty,
39            "abort_on_dirty": self.abort_on_dirty
40        })
41    }
42
43    fn abort_requested(&self) -> bool {
44        self.is_dirty && self.abort_on_dirty
45    }
46}
47
48impl Hook for GitHook {
49    type Config = GitHookConfig;
50    type Output = GitCaptured;
51
52    fn id(&self) -> String {
53        KEY.to_string()
54    }
55
56    fn config(&self) -> &Self::Config {
57        &self.config
58    }
59
60    fn run(&self, params: &RuntimeParams) -> CapsulaResult<Self::Output> {
61        let repo_path = if self.working_dir.as_os_str().is_empty() {
62            std::env::current_dir()?
63        } else {
64            self.working_dir.clone()
65        };
66
67        let repo = Repository::discover(&repo_path).map_err(|e| {
68            if e.code() == git2::ErrorCode::NotFound {
69                GitHookError::NotARepository
70            } else {
71                GitHookError::GitOperation(e)
72            }
73        })?;
74
75        let head = repo.head().map_err(GitHookError::from)?;
76        let oid = head.target().ok_or_else(|| GitHookError::HeadNotFound {
77            message: "HEAD does not point to a valid commit".to_string(),
78        })?;
79
80        // Check if repository is dirty
81        let statuses = repo.statuses(None).map_err(GitHookError::from)?;
82        let is_dirty = !statuses.is_empty();
83
84        // If dirty and not allowed, we'll signal abort through the Captured trait
85        // rather than returning an error, so other hooks can still be captured
86        if is_dirty && !self.allow_dirty {
87            eprintln!(
88                "Warning: Repository has uncommitted changes. Run will be aborted after hooks capture."
89            );
90        }
91
92        // Output diff content if dirty
93        if is_dirty {
94            let run_dir =
95                params
96                    .run_dir
97                    .as_ref()
98                    .ok_or_else(|| GitHookError::RunDirNotSpecified {
99                        message: "Run directory is not specified in runtime parameters".to_string(),
100                    })?;
101            let diff_content = GitHook::diff_content(&repo)?;
102            // Output to a patch file in the run directory
103            let patch_file_path = run_dir.join(format!("{}.patch", self.name));
104            std::fs::write(&patch_file_path, diff_content).map_err(GitHookError::IoError)?;
105        }
106
107        Ok(GitCaptured {
108            name: self.name.clone(),
109            working_dir: repo_path,
110            sha: oid.to_string(),
111            is_dirty,
112            abort_on_dirty: !self.allow_dirty,
113        })
114    }
115}
116
117impl GitHook {
118    fn diff_content(repo: &Repository) -> CapsulaResult<String> {
119        let mut diff_opts = git2::DiffOptions::new();
120        diff_opts.include_untracked(true);
121        let diff = repo
122            .diff_index_to_workdir(None, Some(&mut diff_opts))
123            .map_err(GitHookError::from)?;
124
125        let mut diff_content = String::new();
126        diff.print(git2::DiffFormat::Patch, |_, _, line| {
127            diff_content.push_str(std::str::from_utf8(line.content()).unwrap_or(""));
128            true
129        })
130        .map_err(GitHookError::from)?;
131
132        Ok(diff_content)
133    }
134}
135
136/// Create a factory for GitHook
137pub fn create_factory() -> Box<dyn HookFactory> {
138    Box::new(GitHookFactory)
139}