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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
//! Annotation (blame) data model
use super::id::{ChangeId, CommitId};
/// Blame information for a single line
#[derive(Debug, Clone)]
pub struct AnnotationLine {
/// Change ID (short form, 8 chars)
pub change_id: ChangeId,
/// Commit ID (short form, 8 chars) — used for jj command execution
pub commit_id: CommitId,
/// Author name
pub author: String,
/// Timestamp (YYYY-MM-DD HH:MM format)
pub timestamp: String,
/// 1-based line number
pub line_number: usize,
/// Line content (may contain tabs)
pub content: String,
/// true = first line of hunk (show full info), false = continuation (show "↑")
pub first_in_hunk: bool,
}
/// Blame information for an entire file
#[derive(Debug, Clone, Default)]
pub struct AnnotationContent {
/// File path being annotated
pub file_path: String,
/// Annotation lines
pub lines: Vec<AnnotationLine>,
}
impl AnnotationContent {
/// Create new empty annotation content
pub fn new(file_path: String) -> Self {
Self {
file_path,
lines: Vec::new(),
}
}
/// Check if content is empty
pub fn is_empty(&self) -> bool {
self.lines.is_empty()
}
/// Get line count
pub fn len(&self) -> usize {
self.lines.len()
}
}
impl AnnotationLine {
/// Get short timestamp for display (MM-DD or YY-MM-DD)
pub fn short_timestamp(&self) -> String {
// Input format: "YYYY-MM-DD HH:MM"
if self.timestamp.len() >= 10 {
let date_part = &self.timestamp[..10]; // "YYYY-MM-DD"
// For simplicity, always show MM-DD (could compare with current year)
if date_part.len() >= 10 {
return date_part[5..10].to_string(); // "MM-DD"
}
}
self.timestamp.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_annotation_content_new() {
let content = AnnotationContent::new("src/main.rs".to_string());
assert_eq!(content.file_path, "src/main.rs");
assert!(content.is_empty());
}
#[test]
fn test_annotation_line_short_timestamp() {
let line = AnnotationLine {
change_id: ChangeId::new("twzksoxt".to_string()),
commit_id: CommitId::new("abcd1234".to_string()),
author: "nakamura".to_string(),
timestamp: "2026-01-30 10:43".to_string(),
line_number: 1,
content: "test".to_string(),
first_in_hunk: true,
};
assert_eq!(line.short_timestamp(), "01-30");
}
}