1 //! sync git api for fetching a diff
2
3 use super::{
4 commit_files::get_commit_diff,
5 utils::{self, get_head_repo, work_dir},
6 CommitId,
7 };
8 use crate::{error::Error, error::Result, hash};
9 use git2::{
10 Delta, Diff, DiffDelta, DiffFormat, DiffHunk, DiffOptions, Patch,
11 Repository,
12 };
13 use scopetime::scope_time;
14 use std::{cell::RefCell, fs, path::Path, rc::Rc};
15
16 /// type of diff of a single line
17 #[derive(Copy, Clone, PartialEq, Hash, Debug)]
18 pub enum DiffLineType {
19 /// just surrounding line, no change
20 None,
21 /// header of the hunk
22 Header,
23 /// line added
24 Add,
25 /// line deleted
26 Delete,
27 }
28
29 impl Default for DiffLineType {
default() -> Self30 fn default() -> Self {
31 DiffLineType::None
32 }
33 }
34
35 ///
36 #[derive(Default, Clone, Hash, Debug)]
37 pub struct DiffLine {
38 ///
39 pub content: String,
40 ///
41 pub line_type: DiffLineType,
42 }
43
44 #[derive(Debug, Default, Clone, Copy, PartialEq, Hash)]
45 pub(crate) struct HunkHeader {
46 old_start: u32,
47 old_lines: u32,
48 new_start: u32,
49 new_lines: u32,
50 }
51
52 impl From<DiffHunk<'_>> for HunkHeader {
from(h: DiffHunk) -> Self53 fn from(h: DiffHunk) -> Self {
54 Self {
55 old_start: h.old_start(),
56 old_lines: h.old_lines(),
57 new_start: h.new_start(),
58 new_lines: h.new_lines(),
59 }
60 }
61 }
62
63 /// single diff hunk
64 #[derive(Default, Clone, Hash, Debug)]
65 pub struct Hunk {
66 /// hash of the hunk header
67 pub header_hash: u64,
68 /// list of `DiffLine`s
69 pub lines: Vec<DiffLine>,
70 }
71
72 /// collection of hunks, sum of all diff lines
73 #[derive(Default, Clone, Hash, Debug)]
74 pub struct FileDiff {
75 /// list of hunks
76 pub hunks: Vec<Hunk>,
77 /// lines total summed up over hunks
78 pub lines: usize,
79 ///
80 pub untracked: bool,
81 /// old and new file size in bytes
82 pub sizes: (u64, u64),
83 /// size delta in bytes
84 pub size_delta: i64,
85 }
86
get_diff_raw<'a>( repo: &'a Repository, p: &str, stage: bool, reverse: bool, ) -> Result<Diff<'a>>87 pub(crate) fn get_diff_raw<'a>(
88 repo: &'a Repository,
89 p: &str,
90 stage: bool,
91 reverse: bool,
92 ) -> Result<Diff<'a>> {
93 // scope_time!("get_diff_raw");
94
95 let mut opt = DiffOptions::new();
96 opt.pathspec(p);
97 opt.reverse(reverse);
98
99 let diff = if stage {
100 // diff against head
101 if let Ok(id) = get_head_repo(&repo) {
102 let parent = repo.find_commit(id.into())?;
103
104 let tree = parent.tree()?;
105 repo.diff_tree_to_index(
106 Some(&tree),
107 Some(&repo.index()?),
108 Some(&mut opt),
109 )?
110 } else {
111 repo.diff_tree_to_index(
112 None,
113 Some(&repo.index()?),
114 Some(&mut opt),
115 )?
116 }
117 } else {
118 opt.include_untracked(true);
119 opt.recurse_untracked_dirs(true);
120 repo.diff_index_to_workdir(None, Some(&mut opt))?
121 };
122
123 Ok(diff)
124 }
125
126 /// returns diff of a specific file either in `stage` or workdir
get_diff( repo_path: &str, p: String, stage: bool, ) -> Result<FileDiff>127 pub fn get_diff(
128 repo_path: &str,
129 p: String,
130 stage: bool,
131 ) -> Result<FileDiff> {
132 scope_time!("get_diff");
133
134 let repo = utils::repo(repo_path)?;
135 let work_dir = work_dir(&repo);
136 let diff = get_diff_raw(&repo, &p, stage, false)?;
137
138 raw_diff_to_file_diff(&diff, work_dir)
139 }
140
141 /// returns diff of a specific file inside a commit
142 /// see `get_commit_diff`
get_diff_commit( repo_path: &str, id: CommitId, p: String, ) -> Result<FileDiff>143 pub fn get_diff_commit(
144 repo_path: &str,
145 id: CommitId,
146 p: String,
147 ) -> Result<FileDiff> {
148 scope_time!("get_diff_commit");
149
150 let repo = utils::repo(repo_path)?;
151 let work_dir = work_dir(&repo);
152 let diff = get_commit_diff(&repo, id, Some(p))?;
153
154 raw_diff_to_file_diff(&diff, work_dir)
155 }
156
157 ///
raw_diff_to_file_diff<'a>( diff: &'a Diff, work_dir: &Path, ) -> Result<FileDiff>158 fn raw_diff_to_file_diff<'a>(
159 diff: &'a Diff,
160 work_dir: &Path,
161 ) -> Result<FileDiff> {
162 let res = Rc::new(RefCell::new(FileDiff::default()));
163 {
164 let mut current_lines = Vec::new();
165 let mut current_hunk: Option<HunkHeader> = None;
166
167 let res_cell = Rc::clone(&res);
168 let adder = move |header: &HunkHeader,
169 lines: &Vec<DiffLine>| {
170 let mut res = res_cell.borrow_mut();
171 res.hunks.push(Hunk {
172 header_hash: hash(header),
173 lines: lines.clone(),
174 });
175 res.lines += lines.len();
176 };
177
178 let res_cell = Rc::clone(&res);
179 let mut put = |delta: DiffDelta,
180 hunk: Option<DiffHunk>,
181 line: git2::DiffLine| {
182 {
183 let mut res = res_cell.borrow_mut();
184 res.sizes = (
185 delta.old_file().size(),
186 delta.new_file().size(),
187 );
188 res.size_delta = (res.sizes.1 as i64)
189 .saturating_sub(res.sizes.0 as i64);
190 }
191 if let Some(hunk) = hunk {
192 let hunk_header = HunkHeader::from(hunk);
193
194 match current_hunk {
195 None => current_hunk = Some(hunk_header),
196 Some(h) if h != hunk_header => {
197 adder(&h, ¤t_lines);
198 current_lines.clear();
199 current_hunk = Some(hunk_header)
200 }
201 _ => (),
202 }
203
204 let line_type = match line.origin() {
205 'H' => DiffLineType::Header,
206 '<' | '-' => DiffLineType::Delete,
207 '>' | '+' => DiffLineType::Add,
208 _ => DiffLineType::None,
209 };
210
211 let diff_line = DiffLine {
212 content: String::from_utf8_lossy(line.content())
213 .to_string(),
214 line_type,
215 };
216
217 current_lines.push(diff_line);
218 }
219 };
220
221 let new_file_diff = if diff.deltas().len() == 1 {
222 let delta: DiffDelta = diff
223 .deltas()
224 .next()
225 .expect("it's safe to unwrap here because we check first that diff.deltas has a single element");
226
227 if delta.status() == Delta::Untracked {
228 let relative_path =
229 delta.new_file().path().ok_or_else(|| {
230 Error::Generic(
231 "new file path is unspecified."
232 .to_string(),
233 )
234 })?;
235
236 let newfile_path = work_dir.join(relative_path);
237
238 if let Some(newfile_content) =
239 new_file_content(&newfile_path)
240 {
241 let mut patch = Patch::from_buffers(
242 &[],
243 None,
244 newfile_content.as_slice(),
245 Some(&newfile_path),
246 None,
247 )?;
248
249 patch
250 .print(&mut |delta, hunk:Option<DiffHunk>, line: git2::DiffLine| {
251 put(delta,hunk,line);
252 true
253 })?;
254
255 true
256 } else {
257 false
258 }
259 } else {
260 false
261 }
262 } else {
263 false
264 };
265
266 if !new_file_diff {
267 diff.print(
268 DiffFormat::Patch,
269 move |delta, hunk, line: git2::DiffLine| {
270 put(delta, hunk, line);
271 true
272 },
273 )?;
274 }
275
276 if !current_lines.is_empty() {
277 adder(
278 ¤t_hunk.expect("invalid hunk"),
279 ¤t_lines,
280 );
281 }
282
283 if new_file_diff {
284 res.borrow_mut().untracked = true;
285 }
286 }
287 let res = Rc::try_unwrap(res).expect("rc error");
288 Ok(res.into_inner())
289 }
290
new_file_content(path: &Path) -> Option<Vec<u8>>291 fn new_file_content(path: &Path) -> Option<Vec<u8>> {
292 if let Ok(meta) = fs::symlink_metadata(path) {
293 if meta.file_type().is_symlink() {
294 if let Ok(path) = fs::read_link(path) {
295 return Some(
296 path.to_str()?.to_string().as_bytes().into(),
297 );
298 }
299 } else if meta.file_type().is_file() {
300 if let Ok(content) = fs::read(path) {
301 return Some(content);
302 }
303 }
304 }
305
306 None
307 }
308
309 #[cfg(test)]
310 mod tests {
311 use super::{get_diff, get_diff_commit};
312 use crate::error::Result;
313 use crate::sync::{
314 commit, stage_add_file,
315 status::{get_status, StatusType},
316 tests::{get_statuses, repo_init, repo_init_empty},
317 };
318 use std::{
319 fs::{self, File},
320 io::Write,
321 path::Path,
322 };
323
324 #[test]
test_untracked_subfolder()325 fn test_untracked_subfolder() {
326 let (_td, repo) = repo_init().unwrap();
327 let root = repo.path().parent().unwrap();
328 let repo_path = root.as_os_str().to_str().unwrap();
329
330 assert_eq!(get_statuses(repo_path), (0, 0));
331
332 fs::create_dir(&root.join("foo")).unwrap();
333 File::create(&root.join("foo/bar.txt"))
334 .unwrap()
335 .write_all(b"test\nfoo")
336 .unwrap();
337
338 assert_eq!(get_statuses(repo_path), (1, 0));
339
340 let diff =
341 get_diff(repo_path, "foo/bar.txt".to_string(), false)
342 .unwrap();
343
344 assert_eq!(diff.hunks.len(), 1);
345 assert_eq!(diff.hunks[0].lines[1].content, "test\n");
346 }
347
348 #[test]
test_empty_repo()349 fn test_empty_repo() {
350 let file_path = Path::new("foo.txt");
351 let (_td, repo) = repo_init_empty().unwrap();
352 let root = repo.path().parent().unwrap();
353 let repo_path = root.as_os_str().to_str().unwrap();
354
355 assert_eq!(get_statuses(repo_path), (0, 0));
356
357 File::create(&root.join(file_path))
358 .unwrap()
359 .write_all(b"test\nfoo")
360 .unwrap();
361
362 assert_eq!(get_statuses(repo_path), (1, 0));
363
364 stage_add_file(repo_path, file_path).unwrap();
365
366 assert_eq!(get_statuses(repo_path), (0, 1));
367
368 let diff = get_diff(
369 repo_path,
370 String::from(file_path.to_str().unwrap()),
371 true,
372 )
373 .unwrap();
374
375 assert_eq!(diff.hunks.len(), 1);
376 }
377
378 static HUNK_A: &str = r"
379 1 start
380 2
381 3
382 4
383 5
384 6 middle
385 7
386 8
387 9
388 0
389 1 end";
390
391 static HUNK_B: &str = r"
392 1 start
393 2 newa
394 3
395 4
396 5
397 6 middle
398 7
399 8
400 9
401 0 newb
402 1 end";
403
404 #[test]
test_hunks()405 fn test_hunks() {
406 let (_td, repo) = repo_init().unwrap();
407 let root = repo.path().parent().unwrap();
408 let repo_path = root.as_os_str().to_str().unwrap();
409
410 assert_eq!(get_statuses(repo_path), (0, 0));
411
412 let file_path = root.join("bar.txt");
413
414 {
415 File::create(&file_path)
416 .unwrap()
417 .write_all(HUNK_A.as_bytes())
418 .unwrap();
419 }
420
421 let res = get_status(repo_path, StatusType::WorkingDir, true)
422 .unwrap();
423 assert_eq!(res.len(), 1);
424 assert_eq!(res[0].path, "bar.txt");
425
426 stage_add_file(repo_path, Path::new("bar.txt")).unwrap();
427 assert_eq!(get_statuses(repo_path), (0, 1));
428
429 // overwrite with next content
430 {
431 File::create(&file_path)
432 .unwrap()
433 .write_all(HUNK_B.as_bytes())
434 .unwrap();
435 }
436
437 assert_eq!(get_statuses(repo_path), (1, 1));
438
439 let res = get_diff(repo_path, "bar.txt".to_string(), false)
440 .unwrap();
441
442 assert_eq!(res.hunks.len(), 2)
443 }
444
445 #[test]
test_diff_newfile_in_sub_dir_current_dir()446 fn test_diff_newfile_in_sub_dir_current_dir() {
447 let file_path = Path::new("foo/foo.txt");
448 let (_td, repo) = repo_init_empty().unwrap();
449 let root = repo.path().parent().unwrap();
450
451 let sub_path = root.join("foo/");
452
453 fs::create_dir_all(&sub_path).unwrap();
454 File::create(&root.join(file_path))
455 .unwrap()
456 .write_all(b"test")
457 .unwrap();
458
459 let diff = get_diff(
460 sub_path.to_str().unwrap(),
461 String::from(file_path.to_str().unwrap()),
462 false,
463 )
464 .unwrap();
465
466 assert_eq!(diff.hunks[0].lines[1].content, "test");
467 }
468
469 #[test]
test_diff_delta_size() -> Result<()>470 fn test_diff_delta_size() -> Result<()> {
471 let file_path = Path::new("bar");
472 let (_td, repo) = repo_init_empty().unwrap();
473 let root = repo.path().parent().unwrap();
474 let repo_path = root.as_os_str().to_str().unwrap();
475
476 File::create(&root.join(file_path))?.write_all(b"\x00")?;
477
478 stage_add_file(repo_path, file_path).unwrap();
479
480 commit(repo_path, "commit").unwrap();
481
482 File::create(&root.join(file_path))?
483 .write_all(b"\x00\x02")?;
484
485 let diff = get_diff(
486 repo_path,
487 String::from(file_path.to_str().unwrap()),
488 false,
489 )
490 .unwrap();
491
492 dbg!(&diff);
493 assert_eq!(diff.sizes, (1, 2));
494 assert_eq!(diff.size_delta, 1);
495
496 Ok(())
497 }
498
499 #[test]
test_binary_diff_delta_size_untracked() -> Result<()>500 fn test_binary_diff_delta_size_untracked() -> Result<()> {
501 let file_path = Path::new("bar");
502 let (_td, repo) = repo_init_empty().unwrap();
503 let root = repo.path().parent().unwrap();
504 let repo_path = root.as_os_str().to_str().unwrap();
505
506 File::create(&root.join(file_path))?
507 .write_all(b"\x00\xc7")?;
508
509 let diff = get_diff(
510 repo_path,
511 String::from(file_path.to_str().unwrap()),
512 false,
513 )
514 .unwrap();
515
516 dbg!(&diff);
517 assert_eq!(diff.sizes, (0, 2));
518 assert_eq!(diff.size_delta, 2);
519
520 Ok(())
521 }
522
523 #[test]
test_diff_delta_size_commit() -> Result<()>524 fn test_diff_delta_size_commit() -> Result<()> {
525 let file_path = Path::new("bar");
526 let (_td, repo) = repo_init_empty().unwrap();
527 let root = repo.path().parent().unwrap();
528 let repo_path = root.as_os_str().to_str().unwrap();
529
530 File::create(&root.join(file_path))?.write_all(b"\x00")?;
531
532 stage_add_file(repo_path, file_path).unwrap();
533
534 commit(repo_path, "").unwrap();
535
536 File::create(&root.join(file_path))?
537 .write_all(b"\x00\x02")?;
538
539 stage_add_file(repo_path, file_path).unwrap();
540
541 let id = commit(repo_path, "").unwrap();
542
543 let diff =
544 get_diff_commit(repo_path, id, String::new()).unwrap();
545
546 dbg!(&diff);
547 assert_eq!(diff.sizes, (1, 2));
548 assert_eq!(diff.size_delta, 1);
549
550 Ok(())
551 }
552 }
553