capsula_capture_git_repo/
lib.rs1mod 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, 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 let statuses = repo.statuses(None).map_err(GitHookError::from)?;
82 let is_dirty = !statuses.is_empty();
83
84 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 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 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
136pub fn create_factory() -> Box<dyn HookFactory> {
138 Box::new(GitHookFactory)
139}