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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! What `diff`, `diff-tree`, `diff-index`, `diff-files`, `log` and `show`//! share when they compare what a repository holds: the diff options and//! the configuration they read, the drivers attributes choose, and//! contents read from the object database.//!//! The plumbing commands read only the configuration git calls basic//! (`diff.indentHeuristic`, `diff.suppressBlankEmpty`, `core.quotePath`//! and the drivers); `diff`, `log` and `show` read the rest as well//! (`diff.context`, `diff.interHunkContext`, `diff.algorithm`, the//! prefixes, the stat widths and so on), which the plumbing was seen to//! ignore.
const std = @import("std");const Allocator = std.mem.Allocator;const Io = std.Io;
const git = @import("git");const output = git.diff.output;const LineDiff = git.diff.LineDiff;const tool = @import("zig_git.zig");const Context = tool.Context;const object_names = @import("object_names.zig");const check_attr = @import("check_attr.zig");const revisions = @import("revisions.zig");
/// The commands sharing these options.pub const Command = enum { diff, diff_tree, diff_index, diff_files, log, show, /// `log` as `whatchanged` runs it: the same, but named for itself in /// what it says. whatchanged,
/// Whether it reads the configuration git keeps for porcelain. pub fn porcelain(c: Command) bool { return switch (c) { .diff, .log, .show, .whatchanged => true, else => false, }; }
pub fn name(c: Command) []const u8 { return switch (c) { .diff => "diff", .diff_tree => "diff-tree", .diff_index => "diff-index", .diff_files => "diff-files", .log => "log", .show => "show", .whatchanged => "whatchanged", }; }};
/// What the command line and the configuration asked for.pub const Settings = struct { command: Command, out: output.Options = .{}, /// Whether any format option was given; without one each command /// has its own default. format_given: bool = false, quiet: bool = false, exit_code: bool = false, reverse: bool = false, no_prefix: bool = false, mnemonic: bool = false, src_prefix: ?[]const u8 = null, dst_prefix: ?[]const u8 = null, output_path: ?[]const u8 = null, /// `--relative[=<path>]`: null without it; empty for the current /// directory. relative: ?[]const u8 = null, /// `--diff-filter`: the letters given in upper case and in lower /// case, a bit each from `A`. filter_include: u32 = 0, filter_exclude: u32 = 0, filter_given: bool = false, /// `diff.renames`, `-M`, `--no-renames`: whether to look for /// renames. renames: bool = false, /// `diff.renames=copies`, `-C`: whether to look for copies too. copies: bool = false, /// `--find-copies-harder`, or `-C` given twice: unchanged paths are /// copy sources too. harder: bool = false, /// The minimum score given with `-M` or `-C`; 0 for the default. min_score: u32 = 0, /// `-l`; null when not given. limit: ?i64 = null, /// `diff.renameLimit`. config_limit: ?i64 = null, /// `--rename-empty`, `--no-rename-empty`. rename_empty: bool = true, /// `-B[<n>][/<m>]`: break rewrites, with these scores (0 for the /// defaults). breaks: ?[2]u32 = null, ignore_regexes: std.ArrayList(git.regex.Regex) = .empty, /// The `-I` matcher, kept alive here. matcher: Matcher = .{ .gpa = undefined, .regexes = &.{} }, /// Whether `--abbrev` was given, for the raw format. abbrev_given: bool = false, /// `--abbrev=<n>`'s length; null for the default. abbrev_length: ?usize = null, /// `--full-index`. full_index: bool = false, /// `--no-abbrev`, after any `--abbrev`. no_abbrev: bool = false, stat_width: ?usize = null, stat_name_width: ?usize = null, stat_graph_width: ?usize = null, stat_count: ?usize = null, /// `--compact-summary`. compact_summary: bool = false, /// `-s`, `--name-only`, `--name-status` and `--check` as given, which /// git refuses together. exclusive: u4 = 0, /// `diff.statNameWidth` and `diff.statGraphWidth`. config_name_width: ?usize = null, config_graph_width: ?usize = null, /// The pickaxe asked for: `-S`, `-G` or `--find-object`. pickaxe_kind: ?PickaxeKind = null, /// What `-S` or `-G` looks for (the last given). pickaxe_text: []const u8 = "", /// `--pickaxe-regex`. pickaxe_regex: bool = false, /// `--pickaxe-all`. pickaxe_all: bool = false, /// `-i`, `--regexp-ignore-case`, which the pickaxe heeds. ignore_case: bool = false, /// Every `--find-object`, as given. find_objects: std.ArrayList([]const u8) = .empty, /// Every `--anchored=<text>` since the last `--patience`, which /// clears them (as git was seen to). anchors: std.ArrayList([]const u8) = .empty, /// The pickaxe, set up by `check` and compiled when a diff first needs /// it, which is when git says what is wrong with its pattern. pickaxe: ?*PickaxeState = null, /// `-O<orderfile>`; `/dev/null` cancels `diff.orderFile`. order_file: ?[]const u8 = null, /// `diff.orderFile`, which only the porcelain reads. config_order_file: ?[]const u8 = null, /// The orderfile's patterns and its text, read by `setUp`. order: ?git.diff.order.OrderFile = null, order_text: []u8 = &.{}, /// `--skip-to` or `--rotate-to`, the last given. rotation: ?Rotation = null, /// `--line-prefix`. line_prefix: ?[]const u8 = null, /// What `--relative` strips, settled by `setUp`: null for none. relative_prefix: ?[]const u8 = null, relative_owned: []u8 = &.{},
/// The algorithm an option chose: the last one given wins, and a /// `diff.<driver>.algorithm` no longer applies. fn chooseAlgorithm(s: *Settings, a: LineDiff.Algorithm) void { s.out.patch.lines.algorithm = a; s.out.patch.lines.algorithm_given = true; }
pub fn deinit(s: *Settings, gpa: Allocator) void { for (s.ignore_regexes.items) |*r| r.deinit(); s.ignore_regexes.deinit(gpa); s.find_objects.deinit(gpa); s.anchors.deinit(gpa); if (s.order) |*o| o.deinit(gpa); s.order = null; gpa.free(s.order_text); s.order_text = &.{}; gpa.free(s.relative_owned); s.relative_owned = &.{}; if (s.pickaxe) |p| { if (p.compiled) |*c| c.deinit(); p.ids.deinit(gpa); gpa.destroy(p); } s.pickaxe = null; }
/// Notes the pickaxe an option asks for; git refuses two kinds at /// once as soon as it reads the second. fn choosePickaxe(s: *Settings, kind: PickaxeKind, stderr: *Io.Writer) error{Fatal}!void { if (s.pickaxe_kind) |k| if (k != kind) return tool.fatalMessage(stderr, "options '-G', '-S', and '--find-object' cannot be used together", .{}); s.pickaxe_kind = kind; }
/// Whether a pair passes `--diff-filter`: a rewrite by the letter /// `B`, every other by its status. pub fn passesPair(s: *const Settings, p: *const output.Pair) bool { if (p.rewrite and p.status != .type_changed) return s.passesLetter('B'); return s.passesLetter(p.status.letter()); }
/// Whether a path with `status` passes `--diff-filter`. pub fn passes(s: *const Settings, status: output.Status) bool { return s.passesLetter(status.letter()); }
fn passesLetter(s: *const Settings, letter: u8) bool { // The letters in upper case, or all of them, less those in // lower case; nothing left shows everything, as git was seen to // over every combination of a few options. if (!s.filter_given) return true; var all: u32 = 0; for ("ACDMRTUXB") |c| all |= @as(u32, 1) << @intCast(c - 'A'); const shown = (if (s.filter_include != 0) s.filter_include else all) & ~s.filter_exclude; if (shown == 0) return true; const i: u5 = @intCast(letter - 'A'); return shown & (@as(u32, 1) << i) != 0; }};
/// `--skip-to` and `--rotate-to`.pub const Rotation = struct { name: []const u8, how: git.diff.order.Rotation,};
/// Which option chose the pickaxe.pub const PickaxeKind = enum { s, g, object };
/// The pickaxe, compiled once.pub const PickaxeState = struct { kind: git.diff.pickaxe.Kind, text: []const u8, ignore_case: bool, all: bool, ids: std.ArrayList(git.ObjectId) = .empty, compiled: ?git.diff.pickaxe = null,};
/// The `-I` patterns, as `hunks` asks for them.pub const Matcher = struct { gpa: Allocator, regexes: []const git.regex.Regex,
fn matches(context: *const anyopaque, line: []const u8) Allocator.Error!bool { const self: *const Matcher = @ptrCast(@alignCast(context)); for (self.regexes) |*r| if (try r.matches(self.gpa, line)) return true; return false; }};
/// Refuses `what` by name.pub fn unsupported(stderr: *Io.Writer, command: Command, what: []const u8) error{Fatal} { return tool.fatalMessage(stderr, "zig-git {s}: '{s}' is not supported", .{ command.name(), what });}
/// What a comparison zig-git cannot make as git makes it is refused as:/// `LineDiff.Error.LongSearchUnsupported`.pub const long_search = "a line diff searching past 256 steps";
/// What is refused, by name, for a comparison zig-git cannot make as git/// makes it: a `LineDiff.Error` other than running out of memory, or/// `--check` of a file of blank lines alone. Null for any other error.pub fn comparisonRefusal(err: anyerror) ?[]const u8 { return switch (err) { error.LongSearchUnsupported => long_search, // `LineDiff.splitLines` numbers bytes in 32 bits. error.TooManyLines => "a file of 4 GiB or more", error.BlankFileUnsupported => "--check of a file of blank lines alone", else => null, };}
/// Refuses by name what `comparisonRefusal` names, wherever in a/// command's diffs it was met; hands any other error back.pub fn refuseComparison(stderr: *Io.Writer, command: Command, err: anytype) OrFatal(@TypeOf(err)) { return unsupported(stderr, command, comparisonRefusal(err) orelse return err);}
fn OrFatal(comptime E: type) type { return E || error{Fatal};}
/// Reads a boolean setting, or stops as git does at one that is not:/// git 2.56 names neither the file nor the line for a bad boolean,/// whichever key it is and wherever it was set (a file, an include, `-c`,/// `GIT_CONFIG_PARAMETERS` or `GIT_CONFIG_COUNT`), where a bad number/// names the file.pub fn boolValue(e: *const git.Config.Entry, stderr: *Io.Writer) !bool { return git.Config.value.parseBool(e.value) orelse tool.fatalMessage(stderr, "bad boolean config value '{s}' for '{s}'", .{ e.value orelse "", e.key });}
fn configFatal(stderr: *Io.Writer, e: *const git.Config.Entry) error{Fatal} { if (e.origin.kind == .file) return tool.fatalMessage(stderr, "bad config variable '{s}' in file '{s}' at line {d}", .{ e.key, e.origin.name, e.line }); return tool.fatalMessage(stderr, "unable to parse '{s}' from command-line config", .{e.key});}
fn intValue(config: *const git.Config, e: *const git.Config.Entry, stderr: *Io.Writer) !i64 { _ = config; const v = e.value orelse return configFatal(stderr, e); return git.Config.value.parseInt(i64, v) catch |err| { const why = if (err == error.OutOfRange) "out of range" else "invalid unit"; if (e.origin.kind == .file) return tool.fatalMessage(stderr, "bad numeric config value '{s}' for '{s}' in file {s}: {s}", .{ v, e.key, e.origin.name, why }); return tool.fatalMessage(stderr, "bad numeric config value '{s}' for '{s}': {s}", .{ v, e.key, why }); };}
/// Reads the configuration `s.command` reads.pub fn readConfig(config: *const git.Config, s: *Settings, stderr: *Io.Writer) !void { const porcelain = s.command.porcelain(); if (porcelain) s.renames = true; for (config.entries.items) |*e| { const key = e.key; if ((driverPattern(key) != null or isDriverAlgorithm(key)) and e.value == null) { stderr.print("error: missing value for '{s}'\n", .{key}) catch {}; return configFatal(stderr, e); } // Every driver's `binary` is read as a boolean whether or not a // path uses the driver, and, as git was seen to, without naming // the file it came from. if (driverVariable(key)) |v| if (std.ascii.eqlIgnoreCase(v, "binary") and git.Config.value.parseBool(e.value) == null) return tool.fatalMessage(stderr, "bad boolean config value '{s}' for '{s}'", .{ e.value orelse "", key }); if (std.mem.eql(u8, key, "diff.indentheuristic")) { s.out.patch.lines.indent_heuristic = try boolValue(e, stderr); } else if (std.mem.eql(u8, key, "diff.suppressblankempty")) { s.out.patch.suppress_blank_empty = try boolValue(e, stderr); } else if (std.mem.eql(u8, key, "core.quotepath")) { const b = try boolValue(e, stderr); s.out.patch.quote_path = b; s.out.quote_path = b; } else if (std.mem.eql(u8, key, "diff.renamelimit")) { s.config_limit = try intValue(config, e, stderr); } else if (std.mem.eql(u8, key, "core.whitespace")) { const v = e.value orelse { stderr.print("error: missing value for '{s}'\n", .{key}) catch {}; return configFatal(stderr, e); }; var bad: ?[]const u8 = null; s.out.whitespace = git.diff.check.Rules.parse(.defaults, v, &bad); if (bad) |b| stderr.print("warning: tabwidth {s} out of range\n", .{b}) catch {}; if (s.out.whitespace.conflicting()) return tool.fatalMessage(stderr, "cannot enforce both tab-in-indent and indent-with-non-tab", .{}); } else if (std.mem.eql(u8, key, "diff.dirstat")) { const v = e.value orelse return configFatal(stderr, e); var unknown: std.ArrayList([]const u8) = .empty; defer unknown.deinit(config.gpa); if (v.len != 0 and !try s.out.dirstat.parse(v, &unknown, config.gpa)) { stderr.writeAll("warning: Found errors in 'diff.dirstat' config variable:\n") catch {}; for (unknown.items) |u| stderr.print(" Unknown dirstat parameter '{s}'\n", .{u}) catch {}; stderr.writeAll("\n") catch {}; } } else if (std.mem.eql(u8, key, "core.bigfilethreshold")) { const n = try intValue(config, e, stderr); s.out.big_file_threshold = if (n < 0) 0 else @intCast(n); } else if (!porcelain) { continue; } else if (std.mem.eql(u8, key, "diff.autorefreshindex")) { // Nothing here refreshes the index, but git's porcelain reads // the setting, and stops at one that is not a boolean. _ = try boolValue(e, stderr); } else if (std.mem.eql(u8, key, "diff.algorithm")) { const v = e.value orelse return configFatal(stderr, e); const alg = LineDiff.Algorithm.fromName(v) orelse { stderr.print("error: unknown value for config 'diff.algorithm': {s}\n", .{v}) catch {}; return configFatal(stderr, e); }; s.out.patch.lines.algorithm = alg; } else if (std.mem.eql(u8, key, "diff.context")) { const n = try intValue(config, e, stderr); if (n < 0) return configFatal(stderr, e); s.out.patch.hunks.context = @intCast(n); } else if (std.mem.eql(u8, key, "diff.interhunkcontext")) { const n = try intValue(config, e, stderr); if (n < 0) return configFatal(stderr, e); s.out.patch.hunks.inter_hunk_context = @intCast(n); } else if (std.mem.eql(u8, key, "diff.noprefix")) { s.no_prefix = try boolValue(e, stderr); } else if (std.mem.eql(u8, key, "diff.mnemonicprefix")) { s.mnemonic = try boolValue(e, stderr); } else if (std.mem.eql(u8, key, "diff.srcprefix")) { s.src_prefix = e.value orelse return configFatal(stderr, e); } else if (std.mem.eql(u8, key, "diff.dstprefix")) { s.dst_prefix = e.value orelse return configFatal(stderr, e); } else if (std.mem.eql(u8, key, "diff.statnamewidth")) { const n = try intValue(config, e, stderr); s.config_name_width = if (n <= 0) null else @intCast(n); } else if (std.mem.eql(u8, key, "diff.statgraphwidth")) { const n = try intValue(config, e, stderr); s.config_graph_width = if (n <= 0) null else @intCast(n); } else if (std.mem.eql(u8, key, "diff.renames")) { const v = e.value orelse "true"; if (std.ascii.eqlIgnoreCase(v, "copies") or std.ascii.eqlIgnoreCase(v, "copy")) { s.renames = true; s.copies = true; } else { s.renames = try boolValue(e, stderr); s.copies = false; } } else if (std.mem.eql(u8, key, "diff.external")) { return unsupported(stderr, s.command, "diff.external"); } else if (std.mem.eql(u8, key, "diff.relative")) { s.relative = if (try boolValue(e, stderr)) "" else null; } else if (std.mem.eql(u8, key, "diff.orderfile")) { s.config_order_file = e.value orelse return configFatal(stderr, e); } else if (std.mem.eql(u8, key, "color.diff") or std.mem.eql(u8, key, "color.ui")) { const v = e.value orelse "true"; if (std.ascii.eqlIgnoreCase(v, "always")) return unsupported(stderr, s.command, "color"); } else if (std.mem.eql(u8, key, "diff.ignoresubmodules")) { const v = e.value orelse ""; if (!std.mem.eql(u8, v, "none") and !std.mem.eql(u8, v, "untracked")) return unsupported(stderr, s.command, "diff.ignoreSubmodules"); } else if (std.mem.eql(u8, key, "diff.submodule")) { const v = e.value orelse ""; if (!std.mem.eql(u8, v, "short")) return unsupported(stderr, s.command, "diff.submodule"); } }}
/// Options refused by name: pickaxe, word diffs, colors, external/// programs and the rest not yet matched.const refused = [_][]const u8{ "--binary", "--word-diff", "--word-diff-regex", "--color-words", "--color-moved", "--color-moved-ws", "--follow", "--ext-diff", "--textconv", "--max-depth", "--ws-error-highlight", "--output-indicator-new", "--output-indicator-old", "--output-indicator-context", "--ita-invisible-in-index", "--ita-visible-in-index", "-c", "--cc", "--combined-all-paths", "--remerge-diff", "--dd", "--diff-merges", "--color",};
/// What `parseOption` made of an argument.pub const Parsed = enum { no, yes, took_next };
fn requiresValue(stderr: *Io.Writer, name: []const u8) error{UsageQuiet} { stderr.print("error: option `{s}' requires a value\n", .{name}) catch {}; return error.UsageQuiet;}
fn number(text: []const u8) ?usize { const n = object_names.optionNumber(text) orelse return null; if (n < 0) return null; return @intCast(n);}
/// Reads `arg` (and `next`, if it takes it) as a diff option.pub fn parseOption(gpa: Allocator, s: *Settings, arg: []const u8, next: ?[]const u8, stderr: *Io.Writer) !Parsed { const f = &s.out.format; const eq = std.mem.findScalar(u8, arg, '='); const name = arg[0 .. eq orelse arg.len]; const value: ?[]const u8 = if (eq) |e| arg[e + 1 ..] else null; if (std.mem.eql(u8, arg, "-W") or std.mem.eql(u8, arg, "--function-context")) { s.out.patch.hunks.function_context = true; return .yes; } else if (std.mem.eql(u8, arg, "--no-function-context")) { s.out.patch.hunks.function_context = false; return .yes; } else if (std.mem.eql(u8, name, "--function-context") or std.mem.eql(u8, name, "--no-function-context")) { stderr.print("error: option `{s}' takes no value\n", .{name[2..]}) catch {}; return error.UsageQuiet; } for (refused) |r| if (std.mem.eql(u8, name, r)) { if (std.mem.eql(u8, name, "--color")) { const v = value orelse "always"; if (std.mem.eql(u8, v, "never") or std.mem.eql(u8, v, "auto")) return .yes; } return unsupported(stderr, s.command, name); }; if (std.mem.startsWith(u8, arg, "-S") or std.mem.startsWith(u8, arg, "-G")) { // The pattern is the rest of the argument, or the next one. const letter = arg[1]; const took = arg.len == 2; const v = if (!took) arg[2..] else next orelse { stderr.print("error: switch `{c}' requires a value\n", .{letter}) catch {}; return error.UsageQuiet; }; if (v.len == 0) { stderr.print("error: -{c} requires a non-empty argument\n", .{letter}) catch {}; return error.UsageQuiet; } try s.choosePickaxe(if (letter == 'S') .s else .g, stderr); s.pickaxe_text = v; return if (took) .took_next else .yes; } if (std.mem.eql(u8, name, "--find-object")) { const v = value orelse (next orelse return requiresValue(stderr, "find-object")); try s.choosePickaxe(.object, stderr); try s.find_objects.append(gpa, v); return if (value == null) .took_next else .yes; } if (std.mem.eql(u8, arg, "--pickaxe-all")) { s.pickaxe_all = true; return .yes; } if (std.mem.eql(u8, arg, "--pickaxe-regex")) { s.pickaxe_regex = true; return .yes; } if (std.mem.eql(u8, arg, "-i") or std.mem.eql(u8, arg, "--regexp-ignore-case")) { s.ignore_case = true; return .yes; } if (std.mem.startsWith(u8, arg, "-O")) { const took = arg.len == 2; s.order_file = if (!took) arg[2..] else next orelse { stderr.writeAll("error: switch `O' requires a value\n") catch {}; return error.UsageQuiet; }; return if (took) .took_next else .yes; } if (std.mem.eql(u8, name, "--skip-to") or std.mem.eql(u8, name, "--rotate-to")) { const v = value orelse (next orelse return requiresValue(stderr, name[2..])); s.rotation = .{ .name = v, .how = if (name[2] == 's') .skip else .rotate }; return if (value == null) .took_next else .yes; } if (std.mem.eql(u8, arg, "-D") or std.mem.eql(u8, arg, "--irreversible-delete")) { s.out.patch.irreversible_delete = true; return .yes; } if (std.mem.eql(u8, name, "--line-prefix")) { const v = value orelse (next orelse return requiresValue(stderr, "line-prefix")); s.line_prefix = v; return if (value == null) .took_next else .yes; } if (std.mem.startsWith(u8, arg, "-X") or std.mem.eql(u8, name, "--dirstat") or std.mem.eql(u8, name, "--dirstat-by-file") or std.mem.eql(u8, arg, "--cumulative")) { f.dirstat = true; s.format_given = true; var params: ?[]const u8 = null; if (arg[1] == 'X') { if (arg.len > 2) params = arg[2..]; } else params = value; if (std.mem.eql(u8, name, "--dirstat-by-file")) s.out.dirstat.mode = .files; if (std.mem.eql(u8, arg, "--cumulative")) s.out.dirstat.cumulative = true; if (params) |text| { var unknown: std.ArrayList([]const u8) = .empty; defer unknown.deinit(gpa); if (!try s.out.dirstat.parse(text, &unknown, gpa)) { stderr.writeAll("fatal: Failed to parse --dirstat/-X option parameter:\n") catch {}; for (unknown.items) |u| stderr.print(" Unknown dirstat parameter '{s}'\n", .{u}) catch {}; stderr.writeAll("\n") catch {}; return error.Fatal; } } return .yes; } if (std.mem.eql(u8, arg, "-p") or std.mem.eql(u8, arg, "-u") or std.mem.eql(u8, arg, "--patch")) { f.patch = true; s.format_given = true; } else if (std.mem.eql(u8, arg, "-s") or std.mem.eql(u8, arg, "--no-patch")) { f.* = .{}; s.format_given = true; // -s clears the name formats, but not what -s itself means. s.exclusive = 1; } else if (std.mem.eql(u8, arg, "--check")) { f.check = true; s.format_given = true; s.exclusive |= 8; } else if (std.mem.eql(u8, arg, "--raw")) { f.raw = true; s.format_given = true; } else if (std.mem.eql(u8, arg, "--name-only")) { f.name_only = true; s.format_given = true; s.exclusive |= 2; } else if (std.mem.eql(u8, arg, "--name-status")) { f.name_status = true; s.format_given = true; s.exclusive |= 4; } else if (std.mem.eql(u8, arg, "--numstat")) { f.numstat = true; s.format_given = true; } else if (std.mem.eql(u8, arg, "--shortstat")) { f.shortstat = true; s.format_given = true; } else if (std.mem.eql(u8, arg, "--summary")) { f.summary = true; s.format_given = true; } else if (std.mem.eql(u8, arg, "--patch-with-stat")) { f.patch = true; f.stat = true; s.format_given = true; } else if (std.mem.eql(u8, arg, "--patch-with-raw")) { f.patch = true; f.raw = true; s.format_given = true; } else if (std.mem.eql(u8, name, "--stat")) { f.stat = true; s.format_given = true; if (value) |v| { var it = std.mem.splitScalar(u8, v, ','); var k: usize = 0; while (it.next()) |part| : (k += 1) { const n = number(part) orelse { stderr.print("error: invalid --stat value: {s}\n", .{v}) catch {}; return error.UsageQuiet; }; switch (k) { 0 => s.stat_width = n, 1 => s.stat_name_width = n, 2 => s.stat_count = n, else => { stderr.print("error: invalid --stat value: {s}\n", .{v}) catch {}; return error.UsageQuiet; }, } } } } else if (std.mem.eql(u8, name, "--stat-width") or std.mem.eql(u8, name, "--stat-name-width") or std.mem.eql(u8, name, "--stat-graph-width") or std.mem.eql(u8, name, "--stat-count")) { var took = false; const v = value orelse (next orelse return requiresValue(stderr, name[2..])); if (value == null) took = true; const n = number(v) orelse { stderr.print("error: {s} expects a numerical value\n", .{name}) catch {}; return error.UsageQuiet; }; f.stat = true; s.format_given = true; if (std.mem.eql(u8, name, "--stat-width")) s.stat_width = n else if (std.mem.eql(u8, name, "--stat-name-width")) s.stat_name_width = n else if (std.mem.eql(u8, name, "--stat-graph-width")) s.stat_graph_width = n else s.stat_count = n; return if (took) .took_next else .yes; } else if (std.mem.eql(u8, arg, "--compact-summary")) { // It implies --stat, and says beside each name what --summary // would. f.stat = true; s.format_given = true; s.compact_summary = true; } else if (std.mem.eql(u8, arg, "-z")) { s.out.nul = true; } else if (std.mem.eql(u8, arg, "--minimal")) { s.chooseAlgorithm(.minimal); } else if (std.mem.eql(u8, arg, "--patience")) { // It forgets every --anchored before it, as git was seen to. s.chooseAlgorithm(.patience); s.anchors.clearRetainingCapacity(); } else if (std.mem.eql(u8, arg, "--histogram")) { s.chooseAlgorithm(.histogram); } else if (std.mem.eql(u8, name, "--anchored")) { // Patience, with one more anchor; but not chosen in the sense a // `diff.<driver>.algorithm` gives way to (as git was seen to). const v = value orelse (next orelse return requiresValue(stderr, "anchored")); try s.anchors.append(gpa, v); s.out.patch.lines.algorithm = .patience; return if (value == null) .took_next else .yes; } else if (std.mem.eql(u8, name, "--diff-algorithm")) { const v = value orelse (next orelse return requiresValue(stderr, "diff-algorithm")); const alg = LineDiff.Algorithm.fromName(v) orelse { stderr.writeAll("error: option diff-algorithm accepts \"myers\", \"minimal\", \"patience\" and \"histogram\"\n") catch {}; return error.UsageQuiet; }; s.chooseAlgorithm(alg); return if (value == null) .took_next else .yes; } else if (std.mem.eql(u8, arg, "-w") or std.mem.eql(u8, arg, "--ignore-all-space")) { s.out.patch.lines.whitespace.ignore_all = true; } else if (std.mem.eql(u8, arg, "-b") or std.mem.eql(u8, arg, "--ignore-space-change")) { s.out.patch.lines.whitespace.ignore_change = true; } else if (std.mem.eql(u8, arg, "--ignore-space-at-eol")) { s.out.patch.lines.whitespace.ignore_at_eol = true; } else if (std.mem.eql(u8, arg, "--ignore-cr-at-eol")) { s.out.patch.lines.whitespace.ignore_cr_at_eol = true; } else if (std.mem.eql(u8, arg, "--ignore-blank-lines")) { s.out.patch.hunks.ignore_blank_lines = true; } else if (std.mem.eql(u8, arg, "--no-ignore-blank-lines")) { s.out.patch.hunks.ignore_blank_lines = false; } else if (std.mem.startsWith(u8, arg, "-I") or std.mem.eql(u8, name, "--ignore-matching-lines")) { var took = false; const v = if (std.mem.startsWith(u8, arg, "-I")) (if (arg.len > 2) arg[2..] else blk: { took = true; break :blk next orelse { stderr.writeAll("error: switch `I' requires a value\n") catch {}; return error.UsageQuiet; }; }) else value orelse blk: { took = true; break :blk next orelse return requiresValue(stderr, "ignore-matching-lines"); }; const r = git.regex.Regex.compileWith(gpa, v, .{ .syntax = .extended }, null) catch |err| switch (err) { error.OutOfMemory => return err, else => return tool.fatalMessage(stderr, "invalid regex given to -I: '{s}'", .{v}), }; try s.ignore_regexes.append(gpa, r); return if (took) .took_next else .yes; } else if (std.mem.eql(u8, arg, "--indent-heuristic")) { s.out.patch.lines.indent_heuristic = true; } else if (std.mem.eql(u8, arg, "--no-indent-heuristic")) { s.out.patch.lines.indent_heuristic = false; } else if (std.mem.startsWith(u8, arg, "-U") or std.mem.eql(u8, name, "--unified")) { const v: ?[]const u8 = if (std.mem.startsWith(u8, arg, "-U")) (if (arg.len > 2) arg[2..] else null) else value; f.patch = true; s.format_given = true; if (v) |text| { const n = object_names.optionNumber(text) orelse { stderr.writeAll("error: --unified expects a numerical value\n") catch {}; return error.UsageQuiet; }; if (n < 0) { stderr.writeAll("error: --unified expects a non-negative integer\n") catch {}; return error.UsageQuiet; } s.out.patch.hunks.context = @intCast(n); } } else if (std.mem.eql(u8, name, "--inter-hunk-context")) { const v = value orelse (next orelse return requiresValue(stderr, "inter-hunk-context")); const n = number(v) orelse { stderr.writeAll("error: option `inter-hunk-context' expects a non-negative integer value with an optional k/m/g suffix\n") catch {}; return error.UsageQuiet; }; s.out.patch.hunks.inter_hunk_context = n; return if (value == null) .took_next else .yes; } else if (std.mem.eql(u8, arg, "-a") or std.mem.eql(u8, arg, "--text")) { s.out.patch.text = true; } else if (std.mem.eql(u8, arg, "--no-text")) { s.out.patch.text = false; } else if (std.mem.eql(u8, arg, "--full-index")) { s.full_index = true; } else if (std.mem.eql(u8, arg, "--no-full-index")) { s.full_index = false; } else if (std.mem.eql(u8, name, "--abbrev")) { s.abbrev_given = true; s.no_abbrev = false; s.abbrev_length = null; if (value) |v| { const n = object_names.optionNumber(v) orelse { stderr.print("error: option `abbrev' expects a numerical value\n", .{}) catch {}; return error.UsageQuiet; }; s.abbrev_length = if (n <= 0) 4 else @intCast(n); } } else if (std.mem.eql(u8, arg, "--no-abbrev")) { s.abbrev_given = false; s.no_abbrev = true; s.abbrev_length = null; } else if (std.mem.eql(u8, arg, "--no-prefix")) { s.no_prefix = true; s.src_prefix = null; s.dst_prefix = null; } else if (std.mem.eql(u8, arg, "--default-prefix")) { s.no_prefix = false; s.mnemonic = false; s.src_prefix = "a/"; s.dst_prefix = "b/"; } else if (std.mem.eql(u8, name, "--src-prefix") or std.mem.eql(u8, name, "--dst-prefix")) { const v = value orelse (next orelse return requiresValue(stderr, name[2..])); if (name[2] == 's') s.src_prefix = v else s.dst_prefix = v; return if (value == null) .took_next else .yes; } else if (std.mem.eql(u8, arg, "-R")) { s.reverse = true; } else if (std.mem.eql(u8, arg, "--exit-code")) { s.exit_code = true; } else if (std.mem.eql(u8, arg, "--no-exit-code")) { s.exit_code = false; } else if (std.mem.eql(u8, arg, "--quiet")) { s.quiet = true; } else if (std.mem.eql(u8, name, "--output")) { const v = value orelse (next orelse return requiresValue(stderr, "output")); s.output_path = v; return if (value == null) .took_next else .yes; } else if (std.mem.eql(u8, arg, "--no-color")) { // The default. } else if (std.mem.eql(u8, arg, "--no-renames")) { s.renames = false; s.copies = false; s.harder = false; } else if (std.mem.startsWith(u8, arg, "-B") or std.mem.eql(u8, name, "--break-rewrites")) { const v: []const u8 = if (arg[1] == 'B') arg[2..] else value orelse ""; const slash = std.mem.findScalar(u8, v, '/'); const first = git.diff.similarity.parseScore(v[0 .. slash orelse v.len]); const second = if (slash) |sl| git.diff.similarity.parseScore(v[sl + 1 ..]) else @as(?u32, 0); if (first == null or second == null) { stderr.writeAll("error: break-rewrites expects <n>/<m> form\n") catch {}; return error.UsageQuiet; } s.breaks = .{ first.?, second.? }; } else if (std.mem.startsWith(u8, arg, "-M") or std.mem.eql(u8, name, "--find-renames")) { const v: []const u8 = if (arg[1] == 'M') arg[2..] else value orelse ""; s.min_score = git.diff.similarity.parseScore(v) orelse { stderr.writeAll("error: invalid argument to find-renames\n") catch {}; return error.UsageQuiet; }; s.renames = true; s.copies = false; } else if (std.mem.startsWith(u8, arg, "-C") or std.mem.eql(u8, name, "--find-copies")) { const v: []const u8 = if (arg[1] == 'C') arg[2..] else value orelse ""; s.min_score = git.diff.similarity.parseScore(v) orelse { stderr.writeAll("error: invalid argument to find-copies\n") catch {}; return error.UsageQuiet; }; // -C a second time finds copies harder. if (s.copies) s.harder = true; s.renames = true; s.copies = true; } else if (std.mem.eql(u8, name, "--find-copies-harder") or std.mem.eql(u8, name, "--rename-empty") or std.mem.eql(u8, name, "--no-rename-empty")) { if (value != null) { stderr.print("error: option `{s}' takes no value\n", .{name[2..]}) catch {}; return error.UsageQuiet; } if (name[2] == 'f') { s.harder = true; s.renames = true; s.copies = true; } else s.rename_empty = name[2] == 'r'; } else if (std.mem.startsWith(u8, arg, "-l")) { var took = false; const v = if (arg.len > 2) arg[2..] else blk: { took = true; break :blk next orelse ""; }; s.limit = git.Config.value.parseInt(i64, v) catch { stderr.writeAll("error: switch `l' expects an integer value with an optional k/m/g suffix\n") catch {}; return error.UsageQuiet; }; return if (took) .took_next else .yes; } else if (std.mem.eql(u8, arg, "--no-textconv") or std.mem.eql(u8, arg, "--no-ext-diff")) { // Neither is ever used. } else if (std.mem.eql(u8, name, "--relative")) { s.relative = value orelse ""; } else if (std.mem.eql(u8, arg, "--no-relative")) { s.relative = null; } else if (std.mem.eql(u8, name, "--diff-filter")) { const v = value orelse (next orelse return requiresValue(stderr, "diff-filter")); s.filter_given = true; for (v) |c| { switch (c) { 'A', 'C', 'D', 'M', 'R', 'T', 'U', 'X', 'B' => s.filter_include |= @as(u32, 1) << @intCast(c - 'A'), 'a', 'c', 'd', 'm', 'r', 't', 'u', 'x', 'b' => s.filter_exclude |= @as(u32, 1) << @intCast(c - 'a'), '*' => return unsupported(stderr, s.command, "--diff-filter=*"), else => { stderr.print("error: unknown change class '{c}' in --diff-filter={s}\n", .{ c, v }) catch {}; return error.UsageQuiet; }, } } return if (value == null) .took_next else .yes; } else if (std.mem.eql(u8, name, "--ignore-submodules")) { const v = value orelse "all"; if (!std.mem.eql(u8, v, "none")) return unsupported(stderr, s.command, "--ignore-submodules"); } else if (std.mem.eql(u8, name, "--submodule")) { const v = value orelse "log"; if (!std.mem.eql(u8, v, "short")) return unsupported(stderr, s.command, "--submodule"); } else return .no; return .yes;}
/// Whether `--exit-code` finds nothing differing whatever differs, as/// git was seen to decide: when every format chosen is `--summary` or/// `--dirstat` measuring changes or files (not lines), `git diff` exits 0,/// and `diff-tree`, `diff-index` and `diff-files` do too under a/// whitespace option (`-w`, `-b`, `--ignore-space-at-eol`,/// `--ignore-cr-at-eol`) or `-I`, when whether anything differs would/// take comparing contents, which those formats are not counted as doing./// `--ignore-blank-lines` is not such an option here. Any other format/// alongside them counts again, and so does `-s` or `--quiet` after them,/// which leaves no format at all.pub fn exitCodeSeesNothing(s: *const Settings, porcelain: bool) bool { if (s.quiet) return false; const f = s.out.format; if (!f.summary and !f.dirstat) return false; if (f.dirstat and s.out.dirstat.mode == .lines) return false; if (f.raw or f.name_only or f.name_status or f.numstat or f.stat or f.shortstat or f.patch or f.check) return false; if (porcelain) return true; return s.out.patch.lines.whitespace.any() or s.out.patch.hunks.ignore_line != null;}
/// Settles what the options and configuration leave open: prefixes, the/// stat widths, `-I`, the object IDs' length./// Refuses options git refuses together.pub fn check(s: *Settings, stderr: *Io.Writer) !void { if (@popCount(s.exclusive) > 1) return tool.fatalMessage(stderr, "options '--name-only', '--name-status', '--check', and '-s' cannot be used together", .{}); // Where git puts the prefix among records ended by NULs is not // matched. if (s.line_prefix != null and s.out.nul) return unsupported(stderr, s.command, "--line-prefix with -z"); if (s.pickaxe_kind) |k| { if (k == .g and s.pickaxe_regex) return tool.fatalMessage(stderr, "options '-G' and '--pickaxe-regex' cannot be used together, use '--pickaxe-regex' with '-S'", .{}); // git's --quiet stops at the first difference it finds, before // the pickaxe has seen the rest, by a rule not matched. if (s.quiet) return unsupported(stderr, s.command, "--quiet with the pickaxe"); // git stops on a bug of its own. if (k == .object and s.ignore_case) return unsupported(stderr, s.command, "-i with --find-object"); if (k == .object and s.pickaxe_all) return tool.fatalMessage(stderr, "options '--pickaxe-all' and '--find-object' cannot be used together, use '--pickaxe-all' with '-G' and '-S'", .{}); } // How -W meets the changes these leave out has not been matched. if (s.out.patch.hunks.function_context and (s.out.patch.hunks.ignore_blank_lines or s.ignore_regexes.items.len != 0)) return unsupported(stderr, s.command, "-W with --ignore-blank-lines or -I");}
/// Sets up what the options ask of the diffs once the repository is/// open: the orderfile read, `--relative`'s prefix settled, and the/// pickaxe, each `--find-object` resolved with `revs` as git does while/// reading its options. A tree is refused by name: git then lists the/// trees of every diff it writes (`-t`), which is not matched.pub fn setUp(ctx: Context, repo: *git.Repository, s: *Settings, revs: *revisions.Revisions, stderr: *Io.Writer) !void { const gpa = ctx.gpa; if (s.order_file orelse (if (s.command.porcelain()) s.config_order_file else null)) |path| { // A relative path is the current directory's. const full = try std.fs.path.resolve(gpa, &.{ ctx.cwd, path }); defer gpa.free(full); s.order_text = Io.Dir.cwd().readFileAlloc(ctx.io, full, gpa, .unlimited) catch |err| { const why: []const u8 = switch (err) { error.FileNotFound => "No such file or directory", error.AccessDenied, error.PermissionDenied => "Permission denied", error.IsDir => "Is a directory", else => return unsupported(stderr, s.command, "an orderfile that cannot be read"), }; return tool.fatalMessage(stderr, "failed to read orderfile '{s}': {s}", .{ path, why }); }; s.order = try git.diff.order.OrderFile.parse(gpa, s.order_text); } if (s.relative) |r| { // git writes --summary's paths whole, the prefix left on, which is // not matched. if (s.out.format.summary) return unsupported(stderr, s.command, "--summary with --relative"); // Bare --relative is the current directory; a path given is the // top's, as git was seen to take it, and matched as a string. s.relative_owned = try gpa.dupe(u8, if (r.len == 0) repo.location.prefix orelse "" else r); s.relative_prefix = s.relative_owned; } const kind = s.pickaxe_kind orelse return; const state = try gpa.create(PickaxeState); state.* = .{ .kind = switch (kind) { .s => if (s.pickaxe_regex) .counted_regex else .string, .g => .lines, .object => .object, }, .text = s.pickaxe_text, .ignore_case = s.ignore_case, .all = s.pickaxe_all, }; s.pickaxe = state; if (kind != .object) return; for (s.find_objects.items) |text| { var diag: git.Diagnostics = .{}; const found = revs.resolver.resolve(text, &diag) catch { revs.resolver.clearMessages(); stderr.print("error: unable to resolve '{s}'\n", .{text}) catch {}; return error.UsageQuiet; }; revs.resolver.clearMessages(); if (repo.objects().readHeader(ctx.io, found.id, null)) |h| { if (h.kind == .tree) return unsupported(stderr, s.command, "--find-object naming a tree"); } else |_| {} try state.ids.append(gpa, found.id); }}
pub fn finish(ctx: Context, s: *Settings, mnemonic: ?[2][]const u8, names: ?*object_names.Names, settings: object_names.Settings) void { const gpa = ctx.gpa; var src: []const u8 = "a/"; var dst: []const u8 = "b/"; if (s.command.porcelain()) { if (s.mnemonic) if (mnemonic) |m| { src = m[0]; dst = m[1]; }; if (s.no_prefix) { src = ""; dst = ""; } } if (s.src_prefix) |p| src = p; if (s.dst_prefix) |p| dst = p; if (s.reverse) std.mem.swap([]const u8, &src, &dst); s.out.patch.reversed = s.reverse; s.out.patch.src_prefix = src; s.out.patch.dst_prefix = dst;
// The porcelain takes the width from COLUMNS, as it would from a // terminal; the plumbing was seen to keep to 80. var width: usize = 80; if (s.command.porcelain()) if (ctx.env.get("COLUMNS")) |c| if (std.fmt.parseInt(usize, c, 10)) |n| { if (n > 0) width = n; } else |_| {}; // The porcelain makes room for --line-prefix in the width it takes // from the terminal, counting it as columns, as git was seen to; a // width given is kept. if (s.command.porcelain()) if (s.line_prefix) |p| { width -|= git.width.of(p); }; if (s.stat_width) |w| if (w > 0) { width = w; }; s.out.stat = .{ .width = width, .name_width = s.stat_name_width orelse s.config_name_width, .graph_width = s.stat_graph_width orelse s.config_graph_width, .count = s.stat_count, .compact_summary = s.compact_summary, }; if (s.out.stat.name_width) |n| if (n == 0) { s.out.stat.name_width = null; };
s.matcher = .{ .gpa = gpa, .regexes = s.ignore_regexes.items }; if (s.ignore_regexes.items.len != 0) s.out.patch.hunks.ignore_line = .{ .context = &s.matcher, .matches = Matcher.matches }; s.out.patch.lines.anchors = s.anchors.items;
const default_length: usize = if (names) |n| n.defaultLength() else switch (settings.length) { .auto => 7, .digits => |d| d, .full => s.out.patch.object_format.hexLength(), }; const length = s.abbrev_length orelse default_length; s.out.patch.abbrev = if (s.full_index) null else length; // The porcelain shortens the raw format's object IDs unless told not // to; the plumbing only when told to. s.out.raw_abbrev = if (s.abbrev_given or (s.command.porcelain() and !s.no_abbrev)) length else null;}
// -- drivers -----------------------------------------------------------------
/// The variable `key` sets if it is `diff.<driver>.<variable>`.fn driverVariable(key: []const u8) ?[]const u8 { if (!std.mem.startsWith(u8, key, "diff.")) return null; const dot = std.mem.findScalarLast(u8, key, '.') orelse return null; if (dot <= "diff.".len) return null; return key[dot + 1 ..];}
/// Whether `key` is `diff.<driver>.algorithm`.fn isDriverAlgorithm(key: []const u8) bool { const variable = driverVariable(key) orelse return false; return std.ascii.eqlIgnoreCase(variable, "algorithm");}
/// Which syntax `key` gives a driver's function pattern in, if it is/// `diff.<driver>.xfuncname` or `diff.<driver>.funcname`.fn driverPattern(key: []const u8) ?git.diff.funcname.Syntax { const variable = driverVariable(key) orelse return null; if (std.ascii.eqlIgnoreCase(variable, "xfuncname")) return .xfuncname; if (std.ascii.eqlIgnoreCase(variable, "funcname")) return .funcname; return null;}
/// A driver's function pattern, compiled when a patch first needs it,/// which is when git compiles it and says what is wrong with it.const LazyPattern = struct { drivers: *Drivers, driver: []const u8, text: []const u8, syntax: git.diff.funcname.Syntax, ignore_case: bool = false, compiled: ?git.diff.funcname.Pattern = null,
fn find(context: ?*const anyopaque, line: []const u8, _: *[git.diff.hunks.function_name_max]u8) Allocator.Error!?[]const u8 { const l: *const LazyPattern = @ptrCast(@alignCast(context.?)); // `compileOnce` compiled it, or refused the patch. return l.compiled.?.find(line); }
fn compileOnce(context: ?*const anyopaque) git.diff.hunks.FunctionFinder.PrepareError!void { // The pattern is the driver table's own, and only it is changed. const l: *LazyPattern = @ptrCast(@alignCast(@constCast(context.?))); if (l.compiled != null) return; const d = l.drivers; var failure: git.diff.funcname.Failure = .{}; l.compiled = git.diff.funcname.Pattern.compile(d.gpa, l.text, l.syntax, l.ignore_case, &failure) catch |err| { switch (err) { error.OutOfMemory => return error.OutOfMemory, error.InvalidPattern => d.stderr.print("fatal: Invalid regexp to look for hunk header: {s}\n", .{failure.piece}) catch {}, error.LastNegated => d.stderr.print("fatal: Last expression must not be negated: {s}\n", .{failure.piece}) catch {}, error.Unsupported => d.stderr.print("fatal: zig-git {s}: the function lines of diff driver '{s}' use {s}, which is not supported\n", .{ d.command.name(), l.driver, failure.unsupported }) catch {}, } return error.FunctionLinesUnusable; }; }
fn finder(l: *const LazyPattern) git.diff.hunks.FunctionFinder { return .{ .context = l, .find = find, .prepare = compileOnce }; }};
/// The diff drivers attributes choose, read from the configuration.////// git reads the attributes files once for each set of attributes it asks/// about, saying what is wrong with them each time it does: once for the/// drivers, once for the conversions (which it asks about for every path/// it compares contents of), and once for the `whitespace` attribute when/// it works out a patch. `conversions` and `whitespace`, when given, are/// read at those moments, so that the warnings come as often as git's.pub const Drivers = struct { gpa: Allocator, config: *const git.Config, setup: ?*check_attr.Setup, conversions: ?*check_attr.Setup = null, whitespace: ?*check_attr.Setup = null, diff_id: ?u32 = null, /// For `--check`: the `whitespace` and `conflict-marker-size` /// attributes are read too, onto these rules. check: ?git.diff.check.Rules = null, whitespace_id: ?u32 = null, marker_id: ?u32 = null, stderr: *Io.Writer, command: Command, /// Function patterns, by driver. patterns: std.StringHashMapUnmanaged(*LazyPattern) = .empty,
/// Frees the compiled patterns. pub fn deinit(d: *Drivers) void { var it = d.patterns.valueIterator(); while (it.next()) |l| { if (l.*.compiled) |*c| c.deinit(); d.gpa.free(l.*.driver); d.gpa.destroy(l.*); } d.patterns.deinit(d.gpa); }
/// The `output.Drivers` reading from it. pub fn drivers(d: *Drivers) output.Drivers { return .{ .context = d, .get = get, .before_patch = beforePatch }; }
fn touch(d: *Drivers, setup: ?*check_attr.Setup, path: []const u8) output.Error!void { const s = setup orelse return; _ = s.check(path, d.stderr) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return error.DriverUnsupported, }; }
fn beforePatch(context: *anyopaque, path: []const u8) output.Error!void { const d: *Drivers = @ptrCast(@alignCast(context)); try d.touch(d.whitespace, path); }
fn get(context: *anyopaque, path: []const u8) output.Error!output.Driver { const d: *Drivers = @ptrCast(@alignCast(context)); const s = d.setup orelse return .{}; if (d.diff_id == null) d.diff_id = s.attributes.intern("diff") catch return error.OutOfMemory; if (d.check != null) { if (d.whitespace_id == null) d.whitespace_id = s.attributes.intern("whitespace") catch return error.OutOfMemory; if (d.marker_id == null) d.marker_id = s.attributes.intern("conflict-marker-size") catch return error.OutOfMemory; } const r = s.check(path, d.stderr) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return error.DriverUnsupported, }; const state = r.get(d.diff_id.?); try d.touch(d.conversions, path); var driver = try d.fromState(state); if (d.check) |base| { const again = r; driver.whitespace = switch (again.get(d.whitespace_id.?)) { .unspecified => null, .set => git.diff.check.Rules.everything(base), .unset => git.diff.check.Rules.nothing, .value => |v| git.diff.check.Rules.parse(.defaults, v, null), }; if (driver.whitespace) |ws| if (ws.conflicting()) { d.stderr.print("fatal: zig-git {s}: a whitespace attribute with both tab-in-indent and indent-with-non-tab is not supported\n", .{d.command.name()}) catch {}; return error.DriverUnsupported; }; switch (again.get(d.marker_id.?)) { .value => |v| driver.marker_size = std.fmt.parseInt(usize, v, 10) catch { d.stderr.print("fatal: zig-git {s}: conflict-marker-size '{s}' is not supported\n", .{ d.command.name(), v }) catch {}; return error.DriverUnsupported; }, else => {}, } } return driver; }
fn fromState(d: *Drivers, state: git.Attributes.State) output.Error!output.Driver { switch (state) { .unspecified => return .{}, .set => return .{ .binary = false }, .unset => return .{ .binary = true }, .value => |driver| return d.named(driver), } }
fn setting(d: *Drivers, driver: []const u8, variable: []const u8) ?*const git.Config.Entry { var buf: [512]u8 = undefined; const key = std.fmt.bufPrint(&buf, "diff.{s}.{s}", .{ driver, variable }) catch return null; return d.config.get(key) catch null; }
fn named(d: *Drivers, driver: []const u8) output.Error!output.Driver { var result: output.Driver = .{}; for ([_][]const u8{ "command", "textconv" }) |v| if (d.setting(driver, v) != null) { d.stderr.print("fatal: zig-git {s}: the diff driver '{s}' runs a program ('diff.{s}.{s}'), which is not supported\n", .{ d.command.name(), driver, driver, v }) catch {}; return error.DriverUnsupported; }; // `binary` says either way: false makes a file text whatever is // in it. `readConfig` refused a value that is not a boolean. if (d.setting(driver, "binary")) |e| result.binary = git.Config.value.parseBool(e.value) orelse false; // A name git does not know is passed over, as git was seen to // pass it over; `readConfig` refused one without a value. if (d.setting(driver, "algorithm")) |e| result.algorithm = LineDiff.Algorithm.fromName(e.value orelse ""); // Whichever of xfuncname and funcname was set last, as git was // seen to take it, or the built-in driver's. const x = d.setting(driver, "xfuncname"); const f = d.setting(driver, "funcname"); const chosen: ?*const git.Config.Entry = if (x == null) f else if (f == null) x else if (@intFromPtr(f.?) > @intFromPtr(x.?)) f else x; if (chosen) |e| { const syntax: git.diff.funcname.Syntax = if (e == f) .funcname else .xfuncname; result.function_name = (try d.lazy(driver, e.value orelse "", syntax, false)).finder(); } else if (git.diff.funcname.builtin(driver)) |b| { result.function_name = (try d.lazy(driver, b.pattern, .xfuncname, b.ignore_case)).finder(); } else if (isBuiltinDriver(driver)) { d.stderr.print("fatal: zig-git {s}: the built-in diff driver '{s}' is not supported\n", .{ d.command.name(), driver }) catch {}; return error.DriverUnsupported; } return result; }
fn lazy(d: *Drivers, driver: []const u8, text: []const u8, syntax: git.diff.funcname.Syntax, ignore_case: bool) Allocator.Error!*LazyPattern { const gop = try d.patterns.getOrPut(d.gpa, driver); if (!gop.found_existing) { errdefer d.patterns.removeByPtr(gop.key_ptr); const l = try d.gpa.create(LazyPattern); errdefer d.gpa.destroy(l); const name = try d.gpa.dupe(u8, driver); l.* = .{ .drivers = d, .driver = name, .text = text, .syntax = syntax, .ignore_case = ignore_case }; gop.key_ptr.* = name; gop.value_ptr.* = l; } return gop.value_ptr.*; }};
/// Whether `name` is one of git's built-in diff drivers: those git 2.56/// was seen to treat differently from a driver it does not know.pub fn isBuiltinDriver(name: []const u8) bool { const names = [_][]const u8{ "ada", "bash", "bibtex", "cpp", "csharp", "css", "dts", "elixir", "fortran", "fountain", "golang", "html", "ini", "java", "kotlin", "markdown", "matlab", "objc", "pascal", "perl", "php", "python", "r", "ruby", "rust", "scheme", "swift", "tex", }; for (names) |n| if (std.mem.eql(u8, n, name)) return true; return false;}
// -- contents ----------------------------------------------------------------
/// Contents read from the object database.pub const BlobSource = struct { ctx: Context, repo: *git.Repository,
/// The `output.Source` reading from it. pub fn source(b: *BlobSource) output.Source { return .{ .context = b, .read = read }; }
fn read(context: *anyopaque, gpa: Allocator, pair: *const output.Pair, side: output.Side) output.Error!output.Contents { _ = pair; const b: *BlobSource = @ptrCast(@alignCast(context)); var o = b.repo.objects().read(b.ctx.io, gpa, side.id, null) catch |err| switch (err) { error.OutOfMemory => |e| return e, else => return error.ContentsUnreadable, }; if (o.kind != .blob) { o.deinit(gpa); return error.ContentsUnreadable; } return .{ .bytes = o.data, .allocation = o.allocation }; }};
// -- renames -----------------------------------------------------------------
/// The rename detection the settings ask for; null for none.pub fn renameOptions(s: *const Settings) ?git.diff.renames.Options { if (!s.renames and s.breaks == null) return null; var limit: u32 = 1000; if (s.limit orelse s.config_limit) |l| limit = if (l <= 0) 0 else @intCast(@min(l, std.math.maxInt(u32))); var breaks: ?git.diff.renames.Breaks = null; if (s.breaks) |b| { breaks = .{}; if (b[0] != 0) breaks.?.break_score = b[0]; if (b[1] != 0) breaks.?.merge_score = b[1]; } return .{ .renames = s.renames, .breaks = breaks, .copies = s.copies or s.harder, .min_score = if (s.min_score == 0) git.diff.similarity.default_min_score else s.min_score, .limit = limit, .rename_empty = s.rename_empty, };}
/// What `prepare` can fail with: `Fatal` once it has said why.pub const PrepareError = git.diff.renames.Error || Io.Writer.Error || error{Fatal};
/// The rename limit's warnings, gathered over the diffs of a walk and/// written once at its end, with the largest limit any of them needed./// `git log` and `git diff-tree --stdin` are one walk; `git show` makes/// one of each commit it is given, and was seen to write the warnings/// after each, as long as the last of them whose search was held to the/// limit at all reached it: one whose diffs all came within the limit/// writes none, and one with nothing to search for (no destination or/// no source left for the inexact search) says again what the last/// that reached it needed. Once a search has fallen back on copies from/// modified paths alone, every walk after says so, even one within the/// limit, which then writes that line alone. A merge's combined diff/// (`-c`, `--cc`, `show`'s default) was seen neither to warn nor to stop/// a warning, though its renames are cut short as any others, and nor/// was `--follow`; `quiet` is set for those.pub const RenameWarnings = struct { /// Whether a search has fallen back on copies from modified paths. modified_only: bool = false, /// What the warning names: the most a diff of the last walk that /// reached the limit needed; 0 when the last walk that searched came /// within it. needed: usize = 0, /// The most a diff of this walk has needed; 0 until one reaches the /// limit. walk_needed: usize = 0, /// Whether a diff of this walk came within the limit. walk_within: bool = false, /// Set while a combined diff is worked out, and under `--follow`. quiet: bool = false,
fn note(w: *RenameWarnings, l: git.diff.renames.Limited) void { if (w.quiet) return; if (l.copies_from_modified_only) w.modified_only = true; w.walk_needed = @max(w.walk_needed, l.needed); }
fn noteWithin(w: *RenameWarnings) void { if (!w.quiet) w.walk_within = true; }
/// Ends a walk and writes the warnings it leaves. pub fn flush(w: *RenameWarnings, stderr: *Io.Writer) Io.Writer.Error!void { if (w.walk_needed != 0) { w.needed = w.walk_needed; } else if (w.walk_within) { w.needed = 0; } w.walk_needed = 0; w.walk_within = false; if (w.modified_only) try stderr.writeAll("warning: only found copies from modified paths due to too many files.\n") else if (w.needed != 0) try stderr.writeAll("warning: exhaustive rename detection was skipped due to too many files.\n"); if (w.needed != 0) try stderr.print("warning: you may want to set your diff.renameLimit variable to at least {d} and retry the command.\n", .{w.needed}); }};
/// The pairs as the formats see them: renames and copies found among/// `pairs` as the settings ask (with the `unchanged` paths as sources/// under `--find-copies-harder`), what the limit cut short noted in/// `warnings`, then the pickaxe and `--diff-filter` applied, in that/// order (gitdiffcore(7); `--pickaxe-all --diff-filter=M` was seen to/// keep the modified paths of a diff the pickaxe kept whole). The caller/// frees the list.pub fn prepare(gpa: Allocator, s: *const Settings, all_pairs: []const output.Pair, all_unchanged: []const output.Pair, source: output.Source, drivers: output.Drivers, warnings: *RenameWarnings, stderr: *Io.Writer) PrepareError![]output.Pair { var pairs = all_pairs; var unchanged = all_unchanged; // --relative keeps the paths under its prefix, and strips it, before // anything else looks at them: a rename from outside it was seen to // be an addition. var relative_pairs: []output.Pair = &.{}; defer gpa.free(relative_pairs); var relative_unchanged: []output.Pair = &.{}; defer gpa.free(relative_unchanged); // The orderfile and the rotation still look at the whole paths, so // each cut path (a slice of its whole one) maps back to it. var wholes: std.AutoHashMapUnmanaged(usize, []const u8) = .empty; defer wholes.deinit(gpa); if (s.relative_prefix) |rp| { relative_pairs = try stripRelative(gpa, s, all_pairs, rp, &wholes, stderr); relative_unchanged = try stripRelative(gpa, s, all_unchanged, rp, &wholes, stderr); pairs = relative_pairs; unchanged = relative_unchanged; } var found: []output.Pair = &.{}; defer gpa.free(found); var list: []const output.Pair = pairs; if (renameOptions(s)) |options| { var merged: std.ArrayList(output.Pair) = .empty; defer merged.deinit(gpa); if (s.harder and unchanged.len != 0) { // Both lists are in the order of their paths. var i: usize = 0; var j: usize = 0; while (i < pairs.len or j < unchanged.len) { if (j == unchanged.len or (i < pairs.len and std.mem.order(u8, pairs[i].path, unchanged[j].path) != .gt)) { try merged.append(gpa, pairs[i]); i += 1; } else { try merged.append(gpa, unchanged[j]); j += 1; } } } else try merged.appendSlice(gpa, pairs); const r = try git.diff.renames.detect(gpa, merged.items, source, options); found = r.pairs; list = found; if (r.limited) |l| warnings.note(l) else if (r.within_limit) warnings.noteWithin(); } var picked: []output.Pair = &.{}; defer gpa.free(picked); if (s.pickaxe) |state| { picked = try pick(gpa, s, state, list, source, drivers, stderr); list = picked; } // Then the orderfile and the rotation, before --diff-filter. var ordered: []output.Pair = &.{}; defer gpa.free(ordered); if (s.order != null or s.rotation != null) { ordered = try gpa.dupe(output.Pair, list); list = ordered; // Both go by the paths as they were before --relative cut them. const paths = try gpa.alloc([]const u8, ordered.len); defer gpa.free(paths); for (ordered, paths) |*p, *path| path.* = wholes.get(@intFromPtr(p.path.ptr)) orelse p.path; if (s.order) |o| try o.sort(gpa, ordered, paths); // How many pairs --skip-to drops from the end, once rotated. var dropped: usize = 0; if (s.rotation) |r| { const nearest = s.command == .log or s.command == .show or s.command == .whatchanged; // Where `log` finds no path at or after the one named, it // leaves the paths as they are; with no paths there is nothing // to look for. if (git.diff.order.start(paths, r.name, nearest)) |at| { _ = git.diff.order.rotate(ordered, at, .rotate); if (r.how == .skip) dropped = at; } else if (!nearest and ordered.len != 0) { return tool.fatalMessage(stderr, "No such path '{s}' in the diff", .{r.name}); } } // Which of a deleted source's pairs is the rename goes by this // order, the pairs --skip-to drops still counted. try git.diff.renames.relabel(gpa, ordered); list = ordered[0 .. ordered.len - dropped]; } var shown: std.ArrayList(output.Pair) = .empty; errdefer shown.deinit(gpa); for (list) |*p| if (s.passesPair(p)) try shown.append(gpa, p.*); return shown.toOwnedSlice(gpa);}
/// The pairs under `prefix`, with it taken off their paths (and the `/`/// after it, if the prefix does not end in one). Working tree files are/// refused, since their paths are read from.fn stripRelative(gpa: Allocator, s: *const Settings, pairs: []const output.Pair, prefix: []const u8, wholes: *std.AutoHashMapUnmanaged(usize, []const u8), stderr: *Io.Writer) PrepareError![]output.Pair { var kept: std.ArrayList(output.Pair) = .empty; errdefer kept.deinit(gpa); for (pairs) |p| { if (!std.mem.startsWith(u8, p.path, prefix)) continue; if (p.old.worktree or p.new.worktree) return unsupported(stderr, s.command, "--relative with the working tree"); var q = p; q.path = p.path[prefix.len..]; if (q.path.len != 0 and q.path[0] == '/') q.path = q.path[1..]; try wholes.put(gpa, @intFromPtr(q.path.ptr), p.path); try kept.append(gpa, q); } return kept.toOwnedSlice(gpa);}
/// The pairs the pickaxe keeps, its pattern compiled the first time,/// with git's words for what goes wrong.fn pick(gpa: Allocator, s: *const Settings, state: *PickaxeState, list: []const output.Pair, source: output.Source, drivers: output.Drivers, stderr: *Io.Writer) PrepareError![]output.Pair { if (state.compiled == null) { if (state.kind == .object) { state.compiled = git.diff.pickaxe.initObjects(state.ids.items); } else { var failure: git.regex.Failure = .{}; state.compiled = git.diff.pickaxe.init(gpa, state.kind, state.text, state.ignore_case, &failure) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.InvalidPattern => return tool.fatalMessage(stderr, "invalid regex: {s}", .{failure.problem.message()}), error.Unsupported => return tool.fatalMessage(stderr, "zig-git {s}: a pickaxe pattern using {s} is not supported", .{ s.command.name(), failure.unsupported }), }; state.compiled.?.all = state.all; } } const pk = &state.compiled.?; for (list) |*p| if (p.status == .unmerged) return unsupported(stderr, s.command, "the pickaxe with unmerged paths"); if (pk.kind == .object) for (list) |*p| { if ((p.old.present() and !p.old.id_known) or (p.new.present() and !p.new.id_known)) return unsupported(stderr, s.command, "--find-object with the working tree"); }; return pk.filter(gpa, list, source, drivers, s.out) catch |err| switch (err) { error.TreeContents => return tool.fatalMessage(stderr, "unable to read files to diff", .{}), error.DriverUnsupported => return error.Fatal, error.ContentsUnreadable => return tool.fatalMessage(stderr, "zig-git {s}: cannot read a blob", .{s.command.name()}), error.LongSearchUnsupported, error.TooManyLines => |e| return unsupported(stderr, s.command, comparisonRefusal(e).?), error.BlankFileUnsupported => unreachable, error.OutOfMemory => return error.OutOfMemory, error.WriteFailed => return error.WriteFailed, };}
/// Lines with their newlines; a last line without one is its own.const LineIterator = struct { text: []const u8, pos: usize = 0,
fn next(it: *LineIterator) ?[]const u8 { if (it.pos >= it.text.len) return null; const end = if (std.mem.findScalarPos(u8, it.text, it.pos, '\n')) |nl| nl + 1 else it.text.len; defer it.pos = end; return it.text[it.pos..end]; }};