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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Function lines and function context compared with git's: the text//! after `@@` that `diff.<driver>.xfuncname` and `funcname` choose, with//! negated patterns, drivers the attributes name but nothing configures,//! and git's errors for bad patterns; and `-W`, with git's default rule//! and with drivers, under SHA-1 and SHA-256.
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 });}
/// A file of candidate function lines, each followed by enough lines/// that a change after it makes a hunk whose function line it is, and/// the same file with every such line after a candidate changed.fn candidates(gpa: std.mem.Allocator, lines: []const []const u8, changed: bool) ![]u8 { var out: std.ArrayList(u8) = .empty; errdefer out.deinit(gpa); for (lines, 0..) |l, i| { try out.print(gpa, "{s}\n=1\n=2\n=3\n=4\n={s}{d}\n=5\n=6\n=7\n=8\n", .{ l, if (changed) "new" else "old", i }); } return out.toOwnedSlice(gpa);}
const lines_of_code = [_][]const u8{ "def plain():", " def indented(x):", "\tclass Tabbed:", "int main(int argc, char **argv)", "static void helper(void) {", "sub perlish {", "!bang line", " ", "", "bar x", "baz", "foo", "\\section{Intro}", "a very long function line that goes on and on past the eighty bytes git keeps after @@ x", "caf\xc3\xa9 \xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9", "trailing space \t", "$dollar", "_under",};
fn makeCandidates(repo: *Repo) !void { const gpa = testing.allocator; const old = try candidates(gpa, &lines_of_code, false); defer gpa.free(old); const new = try candidates(gpa, &lines_of_code, true); defer gpa.free(new); try write(repo, ".gitattributes", \\*.t diff=t \\*.u diff=u \\*.none diff=nothing \\*.set diff \\*.unset -diff \\*.py diff=python \\ ); for ([_][]const u8{ "a.t", "b.u", "c.none", "d.set", "e.unset", "f.py", "g.plain" }) |name| try write(repo, name, old); try repo.git(&.{ "add", "." }); try repo.git(&.{ "commit", "-q", "-m", "candidates" }); for ([_][]const u8{ "a.t", "b.u", "c.none", "d.set", "e.unset", "f.py", "g.plain" }) |name| try write(repo, name, new);}
test "function lines: xfuncname and funcname as git chooses them" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); try makeCandidates(&repo); const configs = [_][]const []const u8{ &.{}, &.{ "-c", "diff.t.xfuncname=^[a-z]+" }, &.{ "-c", "diff.t.xfuncname=^[ \t]*(def|class)[ \t]+(.*)$" }, &.{ "-c", "diff.t.xfuncname=^[ \t]*((def|class)[ \t]+.*)$" }, &.{ "-c", "diff.t.xfuncname=!bar x\nb(.*)" }, &.{ "-c", "diff.t.xfuncname=!^ba\n^(b.*)|^(f.*)" }, &.{ "-c", "diff.t.xfuncname=(b.*)\n!bar\nfoo" }, &.{ "-c", "diff.t.xfuncname=foo\n" }, &.{ "-c", "diff.t.xfuncname=" }, &.{ "-c", "diff.t.xfuncname=^( *)" }, &.{ "-c", "diff.t.xfuncname=(.*)" }, &.{ "-c", "diff.t.xfuncname=\\w+" }, &.{ "-c", "diff.t.xfuncname=^(x)?(.*)" }, &.{ "-c", "diff.t.funcname=^\\(sub\\|def\\) \\(.*\\)" }, &.{ "-c", "diff.t.funcname=^[a-z]\\{3\\}" }, &.{ "-c", "diff.t.xfuncname=^d", "-c", "diff.t.funcname=^b" }, &.{ "-c", "diff.t.funcname=^b", "-c", "diff.t.xfuncname=^d" }, &.{ "-c", "diff.T.xfuncname=^b" }, &.{ "-c", "diff.u.xfuncname=^s", "-c", "diff.t.xfuncname=^\\$|^_" }, &.{ "-c", "diff.python.xfuncname=^b" }, &.{ "-c", "diff.python.funcname=^\\(f\\|b\\)" }, &.{ "-c", "diff.nothing.binary=true" }, &.{ "-c", "diff.set.xfuncname=^b", "-c", "diff.unset.xfuncname=^b" }, }; const gpa = testing.allocator; for (configs) |config| { for ([_][]const []const u8{ &.{"diff"}, &.{ "diff", "-U1" }, &.{ "diff", "-W" }, &.{ "diff-files", "-p" }, &.{ "diff", "--stat" } }) |cmd| { // f.py's built-in driver, where nothing replaces it, has // tests of its own below. const replaced = for (config) |c| { if (std.mem.startsWith(u8, c, "diff.python.")) break true; } else false; var args: std.ArrayList([]const u8) = .empty; defer args.deinit(gpa); try args.appendSlice(gpa, config); try args.appendSlice(gpa, cmd); if (!replaced) try args.appendSlice(gpa, &.{ "--", ".", ":!f.py" }); try same(&repo, args.items); } } try same(&repo, &.{ "-c", "diff.t.xfuncname=^d", "log", "-p", "-1", "--format=%s", "--", "a.t", "b.u" }); try same(&repo, &.{ "-c", "diff.t.xfuncname=^d", "diff-tree", "-p", "-W", "HEAD", "--", "a.t" });}
test "function lines: git's errors for bad patterns" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); try makeCandidates(&repo); for ([_][]const u8{ "diff.t.xfuncname=a(", "diff.t.xfuncname=foo\na(", "diff.t.xfuncname=!a(\nb", "diff.t.funcname=a\\(", "diff.t.xfuncname=b(.*)\n!bar", "diff.t.xfuncname=!bar", "diff.t.xfuncname=foo\n!", "diff.t.xfuncname=[[:foo:]]", "diff.t.xfuncname", "diff.t.funcname", }) |config| { try same(&repo, &.{ "-c", config, "diff", "--", ".", ":!f.py" }); try same(&repo, &.{ "-c", config, "diff", "--stat", "--", ".", ":!f.py" }); try same(&repo, &.{ "-c", config, "diff-tree", "-r", "HEAD", "HEAD" }); } // A pattern git takes and zig-git cannot is refused by name. const ours = try tool.runOurs(&repo, &.{ "-c", "diff.t.xfuncname=(a)\\1", "diff", "a.t" }, .{}); defer ours.deinit(repo.gpa); try testing.expectEqual(@as(u8, 128), ours.code); try testing.expect(std.mem.find(u8, ours.stderr, "not supported") != null);}
test "function context: -W with git's default rule" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); // Functions with bodies, blank lines and the lines just above a // function line, which -W counts as part of it; and the ends of the // text, where git does odd things. try write(&repo, "a", \\ preamble \\f1 \\ 1 \\ 2 \\ 3 \\ 4 \\ 5 \\ \\ comment for f2 \\f2 \\ 6 \\ 7 \\ 8 \\ 9 \\ 10 \\ 11 \\ \\ \\f3 \\ 12 \\ 13 \\f4 \\ 14 \\ 15 \\ 16 \\ 17 \\ 18 \\ ); try write(&repo, "b", \\ preamble \\f1 \\ 1 \\ 2 changed \\ 3 \\ 4 \\ 5 \\ \\ comment for f2 \\f2 \\ 6 \\ 7 \\ 8 \\ 9 \\ 10 \\ 11 changed \\ \\ \\f3 \\ 12 \\ 13 \\f4 \\ 14 \\ 15 \\ 16 \\ 17 \\ 18 \\f5 \\ 19 \\ ); try write(&repo, "c", " x\n y\n z\n w\n"); try write(&repo, "d", " x\n y\n Z\n w\n\n"); for ([_][]const []const u8{ &.{"-W"}, &.{ "-W", "-U0" }, &.{ "-W", "-U1" }, &.{ "-W", "-U8" }, &.{ "-W", "--inter-hunk-context=3" }, &.{"--function-context"}, &.{ "-W", "--no-function-context" }, &.{ "-W", "-w" }, &.{ "-W", "-R" }, }) |opts| { for ([_][2][]const u8{ .{ "a", "b" }, .{ "b", "a" }, .{ "c", "d" }, .{ "a", "c" } }) |pair| { var args: std.ArrayList([]const u8) = .empty; defer args.deinit(testing.allocator); try args.appendSlice(testing.allocator, &.{ "diff", "--no-index" }); try args.appendSlice(testing.allocator, opts); try args.appendSlice(testing.allocator, &pair); try same(&repo, args.items); } } try same(&repo, &.{ "diff", "--no-index", "--function-context=1", "a", "b" });}
test "function context: -W with drivers, and what it is refused with" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); try makeCandidates(&repo); for ([_][]const []const u8{ &.{ "-c", "diff.t.xfuncname=^b", "diff", "-W", "--", ".", ":!f.py" }, &.{ "-c", "diff.t.xfuncname=!^bar\n^b", "diff", "-W", "-U0", "--", ".", ":!f.py" }, &.{ "-c", "diff.t.xfuncname=^q*", "diff", "-W", "--", ".", ":!f.py" }, &.{ "-c", "diff.t.xfuncname=^(d)()", "diff", "-W", "--inter-hunk-context=5", "--", ".", ":!f.py" }, &.{ "-c", "diff.t.xfuncname=^d", "log", "-p", "-1", "-W", "--format=%s", "--", "a.t", "b.u" }, }) |args| try same(&repo, args); for ([_][]const []const u8{ &.{ "diff", "-W", "--ignore-blank-lines" }, &.{ "diff", "-W", "-I", "x" }, &.{ "diff", "--no-index", "-W", "-I", "x", "a.t", "b.u" }, }) |args| { const ours = try tool.runOurs(&repo, args, .{}); defer ours.deinit(repo.gpa); try testing.expectEqual(@as(u8, 128), ours.code); try testing.expect(std.mem.find(u8, ours.stderr, "not supported") != null); }}
/// The candidate lines for each built-in driver zig-git matches, one to/// a line, chosen while fitting its pattern to cover each of its rules/// and near misses.const builtin_cases = [_]struct { []const u8, []const u8 }{ .{ "ada", @embedFile("funcname_cases/ada") }, .{ "bash", @embedFile("funcname_cases/bash") }, .{ "bibtex", @embedFile("funcname_cases/bibtex") }, .{ "cpp", @embedFile("funcname_cases/cpp") }, .{ "csharp", @embedFile("funcname_cases/csharp") }, .{ "css", @embedFile("funcname_cases/css") }, .{ "dts", @embedFile("funcname_cases/dts") }, .{ "elixir", @embedFile("funcname_cases/elixir") }, .{ "fortran", @embedFile("funcname_cases/fortran") }, .{ "fountain", @embedFile("funcname_cases/fountain") }, .{ "golang", @embedFile("funcname_cases/golang") }, .{ "html", @embedFile("funcname_cases/html") }, .{ "ini", @embedFile("funcname_cases/ini") }, .{ "java", @embedFile("funcname_cases/java") }, .{ "kotlin", @embedFile("funcname_cases/kotlin") }, .{ "markdown", @embedFile("funcname_cases/markdown") }, .{ "matlab", @embedFile("funcname_cases/matlab") }, .{ "objc", @embedFile("funcname_cases/objc") }, .{ "pascal", @embedFile("funcname_cases/pascal") }, .{ "perl", @embedFile("funcname_cases/perl") }, .{ "php", @embedFile("funcname_cases/php") }, .{ "python", @embedFile("funcname_cases/python") }, .{ "r", @embedFile("funcname_cases/r") }, .{ "ruby", @embedFile("funcname_cases/ruby") }, .{ "rust", @embedFile("funcname_cases/rust") }, .{ "scheme", @embedFile("funcname_cases/scheme") }, .{ "swift", @embedFile("funcname_cases/swift") }, .{ "tex", @embedFile("funcname_cases/tex") },};
test "function lines: every built-in driver matched, against git's" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); const gpa = testing.allocator; var attributes: std.ArrayList(u8) = .empty; defer attributes.deinit(gpa); for (builtin_cases) |case| try attributes.print(gpa, "{s}.f diff={s}\n", .{ case[0], case[0] }); try write(&repo, ".gitattributes", attributes.items); var all: std.ArrayList([]const u8) = .empty; defer all.deinit(gpa); for (builtin_cases) |case| { var lines: std.ArrayList([]const u8) = .empty; defer lines.deinit(gpa); var it = std.mem.splitScalar(u8, case[1], '\n'); while (it.next()) |l| try lines.append(gpa, l); const old = try candidates(gpa, lines.items, false); defer gpa.free(old); const name = try std.fmt.allocPrint(gpa, "{s}.f", .{case[0]}); defer gpa.free(name); try write(&repo, name, old); } try repo.git(&.{ "add", "." }); try repo.git(&.{ "commit", "-q", "-m", "drivers" }); for (builtin_cases) |case| { var lines: std.ArrayList([]const u8) = .empty; defer lines.deinit(gpa); var it = std.mem.splitScalar(u8, case[1], '\n'); while (it.next()) |l| try lines.append(gpa, l); const new = try candidates(gpa, lines.items, true); defer gpa.free(new); const name = try std.fmt.allocPrint(gpa, "{s}.f", .{case[0]}); defer gpa.free(name); try write(&repo, name, new); } for (builtin_cases) |case| { const name = try std.fmt.allocPrint(gpa, "{s}.f", .{case[0]}); defer gpa.free(name); try same(&repo, &.{ "diff", name }); try same(&repo, &.{ "diff", "-W", name }); }}