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