capsula_git_context/
lib.rs

1mod config;
2mod error;
3
4use crate::error::GitContextError;
5
6use crate::config::GitContextFactory;
7use capsula_core::captured::Captured;
8use capsula_core::context::{Context, ContextFactory, RuntimeParams};
9use capsula_core::error::CoreResult;
10use git2::Repository;
11use serde_json::json;
12use std::path::PathBuf;
13
14pub const KEY: &str = "git";
15
16#[derive(Debug)]
17pub struct GitContext {
18    pub name: String,
19    pub working_dir: PathBuf,
20    pub allow_dirty: bool,
21}
22
23#[derive(Debug)]
24pub struct GitCaptured {
25    pub name: String,
26    pub working_dir: PathBuf,
27    pub sha: String, // TODO: Use more suitable type
28    pub is_dirty: bool,
29    pub abort_on_dirty: bool,
30}
31
32impl Captured for GitCaptured {
33    fn to_json(&self) -> serde_json::Value {
34        json!({
35            "type": KEY.to_string(),
36            "name": self.name,
37            "working_dir": self.working_dir.to_string_lossy(),
38            "sha": self.sha,
39            "is_dirty": self.is_dirty,
40            "abort_on_dirty": self.abort_on_dirty
41        })
42    }
43
44    fn abort_requested(&self) -> bool {
45        self.is_dirty && self.abort_on_dirty
46    }
47}
48
49impl Context for GitContext {
50    type Output = GitCaptured;
51
52    fn run(&self, _params: &RuntimeParams) -> CoreResult<Self::Output> {
53        let repo_path = if self.working_dir.as_os_str().is_empty() {
54            std::env::current_dir()?
55        } else {
56            self.working_dir.clone()
57        };
58
59        let repo = Repository::discover(&repo_path).map_err(|e| {
60            if e.code() == git2::ErrorCode::NotFound {
61                GitContextError::NotARepository
62            } else {
63                GitContextError::GitOperation(e)
64            }
65        })?;
66
67        let head = repo.head().map_err(GitContextError::from)?;
68        let oid = head.target().ok_or_else(|| GitContextError::HeadNotFound {
69            message: "HEAD does not point to a valid commit".to_string(),
70        })?;
71
72        // Check if repository is dirty
73        let statuses = repo.statuses(None).map_err(GitContextError::from)?;
74        let is_dirty = !statuses.is_empty();
75
76        // If dirty and not allowed, we'll signal abort through the Captured trait
77        // rather than returning an error, so other contexts can still be captured
78        if is_dirty && !self.allow_dirty {
79            eprintln!("Warning: Repository has uncommitted changes. Run will be aborted after context capture.");
80        }
81
82        Ok(GitCaptured {
83            name: self.name.clone(),
84            working_dir: repo_path,
85            sha: oid.to_string(),
86            is_dirty,
87            abort_on_dirty: !self.allow_dirty,
88        })
89    }
90}
91
92/// Create a factory for GitContext
93pub fn create_factory() -> Box<dyn ContextFactory> {
94    Box::new(GitContextFactory)
95}