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 #[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 #[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 #[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 #[error("couldn't retrieve patch for {0}")]
78 PatchUnavailable(PathBuf),
79 #[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 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}