use crates::itertools::{Itertools, multizip};
use crates::git_workarea::{CommitId, GitContext, GitWorkArea, 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 new<P>(path: P) -> Self
where P: AsRef<str>,
{
Self::new_impl(path.as_ref())
}
fn new_impl(path: &str) -> Self {
if path.starts_with('"') {
let raw = path
.bytes()
.skip(1)
.dropping_back(1)
.batching(|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(Self::parse_octal(iter, sfd)),
None => unreachable!(),
}
},
n => n,
}
})
.collect();
FileName::Quoted {
raw: raw,
name: path.to_string(),
}
} else {
FileName::Normal(path.to_string())
}
}
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 = Self::parse_octal_digit(digit);
let ed = Self::parse_octal_digit(iter.next().expect("expected an eights-digit for an octal escape"));
let od = Self::parse_octal_digit(iter.next().expect("expected a ones-digit for an octal escape"));
64 * sfd + 8 * ed + od
}
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,
}
pub trait Content {
fn workarea(&self, ctx: &GitContext) -> Result<GitWorkArea>;
fn sha1(&self) -> Option<&CommitId>;
fn diffs(&self) -> &Vec<DiffInfo>;
fn modified_files(&self) -> Vec<&FileName> {
modified_files(self.diffs())
}
fn path_diff(&self, path: &FileName) -> Result<String>;
}
#[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(Self {
sha1: sha1.clone(),
message: commit_message,
parents: parents,
diffs: extract_diffs(ctx, None, sha1)?,
author: Identity::new(lines[1], lines[2]),
committer: Identity::new(lines[3], lines[4]),
ctx: ctx.clone(),
})
}
#[deprecated(since="2.3.0", note="please use the `Content::modified_files` trait method instead")]
pub fn changed_files(&self) -> Vec<&FileName> {
Content::modified_files(self)
}
pub fn file_patch<P>(&self, path: P) -> Result<String>
where P: AsRef<OsStr>,
{
file_patch(&self.ctx, None, &self.sha1, path.as_ref())
}
}
impl Content for Commit {
fn workarea(&self, ctx: &GitContext) -> Result<GitWorkArea> {
ctx.prepare(&self.sha1)
.map_err(Into::into)
}
fn sha1(&self) -> Option<&CommitId> {
Some(&self.sha1)
}
fn diffs(&self) -> &Vec<DiffInfo> {
&self.diffs
}
fn path_diff(&self, path: &FileName) -> Result<String> {
self.file_patch(path)
}
}
#[derive(Debug)]
pub struct Topic {
pub base: CommitId,
pub sha1: CommitId,
pub diffs: Vec<DiffInfo>,
ctx: GitContext,
}
impl Topic {
pub fn new(ctx: &GitContext, base: &CommitId, sha1: &CommitId) -> Result<Self> {
let merge_base = Self::best_merge_base(ctx, base, sha1)?;
Ok(Self {
diffs: extract_diffs(ctx, Some(&merge_base), sha1)?,
base: merge_base,
sha1: sha1.clone(),
ctx: ctx.clone(),
})
}
fn best_merge_base(ctx: &GitContext, base: &CommitId, sha1: &CommitId) -> Result<CommitId> {
let merge_base = ctx.git()
.arg("merge-base")
.arg("--all")
.arg(base.as_str())
.arg(sha1.as_str())
.output()
.chain_err(|| "failed to construct merge-base command")?;
Ok(if let Some(1) = merge_base.status.code() {
base.clone()
} else if merge_base.status.success() {
let merge_bases = String::from_utf8_lossy(&merge_base.stdout);
merge_bases.lines()
.map(CommitId::new)
.fold(Ok(base.clone()) as Result<CommitId>, |best_mb, merge_base| {
if let Ok(best) = best_mb {
let rev_parse = ctx.git()
.arg("rev-parse")
.arg("--verify")
.arg("--quiet")
.arg(format!("{}~", merge_base))
.output()
.chain_err(|| "failed to construct merge-base command")?;
let (merge_base_rev, exclude) = if rev_parse.status.success() {
(CommitId::new(String::from_utf8_lossy(&rev_parse.stdout).trim()), "")
} else {
(merge_base.clone(), "")
};
let refs = ctx.git()
.arg("rev-list")
.arg("--first-parent")
.arg("--reverse")
.arg(base.as_str())
.arg(format!("{}{}", exclude, merge_base_rev))
.output()
.chain_err(|| "failed to construct merge-base command")?;
if !refs.status.success() {
bail!(ErrorKind::Git(format!("failed to get list the first parent history \
for the {} branch: {}",
base,
String::from_utf8_lossy(&refs.stderr))));
}
let refs = String::from_utf8_lossy(&refs.stdout);
if !refs.lines().any(|rev| rev == merge_base.as_str()) && &best != base {
return Ok(best);
}
let is_ancestor = ctx.git()
.arg("merge-base")
.arg("--is-ancestor")
.arg(best.as_str())
.arg(merge_base.as_str())
.output()
.chain_err(|| "failed to construct merge-base command")?;
if let Some(1) = is_ancestor.status.code() {
Ok(merge_base)
} else if is_ancestor.status.success() {
Ok(best)
} else {
bail!(ErrorKind::Git(format!("failed to determine if the {} commit is an ancestor of {}: {}",
best,
merge_base,
String::from_utf8_lossy(&is_ancestor.stderr))));
}
} else {
best_mb
}
})?
} else {
bail!(ErrorKind::Git(format!("failed to find the merge-base between {} and {}: {}",
base,
sha1,
String::from_utf8_lossy(&merge_base.stderr))));
})
}
pub fn file_patch<P>(&self, path: P) -> Result<String>
where P: AsRef<OsStr>,
{
file_patch(&self.ctx, Some(&self.base), &self.sha1, path.as_ref())
}
}
impl Content for Topic {
fn workarea(&self, ctx: &GitContext) -> Result<GitWorkArea> {
ctx.prepare(&self.sha1)
.map_err(Into::into)
}
fn sha1(&self) -> Option<&CommitId> {
None
}
fn diffs(&self) -> &Vec<DiffInfo> {
&self.diffs
}
fn path_diff(&self, path: &FileName) -> Result<String> {
self.file_patch(path)
}
}
fn modified_files(diffs: &[DiffInfo]) -> Vec<&FileName> {
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 extract_diffs(ctx: &GitContext, base: Option<&CommitId>, sha1: &CommitId)
-> Result<Vec<DiffInfo>> {
let mut diff_tree_cmd = ctx.git();
diff_tree_cmd.arg("diff-tree")
.arg("--no-commit-id")
.arg("--root")
.arg("-c")
.arg("-r");
if let Some(base) = base {
diff_tree_cmd.arg(base.as_str());
}
let diff_tree = diff_tree_cmd.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 = FileName::new(raw_name.as_str());
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(),
}
})
})
.flat_map(|x| x)
.collect())
}
fn file_patch(ctx: &GitContext, base: Option<&CommitId>, sha1: &CommitId, path: &OsStr)
-> Result<String> {
let mut diff_tree_cmd = ctx.git();
diff_tree_cmd.arg("diff-tree")
.arg("--no-commit-id")
.arg("--root")
.arg("-p");
if let Some(base) = base {
diff_tree_cmd.arg(base.as_str());
}
let diff_tree = diff_tree_cmd.arg(sha1.as_str())
.arg("--")
.arg(path)
.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.to_string_lossy(),
sha1,
String::from_utf8_lossy(&diff_tree.stderr))));
}
Ok(String::from_utf8_lossy(&diff_tree.stdout).into_owned())
}
#[cfg(test)]
mod tests {
use crates::git_workarea::{CommitId, GitContext, Identity};
use checks::test;
use commit::{Commit, StatusChange, Topic};
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)
}
const ROOT_COMMIT: &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);
}
const SECOND_COMMIT: &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);
}
const CHANGES_COMMIT: &str = "5fa80c2ab7df3c1d18c5ff7840a1ef051a1a1f21";
const CHANGES_COMMIT_PARENT: &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));
}
#[test]
fn test_topic_regular_with_changes() {
let ctx = make_context();
let topic = Topic::new(&ctx, &CommitId::new(ROOT_COMMIT), &CommitId::new(CHANGES_COMMIT)).unwrap();
assert_eq!(topic.base.as_str(), ROOT_COMMIT);
assert_eq!(topic.sha1.as_str(), CHANGES_COMMIT);
assert_eq!(topic.diffs.len(), 8);
let diff = &topic.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 = &topic.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(),
"eadf0e09676dae16dbdf2c51d12f237e0ec663c6");
assert_eq!(diff.name.as_str(), "Cargo.toml");
assert_eq!(diff.status, StatusChange::Added);
let diff = &topic.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 = &topic.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(),
"b43524e95861f3dccb3b1b2ce42e004ff8ba53cc");
assert_eq!(diff.name.as_str(), "src/commit.rs");
assert_eq!(diff.status, StatusChange::Added);
let diff = &topic.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(),
"db2811cc69a6fda16f6ef5d08cfc7780b46331d7");
assert_eq!(diff.name.as_str(), "src/context.rs");
assert_eq!(diff.status, StatusChange::Added);
let diff = &topic.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(),
"ea5b00b30d7caaf2ceb7396788b4f0493d84d282");
assert_eq!(diff.name.as_str(), "src/hook.rs");
assert_eq!(diff.status, StatusChange::Added);
let diff = &topic.diffs[6];
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(),
"1784b5969f5948334d6c29b7050bb97c54f3fe8f");
assert_eq!(diff.name.as_str(), "src/lib.rs");
assert_eq!(diff.status, StatusChange::Added);
let diff = &topic.diffs[7];
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(),
"af3a45a85ac6ba0d026279adac509c56308a3e26");
assert_eq!(diff.name.as_str(), "src/run.rs");
assert_eq!(diff.status, StatusChange::Added);
}
const QUOTED_PATHS: &str = "f536f44cf96b82e479d4973d5ea1cf78058bd1fb";
const QUOTED_PATHS_PARENT: &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);
}
#[test]
fn test_commit_quoted_paths_with_bad_config() {
let raw_ctx = make_context();
let tempdir = test::make_temp_dir("test_commit_quoted_paths_with_bad_config");
let config_path = tempdir.path().join("config");
let config = raw_ctx.git()
.arg("config")
.arg("-f").arg(&config_path)
.arg("core.quotePath")
.arg("false")
.output()
.unwrap();
if !config.status.success() {
panic!("setting core.quotePath failed: {}",
String::from_utf8_lossy(&config.stderr));
}
let ctx = GitContext::new_with_config(raw_ctx.gitdir(), config_path);
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);
}
const REGULAR_MERGE_COMMIT: &str = "c58dd19d9976722d82aa6bc6a52a2a01a52bd9e8";
const REGULAR_MERGE_PARENT1: &str = "3a22ca19fda09183da2faab60819ff6807568acd";
const REGULAR_MERGE_PARENT2: &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);
}
const NO_HISTORY_MERGE_COMMIT: &str = "018ef4b25b978e194712e57a8f71d67427ecc065";
const NO_HISTORY_MERGE_PARENT1: &str = "969b794ea82ce51d1555852de33bfcb63dfec969";
const NO_HISTORY_MERGE_PARENT2: &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);
}
const CONFLICT_MERGE_COMMIT: &str = "969b794ea82ce51d1555852de33bfcb63dfec969";
const CONFLICT_MERGE_PARENT1: &str = "fb11c6970f4aff1e16ea85ba17529377b62310b7";
const CONFLICT_MERGE_PARENT2: &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));
}
const EVIL_MERGE_COMMIT: &str = "fb11c6970f4aff1e16ea85ba17529377b62310b7";
const EVIL_MERGE_PARENT1: &str = "c58dd19d9976722d82aa6bc6a52a2a01a52bd9e8";
const EVIL_MERGE_PARENT2: &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);
}
const SMALL_DIFF_COMMIT: &str = "22324be5cac6a797a3c5f3633e4409840e02eae9";
const SMALL_DIFF_PARENT: &str = "1ff04953f1b8dd7f01ecfe51ee962c47cea50e28";
#[test]
fn test_commit_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"));
}
#[test]
fn test_topic_file_patch() {
let ctx = make_context();
let topic = Topic::new(&ctx, &CommitId::new(ROOT_COMMIT), &CommitId::new(CHANGES_COMMIT)).unwrap();
assert_eq!(topic.base.as_str(), ROOT_COMMIT);
assert_eq!(topic.sha1.as_str(), CHANGES_COMMIT);
assert_eq!(topic.diffs.len(), 8);
assert_eq!(topic.file_patch(".gitignore").unwrap(),
concat!("diff --git a/.gitignore b/.gitignore\n",
"new file mode 100644\n",
"index 0000000..fa8d85a\n",
"--- /dev/null\n",
"+++ b/.gitignore\n",
"@@ -0,0 +1,2 @@\n",
"+Cargo.lock\n",
"+target\n"));
}
const ADD_BINARY_COMMIT: &str = "ff7ee6b5c822440613a01bfcf704e18cff4def72";
const CHANGE_BINARY_COMMIT: &str = "8f25adf0f878bdf88b609ca7ef67f235c5237602";
const ADD_BINARY_PARENT: &str = "1b2c7b3dad2fb7d14fbf0b619bf7faca4dd01243";
#[test]
fn test_commit_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_topic_file_patch_binary_add() {
let ctx = make_context();
let topic = Topic::new(&ctx, &CommitId::new(ADD_BINARY_PARENT), &CommitId::new(CHANGE_BINARY_COMMIT)).unwrap();
assert_eq!(topic.base.as_str(), ADD_BINARY_PARENT);
assert_eq!(topic.sha1.as_str(), CHANGE_BINARY_COMMIT);
assert_eq!(topic.diffs.len(), 1);
let diff = &topic.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(),
"53c235a627abeda83e26d6d53cdebf72b52f3c3d");
assert_eq!(diff.name.as_str(), "binary-data");
assert_eq!(diff.status, StatusChange::Added);
assert_eq!(topic.file_patch("binary-data").unwrap(),
concat!("diff --git a/binary-data b/binary-data\n",
"new file mode 100644\n",
"index 0000000..53c235a\n",
"Binary files /dev/null and b/binary-data differ\n"));
}
#[test]
fn test_commit_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"));
}
const TOPIC_ROOT_COMMIT: &str = "1b2c7b3dad2fb7d14fbf0b619bf7faca4dd01243";
const FORK_POINT: &str = "c65b124adb2876f3ce3f328fd22fbc2726607030";
const EXTEND_TARGET: &str = "cc8fa49b9e349250e05967dd6a6be1093da3ac43";
const MERGE_INTO_TARGET: &str = "e590269220be798435848d8428390c980493cc2c";
const SIMPLE_TOPIC: &str = "7876da5555b4efbc32c7df123d5c2171477a6771";
const EXTEND_MERGED_TOPIC: &str = "38d963092fddfd3ef0b38558996c015d01830354";
const CROSS_MERGE_TOPIC: &str = "f194e9af0d6864298e5159152c064d25abaaf858";
const SHARE_ROOT_TOPIC: &str = "3a22ca19fda09183da2faab60819ff6807568acd";
fn test_best_merge_base(target: &str, topic: &str, merge_base: &str) {
let ctx = make_context();
assert_eq!(Topic::best_merge_base(&ctx, &CommitId::new(target), &CommitId::new(topic)).unwrap(),
CommitId::new(merge_base));
}
#[test]
fn test_best_merge_base_extended_target() {
test_best_merge_base(EXTEND_TARGET, SIMPLE_TOPIC, FORK_POINT)
}
#[test]
fn test_best_merge_base_extend_merged_topic() {
test_best_merge_base(MERGE_INTO_TARGET, EXTEND_MERGED_TOPIC, SIMPLE_TOPIC)
}
#[test]
fn test_best_merge_base_share_root() {
test_best_merge_base(EXTEND_TARGET, SHARE_ROOT_TOPIC, TOPIC_ROOT_COMMIT)
}
#[test]
fn test_best_merge_base_cross_merge() {
test_best_merge_base(MERGE_INTO_TARGET, CROSS_MERGE_TOPIC, EXTEND_TARGET)
}
#[test]
fn test_best_merge_base_no_common_commits() {
test_best_merge_base(EXTEND_TARGET, ROOT_COMMIT, EXTEND_TARGET)
}
#[test]
fn test_topic_extended_target() {
let ctx = make_context();
let topic = Topic::new(&ctx, &CommitId::new(EXTEND_TARGET), &CommitId::new(SIMPLE_TOPIC)).unwrap();
assert_eq!(topic.base.as_str(), FORK_POINT);
assert_eq!(topic.sha1.as_str(), SIMPLE_TOPIC);
assert_eq!(topic.diffs.len(), 1);
let diff = &topic.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(), "simple-topic");
assert_eq!(diff.status, StatusChange::Added);
assert_eq!(topic.file_patch("simple-topic").unwrap(),
concat!("diff --git a/simple-topic b/simple-topic\n",
"new file mode 100644\n",
"index 0000000..e69de29\n"));
}
#[test]
fn test_topic_extended_merged_target() {
let ctx = make_context();
let topic = Topic::new(&ctx, &CommitId::new(MERGE_INTO_TARGET), &CommitId::new(EXTEND_MERGED_TOPIC)).unwrap();
assert_eq!(topic.base.as_str(), SIMPLE_TOPIC);
assert_eq!(topic.sha1.as_str(), EXTEND_MERGED_TOPIC);
assert_eq!(topic.diffs.len(), 1);
let diff = &topic.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(), "pre-merge-topic");
assert_eq!(diff.status, StatusChange::Added);
assert_eq!(topic.file_patch("pre-merge-topic").unwrap(),
concat!("diff --git a/pre-merge-topic b/pre-merge-topic\n",
"new file mode 100644\n",
"index 0000000..e69de29\n"));
}
#[test]
fn test_topic_share_root() {
let ctx = make_context();
let topic = Topic::new(&ctx, &CommitId::new(EXTEND_TARGET), &CommitId::new(SHARE_ROOT_TOPIC)).unwrap();
assert_eq!(topic.base.as_str(), TOPIC_ROOT_COMMIT);
assert_eq!(topic.sha1.as_str(), SHARE_ROOT_TOPIC);
assert_eq!(topic.diffs.len(), 1);
let diff = &topic.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(),
"2ef267e25bd6c6a300bb473e604b092b6a48523b");
assert_eq!(diff.name.as_str(), "content");
assert_eq!(diff.status, StatusChange::Added);
assert_eq!(topic.file_patch("content").unwrap(),
concat!("diff --git a/content b/content\n",
"new file mode 100644\n",
"index 0000000..2ef267e\n",
"--- /dev/null\n",
"+++ b/content\n",
"@@ -0,0 +1 @@\n",
"+some content\n"));
}
#[test]
fn test_topic_cross_merge() {
let ctx = make_context();
let topic = Topic::new(&ctx, &CommitId::new(MERGE_INTO_TARGET), &CommitId::new(CROSS_MERGE_TOPIC)).unwrap();
assert_eq!(topic.base.as_str(), EXTEND_TARGET);
assert_eq!(topic.sha1.as_str(), CROSS_MERGE_TOPIC);
assert_eq!(topic.diffs.len(), 2);
let diff = &topic.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(), "pre-merge-topic");
assert_eq!(diff.status, StatusChange::Added);
let diff = &topic.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(), "simple-topic");
assert_eq!(diff.status, StatusChange::Added);
assert_eq!(topic.file_patch("pre-merge-topic").unwrap(),
concat!("diff --git a/pre-merge-topic b/pre-merge-topic\n",
"new file mode 100644\n",
"index 0000000..e69de29\n"));
assert_eq!(topic.file_patch("simple-topic").unwrap(),
concat!("diff --git a/simple-topic b/simple-topic\n",
"new file mode 100644\n",
"index 0000000..e69de29\n"));
}
#[test]
fn test_topic_no_common_commits() {
let ctx = make_context();
let topic = Topic::new(&ctx, &CommitId::new(EXTEND_TARGET), &CommitId::new(ROOT_COMMIT)).unwrap();
assert_eq!(topic.base.as_str(), EXTEND_TARGET);
assert_eq!(topic.sha1.as_str(), ROOT_COMMIT);
assert_eq!(topic.diffs.len(), 3);
let diff = &topic.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 = &topic.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);
let diff = &topic.diffs[2];
assert_eq!(diff.old_mode, "100644");
assert_eq!(diff.old_blob.as_str(),
"e4b0cf6006925052f1172d5dea15d9a7aec373f3");
assert_eq!(diff.new_mode, "000000");
assert_eq!(diff.new_blob.as_str(),
"0000000000000000000000000000000000000000");
assert_eq!(diff.name.as_str(), "content");
assert_eq!(diff.status, StatusChange::Deleted);
}
}