use std::ffi::OsStr;
use std::fmt::{self, Display};
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use crates::git_workarea::{CommitId, GitContext, GitError, GitWorkArea, Identity, WorkAreaError};
use crates::itertools::{multizip, Itertools};
use crates::regex::Regex;
use crates::thiserror::Error;
#[derive(Debug, Error)]
pub enum CommitError {
#[error("git error: {}", source)]
Git {
#[from]
source: GitError,
},
#[error("filename error: {}", source)]
FileName {
#[from]
source: FileNameError,
},
#[error("failed to fetch metadata on the {} commit: {}", ref_, output)]
CommitMetadata {
ref_: CommitId,
output: String,
},
#[error("unexpected output from `git log --pretty=<metadata>`: {}", output)]
CommitMetadataOutput {
output: String,
},
#[error("failed to fetch message on the {} commit: {}", ref_, output)]
CommitMessage {
ref_: CommitId,
output: String,
},
#[error(
"failed to list revisions of the {} commit (merge base parent: {}): {}",
base_rev,
merge_base.as_ref().map_or("<none>", CommitId::as_str),
output
)]
RevList {
base_rev: CommitId,
merge_base: Option<CommitId>,
output: String,
},
#[error(
"failed to determine if the {} commit is an ancestor of {}: {}",
best_rev,
merge_base,
output
)]
AncestorCheck {
best_rev: CommitId,
merge_base: CommitId,
output: String,
},
#[error(
"failed to find the merge-base between {} and {}: {}",
base,
head,
output
)]
MergeBase {
base: CommitId,
head: CommitId,
output: String,
},
#[error(
"failed to get the tree diff information for the {} commit (against {}): {}",
commit,
base.as_ref().map(CommitId::as_str).unwrap_or("parent"),
output
)]
DiffTree {
commit: CommitId,
base: Option<CommitId>,
output: String,
},
#[error(
"failed to get the diff patch for the {} commit (against {}): {}",
commit,
base.as_ref().map(CommitId::as_str).unwrap_or("parent"),
output
)]
DiffPatch {
commit: CommitId,
base: Option<CommitId>,
output: String,
},
#[doc(hidden)]
#[error("unreachable...")]
_NonExhaustive,
}
impl CommitError {
fn commit_metadata(ref_: CommitId, output: &[u8]) -> Self {
CommitError::CommitMetadata {
ref_,
output: String::from_utf8_lossy(output).into(),
}
}
fn commit_metadata_output(output: String) -> Self {
CommitError::CommitMetadataOutput {
output,
}
}
fn commit_message(ref_: CommitId, output: &[u8]) -> Self {
CommitError::CommitMessage {
ref_,
output: String::from_utf8_lossy(output).into(),
}
}
fn rev_list(base_rev: CommitId, merge_base: Option<CommitId>, output: &[u8]) -> Self {
CommitError::RevList {
base_rev,
merge_base,
output: String::from_utf8_lossy(output).into(),
}
}
fn ancestor_check(best_rev: CommitId, merge_base: CommitId, output: &[u8]) -> Self {
CommitError::AncestorCheck {
best_rev,
merge_base,
output: String::from_utf8_lossy(output).into(),
}
}
fn merge_base(base: CommitId, head: CommitId, output: &[u8]) -> Self {
CommitError::MergeBase {
base,
head,
output: String::from_utf8_lossy(output).into(),
}
}
fn diff_tree(commit: CommitId, base: Option<CommitId>, output: &[u8]) -> Self {
CommitError::DiffTree {
commit,
base,
output: String::from_utf8_lossy(output).into(),
}
}
fn diff_patch(commit: CommitId, base: Option<CommitId>, output: &[u8]) -> Self {
CommitError::DiffTree {
commit,
base,
output: String::from_utf8_lossy(output).into(),
}
}
}
#[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, Error)]
pub enum FileNameError {
#[doc(hidden)]
#[error("invalid leading octal digit: {}", _0)]
InvalidLeadingOctalDigit(u8),
#[doc(hidden)]
#[error("invalid octal digit: {}", _0)]
InvalidOctalDigit(u8),
#[doc(hidden)]
#[error("invalid escape character: {}", _0)]
InvalidEscape(u8),
#[doc(hidden)]
#[error("trailing backslash in file name")]
TrailingBackslash,
#[doc(hidden)]
#[error("missing eights digit in octal escape")]
MissingEightsDigit,
#[doc(hidden)]
#[error("missing ones digit in octal escape")]
MissingOnesDigit,
#[doc(hidden)]
#[error("unreachable...")]
_NonExhaustive,
}
#[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) -> Result<Self, FileNameError>
where
P: AsRef<str>,
{
Self::new_impl(path.as_ref())
}
fn new_impl(path: &str) -> Result<Self, FileNameError> {
if path.starts_with('"') {
let raw = path
.bytes()
.skip(1)
.dropping_back(1)
.batching(|iter| {
let n = iter.next();
if let Some(b'\\') = n {
match iter.next() {
Some(b'\\') => Some(Ok(b'\\')),
Some(b't') => Some(Ok(b'\t')),
Some(b'n') => Some(Ok(b'\n')),
Some(b'"') => Some(Ok(b'"')),
Some(sfd @ b'0') | Some(sfd @ b'1') | Some(sfd @ b'2')
| Some(sfd @ b'3') => Some(Self::parse_octal(iter, sfd)),
Some(sfd @ b'4') | Some(sfd @ b'5') | Some(sfd @ b'6')
| Some(sfd @ b'7') => {
Some(Err(FileNameError::InvalidLeadingOctalDigit(sfd)))
},
Some(c) => Some(Err(FileNameError::InvalidEscape(c))),
None => Some(Err(FileNameError::TrailingBackslash)),
}
} else {
n.map(Ok)
}
})
.collect::<Result<_, _>>()?;
Ok(FileName::Quoted {
raw,
name: path.into(),
})
} else {
Ok(FileName::Normal(path.into()))
}
}
fn parse_octal_digit(ch_digit: u8) -> Result<u8, FileNameError> {
if ch_digit < b'0' || b'7' < ch_digit {
Err(FileNameError::InvalidOctalDigit(ch_digit))
} else {
Ok(ch_digit - b'0')
}
}
fn parse_octal<I>(iter: &mut I, digit: u8) -> Result<u8, FileNameError>
where
I: Iterator<Item = u8>,
{
let sfd = Self::parse_octal_digit(digit)?;
let ed = Self::parse_octal_digit(iter.next().ok_or(FileNameError::MissingEightsDigit)?)?;
let od = Self::parse_octal_digit(iter.next().ok_or(FileNameError::MissingOnesDigit)?)?;
Ok(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, WorkAreaError>;
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, CommitError>;
}
#[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, CommitError> {
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()
.map_err(|err| GitError::subcommand("log --pretty=<metadata>", err))?;
if !commit_info.status.success() {
return Err(CommitError::commit_metadata(
sha1.clone(),
&commit_info.stderr,
));
}
let commit_info_output = String::from_utf8_lossy(&commit_info.stdout);
let lines = commit_info_output.lines().collect::<Vec<_>>();
if lines.len() != 5 {
return Err(CommitError::commit_metadata_output(
commit_info_output.into(),
));
}
let commit_message = ctx
.git()
.arg("log")
.arg("--pretty=%B")
.arg("--max-count=1")
.arg(sha1.as_str())
.output()
.map_err(|err| GitError::subcommand("log --pretty=<message>", err))?;
if !commit_message.status.success() {
return Err(CommitError::commit_message(
sha1.clone(),
&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,
diffs: extract_diffs(ctx, None, sha1)?,
author: Identity::new(lines[1], lines[2]),
committer: Identity::new(lines[3], lines[4]),
ctx: ctx.clone(),
})
}
pub fn file_patch<P>(&self, path: P) -> Result<String, CommitError>
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, WorkAreaError> {
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, CommitError> {
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, CommitError> {
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, CommitError> {
let merge_base = ctx
.git()
.arg("merge-base")
.arg("--all")
.arg(base.as_str())
.arg(sha1.as_str())
.output()
.map_err(|err| GitError::subcommand("merge-base --all", err))?;
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, CommitError>,
|best_mb, merge_base| {
let best = match best_mb {
Ok(best) => best,
err => return err,
};
let rev_parse = ctx
.git()
.arg("rev-parse")
.arg("--verify")
.arg("--quiet")
.arg(format!("{}~", merge_base))
.output()
.map_err(|err| GitError::subcommand("rev-parse", err))?;
let merge_base_parent = if rev_parse.status.success() {
Some(String::from(
String::from_utf8_lossy(&rev_parse.stdout).trim(),
))
} else {
None
};
let mut refs = ctx.git();
refs.arg("rev-list")
.arg("--first-parent")
.arg("--reverse")
.arg(base.as_str());
if let Some(merge_base_parent) = merge_base_parent.as_ref() {
refs.arg(format!("^{}", merge_base_parent));
};
let refs = refs
.output()
.map_err(|err| GitError::subcommand("rev-list", err))?;
if !refs.status.success() {
return Err(CommitError::rev_list(
base.clone(),
merge_base_parent.map(CommitId::new),
&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()
.map_err(|err| GitError::subcommand("merge-base --is-ancestor", err))?;
if let Some(1) = is_ancestor.status.code() {
Ok(merge_base)
} else if is_ancestor.status.success() {
Ok(best)
} else {
Err(CommitError::ancestor_check(
best,
merge_base,
&is_ancestor.stderr,
))
}
},
)?
} else {
return Err(CommitError::merge_base(
base.clone(),
sha1.clone(),
&merge_base.stderr,
));
})
}
pub fn file_patch<P>(&self, path: P) -> Result<String, CommitError>
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, WorkAreaError> {
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, CommitError> {
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>, CommitError> {
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()
.map_err(|err| GitError::subcommand("diff-tree", err))?;
if !diff_tree.status.success() {
return Err(CommitError::diff_tree(
sha1.clone(),
base.cloned(),
&diff_tree.stdout,
));
}
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(),
));
Ok(zip.map(move |(m, b, s)| {
DiffInfo {
old_mode: m.into(),
new_mode: new_mode.as_str().into(),
old_blob: CommitId::new(b),
new_blob: CommitId::new(new_blob.as_str()),
status: s.into(),
name: name.clone(),
}
}))
})
.collect::<Result<Vec<_>, FileNameError>>()?
.into_iter()
.flatten()
.collect())
}
fn file_patch(
ctx: &GitContext,
base: Option<&CommitId>,
sha1: &CommitId,
path: &OsStr,
) -> Result<String, CommitError> {
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()
.map_err(|err| GitError::subcommand("diff-tree -p", err))?;
if !diff_tree.status.success() {
return Err(CommitError::diff_patch(
sha1.clone(),
base.cloned(),
&diff_tree.stdout,
));
}
Ok(String::from_utf8_lossy(&diff_tree.stdout).into_owned())
}
#[cfg(test)]
mod tests {
use std::ffi::OsStr;
use std::path::Path;
use crates::git_workarea::{CommitId, GitContext, Identity};
use commit::{Commit, DiffInfo, FileName, StatusChange, Topic};
use test;
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)
}
fn compare_diffs(actual: &DiffInfo, expected: &DiffInfo) {
assert_eq!(actual.old_mode, expected.old_mode);
assert_eq!(actual.old_blob, expected.old_blob);
assert_eq!(actual.new_mode, expected.new_mode);
assert_eq!(actual.new_blob, expected.new_blob);
assert_eq!(actual.name, expected.name);
assert_eq!(actual.status, expected.status);
}
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);
compare_diffs(
&commit.diffs[0],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("16fe87b06e802f094b3fbb0894b137bca2b16ef1"),
name: FileName::Normal("LICENSE-APACHE".into()),
status: StatusChange::Added,
},
);
compare_diffs(
&commit.diffs[1],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("8f6f4b4a936616349e1f2846632c95cff33a3c5d"),
name: FileName::Normal("LICENSE-MIT".into()),
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);
compare_diffs(
&commit.diffs[0],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("fa8d85ac52f19959d6fc9942c265708b4b3c2b04"),
name: FileName::Normal(".gitignore".into()),
status: StatusChange::Added,
},
);
compare_diffs(
&commit.diffs[1],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("e31460ae61b1d69c6d0a58f9b74ae94d463c49bb"),
name: FileName::Normal("Cargo.toml".into()),
status: StatusChange::Added,
},
);
compare_diffs(
&commit.diffs[2],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("b3c9194ad2d95d1498361f70a5480f0433fad1bb"),
name: FileName::Normal("rustfmt.toml".into()),
status: StatusChange::Added,
},
);
compare_diffs(
&commit.diffs[3],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("584691748770380d5b70deb5f489b0c09229404e"),
name: FileName::Normal("src/lib.rs".into()),
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);
compare_diffs(
&commit.diffs[0],
&DiffInfo {
old_mode: "100644".into(),
old_blob: CommitId::new("5710fe019e3de5e07f2bd1a5bcfd2d462834e9d8"),
new_mode: "100644".into(),
new_blob: CommitId::new("db2811cc69a6fda16f6ef5d08cfc7780b46331d7"),
name: FileName::Normal("src/context.rs".into()),
status: StatusChange::Modified(None),
},
);
compare_diffs(
&commit.diffs[1],
&DiffInfo {
old_mode: "100644".into(),
old_blob: CommitId::new("2acafbc5acd2e7b70260a3e39a1a752cc46cf953"),
new_mode: "100644".into(),
new_blob: CommitId::new("af3a45a85ac6ba0d026279adac509c56308a3e26"),
name: FileName::Normal("src/run.rs".into()),
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);
compare_diffs(
&topic.diffs[0],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("fa8d85ac52f19959d6fc9942c265708b4b3c2b04"),
name: FileName::Normal(".gitignore".into()),
status: StatusChange::Added,
},
);
compare_diffs(
&topic.diffs[1],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("eadf0e09676dae16dbdf2c51d12f237e0ec663c6"),
name: FileName::Normal("Cargo.toml".into()),
status: StatusChange::Added,
},
);
compare_diffs(
&topic.diffs[2],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("b3c9194ad2d95d1498361f70a5480f0433fad1bb"),
name: FileName::Normal("rustfmt.toml".into()),
status: StatusChange::Added,
},
);
compare_diffs(
&topic.diffs[3],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("b43524e95861f3dccb3b1b2ce42e004ff8ba53cc"),
name: FileName::Normal("src/commit.rs".into()),
status: StatusChange::Added,
},
);
compare_diffs(
&topic.diffs[4],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("db2811cc69a6fda16f6ef5d08cfc7780b46331d7"),
name: FileName::Normal("src/context.rs".into()),
status: StatusChange::Added,
},
);
compare_diffs(
&topic.diffs[5],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("ea5b00b30d7caaf2ceb7396788b4f0493d84d282"),
name: FileName::Normal("src/hook.rs".into()),
status: StatusChange::Added,
},
);
compare_diffs(
&topic.diffs[6],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("1784b5969f5948334d6c29b7050bb97c54f3fe8f"),
name: FileName::Normal("src/lib.rs".into()),
status: StatusChange::Added,
},
);
compare_diffs(
&topic.diffs[7],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("af3a45a85ac6ba0d026279adac509c56308a3e26"),
name: FileName::Normal("src/run.rs".into()),
status: StatusChange::Added,
},
);
}
const QUOTED_PATHS: &str = "f536f44cf96b82e479d4973d5ea1cf78058bd1fb";
const QUOTED_PATHS_PARENT: &str = "1b2c7b3dad2fb7d14fbf0b619bf7faca4dd01243";
fn quoted_filename(raw: &[u8], readable: &str) -> FileName {
FileName::Quoted {
raw: raw.into(),
name: readable.into(),
}
}
#[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);
compare_diffs(
&commit.diffs[0],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
name: quoted_filename(b"control-character-\x03", r#""control-character-\003""#),
status: StatusChange::Added,
},
);
compare_diffs(
&commit.diffs[1],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
name: quoted_filename(b"invalid-utf8-\x80", r#""invalid-utf8-\200""#),
status: StatusChange::Added,
},
);
compare_diffs(
&commit.diffs[2],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
name: quoted_filename("non-ascii-é".as_bytes(), r#""non-ascii-\303\251""#),
status: StatusChange::Added,
},
);
compare_diffs(
&commit.diffs[3],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
name: quoted_filename(b"with whitespace", "with whitespace"),
status: StatusChange::Added,
},
);
compare_diffs(
&commit.diffs[4],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
name: quoted_filename(b"with-dollar-$", "with-dollar-$"),
status: StatusChange::Added,
},
);
compare_diffs(
&commit.diffs[5],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
name: quoted_filename(b"with-quote-\"", r#""with-quote-\"""#),
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")
.args(&[OsStr::new("-f"), config_path.as_ref()])
.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);
compare_diffs(
&commit.diffs[0],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
name: quoted_filename(b"control-character-\x03", r#""control-character-\003""#),
status: StatusChange::Added,
},
);
compare_diffs(
&commit.diffs[1],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
name: quoted_filename(b"invalid-utf8-\x80", r#""invalid-utf8-\200""#),
status: StatusChange::Added,
},
);
compare_diffs(
&commit.diffs[2],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
name: quoted_filename("non-ascii-é".as_bytes(), r#""non-ascii-\303\251""#),
status: StatusChange::Added,
},
);
compare_diffs(
&commit.diffs[3],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
name: quoted_filename(b"with whitespace", "with whitespace"),
status: StatusChange::Added,
},
);
compare_diffs(
&commit.diffs[4],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
name: quoted_filename(b"with-dollar-$", "with-dollar-$"),
status: StatusChange::Added,
},
);
compare_diffs(
&commit.diffs[5],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
name: quoted_filename(b"with-quote-\"", r#""with-quote-\"""#),
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);
compare_diffs(
&commit.diffs[0],
&DiffInfo {
old_mode: "100644".into(),
old_blob: CommitId::new("829636d299136b6651fd19aa6ac3500c7d50bf10"),
new_mode: "100644".into(),
new_blob: CommitId::new("195cc9d0aeb7324584700f2d17940d6a218a36f2"),
name: FileName::Normal("content".into()),
status: StatusChange::Modified(None),
},
);
compare_diffs(
&commit.diffs[1],
&DiffInfo {
old_mode: "100644".into(),
old_blob: CommitId::new("925b35841fdd59e002bc5b3e685dc158bdbe6ebf"),
new_mode: "100644".into(),
new_blob: CommitId::new("195cc9d0aeb7324584700f2d17940d6a218a36f2"),
name: FileName::Normal("content".into()),
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);
compare_diffs(
&commit.diffs[0],
&DiffInfo {
old_mode: "100644".into(),
old_blob: CommitId::new("2ef267e25bd6c6a300bb473e604b092b6a48523b"),
new_mode: "100644".into(),
new_blob: CommitId::new("829636d299136b6651fd19aa6ac3500c7d50bf10"),
name: FileName::Normal("content".into()),
status: StatusChange::Modified(None),
},
);
compare_diffs(
&commit.diffs[1],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("829636d299136b6651fd19aa6ac3500c7d50bf10"),
name: FileName::Normal("content".into()),
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);
compare_diffs(
&commit.diffs[0],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("7624577a217a08f1103d6c190eb11beab1081744"),
name: FileName::Normal("binary-data".into()),
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);
compare_diffs(
&topic.diffs[0],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("53c235a627abeda83e26d6d53cdebf72b52f3c3d"),
name: FileName::Normal("binary-data".into()),
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);
compare_diffs(
&commit.diffs[0],
&DiffInfo {
old_mode: "100644".into(),
old_blob: CommitId::new("7624577a217a08f1103d6c190eb11beab1081744"),
new_mode: "100644".into(),
new_blob: CommitId::new("53c235a627abeda83e26d6d53cdebf72b52f3c3d"),
name: FileName::Normal("binary-data".into()),
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);
compare_diffs(
&topic.diffs[0],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
name: FileName::Normal("simple-topic".into()),
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);
compare_diffs(
&topic.diffs[0],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
name: FileName::Normal("pre-merge-topic".into()),
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);
compare_diffs(
&topic.diffs[0],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("2ef267e25bd6c6a300bb473e604b092b6a48523b"),
name: FileName::Normal("content".into()),
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);
compare_diffs(
&topic.diffs[0],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
name: FileName::Normal("pre-merge-topic".into()),
status: StatusChange::Added,
},
);
compare_diffs(
&topic.diffs[1],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
name: FileName::Normal("simple-topic".into()),
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);
compare_diffs(
&topic.diffs[0],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("16fe87b06e802f094b3fbb0894b137bca2b16ef1"),
name: FileName::Normal("LICENSE-APACHE".into()),
status: StatusChange::Added,
},
);
compare_diffs(
&topic.diffs[1],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("8f6f4b4a936616349e1f2846632c95cff33a3c5d"),
name: FileName::Normal("LICENSE-MIT".into()),
status: StatusChange::Added,
},
);
compare_diffs(
&topic.diffs[2],
&DiffInfo {
old_mode: "100644".into(),
old_blob: CommitId::new("e4b0cf6006925052f1172d5dea15d9a7aec373f3"),
new_mode: "000000".into(),
new_blob: CommitId::new("0000000000000000000000000000000000000000"),
name: FileName::Normal("content".into()),
status: StatusChange::Deleted,
},
);
}
const ADD_A_COMMIT: &str = "80480d74af8d8a0d3de594606917da06ae518f49";
const ADD_B_COMMIT: &str = "0094392bd07fb7064a98b39791c5aefbcbd5a1e8";
const MERGE_B_INTO_A: &str = "2274b3051218fda7e2efb06ee23d3187a9217941";
const MERGE_A_INTO_B: &str = "6d460df5f502aa4cd855108ec54dcda3b4e86a32";
#[test]
fn test_topic_multiple_merge_base_a_as_base() {
let ctx = make_context();
let topic = Topic::new(
&ctx,
&CommitId::new(MERGE_B_INTO_A),
&CommitId::new(MERGE_A_INTO_B),
)
.unwrap();
assert_eq!(topic.base.as_str(), ADD_A_COMMIT);
assert_eq!(topic.sha1.as_str(), MERGE_A_INTO_B);
assert_eq!(topic.diffs.len(), 1);
compare_diffs(
&topic.diffs[0],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("d95f3ad14dee633a758d2e331151e950dd13e4ed"),
name: FileName::Normal("b".into()),
status: StatusChange::Added,
},
);
}
#[test]
fn test_topic_multiple_merge_base_b_as_base() {
let ctx = make_context();
let topic = Topic::new(
&ctx,
&CommitId::new(MERGE_A_INTO_B),
&CommitId::new(MERGE_B_INTO_A),
)
.unwrap();
assert_eq!(topic.base.as_str(), ADD_B_COMMIT);
assert_eq!(topic.sha1.as_str(), MERGE_B_INTO_A);
assert_eq!(topic.diffs.len(), 1);
compare_diffs(
&topic.diffs[0],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("d95f3ad14dee633a758d2e331151e950dd13e4ed"),
name: FileName::Normal("a".into()),
status: StatusChange::Added,
},
);
}
const ROOT_COMMIT_A: &str = "1b2c7b3dad2fb7d14fbf0b619bf7faca4dd01243";
const ROOT_COMMIT_B: &str = "7531e6df007ca1130e5d64b8627b3288844e38a4";
const MERGE_B_ROOT_INTO_A: &str = "e93b017d9284492e485a53fb5c7566cd4f0a8e6f";
const MERGE_A_ROOT_INTO_B: &str = "c1637e2d837ce657aa54df2d62b19b7e46249eb8";
#[test]
fn test_topic_multiple_merge_base_a_root_as_base() {
let ctx = make_context();
let topic = Topic::new(
&ctx,
&CommitId::new(MERGE_B_ROOT_INTO_A),
&CommitId::new(MERGE_A_ROOT_INTO_B),
)
.unwrap();
assert_eq!(topic.base.as_str(), ROOT_COMMIT_A);
assert_eq!(topic.sha1.as_str(), MERGE_A_ROOT_INTO_B);
assert_eq!(topic.diffs.len(), 2);
compare_diffs(
&topic.diffs[0],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("16fe87b06e802f094b3fbb0894b137bca2b16ef1"),
name: FileName::Normal("LICENSE-APACHE".into()),
status: StatusChange::Added,
},
);
compare_diffs(
&topic.diffs[1],
&DiffInfo {
old_mode: "000000".into(),
old_blob: CommitId::new("0000000000000000000000000000000000000000"),
new_mode: "100644".into(),
new_blob: CommitId::new("8f6f4b4a936616349e1f2846632c95cff33a3c5d"),
name: FileName::Normal("LICENSE-MIT".into()),
status: StatusChange::Added,
},
);
}
#[test]
fn test_topic_multiple_merge_base_b_root_as_base() {
let ctx = make_context();
let topic = Topic::new(
&ctx,
&CommitId::new(MERGE_A_ROOT_INTO_B),
&CommitId::new(MERGE_B_ROOT_INTO_A),
)
.unwrap();
assert_eq!(topic.base.as_str(), ROOT_COMMIT_B);
assert_eq!(topic.sha1.as_str(), MERGE_B_ROOT_INTO_A);
assert_eq!(topic.diffs.len(), 0);
}
}