use crates::itertools::{Itertools, multizip};
use crates::git_workarea::{CommitId, GitContext, Identity};
use crates::regex::Regex;
use error::*;
use std::ffi::OsStr;
use std::fmt::{self, Display};
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
macro_rules! char_byte {
($name: ident, $value: expr) => {
const $name: u8 = $value as u8;
}
}
char_byte!(BACKSLASH_ESCAPE, '\\');
char_byte!(BACKSLASH, '\\');
char_byte!(TAB_ESCAPE, 't');
char_byte!(TAB, '\t');
char_byte!(NEWLINE_ESCAPE, 'n');
char_byte!(NEWLINE, '\n');
char_byte!(QUOTE_ESCAPE, '"');
char_byte!(QUOTE, '"');
char_byte!(ZERO, '0');
char_byte!(SEVEN, '7');
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatusChange {
Added,
Copied(u8),
Deleted,
Modified(Option<u8>),
Renamed(u8),
TypeChanged,
Unmerged,
Unknown,
}
impl From<char> for StatusChange {
fn from(c: char) -> Self {
match c {
'A' => StatusChange::Added,
'C' => StatusChange::Copied(0),
'D' => StatusChange::Deleted,
'M' => StatusChange::Modified(None),
'R' => StatusChange::Renamed(0),
'T' => StatusChange::TypeChanged,
'U' => StatusChange::Unmerged,
'X' => StatusChange::Unknown,
_ => unreachable!("the regex does not match any other characters"),
}
}
}
#[derive(Debug, Clone, Eq)]
pub enum FileName {
#[doc(hidden)]
Normal(String),
#[doc(hidden)]
Quoted {
raw: Vec<u8>,
name: String,
},
}
impl FileName {
pub fn as_str(&self) -> &str {
self.as_ref()
}
pub fn as_path(&self) -> &Path {
self.as_ref()
}
pub fn as_bytes(&self) -> &[u8] {
self.as_ref()
}
}
impl Display for FileName {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl PartialEq for FileName {
fn eq(&self, rhs: &Self) -> bool {
self.as_bytes() == rhs.as_bytes()
}
}
impl AsRef<str> for FileName {
fn as_ref(&self) -> &str {
match *self {
FileName::Normal(ref name) |
FileName::Quoted { ref name, .. } => name,
}
}
}
impl AsRef<[u8]> for FileName {
fn as_ref(&self) -> &[u8] {
match *self {
FileName::Normal(ref name) => name.as_bytes(),
FileName::Quoted { ref raw, .. } => raw,
}
}
}
impl AsRef<OsStr> for FileName {
fn as_ref(&self) -> &OsStr {
match *self {
FileName::Normal(ref name) => name.as_ref(),
FileName::Quoted { ref raw, .. } => OsStr::from_bytes(raw),
}
}
}
impl AsRef<Path> for FileName {
fn as_ref(&self) -> &Path {
Path::new(self)
}
}
#[derive(Debug)]
pub struct DiffInfo {
pub old_mode: String,
pub old_blob: CommitId,
pub new_mode: String,
pub new_blob: CommitId,
pub name: FileName,
pub status: StatusChange,
}
#[derive(Debug)]
pub struct Commit {
pub sha1: CommitId,
pub message: String,
pub parents: Vec<CommitId>,
pub diffs: Vec<DiffInfo>,
pub author: Identity,
pub committer: Identity,
ctx: GitContext,
}
lazy_static! {
static ref DIFF_TREE_LINE_RE: Regex =
Regex::new("^:+\
(?P<old_modes>[0-7]{6}( [0-7]{6})*) \
(?P<new_mode>[0-7]{6}) \
(?P<old_blobs>[0-9a-f]{40}( [0-9a-f]{40})*) \
(?P<new_blob>[0-9a-f]{40}) \
(?P<status>[ACDMRTUX]+)\
\t(?P<name>.*)$").unwrap();
}
impl Commit {
pub fn new(ctx: &GitContext, sha1: &CommitId) -> Result<Self> {
let commit_info = ctx.git()
.arg("log")
.arg("--pretty=%P%n%an%n%ae%n%cn%n%ce")
.arg("--max-count=1")
.arg(sha1.as_str())
.output()
.chain_err(|| "failed to construct log command for information query")?;
if !commit_info.status.success() {
bail!(ErrorKind::Git(format!("failed to fetch information on the {} commit: {}",
sha1,
String::from_utf8_lossy(&commit_info.stderr))));
}
let lines = String::from_utf8_lossy(&commit_info.stdout);
let lines = lines.lines().collect::<Vec<_>>();
assert!(lines.len() == 5,
"got {} rather than 5 lines when logging a commit: {:?}",
lines.len(),
lines);
let commit_message = ctx.git()
.arg("log")
.arg("--pretty=%B")
.arg("--max-count=1")
.arg(sha1.as_str())
.output()
.chain_err(|| "failed to construct log command for the commit message")?;
if !commit_message.status.success() {
bail!(ErrorKind::Git(format!("failed to fetch the commit message on the {} commit: \
{}",
sha1,
String::from_utf8_lossy(&commit_message.stderr))));
}
let mut commit_message = String::from_utf8_lossy(&commit_message.stdout).into_owned();
commit_message.pop();
let parents = lines[0]
.split_whitespace()
.map(CommitId::new)
.collect();
Ok(Commit {
sha1: sha1.clone(),
message: commit_message,
parents: parents,
diffs: Self::_mkdiffs(ctx, sha1)?,
author: Identity::new(lines[1], lines[2]),
committer: Identity::new(lines[3], lines[4]),
ctx: ctx.clone(),
})
}
pub fn changed_files(&self) -> Vec<&FileName> {
self.diffs
.iter()
.filter_map(|diff| {
if diff.new_mode == "160000" {
return None;
}
if diff.new_mode == "120000" {
return None;
}
match diff.status {
StatusChange::Added |
StatusChange::Modified(_) => Some(&diff.name),
_ => None,
}
})
.collect::<Vec<_>>()
}
fn _mkdiffs(ctx: &GitContext, sha1: &CommitId) -> Result<Vec<DiffInfo>> {
let diff_tree = ctx.git()
.arg("diff-tree")
.arg("--no-commit-id")
.arg("--root")
.arg("-c") .arg("-r") .arg(sha1.as_str())
.output()
.chain_err(|| "failed to construct diff-tree command")?;
if !diff_tree.status.success() {
bail!(ErrorKind::Git(format!("failed to get the diff information for the {} \
commit: {}",
sha1,
String::from_utf8_lossy(&diff_tree.stderr))));
}
let diffs = String::from_utf8_lossy(&diff_tree.stdout);
let diff_lines = diffs.lines()
.filter_map(|l| DIFF_TREE_LINE_RE.captures(l));
Ok(diff_lines.map(|diff| {
let old_modes = diff.name("old_modes")
.expect("the diff regex should have a 'old_modes' group");
let new_mode = diff.name("new_mode")
.expect("the diff regex should have a 'new_mode' group");
let old_blobs = diff.name("old_blobs")
.expect("the diff regex should have a 'old_blobs' group");
let new_blob = diff.name("new_blob")
.expect("the diff regex should have a 'new_blob' group");
let status = diff.name("status")
.expect("the diff regex should have a 'status' group");
let raw_name = diff.name("name")
.expect("the diff regex should have a 'name' group");
let name = if raw_name.as_str().starts_with('"') {
let raw = raw_name.as_str()
.bytes()
.skip(1)
.dropping_back(1)
.batching(|mut iter| {
match iter.next() {
Some(BACKSLASH) => {
match iter.next() {
Some(BACKSLASH_ESCAPE) => Some(BACKSLASH),
Some(TAB_ESCAPE) => Some(TAB),
Some(NEWLINE_ESCAPE) => Some(NEWLINE),
Some(QUOTE_ESCAPE) => Some(QUOTE),
Some(sfd) => Some(parse_octal(iter, sfd)),
None => unreachable!(),
}
},
n => n,
}
})
.collect();
FileName::Quoted {
raw: raw,
name: raw_name.as_str().to_string(),
}
} else {
FileName::Normal(raw_name.as_str().to_string())
};
let zip = multizip((old_modes.as_str().split_whitespace(),
old_blobs.as_str().split_whitespace(),
status.as_str().chars()));
zip.map(move |(m, b, s)| {
DiffInfo {
old_mode: m.to_string(),
new_mode: new_mode.as_str().to_string(),
old_blob: CommitId::new(b),
new_blob: CommitId::new(new_blob.as_str()),
status: s.into(),
name: name.clone(),
}
})
})
.flatten()
.collect())
}
pub fn file_patch<P>(&self, path: P) -> Result<String>
where P: AsRef<OsStr>,
{
let diff_tree = self.ctx
.git()
.arg("diff-tree")
.arg("--no-commit-id")
.arg("--root")
.arg("-p")
.arg(self.sha1.as_str())
.arg("--")
.arg(path.as_ref())
.output()
.chain_err(|| "failed to construct diff-tree command")?;
if !diff_tree.status.success() {
bail!(ErrorKind::Git(format!("failed to get the diff for {} in the {} commit: {}",
path.as_ref().to_string_lossy(),
self.sha1,
String::from_utf8_lossy(&diff_tree.stderr))));
}
Ok(String::from_utf8_lossy(&diff_tree.stdout).into_owned())
}
}
fn parse_octal_digit(ch_digit: u8) -> u8 {
assert!(ZERO <= ch_digit,
"octal character out of range: {}",
ch_digit);
assert!(ch_digit <= SEVEN,
"octal character out of range: {}",
ch_digit);
ch_digit - ZERO
}
fn parse_octal<I>(iter: &mut I, digit: u8) -> u8
where I: Iterator<Item = u8>,
{
let sfd = parse_octal_digit(digit);
let ed = parse_octal_digit(iter.next().expect("expected an eights-digit for an octal escape"));
let od = parse_octal_digit(iter.next().expect("expected a ones-digit for an octal escape"));
64 * sfd + 8 * ed + od
}
#[cfg(test)]
mod tests {
use crates::git_workarea::{CommitId, GitContext, Identity};
use commit::{Commit, StatusChange};
use std::path::Path;
fn make_context() -> GitContext {
let gitdir = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/.git"));
if !gitdir.exists() {
panic!("The tests must be run from a git checkout.");
}
GitContext::new(gitdir)
}
static ROOT_COMMIT: &'static str = "7531e6df007ca1130e5d64b8627b3288844e38a4";
#[test]
fn test_commit_root() {
let ctx = make_context();
let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
let commit = Commit::new(&ctx, &CommitId::new(ROOT_COMMIT)).unwrap();
assert_eq!(commit.sha1.as_str(), ROOT_COMMIT);
assert_eq!(commit.message, "license: add Apache v2 + MIT licenses\n");
assert_eq!(commit.parents.len(), 0);
assert_eq!(commit.author, ben);
assert_eq!(commit.committer, ben);
assert_eq!(commit.diffs.len(), 2);
let diff = &commit.diffs[0];
assert_eq!(diff.old_mode, "000000");
assert_eq!(diff.old_blob.as_str(),
"0000000000000000000000000000000000000000");
assert_eq!(diff.new_mode, "100644");
assert_eq!(diff.new_blob.as_str(),
"16fe87b06e802f094b3fbb0894b137bca2b16ef1");
assert_eq!(diff.name.as_str(), "LICENSE-APACHE");
assert_eq!(diff.status, StatusChange::Added);
let diff = &commit.diffs[1];
assert_eq!(diff.old_mode, "000000");
assert_eq!(diff.old_blob.as_str(),
"0000000000000000000000000000000000000000");
assert_eq!(diff.new_mode, "100644");
assert_eq!(diff.new_blob.as_str(),
"8f6f4b4a936616349e1f2846632c95cff33a3c5d");
assert_eq!(diff.name.as_str(), "LICENSE-MIT");
assert_eq!(diff.status, StatusChange::Added);
}
static SECOND_COMMIT: &'static str = "7f0d58bf407b0c80a9daadae88e7541f152ef371";
#[test]
fn test_commit_regular() {
let ctx = make_context();
let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
let commit = Commit::new(&ctx, &CommitId::new(SECOND_COMMIT)).unwrap();
assert_eq!(commit.sha1.as_str(), SECOND_COMMIT);
assert_eq!(commit.message, "cargo: add boilerplate\n");
assert_eq!(commit.parents.len(), 1);
assert_eq!(commit.parents[0].as_str(), ROOT_COMMIT);
assert_eq!(commit.author, ben);
assert_eq!(commit.committer, ben);
assert_eq!(commit.diffs.len(), 4);
let diff = &commit.diffs[0];
assert_eq!(diff.old_mode, "000000");
assert_eq!(diff.old_blob.as_str(),
"0000000000000000000000000000000000000000");
assert_eq!(diff.new_mode, "100644");
assert_eq!(diff.new_blob.as_str(),
"fa8d85ac52f19959d6fc9942c265708b4b3c2b04");
assert_eq!(diff.name.as_str(), ".gitignore");
assert_eq!(diff.status, StatusChange::Added);
let diff = &commit.diffs[1];
assert_eq!(diff.old_mode, "000000");
assert_eq!(diff.old_blob.as_str(),
"0000000000000000000000000000000000000000");
assert_eq!(diff.new_mode, "100644");
assert_eq!(diff.new_blob.as_str(),
"e31460ae61b1d69c6d0a58f9b74ae94d463c49bb");
assert_eq!(diff.name.as_str(), "Cargo.toml");
assert_eq!(diff.status, StatusChange::Added);
let diff = &commit.diffs[2];
assert_eq!(diff.old_mode, "000000");
assert_eq!(diff.old_blob.as_str(),
"0000000000000000000000000000000000000000");
assert_eq!(diff.new_mode, "100644");
assert_eq!(diff.new_blob.as_str(),
"b3c9194ad2d95d1498361f70a5480f0433fad1bb");
assert_eq!(diff.name.as_str(), "rustfmt.toml");
assert_eq!(diff.status, StatusChange::Added);
let diff = &commit.diffs[3];
assert_eq!(diff.old_mode, "000000");
assert_eq!(diff.old_blob.as_str(),
"0000000000000000000000000000000000000000");
assert_eq!(diff.new_mode, "100644");
assert_eq!(diff.new_blob.as_str(),
"584691748770380d5b70deb5f489b0c09229404e");
assert_eq!(diff.name.as_str(), "src/lib.rs");
assert_eq!(diff.status, StatusChange::Added);
}
static CHANGES_COMMIT: &'static str = "5fa80c2ab7df3c1d18c5ff7840a1ef051a1a1f21";
static CHANGES_COMMIT_PARENT: &'static str = "89e44cb662b4a2e6b8f0333f84e5cc4ac818cc96";
#[test]
fn test_commit_regular_with_changes() {
let ctx = make_context();
let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
let commit = Commit::new(&ctx, &CommitId::new(CHANGES_COMMIT)).unwrap();
assert_eq!(commit.sha1.as_str(), CHANGES_COMMIT);
assert_eq!(commit.message,
"Identity: use over strings where it matters\n");
assert_eq!(commit.parents.len(), 1);
assert_eq!(commit.parents[0].as_str(), CHANGES_COMMIT_PARENT);
assert_eq!(commit.author, ben);
assert_eq!(commit.committer, ben);
assert_eq!(commit.diffs.len(), 2);
let diff = &commit.diffs[0];
assert_eq!(diff.old_mode, "100644");
assert_eq!(diff.old_blob.as_str(),
"5710fe019e3de5e07f2bd1a5bcfd2d462834e9d8");
assert_eq!(diff.new_mode, "100644");
assert_eq!(diff.new_blob.as_str(),
"db2811cc69a6fda16f6ef5d08cfc7780b46331d7");
assert_eq!(diff.name.as_str(), "src/context.rs");
assert_eq!(diff.status, StatusChange::Modified(None));
let diff = &commit.diffs[1];
assert_eq!(diff.old_mode, "100644");
assert_eq!(diff.old_blob.as_str(),
"2acafbc5acd2e7b70260a3e39a1a752cc46cf953");
assert_eq!(diff.new_mode, "100644");
assert_eq!(diff.new_blob.as_str(),
"af3a45a85ac6ba0d026279adac509c56308a3e26");
assert_eq!(diff.name.as_str(), "src/run.rs");
assert_eq!(diff.status, StatusChange::Modified(None));
}
static QUOTED_PATHS: &'static str = "f536f44cf96b82e479d4973d5ea1cf78058bd1fb";
static QUOTED_PATHS_PARENT: &'static str = "1b2c7b3dad2fb7d14fbf0b619bf7faca4dd01243";
#[test]
fn test_commit_quoted_paths() {
let ctx = make_context();
let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
let commit = Commit::new(&ctx, &CommitId::new(QUOTED_PATHS)).unwrap();
assert_eq!(commit.sha1.as_str(), QUOTED_PATHS);
assert_eq!(commit.message,
"commit with invalid characters in path names\n");
assert_eq!(commit.parents.len(), 1);
assert_eq!(commit.parents[0].as_str(), QUOTED_PATHS_PARENT);
assert_eq!(commit.author, ben);
assert_eq!(commit.committer, ben);
assert_eq!(commit.diffs.len(), 6);
let diff = &commit.diffs[0];
assert_eq!(diff.old_mode, "000000");
assert_eq!(diff.old_blob.as_str(),
"0000000000000000000000000000000000000000");
assert_eq!(diff.new_mode, "100644");
assert_eq!(diff.new_blob.as_str(),
"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391");
assert_eq!(diff.name.as_str(), "\"control-character-\\003\"");
assert_eq!(diff.name.as_bytes(), "control-character-\x03".as_bytes());
assert_eq!(diff.status, StatusChange::Added);
let mut invalid_utf8_raw = "invalid-utf8-".bytes().collect::<Vec<_>>();
invalid_utf8_raw.push(128);
let diff = &commit.diffs[1];
assert_eq!(diff.old_mode, "000000");
assert_eq!(diff.old_blob.as_str(),
"0000000000000000000000000000000000000000");
assert_eq!(diff.new_mode, "100644");
assert_eq!(diff.new_blob.as_str(),
"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391");
assert_eq!(diff.name.as_str(), "\"invalid-utf8-\\200\"");
assert_eq!(diff.name.as_bytes(), invalid_utf8_raw.as_slice());
assert_eq!(diff.status, StatusChange::Added);
let diff = &commit.diffs[2];
assert_eq!(diff.old_mode, "000000");
assert_eq!(diff.old_blob.as_str(),
"0000000000000000000000000000000000000000");
assert_eq!(diff.new_mode, "100644");
assert_eq!(diff.new_blob.as_str(),
"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391");
assert_eq!(diff.name.as_str(), "\"non-ascii-\\303\\251\"");
assert_eq!(diff.name.as_bytes(), "non-ascii-é".as_bytes());
assert_eq!(diff.status, StatusChange::Added);
let diff = &commit.diffs[3];
assert_eq!(diff.old_mode, "000000");
assert_eq!(diff.old_blob.as_str(),
"0000000000000000000000000000000000000000");
assert_eq!(diff.new_mode, "100644");
assert_eq!(diff.new_blob.as_str(),
"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391");
assert_eq!(diff.name.as_str(), "with whitespace");
assert_eq!(diff.name.as_bytes(), "with whitespace".as_bytes());
assert_eq!(diff.status, StatusChange::Added);
let diff = &commit.diffs[4];
assert_eq!(diff.old_mode, "000000");
assert_eq!(diff.old_blob.as_str(),
"0000000000000000000000000000000000000000");
assert_eq!(diff.new_mode, "100644");
assert_eq!(diff.new_blob.as_str(),
"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391");
assert_eq!(diff.name.as_str(), "with-dollar-$");
assert_eq!(diff.name.as_bytes(), "with-dollar-$".as_bytes());
assert_eq!(diff.status, StatusChange::Added);
let diff = &commit.diffs[5];
assert_eq!(diff.old_mode, "000000");
assert_eq!(diff.old_blob.as_str(),
"0000000000000000000000000000000000000000");
assert_eq!(diff.new_mode, "100644");
assert_eq!(diff.new_blob.as_str(),
"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391");
assert_eq!(diff.name.as_str(), "\"with-quote-\\\"\"");
assert_eq!(diff.name.as_bytes(), "with-quote-\"".as_bytes());
assert_eq!(diff.status, StatusChange::Added);
}
static REGULAR_MERGE_COMMIT: &'static str = "c58dd19d9976722d82aa6bc6a52a2a01a52bd9e8";
static REGULAR_MERGE_PARENT1: &'static str = "3a22ca19fda09183da2faab60819ff6807568acd";
static REGULAR_MERGE_PARENT2: &'static str = "d02f015907371738253a22b9a7fec78607a969b2";
#[test]
fn test_commit_merge() {
let ctx = make_context();
let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
let commit = Commit::new(&ctx, &CommitId::new(REGULAR_MERGE_COMMIT)).unwrap();
assert_eq!(commit.sha1.as_str(), REGULAR_MERGE_COMMIT);
assert_eq!(commit.message,
"Merge commit 'd02f015907371738253a22b9a7fec78607a969b2' into \
tests/commit/merge\n\n* commit 'd02f015907371738253a22b9a7fec78607a969b2':\n \
test-refs: non-root commit\n");
assert_eq!(commit.parents.len(), 2);
assert_eq!(commit.parents[0].as_str(), REGULAR_MERGE_PARENT1);
assert_eq!(commit.parents[1].as_str(), REGULAR_MERGE_PARENT2);
assert_eq!(commit.author, ben);
assert_eq!(commit.committer, ben);
assert_eq!(commit.diffs.len(), 0);
}
static NO_HISTORY_MERGE_COMMIT: &'static str = "018ef4b25b978e194712e57a8f71d67427ecc065";
static NO_HISTORY_MERGE_PARENT1: &'static str = "969b794ea82ce51d1555852de33bfcb63dfec969";
static NO_HISTORY_MERGE_PARENT2: &'static str = "8805c7ae76329a937960448908f4618214fd3546";
#[test]
fn test_commit_merge_no_history() {
let ctx = make_context();
let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
let commit = Commit::new(&ctx, &CommitId::new(NO_HISTORY_MERGE_COMMIT)).unwrap();
assert_eq!(commit.sha1.as_str(), NO_HISTORY_MERGE_COMMIT);
assert_eq!(commit.message,
"Merge branch 'tests/commit/no_history' into tests/commit/merge\n\n* \
tests/commit/no_history:\n tests: a commit with new history\n");
assert_eq!(commit.parents.len(), 2);
assert_eq!(commit.parents[0].as_str(), NO_HISTORY_MERGE_PARENT1);
assert_eq!(commit.parents[1].as_str(), NO_HISTORY_MERGE_PARENT2);
assert_eq!(commit.author, ben);
assert_eq!(commit.committer, ben);
assert_eq!(commit.diffs.len(), 0);
}
static CONFLICT_MERGE_COMMIT: &'static str = "969b794ea82ce51d1555852de33bfcb63dfec969";
static CONFLICT_MERGE_PARENT1: &'static str = "fb11c6970f4aff1e16ea85ba17529377b62310b7";
static CONFLICT_MERGE_PARENT2: &'static str = "bed4760e60c8276e6db53c6c2b641933d2938510";
#[test]
fn test_commit_merge_conflict() {
let ctx = make_context();
let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
let commit = Commit::new(&ctx, &CommitId::new(CONFLICT_MERGE_COMMIT)).unwrap();
assert_eq!(commit.sha1.as_str(), CONFLICT_MERGE_COMMIT);
assert_eq!(commit.message,
"Merge branch 'test/commit/merge/conflict' into tests/commit/merge\n\n* \
test/commit/merge/conflict:\n content: cause some conflicts\n");
assert_eq!(commit.parents.len(), 2);
assert_eq!(commit.parents[0].as_str(), CONFLICT_MERGE_PARENT1);
assert_eq!(commit.parents[1].as_str(), CONFLICT_MERGE_PARENT2);
assert_eq!(commit.author, ben);
assert_eq!(commit.committer, ben);
assert_eq!(commit.diffs.len(), 2);
let diff = &commit.diffs[0];
assert_eq!(diff.old_mode, "100644");
assert_eq!(diff.old_blob.as_str(),
"829636d299136b6651fd19aa6ac3500c7d50bf10");
assert_eq!(diff.new_mode, "100644");
assert_eq!(diff.new_blob.as_str(),
"195cc9d0aeb7324584700f2d17940d6a218a36f2");
assert_eq!(diff.name.as_str(), "content");
assert_eq!(diff.status, StatusChange::Modified(None));
let diff = &commit.diffs[1];
assert_eq!(diff.old_mode, "100644");
assert_eq!(diff.old_blob.as_str(),
"925b35841fdd59e002bc5b3e685dc158bdbe6ebf");
assert_eq!(diff.new_mode, "100644");
assert_eq!(diff.new_blob.as_str(),
"195cc9d0aeb7324584700f2d17940d6a218a36f2");
assert_eq!(diff.name.as_str(), "content");
assert_eq!(diff.status, StatusChange::Modified(None));
}
static EVIL_MERGE_COMMIT: &'static str = "fb11c6970f4aff1e16ea85ba17529377b62310b7";
static EVIL_MERGE_PARENT1: &'static str = "c58dd19d9976722d82aa6bc6a52a2a01a52bd9e8";
static EVIL_MERGE_PARENT2: &'static str = "27ff3ef5532d76afa046f76f4dd8f588dc3e83c3";
#[test]
fn test_commit_merge_evil() {
let ctx = make_context();
let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
let commit = Commit::new(&ctx, &CommitId::new(EVIL_MERGE_COMMIT)).unwrap();
assert_eq!(commit.sha1.as_str(), EVIL_MERGE_COMMIT);
assert_eq!(commit.message,
"Merge commit '27ff3ef5532d76afa046f76f4dd8f588dc3e83c3' into \
tests/commit/merge\n\n* commit '27ff3ef5532d76afa046f76f4dd8f588dc3e83c3':\n \
test-refs: test target commit\n");
assert_eq!(commit.parents.len(), 2);
assert_eq!(commit.parents[0].as_str(), EVIL_MERGE_PARENT1);
assert_eq!(commit.parents[1].as_str(), EVIL_MERGE_PARENT2);
assert_eq!(commit.author, ben);
assert_eq!(commit.committer, ben);
assert_eq!(commit.diffs.len(), 2);
let diff = &commit.diffs[0];
assert_eq!(diff.old_mode, "100644");
assert_eq!(diff.old_blob.as_str(),
"2ef267e25bd6c6a300bb473e604b092b6a48523b");
assert_eq!(diff.new_mode, "100644");
assert_eq!(diff.new_blob.as_str(),
"829636d299136b6651fd19aa6ac3500c7d50bf10");
assert_eq!(diff.name.as_str(), "content");
assert_eq!(diff.status, StatusChange::Modified(None));
let diff = &commit.diffs[1];
assert_eq!(diff.old_mode, "000000");
assert_eq!(diff.old_blob.as_str(),
"0000000000000000000000000000000000000000");
assert_eq!(diff.new_mode, "100644");
assert_eq!(diff.new_blob.as_str(),
"829636d299136b6651fd19aa6ac3500c7d50bf10");
assert_eq!(diff.name.as_str(), "content");
assert_eq!(diff.status, StatusChange::Added);
}
static SMALL_DIFF_COMMIT: &'static str = "22324be5cac6a797a3c5f3633e4409840e02eae9";
static SMALL_DIFF_PARENT: &'static str = "1ff04953f1b8dd7f01ecfe51ee962c47cea50e28";
#[test]
fn test_file_patch() {
let ctx = make_context();
let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
let commit = Commit::new(&ctx, &CommitId::new(SMALL_DIFF_COMMIT)).unwrap();
assert_eq!(commit.sha1.as_str(), SMALL_DIFF_COMMIT);
assert_eq!(commit.message, "commit: use %n rather than \\n\n");
assert_eq!(commit.parents.len(), 1);
assert_eq!(commit.parents[0].as_str(), SMALL_DIFF_PARENT);
assert_eq!(commit.author, ben);
assert_eq!(commit.committer, ben);
assert_eq!(commit.diffs.len(), 1);
assert_eq!(commit.file_patch("src/commit.rs").unwrap(),
concat!("diff --git a/src/commit.rs b/src/commit.rs\n",
"index 2cb15f6..6303de0 100644\n",
"--- a/src/commit.rs\n",
"+++ b/src/commit.rs\n",
"@@ -109,7 +109,7 @@ impl Commit {\n",
" pub fn new(ctx: &GitContext, sha1: &str) -> Result<Self, \
Error> {\n",
" let commit_info = try!(ctx.git()\n",
" .arg(\"log\")\n",
"- .arg(\"--pretty=%P\\n%an\\n%ae\\n%cn\\n%ce\\n%h\")\n",
"+ .arg(\"--pretty=%P%n%an%n%ae%n%cn%n%ce%n%h\")\n",
" .arg(\"-n\").arg(\"1\")\n",
" .arg(sha1)\n",
" .output());\n"));
}
static ADD_BINARY_COMMIT: &'static str = "ff7ee6b5c822440613a01bfcf704e18cff4def72";
static CHANGE_BINARY_COMMIT: &'static str = "8f25adf0f878bdf88b609ca7ef67f235c5237602";
static ADD_BINARY_PARENT: &'static str = "1b2c7b3dad2fb7d14fbf0b619bf7faca4dd01243";
#[test]
fn test_file_patch_binary_add() {
let ctx = make_context();
let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
let commit = Commit::new(&ctx, &CommitId::new(ADD_BINARY_COMMIT)).unwrap();
assert_eq!(commit.sha1.as_str(), ADD_BINARY_COMMIT);
assert_eq!(commit.message, "binary-data: add some binary data\n");
assert_eq!(commit.parents.len(), 1);
assert_eq!(commit.parents[0].as_str(), ADD_BINARY_PARENT);
assert_eq!(commit.author, ben);
assert_eq!(commit.committer, ben);
assert_eq!(commit.diffs.len(), 1);
let diff = &commit.diffs[0];
assert_eq!(diff.old_mode, "000000");
assert_eq!(diff.old_blob.as_str(),
"0000000000000000000000000000000000000000");
assert_eq!(diff.new_mode, "100644");
assert_eq!(diff.new_blob.as_str(),
"7624577a217a08f1103d6c190eb11beab1081744");
assert_eq!(diff.name.as_str(), "binary-data");
assert_eq!(diff.status, StatusChange::Added);
assert_eq!(commit.file_patch("binary-data").unwrap(),
concat!("diff --git a/binary-data b/binary-data\n",
"new file mode 100644\n",
"index 0000000..7624577\n",
"Binary files /dev/null and b/binary-data differ\n"));
}
#[test]
fn test_file_patch_binary_change() {
let ctx = make_context();
let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
let commit = Commit::new(&ctx, &CommitId::new(CHANGE_BINARY_COMMIT)).unwrap();
assert_eq!(commit.sha1.as_str(), CHANGE_BINARY_COMMIT);
assert_eq!(commit.message, "binary-data: change some binary data\n");
assert_eq!(commit.parents.len(), 1);
assert_eq!(commit.parents[0].as_str(), ADD_BINARY_COMMIT);
assert_eq!(commit.author, ben);
assert_eq!(commit.committer, ben);
assert_eq!(commit.diffs.len(), 1);
let diff = &commit.diffs[0];
assert_eq!(diff.old_mode, "100644");
assert_eq!(diff.old_blob.as_str(),
"7624577a217a08f1103d6c190eb11beab1081744");
assert_eq!(diff.new_mode, "100644");
assert_eq!(diff.new_blob.as_str(),
"53c235a627abeda83e26d6d53cdebf72b52f3c3d");
assert_eq!(diff.name.as_str(), "binary-data");
assert_eq!(diff.status, StatusChange::Modified(None));
assert_eq!(commit.file_patch("binary-data").unwrap(),
concat!("diff --git a/binary-data b/binary-data\n",
"index 7624577..53c235a 100644\n",
"Binary files a/binary-data and b/binary-data differ\n"));
}
}