Skip to main content

oxihuman_core/
change_log.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Structured change log entry.
6
7/// Kind of change recorded.
8#[derive(Debug, Clone, PartialEq)]
9pub enum ChangeKind {
10    Added,
11    Modified,
12    Deleted,
13    Renamed,
14}
15
16/// A single structured change log entry.
17#[derive(Debug, Clone)]
18pub struct ChangeEntry {
19    pub id: u64,
20    pub kind: ChangeKind,
21    pub path: String,
22    pub author: String,
23    pub description: String,
24    pub timestamp_ms: u64,
25}
26
27/// Append-only change log.
28#[derive(Debug, Default)]
29pub struct ChangeLog {
30    entries: Vec<ChangeEntry>,
31    next_id: u64,
32}
33
34impl ChangeLog {
35    pub fn new() -> Self {
36        Self::default()
37    }
38
39    pub fn append(
40        &mut self,
41        kind: ChangeKind,
42        path: &str,
43        author: &str,
44        description: &str,
45        timestamp_ms: u64,
46    ) -> u64 {
47        let id = self.next_id;
48        self.next_id += 1;
49        self.entries.push(ChangeEntry {
50            id,
51            kind,
52            path: path.to_string(),
53            author: author.to_string(),
54            description: description.to_string(),
55            timestamp_ms,
56        });
57        id
58    }
59
60    pub fn entry_count(&self) -> usize {
61        self.entries.len()
62    }
63
64    pub fn entries(&self) -> &[ChangeEntry] {
65        &self.entries
66    }
67
68    pub fn filter_kind(&self, kind: &ChangeKind) -> Vec<&ChangeEntry> {
69        self.entries.iter().filter(|e| &e.kind == kind).collect()
70    }
71
72    pub fn filter_author(&self, author: &str) -> Vec<&ChangeEntry> {
73        self.entries.iter().filter(|e| e.author == author).collect()
74    }
75
76    pub fn since(&self, timestamp_ms: u64) -> Vec<&ChangeEntry> {
77        self.entries
78            .iter()
79            .filter(|e| e.timestamp_ms >= timestamp_ms)
80            .collect()
81    }
82}
83
84pub fn new_change_log() -> ChangeLog {
85    ChangeLog::new()
86}
87
88pub fn cl_append(
89    log: &mut ChangeLog,
90    kind: ChangeKind,
91    path: &str,
92    author: &str,
93    desc: &str,
94    ts: u64,
95) -> u64 {
96    log.append(kind, path, author, desc, ts)
97}
98
99pub fn cl_count(log: &ChangeLog) -> usize {
100    log.entry_count()
101}
102
103pub fn cl_filter_kind<'a>(log: &'a ChangeLog, kind: &ChangeKind) -> Vec<&'a ChangeEntry> {
104    log.filter_kind(kind)
105}
106
107pub fn cl_since(log: &ChangeLog, ts: u64) -> Vec<&ChangeEntry> {
108    log.since(ts)
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_append_and_count() {
117        let mut log = new_change_log();
118        cl_append(
119            &mut log,
120            ChangeKind::Added,
121            "src/main.rs",
122            "alice",
123            "init",
124            1000,
125        );
126        assert_eq!(cl_count(&log), 1);
127    }
128
129    #[test]
130    fn test_id_increments() {
131        let mut log = new_change_log();
132        let id0 = cl_append(&mut log, ChangeKind::Added, "a", "u", "d", 0);
133        let id1 = cl_append(&mut log, ChangeKind::Modified, "b", "u", "d", 1);
134        assert_eq!(id0, 0);
135        assert_eq!(id1, 1);
136    }
137
138    #[test]
139    fn test_filter_kind_added() {
140        let mut log = new_change_log();
141        cl_append(&mut log, ChangeKind::Added, "f1", "u", "d", 0);
142        cl_append(&mut log, ChangeKind::Deleted, "f2", "u", "d", 1);
143        assert_eq!(cl_filter_kind(&log, &ChangeKind::Added).len(), 1);
144    }
145
146    #[test]
147    fn test_filter_author() {
148        let mut log = new_change_log();
149        cl_append(&mut log, ChangeKind::Added, "f", "alice", "d", 0);
150        cl_append(&mut log, ChangeKind::Added, "g", "bob", "d", 1);
151        assert_eq!(log.filter_author("alice").len(), 1);
152    }
153
154    #[test]
155    fn test_since_filter() {
156        let mut log = new_change_log();
157        cl_append(&mut log, ChangeKind::Added, "f", "u", "d", 100);
158        cl_append(&mut log, ChangeKind::Added, "g", "u", "d", 200);
159        cl_append(&mut log, ChangeKind::Added, "h", "u", "d", 300);
160        assert_eq!(cl_since(&log, 200).len(), 2);
161    }
162
163    #[test]
164    fn test_renamed_kind() {
165        let mut log = new_change_log();
166        cl_append(&mut log, ChangeKind::Renamed, "old", "u", "renamed", 0);
167        assert_eq!(cl_filter_kind(&log, &ChangeKind::Renamed).len(), 1);
168    }
169
170    #[test]
171    fn test_empty_log() {
172        let log = new_change_log();
173        assert_eq!(cl_count(&log), 0);
174    }
175
176    #[test]
177    fn test_entries_stored() {
178        let mut log = new_change_log();
179        cl_append(
180            &mut log,
181            ChangeKind::Modified,
182            "cfg.toml",
183            "dev",
184            "tweaks",
185            500,
186        );
187        assert_eq!(log.entries()[0].path, "cfg.toml");
188    }
189
190    #[test]
191    fn test_multiple_same_kind() {
192        let mut log = new_change_log();
193        for i in 0..5 {
194            cl_append(&mut log, ChangeKind::Modified, "f", "u", "d", i);
195        }
196        assert_eq!(cl_filter_kind(&log, &ChangeKind::Modified).len(), 5);
197    }
198}