// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // 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\\> ", "--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); }