Skip to main content

chronicle/hooks/
post_rewrite.rs

1use crate::annotate::squash::{migrate_amend_annotation, AmendMigrationContext};
2use crate::error::chronicle_error::{GitSnafu, JsonSnafu};
3use crate::error::Result;
4use crate::git::GitOps;
5use crate::schema::Annotation;
6use snafu::ResultExt;
7
8/// A mapping from old SHA to new SHA, as provided by git's post-rewrite hook.
9#[derive(Debug, Clone)]
10pub struct RewriteMapping {
11    pub old_sha: String,
12    pub new_sha: String,
13}
14
15/// Handle the post-rewrite hook.
16///
17/// Git calls post-rewrite with the rewrite type as the first argument
18/// ("amend" or "rebase") and old→new SHA mappings on stdin.
19///
20/// For v1, we only handle "amend" rewrites. Rebase is logged and skipped.
21pub fn handle_post_rewrite(
22    git_ops: &dyn GitOps,
23    rewrite_type: &str,
24    mappings: &[RewriteMapping],
25) -> Result<()> {
26    if rewrite_type != "amend" {
27        tracing::info!(
28            "post-rewrite: {} rewrites not yet supported, skipping {} mappings",
29            rewrite_type,
30            mappings.len()
31        );
32        return Ok(());
33    }
34
35    for mapping in mappings {
36        if let Err(e) = handle_single_amend(git_ops, &mapping.old_sha, &mapping.new_sha) {
37            tracing::warn!(
38                "Failed to migrate annotation from {} to {}: {}",
39                mapping.old_sha,
40                mapping.new_sha,
41                e
42            );
43            // Continue with other mappings; don't fail the whole hook
44        }
45    }
46
47    Ok(())
48}
49
50/// Handle a single amend migration: copy/update the annotation from old SHA to new SHA.
51fn handle_single_amend(git_ops: &dyn GitOps, old_sha: &str, new_sha: &str) -> Result<()> {
52    // Read the old annotation
53    let old_note = git_ops.note_read(old_sha).context(GitSnafu)?;
54    let old_json = match old_note {
55        Some(json) => json,
56        None => {
57            tracing::debug!("No annotation for old commit {old_sha}, skipping amend migration");
58            return Ok(());
59        }
60    };
61
62    let old_annotation: Annotation = serde_json::from_str(&old_json).context(JsonSnafu)?;
63
64    // Get the new commit's message
65    let new_info = git_ops.commit_info(new_sha).context(GitSnafu)?;
66
67    // Compute diff to determine if code changed or message-only amend
68    // We use the diff of the new commit (new_sha vs its parent)
69    let new_diffs = git_ops.diff(new_sha).context(GitSnafu)?;
70    let old_diffs = git_ops.diff(old_sha).context(GitSnafu)?;
71
72    // Simple heuristic: if the diffs have the same content, it's message-only
73    let new_diff_text = format!("{:?}", new_diffs);
74    let old_diff_text = format!("{:?}", old_diffs);
75    let diff_for_migration = if new_diff_text == old_diff_text {
76        String::new() // message-only amend
77    } else {
78        new_diff_text
79    };
80
81    let ctx = AmendMigrationContext {
82        new_commit: new_sha.to_string(),
83        new_diff: diff_for_migration,
84        old_annotation,
85        new_message: new_info.message,
86    };
87
88    let new_annotation = migrate_amend_annotation(&ctx);
89
90    let json = serde_json::to_string_pretty(&new_annotation).context(JsonSnafu)?;
91    git_ops.note_write(new_sha, &json).context(GitSnafu)?;
92
93    tracing::info!("Migrated annotation from {old_sha} to {new_sha}");
94    Ok(())
95}
96
97/// Parse stdin lines from git post-rewrite into RewriteMapping pairs.
98///
99/// Format: `old_sha new_sha\n` per line. Extra fields (like newline) are ignored.
100pub fn parse_rewrite_mappings(input: &str) -> Vec<RewriteMapping> {
101    input
102        .lines()
103        .filter_map(|line| {
104            let parts: Vec<&str> = line.split_whitespace().collect();
105            if parts.len() >= 2 {
106                Some(RewriteMapping {
107                    old_sha: parts[0].to_string(),
108                    new_sha: parts[1].to_string(),
109                })
110            } else {
111                None
112            }
113        })
114        .collect()
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn test_parse_rewrite_mappings_single() {
123        let input = "abc123 def456\n";
124        let mappings = parse_rewrite_mappings(input);
125        assert_eq!(mappings.len(), 1);
126        assert_eq!(mappings[0].old_sha, "abc123");
127        assert_eq!(mappings[0].new_sha, "def456");
128    }
129
130    #[test]
131    fn test_parse_rewrite_mappings_multiple() {
132        let input = "abc123 def456\nghi789 jkl012\nmno345 pqr678\n";
133        let mappings = parse_rewrite_mappings(input);
134        assert_eq!(mappings.len(), 3);
135        assert_eq!(mappings[0].old_sha, "abc123");
136        assert_eq!(mappings[0].new_sha, "def456");
137        assert_eq!(mappings[1].old_sha, "ghi789");
138        assert_eq!(mappings[1].new_sha, "jkl012");
139        assert_eq!(mappings[2].old_sha, "mno345");
140        assert_eq!(mappings[2].new_sha, "pqr678");
141    }
142
143    #[test]
144    fn test_parse_rewrite_mappings_empty() {
145        let input = "";
146        let mappings = parse_rewrite_mappings(input);
147        assert!(mappings.is_empty());
148    }
149
150    #[test]
151    fn test_parse_rewrite_mappings_blank_lines() {
152        let input = "abc123 def456\n\nghi789 jkl012\n";
153        let mappings = parse_rewrite_mappings(input);
154        assert_eq!(mappings.len(), 2);
155    }
156
157    #[test]
158    fn test_parse_rewrite_mappings_extra_fields() {
159        // Git may include extra info after the two SHAs
160        let input = "abc123 def456 extra info\n";
161        let mappings = parse_rewrite_mappings(input);
162        assert_eq!(mappings.len(), 1);
163        assert_eq!(mappings[0].old_sha, "abc123");
164        assert_eq!(mappings[0].new_sha, "def456");
165    }
166
167    #[test]
168    fn test_parse_rewrite_mappings_malformed_line() {
169        let input = "only_one_sha\nabc123 def456\n";
170        let mappings = parse_rewrite_mappings(input);
171        assert_eq!(mappings.len(), 1);
172        assert_eq!(mappings[0].old_sha, "abc123");
173    }
174}