1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
//! File annotation parser (jj file annotate)
use super::super::JjError;
use super::{ANNOTATE_LINE_REGEX, Parser};
use crate::model::{AnnotationContent, AnnotationLine, ChangeId, CommitId};
impl Parser {
/// Parse `jj file annotate` default output into AnnotationContent
///
/// Default output format (jj 0.37.x compatible):
/// `<change_id> <author> <timestamp> <line_number>: <content>`
///
/// Example: `twzksoxt nakamura 2026-01-30 10:43:19 1: //! Tij`
///
/// Note: first_in_hunk is calculated by comparing consecutive change_ids.
pub fn parse_file_annotate(
output: &str,
file_path: &str,
) -> Result<AnnotationContent, JjError> {
let mut content = AnnotationContent::new(file_path.to_string());
let mut prev_change_id: Option<ChangeId> = None;
for line in output.lines() {
if line.is_empty() {
continue;
}
// Parse the default annotate output format
if let Some(annotation) = Self::parse_annotate_line(line, &prev_change_id) {
prev_change_id = Some(annotation.change_id.clone());
content.lines.push(annotation);
}
}
Ok(content)
}
/// Parse a single line of `jj file annotate` output using regex
///
/// Format: `<change_id>\t<commit_id> <author> <timestamp> <line_number>: <content>`
/// Example: `twzksoxt\tabcd1234 nakamura 2026-01-30 10:43:19 1: //! Tij`
pub(super) fn parse_annotate_line(
line: &str,
prev_change_id: &Option<ChangeId>,
) -> Option<AnnotationLine> {
let caps = ANNOTATE_LINE_REGEX.captures(line)?;
let change_id = ChangeId::new(caps.get(1)?.as_str().to_string());
let commit_id = CommitId::new(caps.get(2)?.as_str().to_string());
let author = caps.get(3)?.as_str().trim().to_string();
let timestamp = caps.get(4)?.as_str().to_string();
let line_number: usize = caps.get(5)?.as_str().parse().ok()?;
let content = caps
.get(6)
.map(|m| m.as_str().to_string())
.unwrap_or_default();
// Determine if this is the first line in hunk (different change_id from previous)
let first_in_hunk = match prev_change_id {
Some(prev) => prev != &change_id,
None => true,
};
Some(AnnotationLine {
change_id,
commit_id,
author,
timestamp,
line_number,
content,
first_in_hunk,
})
}
}