diff_man/
diff.rs

1use std::{
2    fs, io,
3    path::{Path, PathBuf},
4};
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9pub const DIFF_SIGN_LINE_ADDED: &str = "+";
10pub const DIFF_SIGN_LINE_DELETED: &str = "-";
11pub const DIFF_SIGN_LINE_DEFAULT: &str = " ";
12pub const DIFF_SIGN_HEADER_ORIGIN: &str = "---";
13pub const DIFF_SIGN_HEADER_NEW: &str = "+++";
14pub const DIFF_SIGN_HUNK: &str = "@@";
15
16#[cfg(feature = "serde")]
17#[derive(Serialize, Deserialize, Debug)]
18pub enum DiffFormat {
19    GitUdiff,
20}
21#[cfg(not(feature = "serde"))]
22#[derive(Debug)]
23pub enum DiffFormat {
24    GitUdiff,
25}
26
27#[cfg(feature = "serde")]
28#[derive(Serialize, Deserialize, Debug)]
29pub struct DiffComposition {
30    pub format: DiffFormat,
31    pub diff: Vec<Diff>,
32}
33#[cfg(not(feature = "serde"))]
34#[derive(Debug)]
35pub struct DiffComposition {
36    pub format: DiffFormat,
37    pub diff: Vec<Diff>,
38}
39#[cfg(feature = "serde")]
40#[derive(Serialize, Deserialize, Debug)]
41pub struct Diff {
42    pub command: Option<String>,
43    pub index: Option<String>, // TODO: type this
44    pub path: PathBuf,
45    pub hunk: Vec<DiffHunk>,
46}
47#[cfg(not(feature = "serde"))]
48#[derive(Debug)]
49pub struct Diff {
50    pub command: Option<String>,
51    pub index: Option<String>, // TODO: type this
52    pub path: PathBuf,
53    pub hunk: Vec<DiffHunk>,
54}
55#[cfg(feature = "serde")]
56#[derive(Serialize, Deserialize, Debug)]
57pub struct DiffHunk {
58    pub old_line: usize,
59    pub old_len: usize,
60    pub new_line: usize,
61    pub new_len: usize,
62    pub change: Vec<LineChange>,
63}
64#[cfg(not(feature = "serde"))]
65#[derive(Debug)]
66pub struct DiffHunk {
67    pub old_line: usize,
68    pub old_len: usize,
69    pub new_line: usize,
70    pub new_len: usize,
71    pub change: Vec<LineChange>,
72}
73#[cfg(feature = "serde")]
74#[derive(Serialize, Deserialize, Debug)]
75pub struct LineChange {
76    pub kind: Change,
77    pub content: String,
78}
79
80#[cfg(not(feature = "serde"))]
81#[derive(Debug)]
82pub struct LineChange {
83    pub kind: Change,
84    pub content: String,
85}
86#[cfg(feature = "serde")]
87#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
88pub enum Change {
89    Default,
90    Added,
91    Deleted,
92}
93#[cfg(not(feature = "serde"))]
94#[derive(Debug, Copy, Clone)]
95pub enum Change {
96    Default,
97    Added,
98    Deleted,
99}
100
101#[cfg(feature = "serde")]
102#[derive(Serialize, Deserialize, Debug)]
103pub enum Line {
104    Command,
105    Index,
106    OrignPath,
107    NewPath,
108    Hunk,
109    LineChange(Change),
110    Unknown,
111}
112#[cfg(not(feature = "serde"))]
113#[derive(Debug)]
114pub enum Line {
115    Command,
116    Index,
117    OrignPath,
118    NewPath,
119    Hunk,
120    LineChange(Change),
121    Unknown,
122}
123
124#[derive(Debug)]
125pub struct DiffError {
126    kind: DiffErrorKind,
127    reason: String,
128}
129#[derive(Debug)]
130pub enum DiffErrorKind {
131    IOError(io::Error),
132    InvalidIndex(usize),
133    UnmatchedContent(String, String),
134}
135
136impl From<io::Error> for DiffError {
137    fn from(e: io::Error) -> Self {
138        DiffError {
139            kind: DiffErrorKind::IOError(e),
140            reason: "cannot read or write".to_string(),
141        }
142    }
143}
144
145impl DiffComposition {
146    pub fn apply(&self, root: &Path) -> Result<(), DiffError> {
147        for diff in &self.diff {
148            let target_path = root.join(&diff.path);
149            let original = fs::read_to_string(&target_path)?;
150            let after = diff.apply(&original)?;
151            fs::write(target_path, after)?;
152        }
153        Ok(())
154    }
155    pub fn revert(&self, root: &Path) -> Result<(), DiffError> {
156        for diff in &self.diff {
157            let target_path = root.join(&diff.path);
158            let applied = fs::read_to_string(&target_path)?;
159            let after = diff.revert(&applied)?;
160            fs::write(target_path, after)?;
161        }
162        Ok(())
163    }
164}
165
166impl Diff {
167    pub fn apply(&self, original: &str) -> Result<String, DiffError> {
168        let mut buffer = String::new();
169
170        // index of original line
171        let mut oidx: usize = 0;
172        let lines: Vec<&str> = original.lines().collect();
173
174        for hunk in &self.hunk {
175            while oidx < (hunk.old_line - 1) {
176                buffer.push_str(lines.get(oidx).ok_or_else(|| DiffError {
177                    kind: DiffErrorKind::InvalidIndex(oidx),
178                    reason: format!("cannot get line at {oidx}"),
179                })?);
180                buffer.push('\n');
181                oidx += 1;
182            }
183            for change in &hunk.change {
184                match change.kind {
185                    Change::Default => {
186                        buffer.push_str(lines.get(oidx).ok_or_else(|| {
187                            DiffError {
188                                kind: DiffErrorKind::InvalidIndex(oidx),
189                                reason: format!("cannot get line at {oidx}"),
190                            }
191                        })?);
192                        buffer.push('\n');
193                        oidx += 1;
194                    }
195                    Change::Deleted => {
196                        oidx += 1;
197                        continue;
198                    }
199                    Change::Added => {
200                        buffer.push_str(&change.content);
201                        buffer.push('\n');
202                    }
203                }
204            }
205        }
206
207        while oidx < lines.len() {
208            buffer
209                .push_str(lines.get(oidx).expect("there is no line in lines"));
210            buffer.push('\n');
211            oidx += 1;
212        }
213
214        Ok(buffer)
215    }
216
217    pub fn revert(&self, applied: &str) -> Result<String, DiffError> {
218        let mut buffer = String::new();
219
220        let mut aidx: usize = 0;
221        let lines: Vec<&str> = applied.lines().collect();
222
223        for hunk in &self.hunk {
224            while aidx < (hunk.new_line - 1) {
225                buffer.push_str(lines.get(aidx).ok_or_else(|| DiffError {
226                    kind: DiffErrorKind::InvalidIndex(aidx),
227                    reason: format!("cannot get line at {aidx}"),
228                })?);
229                buffer.push('\n');
230                aidx += 1;
231            }
232            for change in &hunk.change {
233                match change.kind {
234                    Change::Default => {
235                        let content =
236                            lines.get(aidx).ok_or_else(|| DiffError {
237                                kind: DiffErrorKind::InvalidIndex(aidx),
238                                reason: format!("cannot get line at {aidx}"),
239                            })?;
240                        if change.content != *content {
241                            Err(DiffError {
242                                kind: DiffErrorKind::UnmatchedContent(
243                                    change.content.to_string(),
244                                    content.to_string(),
245                                ),
246                                reason: format!(
247                                    "(change) : {:?}, line(content) : {}, @line_idx : {aidx}, Buffer:{}, hunk current {:#?}",
248                                    change, content, buffer, hunk
249                                ),
250                            })?;
251                        }
252                        buffer.push_str(content);
253                        buffer.push('\n');
254                        aidx += 1;
255                    }
256                    Change::Deleted => {
257                        buffer.push_str(&change.content);
258                        buffer.push('\n');
259                    }
260                    Change::Added => {
261                        aidx += 1;
262                        continue;
263                    }
264                }
265            }
266        }
267
268        while aidx < lines.len() {
269            buffer
270                .push_str(lines.get(aidx).expect("there is no line in lines"));
271            buffer.push('\n');
272            aidx += 1;
273        }
274
275        Ok(buffer)
276    }
277}
278
279#[cfg(test)]
280mod test {
281    use std::str::FromStr;
282
283    use {core::panic, std::fs};
284
285    use crate::{diff::*, parser::Parser};
286
287    #[test]
288    fn test_diff_apply_simple() {
289        let original = fs::read_to_string("test_data/simple.before").unwrap();
290        println!("<<original start>>");
291        print!("{}", original);
292        println!("<<original end>>");
293
294        let diff_file = fs::read_to_string("test_data/simple.diffs").unwrap();
295
296        let com = Parser::parse_git_udiff(&diff_file).unwrap();
297        println!("{:#?}", com);
298        let diff = com.diff.first().unwrap();
299        let applied = diff.apply(&original).unwrap();
300
301        println!("<<applied start>>");
302        print!("{}", applied);
303        println!("<<applied end>>");
304
305        let expected = fs::read_to_string("test_data/simple.after").unwrap();
306        println!("<<expected start>>");
307        print!("{}", expected);
308        println!("<<expected end>>");
309
310        assert_eq!(applied.as_str(), expected.as_str())
311    }
312
313    #[test]
314    fn test_diff_apply_middle() {
315        let original = fs::read_to_string("test_data/middle.before").unwrap();
316        println!("<<original start>>");
317        print!("{}", original);
318        println!("<<original end>>");
319
320        let diff_file = fs::read_to_string("test_data/middle.diffs").unwrap();
321
322        let com = Parser::parse_git_udiff(&diff_file).unwrap();
323        println!("{:#?}", com);
324        let diff = com.diff.first().unwrap();
325        let applied = diff.apply(&original).unwrap();
326
327        println!("<<applied start>>");
328        print!("{}", applied);
329        println!("<<applied end>>");
330
331        let expected = fs::read_to_string("test_data/middle.after").unwrap();
332        println!("<<expected start>>");
333        print!("{}", expected);
334        println!("<<expected end>>");
335
336        assert_eq!(applied.as_str(), expected.as_str())
337    }
338
339    #[test]
340    fn test_diff_apply_revert_simple() {
341        let original = fs::read_to_string("test_data/simple.before").unwrap();
342        println!("<<original start>>");
343        print!("{}", original);
344        println!("<<original end>>");
345
346        let diff_file = fs::read_to_string("test_data/simple.diffs").unwrap();
347
348        let com = Parser::parse_git_udiff(&diff_file).unwrap();
349        println!("{:#?}", com);
350        let diff = com.diff.first().unwrap();
351        let applied = diff.apply(&original).unwrap();
352
353        println!("<<applied start>>");
354        print!("{}", applied);
355        println!("<<applied end>>");
356
357        let expected = fs::read_to_string("test_data/simple.after").unwrap();
358        println!("<<expected start>>");
359        print!("{}", expected);
360        println!("<<expected end>>");
361
362        assert_eq!(applied.as_str(), expected.as_str());
363        let before = diff.revert(&applied).unwrap();
364        assert_eq!(before.as_str(), original.as_str())
365    }
366
367    #[test]
368    fn test_diff_apply_revert_middle() {
369        let original = fs::read_to_string("test_data/middle.before").unwrap();
370        println!("<<original start>>");
371        print!("{}", original);
372        println!("<<original end>>");
373
374        let diff_file = fs::read_to_string("test_data/middle.diffs").unwrap();
375
376        let com = Parser::parse_git_udiff(&diff_file).unwrap();
377        println!("{:#?}", com);
378        let diff = com.diff.first().unwrap();
379        let applied = diff.apply(&original).unwrap();
380
381        println!("<<applied start>>");
382        print!("{}", applied);
383        println!("<<applied end>>");
384
385        let expected = fs::read_to_string("test_data/middle.after").unwrap();
386        println!("<<expected start>>");
387        print!("{}", expected);
388        println!("<<expected end>>");
389
390        assert_eq!(applied.as_str(), expected.as_str());
391        let before = diff.revert(&applied).unwrap();
392        assert_eq!(before.as_str(), original.as_str())
393    }
394
395    #[test]
396    fn test_comp_simple_apply() {
397        let diff_file =
398            fs::read_to_string("test_data/composition/simple_app.diffs")
399                .unwrap();
400        let com = Parser::parse_git_udiff(&diff_file).unwrap();
401        fs::copy(
402            "test_data/simple.before",
403            "test_data/composition/simple_app",
404        )
405        .expect("failed to copy");
406        let comp_root = PathBuf::from_str("test_data/composition").unwrap();
407        com.apply(&comp_root).unwrap();
408        let applied =
409            fs::read_to_string("test_data/composition/simple_app").unwrap();
410        let expected = fs::read_to_string("test_data/simple.after").unwrap();
411        assert_eq!(applied.as_str(), expected.as_str());
412        fs::remove_file("test_data/composition/simple_app")
413            .expect("failed to remove file");
414    }
415
416    #[test]
417    fn test_comp_simple_revert() {
418        let diff_file =
419            fs::read_to_string("test_data/composition/simple_rev.diffs")
420                .unwrap();
421        let com = Parser::parse_git_udiff(&diff_file).unwrap();
422        fs::copy("test_data/simple.after", "test_data/composition/simple_rev")
423            .expect("failed to copy");
424        let comp_root = PathBuf::from_str("test_data/composition").unwrap();
425        com.revert(&comp_root);
426        let reverted =
427            fs::read_to_string("test_data/composition/simple_rev").unwrap();
428        let expected = fs::read_to_string("test_data/simple.before").unwrap();
429        assert_eq!(reverted.as_str(), expected.as_str());
430        fs::remove_file("test_data/composition/simple_rev")
431            .expect("failed to remove file");
432    }
433
434    #[test]
435    fn test_comp_middle_apply() {
436        let diff_file =
437            fs::read_to_string("test_data/composition/middle_app.diffs")
438                .unwrap();
439        let com = Parser::parse_git_udiff(&diff_file).unwrap();
440        fs::copy(
441            "test_data/middle.before",
442            "test_data/composition/middle_app",
443        )
444        .expect("failed to copy");
445        let comp_root = PathBuf::from_str("test_data/composition").unwrap();
446        com.apply(&comp_root);
447        let applied =
448            fs::read_to_string("test_data/composition/middle_app").unwrap();
449        let expected = fs::read_to_string("test_data/middle.after").unwrap();
450        assert_eq!(applied.as_str(), expected.as_str());
451        fs::remove_file("test_data/composition/middle_app")
452            .expect("failed to remove file");
453    }
454
455    #[test]
456    fn test_comp_middle_revert() {
457        let diff_file =
458            fs::read_to_string("test_data/composition/middle_rev.diffs")
459                .unwrap();
460        let com = Parser::parse_git_udiff(&diff_file).unwrap();
461        fs::copy("test_data/middle.after", "test_data/composition/middle_rev")
462            .expect("failed to copy");
463        let comp_root = PathBuf::from_str("test_data/composition").unwrap();
464        com.revert(&comp_root);
465        let reverted =
466            fs::read_to_string("test_data/composition/middle_rev").unwrap();
467        let expected = fs::read_to_string("test_data/middle.before").unwrap();
468        assert_eq!(reverted.as_str(), expected.as_str());
469        fs::remove_file("test_data/composition/middle_rev")
470            .expect("failed to remove file");
471    }
472}