Skip to main content

chronicle/
export.rs

1use std::io::Write;
2
3use crate::error::chronicle_error::GitSnafu;
4use crate::error::Result;
5use crate::git::GitOps;
6use crate::schema::annotation::Annotation;
7use serde::{Deserialize, Serialize};
8use snafu::ResultExt;
9
10/// A single export entry: commit SHA + timestamp + full annotation.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ExportEntry {
13    pub commit_sha: String,
14    pub timestamp: String,
15    pub annotation: Annotation,
16}
17
18/// Export annotations as JSONL to a writer.
19///
20/// Iterates all notes under `refs/notes/chronicle`, deserializes each as an
21/// Annotation, and writes one JSON object per line.
22pub fn export_annotations<W: Write>(git_ops: &dyn GitOps, writer: &mut W) -> Result<usize> {
23    let note_list = list_annotated_commits(git_ops)?;
24    let mut count = 0;
25
26    for sha in &note_list {
27        let note_content = match git_ops.note_read(sha).context(GitSnafu)? {
28            Some(content) => content,
29            None => continue,
30        };
31
32        let annotation: Annotation = match serde_json::from_str(&note_content) {
33            Ok(a) => a,
34            Err(_) => continue, // skip malformed notes
35        };
36
37        let entry = ExportEntry {
38            commit_sha: sha.clone(),
39            timestamp: annotation.timestamp.clone(),
40            annotation,
41        };
42
43        let line =
44            serde_json::to_string(&entry).map_err(|e| crate::error::ChronicleError::Json {
45                source: e,
46                location: snafu::Location::default(),
47            })?;
48
49        writeln!(writer, "{line}").map_err(|e| crate::error::ChronicleError::Io {
50            source: e,
51            location: snafu::Location::default(),
52        })?;
53
54        count += 1;
55    }
56
57    Ok(count)
58}
59
60/// List all commit SHAs that have chronicle notes.
61fn list_annotated_commits(_git_ops: &dyn GitOps) -> Result<Vec<String>> {
62    // git notes --ref=refs/notes/chronicle list outputs: <note-sha> <commit-sha>
63    // We use the CliOps internals indirectly — iterate by using a known set.
64    // Since GitOps doesn't expose `notes list`, we shell out directly.
65    let output = std::process::Command::new("git")
66        .args(["notes", "--ref", "refs/notes/chronicle", "list"])
67        .output()
68        .map_err(|e| crate::error::ChronicleError::Io {
69            source: e,
70            location: snafu::Location::default(),
71        })?;
72
73    if !output.status.success() {
74        // No notes ref yet — return empty
75        return Ok(Vec::new());
76    }
77
78    let stdout = String::from_utf8_lossy(&output.stdout);
79    let shas: Vec<String> = stdout
80        .lines()
81        .filter_map(|line| {
82            // Format: <note-object-sha> <commit-sha>
83            let parts: Vec<&str> = line.split_whitespace().collect();
84            if parts.len() >= 2 {
85                Some(parts[1].to_string())
86            } else {
87                None
88            }
89        })
90        .collect();
91
92    Ok(shas)
93}