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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Function lines as a diff driver finds them: the patterns of//! `diff.<driver>.xfuncname` (extended regular expressions) and//! `diff.<driver>.funcname` (basic ones), and git's built-in drivers.//!//! gitattributes(5) describes one pattern; git was seen to take more://!//! - A pattern is split at each newline into patterns tried in order, and//! the first to match a line decides it. One that begins with `!` is//! negated: a line it matches is not a function line, whatever the//! patterns after it say. An empty pattern matches every line.//! - The last pattern may not be negated; git stops with "Last//! expression must not be negated: `<pattern>`" before it compiles that//! one, and with "Invalid regexp to look for hunk header: `<pattern>`"//! (without the `!`) at the first earlier one that does not compile.//! - A line is matched without its newline, or one carriage return//! before that, with GNU's escapes (`\w`,//! `\b` and the rest) and without `REG_NEWLINE` or case folding.//! - The text after `@@` is the first group of the match if that group//! took part in it, and otherwise the whole match; cut and trimmed as//! the default rule's line is (`hunks.trimFunctionName`), and written//! up to its first byte that is not UTF-8.//! - A function line whose text comes out empty still ends the search,//! and nothing is written after `@@`.//! - Whichever of `xfuncname` and `funcname` was set last is the one//! used, and either replaces a built-in driver's pattern.//!//! A pattern whose first group lies inside a repetition that may iterate//! more than once is refused (`error.Unsupported`): which iteration glibc//! reports there was matched in only 98 cases in 100, against none//! missed in over 13,000 with the group anywhere else.
const std = @import("std");const Allocator = std.mem.Allocator;
const regex = @import("../regex.zig");const hunks = @import("hunks.zig");
/// Why a pattern was not compiled.pub const Error = error{ /// One of its patterns is malformed; `Failure.piece` is that pattern. InvalidPattern, /// Its last pattern is negated. LastNegated, /// One of its patterns uses what `regex` does not implement. Unsupported,} || Allocator.Error;
/// What was wrong with a pattern that did not compile.pub const Failure = struct { /// The pattern at fault, as git names it: without its `!` when it /// did not compile, with it when it is the negated last one. piece: []const u8 = "", /// The construct refused, for `Unsupported`. unsupported: []const u8 = "",};
/// The two syntaxes a driver's pattern may be written in.pub const Syntax = enum { /// `diff.<driver>.xfuncname`: extended regular expressions. xfuncname, /// `diff.<driver>.funcname`: basic regular expressions. funcname,};
/// A compiled pattern: patterns tried in order, some negated.pub const Pattern = struct { arena: std.heap.ArenaAllocator, /// For matching, which allocates. gpa: Allocator, rules: []const Rule,
const Rule = struct { negated: bool, regex: regex.Regex, };
/// Compiles `text` written in `syntax`. With `ignore_case`, letters /// match either case, which some built-in drivers ask for. pub fn compile(gpa: Allocator, text: []const u8, syntax: Syntax, ignore_case: bool, failure: ?*Failure) Error!Pattern { var arena: std.heap.ArenaAllocator = .init(gpa); errdefer arena.deinit(); const a = arena.allocator(); var rules: std.ArrayList(Rule) = .empty; errdefer for (rules.items) |*r| r.regex.deinit(); const options: regex.Options = .{ .syntax = if (syntax == .xfuncname) .extended else .basic, .gnu = true, .ignore_case = ignore_case, }; var pieces = std.mem.splitScalar(u8, text, '\n'); while (pieces.next()) |whole| { const last = pieces.peek() == null; const negated = whole.len != 0 and whole[0] == '!'; if (last and negated) { if (failure) |f| f.* = .{ .piece = whole }; return error.LastNegated; } const piece = if (negated) whole[1..] else whole; var rf: regex.Failure = .{}; const r = regex.Regex.compileWith(gpa, piece, options, &rf) catch |err| { if (failure) |f| f.* = .{ .piece = piece, .unsupported = rf.unsupported }; return switch (err) { error.InvalidPattern => error.InvalidPattern, error.Unsupported => error.Unsupported, error.OutOfMemory => error.OutOfMemory, }; }; if (r.first_group_repeated) { // glibc's choice of iteration for a repeated group was // matched only about 98 times in a hundred. var x = r; x.deinit(); if (failure) |f| f.* = .{ .piece = piece, .unsupported = "a first group inside a repetition" }; return error.Unsupported; } try rules.append(a, .{ .negated = negated, .regex = r }); } return .{ .arena = arena, .gpa = gpa, .rules = rules.items }; }
/// Frees the pattern. pub fn deinit(p: *Pattern) void { for (p.rules) |*r| { var x = r.regex; x.deinit(); } p.arena.deinit(); }
/// The function name `line` gives, null if it is not a function line; /// a slice of `line`, perhaps empty. pub fn find(p: *const Pattern, line: []const u8) Allocator.Error!?[]const u8 { var text = if (line.len != 0 and line[line.len - 1] == '\n') line[0 .. line.len - 1] else line; // One carriage return before the newline is not matched either. if (text.len != 0 and text[text.len - 1] == '\r') text = text[0 .. text.len - 1]; for (p.rules) |*r| { var groups: [1]?regex.Span = undefined; const whole = try r.regex.exec(p.gpa, text, &groups) orelse continue; if (r.negated) return null; const span = groups[0] orelse whole; return hunks.trimFunctionName(text[span.start..span.end]); } return null; }
/// The pattern as the seam `hunks` and `patch` take. pub fn finder(p: *const Pattern) hunks.FunctionFinder { return .{ .context = p, .find = findThrough }; }
fn findThrough(context: ?*const anyopaque, line: []const u8, _: *[hunks.function_name_max]u8) Allocator.Error!?[]const u8 { const p: *const Pattern = @ptrCast(@alignCast(context.?)); return p.find(line); }};
/// One of git's built-in drivers, as zig-git reproduces it.pub const Builtin = struct { name: []const u8, /// Its function pattern, in `xfuncname`'s syntax, written from what /// git was seen to choose. pattern: []const u8, ignore_case: bool = false,};
/// The built-in drivers matched so far. A built-in driver not here is/// refused by its caller.pub const builtins = [_]Builtin{ .{ .name = "ada", .pattern = @embedFile("drivers/ada") }, .{ .name = "bash", .pattern = @embedFile("drivers/bash") }, .{ .name = "bibtex", .pattern = @embedFile("drivers/bibtex") }, .{ .name = "cpp", .pattern = @embedFile("drivers/cpp") }, .{ .name = "csharp", .pattern = @embedFile("drivers/csharp") }, .{ .name = "css", .pattern = @embedFile("drivers/css") }, .{ .name = "dts", .pattern = @embedFile("drivers/dts") }, .{ .name = "elixir", .pattern = @embedFile("drivers/elixir") }, .{ .name = "fortran", .pattern = @embedFile("drivers/fortran") }, .{ .name = "fountain", .pattern = @embedFile("drivers/fountain") }, .{ .name = "golang", .pattern = @embedFile("drivers/golang") }, .{ .name = "html", .pattern = @embedFile("drivers/html") }, .{ .name = "ini", .pattern = @embedFile("drivers/ini") }, .{ .name = "java", .pattern = @embedFile("drivers/java") }, .{ .name = "kotlin", .pattern = @embedFile("drivers/kotlin") }, .{ .name = "markdown", .pattern = @embedFile("drivers/markdown") }, .{ .name = "matlab", .pattern = @embedFile("drivers/matlab") }, .{ .name = "objc", .pattern = @embedFile("drivers/objc") }, .{ .name = "pascal", .pattern = @embedFile("drivers/pascal") }, .{ .name = "perl", .pattern = @embedFile("drivers/perl") }, .{ .name = "php", .pattern = @embedFile("drivers/php") }, .{ .name = "python", .pattern = @embedFile("drivers/python") }, .{ .name = "r", .pattern = @embedFile("drivers/r") }, .{ .name = "ruby", .pattern = @embedFile("drivers/ruby") }, .{ .name = "rust", .pattern = @embedFile("drivers/rust") }, .{ .name = "scheme", .pattern = @embedFile("drivers/scheme") }, .{ .name = "swift", .pattern = @embedFile("drivers/swift") }, .{ .name = "tex", .pattern = @embedFile("drivers/tex") },};
/// The built-in driver `name`, if it is one zig-git reproduces.pub fn builtin(name: []const u8) ?Builtin { for (builtins) |b| if (std.mem.eql(u8, b.name, name)) return b; return null;}
const testing = std.testing;
fn expectFound(want: ?[]const u8, pattern: []const u8, syntax: Syntax, line: []const u8) !void { var p = try Pattern.compile(testing.allocator, pattern, syntax, false, null); defer p.deinit(); errdefer std.debug.print("/{s}/ against \"{s}\"\n", .{ pattern, line }); const got = try p.find(line); if (want) |w| try testing.expectEqualStrings(w, got orelse return error.TestExpectedEqual) else try testing.expectEqual(null, got);}
const fuzz_input = @import("../testing/fuzz.zig");
test "patterns find the text git shows after @@" { // Each answer is what git 2.56 showed. try expectFound("ar", "!bar x\nb(.*)", .xfuncname, "bar\n"); try expectFound(null, "!bar x\nb(.*)", .xfuncname, "bar x\n"); try expectFound("bar", "(b.*)\n!bar\nfoo", .xfuncname, "bar\n"); try expectFound(null, "!bar\n(b.*)", .xfuncname, "bar\n"); try expectFound("baz", "!bar\n(b.*)", .xfuncname, "baz\n"); try expectFound("", "foo\n", .xfuncname, "bar\n"); try expectFound(null, "!\nfoo", .xfuncname, "foo\n"); try expectFound(null, "foo\r\nbar", .xfuncname, "foo\n"); try expectFound("", "^( *)", .xfuncname, " x\n"); try expectFound(" hello", "(.*)", .xfuncname, " hello \n"); try expectFound("cdef", "(x)|(c.*)", .xfuncname, "abcdef\n"); try expectFound("abc \x0b", "(.*)", .xfuncname, "abc \x0b \n"); try expectFound(null, "x$", .xfuncname, "xx \n"); try expectFound("x", "x$", .xfuncname, "x\r\n"); try expectFound(null, "x$", .xfuncname, "x\r\r\n"); try expectFound("def", "\\w+", .xfuncname, "def x():\n"); try expectFound("el", "h\\(el\\)lo", .funcname, "hello\n"); try expectFound(null, "h(el)lo", .funcname, "hello\n");}
test "a malformed pattern is named as git names it" { const cases = [_]struct { []const u8, Syntax, Error, []const u8 }{ .{ "a(", .xfuncname, error.InvalidPattern, "a(" }, .{ "foo\na(", .xfuncname, error.InvalidPattern, "a(" }, .{ "!a(\nb", .xfuncname, error.InvalidPattern, "a(" }, .{ "a\\(", .funcname, error.InvalidPattern, "a\\(" }, .{ "b(.*)\n!bar", .xfuncname, error.LastNegated, "!bar" }, .{ "!a(", .xfuncname, error.LastNegated, "!a(" }, .{ "foo\n!", .xfuncname, error.LastNegated, "!" }, .{ "a(\n!b", .xfuncname, error.InvalidPattern, "a(" }, .{ "(a)\\1", .xfuncname, error.Unsupported, "(a)\\1" }, .{ "x\n(a|b)*", .xfuncname, error.Unsupported, "(a|b)*" }, }; for (cases) |case| { var failure: Failure = .{}; errdefer std.debug.print("/{s}/\n", .{case[0]}); try testing.expectError(case[2], Pattern.compile(testing.allocator, case[0], case[1], false, &failure)); try testing.expectEqualStrings(case[3], failure.piece); }}
fn fuzzParse(_: void, smith: *testing.Smith) anyerror!void { var buf: [64]u8 = undefined; const text = @import("../testing/fuzz.zig").bytes(smith, &buf); var line_buf: [32]u8 = undefined; const line = line_buf[0..smith.slice(&line_buf)]; var failure: Failure = .{}; var p = Pattern.compile(testing.allocator, text, .xfuncname, false, &failure) catch |err| switch (err) { error.OutOfMemory => return err, error.LastNegated => { try testing.expect(failure.piece.len != 0 and failure.piece[0] == '!'); try testing.expect(std.mem.endsWith(u8, text, failure.piece)); return; }, else => { try testing.expect(std.mem.find(u8, text, failure.piece) != null); return; }, }; defer p.deinit(); // One pattern for each line of the text, none of them negated last. try testing.expectEqual(std.mem.count(u8, text, "\n") + 1, p.rules.len); try testing.expect(!p.rules[p.rules.len - 1].negated); if (try p.find(line)) |name| { try testing.expect(name.len <= hunks.function_name_max); try testing.expect(name.len == 0 or name[name.len - 1] != ' '); }}
test "fuzz: multi-line function patterns" { try fuzz_input.fuzz({}, fuzzParse, .{ .corpus = &.{ @embedFile("../fuzz/funcname-patterns"), "!bar x\nb(.*)", "foo\n\n!", "^[ \t]*(def|class)[ \t].*$", } });}