Skip to main content

chronicle/annotate/
gather.rs

1use crate::error::{chronicle_error, Result};
2use crate::git::{FileDiff, GitOps};
3use snafu::ResultExt;
4use std::path::PathBuf;
5
6/// Author-provided context captured at commit time.
7#[derive(Debug, Clone, Default)]
8pub struct AuthorContext {
9    pub task: Option<String>,
10    pub reasoning: Option<String>,
11    pub dependencies: Option<String>,
12    pub tags: Vec<String>,
13}
14
15/// All the context needed for annotation, assembled before calling the agent.
16#[derive(Debug, Clone)]
17pub struct AnnotationContext {
18    pub commit_sha: String,
19    pub commit_message: String,
20    pub author_name: String,
21    pub author_email: String,
22    pub timestamp: String,
23    pub diffs: Vec<FileDiff>,
24    pub author_context: Option<AuthorContext>,
25}
26
27/// Build the annotation context for a commit.
28pub fn build_context(git_ops: &dyn GitOps, commit: &str) -> Result<AnnotationContext> {
29    // Get commit metadata
30    let info = git_ops
31        .commit_info(commit)
32        .context(chronicle_error::GitSnafu)?;
33
34    // Get file diffs
35    let diffs = git_ops.diff(commit).context(chronicle_error::GitSnafu)?;
36
37    // Gather author context from pending-context file and env vars
38    let author_context = gather_author_context();
39
40    Ok(AnnotationContext {
41        commit_sha: info.sha,
42        commit_message: info.message,
43        author_name: info.author_name,
44        author_email: info.author_email,
45        timestamp: info.timestamp,
46        diffs,
47        author_context,
48    })
49}
50
51/// Gather author context from pending-context.json and environment variables.
52fn gather_author_context() -> Option<AuthorContext> {
53    // Try reading pending context from .git/chronicle/pending-context.json
54    let pending = read_pending_context_from_git_dir();
55
56    // Also check environment variables
57    let env_task = std::env::var("CHRONICLE_TASK")
58        .ok()
59        .filter(|s| !s.is_empty());
60    let env_reasoning = std::env::var("CHRONICLE_REASONING")
61        .ok()
62        .filter(|s| !s.is_empty());
63    let env_dependencies = std::env::var("CHRONICLE_DEPENDENCIES")
64        .ok()
65        .filter(|s| !s.is_empty());
66    let env_tags: Vec<String> = std::env::var("CHRONICLE_TAGS")
67        .ok()
68        .filter(|s| !s.is_empty())
69        .map(|s| s.split(',').map(|t| t.trim().to_string()).collect())
70        .unwrap_or_default();
71
72    // Merge: pending context provides base, env vars override
73    let mut ctx = pending.map(|p| p.to_author_context()).unwrap_or_default();
74
75    if env_task.is_some() {
76        ctx.task = env_task;
77    }
78    if env_reasoning.is_some() {
79        ctx.reasoning = env_reasoning;
80    }
81    if env_dependencies.is_some() {
82        ctx.dependencies = env_dependencies;
83    }
84    if !env_tags.is_empty() {
85        ctx.tags = env_tags;
86    }
87
88    // Return None if everything is empty
89    if ctx.task.is_none()
90        && ctx.reasoning.is_none()
91        && ctx.dependencies.is_none()
92        && ctx.tags.is_empty()
93    {
94        None
95    } else {
96        Some(ctx)
97    }
98}
99
100/// Try to read pending context from the .git directory.
101fn read_pending_context_from_git_dir() -> Option<crate::hooks::PendingContext> {
102    // Try to find .git directory by looking at current dir
103    let git_dir = PathBuf::from(".git");
104    if !git_dir.exists() {
105        return None;
106    }
107    crate::hooks::read_pending_context(&git_dir).ok().flatten()
108}