code_repo_wiki/incremental/
change.rs1use std::collections::HashMap;
16use std::path::{Path, PathBuf};
17
18use anyhow::{Context, Result};
19
20use crate::ingest::parser::{Entity, FileInsight, ParserRegistry};
21
22use super::diff::GitDiffResult;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum EntityChangeKind {
27 Added,
29 Removed,
31 SignatureChanged,
33 BodyChanged,
35}
36
37#[derive(Debug, Clone)]
39pub struct EntityChange {
40 pub file: PathBuf,
41 pub entity_name: String,
42 pub kind: EntityChangeKind,
43 pub old_range: Option<(usize, usize)>,
45 pub new_range: Option<(usize, usize)>,
47}
48
49#[derive(Debug, Clone, Default)]
51pub struct EntityChangeSet {
52 pub changes: Vec<EntityChange>,
53}
54
55impl EntityChangeSet {
56 pub fn has_interface_change(&self) -> bool {
58 self.changes.iter().any(|c| match c.kind {
59 EntityChangeKind::Added
60 | EntityChangeKind::Removed
61 | EntityChangeKind::SignatureChanged => true,
62 EntityChangeKind::BodyChanged => false,
63 })
64 }
65}
66
67pub fn no_entity_change_files(
81 changed_files: &[PathBuf],
82 entity_changes: &EntityChangeSet,
83 root: &crate::project::ProjectRoot,
84) -> std::collections::HashSet<PathBuf> {
85 if entity_changes.changes.is_empty() {
91 return std::collections::HashSet::new();
92 }
93 changed_files
94 .iter()
95 .filter(|f| {
96 root.path().join(f).exists()
97 && !entity_changes
98 .changes
99 .iter()
100 .any(|c| c.file.as_path() == f.as_path())
101 })
102 .cloned()
103 .collect()
104}
105
106pub fn classify_entity_changes_at(
111 root: &crate::project::ProjectRoot,
112 diff: &GitDiffResult,
113 current_insights: &[FileInsight],
114) -> Result<EntityChangeSet> {
115 if diff.from_commit.is_empty() {
116 return Ok(EntityChangeSet::default());
118 }
119 let repo = git2::Repository::open(root.path())
120 .with_context(|| "实体级变化分类需要 Git 仓库")?;
121 let from_commit = repo.find_commit(git2::Oid::from_str(&diff.from_commit)?)?;
123 let from_tree = from_commit.tree()?;
124 let registry = ParserRegistry::new();
125
126 let current: HashMap<String, Vec<Entity>> = current_insights
132 .iter()
133 .map(|i| {
134 let rel = std::path::Path::new(&i.path)
135 .strip_prefix(root.path())
136 .unwrap_or(std::path::Path::new(&i.path));
137 (super::norm_sep(&rel.to_string_lossy()), i.entities.clone())
138 })
139 .collect();
140
141 let mut set = EntityChangeSet::default();
142
143 for path in &diff.modified {
145 let old_entities = read_old_entities(&repo, &from_tree, path, ®istry)?;
146 let new_entities = current
147 .get(&super::norm_sep(&path.to_string_lossy()))
148 .cloned()
149 .unwrap_or_default();
150 compare_entities(&mut set, path, &old_entities, &new_entities);
151 }
152 for path in &diff.added {
154 if let Some(ents) = current.get(&super::norm_sep(&path.to_string_lossy())) {
155 for e in ents {
156 set.changes.push(EntityChange {
157 file: path.clone(),
158 entity_name: e.name.clone(),
159 kind: EntityChangeKind::Added,
160 old_range: None,
161 new_range: Some((e.line_start, e.line_end)),
162 });
163 }
164 }
165 }
166 for path in &diff.deleted {
168 for e in read_old_entities(&repo, &from_tree, path, ®istry)? {
169 set.changes.push(EntityChange {
170 file: path.clone(),
171 entity_name: e.name.clone(),
172 kind: EntityChangeKind::Removed,
173 old_range: Some((e.line_start, e.line_end)),
174 new_range: None,
175 });
176 }
177 }
178 Ok(set)
179}
180
181fn compare_entities(
186 set: &mut EntityChangeSet,
187 path: &Path,
188 old: &[Entity],
189 new: &[Entity],
190) {
191 let old_by_name: HashMap<&str, Vec<&Entity>> = group_by_name(old);
192 let new_by_name: HashMap<&str, Vec<&Entity>> = group_by_name(new);
193
194 for (name, entries) in &new_by_name {
196 if !old_by_name.contains_key(*name) {
197 for e in entries {
198 set.changes.push(EntityChange {
199 file: path.to_path_buf(),
200 entity_name: e.name.clone(),
201 kind: EntityChangeKind::Added,
202 old_range: None,
203 new_range: Some((e.line_start, e.line_end)),
204 });
205 }
206 }
207 }
208 for (name, entries) in &old_by_name {
210 if !new_by_name.contains_key(*name) {
211 for e in entries {
212 set.changes.push(EntityChange {
213 file: path.to_path_buf(),
214 entity_name: e.name.clone(),
215 kind: EntityChangeKind::Removed,
216 old_range: Some((e.line_start, e.line_end)),
217 new_range: None,
218 });
219 }
220 }
221 }
222 for (name, old_entries) in &old_by_name {
224 if let Some(new_entries) = new_by_name.get(*name) {
225 let old_sigs: Vec<String> = old_entries
226 .iter()
227 .map(|e| normalize_sig(e.signature.as_deref()))
228 .collect();
229 let new_sigs: Vec<String> = new_entries
230 .iter()
231 .map(|e| normalize_sig(e.signature.as_deref()))
232 .collect();
233 let kind = if old_sigs == new_sigs {
234 EntityChangeKind::BodyChanged
235 } else {
236 EntityChangeKind::SignatureChanged
237 };
238 for (old_e, new_e) in old_entries.iter().zip(new_entries.iter()) {
240 let unchanged = kind == EntityChangeKind::BodyChanged
246 && old_e.line_start == new_e.line_start
247 && old_e.line_end == new_e.line_end
248 && normalize_sig(old_e.signature.as_deref())
249 == normalize_sig(new_e.signature.as_deref());
250 if unchanged {
251 continue;
252 }
253 set.changes.push(EntityChange {
254 file: path.to_path_buf(),
255 entity_name: (*name).to_string(),
256 kind,
257 old_range: Some((old_e.line_start, old_e.line_end)),
258 new_range: Some((new_e.line_start, new_e.line_end)),
259 });
260 }
261 }
262 }
263}
264
265fn normalize_sig(sig: Option<&str>) -> String {
267 sig.unwrap_or("")
268 .chars()
269 .filter(|c| !c.is_whitespace())
270 .collect()
271}
272
273fn read_old_entities(
278 repo: &git2::Repository,
279 from_tree: &git2::Tree,
280 path: &Path,
281 registry: &ParserRegistry,
282) -> Result<Vec<Entity>> {
283 let entry = match from_tree.get_path(path) {
284 Ok(e) => e,
285 Err(e) if e.code() == git2::ErrorCode::NotFound => return Ok(Vec::new()),
286 Err(e) => return Err(e.into()),
287 };
288 let obj = entry
289 .to_object(repo)
290 .with_context(|| format!("读取 {} 旧版本失败", path.display()))?;
291 let blob = obj
292 .into_blob()
293 .map_err(|_| anyhow::anyhow!("{} 在 from_commit 中不是 blob", path.display()))?;
294 let content = match std::str::from_utf8(blob.content()) {
295 Ok(c) => c,
296 Err(_) => return Ok(Vec::new()),
300 };
301 Ok(match registry.get_for_file(path) {
302 Some(parser) => parser.parse(content, path)?.entities,
303 None => Vec::new(),
304 })
305}
306
307fn group_by_name(entities: &[Entity]) -> HashMap<&str, Vec<&Entity>> {
309 let mut map: HashMap<&str, Vec<&Entity>> = HashMap::new();
310 for e in entities {
311 map.entry(e.name.as_str()).or_default().push(e);
312 }
313 map
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 fn make_entity(name: &str, sig: &str, start: usize, end: usize) -> Entity {
321 Entity {
322 name: name.into(),
323 kind: "fn".into(),
324 line_start: start,
325 line_end: end,
326 doc_comment: None,
327 signature: Some(sig.into()), visibility: None,
328 }
329 }
330
331 #[test]
332 fn test_compare_added_and_removed() {
333 let mut set = EntityChangeSet::default();
334 let old = vec![make_entity("gone", "fn gone()", 1, 2)];
335 let new = vec![make_entity("fresh", "fn fresh()", 5, 6)];
336 compare_entities(&mut set, Path::new("src/a.rs"), &old, &new);
337 assert_eq!(set.changes.len(), 2);
338 assert_eq!(set.changes[0].kind, EntityChangeKind::Added);
339 assert_eq!(set.changes[1].kind, EntityChangeKind::Removed);
340 assert!(set.has_interface_change());
341 }
342
343 #[test]
344 fn test_compare_signature_changed() {
345 let mut set = EntityChangeSet::default();
346 let old = vec![make_entity("f", "fn f(a: i32)", 1, 3)];
347 let new = vec![make_entity("f", "fn f(a: i32, b: i32)", 1, 4)];
348 compare_entities(&mut set, Path::new("src/a.rs"), &old, &new);
349 assert_eq!(set.changes.len(), 1);
350 assert_eq!(set.changes[0].kind, EntityChangeKind::SignatureChanged);
351 assert!(set.has_interface_change());
352 }
353
354 #[test]
355 fn test_compare_body_changed_only() {
356 let mut set = EntityChangeSet::default();
357 let old = vec![make_entity("f", "fn f()", 1, 3)];
359 let new = vec![make_entity("f", "fn f()", 1, 5)];
360 compare_entities(&mut set, Path::new("src/a.rs"), &old, &new);
361 assert_eq!(set.changes.len(), 1);
362 assert_eq!(set.changes[0].kind, EntityChangeKind::BodyChanged);
363 assert!(!set.has_interface_change());
364 }
365
366 #[test]
367 fn test_normalize_sig_ignores_whitespace() {
368 let a = "fn f( a : i32 )";
369 let b = "fn f(a: i32)";
370 assert_eq!(normalize_sig(Some(a)), normalize_sig(Some(b)));
371 }
372}