// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! `zig-git diff-index` and `zig-git diff-files` compared with git's: the //! working tree changed every way a file can change, staged and not, //! intent-to-add entries, racily clean files and unmerged paths. The //! conversions on the way in are `convert_tests.zig`'s. const std = @import("std"); const testing = std.testing; const tool = @import("zig_git.zig"); const Repo = tool.Repo; const diff_tree_tests = @import("diff_tree_tests.zig"); 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 chmod(repo: *Repo, mode: []const u8, path: []const u8) !void { const r = try std.process.run(testing.allocator, testing.io, .{ .argv = &.{ "chmod", mode, path }, .cwd = .{ .path = repo.path } }); testing.allocator.free(r.stdout); testing.allocator.free(r.stderr); } fn changeWorktree(repo: *Repo) !void { try write(repo, "mod", "one\nTWO\nthree\nfour\nfive\nsix\nseven\neight\nnine\n10\neleven\n"); try repo.tmp.dir.deleteFile(repo.io, "sp ace"); try chmod(repo, "+x", "ws"); try write(repo, "ws", "a b\nc \n"); try write(repo, "staged", "staged\n"); try repo.git(&.{ "add", "staged" }); try write(repo, "staged", "staged, then changed\n"); try write(repo, "ita", "intent to add\n"); try repo.git(&.{ "add", "-N", "ita" }); try repo.tmp.dir.deleteFile(repo.io, "exec"); try repo.tmp.dir.symLink(repo.io, "elsewhere", "exec", .{}); try write(repo, "dir/g", "g\nmore\n"); try repo.git(&.{ "rm", "-q", "--cached", "blank" }); try write(repo, "bin", "a\x00c\x00d\x00e"); } test "diff-index and diff-files: every format against a changed working tree" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); try diff_tree_tests.makeHistory(&repo); try changeWorktree(&repo); const formats = [_][]const []const u8{ &.{}, &.{"-p"}, &.{"--stat"}, &.{"--numstat"}, &.{"--shortstat"}, &.{"--summary"}, &.{"--name-only"}, &.{"--name-status"}, &.{"-z"}, &.{"--abbrev"}, &.{"-R"}, &.{ "-p", "-R" }, &.{ "-p", "-U1" }, &.{ "-p", "-w" }, &.{"-w"}, &.{ "--stat", "-w" }, &.{"--exit-code"}, &.{"--quiet"}, &.{"--patch-with-stat"}, &.{"--diff-filter=MT"}, &.{ "-p", "--", "dir" }, }; const gpa = testing.allocator; for (formats) |opts| { for ([_][]const []const u8{ &.{"diff-files"}, &.{ "diff-index", "HEAD" }, &.{ "diff-index", "--cached", "HEAD" }, &.{ "diff-index", "-m", "HEAD~" } }) |cmd| { var args: std.ArrayList([]const u8) = .empty; defer args.deinit(gpa); try args.appendSlice(gpa, cmd); try args.appendSlice(gpa, opts); try same(&repo, args.items); } } try same(&repo, &.{ "diff-index", "--bogus", "HEAD" }); try same(&repo, &.{"diff-index"}); try same(&repo, &.{ "diff-files", "--bogus" }); } test "diff-files: unmerged paths and their stages" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); try write(&repo, "f", "l1\nl2\nl3\n"); try write(&repo, "k", "k\n"); try repo.git(&.{ "add", "." }); try repo.commit("base"); try repo.git(&.{ "checkout", "-q", "-b", "side" }); try write(&repo, "f", "l1\nside\nl3\n"); try repo.git(&.{ "commit", "-q", "-a", "-m", "side" }); try repo.git(&.{ "checkout", "-q", "main" }); try write(&repo, "f", "l1\nmain\nl3\n"); try write(&repo, "k", "k2\n"); try repo.git(&.{ "commit", "-q", "-a", "-m", "main" }); // The merge stops on the conflict, which git reports by failing. const merge = try repo.runIn(".", &.{ "merge", "-q", "side" }, ""); merge.deinit(repo.gpa); for ([_][]const []const u8{ &.{"diff-files"}, &.{ "diff-files", "-1", "-p" }, &.{ "diff-files", "-2", "-p" }, &.{ "diff-files", "--theirs", "-p" }, &.{ "diff-files", "-0", "-p" }, &.{ "diff-files", "--stat" }, &.{ "diff-files", "--numstat" }, &.{ "diff-files", "-R", "-2" }, &.{ "diff-index", "-p", "HEAD" }, &.{ "diff-index", "--cached", "HEAD" }, &.{ "diff-index", "--cached", "-p", "HEAD" }, &.{ "diff-index", "--cached", "--stat", "HEAD" }, }) |args| try same(&repo, args); } test "diff: the porcelain against the index, a commit and two commits" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); try diff_tree_tests.makeHistory(&repo); try changeWorktree(&repo); const gpa = testing.allocator; const formats = [_][]const []const u8{ &.{}, &.{"--raw"}, &.{"--stat"}, &.{"--numstat"}, &.{"--name-status"}, &.{"--no-renames"}, &.{ "--raw", "--no-abbrev" }, &.{"-R"}, &.{"-U1"}, &.{"--exit-code"}, &.{"--quiet"}, &.{"-w"}, &.{"--summary"}, &.{"--patch-with-stat"}, }; for (formats) |opts| { for ([_][]const []const u8{ &.{}, &.{"--cached"}, &.{"HEAD"}, &.{ "HEAD~2", "HEAD~" }, &.{"HEAD~2..HEAD~"} }) |revs| { var args: std.ArrayList([]const u8) = .empty; defer args.deinit(gpa); try args.append(gpa, "diff"); try args.appendSlice(gpa, opts); try args.appendSlice(gpa, revs); try same(&repo, args.items); } } for ([_][]const u8{ "diff.context=1", "diff.noprefix=true", "diff.mnemonicPrefix=true", "diff.statGraphWidth=5", "diff.renames=false" }) |cfg| { try same(&repo, &.{ "-c", cfg, "diff", "--stat", "-p", "HEAD" }); try same(&repo, &.{ "-c", cfg, "diff", "--cached" }); } try same(&repo, &.{ "diff", "HEAD", "--", "dir" }); try same(&repo, &.{ "diff", "--bogus" }); } test "diff commands: a bad boolean names no file, wherever it was set" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); try diff_tree_tests.makeHistory(&repo); try write(&repo, "x", "x\n"); try write(&repo, "y", "y\n"); const keys = [_][]const u8{ "diff.indentHeuristic", "diff.suppressBlankEmpty", "core.quotePath", "diff.noprefix", "diff.mnemonicPrefix", "diff.renames", "diff.relative", "diff.autoRefreshIndex", }; const commands = [_][]const []const u8{ &.{ "diff", "HEAD~1" }, &.{ "diff-tree", "-p", "HEAD" }, &.{ "log", "-1", "-p" }, &.{"show"}, &.{ "diff-index", "-p", "HEAD" }, &.{ "diff-files", "-p" }, &.{ "diff", "--no-index", "x", "y" }, }; const gpa = testing.allocator; for (keys) |key| { const setting = try std.fmt.allocPrint(gpa, "{s}=bogus", .{key}); defer gpa.free(setting); for (commands) |command| { var args: std.ArrayList([]const u8) = .empty; defer args.deinit(gpa); try args.appendSlice(gpa, &.{ "-c", setting }); try args.appendSlice(gpa, command); try same(&repo, args.items); // And from the repository's own file. try repo.git(&.{ "config", key, "bogus" }); try same(&repo, command); try repo.git(&.{ "config", "--unset", key }); } } }