agentic_codebase/temporal/
history.rs1use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub enum ChangeType {
15 Add,
17 Modify,
19 Delete,
21 Rename,
23}
24
25impl std::fmt::Display for ChangeType {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 match self {
28 Self::Add => write!(f, "add"),
29 Self::Modify => write!(f, "modify"),
30 Self::Delete => write!(f, "delete"),
31 Self::Rename => write!(f, "rename"),
32 }
33 }
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct FileChange {
39 pub path: PathBuf,
41 pub change_type: ChangeType,
43 pub commit_id: String,
45 pub timestamp: u64,
47 pub author: String,
49 pub is_bugfix: bool,
51 pub lines_added: u32,
53 pub lines_deleted: u32,
55 pub old_path: Option<PathBuf>,
57}
58
59#[derive(Debug, Clone)]
61pub struct HistoryOptions {
62 pub max_commits: usize,
64 pub since_timestamp: u64,
66 pub until_timestamp: u64,
68 pub path_filter: Vec<PathBuf>,
70}
71
72impl Default for HistoryOptions {
73 fn default() -> Self {
74 Self {
75 max_commits: 10000,
76 since_timestamp: 0,
77 until_timestamp: 0,
78 path_filter: Vec::new(),
79 }
80 }
81}
82
83#[derive(Debug, Clone, Default)]
89pub struct ChangeHistory {
90 by_path: HashMap<PathBuf, Vec<FileChange>>,
92 chronological: Vec<FileChange>,
94 commits: HashMap<String, Vec<FileChange>>,
96}
97
98impl ChangeHistory {
99 pub fn new() -> Self {
101 Self::default()
102 }
103
104 pub fn add_change(&mut self, change: FileChange) {
109 self.by_path
110 .entry(change.path.clone())
111 .or_default()
112 .push(change.clone());
113 self.commits
114 .entry(change.commit_id.clone())
115 .or_default()
116 .push(change.clone());
117 self.chronological.push(change);
118 }
119
120 pub fn changes_for_path(&self, path: &Path) -> &[FileChange] {
122 self.by_path.get(path).map(|v| v.as_slice()).unwrap_or(&[])
123 }
124
125 pub fn files_in_commit(&self, commit_id: &str) -> &[FileChange] {
127 self.commits
128 .get(commit_id)
129 .map(|v| v.as_slice())
130 .unwrap_or(&[])
131 }
132
133 pub fn change_count(&self, path: &Path) -> usize {
135 self.by_path.get(path).map(|v| v.len()).unwrap_or(0)
136 }
137
138 pub fn bugfix_count(&self, path: &Path) -> usize {
140 self.by_path
141 .get(path)
142 .map(|v| v.iter().filter(|c| c.is_bugfix).count())
143 .unwrap_or(0)
144 }
145
146 pub fn all_commits(&self) -> Vec<&str> {
148 self.commits.keys().map(|s| s.as_str()).collect()
149 }
150
151 pub fn chronological(&self) -> &[FileChange] {
153 &self.chronological
154 }
155
156 pub fn all_paths(&self) -> Vec<&Path> {
158 self.by_path.keys().map(|p| p.as_path()).collect()
159 }
160
161 pub fn authors_for_path(&self, path: &Path) -> Vec<String> {
163 let mut authors: Vec<String> = self
164 .by_path
165 .get(path)
166 .map(|v| v.iter().map(|c| c.author.clone()).collect())
167 .unwrap_or_default();
168 authors.sort();
169 authors.dedup();
170 authors
171 }
172
173 pub fn total_churn(&self, path: &Path) -> u64 {
175 self.by_path
176 .get(path)
177 .map(|v| {
178 v.iter()
179 .map(|c| c.lines_added as u64 + c.lines_deleted as u64)
180 .sum()
181 })
182 .unwrap_or(0)
183 }
184
185 pub fn total_changes(&self) -> usize {
187 self.chronological.len()
188 }
189
190 pub fn total_commits(&self) -> usize {
192 self.commits.len()
193 }
194
195 pub fn latest_timestamp(&self, path: &Path) -> u64 {
197 self.by_path
198 .get(path)
199 .and_then(|v| v.iter().map(|c| c.timestamp).max())
200 .unwrap_or(0)
201 }
202
203 pub fn oldest_timestamp(&self, path: &Path) -> u64 {
205 self.by_path
206 .get(path)
207 .and_then(|v| v.iter().map(|c| c.timestamp).min())
208 .unwrap_or(0)
209 }
210}