Something went wrong. Try again.
A Zig library that reads git repositories without running git, with log, show and diff output byte-identical to git's.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The diffcore options compared with git: the pickaxe (`-S`, `-G`,//! `--pickaxe-regex`, `--pickaxe-all`, `--find-object`), orderfiles,//! `--skip-to`, `--rotate-to`, `--relative`, `--line-prefix`, `-D` and//! `--check`, through `diff`, `diff-tree`, `log` and `show`. Each case//! pins a probe that taught something.
const std = @import("std");const testing = std.testing;
const tool = @import("zig_git.zig");const Repo = tool.Repo;
fn same(repo: *Repo, args: []const []const u8) !void { try tool.expectSameAsGitHow(repo, args, .{});}
fn write(repo: *Repo, path: []const u8, data: []const u8) !void { if (std.fs.path.dirname(path)) |dir| try repo.tmp.dir.createDirPath(repo.io, dir); try repo.tmp.dir.writeFile(repo.io, .{ .sub_path = path, .data = data });}
/// A history of small edits whose counts of `aa`, `foo` and `^a` tell/// the pickaxe's rules apart, a rename, a binary file and a merge.fn makeHistory(repo: *Repo) !void { try write(repo, "f", "x\n"); try write(repo, "dir/g", "foo\nbar\n"); try write(repo, "README", "read me\n"); try repo.git(&.{ "add", "." }); try repo.commit("c0"); for ([_][]const u8{ "aaa\n", "aaaa\n", "aaaaa\n", "aa aa\n", "a\na\n", "\na\n", "x\na\x00a\n" }, 0..) |text, i| { try write(repo, "f", text); var buf: [16]u8 = undefined; try repo.git(&.{ "add", "." }); try repo.commit(try std.fmt.bufPrint(&buf, "f{d}", .{i})); } try repo.git(&.{ "mv", "dir/g", "dir/h" }); try write(repo, "dir/new.c", "foo \n\tbad \n<<<<<<< ours\n"); try repo.git(&.{ "add", "-A" }); try repo.commit("move"); try repo.git(&.{ "checkout", "-q", "-b", "side", "HEAD~2" }); try write(repo, "side", "foo\n"); try repo.git(&.{ "add", "-A" }); try repo.commit("side"); try repo.git(&.{ "checkout", "-q", "main" }); try repo.git(&.{ "merge", "-q", "--no-edit", "side" });}
test "the pickaxe is git's" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try makeHistory(&repo); const cases = [_][]const []const u8{ // Fixed strings are counted without overlapping, NUL bytes and // all. &.{ "log", "--format=%s", "-Saa" }, &.{ "log", "--format=%s", "-Sa" }, // A pattern's empty matches count, and `^` does not match where // a search resumes. &.{ "log", "--format=%s", "-Sa*", "--pickaxe-regex" }, &.{ "log", "--format=%s", "-S^a", "--pickaxe-regex" }, &.{ "log", "--format=%s", "-S\\<a", "--pickaxe-regex" }, &.{ "log", "--format=%s", "-SA", "-i" }, &.{ "log", "--format=%s", "-Ga" }, &.{ "log", "--format=%s", "-G^$" }, &.{ "log", "--format=%s", "-G^\\+" }, &.{ "log", "--format=%s", "-Gx", "-a" }, &.{ "log", "--format=%s", "--raw", "-Sfoo" }, &.{ "log", "--format=%s", "--name-status", "-Sfoo", "--pickaxe-all" }, &.{ "log", "--format=%s", "--name-status", "-Sfoo", "--pickaxe-all", "--diff-filter=A" }, &.{ "log", "--format=%s", "--name-status", "-Sfoo", "-m" }, &.{ "log", "--format=%s", "-Sfoo", "-c" }, &.{ "show", "--format=%s", "--raw", "--find-object=HEAD:side", "HEAD^2" }, &.{ "diff-tree", "-r", "--find-object=HEAD:side", "HEAD~2", "HEAD" }, &.{ "show", "-Sfoo", "HEAD~1" }, &.{ "show", "-m", "--name-status", "-Sfoo" }, &.{ "diff-tree", "-Sfoo", "HEAD~2", "HEAD" }, &.{ "diff-tree", "-r", "-Gfoo", "-p", "HEAD~3", "HEAD" }, &.{ "diff", "--stat", "-Sfoo", "HEAD~3", "HEAD" }, // git's errors. &.{ "log", "-Sa", "-Gb" }, &.{ "log", "-G[" }, &.{ "log", "-Gx", "--pickaxe-regex" }, &.{ "log", "-S" }, &.{ "log", "-G", "" }, &.{ "log", "--find-object=nothing" }, &.{ "log", "--find-object=HEAD:side", "--pickaxe-all" }, }; for (cases) |args| try same(&repo, args);}
/// Enough lines that two near copies of them score as renames.const long_text = "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\n";
test "orders, rotations, prefixes and irreversible deletions are git's" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try write(&repo, "a/x.c", "1\n"); try write(&repo, "a/y.h", "1\n"); try write(&repo, "ab/q", "1\n"); try write(&repo, "b/z.c", "1\n"); try write(&repo, "README", "r\n"); try write(&repo, "del", "gone\nline2\n"); try write(&repo, "long", long_text); try repo.git(&.{ "add", "." }); try repo.commit("one"); try write(&repo, "a/x.c", "2\n"); try write(&repo, "a/y.h", "2\n"); try write(&repo, "ab/q", "2\n"); try write(&repo, "b/z.c", "2\n"); try write(&repo, "README", "r2\n"); try repo.tmp.dir.deleteFile(repo.io, "del"); try repo.git(&.{ "add", "-A" }); try repo.commit("two"); try write(&repo, ".git/order", "*.h\n\n# comment\nb\nREADME\n"); try write(&repo, ".git/order-cut", "y.h\n"); try write(&repo, ".git/order-whole", "a/y.h\n"); const cases = [_][]const []const u8{ &.{ "diff", "--name-only", "-O.git/order", "HEAD~", "HEAD" }, &.{ "diff-tree", "-r", "--name-only", "-O", ".git/order", "HEAD" }, &.{ "-c", "diff.orderFile=.git/order", "diff", "--name-only", "HEAD~", "HEAD" }, &.{ "-c", "diff.orderFile=.git/order", "diff-tree", "-r", "--name-only", "HEAD" }, &.{ "-c", "diff.orderFile=.git/order", "diff", "--name-only", "-O/dev/null", "HEAD~", "HEAD" }, &.{ "diff", "--name-only", "-Omissing", "HEAD~", "HEAD" }, &.{ "diff", "--name-only", "--skip-to=b/z.c", "HEAD~", "HEAD" }, &.{ "diff", "--name-only", "--rotate-to=b/z.c", "HEAD~", "HEAD" }, &.{ "diff", "--name-only", "--rotate-to=b", "HEAD~", "HEAD" }, &.{ "log", "--name-only", "--format=%s", "--skip-to=b" }, &.{ "log", "--name-only", "--format=%s", "--rotate-to=b" }, &.{ "log", "--name-only", "--format=%s", "--skip-to=zzz" }, // The prefix is matched as a string: `a` takes `ab/q` too. &.{ "diff", "--name-status", "--relative=a", "HEAD~", "HEAD" }, &.{ "diff", "-p", "--relative=a/", "HEAD~", "HEAD" }, &.{ "diff-tree", "-r", "--stat", "--relative=b", "HEAD" }, &.{ "log", "--format=%s", "--raw", "--relative=a/" }, // The orderfile and the rotation look at the whole paths, before // --relative cuts them: `xyz` sorts after `a/y.h`, not before // `y.h`, and `y.h` names no path. &.{ "log", "--relative=a/", "--rotate-to=xyz", "-D", "-R", "--patch-with-raw", "--oneline" }, &.{ "log", "--relative=a/", "--rotate-to=a/y.h", "--name-only", "--format=%s" }, &.{ "diff", "--relative=a/", "--rotate-to=y.h", "--name-only", "HEAD~", "HEAD" }, &.{ "diff", "--relative=a/", "--rotate-to=a/y.h", "--name-only", "HEAD~", "HEAD" }, &.{ "diff", "--relative=a", "--skip-to=ab/q", "--name-only", "HEAD~", "HEAD" }, &.{ "diff", "--relative=a/", "-O.git/order-cut", "--name-only", "HEAD~", "HEAD" }, &.{ "diff-tree", "-r", "--relative=a/", "-O.git/order-whole", "--name-only", "HEAD" }, &.{ "diff", "--line-prefix=>> ", "--stat", "-p", "HEAD~", "HEAD" }, &.{ "diff-tree", "-r", "--line-prefix=>> ", "--stat", "HEAD" }, &.{ "log", "--line-prefix=| ", "--stat" }, &.{ "show", "--line-prefix=| ", "--format=%s", "HEAD" }, &.{ "diff", "-D", "HEAD~", "HEAD" }, &.{ "diff", "-D", "-R", "HEAD~", "HEAD" }, }; for (cases) |args| try same(&repo, args);
// Which of a deleted source's pairs is the rename goes by the order // written, the pairs --skip-to drops counted: `long` moves to two // places. try repo.tmp.dir.deleteFile(repo.io, "long"); try write(&repo, "a/moved", long_text ++ "a\n"); try write(&repo, "copy/z", long_text ++ "z\nz\n"); try repo.git(&.{ "add", "-A" }); try repo.commit("three"); try write(&repo, ".git/order-copy", "copy/z\n"); const moves = [_][]const []const u8{ &.{ "diff", "-C", "--name-status", "HEAD~", "HEAD" }, &.{ "diff", "-C", "--name-status", "--rotate-to=copy/z", "HEAD~", "HEAD" }, &.{ "diff", "-C", "--name-status", "--skip-to=copy/z", "HEAD~", "HEAD" }, &.{ "diff", "-C", "--name-status", "-O.git/order-copy", "HEAD~", "HEAD" }, &.{ "log", "-C", "--summary", "--format=%s", "--rotate-to=copy/z" }, }; for (moves) |args| try same(&repo, args);}
test "--check is git's" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try write(&repo, "f", "ok\n"); try write(&repo, "g", "x\n\n"); try repo.git(&.{ "add", "." }); try repo.commit("one"); try write(&repo, "f", "ok\ntrail \n \t\tsbt\n eight\n\ttab\n\t \tx\n<<<<<<< a\n=======\n>>>>>>> b\n|||||||\x0cx\ncr\r\n \n\n\n"); try write(&repo, "g", "x\n\n\n"); try write(&repo, "h", "a\nno newline "); try write(&repo, ".gitattributes", "h whitespace\n"); try repo.git(&.{ "add", "." }); try repo.commit("two"); const settings = [_][]const u8{ "", "indent-with-non-tab", "tab-in-indent", "cr-at-eol,-space-before-tab", "-blank-at-eol,tab-in-indent,tabwidth=4", "-trailing-space", "tabwidth=x", "tab-in-indent,indent-with-non-tab" }; for (settings) |ws| { var buf: [96]u8 = undefined; const c = try std.fmt.bufPrint(&buf, "core.whitespace={s}", .{ws}); try same(&repo, &.{ "-c", c, "diff", "--check", "HEAD~", "HEAD" }); try same(&repo, &.{ "-c", c, "log", "--check", "--format=%s" }); } try same(&repo, &.{ "diff", "--check", "--exit-code", "HEAD~", "HEAD" }); try same(&repo, &.{ "diff", "--check", "-R", "HEAD~", "HEAD" }); try same(&repo, &.{ "diff-tree", "--check", "-r", "HEAD" }); try same(&repo, &.{ "diff", "--check", "--name-only", "HEAD~", "HEAD" });}
test "--dirstat and whatchanged are git's" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try write(&repo, "top", "t\n"); try write(&repo, "a/x", "x\n"); try write(&repo, "a/b/y", "y\n"); try write(&repo, "a/b/c/z", "z\n"); try write(&repo, "q/r/s/t", "x\n"); try write(&repo, "crlf/f", "a\r\nb\r\n"); try repo.git(&.{ "add", "." }); try repo.commit("one"); try write(&repo, "top", "t\nmore\nlines\n"); try write(&repo, "a/x", "x\nx2\n"); try write(&repo, "a/b/y", "Y\n"); try write(&repo, "a/b/c/z", "z\nzz\nzzz\nzzzz\n"); try write(&repo, "q/r/s/t", "y\n"); try write(&repo, "crlf/f", "a\r\nc\r\n"); try write(&repo, "new/empty", ""); try repo.git(&.{ "add", "." }); try repo.commit("two"); try repo.git(&.{ "commit", "-q", "--allow-empty", "-m", "empty" }); const cases = [_][]const []const u8{ &.{ "diff", "--dirstat", "HEAD~2", "HEAD" }, &.{ "diff", "--dirstat=0", "HEAD~2", "HEAD" }, &.{ "diff", "--dirstat=lines,0,cumulative", "-p", "HEAD~2", "HEAD" }, &.{ "diff", "--dirstat=files,1.25", "HEAD~2", "HEAD" }, &.{ "diff-tree", "-r", "-X10", "--stat", "HEAD~" }, &.{ "-c", "diff.dirstat=files,0", "diff-tree", "-r", "--dirstat", "HEAD~" }, &.{ "-c", "diff.dirstat=bogus", "diff", "--dirstat", "HEAD~2", "HEAD" }, &.{ "diff", "--dirstat=x5,", "HEAD~2", "HEAD" }, &.{ "log", "--format=%s", "--cumulative" }, &.{"whatchanged"}, &.{ "whatchanged", "--i-still-use-this" }, &.{ "whatchanged", "--i-still-use-this", "-p", "--format=%s" }, &.{ "whatchanged", "--i-still-use-this", "-s", "--oneline" }, }; for (cases) |args| try same(&repo, args);}