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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! `zig-git diff-index` and `zig-git diff-files`: a tree against the index//! or the working tree, and the index against the working tree, written as//! git's plumbing writes them, from the stat data the index holds; the//! index is never refreshed or written.//!//! zig-git diff-index [-m] [--cached] [<diff-option>...] <tree-ish> [[--] <path>...]//! zig-git diff-files [-q] [-0 | -1 | -2 | -3 | --base | --ours | --theirs] [<diff-option>...] [[--] <path>...]//!//! Files in the working tree are converted on their way in as git//! converts them (`git.convert`), with git's warnings. Refused by name://! `core.fsmonitor`, a file a filter driver's command would clean, one in//! a `working-tree-encoding` zig-charset does not convert, a checked-out//! submodule, combined diffs of unmerged paths, `--merge-base` and a//! sparse index.
const std = @import("std");const Allocator = std.mem.Allocator;const Io = std.Io;
const git = @import("git");const output = git.diff.output;const IndexDiff = git.IndexDiff;const convert = git.convert;const ObjectId = git.ObjectId;const Diagnostics = git.Diagnostics;const tool = @import("zig_git.zig");const Context = tool.Context;const discovery = @import("discovery.zig");const object_names = @import("object_names.zig");const revisions = @import("revisions.zig");const pathspecs = @import("pathspecs.zig");const check_attr = @import("check_attr.zig");const repo_diff = @import("repo_diff.zig");const line_prefix = @import("line_prefix.zig");const ls_files = @import("ls_files.zig");const diff_tree = @import("diff_tree.zig");
/// The working tree as git sees it: files converted on their way in as/// git converts them (`git.convert`), with what git says about them.pub const Worktree = struct { ctx: Context, native: IndexDiff.NativeWorkTree, attrs: *check_attr.Setup, config: *const git.Config, settings: convert.Settings, /// The index, whose blobs `auto` asks about. index: *const git.Index, db: git.ObjectDatabase, stderr: *Io.Writer, command: repo_diff.Command, names: [convert.AttributeStates.names.len]?u32 = @splat(null), diff_name: ?u32 = null, /// Set once something has been refused or git's fatal error written. refused: bool = false, /// Whether the filter drivers' settings have been looked at, which /// git does the first time it converts anything. filters_checked: bool = false, /// Whether the comparison refreshes as `git diff` does. refresh: bool = false, /// The section of the output under way, as git writes it; null /// before the output, while the comparison reads. section: ?Section = null, /// How many sections have begun. sections: u32 = 0, /// Whether the whitespace filter is a section of its own, as git's /// check of the raw lines and the names is, rather than the patch's. separate_check: bool = false, /// `--dirstat=lines`, which reads as the counts do rather than as /// the other dirstats. dirstat_lines: bool = false, /// Whether the section under way began with a whitespace filter /// that is not a check of its own. filtering: bool = false, /// `--quiet`: git reads nothing more once the output begins. quiet: bool = false, /// What git has said, or is to say, of each path it converted. said: std.StringHashMapUnmanaged(Said) = .empty, /// What the patch section begins by saying, in the order of the /// paths: what a whitespace check read, which git reads again for /// the patch whether or not it shows anything. owed: std.ArrayList([]const u8) = .empty, arena: std.heap.ArenaAllocator, /// What `hintColor` found, once it has looked. hint_color: ?*const ?[]const u8 = null,
/// A section of git's output: it reads a working tree file afresh /// for each, and says again what converting it did. /// The dirstat, but for `--dirstat=lines`, converts each file once. const Section = enum { check, counts, dirstat, patch };
const Said = struct { /// The section the path was last read in. section: u32 = 0, /// The comparison's read, which the first section reads again /// without a word, as `git diff` keeps what it read to see /// whether a file whose stat data changed did. held: bool = false, /// Said at the start of the patch section. owed: bool = false, /// Found binary by its contents by a section of the output. seen_binary: bool = false, /// What converting the path says: the encoding's error, and the /// warning about its line endings, each a line or nothing. err: []const u8 = "", warning: []const u8 = "",
/// Writes, for each letter of `pattern`, the error (`E`) or the /// warning (`W`). fn emit(said: *const Said, w: *Io.Writer, pattern: []const u8) void { for (pattern) |c| w.writeAll(if (c == 'E') said.err else said.warning) catch {}; } };
/// Frees what it keeps. pub fn deinit(w: *Worktree) void { w.said.deinit(w.ctx.gpa); w.owed.deinit(w.ctx.gpa); w.arena.deinit(); }
/// Readies it for the output, which `options` asks for: `quiet` for /// `--quiet`. pub fn startOutput(w: *Worktree, options: output.Options, quiet: bool) void { const f = options.format; w.separate_check = f.raw or f.name_only or f.name_status; w.dirstat_lines = options.dirstat.mode == .lines; w.quiet = quiet; }
/// Sets up the conversions for `repo`, whose settings `discovery` /// has already found good. pub fn init(ctx: Context, repo: *git.Repository, attrs: *check_attr.Setup, index: *const git.Index, stderr: *Io.Writer, command: repo_diff.Command) Worktree { return .{ .ctx = ctx, .native = .{ .io = ctx.io, .top = repo.location.work_tree orelse repo.location.work_dir, .dir = repo.work_dir }, .attrs = attrs, .config = &repo.config, .settings = convert.Settings.fromConfig(&repo.config, null) catch .{}, .index = index, .db = repo.objects(), .stderr = stderr, .command = command, .arena = .init(ctx.gpa), }; }
/// The `WorkTree` converting what it reads. pub fn workTree(w: *Worktree) IndexDiff.WorkTree { return .{ .context = w, .lstat = lstat, .read = read, .isEmptyDirectory = isEmpty, .beginPass = beginPass }; }
fn beginPass(context: *anyopaque, pass: output.Pass) void { const w: *Worktree = @ptrCast(@alignCast(context)); const section: Section = switch (pass) { .filter => if (w.separate_check) .check else .patch, .check, .patch => .patch, .counts => .counts, .dirstat => if (w.dirstat_lines) .counts else .dirstat, }; // A whitespace filter that is the patch's own begins it, or // begins the counts when they come first. const filtering = w.filtering; w.filtering = pass == .filter and section == .patch; if (w.section == .patch and section == .patch) return; // `--dirstat=lines` takes the counts' when they were made. if (pass == .dirstat and w.section == .counts and section == .counts) return; if (filtering and pass == .counts) { w.section = .counts; return; } w.section = section; w.sections += 1; if (section != .patch) return; if (!w.quiet) for (w.owed.items) |path| { const said = w.said.getPtr(path) orelse continue; said.emit(w.stderr, if (said.seen_binary) "E" else "EEW"); said.section = w.sections; }; }
/// Says, as git does, what converting `path` said, once the /// comparison or the output read it: `message`, and `changed` /// whether what it read differs from the index. /// Whether a patch shows a file as binary, which git reads no more /// for its patch than it has to. const Binary = enum { no, by_contents, by_attribute };
fn say(w: *Worktree, path: []const u8, said_text: Said, reading: IndexDiff.Reading, changed: bool, binary: Binary) Allocator.Error!void { const gpa = w.ctx.gpa; const entry = try w.said.getOrPut(gpa, path); if (!entry.found_existing) { const arena = w.arena.allocator(); entry.key_ptr.* = arena.dupe(u8, path) catch |err| { w.said.removeByPtr(entry.key_ptr); return err; }; entry.value_ptr.* = .{ .err = try arena.dupe(u8, said_text.err), .warning = try arena.dupe(u8, said_text.warning) }; } const said = entry.value_ptr; if (w.section == null) { // The comparison. A racily clean file is converted to compare // it, which says what the encoding could not do and nothing // more; `git diff` reads one that changed again, and one // whose stat data changed, to see whether it did, and says it // all, keeping what it read for the first section. if (reading == .racy) { said.emit(w.stderr, "E"); if (!w.refresh or !changed) return; } else if (!w.refresh) return; said.emit(w.stderr, "EW"); said.held = changed; return; } if (said.section == w.sections) return; said.section = w.sections; if (w.quiet) return; const held = said.held; said.held = false; if (w.section == .patch and said.owed) return; // A file `-diff` makes binary is only hashed for the whitespace // check and the patch. if (w.section != .counts and w.section != .dirstat and binary == .by_attribute) return said.emit(w.stderr, "E"); // The dirstat converts a file once, and not at all after the // comparison's read that `git diff` keeps. if (w.section == .dirstat) { if (binary == .by_contents) said.seen_binary = true; return said.emit(w.stderr, if (held) "" else "EW"); } // One an earlier section found binary by its contents is only // hashed in each one after. if (said.seen_binary) return said.emit(w.stderr, "E"); if (binary == .by_contents) said.seen_binary = true; // One a whitespace check read is read again for the patch. if (w.section == .check and !said.owed) { said.owed = true; try w.owed.append(gpa, entry.key_ptr.*); } // A fresh read converts once for the size and once for the // contents, which is when git warns. said.emit(w.stderr, if (held) "E" else "EEW"); }
fn lstat(context: *anyopaque, path: []const u8) IndexDiff.WorkTreeError!?IndexDiff.FileInfo { const w: *Worktree = @ptrCast(@alignCast(context)); return IndexDiff.NativeWorkTree.workTree(&w.native).lstat(&w.native, path); }
fn isEmpty(context: *anyopaque, path: []const u8) IndexDiff.WorkTreeError!bool { const w: *Worktree = @ptrCast(@alignCast(context)); return IndexDiff.NativeWorkTree.workTree(&w.native).isEmptyDirectory(&w.native, path); }
fn read(context: *anyopaque, gpa: Allocator, path: []const u8, symlink: bool, reading: IndexDiff.Reading) IndexDiff.WorkTreeError![]u8 { const w: *Worktree = @ptrCast(@alignCast(context)); const data = try IndexDiff.NativeWorkTree.workTree(&w.native).read(&w.native, gpa, path, symlink, reading); if (symlink) return data; return w.convertIn(gpa, path, data, reading); }
/// Stops as git stops, having said why. fn fatal(w: *Worktree, comptime fmt: []const u8, args: anytype) IndexDiff.WorkTreeError { if (!w.refused) w.stderr.print(fmt, args) catch {}; w.refused = true; return error.WorkTreeUnsupported; }
/// `data`, read from `path`, converted as git converts it. fn convertIn(w: *Worktree, gpa: Allocator, path: []const u8, data: []u8, reading: IndexDiff.Reading) IndexDiff.WorkTreeError![]u8 { // `convert.clean` takes `data`, and frees it when it fails. var owned = true; errdefer if (owned) gpa.free(data); if (!w.filters_checked) { w.filters_checked = true; if (convert.filterSettingsProblem(w.config)) |problem| switch (problem) { .bad_bool => |e| return w.fatal("fatal: bad boolean config value '{s}' for '{s}'\n", .{ e.value orelse "", e.key }), .missing_value => |e| { if (e.origin.kind == .file) return w.fatal("error: missing value for '{s}'\nfatal: bad config variable '{s}' in file '{s}' at line {d}\n", .{ e.key, e.key, e.origin.name, e.line }); return w.fatal("error: missing value for '{s}'\nfatal: unable to parse '{s}' from command-line config\n", .{ e.key, e.key }); }, }; } const names = convert.AttributeStates.names; if (w.names[0] == null) { for (names, 0..) |n, k| w.names[k] = w.attrs.attributes.intern(n) catch return error.OutOfMemory; w.diff_name = w.attrs.attributes.intern("diff") catch return error.OutOfMemory; } const r = w.attrs.check(path, w.stderr) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => { w.refused = true; return error.WorkTreeUnsupported; }, }; const rules = convert.Rules.of(.{ .text = r.get(w.names[0].?), .crlf = r.get(w.names[1].?), .eol = r.get(w.names[2].?), .ident = r.get(w.names[3].?), .working_tree_encoding = r.get(w.names[4].?), .filter = r.get(w.names[5].?), }, w.settings); if (!rules.converts()) return data; const unset_diff = r.get(w.diff_name.?) == .unset; if (rules.filter) |driver| switch (try convert.filterOf(gpa, w.config, driver)) { .none => {}, .required_missing => return w.fatal("fatal: {s}: clean filter '{s}' failed\n", .{ path, driver }), .configured => return w.fatal("fatal: zig-git {s}: running the clean filter '{s}' for '{s}' is not supported\n", .{ w.command.name(), driver, path }), }; var blob: IndexBlob = .{ .w = w, .path = path }; owned = false; const cleaned = convert.clean(gpa, data, rules, .{ .context = &blob, .keepsCrlf = IndexBlob.keepsCrlf }) catch |err| switch (err) { error.OutOfMemory => |e| return e, error.EncodingInvalid => return w.fatal("fatal: true/false are no valid working-tree-encodings\n", .{}), error.EncodingUnsupported => return w.fatal("fatal: zig-git {s}: converting '{s}' from the working-tree-encoding '{s}' is not supported\n", .{ w.command.name(), path, rules.encoding.name }), }; errdefer gpa.free(cleaned.data); if (cleaned.encoding_failed or cleaned.bom != null or (cleaned.warning != null and w.settings.safecrlf != .false)) { var err: Io.Writer.Allocating = .init(gpa); defer err.deinit(); var warning: Io.Writer.Allocating = .init(gpa); defer warning.deinit(); if (cleaned.encoding_failed) try printOrOom(&err.writer, "error: failed to encode '{s}' from {s} to UTF-8\n", .{ path, rules.encoding.name }); if (cleaned.bom) |p| p.write(&err.writer, path, rules.encoding.name, try w.hintColor()) catch return error.OutOfMemory; if (cleaned.warning) |kind| if (w.settings.safecrlf != .false) try printOrOom(&warning.writer, "warning: in the working copy of '{s}', {s}\n", .{ path, kind.text() }); var changed = true; if (w.section == null) if (w.index.find(path, 0)) |at| { const e = &w.index.entries[at]; changed = !git.hashObject(e.id.object_format, .blob, cleaned.data).eql(e.id); }; const binary: Binary = if (unset_diff) .by_attribute else if (git.diff.patch.isBinary(cleaned.data)) .by_contents else .no; try w.say(path, .{ .err = err.written(), .warning = warning.written() }, reading, changed, binary); } return cleaned.data; }
/// The color git writes a hint in, or null for none: `color.advice` /// set to `always`, or, for the porcelain and with `color.advice` not /// set, `color.ui` set to `always`; in `color.advice.hint`, yellow /// unless that says otherwise. Any other value is git's for a /// terminal, which this takes stderr never to be. fn hintColor(w: *Worktree) Allocator.Error!?[]const u8 { if (w.hint_color) |c| return c.*; const always = struct { fn of(config: *const git.Config, key: []const u8) ?bool { const e = (config.get(key) catch return null) orelse return null; return std.ascii.eqlIgnoreCase(e.value orelse "", "always"); } }.of; const on = always(w.config, "color.advice") orelse (w.command.porcelain() and (always(w.config, "color.ui") orelse false)); const arena = w.arena.allocator(); const color = try arena.create(?[]const u8); color.* = null; if (on) { var buffer: [git.pretty.color_max]u8 = undefined; const spec = (w.config.getString("color.advice.hint") catch null) orelse "yellow"; color.* = try arena.dupe(u8, git.pretty.parseColor(spec, &buffer) orelse "\x1b[33m"); } w.hint_color = color; return color.*; }
/// The blob the index holds for a path, for `auto`. const IndexBlob = struct { w: *Worktree, path: []const u8,
fn keepsCrlf(context: *anyopaque) Allocator.Error!bool { const b: *IndexBlob = @ptrCast(@alignCast(context)); const at = b.w.index.find(b.path, 0) orelse return false; const e = &b.w.index.entries[at]; if (e.mode & 0o170000 != 0o100000) return false; var o = b.w.db.read(b.w.ctx.io, b.w.ctx.gpa, e.id, null) catch |err| switch (err) { error.OutOfMemory => |x| return x, else => return false, }; defer o.deinit(b.w.ctx.gpa); if (o.kind != .blob) return false; return convert.keepsCrlf(o.data); } };};
fn printOrOom(w: *Io.Writer, comptime fmt: []const u8, args: anytype) Allocator.Error!void { w.print(fmt, args) catch return error.OutOfMemory;}
/// Settings read for the working tree: `core.fileMode` and the rest.fn worktreeOptions(repo: *git.Repository, stderr: *Io.Writer) !IndexDiff.Options { var o: IndexDiff.Options = .{}; o.file_mode = try ls_files.boolSetting(repo, "core.fileMode", stderr) orelse true; o.trust_ctime = try ls_files.boolSetting(repo, "core.trustctime", stderr) orelse true; o.symlinks = try ls_files.boolSetting(repo, "core.symlinks", stderr) orelse true; if (repo.config.get("core.checkstat") catch null) |e| if (e.value) |v| { o.minimal_stat = std.mem.eql(u8, v, "minimal"); }; return o;}
const Which = enum { index, files };
/// Runs `zig-git diff-index`.pub fn runIndex(ctx: Context, args: []const []const u8, stdout: *Io.Writer, stderr: *Io.Writer) !void { return run(ctx, .index, args, stdout, stderr);}
/// Runs `zig-git diff-files`.pub fn runFiles(ctx: Context, args: []const []const u8, stdout: *Io.Writer, stderr: *Io.Writer) !void { return run(ctx, .files, args, stdout, stderr);}
fn run(ctx: Context, which: Which, args: []const []const u8, stdout: *Io.Writer, stderr: *Io.Writer) !void { const gpa = ctx.gpa; const command: repo_diff.Command = if (which == .index) .diff_index else .diff_files; const usage = if (which == .index) @embedFile("diff_index_usage.txt") else @embedFile("diff_files_usage.txt"); var s: repo_diff.Settings = .{ .command = command }; defer s.deinit(gpa); var cached = false; var missing_as_index = false; var stage: ?IndexDiff.Stage = null; var words: std.ArrayList([]const u8) = .empty; defer words.deinit(gpa); var after: std.ArrayList([]const u8) = .empty; defer after.deinit(gpa); var dashdash = false;
var repo = try discovery.open(ctx, stderr); defer repo.close(ctx.io); try repo_diff.readConfig(&repo.config, &s, stderr);
var i: usize = 0; while (i < args.len) : (i += 1) { const arg = args[i]; if (dashdash) { try after.append(gpa, arg); continue; } if (std.mem.eql(u8, arg, "--")) { dashdash = true; continue; } if (arg.len < 2 or arg[0] != '-') { try words.append(gpa, arg); continue; } if (which == .index and std.mem.eql(u8, arg, "--cached")) { cached = true; } else if (which == .index and std.mem.eql(u8, arg, "-m")) { missing_as_index = true; } else if (which == .index and std.mem.eql(u8, arg, "--merge-base")) { return repo_diff.unsupported(stderr, command, arg); } else if (which == .files and std.mem.eql(u8, arg, "-q")) { // git was seen to list deleted files under -q all the same. } else if (which == .files and (std.mem.eql(u8, arg, "-0"))) { stage = .none; } else if (which == .files and (std.mem.eql(u8, arg, "-1") or std.mem.eql(u8, arg, "--base"))) { stage = .base; } else if (which == .files and (std.mem.eql(u8, arg, "-2") or std.mem.eql(u8, arg, "--ours"))) { stage = .ours; } else if (which == .files and (std.mem.eql(u8, arg, "-3") or std.mem.eql(u8, arg, "--theirs"))) { stage = .theirs; } else { const next: ?[]const u8 = if (i + 1 < args.len) args[i + 1] else null; switch (try repo_diff.parseOption(gpa, &s, arg, next, stderr)) { .yes => {}, .took_next => i += 1, .no => { stderr.writeAll(usage) catch {}; return error.UsageQuiet; }, } } } try repo_diff.check(&s, stderr); if (!s.format_given) s.out.format.raw = true; if (s.quiet) s.exit_code = true; if (fsmonitor(&repo)) return repo_diff.unsupported(stderr, command, "core.fsmonitor");
const settings = try object_names.settings(&repo.config, stderr); var names: object_names.Names = .init(ctx, &repo, settings); defer names.deinit(); var revs = try revisions.Revisions.init(ctx, &repo, settings, .rev_parse, stderr); defer revs.deinit(); revs.resolver.options.sets = false; try repo_diff.setUp(ctx, &repo, &s, &revs, stderr);
// diff-index takes one tree-ish, then paths. var tree_id: ?ObjectId = null; var paths: std.ArrayList([]const u8) = .empty; defer paths.deinit(gpa); var k: usize = 0; if (which == .index) { if (words.items.len == 0) { stderr.writeAll(usage) catch {}; return error.UsageQuiet; } var diag: Diagnostics = .{}; const found = revs.resolver.resolve(words.items[0], &diag) catch { revs.resolver.clearMessages(); try stderr.print( \\fatal: ambiguous argument '{s}': unknown revision or path not in the working tree. \\Use '--' to separate paths from revisions, like this: \\'git <command> [<revision>...] -- [<file>...]' \\ , .{words.items[0]}); return error.Fatal; }; revs.resolver.clearMessages(); tree_id = (revs.resolver.peelToTree(found.id) catch null) orelse return tool.fatalMessage(stderr, "bad tree object {s}", .{words.items[0]}); k = 1; } for (words.items[k..]) |w| { if (!dashdash and which == .index and !try pathspecs.takenForPath(ctx, &repo, w)) { return tool.fatalMessage(stderr, "{s}: no such path in the working tree.\nUse 'git <command> -- <path>...' to specify paths that do not exist locally.", .{w}); } try paths.append(gpa, w); } try paths.appendSlice(gpa, after.items); if (repo.location.work_tree == null and !(which == .index and cached)) return tool.fatalMessage(stderr, "this operation must be run in a work tree", .{});
if (s.relative != null and paths.items.len != 0) return repo_diff.unsupported(stderr, command, "--relative with paths"); var spec: git.Pathspec = if (paths.items.len == 0) .empty else try pathspecs.parse(ctx, &repo, paths.items, if (which == .index) "diff-index" else "diff-files", stderr); defer if (paths.items.len != 0) spec.deinit(gpa);
s.out.patch.object_format = repo.objectFormat(); repo_diff.finish(ctx, &s, null, &names, settings); var short: diff_tree.Short = .{ .ctx = ctx, .names = &names }; s.out.patch.shorten = .{ .context = &short, .length = diff_tree.Short.length };
var diag: Diagnostics = .{}; var index = repo.readIndex(ctx.io, .{ .index_file = ctx.env.get("GIT_INDEX_FILE") }, &diag) catch |err| return ls_files.describe(stderr, err, &diag); defer index.deinit(); for (index.warnings) |w| try ls_files.writeWarning(stderr, w); if (index.hasSparseDirectories()) return repo_diff.unsupported(stderr, command, "a sparse index");
var from: check_attr.From = .worktree; if (try check_attr.configuredTree(ctx, &repo, &revs, stderr)) |f| from = f; const attrs = try check_attr.setup(ctx, &repo, from, stderr); defer attrs.deinit(); var drivers: repo_diff.Drivers = .{ .gpa = gpa, .config = &repo.config, .setup = attrs, .stderr = stderr, .command = command }; if (s.out.format.check) drivers.check = s.out.whitespace; defer drivers.deinit(); // git reads the attributes files once for each set of attributes it // asks about, saying what is wrong with them each time: once for the // diff drivers, once for the conversions. const conversion_attrs = try check_attr.setup(ctx, &repo, from, stderr); defer conversion_attrs.deinit(); const whitespace_attrs = try check_attr.setup(ctx, &repo, from, stderr); defer whitespace_attrs.deinit(); drivers.conversions = conversion_attrs; drivers.whitespace = whitespace_attrs;
var options = try worktreeOptions(&repo, stderr); options.missing_as_index = missing_as_index; // git stops at the first change, before reading the files after it; // with a filter or a whitespace option what counts as one is decided // later, and everything is compared. options.first_only = s.quiet and !s.filter_given and !s.out.patch.lines.whitespace.any() and s.out.patch.hunks.ignore_line == null; { const index_path = try repo.indexPath(gpa, .{ .index_file = ctx.env.get("GIT_INDEX_FILE") }); defer gpa.free(index_path); if (repo.work_dir.statFile(ctx.io, index_path, .{})) |st| { options.index_mtime = @truncate(@as(u64, @intCast(@max(0, @divFloor(st.mtime.toNanoseconds(), std.time.ns_per_s))))); } else |_| {} } var wt: Worktree = .init(ctx, &repo, conversion_attrs, &index, stderr, command); defer wt.deinit(); var comparison: IndexDiff.Comparison = .{ .gpa = gpa, .io = ctx.io, .db = repo.objects(), .format = repo.objectFormat(), .index = &index, .pathspec = &spec, .worktree = wt.workTree(), .options = options, }; var pairs: IndexDiff.Pairs = .init(gpa); defer pairs.deinit(gpa); (switch (which) { .index => comparison.treeAgainst(tree_id, !cached, &pairs), .files => comparison.filesAgainstWorktree(stage, &pairs), }) catch |err| return failed(stderr, command, &wt, err);
// Filter, reverse and drop what -q hides. var list: std.ArrayList(output.Pair) = .empty; defer list.deinit(gpa); var unmerged = false; for (pairs.list.items) |p| { var q = p; if (q.status == .unmerged) unmerged = true; if (s.reverse and q.status != .unmerged) { std.mem.swap(output.Side, &q.old, &q.new); q.status = output.statusOf(q.old, q.new); } try list.append(gpa, q); } if (which == .files and stage == null and unmerged and s.out.format.patch and !s.out.format.name_only and !s.out.format.name_status) return repo_diff.unsupported(stderr, command, "a combined diff of unmerged paths");
var contents: IndexDiff.Contents = .{ .io = ctx.io, .db = repo.objects(), .worktree = wt.workTree(), .symlinks = options.symlinks }; var unchanged: diff_tree.Pairs = .init(gpa); defer unchanged.deinit(gpa); var warnings: repo_diff.RenameWarnings = .{}; if (s.harder and s.renames) { if (s.reverse) return repo_diff.unsupported(stderr, command, "--find-copies-harder with -R"); if (which == .files) { try collectUnchangedIndex(gpa, &index, &spec, list.items, &unchanged); } else { var tp: diff_tree.Pairs = .init(gpa); defer tp.deinit(gpa); try tp.list.appendSlice(gpa, list.items); try diff_tree.collectUnchanged(ctx, &repo, &spec, tree_id, &tp, &s, &unchanged, stderr); } } wt.startOutput(s.out, s.quiet); const prepared = repo_diff.prepare(gpa, &s, list.items, unchanged.list.items, contents.source(), drivers.drivers(), &warnings, stderr) catch |err| switch (err) { error.ContentsUnreadable => { if (wt.refused) return error.Fatal; return tool.fatalMessage(stderr, "zig-git {s}: cannot read the contents of a file", .{if (which == .index) "diff-index" else "diff-files"}); }, else => |e| return e, }; defer gpa.free(prepared); try warnings.flush(stderr); list.clearRetainingCapacity(); try list.appendSlice(gpa, prepared); var out_alloc: Io.Writer.Allocating = .init(gpa); defer out_alloc.deinit(); const base_w: *Io.Writer = if (s.output_path != null) &out_alloc.writer else stdout; var prefix_buffer: [256]u8 = undefined; var prefixed: line_prefix.LinePrefix = .init(base_w, s.line_prefix orelse "", &prefix_buffer); defer prefixed.writer.flush() catch {}; const w: *Io.Writer = if (s.line_prefix != null) &prefixed.writer else base_w; var discarding: Io.Writer.Discarding = .init(&.{}); const sink = if (s.quiet) &discarding.writer else w; const outcome = output.write(gpa, sink, list.items, contents.source(), drivers.drivers(), s.out) catch |err| switch (err) { error.DriverUnsupported => return error.Fatal, error.ContentsUnreadable => { if (wt.refused) return error.Fatal; return tool.fatalMessage(stderr, "zig-git {s}: cannot read the contents of a file", .{if (which == .index) "diff-index" else "diff-files"}); }, else => |e| return repo_diff.refuseComparison(stderr, command, e), }; try prefixed.writer.flush(); if (s.output_path) |p| { var base = Io.Dir.cwd().openDir(ctx.io, ctx.cwd, .{}) catch |err| return tool.fatalMessage(stderr, "cannot change to '{s}': {s}", .{ ctx.cwd, git.Config.reason(err) }); defer base.close(ctx.io); base.writeFile(ctx.io, .{ .sub_path = p, .data = out_alloc.written() }) catch |err| return tool.fatalMessage(stderr, "could not open '{s}' for writing: {s}", .{ p, git.Config.reason(err) }); } if (outcome.problems) return if (s.exit_code and outcome.differs) error.CheckProblemsDiffer else error.CheckProblems; if (s.exit_code and outcome.differs and !repo_diff.exitCodeSeesNothing(&s, false)) return error.No;}
fn fsmonitor(repo: *git.Repository) bool { const e = (repo.config.get("core.fsmonitor") catch null) orelse return false; const v = e.value orelse return true; if (git.Config.value.parseBool(v)) |b| return b; return v.len != 0;}
fn failed(stderr: *Io.Writer, command: repo_diff.Command, wt: *Worktree, err: anyerror) anyerror { return switch (err) { error.WorkTreeUnsupported => if (wt.refused) error.Fatal else repo_diff.unsupported(stderr, command, "reading the working tree on this system"), error.SubmoduleUnsupported => repo_diff.unsupported(stderr, command, "a checked-out submodule"), error.WorkTreeUnreadable => tool.fatalMessage(stderr, "zig-git: cannot read the working tree", .{}), error.OutOfMemory => err, else => tool.fatalMessage(stderr, "zig-git: cannot compare: {f}", .{tool.errorWords(err)}), };}
/// The index's entries under the pathspec that `pairs` does not list as/// changed, as unchanged pairs: the copy sources `--find-copies-harder`/// adds when the index is the old side.pub fn collectUnchangedIndex(gpa: Allocator, index: *const git.Index, spec: *const git.Pathspec, pairs: []const output.Pair, out: *diff_tree.Pairs) !void { var changed: std.StringHashMapUnmanaged(void) = .empty; defer changed.deinit(gpa); for (pairs) |p| try changed.put(gpa, p.path, {}); for (index.entries) |e| { if (e.stage() != 0) continue; if (!spec.matches(e.path, e.mode & 0o170000 == 0o160000)) continue; if (changed.contains(e.path)) continue; const side: output.Side = .{ .mode = e.mode, .id = e.id }; const path = try out.paths.allocator().dupe(u8, e.path); try out.list.append(gpa, .{ .status = .unmodified, .path = path, .old = side, .new = side }); }}