use std::path::Path;
use std::process::Command;
use git_stats::app;
use git_stats::model::{Options, SortBy};
use git_stats::repo::Repo;
fn git(dir: &Path, args: &[&str]) {
let status = Command::new("git")
.current_dir(dir)
.args(args)
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.status()
.expect("git should be installed");
assert!(status.success(), "git {args:?} failed");
}
fn options(range: &str, reviews: bool) -> Options {
Options {
range: range.to_string(),
email: false,
reviews,
sort: SortBy::Commits,
reverse: false,
authors: Vec::new(),
since: None,
until: None,
}
}
fn row<'a>(out: &'a str, label: &str) -> &'a str {
out.lines()
.find(|l| l.trim_start().starts_with(label))
.unwrap_or_else(|| panic!("no row starting with {label:?} in:\n{out}"))
}
fn report(repo: &Repo, opts: &Options) -> String {
yansi::disable();
app::run(repo, opts).unwrap()
}
fn git_available() -> bool {
Command::new("git").arg("--version").output().is_ok()
}
#[test]
fn reads_a_real_repository() {
if !git_available() {
eprintln!("git not available; skipping integration test");
return;
}
let dir = tempfile::tempdir().unwrap();
let p = dir.path();
git(p, &["init", "-q", "-b", "main"]);
git(p, &["config", "user.name", "Ada"]);
git(p, &["config", "user.email", "ada@example.com"]);
std::fs::write(p.join("a.txt"), "line1\nline2\n").unwrap();
git(p, &["add", "."]);
git(
p,
&[
"commit",
"-q",
"-m",
"first",
"-m",
"Reviewed-by: Rev Iewer <rev@example.com>",
],
);
std::fs::write(p.join("a.txt"), "line1\nline2\nline3\n").unwrap();
git(p, &["add", "."]);
git(p, &["commit", "-q", "-m", "second"]);
let repo = Repo::open(p).unwrap();
let out = report(&repo, &options("HEAD", true));
let ada = row(&out, "Ada");
assert!(ada.contains("+3"), "ada row: {ada}");
assert!(out.contains("Rev Iewer"), "reviews missing:\n{out}");
}
#[test]
fn range_excludes_commits_on_the_excluded_side() {
if !git_available() {
eprintln!("git not available; skipping integration test");
return;
}
let dir = tempfile::tempdir().unwrap();
let p = dir.path();
git(p, &["init", "-q", "-b", "main"]);
git(p, &["config", "user.name", "Ada"]);
git(p, &["config", "user.email", "ada@example.com"]);
std::fs::write(p.join("a.txt"), "a\n").unwrap();
git(p, &["add", "."]);
git(p, &["commit", "-q", "-m", "base on main"]);
git(p, &["checkout", "-q", "-b", "feature"]);
std::fs::write(p.join("b.txt"), "b\n").unwrap();
git(p, &["add", "."]);
git(p, &["commit", "-q", "-m", "feature 1"]);
std::fs::write(p.join("c.txt"), "c\n").unwrap();
git(p, &["add", "."]);
git(p, &["commit", "-q", "-m", "feature 2"]);
let repo = Repo::open(p).unwrap();
let out = report(&repo, &options("main..feature", false));
let total = row(&out, "Total");
assert_eq!(
total.split_whitespace().nth(1),
Some("2"),
"main..feature should count only the 2 feature commits:\n{out}"
);
}
#[test]
fn symmetric_difference_range_excludes_the_common_ancestor() {
if !git_available() {
eprintln!("git not available; skipping integration test");
return;
}
let dir = tempfile::tempdir().unwrap();
let p = dir.path();
git(p, &["init", "-q", "-b", "main"]);
git(p, &["config", "user.name", "Ada"]);
git(p, &["config", "user.email", "ada@example.com"]);
std::fs::write(p.join("a.txt"), "a\n").unwrap();
git(p, &["add", "."]);
git(p, &["commit", "-q", "-m", "base"]);
git(p, &["checkout", "-q", "-b", "feature"]);
std::fs::write(p.join("b.txt"), "b\n").unwrap();
git(p, &["add", "."]);
git(p, &["commit", "-q", "-m", "feature only"]);
git(p, &["checkout", "-q", "main"]);
std::fs::write(p.join("c.txt"), "c\n").unwrap();
git(p, &["add", "."]);
git(p, &["commit", "-q", "-m", "main only"]);
let repo = Repo::open(p).unwrap();
let out = report(&repo, &options("main...feature", false));
let total = row(&out, "Total");
assert_eq!(
total.split_whitespace().nth(1),
Some("2"),
"main...feature should count the 2 divergent commits:\n{out}"
);
}
fn tag_fixture() -> (tempfile::TempDir, Repo) {
let dir = tempfile::tempdir().unwrap();
let p = dir.path();
git(p, &["init", "-q", "-b", "main"]);
git(p, &["config", "user.name", "Ada"]);
git(p, &["config", "user.email", "ada@example.com"]);
for name in ["a", "b", "c"] {
std::fs::write(p.join(name).with_extension("txt"), "x\n").unwrap();
git(p, &["add", "."]);
git(p, &["commit", "-q", "-m", name]);
}
git(p, &["tag", "-a", "-m", "release", "annotated", "HEAD~1"]);
git(p, &["tag", "lightweight", "HEAD~1"]);
let repo = Repo::open(p).unwrap();
(dir, repo)
}
fn total_commits(out: &str) -> Option<&str> {
row(out, "Total").split_whitespace().nth(1)
}
#[test]
fn lightweight_tag_range_is_unaffected() {
if !git_available() {
eprintln!("git not available; skipping integration test");
return;
}
let (_dir, repo) = tag_fixture();
let out = report(&repo, &options("lightweight..HEAD", false));
assert_eq!(
total_commits(&out),
Some("1"),
"lightweight..HEAD should count only the commit after the tag:\n{out}"
);
}
#[test]
fn annotated_tag_range_excludes_commits_behind_the_tag() {
if !git_available() {
eprintln!("git not available; skipping integration test");
return;
}
let (_dir, repo) = tag_fixture();
let out = report(&repo, &options("annotated..HEAD", false));
assert_eq!(
total_commits(&out),
Some("1"),
"annotated..HEAD should count only the commit after the tag:\n{out}"
);
}
#[test]
fn annotated_tag_on_the_inclusion_side_resolves() {
if !git_available() {
eprintln!("git not available; skipping integration test");
return;
}
let (_dir, repo) = tag_fixture();
let out = report(&repo, &options("HEAD~2..annotated", false));
assert_eq!(
total_commits(&out),
Some("1"),
"HEAD~2..annotated should count only the tagged commit:\n{out}"
);
}
#[test]
fn a_bare_annotated_tag_resolves_to_its_target_commit() {
if !git_available() {
eprintln!("git not available; skipping integration test");
return;
}
let (dir, repo) = tag_fixture();
let out = report(&repo, &options("annotated", false));
assert_eq!(
total_commits(&out),
Some("2"),
"a bare annotated tag should walk from its target commit:\n{out}"
);
git(
dir.path(),
&["tag", "-a", "-m", "wrap", "nested", "annotated"],
);
let out = report(&repo, &options("nested", false));
assert_eq!(
total_commits(&out),
Some("2"),
"a tag-to-tag chain should peel to the same commit:\n{out}"
);
}
#[test]
fn symmetric_difference_with_an_annotated_tag_resolves() {
if !git_available() {
eprintln!("git not available; skipping integration test");
return;
}
let dir = tempfile::tempdir().unwrap();
let p = dir.path();
git(p, &["init", "-q", "-b", "main"]);
git(p, &["config", "user.name", "Ada"]);
git(p, &["config", "user.email", "ada@example.com"]);
std::fs::write(p.join("a.txt"), "a\n").unwrap();
git(p, &["add", "."]);
git(p, &["commit", "-q", "-m", "base"]);
git(p, &["checkout", "-q", "-b", "feature"]);
std::fs::write(p.join("b.txt"), "b\n").unwrap();
git(p, &["add", "."]);
git(p, &["commit", "-q", "-m", "feature only"]);
git(p, &["tag", "-a", "-m", "release", "feature-tag"]);
git(p, &["checkout", "-q", "main"]);
std::fs::write(p.join("c.txt"), "c\n").unwrap();
git(p, &["add", "."]);
git(p, &["commit", "-q", "-m", "main only"]);
let repo = Repo::open(p).unwrap();
let out = report(&repo, &options("main...feature-tag", false));
assert_eq!(
total_commits(&out),
Some("2"),
"main...feature-tag should count the 2 divergent commits:\n{out}"
);
}
#[test]
fn merge_commits_are_counted_but_add_no_lines() {
if !git_available() {
eprintln!("git not available; skipping integration test");
return;
}
let dir = tempfile::tempdir().unwrap();
let p = dir.path();
git(p, &["init", "-q", "-b", "main"]);
git(p, &["config", "user.name", "Ada"]);
git(p, &["config", "user.email", "ada@example.com"]);
std::fs::write(p.join("a.txt"), "a\n").unwrap();
git(p, &["add", "."]);
git(p, &["commit", "-q", "-m", "base"]);
git(p, &["checkout", "-q", "-b", "feature"]);
std::fs::write(p.join("b.txt"), "b\n").unwrap();
git(p, &["add", "."]);
git(p, &["commit", "-q", "-m", "feature"]);
git(p, &["checkout", "-q", "main"]);
std::fs::write(p.join("c.txt"), "c\n").unwrap();
git(p, &["add", "."]);
git(p, &["commit", "-q", "-m", "main"]);
git(p, &["merge", "--no-ff", "--no-edit", "feature"]);
let repo = Repo::open(p).unwrap();
let out = report(&repo, &options("HEAD", false));
let cols: Vec<&str> = row(&out, "Total").split_whitespace().collect();
assert_eq!(cols[1], "4", "merge commit should be counted:\n{out}");
assert_eq!(
cols[3], "+3",
"merge should contribute no insertions:\n{out}"
);
}