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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! `zig-git diff-tree` compared with `git diff-tree` over a history with//! every kind of change: additions, deletions, modifications, mode//! changes, a file becoming a symbolic link, binary files, a gitlink,//! directories, attributes and odd names, in every output format.
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, contents: []const u8) !void { if (std.fs.path.dirname(path)) |d| try repo.tmp.dir.createDirPath(repo.io, d); try repo.tmp.dir.writeFile(repo.io, .{ .sub_path = path, .data = contents });}
fn remove(repo: *Repo, path: []const u8) !void { try repo.tmp.dir.deleteFile(repo.io, path);}
fn revParse(repo: *Repo, rev: []const u8) ![]u8 { const out = try repo.output(&.{ "rev-parse", rev }); defer repo.gpa.free(out); return repo.gpa.dupe(u8, std.mem.trimEnd(u8, out, "\n"));}
/// A history of two commits after a root, and a merge.pub fn makeHistory(repo: *Repo) !void { try write(repo, "mod", "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\n"); try write(repo, "gone", "gone\n"); try write(repo, "exec", "x\n"); try write(repo, "tolink", "a file\n"); try write(repo, "bin", "a\x00b"); try write(repo, "dir/sub/f", "deep\n"); try write(repo, "dir/g", "g\n"); try write(repo, "sp ace", "space\n"); try write(repo, "caf\xc3\xa9", "unicode\n"); try write(repo, "ws", "a b\nc\n"); try write(repo, "blank", "a\nb\n"); try write(repo, "attr.dat", "text but -diff\n"); try write(repo, ".gitattributes", "*.dat -diff\n"); try repo.git(&.{ "add", "." }); try repo.commit("root"); const root = try revParse(repo, "HEAD"); defer repo.gpa.free(root);
try write(repo, "mod", "one\n2\nthree\nfour\nfive\nsix\nseven\neight\nnine\n10\neleven\n"); try remove(repo, "gone"); try remove(repo, "tolink"); try repo.tmp.dir.symLink(repo.io, "target", "tolink", .{}); try write(repo, "bin", "a\x00c\x00d"); try write(repo, "dir/sub/f", "deeper\n"); try write(repo, "added", "new\n"); try write(repo, "ws", "a b\nc \n"); try write(repo, "blank", "a\n\nb\n"); try write(repo, "attr.dat", "changed text\n"); try repo.git(&.{ "add", "-A" }); try repo.git(&.{ "update-index", "--chmod=+x", "exec" }); const info = try std.fmt.allocPrint(repo.gpa, "160000,{s},vendor", .{root}); defer repo.gpa.free(info); try repo.git(&.{ "update-index", "--add", "--cacheinfo", info }); try repo.commit("second");
try repo.git(&.{ "checkout", "-q", "-f", "-b", "side", "HEAD~" }); try write(repo, "side-file", "side\n"); try repo.git(&.{ "add", "side-file" }); try repo.commit("side"); try repo.git(&.{ "checkout", "-q", "-f", "main" }); try repo.git(&.{ "merge", "-q", "--no-edit", "side" });}
test "diff-tree: every format over every kind of change" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); try makeHistory(&repo); const formats = [_][]const []const u8{ &.{}, &.{"-r"}, &.{"-t"}, &.{"-p"}, &.{"--raw"}, &.{"--stat"}, &.{"--numstat"}, &.{"--shortstat"}, &.{"--summary"}, &.{"--name-only"}, &.{"--name-status"}, &.{"-z"}, &.{ "-r", "-z" }, &.{ "-p", "-z" }, &.{"--abbrev"}, &.{"--abbrev=10"}, &.{"-R"}, &.{ "-p", "-R" }, &.{"--full-index"}, &.{ "-p", "-U0" }, &.{ "-p", "-U1" }, &.{ "-p", "-w" }, &.{ "-r", "-b" }, &.{"--ignore-blank-lines"}, &.{"--no-commit-id"}, &.{"-s"}, &.{"--patch-with-stat"}, &.{"--patch-with-raw"}, &.{ "-p", "-a" }, &.{ "--stat", "-a" }, &.{"--stat=40,10"}, &.{"--stat-count=2"}, &.{"--stat-graph-width=3"}, &.{"--diff-filter=AD"}, &.{"--diff-filter=m"}, &.{"--exit-code"}, &.{"--quiet"}, &.{ "--stat", "--shortstat", "--summary", "--numstat", "--raw", "-p" }, &.{ "-s", "--name-only" }, }; const gpa = testing.allocator; for (formats) |opts| { for ([_][]const []const u8{ &.{"HEAD~"}, &.{ "HEAD~2", "HEAD~" }, &.{"HEAD"} }) |revs| { var args: std.ArrayList([]const u8) = .empty; defer args.deinit(gpa); try args.append(gpa, "diff-tree"); try args.appendSlice(gpa, opts); try args.appendSlice(gpa, revs); try same(&repo, args.items); } } try same(&repo, &.{ "diff-tree", "-m", "HEAD" }); try same(&repo, &.{ "diff-tree", "-m", "-p", "HEAD" }); try same(&repo, &.{ "diff-tree", "--root", "-p", "HEAD~2" }); try same(&repo, &.{ "diff-tree", "--always", "HEAD~2" }); try same(&repo, &.{ "diff-tree", "--no-commit-id", "--always", "HEAD~" }); try same(&repo, &.{ "diff-tree", "HEAD~2..HEAD~" }); try same(&repo, &.{ "diff-tree", "HEAD~", "mod" }); try same(&repo, &.{ "diff-tree", "HEAD~2", "HEAD~", "mod", "nonexistent" }); try same(&repo, &.{ "diff-tree", "HEAD~2", "HEAD~", "nonexistent" }); try same(&repo, &.{ "diff-tree", "--bogus", "HEAD" });}
test "diff-tree: pathspecs, with and without -r and -t" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); try makeHistory(&repo); const gpa = testing.allocator; for ([_][]const u8{ "dir", "dir/sub/f", "dir/*", "d*", "*.c", "*/f", ":!dir", ":!d*", "dir/sub/nonexistent", ":(glob)dir/**" }) |spec| { for ([_][]const []const u8{ &.{}, &.{"-r"}, &.{"-t"}, &.{"--name-only"} }) |opts| { var args: std.ArrayList([]const u8) = .empty; defer args.deinit(gpa); try args.append(gpa, "diff-tree"); try args.appendSlice(gpa, opts); try args.appendSlice(gpa, &.{ "HEAD~", "--", spec }); try same(&repo, args.items); } }}
test "diff-tree: --stdin" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); try makeHistory(&repo); const gpa = testing.allocator; const c = try revParse(&repo, "HEAD~"); defer gpa.free(c); const t1 = try revParse(&repo, "HEAD~2^{tree}"); defer gpa.free(t1); const t2 = try revParse(&repo, "HEAD~^{tree}"); defer gpa.free(t2); const input = try std.fmt.allocPrint(gpa, "{s}\n{s} {s}\n", .{ c, t1, t2 }); defer gpa.free(input); try tool.expectSameAsGitHow(&repo, &.{ "diff-tree", "--stdin", "-r" }, .{ .input = input }); try tool.expectSameAsGitHow(&repo, &.{ "diff-tree", "--stdin", "-p" }, .{ .input = input });}