This repository has no description
1use std::convert::Infallible;
2use std::ops::ControlFlow;
3
4use gix::diff::blob::unified_diff::{ConsumeHunk, ContextSize, DiffLineKind, HunkHeader};
5use gix::diff::blob::{Algorithm, Diff, InternedInput, UnifiedDiff};
6use knot_types::{ChangedFiles, ChangedFilesBudget, Listing, Oid, RepoPath};
7
8use crate::error::{GitError, backend};
9use crate::objects::EntryKind;
10use crate::repo::Repo;
11
12const BINARY_SNIFF_BYTES: usize = 8000;
13pub const MAX_DIFF_BLOB_BYTES: u64 = 25 * 1024 * 1024;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum LineOp {
17 Context,
18 Delete,
19 Add,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct HunkLine {
24 pub op: LineOp,
25 pub text: Vec<u8>,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub struct PatchRange {
30 pub base: Option<Oid>,
31 pub head: Oid,
32}
33
34// Just making sure a count in a start slot doesn't even compile.
35knot_types::scalar_newtype! {
36 pub struct LineNumber(u32);
37 pub struct LineCount(u32);
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct Hunk {
42 pub old_start: LineNumber,
43 pub old_lines: LineCount,
44 pub new_start: LineNumber,
45 pub new_lines: LineCount,
46 pub lines: Vec<HunkLine>,
47}
48
49impl Hunk {
50 pub fn added(&self) -> LineCount {
51 self.count(LineOp::Add)
52 }
53
54 pub fn deleted(&self) -> LineCount {
55 self.count(LineOp::Delete)
56 }
57
58 fn count(&self, op: LineOp) -> LineCount {
59 LineCount::new(
60 self.lines
61 .iter()
62 .filter(|line| line.op == op)
63 .count()
64 .try_into()
65 .unwrap_or(u32::MAX),
66 )
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum PatchStatus {
72 Added,
73 Deleted,
74 Modified,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct FilePatch {
79 pub status: PatchStatus,
80 pub path: RepoPath,
81 pub old_oid: Oid,
82 pub new_oid: Oid,
83 pub old_kind: Option<EntryKind>,
84 pub new_kind: Option<EntryKind>,
85 pub is_binary: bool,
86 pub hunks: Vec<Hunk>,
87}
88
89fn is_binary(content: &[u8]) -> bool {
90 content[..content.len().min(BINARY_SNIFF_BYTES)].contains(&0)
91}
92
93struct CollectHunks {
94 hunks: Vec<Hunk>,
95}
96
97impl ConsumeHunk for CollectHunks {
98 type Out = Vec<Hunk>;
99
100 fn consume_hunk(
101 &mut self,
102 header: HunkHeader,
103 lines: &[(DiffLineKind, &[u8])],
104 ) -> std::io::Result<()> {
105 let map_op = |kind: DiffLineKind| match kind {
106 DiffLineKind::Context => LineOp::Context,
107 DiffLineKind::Remove => LineOp::Delete,
108 DiffLineKind::Add => LineOp::Add,
109 };
110 let adjust = |start: u32, len: u32| {
111 if len == 0 {
112 start.saturating_sub(1)
113 } else {
114 start
115 }
116 };
117 self.hunks.push(Hunk {
118 old_start: LineNumber::new(adjust(header.before_hunk_start, header.before_hunk_len)),
119 old_lines: LineCount::new(header.before_hunk_len),
120 new_start: LineNumber::new(adjust(header.after_hunk_start, header.after_hunk_len)),
121 new_lines: LineCount::new(header.after_hunk_len),
122 lines: lines
123 .iter()
124 .map(|(kind, text)| HunkLine {
125 op: map_op(*kind),
126 text: text.to_vec(),
127 })
128 .collect(),
129 });
130 Ok(())
131 }
132
133 fn finish(self) -> Self::Out {
134 self.hunks
135 }
136}
137
138fn text_hunks(old: &[u8], new: &[u8]) -> Result<Vec<Hunk>, GitError> {
139 let input = InternedInput::new(old, new);
140 let diff = Diff::compute(Algorithm::Histogram, &input);
141 UnifiedDiff::new(
142 &diff,
143 &input,
144 CollectHunks { hunks: Vec::new() },
145 ContextSize::symmetrical(3),
146 )
147 .consume()
148 .map_err(backend)
149}
150
151enum Side {
152 Absent,
153 Present { oid: Oid, kind: EntryKind },
154}
155
156impl Side {
157 fn oid(&self, absent: Oid) -> Oid {
158 match self {
159 Side::Absent => absent,
160 Side::Present { oid, .. } => *oid,
161 }
162 }
163
164 fn kind(&self) -> Option<EntryKind> {
165 match self {
166 Side::Absent => None,
167 Side::Present { kind, .. } => Some(*kind),
168 }
169 }
170}
171
172impl Repo {
173 fn patch_content(&self, side: &Side) -> Result<Vec<u8>, GitError> {
174 match side {
175 Side::Absent => Ok(Vec::new()),
176 Side::Present { oid, kind } => match kind {
177 EntryKind::Commit => {
178 Ok(format!("Subproject commit {}\n", oid.to_hex()).into_bytes())
179 }
180 EntryKind::Tree => Ok(Vec::new()),
181 _ => self.read_blob(*oid),
182 },
183 }
184 }
185
186 fn side_within_diff_budget(&self, side: &Side) -> Result<bool, GitError> {
187 match side {
188 Side::Present {
189 oid,
190 kind: EntryKind::Blob | EntryKind::BlobExecutable | EntryKind::Link,
191 } => Ok(self.blob_size(*oid)? <= MAX_DIFF_BLOB_BYTES),
192 _ => Ok(true),
193 }
194 }
195
196 fn file_patch(
197 &self,
198 status: PatchStatus,
199 path: RepoPath,
200 old: Side,
201 new: Side,
202 ) -> Result<FilePatch, GitError> {
203 let within_budget =
204 self.side_within_diff_budget(&old)? && self.side_within_diff_budget(&new)?;
205 let (binary, hunks) = match within_budget {
206 false => (true, Vec::new()),
207 true => {
208 let old_content = self.patch_content(&old)?;
209 let new_content = self.patch_content(&new)?;
210 let binary = is_binary(&old_content) || is_binary(&new_content);
211 let hunks = match binary {
212 true => Vec::new(),
213 false => text_hunks(&old_content, &new_content)?,
214 };
215 (binary, hunks)
216 }
217 };
218 Ok(FilePatch {
219 status,
220 path,
221 old_oid: old.oid(self.object_format().null_oid()),
222 new_oid: new.oid(self.object_format().null_oid()),
223 old_kind: old.kind(),
224 new_kind: new.kind(),
225 is_binary: binary,
226 hunks,
227 })
228 }
229
230 fn diff_trees(&self, range: PatchRange) -> Result<(gix::Tree<'_>, gix::Tree<'_>), GitError> {
231 let PatchRange {
232 base: old_commit,
233 head: new_commit,
234 } = range;
235 let new_tree = self.root_tree(self.peel_to_commit(new_commit)?)?;
236 let old_tree = match old_commit {
237 Some(commit) => self.root_tree(self.peel_to_commit(commit)?)?,
238 None => self.git().empty_tree(),
239 };
240 Ok((old_tree, new_tree))
241 }
242
243 pub fn changed_paths(&self, range: PatchRange) -> Result<ChangedFiles, GitError> {
244 let (old_tree, new_tree) = self.diff_trees(range)?;
245 let mut budget = ChangedFilesBudget::new();
246 let walked = old_tree
247 .changes()
248 .map_err(backend)?
249 .options(|options| {
250 options.track_rewrites(None);
251 })
252 .for_each_to_obtain_tree(&new_tree, |change| -> Result<ControlFlow<()>, Infallible> {
253 use gix::object::tree::diff::Change;
254 let (location, is_tree) = match change {
255 Change::Addition {
256 location,
257 entry_mode,
258 ..
259 }
260 | Change::Deletion {
261 location,
262 entry_mode,
263 ..
264 } => (location, entry_mode.is_tree()),
265 Change::Modification {
266 location,
267 previous_entry_mode,
268 entry_mode,
269 ..
270 } => (
271 location,
272 previous_entry_mode.is_tree() || entry_mode.is_tree(),
273 ),
274 Change::Rewrite { .. } => return Ok(ControlFlow::Continue(())),
275 };
276 match (is_tree, RepoPath::new(location.to_string())) {
277 (true, _) => Ok(ControlFlow::Continue(())),
278 (false, Ok(path)) => Ok(budget.admit(path)),
279 (false, Err(_)) => Ok(budget.truncate()),
280 }
281 });
282 let changed = budget.finish();
283 // When the above gives us `Break`,
284 // gix doesn't return partial-success
285 // but instead `Error::Cancelled`.
286 // So if the listing comes out truncated,
287 // the "error" in `walked` is our own stop-sign given
288 // back at us and we ignore it on purpose.
289 // If the listing is complete,
290 // nothing ever asked to stop
291 // and when `walked` errors out it's actually
292 // from the diff itself that we should believe.
293 match changed.listing() {
294 Listing::Truncated => Ok(changed),
295 Listing::Complete => walked.map(|_| changed).map_err(backend),
296 }
297 }
298
299 pub fn commit_patches(&self, range: PatchRange) -> Result<Vec<FilePatch>, GitError> {
300 let (old_tree, new_tree) = self.diff_trees(range)?;
301 let mut sides: Vec<(PatchStatus, String, Side, Side)> = Vec::new();
302 old_tree
303 .changes()
304 .map_err(backend)?
305 .options(|options| {
306 options.track_rewrites(None);
307 })
308 .for_each_to_obtain_tree(&new_tree, |change| {
309 use gix::object::tree::diff::Change;
310 match change {
311 Change::Addition {
312 location,
313 id,
314 entry_mode,
315 ..
316 } => sides.push((
317 PatchStatus::Added,
318 location.to_string(),
319 Side::Absent,
320 Side::Present {
321 oid: Oid::from(id.detach()),
322 kind: crate::objects::map_kind(entry_mode.kind()),
323 },
324 )),
325 Change::Deletion {
326 location,
327 id,
328 entry_mode,
329 ..
330 } => sides.push((
331 PatchStatus::Deleted,
332 location.to_string(),
333 Side::Present {
334 oid: Oid::from(id.detach()),
335 kind: crate::objects::map_kind(entry_mode.kind()),
336 },
337 Side::Absent,
338 )),
339 Change::Modification {
340 location,
341 previous_id,
342 id,
343 previous_entry_mode,
344 entry_mode,
345 } => sides.push((
346 PatchStatus::Modified,
347 location.to_string(),
348 Side::Present {
349 oid: Oid::from(previous_id.detach()),
350 kind: crate::objects::map_kind(previous_entry_mode.kind()),
351 },
352 Side::Present {
353 oid: Oid::from(id.detach()),
354 kind: crate::objects::map_kind(entry_mode.kind()),
355 },
356 )),
357 Change::Rewrite { .. } => {}
358 }
359 Ok::<_, std::convert::Infallible>(std::ops::ControlFlow::Continue(()))
360 })
361 .map_err(backend)?;
362
363 sides
364 .into_iter()
365 .filter(|(_, _, old, new)| {
366 !matches!(
367 (old, new),
368 (
369 Side::Present {
370 kind: EntryKind::Tree,
371 ..
372 },
373 _
374 ) | (
375 _,
376 Side::Present {
377 kind: EntryKind::Tree,
378 ..
379 }
380 )
381 )
382 })
383 .map(|(status, path, old, new)| {
384 let path =
385 RepoPath::new(path).map_err(|error| GitError::Decode(error.to_string()))?;
386 self.file_patch(status, path, old, new)
387 })
388 .collect()
389 }
390}