1pub mod post_rewrite;
2pub mod prepare_commit_msg;
3
4use serde::{Deserialize, Serialize};
5use std::path::Path;
6
7use crate::annotate::gather::AuthorContext;
8use crate::error::chronicle_error::{IoSnafu, JsonSnafu};
9use crate::error::Result;
10use snafu::ResultExt;
11
12const HOOK_BEGIN_MARKER: &str = "# --- chronicle hook begin ---";
13const HOOK_END_MARKER: &str = "# --- chronicle hook end ---";
14
15const POST_COMMIT_SCRIPT: &str = r#"# --- chronicle hook begin ---
16# Installed by chronicle. Do not edit between these markers.
17if command -v git-chronicle >/dev/null 2>&1; then
18 git-chronicle annotate --commit HEAD --sync &
19fi
20# --- chronicle hook end ---"#;
21
22const PREPARE_COMMIT_MSG_SCRIPT: &str = r#"# --- chronicle hook begin ---
23# Installed by chronicle. Do not edit between these markers.
24if command -v git-chronicle >/dev/null 2>&1; then
25 git-chronicle hook prepare-commit-msg "$@"
26fi
27# --- chronicle hook end ---"#;
28
29const POST_REWRITE_SCRIPT: &str = r#"# --- chronicle hook begin ---
30# Installed by chronicle. Do not edit between these markers.
31if command -v git-chronicle >/dev/null 2>&1; then
32 git-chronicle hook post-rewrite "$@"
33fi
34# --- chronicle hook end ---"#;
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct PendingContext {
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub task: Option<String>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub reasoning: Option<String>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub dependencies: Option<String>,
45 #[serde(default, skip_serializing_if = "Vec::is_empty")]
46 pub tags: Vec<String>,
47}
48
49impl PendingContext {
50 pub fn to_author_context(&self) -> AuthorContext {
51 AuthorContext {
52 task: self.task.clone(),
53 reasoning: self.reasoning.clone(),
54 dependencies: self.dependencies.clone(),
55 tags: self.tags.clone(),
56 }
57 }
58}
59
60fn pending_context_path(git_dir: &Path) -> std::path::PathBuf {
61 git_dir.join("chronicle").join("pending-context.json")
62}
63
64pub fn read_pending_context(git_dir: &Path) -> Result<Option<PendingContext>> {
66 let path = pending_context_path(git_dir);
67 if !path.exists() {
68 return Ok(None);
69 }
70 let contents = std::fs::read_to_string(&path).context(IoSnafu)?;
71 let ctx: PendingContext = serde_json::from_str(&contents).context(JsonSnafu)?;
72 Ok(Some(ctx))
73}
74
75pub fn write_pending_context(git_dir: &Path, ctx: &PendingContext) -> Result<()> {
77 let path = pending_context_path(git_dir);
78 if let Some(parent) = path.parent() {
79 std::fs::create_dir_all(parent).context(IoSnafu)?;
80 }
81 let json = serde_json::to_string_pretty(ctx).context(JsonSnafu)?;
82 std::fs::write(&path, json).context(IoSnafu)?;
83 Ok(())
84}
85
86pub fn delete_pending_context(git_dir: &Path) -> Result<()> {
88 let path = pending_context_path(git_dir);
89 if path.exists() {
90 std::fs::remove_file(&path).context(IoSnafu)?;
91 }
92 Ok(())
93}
94
95fn install_single_hook(hooks_dir: &Path, hook_name: &str, script: &str) -> Result<()> {
97 let hook_path = hooks_dir.join(hook_name);
98
99 let existing = if hook_path.exists() {
100 std::fs::read_to_string(&hook_path).context(IoSnafu)?
101 } else {
102 String::new()
103 };
104
105 let new_content = if existing.contains(HOOK_BEGIN_MARKER) {
106 let mut result = String::new();
108 let mut in_section = false;
109 for line in existing.lines() {
110 if line.contains(HOOK_BEGIN_MARKER) {
111 in_section = true;
112 result.push_str(script);
113 result.push('\n');
114 continue;
115 }
116 if line.contains(HOOK_END_MARKER) {
117 in_section = false;
118 continue;
119 }
120 if !in_section {
121 result.push_str(line);
122 result.push('\n');
123 }
124 }
125 result
126 } else if existing.is_empty() {
127 format!("#!/bin/sh\n{script}\n")
128 } else {
129 let mut content = existing.clone();
130 if !content.ends_with('\n') {
131 content.push('\n');
132 }
133 content.push('\n');
134 content.push_str(script);
135 content.push('\n');
136 content
137 };
138
139 std::fs::write(&hook_path, &new_content).context(IoSnafu)?;
140
141 #[cfg(unix)]
142 {
143 use std::os::unix::fs::PermissionsExt;
144 let perms = std::fs::Permissions::from_mode(0o755);
145 std::fs::set_permissions(&hook_path, perms).context(IoSnafu)?;
146 }
147
148 Ok(())
149}
150
151pub fn install_hooks(git_dir: &Path) -> Result<()> {
153 let hooks_dir = git_dir.join("hooks");
154 std::fs::create_dir_all(&hooks_dir).context(IoSnafu)?;
155
156 install_single_hook(&hooks_dir, "post-commit", POST_COMMIT_SCRIPT)?;
157 install_single_hook(&hooks_dir, "prepare-commit-msg", PREPARE_COMMIT_MSG_SCRIPT)?;
158 install_single_hook(&hooks_dir, "post-rewrite", POST_REWRITE_SCRIPT)?;
159
160 Ok(())
161}