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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Describing commits on `zig-git`'s command lines, for `log`, `show` and//! `rev-list`: the formatting and searching options they share, read as//! git reads them; the configuration git consults (`format.pretty`,//! `pretty.<name>`, `log.date`, `log.abbrevCommit`, `log.mailmap`, the//! color settings, `grep.*`, `trailer.*`, `core.commentChar`, the mailmap//! sources); and each commit written with the separators each command//! puts between them.
const std = @import("std");const Allocator = std.mem.Allocator;const Io = std.Io;
const git = @import("git");const ObjectId = git.ObjectId;const Diagnostics = git.Diagnostics;const pretty = git.pretty;const tool = @import("zig_git.zig");const Context = tool.Context;const object_names = @import("object_names.zig");const revisions = @import("revisions.zig");const log_diff = @import("log_diff.zig");
/// Which command is describing commits; each frames them its own way.pub const Command = enum { rev_list, log, show };
/// The formatting and searching options, as given.pub const Options = struct { /// `--pretty[=<format>]` or `--format=<format>`, the last given. pretty: ?[]const u8 = null, /// `--abbrev-commit` or `--no-abbrev-commit`, or `--oneline`'s. abbrev_commit: ?bool = null, /// `--abbrev=<n>`; null for the default. abbrev: ?usize = null, /// `--no-abbrev`. no_abbrev: bool = false, /// `--date=<mode>`. date: ?[]const u8 = null, /// `--use-mailmap` or `--no-use-mailmap`. mailmap: ?bool = null, /// `--expand-tabs[=<n>]`, `--no-expand-tabs`. expand_tabs: ?usize = null, /// `--color[=<when>]`, `--no-color`. color: ?enum { always, never, auto } = null, /// `rev-list --header`. header: bool = false, /// `--encoding=<encoding>`. encoding: ?[]const u8 = null, /// `--grep`. messages: std.ArrayList([]const u8) = .empty, /// `--author`. authors: std.ArrayList([]const u8) = .empty, /// `--committer`. committers: std.ArrayList([]const u8) = .empty, all_match: bool = false, invert_grep: bool = false, ignore_case: bool = false, pattern_type: ?git.grep.PatternType = null, /// `--decorate[=<how>]` or `--no-decorate`. decorate: ?DecorateMode = null, /// `--decorate-refs`, since the last `--clear-decorations`. decorate_refs: std.ArrayList([]const u8) = .empty, /// `--decorate-refs-exclude`, since the last `--clear-decorations`. decorate_refs_exclude: std.ArrayList([]const u8) = .empty, /// `--clear-decorations`. clear_decorations: bool = false, /// `--source`. source: bool = false, /// `-z`: each commit ended, or the commits separated, by a NUL /// rather than a line feed. nul: bool = false,
pub fn deinit(o: *Options, gpa: Allocator) void { o.messages.deinit(gpa); o.authors.deinit(gpa); o.committers.deinit(gpa); o.decorate_refs.deinit(gpa); o.decorate_refs_exclude.deinit(gpa); }
/// Whether a format was asked for at all. pub fn formatted(o: *const Options) bool { return o.pretty != null or o.header; }};
/// How decorations are shown: `--decorate=<how>` or `log.decorate`.pub const DecorateMode = enum { no, short, full, auto };
fn decorateMode(text: []const u8) ?DecorateMode { if (std.meta.stringToEnum(DecorateMode, text)) |m| return m; return null;}
/// Whether `arg` is one of the options of `log` and `show` that/// `rev-list` does not take.pub fn isLogOnly(arg: []const u8) bool { const eq = std.mem.findScalar(u8, arg, '='); const name = arg[0 .. eq orelse arg.len]; const names = [_][]const u8{ "--decorate", "--no-decorate", "--decorate-refs", "--decorate-refs-exclude", "--clear-decorations", "--source" }; for (names) |n| if (std.mem.eql(u8, n, name)) return true; return false;}
/// What `parseOption` made of an argument.pub const Parsed = enum { /// Not one of these options. no, /// One of them. yes, /// One of them, which took the next argument as its value. took_next,};
/// Reads `arg`, and `next` if it takes it, as a formatting or searching/// option.pub fn parseOption(o: *Options, gpa: Allocator, arg: []const u8, next: ?[]const u8, stderr: *Io.Writer) !Parsed { 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; const Name = enum { @"--pretty", @"--format", @"--oneline", @"--abbrev-commit", @"--no-abbrev-commit", @"--abbrev", @"--no-abbrev", @"--date", @"--relative-date", @"--use-mailmap", @"--mailmap", @"--no-use-mailmap", @"--no-mailmap", @"--expand-tabs", @"--no-expand-tabs", @"--color", @"--no-color", @"--encoding", @"--header", @"--grep", @"--author", @"--committer", @"--all-match", @"--no-all-match", @"--invert-grep", @"--no-invert-grep", @"-i", @"--regexp-ignore-case", @"-E", @"--extended-regexp", @"-F", @"--fixed-strings", @"--basic-regexp", @"-P", @"--perl-regexp", @"--decorate", @"--no-decorate", @"--decorate-refs", @"--decorate-refs-exclude", @"--clear-decorations", @"--source", @"-z", }; const n = std.meta.stringToEnum(Name, name) orelse return .no; switch (n) { .@"--pretty" => o.pretty = value orelse "medium", .@"--format" => o.pretty = value orelse return .no, .@"--oneline" => { o.pretty = "oneline"; o.abbrev_commit = true; }, .@"--abbrev-commit" => o.abbrev_commit = true, .@"--no-abbrev-commit" => o.abbrev_commit = false, .@"--abbrev" => { o.no_abbrev = false; o.abbrev = if (value) |v| abbrevLength(v) else null; }, .@"--no-abbrev" => o.no_abbrev = true, .@"--date" => o.date = value orelse return .no, .@"--relative-date" => o.date = "relative", .@"--use-mailmap", .@"--mailmap" => o.mailmap = true, .@"--no-use-mailmap", .@"--no-mailmap" => o.mailmap = false, .@"--expand-tabs" => o.expand_tabs = if (value) |v| @intCast(@max(0, object_names.optionNumber(v) orelse return tool.fatalMessage(stderr, "'{s}': not an integer", .{v}))) else 8, .@"--no-expand-tabs" => o.expand_tabs = 0, .@"--color" => { const v = value orelse "always"; o.color = if (std.mem.eql(u8, v, "always")) .always else if (std.mem.eql(u8, v, "never")) .never else if (std.mem.eql(u8, v, "auto")) .auto else { try stderr.print("error: option `color' expects \"always\", \"auto\", or \"never\"\n", .{}); return error.UsageQuiet; }; }, .@"--no-color" => o.color = .never, .@"--encoding" => { const v = value orelse return .no; if (!git.encoding.isWritable(v)) return tool.fatalMessage(stderr, "zig-git: '--encoding={s}' is not supported", .{v}); o.encoding = v; }, .@"--header" => o.header = true, .@"--grep", .@"--author", .@"--committer" => { const v = value orelse next orelse return .no; const list = switch (n) { .@"--grep" => &o.messages, .@"--author" => &o.authors, else => &o.committers, }; try list.append(gpa, v); if (value == null) return .took_next; }, .@"--all-match" => o.all_match = true, .@"--no-all-match" => o.all_match = false, .@"--invert-grep" => o.invert_grep = true, .@"--no-invert-grep" => o.invert_grep = false, .@"-i", .@"--regexp-ignore-case" => o.ignore_case = true, .@"-E", .@"--extended-regexp" => o.pattern_type = .extended, .@"-F", .@"--fixed-strings" => o.pattern_type = .fixed, .@"--basic-regexp" => o.pattern_type = .basic, .@"-P", .@"--perl-regexp" => return tool.fatalMessage(stderr, "zig-git: '{s}' is not supported: Perl's regular expressions are not", .{arg}), .@"--decorate" => o.decorate = if (value) |v| decorateMode(v) orelse return tool.fatalMessage(stderr, "invalid --decorate option: {s}", .{v}) else .short, .@"--no-decorate" => o.decorate = .no, .@"--decorate-refs", .@"--decorate-refs-exclude" => { const v = value orelse next orelse return .no; try (if (n == .@"--decorate-refs") &o.decorate_refs else &o.decorate_refs_exclude).append(gpa, v); if (value == null) return .took_next; }, .@"--clear-decorations" => { o.clear_decorations = true; o.decorate_refs.clearRetainingCapacity(); o.decorate_refs_exclude.clearRetainingCapacity(); }, .@"--source" => o.source = true, .@"-z" => o.nul = true, } if (value != null) switch (n) { .@"--pretty", .@"--format", .@"--abbrev", .@"--date", .@"--expand-tabs", .@"--color", .@"--encoding", .@"--grep", .@"--author", .@"--committer", .@"--decorate", .@"--decorate-refs", .@"--decorate-refs-exclude" => {}, else => return .no, }; return .yes;}
fn abbrevLength(text: []const u8) ?usize { const n = object_names.optionNumber(text) orelse return null; if (n <= 0) return null; return @intCast(@max(4, @min(n, git.object_max_hex_length)));}
/// The options that need what this does not have, refused by name.pub const unsupported = [_][]const u8{ "-L", "--graph", "--notes", "--show-notes", "--standard-notes", "--show-signature", "--log-size", "--walk-reflogs", "-g", "--grep-reflog", "--show-linear-break", "--word-diff", "--color-words", "--binary",};
/// Whether `arg` is one of the options refused by name.pub fn isUnsupported(arg: []const u8) bool { const eq = std.mem.findScalar(u8, arg, '='); const name = arg[0 .. eq orelse arg.len]; for (unsupported) |u| if (std.mem.eql(u8, u, name)) return true; // `-L<range>:<file>`. if (arg.len > 2 and arg[0] == '-' and arg[1] == 'L') return true; return false;}
/// Commits described: the format, its settings and everything they read.pub const Describer = struct { ctx: Context, repo: *git.Repository, command: Command, format: ?pretty.Format, settings: pretty.Settings, names: *object_names.Names, mailmap: *git.Mailmap, grep: ?git.grep.Grep = null, configured: []git.trailers.Configured = &.{}, comment: []u8 = &.{}, separators: ?[]const u8 = null, /// Text the format string's options borrow. arena: std.heap.ArenaAllocator, /// Whether a commit has been written, for the separators. written: bool = false, /// `-z`: the line feed ending a commit, or between two, is a NUL. nul: bool = false, /// What `keeps` could not re-encode. encoding_failure: Diagnostics = .{}, /// `--no-commit-header`. commit_header: bool = true, /// `--timestamp`. timestamp: bool = false, header: bool = false, /// The decorations, when anything shows them. decorations: ?git.Decorations = null, /// The names each `%(describe...)` describes with, found when first /// needed. describe_names: []?git.describe.Names = &.{}, /// The commits read for describing, when first needed. describe_history: ?*@import("history.zig").History = null,
/// Sets up describing for `command` from `options` and `repo`'s /// configuration. pub fn init(ctx: Context, repo: *git.Repository, names: *object_names.Names, revs: *revisions.Revisions, options: *const Options, command: Command, stderr: *Io.Writer) !Describer { const gpa = ctx.gpa; var arena: std.heap.ArenaAllocator = .init(gpa); errdefer arena.deinit(); const a = arena.allocator(); const config = &repo.config;
// The output encoding, as git-config(1) says: the option, // `i18n.logOutputEncoding`, or `i18n.commitEncoding`. const output_encoding: []const u8 = if (options.encoding) |e| e else if (config.getString("i18n.logOutputEncoding") catch null) |e| blk: { if (!git.encoding.isWritable(e)) return tool.fatalMessage(stderr, "zig-git: i18n.logOutputEncoding '{s}' is not supported", .{e}); break :blk try a.dupe(u8, e); } else if (config.getString("i18n.commitEncoding") catch null) |e| blk: { if (!git.encoding.isWritable(e)) return tool.fatalMessage(stderr, "zig-git: i18n.commitEncoding '{s}' is not supported", .{e}); break :blk try a.dupe(u8, e); } else "UTF-8"; if (command != .rev_list) { if ((repo.boolSetting("log.showSignature", null) catch null) orelse false) return tool.fatalMessage(stderr, "zig-git: log.showSignature is not supported", .{}); }
// The format: the option, `format.pretty` for log and show, or // the command's default. const default_text: ?[]const u8 = if (command == .rev_list) null else ((config.getString("format.pretty") catch null) orelse "medium"); var format: ?pretty.Format = null; const header = command == .rev_list and options.header and options.pretty == null; if (header) { format = .{ .builtin = .raw }; } else if (options.pretty orelse default_text) |text| { const Lookup = struct { fn get(context: *const anyopaque, name: []const u8) ?[]const u8 { const c: *const git.Config = @ptrCast(@alignCast(context)); var buffer: [256]u8 = undefined; const key = std.fmt.bufPrint(&buffer, "pretty.{s}", .{name}) catch return null; return c.getString(key) catch null; } }; const resolved = pretty.resolve(text, .{ .context = config, .get = Lookup.get }); var diag: Diagnostics = .{}; format = pretty.parse(gpa, resolved, &diag) catch |err| switch (err) { error.UnknownFormat => return tool.fatalMessage(stderr, "invalid --pretty format: {s}", .{text}), error.UnsupportedPlaceholder => return tool.fatalMessage(stderr, "zig-git: the placeholder '{s}' is not supported", .{diag.detail()}), else => |e| return e, }; } errdefer if (format) |*f| f.deinit();
// Dates. var date_mode: git.date.Mode = .{}; var date_given = false; const date_text: ?[]const u8 = options.date orelse if (command != .rev_list) (config.getString("log.date") catch null) else null; if (date_text) |t| { var diag: Diagnostics = .{}; date_mode = git.date.parseMode(t, .{ .terminal = false }, &diag) catch return tool.fatalMessage(stderr, "{s}", .{diag.detail()}); date_given = options.date != null or command != .rev_list; }
// Abbreviation. const abbrev_commit = options.abbrev_commit orelse if (command != .rev_list) ((repo.boolSetting("log.abbrevCommit", null) catch null) orelse false) else false;
// The mailmap: `%aN` and the rest always, the built-in formats' // identities and the searches only when asked. const use_mailmap = options.mailmap orelse if (command != .rev_list) ((repo.boolSetting("log.mailmap", null) catch null) orelse true) else false; const mailmap = try gpa.create(git.Mailmap); errdefer gpa.destroy(mailmap); mailmap.* = .init(gpa); errdefer mailmap.deinit(); var warnings: std.ArrayList(git.Mailmap.Warning) = .empty; defer warnings.deinit(gpa); try mailmap.addRepository(ctx.io, repo, &revs.resolver, .{ .home = ctx.env.get("HOME") }, &warnings); for (warnings.items) |w| switch (w) { .symbolic_link => try stderr.writeAll("error: unable to open mailmap at .mailmap: Too many levels of symbolic links\n"), };
// Color: the option, then `color.diff`, then `color.ui`; output is // never a terminal here. const color = color: { if (options.color) |c| break :color c == .always; for ([_][]const u8{ "color.diff", "color.ui" }) |key| { if (config.getString(key) catch null) |v| { if (std.mem.eql(u8, v, "always")) break :color true; break :color false; } } break :color false; }; var commit_color: []const u8 = "\x1b[33m"; if (color) if (config.getString("color.diff.commit") catch null) |spec| { var buffer: [pretty.color_max]u8 = undefined; const escape = pretty.parseColor(spec, &buffer) orelse return tool.fatalMessage(stderr, "invalid color value: {s}", .{spec}); commit_color = try a.dupe(u8, escape); };
// Trailers. var configured: std.ArrayList(git.trailers.Configured) = .empty; for (config.entries.items) |e| { const parts = e.parts(); if (!std.ascii.eqlIgnoreCase(parts.section, "trailer")) continue; const sub = parts.subsection orelse continue; const slot = for (configured.items) |*c| { if (std.mem.eql(u8, c.name, sub)) break c; } else slot: { try configured.append(a, .{ .name = try a.dupe(u8, sub) }); break :slot &configured.items[configured.items.len - 1]; }; if (std.ascii.eqlIgnoreCase(parts.name, "key")) if (e.value) |v| { slot.key = try a.dupe(u8, v); }; } const separators: []const u8 = if (config.getString("trailer.separators") catch null) |s| try a.dupe(u8, s) else ":"; const comment: []const u8 = comment: { if (config.getString("core.commentString") catch null) |s| break :comment try a.dupe(u8, s); if (config.getString("core.commentChar") catch null) |s| { if (std.mem.eql(u8, s, "auto")) break :comment "#"; break :comment try a.dupe(u8, s); } break :comment "#"; };
// Searches. var grep: ?git.grep.Grep = null; if (options.messages.items.len != 0 or options.authors.items.len != 0 or options.committers.items.len != 0) { var pattern_type: git.grep.PatternType = .basic; if (options.pattern_type) |t| { pattern_type = t; } else if (config.getString("grep.patternType") catch null) |t| { const Type = enum { basic, extended, fixed, perl, default }; const kind = std.meta.stringToEnum(Type, t) orelse return tool.fatalMessage(stderr, "bad grep.patterntype argument: {s}", .{t}); pattern_type = switch (kind) { .basic => .basic, .extended => .extended, .fixed => .fixed, .perl => return tool.fatalMessage(stderr, "zig-git: grep.patternType 'perl' is not supported: Perl's regular expressions are not", .{}), .default => if ((repo.boolSetting("grep.extendedRegexp", null) catch null) orelse false) .extended else .basic, }; } else if ((repo.boolSetting("grep.extendedRegexp", null) catch null) orelse false) { pattern_type = .extended; } var failure: git.grep.Failure = .{}; grep = git.grep.Grep.init(gpa, .{ .pattern_type = pattern_type, .ignore_case = options.ignore_case, .all_match = options.all_match, .invert = options.invert_grep, .messages = options.messages.items, .authors = options.authors.items, .committers = options.committers.items, }, &failure) catch |err| switch (err) { error.InvalidPattern => return tool.fatalMessage(stderr, "{s}, '{s}': {s}", .{ if (failure.header) "header" else "command line", failure.pattern, failure.regex.problem.message() }), error.Unsupported => return tool.fatalMessage(stderr, "zig-git: a search pattern using {s} is not supported", .{failure.regex.unsupported}), error.UnsupportedPatternType => return tool.fatalMessage(stderr, "zig-git: Perl's regular expressions are not supported", .{}), else => |e| return e, }; }
// Decorations: `--decorate`, or `log.decorate`, which `auto` and // output that is never a terminal leave off. var mode: DecorateMode = options.decorate orelse .auto; if (options.decorate == null and command != .rev_list) if (config.getString("log.decorate") catch null) |v| { mode = decorateMode(v) orelse if (git.Config.value.parseBool(v)) |b| (if (b) DecorateMode.short else .no) else .auto; }; const show_decorations = mode == .short or mode == .full; const wants_decorations = show_decorations or (if (format) |f| f == .user and f.user.usesDecorations() else false); var decoration_colors: pretty.DecorationColors = .{}; if (command != .rev_list or wants_decorations) try readDecorationColors(config, a, &decoration_colors, stderr); var decorations: ?git.Decorations = null; errdefer if (decorations) |*x| x.deinit(); if (wants_decorations) decorations = try loadDecorations(ctx, repo, options, command, mode == .full, stderr);
const columns: usize = if (ctx.env.get("COLUMNS")) |c| (std.fmt.parseInt(usize, c, 10) catch 80) else 80; return .{ .ctx = ctx, .repo = repo, .command = command, .format = format, .names = names, .mailmap = mailmap, .grep = grep, .configured = configured.items, .arena = arena, .header = header, .nul = options.nul, .settings = .{ .output_encoding = output_encoding, .abbreviator = .{ .context = names, .shorten = shorten, .length = if (options.no_abbrev) git.object_max_hex_length else options.abbrev orelse names.defaultLength() }, .abbrev_commit = abbrev_commit and !options.no_abbrev, .full_merge_ids = options.no_abbrev, .date_mode = date_mode, .date_given = date_given, .now = revisions.now(ctx), .zone = revs.zone, .mailmap = mailmap, .use_mailmap = use_mailmap, .color = color, .commit_color = commit_color, // rev-list expands no tabs, whatever it is asked. .expand_tabs = if (command == .rev_list) 0 else options.expand_tabs, .trailers = .{ .separators = separators, .configured = configured.items, .comment = comment }, .columns = columns, .rev_list = command == .rev_list, .first_line = command != .rev_list, .decorate = show_decorations, .show_source = options.source, .decoration_colors = decoration_colors, }, .decorations = decorations, }; }
pub fn deinit(d: *Describer) void { for (d.describe_names) |*n| if (n.*) |*x| x.deinit(); d.ctx.gpa.free(d.describe_names); if (d.describe_history) |h| { h.close(); d.ctx.gpa.destroy(h); } if (d.decorations) |*x| x.deinit(); if (d.format) |*f| f.deinit(); if (d.grep) |*g| g.deinit(); d.mailmap.deinit(); d.ctx.gpa.destroy(d.mailmap); d.arena.deinit(); }
fn shorten(context: *anyopaque, w: *Io.Writer, id: ObjectId, length: usize) pretty.ShortenError!void { const names: *object_names.Names = @ptrCast(@alignCast(context)); names.write(w, id, length) catch |err| switch (err) { error.WriteFailed => return error.WriteFailed, else => return error.ShortenFailed, }; }
/// What `%(describe...)` with `options`, the format's `i`th, makes of /// `id`: what `git describe` would print, or null. fn describeOne(d: *Describer, a: Allocator, options: pretty.DescribeOptions, i: usize, id: ObjectId, stderr: *Io.Writer) !?[]const u8 { const gpa = d.ctx.gpa; const history = @import("history.zig"); if (d.describe_names.len == 0) { d.describe_names = try gpa.alloc(?git.describe.Names, d.format.?.user.describes.len); @memset(d.describe_names, null); } if (d.describe_history == null) { const h = try gpa.create(history.History); errdefer gpa.destroy(h); h.* = try history.History.open(d.ctx, d.repo, stderr); d.describe_history = h; } const h = d.describe_history.?; if (d.describe_names[i] == null) { var diag: Diagnostics = .{}; d.describe_names[i] = git.describe.loadNames(gpa, d.ctx.io, d.repo.refs(), d.repo.objects(), .{ .tags = options.tags, .match = options.match, .exclude = options.exclude, }, &diag) catch |err| return tool.fatalMessage(stderr, "zig-git: cannot read the refs: {f}", .{tool.errorWords(err)}); } const names = &d.describe_names[i].?; const c = try h.store.name(id); var diag: Diagnostics = .{}; const outcome = git.describe.search(gpa, names, &h.store, c, .{}, &diag) catch |err| return h.fail(err, &diag, null, stderr); switch (outcome) { .none => return null, .mixed => return tool.fatalMessage(stderr, "zig-git: %(describe) choosing among annotated tags, lightweight tags and other refs is not supported", .{}), .found => |r| { const length = options.abbrev orelse d.names.defaultLength(); var text: Io.Writer.Allocating = .init(a); try text.writer.writeAll(r.name.shown); if (r.depth != 0 and length != 0) { try text.writer.print("-{d}-g", .{r.depth}); try d.names.write(&text.writer, id, length); } return text.written(); }, } }
/// Whether the searches keep a commit, by its contents. pub fn keeps(d: *Describer, contents: []const u8) !bool { const g = &(d.grep orelse return true); // git searches the commit as it would write it, in the output // encoding. const original = git.Commit.parse(d.repo.objectFormat(), contents) catch return false; const reencoded = pretty.reencode(d.ctx.gpa, contents, original.encoding(), d.settings.output_encoding, &d.encoding_failure) catch |err| switch (err) { error.UnsupportedEncoding => return error.Fatal, else => |e| return e, }; defer if (reencoded) |x| d.ctx.gpa.free(x); const commit = if (reencoded) |x| original.reencoded(x) else original; return g.matchesCommit(d.ctx.gpa, commit, if (d.settings.use_mailmap) d.mailmap else null); }
/// Says what `keeps` failed to re-encode, when it failed with `Fatal`. pub fn reportEncodingFailure(d: *const Describer, stderr: *Io.Writer) error{Fatal} { return tool.fatalMessage(stderr, "zig-git: {s} is not supported", .{d.encoding_failure.detail()}); }
/// What one commit is shown as, besides its contents. pub const Entry = struct { id: ObjectId, parents: []const ObjectId, children: []const ObjectId = &.{}, mark: pretty.Mark = .none, /// Whether the mark is written before the commit ID. show_mark: bool = false, /// `--timestamp`'s. time: ?i64 = null, /// Whether `git rev-list` writes the parents after the commit ID: /// `--parents`. header_parents: bool = false, /// What it was reached from, for `--source` and `%S`. source: ?[]const u8 = null, /// The parent a merge is shown against under `-m`. from: ?ObjectId = null, };
/// How commits are written, as the diffs after them need to know. pub fn framing(d: *const Describer) log_diff.Framing { const f = d.format orelse return .full; return switch (f) { .builtin => |b| if (b == .oneline) .oneline else .full, .user => |u| if (u.empty) .empty else .full, }; }
/// Writes one commit, as `command` frames it. pub fn writeCommit(d: *Describer, stdout: *Io.Writer, entry: Entry, stderr: *Io.Writer) !void { const gpa = d.ctx.gpa; var diag: Diagnostics = .{}; var obj = d.repo.objects().read(d.ctx.io, gpa, entry.id, &diag) catch |err| switch (err) { error.ObjectNotFound => return tool.fatalMessage(stderr, "cannot read commit object {f}", .{entry.id}), error.ReplaceDepthTooHigh => return tool.fatalMessage(stderr, "replace depth too high for object {f}", .{entry.id}), error.ReplacementNotFound => return tool.fatalMessage(stderr, "replacement {s} not found for {f}", .{ diag.detail(), entry.id }), else => return tool.fatalMessage(stderr, "zig-git: cannot read commit {f}: {f}", .{ entry.id, tool.errorWords(err) }), }; defer obj.deinit(gpa); const f = &(d.format orelse unreachable); var settings = d.settings; settings.show_mark = entry.show_mark; var text: Io.Writer.Allocating = .init(gpa); defer text.deinit(); var descriptions: std.ArrayList(?[]const u8) = .empty; defer descriptions.deinit(gpa); var description_text: std.heap.ArenaAllocator = .init(gpa); defer description_text.deinit(); if (f.* == .user) for (f.user.describes, 0..) |options, i| { try descriptions.append(gpa, try d.describeOne(description_text.allocator(), options, i, entry.id, stderr)); }; const shown: pretty.Shown = .{ .descriptions = descriptions.items, .id = entry.id, .contents = obj.data, .parents = entry.parents, .children = entry.children, .mark = entry.mark, .decorations = if (d.decorations) |*x| x.of(entry.id) else &.{}, .source = entry.source, .from = entry.from, }; const user_like = switch (f.*) { .user => true, .builtin => |x| x == .reference, }; const oneline = f.* == .builtin and f.builtin == .oneline; // What comes before the commit is written first, as git writes it // before anything that can fail. // Under -z each of the line feeds that end a commit or separate // two is a NUL, and nothing else is. const end: u8 = if (d.nul) 0 else '\n'; switch (d.command) { .log, .show => { if (user_like) { if (!f.terminated() and d.written) try stdout.writeByte(end); } else if (d.written and !oneline) try stdout.writeByte(end); }, .rev_list => { if (entry.time) |t| try stdout.print("{d} ", .{t}); if (!user_like or d.commit_header) { if (!oneline and !d.header) try stdout.writeAll("commit "); if (entry.show_mark) try stdout.writeByte(switch (entry.mark) { .left => '<', .right => '>', .boundary => '-', .none => ' ', }); try d.names.write(stdout, entry.id, if (d.settings.abbrev_commit) d.settings.abbreviator.length else null); if (entry.header_parents) for (entry.parents) |p| try stdout.print(" {f}", .{p}); for (entry.children) |c| try stdout.print(" {f}", .{c}); try stdout.writeByte(if (oneline) ' ' else '\n'); } }, } d.written = true; pretty.write(gpa, &text.writer, f, shown, settings, &diag) catch |err| { if (err != error.OutOfMemory and err != error.WriteFailed) pretty.writeFirstLine(gpa, stdout, f, shown, settings) catch {}; return describeFailure(err, &diag, stderr); }; const out = text.written(); switch (d.command) { .log, .show => { // --oneline's line ends in the line feed ending the // commit. if (oneline and d.nul and std.mem.endsWith(u8, out, "\n")) { try stdout.writeAll(out[0 .. out.len - 1]); try stdout.writeByte(0); } else try stdout.writeAll(out); // An empty format string writes nothing, not even the // line feed ending the commit. const empty = f.* == .user and f.user.empty; if (user_like and f.terminated() and !empty) try stdout.writeByte(end); }, .rev_list => { if (d.header) { try stdout.writeAll(out); try stdout.writeByte(0); } else if (user_like) { if (out.len != 0) { try stdout.writeAll(out); try stdout.writeByte('\n'); } } else { try stdout.writeAll(out); if (!oneline) try stdout.writeByte('\n'); } }, } d.written = true; }};
/// The decorations the options and settings choose, `command` reading/// `log.excludeDecoration` and `log.initialDecorationSet` unless it is/// `rev-list`, which decorates with every ref.pub fn loadDecorations(ctx: Context, repo: *git.Repository, options: *const Options, command: Command, full: bool, stderr: *Io.Writer) !git.Decorations { const gpa = ctx.gpa; var arena: std.heap.ArenaAllocator = .init(gpa); defer arena.deinit(); const a = arena.allocator(); const config = &repo.config; // rev-list decorates with every ref. const all = command == .rev_list or options.clear_decorations or if (config.getString("log.initialDecorationSet") catch null) |v| std.mem.eql(u8, v, "all") else false; var config_exclude: std.ArrayList([]const u8) = .empty; if (command != .rev_list) if (config.all("log.excludeDecoration") catch null) |found| { var excludes = found; while (excludes.next()) |e| if (e.value) |v| try config_exclude.append(a, v); }; const grafts = try @import("history.zig").loadGrafts(ctx, repo, stderr); var diag: Diagnostics = .{}; return git.Decorations.load(gpa, ctx.io, repo.refs(), repo.objects(), .{ .full = full, .filter = .{ .include = options.decorate_refs.items, .exclude = options.decorate_refs_exclude.items, .config_exclude = config_exclude.items, .all = all, }, .replacements = if (repo.replacements.isEmpty()) null else &repo.replacements, .replace_base = ctx.env.get("GIT_REPLACE_REF_BASE") orelse git.Replacements.default_base, .grafts = grafts, }, &diag) catch |err| tool.fatalMessage(stderr, "zig-git: cannot read the refs for decorations: {f}", .{tool.errorWords(err)});}
/// Reads `color.decorate.<slot>`, stopping as git stops at a color it/// cannot read, whether or not color is on.fn readDecorationColors(config: *const git.Config, a: Allocator, colors: *pretty.DecorationColors, stderr: *Io.Writer) !void { for (config.entries.items) |e| { const parts = e.parts(); if (!std.ascii.eqlIgnoreCase(parts.section, "color") or parts.subsection == null) continue; if (!std.mem.eql(u8, parts.subsection.?, "decorate")) continue; const Slot = enum { branch, remotebranch, tag, stash, head, grafted }; var lower: [32]u8 = undefined; if (parts.name.len > lower.len) continue; const slot = std.meta.stringToEnum(Slot, std.ascii.lowerString(&lower, parts.name)) orelse continue; var buffer: [pretty.color_max]u8 = undefined; const escape = pretty.parseColor(e.value orelse "", &buffer) orelse { try stderr.print("error: invalid color value: {s}\n", .{e.value orelse ""}); if (e.origin.kind == .command_line) return tool.fatalMessage(stderr, "unable to parse '{s}' from command-line config", .{e.key}); return tool.fatalMessage(stderr, "bad config variable '{s}' in file '{s}' at line {d}", .{ e.key, e.origin.name, e.line }); }; const copy = try a.dupe(u8, escape); switch (slot) { .branch => colors.branch = copy, .remotebranch => colors.remote_branch = copy, .tag => colors.tag = copy, .stash => colors.stash = copy, .head => colors.head = copy, .grafted => colors.grafted = copy, } }}
/// Says what git says of a commit it could not describe, or refuses what/// is not supported.pub fn describeFailure(err: anyerror, diag: *const Diagnostics, stderr: *Io.Writer) error{Fatal} { switch (err) { error.InvalidColor => { stderr.print("error: {s}\n", .{diag.detail()}) catch {}; return tool.fatalMessage(stderr, "unable to parse --pretty format", .{}); }, error.TimestampBeforeEpoch => return tool.fatalMessage(stderr, "{s}", .{diag.detail()}), error.UnsupportedEncoding, error.UnsupportedConversion => return tool.fatalMessage(stderr, "zig-git: {s} is not supported", .{diag.detail()}), error.DateOutOfRange => return tool.fatalMessage(stderr, "zig-git: a date out of the range supported", .{}), else => return tool.fatalMessage(stderr, "zig-git: cannot describe a commit: {f}", .{tool.errorWords(err)}), }}