1use std::ffi::OsStr;
10use std::fmt::{self, Display};
11use std::os::unix::ffi::OsStrExt;
12use std::path::{Path, PathBuf};
13
14use git_workarea::{CommitId, GitContext, GitError, GitWorkArea, Identity, WorkAreaError};
15use itertools::Itertools;
16use lazy_static::lazy_static;
17use regex::Regex;
18use thiserror::Error;
19
20#[derive(Debug, Error)]
22#[non_exhaustive]
23pub enum CommitError {
24 #[error("git error: {}", source)]
26 Git {
27 #[from]
29 source: GitError,
30 },
31 #[error("filename error: {}", source)]
33 FileName {
34 #[from]
36 source: FileNameError,
37 },
38 #[error("failed to fetch metadata on the {} commit: {}", ref_, output)]
40 CommitMetadata {
41 ref_: CommitId,
43 output: String,
45 },
46 #[error("unexpected output from `git log --pretty=<metadata>`: {}", output)]
48 CommitMetadataOutput {
49 output: String,
51 },
52 #[error("failed to fetch message on the {} commit: {}", ref_, output)]
54 CommitMessage {
55 ref_: CommitId,
57 output: String,
59 },
60 #[error(
62 "failed to list revisions of the {} commit (merge base parent: {}): {}",
63 base_rev,
64 merge_base.as_ref().map_or("<none>", CommitId::as_str),
65 output
66 )]
67 RevList {
68 base_rev: CommitId,
70 merge_base: Option<CommitId>,
72 output: String,
74 },
75 #[error(
77 "failed to determine if the {} commit is an ancestor of {}: {}",
78 best_rev,
79 merge_base,
80 output
81 )]
82 AncestorCheck {
83 best_rev: CommitId,
85 merge_base: CommitId,
87 output: String,
89 },
90 #[error(
92 "failed to find the merge-base between {} and {}: {}",
93 base,
94 head,
95 output
96 )]
97 MergeBase {
98 base: CommitId,
100 head: CommitId,
102 output: String,
104 },
105 #[error(
107 "failed to get the tree diff information for the {} commit (against {}): {}",
108 commit,
109 base.as_ref().map(CommitId::as_str).unwrap_or("parent"),
110 output
111 )]
112 DiffTree {
113 commit: CommitId,
115 base: Option<CommitId>,
117 output: String,
119 },
120 #[error(
122 "failed to get the diff patch for the {} commit (against {}) for {}: {}",
123 commit,
124 base.as_ref().map(CommitId::as_str).unwrap_or("parent"),
125 path.display(),
126 output
127 )]
128 DiffPatch {
129 commit: CommitId,
131 base: Option<CommitId>,
133 path: PathBuf,
135 output: String,
137 },
138}
139
140impl CommitError {
141 fn commit_metadata(ref_: CommitId, output: &[u8]) -> Self {
142 CommitError::CommitMetadata {
143 ref_,
144 output: String::from_utf8_lossy(output).into(),
145 }
146 }
147
148 fn commit_metadata_output(output: String) -> Self {
149 CommitError::CommitMetadataOutput {
150 output,
151 }
152 }
153
154 fn commit_message(ref_: CommitId, output: &[u8]) -> Self {
155 CommitError::CommitMessage {
156 ref_,
157 output: String::from_utf8_lossy(output).into(),
158 }
159 }
160
161 fn rev_list(base_rev: CommitId, merge_base: Option<CommitId>, output: &[u8]) -> Self {
162 CommitError::RevList {
163 base_rev,
164 merge_base,
165 output: String::from_utf8_lossy(output).into(),
166 }
167 }
168
169 fn ancestor_check(best_rev: CommitId, merge_base: CommitId, output: &[u8]) -> Self {
170 CommitError::AncestorCheck {
171 best_rev,
172 merge_base,
173 output: String::from_utf8_lossy(output).into(),
174 }
175 }
176
177 fn merge_base(base: CommitId, head: CommitId, output: &[u8]) -> Self {
178 CommitError::MergeBase {
179 base,
180 head,
181 output: String::from_utf8_lossy(output).into(),
182 }
183 }
184
185 fn diff_tree(commit: CommitId, base: Option<CommitId>, output: &[u8]) -> Self {
186 CommitError::DiffTree {
187 commit,
188 base,
189 output: String::from_utf8_lossy(output).into(),
190 }
191 }
192
193 fn diff_patch(commit: CommitId, base: Option<CommitId>, path: PathBuf, output: &[u8]) -> Self {
194 CommitError::DiffPatch {
195 commit,
196 base,
197 path,
198 output: String::from_utf8_lossy(output).into(),
199 }
200 }
201}
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205pub enum StatusChange {
206 Added,
208 Copied(u8),
210 Deleted,
212 Modified(Option<u8>),
214 Renamed(u8),
216 TypeChanged,
218 Unmerged,
220 Unknown,
222}
223
224impl From<char> for StatusChange {
225 fn from(c: char) -> Self {
226 match c {
227 'A' => StatusChange::Added,
228 'C' => StatusChange::Copied(0),
229 'D' => StatusChange::Deleted,
230 'M' => StatusChange::Modified(None),
231 'R' => StatusChange::Renamed(0),
232 'T' => StatusChange::TypeChanged,
233 'U' => StatusChange::Unmerged,
234 'X' => StatusChange::Unknown,
235 _ => unreachable!("the regex does not match any other characters"),
236 }
237 }
238}
239
240#[derive(Debug, Error)]
242#[non_exhaustive]
243pub enum FileNameError {
244 #[doc(hidden)]
245 #[error("invalid leading octal digit: {}", _0)]
246 InvalidLeadingOctalDigit(u8),
247 #[doc(hidden)]
248 #[error("invalid octal digit: {}", _0)]
249 InvalidOctalDigit(u8),
250 #[doc(hidden)]
251 #[error("invalid escape character: {}", _0)]
252 InvalidEscape(u8),
253 #[doc(hidden)]
254 #[error("trailing backslash in file name")]
255 TrailingBackslash,
256 #[doc(hidden)]
257 #[error("missing eights digit in octal escape")]
258 MissingEightsDigit,
259 #[doc(hidden)]
260 #[error("missing ones digit in octal escape")]
261 MissingOnesDigit,
262}
263
264#[derive(Debug, Clone, Eq)]
272pub enum FileName {
273 #[doc(hidden)]
274 Normal(String),
275 #[doc(hidden)]
276 Quoted { raw: Vec<u8>, name: String },
277}
278
279impl FileName {
280 pub fn new<P>(path: P) -> Result<Self, FileNameError>
282 where
283 P: AsRef<str>,
284 {
285 Self::new_impl(path.as_ref())
286 }
287
288 fn new_impl(path: &str) -> Result<Self, FileNameError> {
290 if path.starts_with('"') {
291 let raw = path
292 .bytes()
294 .skip(1)
296 .dropping_back(1)
297 .batching(|iter| {
299 let n = iter.next();
300 if let Some(b'\\') = n {
301 match iter.next() {
302 Some(b'\\') => Some(Ok(b'\\')),
303 Some(b't') => Some(Ok(b'\t')),
304 Some(b'n') => Some(Ok(b'\n')),
305 Some(b'"') => Some(Ok(b'"')),
306 Some(sfd @ b'0') | Some(sfd @ b'1') | Some(sfd @ b'2')
309 | Some(sfd @ b'3') => Some(Self::parse_octal(iter, sfd)),
310 Some(sfd @ b'4') | Some(sfd @ b'5') | Some(sfd @ b'6')
311 | Some(sfd @ b'7') => {
312 Some(Err(FileNameError::InvalidLeadingOctalDigit(sfd)))
313 },
314 Some(c) => Some(Err(FileNameError::InvalidEscape(c))),
315 None => Some(Err(FileNameError::TrailingBackslash)),
316 }
317 } else {
318 n.map(Ok)
319 }
320 })
321 .collect::<Result<_, _>>()?;
322 Ok(FileName::Quoted {
323 raw,
324 name: path.into(),
325 })
326 } else {
327 Ok(FileName::Normal(path.into()))
328 }
329 }
330
331 fn parse_octal_digit(ch_digit: u8) -> Result<u8, FileNameError> {
333 if !(b'0'..=b'7').contains(&ch_digit) {
334 Err(FileNameError::InvalidOctalDigit(ch_digit))
335 } else {
336 Ok(ch_digit - b'0')
337 }
338 }
339
340 fn parse_octal<I>(iter: &mut I, digit: u8) -> Result<u8, FileNameError>
342 where
343 I: Iterator<Item = u8>,
344 {
345 let sfd = Self::parse_octal_digit(digit)?;
346 let ed = Self::parse_octal_digit(iter.next().ok_or(FileNameError::MissingEightsDigit)?)?;
347 let od = Self::parse_octal_digit(iter.next().ok_or(FileNameError::MissingOnesDigit)?)?;
348
349 Ok(64 * sfd + 8 * ed + od)
350 }
351
352 pub fn as_str(&self) -> &str {
354 self.as_ref()
355 }
356
357 pub fn as_path(&self) -> &Path {
359 self.as_ref()
360 }
361
362 pub fn as_bytes(&self) -> &[u8] {
364 self.as_ref()
365 }
366}
367
368impl Display for FileName {
369 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
370 write!(f, "{}", self.as_str())
371 }
372}
373
374impl PartialEq for FileName {
375 fn eq(&self, rhs: &Self) -> bool {
376 self.as_bytes() == rhs.as_bytes()
377 }
378}
379
380impl AsRef<str> for FileName {
381 fn as_ref(&self) -> &str {
382 match *self {
383 FileName::Normal(ref name)
384 | FileName::Quoted {
385 ref name, ..
386 } => name,
387 }
388 }
389}
390
391impl AsRef<[u8]> for FileName {
392 fn as_ref(&self) -> &[u8] {
393 match *self {
394 FileName::Normal(ref name) => name.as_bytes(),
395 FileName::Quoted {
396 ref raw, ..
397 } => raw,
398 }
399 }
400}
401
402impl AsRef<OsStr> for FileName {
403 fn as_ref(&self) -> &OsStr {
404 match *self {
405 FileName::Normal(ref name) => name.as_ref(),
406 FileName::Quoted {
407 ref raw, ..
408 } => OsStr::from_bytes(raw),
409 }
410 }
411}
412
413impl AsRef<Path> for FileName {
414 fn as_ref(&self) -> &Path {
415 Path::new(self)
416 }
417}
418
419#[derive(Debug)]
421pub struct DiffInfo {
422 pub old_mode: String,
424 pub old_blob: CommitId,
426 pub new_mode: String,
428 pub new_blob: CommitId,
430
431 pub name: FileName,
433 pub status: StatusChange,
435}
436
437pub trait Content {
441 fn workarea(&self, ctx: &GitContext) -> Result<GitWorkArea, WorkAreaError>;
443
444 fn sha1(&self) -> Option<&CommitId>;
446
447 fn diffs(&self) -> &Vec<DiffInfo>;
449
450 fn modified_files(&self) -> Vec<&FileName> {
455 modified_files(self.diffs())
456 }
457
458 fn path_diff(&self, path: &FileName) -> Result<String, CommitError>;
460}
461
462#[derive(Debug)]
464pub struct Commit {
465 pub sha1: CommitId,
467
468 pub message: String,
470
471 pub parents: Vec<CommitId>,
473 pub diffs: Vec<DiffInfo>,
475
476 pub author: Identity,
478 pub committer: Identity,
480
481 ctx: GitContext,
483}
484
485lazy_static! {
486 static ref DIFF_TREE_LINE_RE: Regex = Regex::new(
487 "^:+\
488 (?P<old_modes>[0-7]{6}( [0-7]{6})*) \
489 (?P<new_mode>[0-7]{6}) \
490 (?P<old_blobs>[0-9a-f]{40}( [0-9a-f]{40})*) \
491 (?P<new_blob>[0-9a-f]{40}) \
492 (?P<status>[ACDMRTUX]+)\
493 \t(?P<name>.*)$",
494 )
495 .unwrap();
496}
497
498impl Commit {
499 pub fn new(ctx: &GitContext, sha1: &CommitId) -> Result<Self, CommitError> {
501 let commit_info = ctx
502 .git()
503 .arg("log")
504 .arg("--pretty=%P%n%an%n%ae%n%cn%n%ce")
505 .arg("--max-count=1")
506 .arg(sha1.as_str())
507 .output()
508 .map_err(|err| GitError::subcommand("log --pretty=<metadata>", err))?;
509 if !commit_info.status.success() {
510 return Err(CommitError::commit_metadata(
511 sha1.clone(),
512 &commit_info.stderr,
513 ));
514 }
515 let commit_info_output = String::from_utf8_lossy(&commit_info.stdout);
516 let lines = commit_info_output.lines().collect::<Vec<_>>();
517
518 if lines.len() != 5 {
519 return Err(CommitError::commit_metadata_output(
520 commit_info_output.into(),
521 ));
522 }
523
524 let commit_message = ctx
525 .git()
526 .arg("log")
527 .arg("--pretty=%B")
528 .arg("--max-count=1")
529 .arg(sha1.as_str())
530 .output()
531 .map_err(|err| GitError::subcommand("log --pretty=<message>", err))?;
532 if !commit_message.status.success() {
533 return Err(CommitError::commit_message(
534 sha1.clone(),
535 &commit_message.stderr,
536 ));
537 }
538 let mut commit_message = String::from_utf8_lossy(&commit_message.stdout).into_owned();
539 commit_message.pop();
541
542 let parents = lines[0].split_whitespace().map(CommitId::new).collect();
543
544 Ok(Self {
545 sha1: sha1.clone(),
546 message: commit_message,
547
548 parents,
549 diffs: extract_diffs(ctx, None, sha1)?,
550
551 author: Identity::new(lines[1], lines[2]),
552 committer: Identity::new(lines[3], lines[4]),
553
554 ctx: ctx.clone(),
555 })
556 }
557
558 pub fn file_patch<P>(&self, path: P) -> Result<String, CommitError>
560 where
561 P: AsRef<OsStr>,
562 {
563 file_patch(&self.ctx, None, &self.sha1, path.as_ref())
564 }
565}
566
567impl Content for Commit {
568 fn workarea(&self, ctx: &GitContext) -> Result<GitWorkArea, WorkAreaError> {
569 ctx.prepare(&self.sha1)
570 }
571
572 fn sha1(&self) -> Option<&CommitId> {
573 Some(&self.sha1)
574 }
575
576 fn diffs(&self) -> &Vec<DiffInfo> {
577 &self.diffs
578 }
579
580 fn path_diff(&self, path: &FileName) -> Result<String, CommitError> {
581 self.file_patch(path)
582 }
583}
584
585#[derive(Debug)]
587pub struct Topic {
588 pub base: CommitId,
590 pub sha1: CommitId,
592
593 pub diffs: Vec<DiffInfo>,
595
596 ctx: GitContext,
598}
599
600impl Topic {
601 pub fn new(ctx: &GitContext, base: &CommitId, sha1: &CommitId) -> Result<Self, CommitError> {
603 let merge_base = Self::best_merge_base(ctx, base, sha1)?;
604
605 Ok(Self {
606 diffs: extract_diffs(ctx, Some(&merge_base), sha1)?,
607
608 base: merge_base,
609 sha1: sha1.clone(),
610
611 ctx: ctx.clone(),
612 })
613 }
614
615 fn best_merge_base(
616 ctx: &GitContext,
617 base: &CommitId,
618 sha1: &CommitId,
619 ) -> Result<CommitId, CommitError> {
620 let merge_base = ctx
621 .git()
622 .arg("merge-base")
623 .arg("--all")
624 .arg(base.as_str())
625 .arg(sha1.as_str())
626 .output()
627 .map_err(|err| GitError::subcommand("merge-base --all", err))?;
628
629 Ok(if let Some(1) = merge_base.status.code() {
630 base.clone()
633 } else if merge_base.status.success() {
634 let merge_bases = String::from_utf8_lossy(&merge_base.stdout);
635
636 merge_bases.lines().map(CommitId::new).fold(
637 Ok(base.clone()) as Result<CommitId, CommitError>,
638 |best_mb, merge_base| {
639 let best = match best_mb {
641 Ok(best) => best,
642 err => return err,
643 };
644
645 let rev_parse = ctx
648 .git()
649 .arg("rev-parse")
650 .arg("--verify")
651 .arg("--quiet")
652 .arg(format!("{}~", merge_base))
653 .output()
654 .map_err(|err| GitError::subcommand("rev-parse", err))?;
655 let merge_base_parent = if rev_parse.status.success() {
656 Some(String::from(
657 String::from_utf8_lossy(&rev_parse.stdout).trim(),
658 ))
659 } else {
660 None
661 };
662
663 let mut refs = ctx.git();
665 refs.arg("rev-list")
666 .arg("--first-parent") .arg("--reverse") .arg(base.as_str());
669 if let Some(merge_base_parent) = merge_base_parent.as_ref() {
670 refs.arg(format!("^{}", merge_base_parent));
671 };
672
673 let refs = refs
674 .output()
675 .map_err(|err| GitError::subcommand("rev-list", err))?;
676 if !refs.status.success() {
677 return Err(CommitError::rev_list(
678 base.clone(),
679 merge_base_parent.map(CommitId::new),
680 &refs.stderr,
681 ));
682 }
683 let refs = String::from_utf8_lossy(&refs.stdout);
684
685 if !refs.lines().any(|rev| rev == merge_base.as_str()) && &best != base {
688 return Ok(best);
689 }
690
691 let is_ancestor = ctx
693 .git()
694 .arg("merge-base")
695 .arg("--is-ancestor")
696 .arg(best.as_str())
697 .arg(merge_base.as_str())
698 .output()
699 .map_err(|err| GitError::subcommand("merge-base --is-ancestor", err))?;
700 if let Some(1) = is_ancestor.status.code() {
701 Ok(merge_base)
702 } else if is_ancestor.status.success() {
703 Ok(best)
704 } else {
705 Err(CommitError::ancestor_check(
706 best,
707 merge_base,
708 &is_ancestor.stderr,
709 ))
710 }
711 },
712 )?
713 } else {
714 return Err(CommitError::merge_base(
715 base.clone(),
716 sha1.clone(),
717 &merge_base.stderr,
718 ));
719 })
720 }
721
722 pub fn file_patch<P>(&self, path: P) -> Result<String, CommitError>
724 where
725 P: AsRef<OsStr>,
726 {
727 file_patch(&self.ctx, Some(&self.base), &self.sha1, path.as_ref())
728 }
729}
730
731impl Content for Topic {
732 fn workarea(&self, ctx: &GitContext) -> Result<GitWorkArea, WorkAreaError> {
733 ctx.prepare(&self.sha1)
734 }
735
736 fn sha1(&self) -> Option<&CommitId> {
737 None
738 }
739
740 fn diffs(&self) -> &Vec<DiffInfo> {
741 &self.diffs
742 }
743
744 fn path_diff(&self, path: &FileName) -> Result<String, CommitError> {
745 self.file_patch(path)
746 }
747}
748
749fn modified_files(diffs: &[DiffInfo]) -> Vec<&FileName> {
750 diffs
751 .iter()
752 .filter_map(|diff| {
753 if diff.new_mode == "160000" {
755 return None;
756 }
757
758 if diff.new_mode == "120000" {
760 return None;
761 }
762
763 match diff.status {
764 StatusChange::Added | StatusChange::Modified(_) => Some(&diff.name),
765 _ => None,
767 }
768 })
769 .unique_by(|diff| diff.as_bytes())
770 .collect::<Vec<_>>()
771}
772
773fn extract_diffs(
775 ctx: &GitContext,
776 base: Option<&CommitId>,
777 sha1: &CommitId,
778) -> Result<Vec<DiffInfo>, CommitError> {
779 let mut diff_tree_cmd = ctx.git();
780 diff_tree_cmd
781 .arg("diff-tree")
782 .arg("--no-commit-id")
783 .arg("--root")
784 .arg("-c") .arg("-r"); if let Some(base) = base {
787 diff_tree_cmd.arg(base.as_str());
788 }
789 let diff_tree = diff_tree_cmd
790 .arg(sha1.as_str())
791 .output()
792 .map_err(|err| GitError::subcommand("diff-tree", err))?;
793 if !diff_tree.status.success() {
794 return Err(CommitError::diff_tree(
795 sha1.clone(),
796 base.cloned(),
797 &diff_tree.stderr,
798 ));
799 }
800 let diffs = String::from_utf8_lossy(&diff_tree.stdout);
801 let diff_lines = diffs.lines().filter_map(|l| DIFF_TREE_LINE_RE.captures(l));
802
803 Ok(diff_lines
804 .map(|diff| {
805 let old_modes = diff
806 .name("old_modes")
807 .expect("the diff regex should have a 'old_modes' group");
808 let new_mode = diff
809 .name("new_mode")
810 .expect("the diff regex should have a 'new_mode' group");
811 let old_blobs = diff
812 .name("old_blobs")
813 .expect("the diff regex should have a 'old_blobs' group");
814 let new_blob = diff
815 .name("new_blob")
816 .expect("the diff regex should have a 'new_blob' group");
817 let status = diff
818 .name("status")
819 .expect("the diff regex should have a 'status' group");
820 let raw_name = diff
821 .name("name")
822 .expect("the diff regex should have a 'name' group");
823
824 let name = FileName::new(raw_name.as_str())?;
825 let zip = itertools::multizip((
826 old_modes.as_str().split_whitespace(),
827 old_blobs.as_str().split_whitespace(),
828 status.as_str().chars(),
829 ));
830
831 Ok(zip.map(move |(m, b, s)| {
832 DiffInfo {
833 old_mode: m.into(),
834 new_mode: new_mode.as_str().into(),
835 old_blob: CommitId::new(b),
836 new_blob: CommitId::new(new_blob.as_str()),
837 status: s.into(),
838 name: name.clone(),
839 }
840 }))
841 })
842 .collect::<Result<Vec<_>, FileNameError>>()?
843 .into_iter()
844 .flatten()
845 .collect())
846}
847
848fn file_patch(
849 ctx: &GitContext,
850 base: Option<&CommitId>,
851 sha1: &CommitId,
852 path: &OsStr,
853) -> Result<String, CommitError> {
854 let mut diff_tree_cmd = ctx.git();
855 diff_tree_cmd
856 .arg("diff-tree")
857 .arg("--no-commit-id")
858 .arg("--root")
859 .arg("-p");
860 if let Some(base) = base {
861 diff_tree_cmd.arg(base.as_str());
862 }
863 let diff_tree = diff_tree_cmd
864 .arg(sha1.as_str())
865 .arg("--")
866 .arg(path)
867 .output()
868 .map_err(|err| GitError::subcommand("diff-tree -p", err))?;
869 if !diff_tree.status.success() {
870 return Err(CommitError::diff_patch(
871 sha1.clone(),
872 base.cloned(),
873 path.into(),
874 &diff_tree.stderr,
875 ));
876 }
877
878 Ok(String::from_utf8_lossy(&diff_tree.stdout).into_owned())
879}
880
881#[cfg(test)]
882mod tests {
883 use std::ffi::OsStr;
884 use std::path::Path;
885
886 use git_workarea::{CommitId, GitContext, Identity};
887
888 use crate::commit::{
889 self, Commit, CommitError, Content, DiffInfo, FileName, FileNameError, StatusChange, Topic,
890 };
891 use crate::test;
892
893 fn make_context() -> GitContext {
894 let gitdir = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../.git"));
895 if !gitdir.exists() {
896 panic!("The tests must be run from a git checkout.");
897 }
898
899 GitContext::new(gitdir)
900 }
901
902 fn compare_diffs(actual: &DiffInfo, expected: &DiffInfo) {
903 assert_eq!(actual.old_mode, expected.old_mode);
904 assert_eq!(actual.old_blob, expected.old_blob);
905 assert_eq!(actual.new_mode, expected.new_mode);
906 assert_eq!(actual.new_blob, expected.new_blob);
907 assert_eq!(actual.name, expected.name);
908 assert_eq!(actual.status, expected.status);
909 }
910
911 #[test]
912 fn test_status_characters() {
913 assert_eq!(StatusChange::from('A'), StatusChange::Added);
914 assert_eq!(StatusChange::from('C'), StatusChange::Copied(0));
915 assert_eq!(StatusChange::from('D'), StatusChange::Deleted);
916 assert_eq!(StatusChange::from('M'), StatusChange::Modified(None));
917 assert_eq!(StatusChange::from('R'), StatusChange::Renamed(0));
918 assert_eq!(StatusChange::from('T'), StatusChange::TypeChanged);
919 assert_eq!(StatusChange::from('U'), StatusChange::Unmerged);
920 assert_eq!(StatusChange::from('X'), StatusChange::Unknown);
921 }
922
923 #[test]
924 fn test_filename_parse_quoted() {
925 let name = FileName::new(r#""quoted-with-backslash-\\""#).unwrap();
926 assert_eq!(name.as_str(), r#""quoted-with-backslash-\\""#);
927 assert_eq!(name.as_path(), Path::new("quoted-with-backslash-\\"));
928 assert_eq!(name.as_bytes(), b"quoted-with-backslash-\\");
929 assert_eq!(name.to_string(), r#""quoted-with-backslash-\\""#);
930
931 let name = FileName::new(r#""quoted-with-tab-\t""#).unwrap();
932 assert_eq!(name.as_str(), r#""quoted-with-tab-\t""#);
933 assert_eq!(name.as_path(), Path::new("quoted-with-tab-\t"));
934 assert_eq!(name.as_bytes(), b"quoted-with-tab-\t");
935 assert_eq!(name.to_string(), r#""quoted-with-tab-\t""#);
936
937 let name = FileName::new(r#""quoted-with-newline-\n""#).unwrap();
938 assert_eq!(name.as_str(), r#""quoted-with-newline-\n""#);
939 assert_eq!(name.as_path(), Path::new("quoted-with-newline-\n"));
940 assert_eq!(name.as_bytes(), b"quoted-with-newline-\n");
941 assert_eq!(name.to_string(), r#""quoted-with-newline-\n""#);
942
943 let name = FileName::new(r#""quoted-with-quote-\"""#).unwrap();
944 assert_eq!(name.as_str(), r#""quoted-with-quote-\"""#);
945 assert_eq!(name.as_path(), Path::new("quoted-with-quote-\""));
946 assert_eq!(name.as_bytes(), b"quoted-with-quote-\"");
947 assert_eq!(name.to_string(), r#""quoted-with-quote-\"""#);
948
949 let name = FileName::new(r#""quoted-with-octal-\001""#).unwrap();
950 assert_eq!(name.as_str(), r#""quoted-with-octal-\001""#);
951 assert_eq!(name.as_path(), Path::new("quoted-with-octal-\x01"));
952 assert_eq!(name.as_bytes(), b"quoted-with-octal-\x01");
953 assert_eq!(name.to_string(), r#""quoted-with-octal-\001""#);
954 }
955
956 #[test]
957 fn test_filename_parse_quoted_invalid() {
958 let err = FileName::new(r#""quoted-with-trailing-backslash-\""#).unwrap_err();
959 if let FileNameError::TrailingBackslash = err {
960 } else {
962 panic!("unexpected error: {:?}", err);
963 }
964
965 let err = FileName::new(r#""quoted-with-bad-escape-\x""#).unwrap_err();
966 if let FileNameError::InvalidEscape(ch) = err {
967 assert_eq!(ch, b'x');
968 } else {
969 panic!("unexpected error: {:?}", err);
970 }
971
972 let err = FileName::new(r#""quoted-with-bad-leading-octal-\400""#).unwrap_err();
973 if let FileNameError::InvalidLeadingOctalDigit(ch) = err {
974 assert_eq!(ch, b'4');
975 } else {
976 panic!("unexpected error: {:?}", err);
977 }
978
979 let err = FileName::new(r#""quoted-with-bad-leading-octal-\500""#).unwrap_err();
980 if let FileNameError::InvalidLeadingOctalDigit(ch) = err {
981 assert_eq!(ch, b'5');
982 } else {
983 panic!("unexpected error: {:?}", err);
984 }
985
986 let err = FileName::new(r#""quoted-with-bad-leading-octal-\600""#).unwrap_err();
987 if let FileNameError::InvalidLeadingOctalDigit(ch) = err {
988 assert_eq!(ch, b'6');
989 } else {
990 panic!("unexpected error: {:?}", err);
991 }
992
993 let err = FileName::new(r#""quoted-with-bad-leading-octal-\700""#).unwrap_err();
994 if let FileNameError::InvalidLeadingOctalDigit(ch) = err {
995 assert_eq!(ch, b'7');
996 } else {
997 panic!("unexpected error: {:?}", err);
998 }
999
1000 let err = FileName::new(r#""quoted-with-bad-octal-\009""#).unwrap_err();
1001 if let FileNameError::InvalidOctalDigit(ch) = err {
1002 assert_eq!(ch, b'9');
1003 } else {
1004 panic!("unexpected error: {:?}", err);
1005 }
1006 }
1007
1008 #[test]
1009 fn test_filename_partial_eq() {
1010 let fn_a1 = FileName::new("a").unwrap();
1011 let fn_a2 = FileName::new("a").unwrap();
1012 let fn_b = FileName::new("b").unwrap();
1013
1014 assert_eq!(fn_a1, fn_a2);
1015 assert_ne!(fn_a1, fn_b);
1016 assert_ne!(fn_a2, fn_b);
1017 }
1018
1019 const BAD_COMMIT: &str = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
1020
1021 #[test]
1022 fn test_bad_commit() {
1023 let ctx = make_context();
1024
1025 let err = Commit::new(&ctx, &CommitId::new(BAD_COMMIT)).unwrap_err();
1026
1027 if let CommitError::CommitMetadata {
1028 ref_,
1029 output,
1030 } = err
1031 {
1032 assert_eq!(ref_.as_str(), BAD_COMMIT);
1033 assert!(
1034 output.contains("bad object deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
1035 "unexpected error output: {}",
1036 output,
1037 );
1038 } else {
1039 panic!("unexpected error: {:?}", err);
1040 }
1041 }
1042
1043 #[test]
1044 fn test_bad_commit_extract_diffs() {
1045 let ctx = make_context();
1046
1047 let err = commit::extract_diffs(&ctx, None, &CommitId::new(BAD_COMMIT)).unwrap_err();
1048
1049 if let CommitError::DiffTree {
1050 commit,
1051 base,
1052 output,
1053 } = err
1054 {
1055 assert_eq!(commit.as_str(), BAD_COMMIT);
1056 assert_eq!(base, None);
1057 assert!(
1058 output.contains("bad object deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
1059 "unexpected error output: {}",
1060 output,
1061 );
1062 } else {
1063 panic!("unexpected error: {:?}", err);
1064 }
1065 }
1066
1067 #[test]
1068 fn test_bad_commit_extract_diff_patch() {
1069 let ctx = make_context();
1070 let file_path = Path::new("LICENSE-MIT");
1071
1072 let err = commit::file_patch(&ctx, None, &CommitId::new(BAD_COMMIT), file_path.as_ref())
1073 .unwrap_err();
1074
1075 if let CommitError::DiffPatch {
1076 commit,
1077 base,
1078 path,
1079 output,
1080 } = err
1081 {
1082 assert_eq!(commit.as_str(), BAD_COMMIT);
1083 assert_eq!(base, None);
1084 assert_eq!(path, file_path);
1085 assert!(
1086 output.contains("bad object deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
1087 "unexpected error output: {}",
1088 output,
1089 );
1090 } else {
1091 panic!("unexpected error: {:?}", err);
1092 }
1093 }
1094
1095 const ROOT_COMMIT: &str = "7531e6df007ca1130e5d64b8627b3288844e38a4";
1096
1097 #[test]
1098 fn test_bad_path_extract_diff_patch() {
1099 let ctx = make_context();
1100 let file_path = Path::new("noexist");
1101
1102 let patch = commit::file_patch(&ctx, None, &CommitId::new(ROOT_COMMIT), file_path.as_ref())
1103 .unwrap();
1104
1105 assert_eq!(patch, "");
1106 }
1107
1108 #[test]
1109 fn test_commit_root() {
1110 let ctx = make_context();
1111
1112 let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
1113 let commit = Commit::new(&ctx, &CommitId::new(ROOT_COMMIT)).unwrap();
1114
1115 assert_eq!(commit.sha1.as_str(), ROOT_COMMIT);
1116 assert_eq!(commit.message, "license: add Apache v2 + MIT licenses\n");
1117 assert_eq!(commit.parents.len(), 0);
1118 assert_eq!(commit.author, ben);
1119 assert_eq!(commit.committer, ben);
1120 assert_eq!(commit.sha1(), Some(&CommitId::new(ROOT_COMMIT)));
1121
1122 assert_eq!(commit.diffs.len(), 2);
1123
1124 compare_diffs(
1125 &commit.diffs[0],
1126 &DiffInfo {
1127 old_mode: "000000".into(),
1128 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1129 new_mode: "100644".into(),
1130 new_blob: CommitId::new("16fe87b06e802f094b3fbb0894b137bca2b16ef1"),
1131 name: FileName::Normal("LICENSE-APACHE".into()),
1132 status: StatusChange::Added,
1133 },
1134 );
1135 compare_diffs(
1136 &commit.diffs[1],
1137 &DiffInfo {
1138 old_mode: "000000".into(),
1139 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1140 new_mode: "100644".into(),
1141 new_blob: CommitId::new("8f6f4b4a936616349e1f2846632c95cff33a3c5d"),
1142 name: FileName::Normal("LICENSE-MIT".into()),
1143 status: StatusChange::Added,
1144 },
1145 );
1146 }
1147
1148 const SECOND_COMMIT: &str = "7f0d58bf407b0c80a9daadae88e7541f152ef371";
1149
1150 #[test]
1151 fn test_commit_regular() {
1152 let ctx = make_context();
1153
1154 let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
1155 let commit = Commit::new(&ctx, &CommitId::new(SECOND_COMMIT)).unwrap();
1156
1157 assert_eq!(commit.sha1.as_str(), SECOND_COMMIT);
1158 assert_eq!(commit.message, "cargo: add boilerplate\n");
1159 assert_eq!(commit.parents.len(), 1);
1160 assert_eq!(commit.parents[0].as_str(), ROOT_COMMIT);
1161 assert_eq!(commit.author, ben);
1162 assert_eq!(commit.committer, ben);
1163 assert_eq!(commit.sha1(), Some(&CommitId::new(SECOND_COMMIT)));
1164
1165 assert_eq!(commit.diffs.len(), 4);
1166
1167 compare_diffs(
1168 &commit.diffs[0],
1169 &DiffInfo {
1170 old_mode: "000000".into(),
1171 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1172 new_mode: "100644".into(),
1173 new_blob: CommitId::new("fa8d85ac52f19959d6fc9942c265708b4b3c2b04"),
1174 name: FileName::Normal(".gitignore".into()),
1175 status: StatusChange::Added,
1176 },
1177 );
1178
1179 compare_diffs(
1180 &commit.diffs[1],
1181 &DiffInfo {
1182 old_mode: "000000".into(),
1183 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1184 new_mode: "100644".into(),
1185 new_blob: CommitId::new("e31460ae61b1d69c6d0a58f9b74ae94d463c49bb"),
1186 name: FileName::Normal("Cargo.toml".into()),
1187 status: StatusChange::Added,
1188 },
1189 );
1190
1191 compare_diffs(
1192 &commit.diffs[2],
1193 &DiffInfo {
1194 old_mode: "000000".into(),
1195 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1196 new_mode: "100644".into(),
1197 new_blob: CommitId::new("b3c9194ad2d95d1498361f70a5480f0433fad1bb"),
1198 name: FileName::Normal("rustfmt.toml".into()),
1199 status: StatusChange::Added,
1200 },
1201 );
1202
1203 compare_diffs(
1204 &commit.diffs[3],
1205 &DiffInfo {
1206 old_mode: "000000".into(),
1207 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1208 new_mode: "100644".into(),
1209 new_blob: CommitId::new("584691748770380d5b70deb5f489b0c09229404e"),
1210 name: FileName::Normal("src/lib.rs".into()),
1211 status: StatusChange::Added,
1212 },
1213 );
1214 }
1215
1216 const CHANGES_COMMIT: &str = "5fa80c2ab7df3c1d18c5ff7840a1ef051a1a1f21";
1217 const CHANGES_COMMIT_PARENT: &str = "89e44cb662b4a2e6b8f0333f84e5cc4ac818cc96";
1218
1219 #[test]
1220 fn test_commit_regular_with_changes() {
1221 let ctx = make_context();
1222
1223 let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
1224 let commit = Commit::new(&ctx, &CommitId::new(CHANGES_COMMIT)).unwrap();
1225
1226 assert_eq!(commit.sha1.as_str(), CHANGES_COMMIT);
1227 assert_eq!(
1228 commit.message,
1229 "Identity: use over strings where it matters\n",
1230 );
1231 assert_eq!(commit.parents.len(), 1);
1232 assert_eq!(commit.parents[0].as_str(), CHANGES_COMMIT_PARENT);
1233 assert_eq!(commit.author, ben);
1234 assert_eq!(commit.committer, ben);
1235 assert_eq!(commit.sha1(), Some(&CommitId::new(CHANGES_COMMIT)));
1236
1237 assert_eq!(commit.diffs.len(), 2);
1238
1239 compare_diffs(
1240 &commit.diffs[0],
1241 &DiffInfo {
1242 old_mode: "100644".into(),
1243 old_blob: CommitId::new("5710fe019e3de5e07f2bd1a5bcfd2d462834e9d8"),
1244 new_mode: "100644".into(),
1245 new_blob: CommitId::new("db2811cc69a6fda16f6ef5d08cfc7780b46331d7"),
1246 name: FileName::Normal("src/context.rs".into()),
1247 status: StatusChange::Modified(None),
1248 },
1249 );
1250
1251 compare_diffs(
1252 &commit.diffs[1],
1253 &DiffInfo {
1254 old_mode: "100644".into(),
1255 old_blob: CommitId::new("2acafbc5acd2e7b70260a3e39a1a752cc46cf953"),
1256 new_mode: "100644".into(),
1257 new_blob: CommitId::new("af3a45a85ac6ba0d026279adac509c56308a3e26"),
1258 name: FileName::Normal("src/run.rs".into()),
1259 status: StatusChange::Modified(None),
1260 },
1261 );
1262 }
1263
1264 #[test]
1265 fn test_topic_regular_with_changes() {
1266 let ctx = make_context();
1267
1268 let topic = Topic::new(
1269 &ctx,
1270 &CommitId::new(ROOT_COMMIT),
1271 &CommitId::new(CHANGES_COMMIT),
1272 )
1273 .unwrap();
1274
1275 assert_eq!(topic.base.as_str(), ROOT_COMMIT);
1276 assert_eq!(topic.sha1.as_str(), CHANGES_COMMIT);
1277 assert_eq!(topic.sha1(), None);
1278
1279 assert_eq!(topic.diffs.len(), 8);
1280
1281 compare_diffs(
1282 &topic.diffs[0],
1283 &DiffInfo {
1284 old_mode: "000000".into(),
1285 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1286 new_mode: "100644".into(),
1287 new_blob: CommitId::new("fa8d85ac52f19959d6fc9942c265708b4b3c2b04"),
1288 name: FileName::Normal(".gitignore".into()),
1289 status: StatusChange::Added,
1290 },
1291 );
1292
1293 compare_diffs(
1294 &topic.diffs[1],
1295 &DiffInfo {
1296 old_mode: "000000".into(),
1297 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1298 new_mode: "100644".into(),
1299 new_blob: CommitId::new("eadf0e09676dae16dbdf2c51d12f237e0ec663c6"),
1300 name: FileName::Normal("Cargo.toml".into()),
1301 status: StatusChange::Added,
1302 },
1303 );
1304
1305 compare_diffs(
1306 &topic.diffs[2],
1307 &DiffInfo {
1308 old_mode: "000000".into(),
1309 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1310 new_mode: "100644".into(),
1311 new_blob: CommitId::new("b3c9194ad2d95d1498361f70a5480f0433fad1bb"),
1312 name: FileName::Normal("rustfmt.toml".into()),
1313 status: StatusChange::Added,
1314 },
1315 );
1316
1317 compare_diffs(
1318 &topic.diffs[3],
1319 &DiffInfo {
1320 old_mode: "000000".into(),
1321 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1322 new_mode: "100644".into(),
1323 new_blob: CommitId::new("b43524e95861f3dccb3b1b2ce42e004ff8ba53cc"),
1324 name: FileName::Normal("src/commit.rs".into()),
1325 status: StatusChange::Added,
1326 },
1327 );
1328
1329 compare_diffs(
1330 &topic.diffs[4],
1331 &DiffInfo {
1332 old_mode: "000000".into(),
1333 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1334 new_mode: "100644".into(),
1335 new_blob: CommitId::new("db2811cc69a6fda16f6ef5d08cfc7780b46331d7"),
1336 name: FileName::Normal("src/context.rs".into()),
1337 status: StatusChange::Added,
1338 },
1339 );
1340
1341 compare_diffs(
1342 &topic.diffs[5],
1343 &DiffInfo {
1344 old_mode: "000000".into(),
1345 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1346 new_mode: "100644".into(),
1347 new_blob: CommitId::new("ea5b00b30d7caaf2ceb7396788b4f0493d84d282"),
1348 name: FileName::Normal("src/hook.rs".into()),
1349 status: StatusChange::Added,
1350 },
1351 );
1352
1353 compare_diffs(
1354 &topic.diffs[6],
1355 &DiffInfo {
1356 old_mode: "000000".into(),
1357 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1358 new_mode: "100644".into(),
1359 new_blob: CommitId::new("1784b5969f5948334d6c29b7050bb97c54f3fe8f"),
1360 name: FileName::Normal("src/lib.rs".into()),
1361 status: StatusChange::Added,
1362 },
1363 );
1364
1365 compare_diffs(
1366 &topic.diffs[7],
1367 &DiffInfo {
1368 old_mode: "000000".into(),
1369 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1370 new_mode: "100644".into(),
1371 new_blob: CommitId::new("af3a45a85ac6ba0d026279adac509c56308a3e26"),
1372 name: FileName::Normal("src/run.rs".into()),
1373 status: StatusChange::Added,
1374 },
1375 );
1376 }
1377
1378 const QUOTED_PATHS: &str = "f536f44cf96b82e479d4973d5ea1cf78058bd1fb";
1379 const QUOTED_PATHS_PARENT: &str = "1b2c7b3dad2fb7d14fbf0b619bf7faca4dd01243";
1380
1381 fn quoted_filename(raw: &[u8], readable: &str) -> FileName {
1382 FileName::Quoted {
1383 raw: raw.into(),
1384 name: readable.into(),
1385 }
1386 }
1387
1388 #[test]
1389 fn test_commit_quoted_paths() {
1390 let ctx = make_context();
1391
1392 let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
1393 let commit = Commit::new(&ctx, &CommitId::new(QUOTED_PATHS)).unwrap();
1394
1395 assert_eq!(commit.sha1.as_str(), QUOTED_PATHS);
1396 assert_eq!(
1397 commit.message,
1398 "commit with invalid characters in path names\n",
1399 );
1400 assert_eq!(commit.parents.len(), 1);
1401 assert_eq!(commit.parents[0].as_str(), QUOTED_PATHS_PARENT);
1402 assert_eq!(commit.author, ben);
1403 assert_eq!(commit.committer, ben);
1404 assert_eq!(commit.sha1(), Some(&CommitId::new(QUOTED_PATHS)));
1405
1406 assert_eq!(commit.diffs.len(), 6);
1407
1408 compare_diffs(
1409 &commit.diffs[0],
1410 &DiffInfo {
1411 old_mode: "000000".into(),
1412 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1413 new_mode: "100644".into(),
1414 new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
1415 name: quoted_filename(b"control-character-\x03", r#""control-character-\003""#),
1416 status: StatusChange::Added,
1417 },
1418 );
1419
1420 compare_diffs(
1421 &commit.diffs[1],
1422 &DiffInfo {
1423 old_mode: "000000".into(),
1424 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1425 new_mode: "100644".into(),
1426 new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
1427 name: quoted_filename(b"invalid-utf8-\x80", r#""invalid-utf8-\200""#),
1428 status: StatusChange::Added,
1429 },
1430 );
1431
1432 compare_diffs(
1433 &commit.diffs[2],
1434 &DiffInfo {
1435 old_mode: "000000".into(),
1436 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1437 new_mode: "100644".into(),
1438 new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
1439 name: quoted_filename("non-ascii-é".as_bytes(), r#""non-ascii-\303\251""#),
1440 status: StatusChange::Added,
1441 },
1442 );
1443
1444 compare_diffs(
1445 &commit.diffs[3],
1446 &DiffInfo {
1447 old_mode: "000000".into(),
1448 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1449 new_mode: "100644".into(),
1450 new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
1451 name: quoted_filename(b"with whitespace", "with whitespace"),
1452 status: StatusChange::Added,
1453 },
1454 );
1455
1456 compare_diffs(
1457 &commit.diffs[4],
1458 &DiffInfo {
1459 old_mode: "000000".into(),
1460 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1461 new_mode: "100644".into(),
1462 new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
1463 name: quoted_filename(b"with-dollar-$", "with-dollar-$"),
1464 status: StatusChange::Added,
1465 },
1466 );
1467
1468 compare_diffs(
1469 &commit.diffs[5],
1470 &DiffInfo {
1471 old_mode: "000000".into(),
1472 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1473 new_mode: "100644".into(),
1474 new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
1475 name: quoted_filename(b"with-quote-\"", r#""with-quote-\"""#),
1476 status: StatusChange::Added,
1477 },
1478 );
1479 }
1480
1481 #[test]
1482 fn test_commit_quoted_paths_with_bad_config() {
1483 let raw_ctx = make_context();
1484 let tempdir = test::make_temp_dir();
1485 let config_path = tempdir.path().join("config");
1486 let config = raw_ctx
1487 .git()
1488 .arg("config")
1489 .args([OsStr::new("-f"), config_path.as_ref()])
1490 .arg("core.quotePath")
1491 .arg("false")
1492 .output()
1493 .unwrap();
1494 if !config.status.success() {
1495 panic!(
1496 "setting core.quotePath failed: {}",
1497 String::from_utf8_lossy(&config.stderr),
1498 );
1499 }
1500 let ctx = GitContext::new_with_config(raw_ctx.gitdir(), config_path);
1501
1502 let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
1503 let commit = Commit::new(&ctx, &CommitId::new(QUOTED_PATHS)).unwrap();
1504
1505 assert_eq!(commit.sha1.as_str(), QUOTED_PATHS);
1506 assert_eq!(
1507 commit.message,
1508 "commit with invalid characters in path names\n",
1509 );
1510 assert_eq!(commit.parents.len(), 1);
1511 assert_eq!(commit.parents[0].as_str(), QUOTED_PATHS_PARENT);
1512 assert_eq!(commit.author, ben);
1513 assert_eq!(commit.committer, ben);
1514 assert_eq!(commit.sha1(), Some(&CommitId::new(QUOTED_PATHS)));
1515
1516 assert_eq!(commit.diffs.len(), 6);
1517
1518 compare_diffs(
1519 &commit.diffs[0],
1520 &DiffInfo {
1521 old_mode: "000000".into(),
1522 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1523 new_mode: "100644".into(),
1524 new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
1525 name: quoted_filename(b"control-character-\x03", r#""control-character-\003""#),
1526 status: StatusChange::Added,
1527 },
1528 );
1529
1530 compare_diffs(
1531 &commit.diffs[1],
1532 &DiffInfo {
1533 old_mode: "000000".into(),
1534 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1535 new_mode: "100644".into(),
1536 new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
1537 name: quoted_filename(b"invalid-utf8-\x80", r#""invalid-utf8-\200""#),
1538 status: StatusChange::Added,
1539 },
1540 );
1541
1542 compare_diffs(
1543 &commit.diffs[2],
1544 &DiffInfo {
1545 old_mode: "000000".into(),
1546 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1547 new_mode: "100644".into(),
1548 new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
1549 name: quoted_filename("non-ascii-é".as_bytes(), r#""non-ascii-\303\251""#),
1550 status: StatusChange::Added,
1551 },
1552 );
1553
1554 compare_diffs(
1555 &commit.diffs[3],
1556 &DiffInfo {
1557 old_mode: "000000".into(),
1558 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1559 new_mode: "100644".into(),
1560 new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
1561 name: quoted_filename(b"with whitespace", "with whitespace"),
1562 status: StatusChange::Added,
1563 },
1564 );
1565
1566 compare_diffs(
1567 &commit.diffs[4],
1568 &DiffInfo {
1569 old_mode: "000000".into(),
1570 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1571 new_mode: "100644".into(),
1572 new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
1573 name: quoted_filename(b"with-dollar-$", "with-dollar-$"),
1574 status: StatusChange::Added,
1575 },
1576 );
1577
1578 compare_diffs(
1579 &commit.diffs[5],
1580 &DiffInfo {
1581 old_mode: "000000".into(),
1582 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1583 new_mode: "100644".into(),
1584 new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
1585 name: quoted_filename(b"with-quote-\"", r#""with-quote-\"""#),
1586 status: StatusChange::Added,
1587 },
1588 );
1589 }
1590
1591 const REGULAR_MERGE_COMMIT: &str = "c58dd19d9976722d82aa6bc6a52a2a01a52bd9e8";
1592 const REGULAR_MERGE_PARENT1: &str = "3a22ca19fda09183da2faab60819ff6807568acd";
1593 const REGULAR_MERGE_PARENT2: &str = "d02f015907371738253a22b9a7fec78607a969b2";
1594
1595 #[test]
1596 fn test_commit_merge() {
1597 let ctx = make_context();
1598
1599 let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
1600 let commit = Commit::new(&ctx, &CommitId::new(REGULAR_MERGE_COMMIT)).unwrap();
1601
1602 assert_eq!(commit.sha1.as_str(), REGULAR_MERGE_COMMIT);
1603 assert_eq!(
1604 commit.message,
1605 "Merge commit 'd02f015907371738253a22b9a7fec78607a969b2' into \
1606 tests/commit/merge\n\n* commit 'd02f015907371738253a22b9a7fec78607a969b2':\n \
1607 test-refs: non-root commit\n",
1608 );
1609 assert_eq!(commit.parents.len(), 2);
1610 assert_eq!(commit.parents[0].as_str(), REGULAR_MERGE_PARENT1);
1611 assert_eq!(commit.parents[1].as_str(), REGULAR_MERGE_PARENT2);
1612 assert_eq!(commit.author, ben);
1613 assert_eq!(commit.committer, ben);
1614 assert_eq!(commit.sha1(), Some(&CommitId::new(REGULAR_MERGE_COMMIT)));
1615
1616 assert_eq!(commit.diffs.len(), 0);
1617 }
1618
1619 const EVIL_MERGE_DIFFS: &str = "add18e5ab9a67303337cb2754c675fb2e0a45a79";
1620 const EVIL_MERGE_DIFFS_PARENT1: &str = "543322ad2b14bc455e4d10f3aac9b8c1c7aca3bd";
1621 const EVIL_MERGE_DIFFS_PARENT2: &str = "3c1ca95f1ac7cdef19dc87b9984f76c1761ac7c6";
1622
1623 #[test]
1624 fn test_evil_merge_diffs() {
1625 let ctx = make_context();
1626
1627 let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
1628 let commit = Commit::new(&ctx, &CommitId::new(EVIL_MERGE_DIFFS)).unwrap();
1629
1630 assert_eq!(commit.sha1.as_str(), EVIL_MERGE_DIFFS);
1631 assert_eq!(
1632 commit.message,
1633 "Merge branch 'upstream-check_size' into third-party-base-update-evil\n\n\
1634 * upstream-check_size:\n check_size 2016-07-09 (112e9b34)\n",
1635 );
1636 assert_eq!(commit.parents.len(), 2);
1637 assert_eq!(commit.parents[0].as_str(), EVIL_MERGE_DIFFS_PARENT1);
1638 assert_eq!(commit.parents[1].as_str(), EVIL_MERGE_DIFFS_PARENT2);
1639 assert_eq!(commit.author, ben);
1640 assert_eq!(commit.committer, ben);
1641 assert_eq!(commit.sha1(), Some(&CommitId::new(EVIL_MERGE_DIFFS)));
1642
1643 assert_eq!(commit.diffs.len(), 6);
1644
1645 compare_diffs(
1646 &commit.diffs[0],
1647 &DiffInfo {
1648 old_mode: "100644".into(),
1649 old_blob: CommitId::new("431f4196e62bb1ae394469c6ddd7c8f3d3e8dbd6"),
1650 new_mode: "100644".into(),
1651 new_blob: CommitId::new("8047698eb7556bd35bb1667de3e64243bb601eaa"),
1652 name: FileName::Normal("check_size/.gitattributes".into()),
1653 status: StatusChange::Modified(None),
1654 },
1655 );
1656
1657 compare_diffs(
1658 &commit.diffs[1],
1659 &DiffInfo {
1660 old_mode: "000000".into(),
1661 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1662 new_mode: "100644".into(),
1663 new_blob: CommitId::new("8047698eb7556bd35bb1667de3e64243bb601eaa"),
1664 name: FileName::Normal("check_size/.gitattributes".into()),
1665 status: StatusChange::Added,
1666 },
1667 );
1668
1669 compare_diffs(
1670 &commit.diffs[2],
1671 &DiffInfo {
1672 old_mode: "000000".into(),
1673 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1674 new_mode: "100644".into(),
1675 new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
1676 name: FileName::Normal("check_size/bad-attr-value".into()),
1677 status: StatusChange::Added,
1678 },
1679 );
1680
1681 compare_diffs(
1682 &commit.diffs[3],
1683 &DiffInfo {
1684 old_mode: "000000".into(),
1685 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1686 new_mode: "100644".into(),
1687 new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
1688 name: FileName::Normal("check_size/bad-attr-value".into()),
1689 status: StatusChange::Added,
1690 },
1691 );
1692
1693 compare_diffs(
1694 &commit.diffs[4],
1695 &DiffInfo {
1696 old_mode: "100644".into(),
1697 old_blob: CommitId::new("289aa5399387180f356b4beab60fb2f16207a7d7"),
1698 new_mode: "100644".into(),
1699 new_blob: CommitId::new("7b5c7a2fe126cd3476d993c40525736c45796a60"),
1700 name: FileName::Normal("check_size/increased-size".into()),
1701 status: StatusChange::Modified(None),
1702 },
1703 );
1704
1705 compare_diffs(
1706 &commit.diffs[5],
1707 &DiffInfo {
1708 old_mode: "000000".into(),
1709 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1710 new_mode: "100644".into(),
1711 new_blob: CommitId::new("7b5c7a2fe126cd3476d993c40525736c45796a60"),
1712 name: FileName::Normal("check_size/increased-size".into()),
1713 status: StatusChange::Added,
1714 },
1715 );
1716 }
1717
1718 #[test]
1719 fn test_evil_merge_modified_files() {
1720 let ctx = make_context();
1721
1722 let commit = Commit::new(&ctx, &CommitId::new(EVIL_MERGE_DIFFS)).unwrap();
1723 let modified_files = commit.modified_files();
1724
1725 assert_eq!(modified_files.len(), 3);
1726 assert_eq!(modified_files[0].as_str(), "check_size/.gitattributes");
1727 assert_eq!(modified_files[1].as_str(), "check_size/bad-attr-value");
1728 assert_eq!(modified_files[2].as_str(), "check_size/increased-size");
1729 }
1730
1731 const NO_HISTORY_MERGE_COMMIT: &str = "018ef4b25b978e194712e57a8f71d67427ecc065";
1732 const NO_HISTORY_MERGE_PARENT1: &str = "969b794ea82ce51d1555852de33bfcb63dfec969";
1733 const NO_HISTORY_MERGE_PARENT2: &str = "8805c7ae76329a937960448908f4618214fd3546";
1734
1735 #[test]
1736 fn test_commit_merge_no_history() {
1737 let ctx = make_context();
1738
1739 let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
1740 let commit = Commit::new(&ctx, &CommitId::new(NO_HISTORY_MERGE_COMMIT)).unwrap();
1741
1742 assert_eq!(commit.sha1.as_str(), NO_HISTORY_MERGE_COMMIT);
1743 assert_eq!(
1744 commit.message,
1745 "Merge branch 'tests/commit/no_history' into tests/commit/merge\n\n* \
1746 tests/commit/no_history:\n tests: a commit with new history\n",
1747 );
1748 assert_eq!(commit.parents.len(), 2);
1749 assert_eq!(commit.parents[0].as_str(), NO_HISTORY_MERGE_PARENT1);
1750 assert_eq!(commit.parents[1].as_str(), NO_HISTORY_MERGE_PARENT2);
1751 assert_eq!(commit.author, ben);
1752 assert_eq!(commit.committer, ben);
1753 assert_eq!(commit.sha1(), Some(&CommitId::new(NO_HISTORY_MERGE_COMMIT)));
1754
1755 assert_eq!(commit.diffs.len(), 0);
1756 }
1757
1758 const CONFLICT_MERGE_COMMIT: &str = "969b794ea82ce51d1555852de33bfcb63dfec969";
1759 const CONFLICT_MERGE_PARENT1: &str = "fb11c6970f4aff1e16ea85ba17529377b62310b7";
1760 const CONFLICT_MERGE_PARENT2: &str = "bed4760e60c8276e6db53c6c2b641933d2938510";
1761
1762 #[test]
1763 fn test_commit_merge_conflict() {
1764 let ctx = make_context();
1765
1766 let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
1767 let commit = Commit::new(&ctx, &CommitId::new(CONFLICT_MERGE_COMMIT)).unwrap();
1768
1769 assert_eq!(commit.sha1.as_str(), CONFLICT_MERGE_COMMIT);
1770 assert_eq!(
1771 commit.message,
1772 "Merge branch 'test/commit/merge/conflict' into tests/commit/merge\n\n* \
1773 test/commit/merge/conflict:\n content: cause some conflicts\n",
1774 );
1775 assert_eq!(commit.parents.len(), 2);
1776 assert_eq!(commit.parents[0].as_str(), CONFLICT_MERGE_PARENT1);
1777 assert_eq!(commit.parents[1].as_str(), CONFLICT_MERGE_PARENT2);
1778 assert_eq!(commit.author, ben);
1779 assert_eq!(commit.committer, ben);
1780 assert_eq!(commit.sha1(), Some(&CommitId::new(CONFLICT_MERGE_COMMIT)));
1781
1782 assert_eq!(commit.diffs.len(), 2);
1783
1784 compare_diffs(
1785 &commit.diffs[0],
1786 &DiffInfo {
1787 old_mode: "100644".into(),
1788 old_blob: CommitId::new("829636d299136b6651fd19aa6ac3500c7d50bf10"),
1789 new_mode: "100644".into(),
1790 new_blob: CommitId::new("195cc9d0aeb7324584700f2d17940d6a218a36f2"),
1791 name: FileName::Normal("content".into()),
1792 status: StatusChange::Modified(None),
1793 },
1794 );
1795
1796 compare_diffs(
1797 &commit.diffs[1],
1798 &DiffInfo {
1799 old_mode: "100644".into(),
1800 old_blob: CommitId::new("925b35841fdd59e002bc5b3e685dc158bdbe6ebf"),
1801 new_mode: "100644".into(),
1802 new_blob: CommitId::new("195cc9d0aeb7324584700f2d17940d6a218a36f2"),
1803 name: FileName::Normal("content".into()),
1804 status: StatusChange::Modified(None),
1805 },
1806 );
1807 }
1808
1809 const EVIL_MERGE_COMMIT: &str = "fb11c6970f4aff1e16ea85ba17529377b62310b7";
1810 const EVIL_MERGE_PARENT1: &str = "c58dd19d9976722d82aa6bc6a52a2a01a52bd9e8";
1811 const EVIL_MERGE_PARENT2: &str = "27ff3ef5532d76afa046f76f4dd8f588dc3e83c3";
1812
1813 #[test]
1814 fn test_commit_merge_evil() {
1815 let ctx = make_context();
1816
1817 let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
1818 let commit = Commit::new(&ctx, &CommitId::new(EVIL_MERGE_COMMIT)).unwrap();
1819
1820 assert_eq!(commit.sha1.as_str(), EVIL_MERGE_COMMIT);
1821 assert_eq!(
1822 commit.message,
1823 "Merge commit '27ff3ef5532d76afa046f76f4dd8f588dc3e83c3' into \
1824 tests/commit/merge\n\n* commit '27ff3ef5532d76afa046f76f4dd8f588dc3e83c3':\n \
1825 test-refs: test target commit\n",
1826 );
1827 assert_eq!(commit.parents.len(), 2);
1828 assert_eq!(commit.parents[0].as_str(), EVIL_MERGE_PARENT1);
1829 assert_eq!(commit.parents[1].as_str(), EVIL_MERGE_PARENT2);
1830 assert_eq!(commit.author, ben);
1831 assert_eq!(commit.committer, ben);
1832 assert_eq!(commit.sha1(), Some(&CommitId::new(EVIL_MERGE_COMMIT)));
1833
1834 assert_eq!(commit.diffs.len(), 2);
1835
1836 compare_diffs(
1837 &commit.diffs[0],
1838 &DiffInfo {
1839 old_mode: "100644".into(),
1840 old_blob: CommitId::new("2ef267e25bd6c6a300bb473e604b092b6a48523b"),
1841 new_mode: "100644".into(),
1842 new_blob: CommitId::new("829636d299136b6651fd19aa6ac3500c7d50bf10"),
1843 name: FileName::Normal("content".into()),
1844 status: StatusChange::Modified(None),
1845 },
1846 );
1847
1848 compare_diffs(
1849 &commit.diffs[1],
1850 &DiffInfo {
1851 old_mode: "000000".into(),
1852 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1853 new_mode: "100644".into(),
1854 new_blob: CommitId::new("829636d299136b6651fd19aa6ac3500c7d50bf10"),
1855 name: FileName::Normal("content".into()),
1856 status: StatusChange::Added,
1857 },
1858 );
1859 }
1860
1861 const SMALL_DIFF_COMMIT: &str = "22324be5cac6a797a3c5f3633e4409840e02eae9";
1862 const SMALL_DIFF_PARENT: &str = "1ff04953f1b8dd7f01ecfe51ee962c47cea50e28";
1863
1864 #[test]
1865 fn test_commit_file_patch() {
1866 let ctx = make_context();
1867
1868 let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
1869 let commit = Commit::new(&ctx, &CommitId::new(SMALL_DIFF_COMMIT)).unwrap();
1870
1871 assert_eq!(commit.sha1.as_str(), SMALL_DIFF_COMMIT);
1872 assert_eq!(commit.message, "commit: use %n rather than \\n\n");
1873 assert_eq!(commit.parents.len(), 1);
1874 assert_eq!(commit.parents[0].as_str(), SMALL_DIFF_PARENT);
1875 assert_eq!(commit.author, ben);
1876 assert_eq!(commit.committer, ben);
1877 assert_eq!(commit.sha1(), Some(&CommitId::new(SMALL_DIFF_COMMIT)));
1878
1879 assert_eq!(commit.diffs.len(), 1);
1880
1881 assert_eq!(
1882 commit.file_patch("src/commit.rs").unwrap(),
1883 concat!(
1884 "diff --git a/src/commit.rs b/src/commit.rs\n",
1885 "index 2cb15f6..6303de0 100644\n",
1886 "--- a/src/commit.rs\n",
1887 "+++ b/src/commit.rs\n",
1888 "@@ -109,7 +109,7 @@ impl Commit {\n",
1889 " pub fn new(ctx: &GitContext, sha1: &str) -> Result<Self, Error> {\n",
1890 " let commit_info = try!(ctx.git()\n",
1891 " .arg(\"log\")\n",
1892 "- .arg(\"--pretty=%P\\n%an\\n%ae\\n%cn\\n%ce\\n%h\")\n",
1893 "+ .arg(\"--pretty=%P%n%an%n%ae%n%cn%n%ce%n%h\")\n",
1894 " .arg(\"-n\").arg(\"1\")\n",
1895 " .arg(sha1)\n",
1896 " .output());\n",
1897 ),
1898 );
1899 }
1900
1901 #[test]
1902 fn test_topic_file_patch() {
1903 let ctx = make_context();
1904
1905 let topic = Topic::new(
1906 &ctx,
1907 &CommitId::new(ROOT_COMMIT),
1908 &CommitId::new(CHANGES_COMMIT),
1909 )
1910 .unwrap();
1911
1912 assert_eq!(topic.base.as_str(), ROOT_COMMIT);
1913 assert_eq!(topic.sha1.as_str(), CHANGES_COMMIT);
1914 assert_eq!(topic.sha1(), None);
1915
1916 assert_eq!(topic.diffs.len(), 8);
1917
1918 assert_eq!(
1919 topic.file_patch(".gitignore").unwrap(),
1920 concat!(
1921 "diff --git a/.gitignore b/.gitignore\n",
1922 "new file mode 100644\n",
1923 "index 0000000..fa8d85a\n",
1924 "--- /dev/null\n",
1925 "+++ b/.gitignore\n",
1926 "@@ -0,0 +1,2 @@\n",
1927 "+Cargo.lock\n",
1928 "+target\n",
1929 ),
1930 );
1931 }
1932
1933 const ADD_BINARY_COMMIT: &str = "ff7ee6b5c822440613a01bfcf704e18cff4def72";
1934 const CHANGE_BINARY_COMMIT: &str = "8f25adf0f878bdf88b609ca7ef67f235c5237602";
1935 const ADD_BINARY_PARENT: &str = "1b2c7b3dad2fb7d14fbf0b619bf7faca4dd01243";
1936
1937 #[test]
1938 fn test_commit_file_patch_binary_add() {
1939 let ctx = make_context();
1940
1941 let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
1942 let commit = Commit::new(&ctx, &CommitId::new(ADD_BINARY_COMMIT)).unwrap();
1943
1944 assert_eq!(commit.sha1.as_str(), ADD_BINARY_COMMIT);
1945 assert_eq!(commit.message, "binary-data: add some binary data\n");
1946 assert_eq!(commit.parents.len(), 1);
1947 assert_eq!(commit.parents[0].as_str(), ADD_BINARY_PARENT);
1948 assert_eq!(commit.author, ben);
1949 assert_eq!(commit.committer, ben);
1950 assert_eq!(commit.sha1(), Some(&CommitId::new(ADD_BINARY_COMMIT)));
1951
1952 assert_eq!(commit.diffs.len(), 1);
1953
1954 compare_diffs(
1955 &commit.diffs[0],
1956 &DiffInfo {
1957 old_mode: "000000".into(),
1958 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1959 new_mode: "100644".into(),
1960 new_blob: CommitId::new("7624577a217a08f1103d6c190eb11beab1081744"),
1961 name: FileName::Normal("binary-data".into()),
1962 status: StatusChange::Added,
1963 },
1964 );
1965
1966 assert_eq!(
1967 commit.file_patch("binary-data").unwrap(),
1968 concat!(
1969 "diff --git a/binary-data b/binary-data\n",
1970 "new file mode 100644\n",
1971 "index 0000000..7624577\n",
1972 "Binary files /dev/null and b/binary-data differ\n",
1973 ),
1974 );
1975 }
1976
1977 #[test]
1978 fn test_topic_file_patch_binary_add() {
1979 let ctx = make_context();
1980
1981 let topic = Topic::new(
1982 &ctx,
1983 &CommitId::new(ADD_BINARY_PARENT),
1984 &CommitId::new(CHANGE_BINARY_COMMIT),
1985 )
1986 .unwrap();
1987
1988 assert_eq!(topic.base.as_str(), ADD_BINARY_PARENT);
1989 assert_eq!(topic.sha1.as_str(), CHANGE_BINARY_COMMIT);
1990 assert_eq!(topic.sha1(), None);
1991
1992 assert_eq!(topic.diffs.len(), 1);
1993
1994 compare_diffs(
1995 &topic.diffs[0],
1996 &DiffInfo {
1997 old_mode: "000000".into(),
1998 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
1999 new_mode: "100644".into(),
2000 new_blob: CommitId::new("53c235a627abeda83e26d6d53cdebf72b52f3c3d"),
2001 name: FileName::Normal("binary-data".into()),
2002 status: StatusChange::Added,
2003 },
2004 );
2005
2006 assert_eq!(
2007 topic.file_patch("binary-data").unwrap(),
2008 concat!(
2009 "diff --git a/binary-data b/binary-data\n",
2010 "new file mode 100644\n",
2011 "index 0000000..53c235a\n",
2012 "Binary files /dev/null and b/binary-data differ\n",
2013 ),
2014 );
2015 }
2016
2017 #[test]
2018 fn test_commit_file_patch_binary_change() {
2019 let ctx = make_context();
2020
2021 let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
2022 let commit = Commit::new(&ctx, &CommitId::new(CHANGE_BINARY_COMMIT)).unwrap();
2023
2024 assert_eq!(commit.sha1.as_str(), CHANGE_BINARY_COMMIT);
2025 assert_eq!(commit.message, "binary-data: change some binary data\n");
2026 assert_eq!(commit.parents.len(), 1);
2027 assert_eq!(commit.parents[0].as_str(), ADD_BINARY_COMMIT);
2028 assert_eq!(commit.author, ben);
2029 assert_eq!(commit.committer, ben);
2030 assert_eq!(commit.sha1(), Some(&CommitId::new(CHANGE_BINARY_COMMIT)));
2031
2032 assert_eq!(commit.diffs.len(), 1);
2033
2034 compare_diffs(
2035 &commit.diffs[0],
2036 &DiffInfo {
2037 old_mode: "100644".into(),
2038 old_blob: CommitId::new("7624577a217a08f1103d6c190eb11beab1081744"),
2039 new_mode: "100644".into(),
2040 new_blob: CommitId::new("53c235a627abeda83e26d6d53cdebf72b52f3c3d"),
2041 name: FileName::Normal("binary-data".into()),
2042 status: StatusChange::Modified(None),
2043 },
2044 );
2045
2046 assert_eq!(
2047 commit.file_patch("binary-data").unwrap(),
2048 concat!(
2049 "diff --git a/binary-data b/binary-data\n",
2050 "index 7624577..53c235a 100644\n",
2051 "Binary files a/binary-data and b/binary-data differ\n",
2052 ),
2053 );
2054 }
2055
2056 const TOPIC_ROOT_COMMIT: &str = "1b2c7b3dad2fb7d14fbf0b619bf7faca4dd01243";
2072 const FORK_POINT: &str = "c65b124adb2876f3ce3f328fd22fbc2726607030";
2074 const EXTEND_TARGET: &str = "cc8fa49b9e349250e05967dd6a6be1093da3ac43";
2076 const MERGE_INTO_TARGET: &str = "e590269220be798435848d8428390c980493cc2c";
2078 const SIMPLE_TOPIC: &str = "7876da5555b4efbc32c7df123d5c2171477a6771";
2080 const EXTEND_MERGED_TOPIC: &str = "38d963092fddfd3ef0b38558996c015d01830354";
2082 const CROSS_MERGE_TOPIC: &str = "f194e9af0d6864298e5159152c064d25abaaf858";
2084 const SHARE_ROOT_TOPIC: &str = "3a22ca19fda09183da2faab60819ff6807568acd";
2086
2087 fn test_best_merge_base(target: &str, topic: &str, merge_base: &str) {
2088 let ctx = make_context();
2089
2090 assert_eq!(
2091 Topic::best_merge_base(&ctx, &CommitId::new(target), &CommitId::new(topic)).unwrap(),
2092 CommitId::new(merge_base),
2093 );
2094 }
2095
2096 #[test]
2097 fn test_best_merge_base_extended_target() {
2098 test_best_merge_base(EXTEND_TARGET, SIMPLE_TOPIC, FORK_POINT)
2099 }
2100
2101 #[test]
2102 fn test_best_merge_base_extend_merged_topic() {
2103 test_best_merge_base(MERGE_INTO_TARGET, EXTEND_MERGED_TOPIC, SIMPLE_TOPIC)
2104 }
2105
2106 #[test]
2107 fn test_best_merge_base_share_root() {
2108 test_best_merge_base(EXTEND_TARGET, SHARE_ROOT_TOPIC, TOPIC_ROOT_COMMIT)
2109 }
2110
2111 #[test]
2112 fn test_best_merge_base_cross_merge() {
2113 test_best_merge_base(MERGE_INTO_TARGET, CROSS_MERGE_TOPIC, EXTEND_TARGET)
2114 }
2115
2116 #[test]
2117 fn test_best_merge_base_no_common_commits() {
2118 test_best_merge_base(EXTEND_TARGET, ROOT_COMMIT, EXTEND_TARGET)
2119 }
2120
2121 #[test]
2122 fn test_topic_extended_target() {
2123 let ctx = make_context();
2124
2125 let topic = Topic::new(
2126 &ctx,
2127 &CommitId::new(EXTEND_TARGET),
2128 &CommitId::new(SIMPLE_TOPIC),
2129 )
2130 .unwrap();
2131
2132 assert_eq!(topic.base.as_str(), FORK_POINT);
2133 assert_eq!(topic.sha1.as_str(), SIMPLE_TOPIC);
2134 assert_eq!(topic.sha1(), None);
2135
2136 assert_eq!(topic.diffs.len(), 1);
2137
2138 compare_diffs(
2139 &topic.diffs[0],
2140 &DiffInfo {
2141 old_mode: "000000".into(),
2142 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
2143 new_mode: "100644".into(),
2144 new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
2145 name: FileName::Normal("simple-topic".into()),
2146 status: StatusChange::Added,
2147 },
2148 );
2149
2150 assert_eq!(
2151 topic.file_patch("simple-topic").unwrap(),
2152 concat!(
2153 "diff --git a/simple-topic b/simple-topic\n",
2154 "new file mode 100644\n",
2155 "index 0000000..e69de29\n",
2156 ),
2157 );
2158 }
2159
2160 #[test]
2161 fn test_topic_extended_merged_target() {
2162 let ctx = make_context();
2163
2164 let topic = Topic::new(
2165 &ctx,
2166 &CommitId::new(MERGE_INTO_TARGET),
2167 &CommitId::new(EXTEND_MERGED_TOPIC),
2168 )
2169 .unwrap();
2170
2171 assert_eq!(topic.base.as_str(), SIMPLE_TOPIC);
2172 assert_eq!(topic.sha1.as_str(), EXTEND_MERGED_TOPIC);
2173
2174 assert_eq!(topic.diffs.len(), 1);
2175
2176 compare_diffs(
2177 &topic.diffs[0],
2178 &DiffInfo {
2179 old_mode: "000000".into(),
2180 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
2181 new_mode: "100644".into(),
2182 new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
2183 name: FileName::Normal("pre-merge-topic".into()),
2184 status: StatusChange::Added,
2185 },
2186 );
2187
2188 assert_eq!(
2189 topic.file_patch("pre-merge-topic").unwrap(),
2190 concat!(
2191 "diff --git a/pre-merge-topic b/pre-merge-topic\n",
2192 "new file mode 100644\n",
2193 "index 0000000..e69de29\n",
2194 ),
2195 );
2196 }
2197
2198 #[test]
2199 fn test_topic_share_root() {
2200 let ctx = make_context();
2201
2202 let topic = Topic::new(
2203 &ctx,
2204 &CommitId::new(EXTEND_TARGET),
2205 &CommitId::new(SHARE_ROOT_TOPIC),
2206 )
2207 .unwrap();
2208
2209 assert_eq!(topic.base.as_str(), TOPIC_ROOT_COMMIT);
2210 assert_eq!(topic.sha1.as_str(), SHARE_ROOT_TOPIC);
2211
2212 assert_eq!(topic.diffs.len(), 1);
2213
2214 compare_diffs(
2215 &topic.diffs[0],
2216 &DiffInfo {
2217 old_mode: "000000".into(),
2218 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
2219 new_mode: "100644".into(),
2220 new_blob: CommitId::new("2ef267e25bd6c6a300bb473e604b092b6a48523b"),
2221 name: FileName::Normal("content".into()),
2222 status: StatusChange::Added,
2223 },
2224 );
2225
2226 assert_eq!(
2227 topic.file_patch("content").unwrap(),
2228 concat!(
2229 "diff --git a/content b/content\n",
2230 "new file mode 100644\n",
2231 "index 0000000..2ef267e\n",
2232 "--- /dev/null\n",
2233 "+++ b/content\n",
2234 "@@ -0,0 +1 @@\n",
2235 "+some content\n",
2236 ),
2237 );
2238 }
2239
2240 #[test]
2241 fn test_topic_cross_merge() {
2242 let ctx = make_context();
2243
2244 let topic = Topic::new(
2245 &ctx,
2246 &CommitId::new(MERGE_INTO_TARGET),
2247 &CommitId::new(CROSS_MERGE_TOPIC),
2248 )
2249 .unwrap();
2250
2251 assert_eq!(topic.base.as_str(), EXTEND_TARGET);
2252 assert_eq!(topic.sha1.as_str(), CROSS_MERGE_TOPIC);
2253
2254 assert_eq!(topic.diffs.len(), 2);
2255
2256 compare_diffs(
2257 &topic.diffs[0],
2258 &DiffInfo {
2259 old_mode: "000000".into(),
2260 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
2261 new_mode: "100644".into(),
2262 new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
2263 name: FileName::Normal("pre-merge-topic".into()),
2264 status: StatusChange::Added,
2265 },
2266 );
2267
2268 compare_diffs(
2269 &topic.diffs[1],
2270 &DiffInfo {
2271 old_mode: "000000".into(),
2272 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
2273 new_mode: "100644".into(),
2274 new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
2275 name: FileName::Normal("simple-topic".into()),
2276 status: StatusChange::Added,
2277 },
2278 );
2279
2280 assert_eq!(
2281 topic.file_patch("pre-merge-topic").unwrap(),
2282 concat!(
2283 "diff --git a/pre-merge-topic b/pre-merge-topic\n",
2284 "new file mode 100644\n",
2285 "index 0000000..e69de29\n",
2286 ),
2287 );
2288 assert_eq!(
2289 topic.file_patch("simple-topic").unwrap(),
2290 concat!(
2291 "diff --git a/simple-topic b/simple-topic\n",
2292 "new file mode 100644\n",
2293 "index 0000000..e69de29\n",
2294 ),
2295 );
2296 }
2297
2298 #[test]
2299 fn test_topic_no_common_commits() {
2300 let ctx = make_context();
2301
2302 let topic = Topic::new(
2303 &ctx,
2304 &CommitId::new(EXTEND_TARGET),
2305 &CommitId::new(ROOT_COMMIT),
2306 )
2307 .unwrap();
2308
2309 assert_eq!(topic.base.as_str(), EXTEND_TARGET);
2310 assert_eq!(topic.sha1.as_str(), ROOT_COMMIT);
2311
2312 assert_eq!(topic.diffs.len(), 3);
2313
2314 compare_diffs(
2315 &topic.diffs[0],
2316 &DiffInfo {
2317 old_mode: "000000".into(),
2318 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
2319 new_mode: "100644".into(),
2320 new_blob: CommitId::new("16fe87b06e802f094b3fbb0894b137bca2b16ef1"),
2321 name: FileName::Normal("LICENSE-APACHE".into()),
2322 status: StatusChange::Added,
2323 },
2324 );
2325
2326 compare_diffs(
2327 &topic.diffs[1],
2328 &DiffInfo {
2329 old_mode: "000000".into(),
2330 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
2331 new_mode: "100644".into(),
2332 new_blob: CommitId::new("8f6f4b4a936616349e1f2846632c95cff33a3c5d"),
2333 name: FileName::Normal("LICENSE-MIT".into()),
2334 status: StatusChange::Added,
2335 },
2336 );
2337
2338 compare_diffs(
2339 &topic.diffs[2],
2340 &DiffInfo {
2341 old_mode: "100644".into(),
2342 old_blob: CommitId::new("e4b0cf6006925052f1172d5dea15d9a7aec373f3"),
2343 new_mode: "000000".into(),
2344 new_blob: CommitId::new("0000000000000000000000000000000000000000"),
2345 name: FileName::Normal("content".into()),
2346 status: StatusChange::Deleted,
2347 },
2348 );
2349 }
2350
2351 const ADD_A_COMMIT: &str = "80480d74af8d8a0d3de594606917da06ae518f49";
2365 const ADD_B_COMMIT: &str = "0094392bd07fb7064a98b39791c5aefbcbd5a1e8";
2367 const MERGE_B_INTO_A: &str = "2274b3051218fda7e2efb06ee23d3187a9217941";
2369 const MERGE_A_INTO_B: &str = "6d460df5f502aa4cd855108ec54dcda3b4e86a32";
2371
2372 #[test]
2373 fn test_topic_multiple_merge_base_a_as_base() {
2374 let ctx = make_context();
2375
2376 let topic = Topic::new(
2377 &ctx,
2378 &CommitId::new(MERGE_B_INTO_A),
2379 &CommitId::new(MERGE_A_INTO_B),
2380 )
2381 .unwrap();
2382
2383 assert_eq!(topic.base.as_str(), ADD_A_COMMIT);
2384 assert_eq!(topic.sha1.as_str(), MERGE_A_INTO_B);
2385
2386 assert_eq!(topic.diffs.len(), 1);
2387
2388 compare_diffs(
2389 &topic.diffs[0],
2390 &DiffInfo {
2391 old_mode: "000000".into(),
2392 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
2393 new_mode: "100644".into(),
2394 new_blob: CommitId::new("d95f3ad14dee633a758d2e331151e950dd13e4ed"),
2395 name: FileName::Normal("b".into()),
2396 status: StatusChange::Added,
2397 },
2398 );
2399 }
2400
2401 #[test]
2402 fn test_topic_multiple_merge_base_b_as_base() {
2403 let ctx = make_context();
2404
2405 let topic = Topic::new(
2406 &ctx,
2407 &CommitId::new(MERGE_A_INTO_B),
2408 &CommitId::new(MERGE_B_INTO_A),
2409 )
2410 .unwrap();
2411
2412 assert_eq!(topic.base.as_str(), ADD_B_COMMIT);
2413 assert_eq!(topic.sha1.as_str(), MERGE_B_INTO_A);
2414
2415 assert_eq!(topic.diffs.len(), 1);
2416
2417 compare_diffs(
2418 &topic.diffs[0],
2419 &DiffInfo {
2420 old_mode: "000000".into(),
2421 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
2422 new_mode: "100644".into(),
2423 new_blob: CommitId::new("d95f3ad14dee633a758d2e331151e950dd13e4ed"),
2424 name: FileName::Normal("a".into()),
2425 status: StatusChange::Added,
2426 },
2427 );
2428 }
2429 const ROOT_COMMIT_A: &str = "1b2c7b3dad2fb7d14fbf0b619bf7faca4dd01243";
2444 const ROOT_COMMIT_B: &str = "7531e6df007ca1130e5d64b8627b3288844e38a4";
2446 const MERGE_B_ROOT_INTO_A: &str = "e93b017d9284492e485a53fb5c7566cd4f0a8e6f";
2448 const MERGE_A_ROOT_INTO_B: &str = "c1637e2d837ce657aa54df2d62b19b7e46249eb8";
2450
2451 #[test]
2452 fn test_topic_multiple_merge_base_a_root_as_base() {
2453 let ctx = make_context();
2454
2455 let topic = Topic::new(
2456 &ctx,
2457 &CommitId::new(MERGE_B_ROOT_INTO_A),
2458 &CommitId::new(MERGE_A_ROOT_INTO_B),
2459 )
2460 .unwrap();
2461
2462 assert_eq!(topic.base.as_str(), ROOT_COMMIT_A);
2463 assert_eq!(topic.sha1.as_str(), MERGE_A_ROOT_INTO_B);
2464
2465 assert_eq!(topic.diffs.len(), 2);
2466
2467 compare_diffs(
2468 &topic.diffs[0],
2469 &DiffInfo {
2470 old_mode: "000000".into(),
2471 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
2472 new_mode: "100644".into(),
2473 new_blob: CommitId::new("16fe87b06e802f094b3fbb0894b137bca2b16ef1"),
2474 name: FileName::Normal("LICENSE-APACHE".into()),
2475 status: StatusChange::Added,
2476 },
2477 );
2478 compare_diffs(
2479 &topic.diffs[1],
2480 &DiffInfo {
2481 old_mode: "000000".into(),
2482 old_blob: CommitId::new("0000000000000000000000000000000000000000"),
2483 new_mode: "100644".into(),
2484 new_blob: CommitId::new("8f6f4b4a936616349e1f2846632c95cff33a3c5d"),
2485 name: FileName::Normal("LICENSE-MIT".into()),
2486 status: StatusChange::Added,
2487 },
2488 );
2489 }
2490
2491 #[test]
2492 fn test_topic_multiple_merge_base_b_root_as_base() {
2493 let ctx = make_context();
2494
2495 let topic = Topic::new(
2496 &ctx,
2497 &CommitId::new(MERGE_A_ROOT_INTO_B),
2498 &CommitId::new(MERGE_B_ROOT_INTO_A),
2499 )
2500 .unwrap();
2501
2502 assert_eq!(topic.base.as_str(), ROOT_COMMIT_B);
2503 assert_eq!(topic.sha1.as_str(), MERGE_B_ROOT_INTO_A);
2504
2505 assert_eq!(topic.diffs.len(), 0);
2506 }
2507
2508 #[test]
2509 fn test_impl_content_for_commit_path_diff() {
2510 let ctx = make_context();
2511
2512 let commit = Commit::new(&ctx, &CommitId::new(CHANGES_COMMIT)).unwrap();
2513 let file = FileName::Normal("src/context.rs".into());
2514
2515 assert_eq!(
2516 Content::path_diff(&commit, &file).unwrap(),
2517 commit.path_diff(&file).unwrap(),
2518 );
2519 }
2520
2521 #[test]
2522 fn test_impl_content_for_topic_path_diff() {
2523 let ctx = make_context();
2524
2525 let topic = Topic::new(
2526 &ctx,
2527 &CommitId::new(ROOT_COMMIT),
2528 &CommitId::new(CHANGES_COMMIT),
2529 )
2530 .unwrap();
2531 let file = FileName::Normal("src/context.rs".into());
2532
2533 assert_eq!(
2534 Content::path_diff(&topic, &file).unwrap(),
2535 topic.path_diff(&file).unwrap(),
2536 );
2537 }
2538}