1use std::path::{Path, PathBuf};
6use anyhow::Result;
7
8use crate::vcs::{ObjectId, ObjectStore};
9
10#[derive(Debug, Clone)]
12pub struct DiffOptions {
13 pub context_lines: usize,
14 pub ignore_whitespace: bool,
15 pub ignore_case: bool,
16}
17
18impl Default for DiffOptions {
19 fn default() -> Self {
20 Self {
21 context_lines: 3,
22 ignore_whitespace: false,
23 ignore_case: false,
24 }
25 }
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum DiffChange {
31 Added {
33 start: usize,
34 lines: Vec<String>,
35 },
36 Removed {
38 start: usize,
39 lines: Vec<String>,
40 },
41 Changed {
43 old_start: usize,
44 old_lines: Vec<String>,
45 new_start: usize,
46 new_lines: Vec<String>,
47 },
48}
49
50#[derive(Debug, Clone)]
52pub struct DiffHunk {
53 pub old_start: usize,
54 pub old_count: usize,
55 pub new_start: usize,
56 pub new_count: usize,
57 pub changes: Vec<DiffChange>,
58}
59
60#[derive(Debug, Clone)]
62pub struct DiffResult {
63 pub old_path: PathBuf,
64 pub new_path: PathBuf,
65 pub hunks: Vec<DiffHunk>,
66 pub is_binary: bool,
67}
68
69pub struct Diff {
71 pub options: DiffOptions,
72}
73
74impl Diff {
75 pub fn new() -> Self {
77 Self {
78 options: DiffOptions::default(),
79 }
80 }
81
82 pub fn with_options(options: DiffOptions) -> Self {
84 Self { options }
85 }
86
87 pub fn diff_files(&self, old_path: &Path, new_path: &Path) -> Result<DiffResult> {
89 let old_content = std::fs::read_to_string(old_path)?;
91 let new_content = std::fs::read_to_string(new_path)?;
92
93 self.diff_content(
95 old_path.to_path_buf(),
96 &old_content,
97 new_path.to_path_buf(),
98 &new_content,
99 )
100 }
101
102 pub fn diff_objects(
104 &self,
105 object_store: &ObjectStore,
106 old_id: &ObjectId,
107 old_path: PathBuf,
108 new_id: &ObjectId,
109 new_path: PathBuf,
110 ) -> Result<DiffResult> {
111 let old_content = String::from_utf8(object_store.get_object(old_id)?)?;
113 let new_content = String::from_utf8(object_store.get_object(new_id)?)?;
114
115 self.diff_content(old_path, &old_content, new_path, &new_content)
117 }
118
119 pub fn diff_content(
121 &self,
122 old_path: PathBuf,
123 old_content: &str,
124 new_path: PathBuf,
125 new_content: &str,
126 ) -> Result<DiffResult> {
127 let old_lines: Vec<&str> = old_content.lines().collect();
129 let new_lines: Vec<&str> = new_content.lines().collect();
130
131 let is_binary = old_content.contains('\0') || new_content.contains('\0');
133 if is_binary {
134 return Ok(DiffResult {
135 old_path,
136 new_path,
137 hunks: vec![],
138 is_binary: true,
139 });
140 }
141
142 let hunks = self.myers_diff(&old_lines, &new_lines)?;
146
147 Ok(DiffResult {
148 old_path,
149 new_path,
150 hunks,
151 is_binary: false,
152 })
153 }
154
155 fn myers_diff(&self, old_lines: &[&str], new_lines: &[&str]) -> Result<Vec<DiffHunk>> {
157 let mut hunks = Vec::new();
162
163 if !old_lines.is_empty() || !new_lines.is_empty() {
164 let changes = vec![DiffChange::Changed {
165 old_start: 1,
166 old_lines: old_lines.iter().map(|s| s.to_string()).collect(),
167 new_start: 1,
168 new_lines: new_lines.iter().map(|s| s.to_string()).collect(),
169 }];
170
171 hunks.push(DiffHunk {
172 old_start: 1,
173 old_count: old_lines.len(),
174 new_start: 1,
175 new_count: new_lines.len(),
176 changes,
177 });
178 }
179
180 Ok(hunks)
181 }
182
183 }