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