radicle_surf/diff/
git.rs

1use std::convert::TryFrom;
2
3use super::{
4    Diff, DiffContent, DiffFile, EofNewLine, FileMode, FileStats, Hunk, Hunks, Line, Modification,
5    Stats,
6};
7
8pub mod error {
9    use std::path::PathBuf;
10
11    use thiserror::Error;
12
13    #[derive(Debug, Error)]
14    #[non_exhaustive]
15    pub enum Addition {
16        #[error(transparent)]
17        Git(#[from] git2::Error),
18        #[error("the new line number was missing for an added line")]
19        MissingNewLineNo,
20    }
21
22    #[derive(Debug, Error)]
23    #[non_exhaustive]
24    pub enum Deletion {
25        #[error(transparent)]
26        Git(#[from] git2::Error),
27        #[error("the new line number was missing for an deleted line")]
28        MissingOldLineNo,
29    }
30
31    #[derive(Debug, Error)]
32    #[non_exhaustive]
33    pub enum FileMode {
34        #[error("unknown file mode `{0:?}`")]
35        Unknown(git2::FileMode),
36    }
37
38    #[derive(Debug, Error)]
39    #[non_exhaustive]
40    pub enum Modification {
41        /// A Git `DiffLine` is invalid.
42        #[error(
43            "invalid `git2::DiffLine` which contains no line numbers for either side of the diff"
44        )]
45        Invalid,
46    }
47
48    #[derive(Debug, Error)]
49    #[non_exhaustive]
50    pub enum Hunk {
51        #[error(transparent)]
52        Git(#[from] git2::Error),
53        #[error(transparent)]
54        Line(#[from] Modification),
55    }
56
57    /// A Git diff error.
58    #[derive(Debug, Error)]
59    #[non_exhaustive]
60    pub enum Diff {
61        #[error(transparent)]
62        Addition(#[from] Addition),
63        #[error(transparent)]
64        Deletion(#[from] Deletion),
65        /// A Git delta type isn't currently handled.
66        #[error("git delta type is not handled")]
67        DeltaUnhandled(git2::Delta),
68        #[error(transparent)]
69        Git(#[from] git2::Error),
70        #[error(transparent)]
71        FileMode(#[from] FileMode),
72        #[error(transparent)]
73        Hunk(#[from] Hunk),
74        #[error(transparent)]
75        Line(#[from] Modification),
76        /// A patch is unavailable.
77        #[error("couldn't retrieve patch for {0}")]
78        PatchUnavailable(PathBuf),
79        /// A The path of a file isn't available.
80        #[error("couldn't retrieve file path")]
81        PathUnavailable,
82    }
83}
84
85impl TryFrom<git2::DiffFile<'_>> for DiffFile {
86    type Error = error::FileMode;
87
88    fn try_from(value: git2::DiffFile) -> Result<Self, Self::Error> {
89        Ok(Self {
90            mode: value.mode().try_into()?,
91            oid: value.id().into(),
92        })
93    }
94}
95
96impl TryFrom<git2::FileMode> for FileMode {
97    type Error = error::FileMode;
98
99    fn try_from(value: git2::FileMode) -> Result<Self, Self::Error> {
100        match value {
101            git2::FileMode::Blob => Ok(Self::Blob),
102            git2::FileMode::BlobExecutable => Ok(Self::BlobExecutable),
103            git2::FileMode::Commit => Ok(Self::Commit),
104            git2::FileMode::Tree => Ok(Self::Tree),
105            git2::FileMode::Link => Ok(Self::Link),
106            _ => Err(error::FileMode::Unknown(value)),
107        }
108    }
109}
110
111impl From<FileMode> for git2::FileMode {
112    fn from(m: FileMode) -> Self {
113        match m {
114            FileMode::Blob => git2::FileMode::Blob,
115            FileMode::BlobExecutable => git2::FileMode::BlobExecutable,
116            FileMode::Tree => git2::FileMode::Tree,
117            FileMode::Link => git2::FileMode::Link,
118            FileMode::Commit => git2::FileMode::Commit,
119        }
120    }
121}
122
123impl TryFrom<git2::Patch<'_>> for DiffContent {
124    type Error = error::Hunk;
125
126    fn try_from(patch: git2::Patch) -> Result<Self, Self::Error> {
127        let mut hunks = Vec::new();
128        let mut old_missing_eof = false;
129        let mut new_missing_eof = false;
130        let mut additions = 0;
131        let mut deletions = 0;
132
133        for h in 0..patch.num_hunks() {
134            let (hunk, hunk_lines) = patch.hunk(h)?;
135            let header = Line(hunk.header().to_owned());
136            let mut lines: Vec<Modification> = Vec::new();
137
138            for l in 0..hunk_lines {
139                let line = patch.line_in_hunk(h, l)?;
140                match line.origin_value() {
141                    git2::DiffLineType::ContextEOFNL => {
142                        new_missing_eof = true;
143                        old_missing_eof = true;
144                        continue;
145                    }
146                    git2::DiffLineType::Addition => {
147                        additions += 1;
148                    }
149                    git2::DiffLineType::Deletion => {
150                        deletions += 1;
151                    }
152                    git2::DiffLineType::AddEOFNL => {
153                        additions += 1;
154                        old_missing_eof = true;
155                        continue;
156                    }
157                    git2::DiffLineType::DeleteEOFNL => {
158                        deletions += 1;
159                        new_missing_eof = true;
160                        continue;
161                    }
162                    _ => {}
163                }
164                let line = Modification::try_from(line)?;
165                lines.push(line);
166            }
167            hunks.push(Hunk {
168                header,
169                lines,
170                old: hunk.old_start()..hunk.old_start() + hunk.old_lines(),
171                new: hunk.new_start()..hunk.new_start() + hunk.new_lines(),
172            });
173        }
174        let eof = match (old_missing_eof, new_missing_eof) {
175            (true, true) => EofNewLine::BothMissing,
176            (true, false) => EofNewLine::OldMissing,
177            (false, true) => EofNewLine::NewMissing,
178            (false, false) => EofNewLine::NoneMissing,
179        };
180        Ok(DiffContent::Plain {
181            hunks: Hunks(hunks),
182            stats: FileStats {
183                additions,
184                deletions,
185            },
186            eof,
187        })
188    }
189}
190
191impl TryFrom<git2::DiffLine<'_>> for Modification {
192    type Error = error::Modification;
193
194    fn try_from(line: git2::DiffLine) -> Result<Self, Self::Error> {
195        match (line.old_lineno(), line.new_lineno()) {
196            (None, Some(n)) => Ok(Self::addition(line.content().to_owned(), n)),
197            (Some(n), None) => Ok(Self::deletion(line.content().to_owned(), n)),
198            (Some(l), Some(r)) => Ok(Self::context(line.content().to_owned(), l, r)),
199            (None, None) => Err(error::Modification::Invalid),
200        }
201    }
202}
203
204impl From<git2::DiffStats> for Stats {
205    fn from(stats: git2::DiffStats) -> Self {
206        Self {
207            files_changed: stats.files_changed(),
208            insertions: stats.insertions(),
209            deletions: stats.deletions(),
210        }
211    }
212}
213
214impl TryFrom<git2::Diff<'_>> for Diff {
215    type Error = error::Diff;
216
217    fn try_from(git_diff: git2::Diff) -> Result<Diff, Self::Error> {
218        use git2::Delta;
219
220        let mut diff = Diff::new();
221
222        // This allows libgit2 to run the binary detection.
223        // Reference: <https://github.com/libgit2/libgit2/issues/6637>
224        git_diff.foreach(&mut |_, _| true, None, None, None)?;
225
226        for (idx, delta) in git_diff.deltas().enumerate() {
227            match delta.status() {
228                Delta::Added => created(&mut diff, &git_diff, idx, &delta)?,
229                Delta::Deleted => deleted(&mut diff, &git_diff, idx, &delta)?,
230                Delta::Modified => modified(&mut diff, &git_diff, idx, &delta)?,
231                Delta::Renamed => renamed(&mut diff, &git_diff, idx, &delta)?,
232                Delta::Copied => copied(&mut diff, &git_diff, idx, &delta)?,
233                status => {
234                    return Err(error::Diff::DeltaUnhandled(status));
235                }
236            }
237        }
238
239        Ok(diff)
240    }
241}
242
243fn created(
244    diff: &mut Diff,
245    git_diff: &git2::Diff<'_>,
246    idx: usize,
247    delta: &git2::DiffDelta<'_>,
248) -> Result<(), error::Diff> {
249    let diff_file = delta.new_file();
250    let is_binary = diff_file.is_binary();
251    let path = diff_file
252        .path()
253        .ok_or(error::Diff::PathUnavailable)?
254        .to_path_buf();
255    let new = DiffFile::try_from(diff_file)?;
256
257    let patch = git2::Patch::from_diff(git_diff, idx)?;
258    if is_binary {
259        diff.insert_added(path, DiffContent::Binary, new);
260    } else if let Some(patch) = patch {
261        diff.insert_added(path, DiffContent::try_from(patch)?, new);
262    } else {
263        return Err(error::Diff::PatchUnavailable(path));
264    }
265    Ok(())
266}
267
268fn deleted(
269    diff: &mut Diff,
270    git_diff: &git2::Diff<'_>,
271    idx: usize,
272    delta: &git2::DiffDelta<'_>,
273) -> Result<(), error::Diff> {
274    let diff_file = delta.old_file();
275    let is_binary = diff_file.is_binary();
276    let path = diff_file
277        .path()
278        .ok_or(error::Diff::PathUnavailable)?
279        .to_path_buf();
280    let patch = git2::Patch::from_diff(git_diff, idx)?;
281    let old = DiffFile::try_from(diff_file)?;
282
283    if is_binary {
284        diff.insert_deleted(path, DiffContent::Binary, old);
285    } else if let Some(patch) = patch {
286        diff.insert_deleted(path, DiffContent::try_from(patch)?, old);
287    } else {
288        return Err(error::Diff::PatchUnavailable(path));
289    }
290    Ok(())
291}
292
293fn modified(
294    diff: &mut Diff,
295    git_diff: &git2::Diff<'_>,
296    idx: usize,
297    delta: &git2::DiffDelta<'_>,
298) -> Result<(), error::Diff> {
299    let diff_file = delta.new_file();
300    let path = diff_file
301        .path()
302        .ok_or(error::Diff::PathUnavailable)?
303        .to_path_buf();
304    let patch = git2::Patch::from_diff(git_diff, idx)?;
305    let old = DiffFile::try_from(delta.old_file())?;
306    let new = DiffFile::try_from(delta.new_file())?;
307
308    if diff_file.is_binary() {
309        diff.insert_modified(path, DiffContent::Binary, old, new);
310        Ok(())
311    } else if let Some(patch) = patch {
312        diff.insert_modified(path, DiffContent::try_from(patch)?, old, new);
313        Ok(())
314    } else {
315        Err(error::Diff::PatchUnavailable(path))
316    }
317}
318
319fn renamed(
320    diff: &mut Diff,
321    git_diff: &git2::Diff<'_>,
322    idx: usize,
323    delta: &git2::DiffDelta<'_>,
324) -> Result<(), error::Diff> {
325    let old_path = delta
326        .old_file()
327        .path()
328        .ok_or(error::Diff::PathUnavailable)?
329        .to_path_buf();
330    let new_path = delta
331        .new_file()
332        .path()
333        .ok_or(error::Diff::PathUnavailable)?
334        .to_path_buf();
335    let patch = git2::Patch::from_diff(git_diff, idx)?;
336    let old = DiffFile::try_from(delta.old_file())?;
337    let new = DiffFile::try_from(delta.new_file())?;
338
339    if delta.new_file().is_binary() {
340        diff.insert_moved(old_path, new_path, old, new, DiffContent::Binary);
341    } else if let Some(patch) = patch {
342        diff.insert_moved(old_path, new_path, old, new, DiffContent::try_from(patch)?);
343    } else {
344        diff.insert_moved(old_path, new_path, old, new, DiffContent::Empty);
345    }
346    Ok(())
347}
348
349fn copied(
350    diff: &mut Diff,
351    git_diff: &git2::Diff<'_>,
352    idx: usize,
353    delta: &git2::DiffDelta<'_>,
354) -> Result<(), error::Diff> {
355    let old_path = delta
356        .old_file()
357        .path()
358        .ok_or(error::Diff::PathUnavailable)?
359        .to_path_buf();
360    let new_path = delta
361        .new_file()
362        .path()
363        .ok_or(error::Diff::PathUnavailable)?
364        .to_path_buf();
365    let patch = git2::Patch::from_diff(git_diff, idx)?;
366    let old = DiffFile::try_from(delta.old_file())?;
367    let new = DiffFile::try_from(delta.new_file())?;
368
369    if delta.new_file().is_binary() {
370        diff.insert_copied(old_path, new_path, old, new, DiffContent::Binary);
371    } else if let Some(patch) = patch {
372        diff.insert_copied(old_path, new_path, old, new, DiffContent::try_from(patch)?);
373    } else {
374        diff.insert_copied(old_path, new_path, old, new, DiffContent::Empty);
375    }
376    Ok(())
377}