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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Describing commits, compared with git: `log`, `rev-list` and `show` in//! every built-in format and in format strings of every placeholder, with//! the date modes, colors, padding, wrapping, trailers and the mailmap;//! searching by message and identity; and `check-mailmap`. The histories//! are written object by object so that their commits can be as awkward as//! git allows: messages without a body, with trailing blank space, CRLF,//! long subjects, characters past ASCII and trailers in every shape;//! identities with odd spacing or none at all; time zones on both sides of//! UTC; and dates at the edges. Every test runs under SHA-1 and SHA-256.
const std = @import("std");const Io = std.Io;
const tool = @import("zig_git.zig");const Repo = tool.Repo;
const testing = std.testing;
fn same(repo: *Repo, args: []const []const u8) !void { try tool.expectSameAsGitHow(repo, args, .{});}
/// Usage errors, whose text is git's own.fn sameOutput(repo: *Repo, args: []const []const u8) !void { try tool.expectSameAsGitHow(repo, args, .{ .compare = .output });}
/// `zig-git <args>` refuses by name what git would answer.fn refused(repo: *Repo, args: []const []const u8) !void { const ours = try tool.runOurs(repo, args, .{}); defer ours.deinit(repo.gpa); errdefer { std.debug.print("zig-git", .{}); for (args) |a| std.debug.print(" {s}", .{a}); std.debug.print("\n{s}", .{ours.stderr}); } try testing.expectEqual(128, ours.code); try testing.expect(std.mem.find(u8, ours.stderr, "not supported") != null);}
fn writeFile(repo: *Repo, path: []const u8, contents: []const u8) !void { if (std.fs.path.dirname(path)) |d| try repo.tmp.dir.createDirPath(repo.io, d); try repo.tmp.dir.writeFile(repo.io, .{ .sub_path = path, .data = contents });}
const subjects = [_][]const u8{ "Fix the thing", " leading spaces", "trailing space ", "tab\there", "caf\xc3\xa9 au lait", "\xe4\xb8\xad\xe6\x96\x87 subject", "a very long subject line that goes on and on and on and on and on and on and on and on forever and more", "", "x", "Subj: with [weird] chars... & stuff!!", "From the top", "CRLF subject\r", "multi\nline subject", "e\xcc\x81 combining", "a\xe6\x97\xa5b wide between",};
const bodies = [_][]const u8{ "", "Body line one.\n", "Body\twith tab\nand a second line \n", "para one\n\npara two\n", "line\r\nwith crlf\r\n", "Signed-off-by: A U Thor <a@example.com>\n", "Some text.\n\nReviewed-by: X <x@y>\nAcked-by: Y\n folded\nnot a trailer\n", "Text\n\nCloses: #1\nSee-also: http://example.com\n", ">From quoted\nFrom me\n", " \n\n\n", "Text\n\nKey: value\n# comment\nOther: v2\n", "Text\n\n(cherry picked from commit 0123456789abcdef0123456789abcdef01234567)\nn1\nn2\n", "\xc3\xbcber body \xe4\xb8\xad\n", "x\ta\x1bb\tc\n\xe9\tnot UTF-8\n",};
const names = [_][]const u8{ "A U Thor", "Jos\xc3\xa9", " Spaced Name", "Doe, John", "x", "", "Name\twith tab" };const emails = [_][]const u8{ "a@example.com", "Mixed@Case.ORG", "no-at", "", "local.part@x" };const zones = [_][]const u8{ "+0000", "-0700", "+0530", "-0230", "+1400", "-1200", "+0100" };const times = [_]u64{ 86400, 1000000000, 1700000000, 1799999999, 2147483647, 4102444800 };
fn identity(random: std.Random, w: *Io.Writer) !void { const r = random.uintLessThan(u8, 100); if (r < 3) return w.print("{s} <{s}>", .{ names[random.uintLessThan(usize, names.len)], emails[random.uintLessThan(usize, emails.len)] }); if (r < 5) return w.writeAll("Bad line"); try w.print("{s} <{s}> {d} {s}", .{ names[random.uintLessThan(usize, names.len)], emails[random.uintLessThan(usize, emails.len)], times[random.uintLessThan(usize, times.len)], zones[random.uintLessThan(usize, zones.len)], });}
/// A history of `n` commits written object by object, each with an/// awkward message and awkward identities, some of them merges; the last/// is `master`. Times are such that no date is before the epoch in its/// own zone, which git refuses to show.fn makeHistory(repo: *Repo, seed: u64, n: usize) ![][]u8 { const gpa = repo.gpa; var prng: std.Random.DefaultPrng = .init(seed); const random = prng.random(); const tree_out = try repo.output(&.{ "mktree", "--missing" }); defer gpa.free(tree_out); const tree = std.mem.trimEnd(u8, tree_out, "\n"); var ids: std.ArrayList([]u8) = .empty; errdefer { for (ids.items) |id| gpa.free(id); ids.deinit(gpa); } for (0..n) |k| { var c: Io.Writer.Allocating = .init(gpa); defer c.deinit(); const w = &c.writer; try w.print("tree {s}\n", .{tree}); if (k > 0) try w.print("parent {s}\n", .{ids.items[k - 1]}); if (k > 2 and random.uintLessThan(u8, 100) < 15) try w.print("parent {s}\n", .{ids.items[random.uintLessThan(usize, k - 1)]}); if (random.uintLessThan(u8, 100) >= 2) { try w.writeAll("author "); try identity(random, w); try w.writeByte('\n'); } if (random.uintLessThan(u8, 100) >= 2) { try w.writeAll("committer "); try identity(random, w); try w.writeByte('\n'); } if (random.uintLessThan(u8, 100) < 5) try w.writeAll("encoding UTF-8\n"); try w.writeByte('\n'); const subject = subjects[random.uintLessThan(usize, subjects.len)]; const body = bodies[random.uintLessThan(usize, bodies.len)]; const r = random.uintLessThan(u8, 100); if (r < 10) { try w.writeAll(subject); } else if (r >= 15) { try w.print("{s}\n", .{subject}); if (body.len != 0) { const gaps = [_][]const u8{ "\n", "", "\n\n", " \n" }; try w.print("{s}{s}", .{ gaps[random.uintLessThan(usize, gaps.len)], body }); } } try ids.append(gpa, try repo.hashObject("commit", c.written())); } try repo.git(&.{ "update-ref", "HEAD", ids.items[n - 1] }); return ids.toOwnedSlice(gpa);}
fn freeIds(gpa: std.mem.Allocator, ids: [][]u8) void { for (ids) |id| gpa.free(id); gpa.free(ids);}
/// The date modes compared; the times in the histories are never before/// the epoch in a zone west of UTC, so every mode shows every date.const date_modes = [_][]const u8{ "--date=default", "--date=iso", "--date=iso-strict", "--date=rfc", "--date=short", "--date=raw", "--date=unix", "--date=relative", "--date=human", "--date=local", "--date=format:%Y-%m-%d %H:%M %z", "--date=format-local:%c" };
test "log describes awkward commits in every built-in format as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try repo.env.put("GIT_TEST_DATE_NOW", "1800000000"); const ids = try makeHistory(&repo, 1, 40); defer freeIds(gpa, ids); const formats = [_][]const u8{ "--pretty=oneline", "--pretty=short", "--pretty=medium", "--pretty=full", "--pretty=fuller", "--pretty=reference", "--pretty=email", "--pretty=mboxrd", "--pretty=raw", "--oneline" }; for (formats) |f| { try same(&repo, &.{ "log", f }); try same(&repo, &.{ "log", f, "--abbrev-commit" }); try same(&repo, &.{ "log", f, "--no-abbrev" }); try same(&repo, &.{ "log", f, "--color=always" }); try same(&repo, &.{ "log", f, "--parents" }); try same(&repo, &.{ "log", f, "--reverse", "-n5" }); try same(&repo, &.{ "rev-list", f, "HEAD" }); try same(&repo, &.{ "rev-list", f, "--parents", "HEAD" }); } try same(&repo, &.{ "log", "--expand-tabs=4" }); try same(&repo, &.{ "log", "--no-expand-tabs", "--pretty=fuller" }); try same(&repo, &.{ "rev-list", "--header", "HEAD" }); try same(&repo, &.{ "rev-list", "--header", "--parents", "-n3", "HEAD" }); try same(&repo, &.{ "rev-list", "--pretty", "--expand-tabs", "HEAD" }); for (date_modes) |d| { try same(&repo, &.{ "log", d }); try same(&repo, &.{ "log", "--pretty=fuller", d }); try same(&repo, &.{ "log", "--pretty=reference", d }); }}
const placeholders = [_][]const u8{ "%H", "%h", "%T", "%t", "%P", "%p", "%an", "%aN", "%ae", "%aE", "%al", "%aL", "%ad", "%aD", "%ar", "%at", "%ai", "%aI", "%as", "%ah", "%cn", "%cN", "%ce", "%cE", "%cl", "%cL", "%cd", "%cD", "%cr", "%ct", "%ci", "%cI", "%cs", "%ch", "%s", "%f", "%b", "%B", "%e", "%m", "%n", "%%", "%x00", "%x41", "%Cred", "%Cgreen", "%Creset", "%C(auto)", "%C(bold yellow)", "%C(always,red)", "%C(#ff0000 ul)", "%z", "%(foo)", "%<(10)", "%<(10,trunc)", "%>(12)", "%><(9)", "%>>(8)", "%<(6,ltrunc)", "%<(7,mtrunc)", "%<|(30)", "%>|(-20)", "%w(20)", "%w(30,2,4)", "%w(0)", "%(trailers)", "%(trailers:only)", "%(trailers:unfold)", "%(trailers:keyonly)", "%(trailers:valueonly,separator=%x2C)", "%(trailers:key=Signed-off-by)", "%(trailers:only=no,unfold)", "%(trailers:key_value_separator==)", " ", "|", "lit", "%+s", "%-b", "% e", "%+b", "%-B",};
test "log writes format strings of every placeholder as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try repo.env.put("GIT_TEST_DATE_NOW", "1800000000"); const ids = try makeHistory(&repo, 2, 30); defer freeIds(gpa, ids); try writeFile(&repo, ".mailmap", "Mapped Name <mapped@x> <a@example.com>\nOther <o@x> x <no-at>\n"); // Each placeholder alone, then generated strings of several. for (placeholders) |p| { const arg = try std.fmt.allocPrint(gpa, "--format=[{s}]", .{p}); defer gpa.free(arg); try same(&repo, &.{ "log", arg }); } // git keeps a NUL in a trailers separator, where it ends a decoration // option's value. try same(&repo, &.{ "log", "--format=<%(trailers:separator=[%x00x],key_value_separator=Q%x00R)>" }); var prng: std.Random.DefaultPrng = .init(7); const random = prng.random(); const extras = [_][]const []const u8{ &.{}, &.{"--date=iso"}, &.{"--color"}, &.{"--abbrev-commit"}, &.{"--use-mailmap"}, &.{"--no-use-mailmap"}, &.{"--date=relative"}, &.{"--abbrev=10"} }; for (0..150) |_| { var f: Io.Writer.Allocating = .init(gpa); defer f.deinit(); const prefixes = [_][]const u8{ "--format=format:", "--format=tformat:", "--format=" }; try f.writer.writeAll(prefixes[random.uintLessThan(usize, prefixes.len)]); for (0..1 + random.uintLessThan(usize, 6)) |_| try f.writer.writeAll(placeholders[random.uintLessThan(usize, placeholders.len)]); const extra = extras[random.uintLessThan(usize, extras.len)]; var args: std.ArrayList([]const u8) = .empty; defer args.deinit(gpa); try args.appendSlice(gpa, &.{ "log", f.written() }); try args.appendSlice(gpa, extra); try same(&repo, args.items); } try same(&repo, &.{ "rev-list", "--format=%h %s", "HEAD" }); try same(&repo, &.{ "rev-list", "--format=%s", "--no-commit-header", "HEAD" }); try same(&repo, &.{ "rev-list", "--format=", "HEAD" }); try same(&repo, &.{ "rev-list", "--format=%h", "--abbrev-commit", "--timestamp", "HEAD" });}
test "log reads its configuration as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); const ids = try makeHistory(&repo, 3, 12); defer freeIds(gpa, ids); const cases = [_][]const []const u8{ &.{ "-c", "format.pretty=oneline", "log" }, &.{ "-c", "format.pretty=%h %an", "log" }, &.{ "-c", "pretty.mine=format:%h [%s]", "log", "--pretty=mine" }, &.{ "-c", "pretty.a=b", "-c", "pretty.b=tformat:%H", "log", "--pretty=a" }, &.{ "-c", "log.date=iso", "log" }, &.{ "-c", "log.date=relative", "log", "--date=short" }, &.{ "-c", "log.abbrevCommit=true", "log" }, &.{ "-c", "log.abbrevCommit=true", "log", "--no-abbrev-commit" }, &.{ "-c", "log.showRoot=false", "log" }, &.{ "-c", "color.ui=always", "log", "--format=%C(red)%h%C(auto)%h%Creset" }, &.{ "-c", "color.ui=always", "-c", "color.diff=never", "log", "--format=%C(red)%h" }, &.{ "-c", "color.diff=always", "-c", "color.diff.commit=blue bold", "log", "--oneline" }, &.{ "-c", "color.ui=never", "log", "--color", "--format=%Cred%h" }, &.{ "-c", "trailer.separators=:=", "log", "--format=%(trailers:only)" }, &.{ "-c", "trailer.ack.key=Acked-by", "log", "--format=%(trailers:only)" }, &.{ "-c", "core.commentChar=;", "log", "--format=%(trailers)" }, &.{ "log", "--pretty=nonesuch" }, &.{ "log", "--date=nonesuch" }, &.{ "log", "--format=%C(red green blue)x", "--color" }, &.{ "log", "--format=%C(always,bogus)x" }, }; for (cases) |c| try same(&repo, c);}
test "log searches messages and identities as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); const ids = try makeHistory(&repo, 4, 40); defer freeIds(gpa, ids); try writeFile(&repo, ".mailmap", "Mapped Name <mapped@x> <a@example.com>\n<new@x> <Mixed@Case.ORG>\n"); const patterns = [_][]const u8{ "Fix", "fix", "^Fix", "thing$", "a.c", "caf", "[[:upper:]]ix", "\\<the\\>", "x\\|y", "^$", "Signed", "e|i", "(a|b)c", "a+", "A U", "Jos", "^Mapped", "<a@", "Mixed@", "\\(Fix\\)", "x\\{2\\}", "", "\\w+", "\xc3\xa9" }; var prng: std.Random.DefaultPrng = .init(11); const random = prng.random(); const kinds = [_][]const u8{ "--grep=", "--author=", "--committer=" }; const flags = [_][]const u8{ "--all-match", "--invert-grep", "-i", "-E", "-F", "--use-mailmap", "--no-use-mailmap", "--regexp-ignore-case", "--basic-regexp" }; for (0..150) |_| { var args: std.ArrayList([]const u8) = .empty; defer args.deinit(gpa); var owned: std.ArrayList([]u8) = .empty; defer { for (owned.items) |o| gpa.free(o); owned.deinit(gpa); } try args.appendSlice(gpa, &.{ "log", "--format=%h %an <%ae> %s" }); for (0..1 + random.uintLessThan(usize, 3)) |_| { const o = try std.mem.concat(gpa, u8, &.{ kinds[random.uintLessThan(usize, kinds.len)], patterns[random.uintLessThan(usize, patterns.len)] }); try owned.append(gpa, o); try args.append(gpa, o); } for (flags) |f| if (random.uintLessThan(u8, 100) < 20) try args.append(gpa, f); if (random.boolean()) try args.append(gpa, "-n3"); try same(&repo, args.items); } try same(&repo, &.{ "log", "--grep", "Fix", "--format=%s" }); try same(&repo, &.{ "rev-list", "--grep=Fix", "--author=Jos", "HEAD" }); try same(&repo, &.{ "-c", "grep.patternType=extended", "log", "--grep=e|i", "--format=%s" }); try same(&repo, &.{ "-c", "grep.extendedRegexp=true", "log", "--grep=e|i", "--format=%s" }); try same(&repo, &.{ "-c", "grep.patternType=fixed", "log", "--grep=a.c", "--format=%s" }); try same(&repo, &.{ "-c", "grep.patternType=bogus", "log", "--grep=x" }); for ([_][]const u8{ "[", "[a", "a\\{x\\}", "\\(", "a**", "\\1", "\\" }) |p| { const arg = try std.mem.concat(gpa, u8, &.{ "--grep=", p }); defer gpa.free(arg); try same(&repo, &.{ "log", arg }); const author = try std.mem.concat(gpa, u8, &.{ "--author=", p }); defer gpa.free(author); try same(&repo, &.{ "log", author }); } try refused(&repo, &.{ "log", "-P", "--grep=x" }); try refused(&repo, &.{ "-c", "grep.patternType=perl", "log", "--grep=x" });}
test "show shows commits, tags, trees and blobs as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try repo.commitFile("a", "a\n", "first\n\nbody\n"); try writeFile(&repo, "d/b", "b\n"); try repo.git(&.{ "add", "d/b" }); try repo.commit("second"); try repo.git(&.{ "tag", "-a", "-m", "tag message\n\nmore", "v1", "HEAD~1" }); try repo.git(&.{ "tag", "-a", "-m", "tree tag", "tt", "HEAD^{tree}" }); try repo.git(&.{ "tag", "-a", "-m", "blob tag", "bt", "HEAD:a" }); try repo.git(&.{ "-c", "advice.nestedTag=false", "tag", "-a", "-m", "tag of a tag", "tot", "v1" }); const cases = [_][]const []const u8{ &.{ "show", "-s" }, &.{ "show", "--no-patch", "HEAD~1", "HEAD" }, &.{ "show", "-s", "HEAD", "HEAD", "HEAD~1" }, &.{ "show", "-s", "--oneline", "HEAD~1", "HEAD^{tree}", "HEAD" }, &.{ "show", "-s", "--format=format:%h", "HEAD~1", "HEAD" }, &.{ "show", "-s", "--format=%h", "HEAD^{tree}", "HEAD~1" }, &.{ "show", "HEAD^{tree}" }, &.{ "show", "HEAD:a", "HEAD^{tree}", "HEAD:d/b" }, &.{ "show", "-s", "HEAD^{tree}", "HEAD:a", "HEAD~1" }, &.{ "show", "-s", "v1" }, &.{ "show", "tt", "bt" }, &.{ "show", "-s", "tot" }, &.{ "show", "-s", "--pretty=fuller", "v1" }, &.{ "show", "-s", "--pretty=email", "v1" }, &.{ "show", "-s", "--oneline", "v1" }, &.{ "show", "-s", "--format=%s", "v1" }, &.{ "show", "-s", "--date=iso", "v1" }, &.{ "show", "-s", "nonesuch" }, }; for (cases) |c| try same(&repo, c); try same(&repo, &.{ "show", "HEAD" }); try refused(&repo, &.{ "show", "-s", "HEAD~1..HEAD" });}
test "check-mailmap maps contacts as git does, from every source" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try repo.commitFile("x", "x\n", "x"); try writeFile(&repo, ".mailmap", "W <w@x> <a@x>\nSpaced Name <s@x>\n"); try writeFile(&repo, "other.mailmap", "F <f@x> <a@x>\n"); const blob = try repo.outputWithInput(&.{ "hash-object", "-w", "--stdin" }, "B <b@x> <a@x>\nB2 <b2@x> <c@x>\n"); defer gpa.free(blob); const b = std.mem.trimEnd(u8, blob, "\n"); const blob_arg = try std.fmt.allocPrint(gpa, "mailmap.blob={s}", .{b}); defer gpa.free(blob_arg); const blob_option = try std.fmt.allocPrint(gpa, "--mailmap-blob={s}", .{b}); defer gpa.free(blob_option); const cases = [_][]const []const u8{ &.{ "check-mailmap", "X <a@x>", "<a@x>", "a@x", "Nobody <n@x>", "X <s@x>", " X <a@x>", "X <a@x> trailing", "<>", "plain" }, &.{ "-c", blob_arg, "check-mailmap", "X <a@x>", "X <c@x>" }, &.{ "-c", "mailmap.file=other.mailmap", "-c", blob_arg, "check-mailmap", "X <a@x>" }, &.{ "-c", "mailmap.file=other.mailmap", "check-mailmap", blob_option, "X <a@x>" }, &.{ "check-mailmap", "--mailmap-file=other.mailmap", "X <a@x>" }, &.{ "-c", "mailmap.blob=nonesuch", "check-mailmap", "X <a@x>" }, &.{ "-c", "mailmap.file=nonesuch", "check-mailmap", "X <a@x>" }, &.{"check-mailmap"}, }; for (cases) |c| try same(&repo, c); try sameOutput(&repo, &.{ "check-mailmap", "--bogus" }); try tool.expectSameAsGitHow(&repo, &.{ "check-mailmap", "--stdin", "X <a@x>" }, .{ .input = "Y <a@x>\nfoo@bar\nlast <s@x>" }); try tool.expectSameAsGitHow(&repo, &.{ "check-mailmap", "X <a@x>" }, .{ .cwd = "." });}
test "the mailmap is read from a bare repository's HEAD and never through a symbolic link" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try repo.commitFile(".mailmap", "H <h@x> <a@x>\n", "mailmap"); try repo.git(&.{ "clone", "-q", "--bare", ".", "bare.git" }); try tool.expectSameAsGitHow(&repo, &.{ "check-mailmap", "X <a@x>" }, .{ .cwd = "bare.git" }); try tool.expectSameAsGitHow(&repo, &.{ "log", "--format=%aN|%an", "-1" }, .{ .cwd = "bare.git" }); try repo.git(&.{ "rm", "-q", ".mailmap" }); try writeFile(&repo, "real.mailmap", "R <r@x> <a@x>\n"); try repo.tmp.dir.symLink(repo.io, "real.mailmap", ".mailmap", .{}); try same(&repo, &.{ "check-mailmap", "X <a@x>" });}
test "log refuses by name what needs what this does not have" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try repo.commitFile("x", "x\n", "x"); for ([_][]const []const u8{ &.{ "rev-list", "-z", "HEAD" }, &.{ "log", "--graph" }, &.{ "log", "--notes" }, &.{ "log", "--show-signature" }, &.{ "log", "--format=%N" }, &.{ "log", "--format=%G?" }, &.{ "log", "--format=%gD" }, &.{ "log", "--encoding=EUC-JISX0213" }, &.{ "-c", "i18n.logOutputEncoding=BIG5-HKSCS", "log" }, &.{ "-c", "i18n.commitEncoding=EUC-TW", "log" }, }) |c| try refused(&repo, c); // A commit in a charset zig-charset does not convert. const tree_out = try repo.output(&.{ "mktree", "--missing" }); defer gpa.free(tree_out); const contents = try std.fmt.allocPrint(gpa, "tree {s}\nauthor A <a@x> 1 +0000\ncommitter C <c@x> 1 +0000\nencoding EUC-TW\n\ncaf\xe9\n", .{std.mem.trimEnd(u8, tree_out, "\n")}); defer gpa.free(contents); const id = try repo.hashObject("commit", contents); defer gpa.free(id); try refused(&repo, &.{ "log", id }); try same(&repo, &.{ "log", "--format=%H", id });}
test "log, show and rev-list write commits in the output encoding as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); const tree_out = try repo.output(&.{ "mktree", "--missing" }); defer gpa.free(tree_out); const tree = std.mem.trimEnd(u8, tree_out, "\n"); // What each commit declares, and its author and message: in // ISO-8859-1 under a name of it git knows, in UTF-8 with and without // the header, what neither encoding can hold, bytes that are not // UTF-8, and two headers. const commits = [_]struct { header: []const u8, author: []const u8, message: []const u8 }{ .{ .header = "encoding ISO-8859-1\n", .author = "Jos\xe9", .message = "caf\xe9\n\nbody \xe9\n" }, .{ .header = "encoding latin1\n", .author = "J", .message = "caf\xe9\n" }, .{ .header = "encoding LATIN1\nencoding x\n", .author = "J", .message = "\xe9\n" }, .{ .header = "encoding csISOLatin1\n", .author = "J", .message = "\xe9\n" }, .{ .header = "", .author = "Jos\xc3\xa9", .message = "caf\xc3\xa9\n" }, .{ .header = "encoding UTF-8\n", .author = "J", .message = "\xe6\x97\xa5 caf\xc3\xa9\n" }, .{ .header = "encoding utf8\n", .author = "J", .message = "x\n" }, .{ .header = "", .author = "J", .message = "caf\xe9 not UTF-8\n" }, }; var ids: std.ArrayList([]u8) = .empty; defer { for (ids.items) |id| gpa.free(id); ids.deinit(gpa); } for (commits, 0..) |c, k| { var text: Io.Writer.Allocating = .init(gpa); defer text.deinit(); try text.writer.print("tree {s}\n", .{tree}); if (k > 0) try text.writer.print("parent {s}\n", .{ids.items[k - 1]}); try text.writer.print("author {s} <a@x> 1 +0000\ncommitter C <c@x> 1 +0000\n{s}\n{s}", .{ c.author, c.header, c.message }); try ids.append(gpa, try repo.hashObject("commit", text.written())); } try repo.git(&.{ "update-ref", "HEAD", ids.items[ids.items.len - 1] }); const outputs = [_][]const []const u8{ &.{}, &.{"--encoding=UTF-8"}, &.{"--encoding=latin1"}, &.{"--encoding=ISO-8859-1"}, &.{ "-c", "i18n.commitEncoding=latin1" }, &.{ "-c", "i18n.logOutputEncoding=ISO-8859-1" }, &.{ "-c", "i18n.commitEncoding=latin1", "--encoding=utf8" }, }; const formats = [_][]const u8{ "--pretty=raw", "--pretty=fuller", "--format=%an%x00%s%x00%e%x00%B", "--pretty=reference", "--oneline" }; var args: std.ArrayList([]const u8) = .empty; defer args.deinit(gpa); for (outputs) |o| for (formats) |f| for ([_][]const u8{ "log", "show", "rev-list" }) |command| { args.clearRetainingCapacity(); // `-c` goes before the command, the rest after. const before = o.len != 0 and std.mem.eql(u8, o[0], "-c"); if (before) try args.appendSlice(gpa, o[0..2]); try args.append(gpa, command); if (before) try args.appendSlice(gpa, o[2..]) else try args.appendSlice(gpa, o); try args.append(gpa, f); if (std.mem.eql(u8, command, "show")) { try args.appendSlice(gpa, &.{ "-s", ids.items[0], ids.items[4], ids.items[5], ids.items[7] }); } else try args.append(gpa, "HEAD"); try same(&repo, args.items); }; // A search sees each commit as it is written. for ([_][]const u8{ "caf\xc3\xa9", "caf\xe9", "Jos\xc3\xa9", "\xe9$" }) |pattern| { const grep = try std.fmt.allocPrint(gpa, "--grep={s}", .{pattern}); defer gpa.free(grep); const author = try std.fmt.allocPrint(gpa, "--author={s}", .{pattern}); defer gpa.free(author); for ([_][]const []const u8{ &.{}, &.{"--encoding=latin1"} }) |o| { args.clearRetainingCapacity(); try args.append(gpa, "log"); try args.appendSlice(gpa, o); try args.appendSlice(gpa, &.{ "--format=%H", grep }); try same(&repo, args.items); args.items[args.items.len - 1] = author; try same(&repo, args.items); } }}
test "log, show and rev-list convert commits between glibc's charsets as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); const tree_out = try repo.output(&.{ "mktree", "--missing" }); defer gpa.free(tree_out); const tree = std.mem.trimEnd(u8, tree_out, "\n"); // A commit in each of a spread of charsets: single-byte Cyrillic, an // EBCDIC code page, two of two bytes a character, under names glibc // has for them in any case; one in UTF-8 that some of them can hold; // bytes not valid in what they claim to be; names glibc does not // know; and git's own name for ISO-8859-1. const commits = [_]struct { header: []const u8, author: []const u8, message: []const u8 }{ .{ .header = "encoding KOI8-R\n", .author = "\xe9\xd7\xc1\xce", .message = "\xfd\xc9 \xc4\xc1 \xcb\xc1\xdb\xc1\n" }, .{ .header = "encoding windows-1251\n", .author = "\xcf\xb8\xf2\xf0", .message = "\xcf\xf0\xe8\xe2\xe5\xf2 \xec\xe8\xf0\n\nbody\n" }, .{ .header = "encoding ibm037\n", .author = "\xd1\x96\xa2Q", .message = "\xc3\x81\x86Q%%\x82\x96\x84\xa8@Q%" }, .{ .header = "encoding euc-kr\n", .author = "\xb1\xe8\xc3\xb6\xbc\xf6", .message = "\xc7\xd1\xb1\xb9\xbe\xee \xc1\xa6\xb8\xf1\n\n\xba\xbb\xb9\xae\n" }, .{ .header = "encoding Windows-31J\n", .author = "\x8eR\x93c", .message = "\x93\xfa\x96{\x8c\xea\x82\xcc\x83R\x83~\x83b\x83g\n\n\x96{\x95\xb6\n" }, .{ .header = "", .author = "J\xc3\xb6ns", .message = "\xd0\xa9\xd0\xb8 \xed\x95\x9c caf\xc3\xa9\n" }, .{ .header = "", .author = "J", .message = "\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82\n" }, .{ .header = "encoding EUC-KR\n", .author = "J", .message = "cut short \xc7\n" }, .{ .header = "encoding ASCII\n", .author = "J", .message = "caf\xe9\n" }, .{ .header = "encoding 8859-1\n", .author = "J", .message = "caf\xe9\n" }, .{ .header = "encoding Latin-1\n", .author = "Jos\xe9", .message = "caf\xe9\n" }, .{ .header = "encoding ISO 8859 5\n", .author = "J", .message = "\xbf\xe0\xd8\xd2\xd5\xe2\n" }, }; var ids: std.ArrayList([]u8) = .empty; defer { for (ids.items) |id| gpa.free(id); ids.deinit(gpa); } for (commits, 0..) |c, k| { var text: Io.Writer.Allocating = .init(gpa); defer text.deinit(); try text.writer.print("tree {s}\n", .{tree}); if (k > 0) try text.writer.print("parent {s}\n", .{ids.items[k - 1]}); try text.writer.print("author {s} <a@x> 1 +0000\ncommitter C <c@x> 1 +0000\n{s}\n{s}", .{ c.author, c.header, c.message }); try ids.append(gpa, try repo.hashObject("commit", text.written())); } try repo.git(&.{ "update-ref", "HEAD", ids.items[ids.items.len - 1] }); // Into UTF-8, into each of the legacy charsets above, into EBCDIC, and // into names that convert nothing: an empty one, one glibc does not // know, and a name of UTF-8 git does not take for one. const outputs = [_][]const u8{ "--encoding=UTF-8", "--encoding=koi8-r", "--encoding=CP1251", "--encoding=EUC-KR", "--encoding=cp932", "--encoding=IBM500", "--encoding=latin-1", "--encoding=", "--encoding=bogus", "--encoding=ISO-10646/UTF8/" }; const formats = [_][]const u8{ "--pretty=raw", "--pretty=medium", "--pretty=email", "--pretty=reference", "--oneline", "--format=%an|%s|%e|\xc3\xa9\xd0\xa9|%B", "--format=%<(8,trunc)%s|%>(6)%an|" }; var args: std.ArrayList([]const u8) = .empty; defer args.deinit(gpa); for (outputs) |o| for (formats) |f| { try same(&repo, &.{ "log", o, f }); try same(&repo, &.{ "rev-list", o, f, "HEAD" }); }; try same(&repo, &.{ "show", "-s", "--encoding=KOI8-U", ids.items[0], ids.items[5] }); try same(&repo, &.{ "-c", "i18n.logOutputEncoding=CP949", "log", "--format=%s", "HEAD~6" }); try same(&repo, &.{ "-c", "i18n.commitEncoding=MS932", "log", "--oneline" }); // A search sees each commit as it is written in the output encoding. for ([_][2][]const u8{ .{ "--encoding=KOI8-R", "--grep=\xc4\xc1 \xcb" }, .{ "--encoding=KOI8-R", "--author=\xe9\xd7" }, .{ "--encoding=UTF-8", "--grep=\xd0\xb4\xd0\xb0 \xd0\xba" }, .{ "--encoding=EUC-KR", "--grep=\xba\xbb" }, .{ "--encoding=cp1251", "--grep=^\xcf\xf0\xe8\xe2" }, }) |g| try same(&repo, &.{ "log", g[0], g[1], "--format=%H" }); // What zig-charset does not convert is refused, by name. for ([_][]const []const u8{ &.{ "log", "--encoding=ISO-2022-JP-3" }, &.{ "log", "--encoding=utf-8//IGNORE" }, &.{ "log", "--encoding=/" }, }) |c| try refused(&repo, c); const euc_tw = try std.fmt.allocPrint(gpa, "tree {s}\nauthor A <a@x> 1 +0000\ncommitter C <c@x> 1 +0000\nencoding EUC-TW\n\n\xa4\xa4\n", .{tree}); defer gpa.free(euc_tw); const id = try repo.hashObject("commit", euc_tw); defer gpa.free(id); try refused(&repo, &.{ "log", id }); try same(&repo, &.{ "log", "--format=%H", id }); // Converted from EBCDIC with no blank line left, a commit is read by // git's formats of the user's past its end: refused for them alone. const no_blank = try std.fmt.allocPrint(gpa, "tree {s}\nauthor A <a@x> 1 +0000\ncommitter C <c@x> 1 +0000\nencoding IBM037\n\n\xc3\x81\x86Q%", .{tree}); defer gpa.free(no_blank); const ebcdic = try repo.hashObject("commit", no_blank); defer gpa.free(ebcdic); try refused(&repo, &.{ "log", "--format=%s", ebcdic }); for ([_][]const u8{ "--pretty=medium", "--pretty=raw", "--pretty=email", "--oneline", "--format=%H" }) |f| try same(&repo, &.{ "log", f, ebcdic });}
test "log, show and rev-list read and write ISO-2022-JP, the Japanese and Chinese multibyte charsets, CP1255 and CP1258 as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); const tree_out = try repo.output(&.{ "mktree", "--missing" }); defer gpa.free(tree_out); const tree = std.mem.trimEnd(u8, tree_out, "\n"); // Commits in ISO-2022-JP, under its three names: with and without a // line feed after the last character, ending in JIS X 0208 with no // escape back, with a body, JIS X 0201 Roman, a name in JIS and one in // UTF-8 (which it cannot be read as), a subject long enough to fold, // a tab after JIS text, and an escape sequence cut short. Then commits // in UTF-8 to be written in it: what it holds, half-width katakana it // does not, an ESC of its own, and a name too wide for its line. Last, // CP1258 and CP1255, which compose, with last characters glibc holds // back for a mark, and UTF-8 with combining marks to be written in them; // and EUC-JP, with half-width katakana, JIS X 0212, a C1 control and // a JIS X 0212 character cut short, and the yen sign and overline, // which it writes as ASCII's backslash and tilde; and glibc's SJIS, whose // second bytes can be ASCII's backslash, in a name, a trailer and a // message, which is read back as the yen sign; and GBK, whose second // bytes can be too, with its euro sign and a cell of GB 18030's it lacks; // EUC-CN; Big5, whose second bytes can be ASCII's too; and GB18030's // four-byte sequences, whose second and fourth bytes are digits. const jis = "\x1b$B7oL>\x1b(B"; const commits = [_]struct { header: []const u8, author: []const u8, message: []const u8 }{ .{ .header = "encoding ISO-2022-JP\n", .author = "t", .message = "\x1b$B7oL>\x1b(B\n" }, .{ .header = "encoding iso-2022-jp\n", .author = "t", .message = "\x1b$B7oL>" }, .{ .header = "encoding CSISO2022JP\n", .author = "t", .message = "\x1b$B7oL>\n\n\x1b$BK\\J8\x1b(B\n" }, .{ .header = "encoding ISO2022JP\n", .author = "\x1b$B;3ED\x1b(B", .message = "\x1b(Jyen\\tilde~\x1b(B\n" }, .{ .header = "encoding ISO-2022-JP\n", .author = "\xe5\xb1\xb1\xe7\x94\xb0", .message = jis ++ "\n" }, .{ .header = "encoding ISO-2022-JP\n", .author = "t", .message = jis ++ " " ++ jis ++ " " ++ jis ++ " " ++ jis ++ " " ++ jis ++ " " ++ jis ++ " " ++ jis ++ " " ++ jis ++ "\n\n" ++ jis ++ "\t" ++ jis ++ "\n" }, .{ .header = "encoding ISO-2022-JP\n", .author = "t", .message = jis ++ "\x1b$" }, .{ .header = "", .author = "\xe5\xb1\xb1\xe7\x94\xb0", .message = "\xe4\xbb\xb6\xe5\x90\x8d" }, .{ .header = "", .author = "t", .message = "\xef\xbd\xb1 half-width\n" }, .{ .header = "", .author = "a\x1bb", .message = "sub\x1bj\n\nbo\x1bdy\n" }, .{ .header = "", .author = "x \xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5\xe6\x97\xa5", .message = "a\xe6\x97\xa5b\n" }, .{ .header = "encoding CP1258\n", .author = "A\xcc", .message = "Vi\xd2\xeat A\xcc\n\nbody\n" }, .{ .header = "encoding windows-1258\n", .author = "t", .message = "abcA" }, .{ .header = "encoding CP1258\n", .author = "t", .message = "caf\xe9 \xdd" }, .{ .header = "encoding CP1255\n", .author = "\xf9\xd1\xcc", .message = "x\xf9\xd1" }, .{ .header = "", .author = "A\xcc\x80 e\xcc\x81", .message = "A\xcc\x80 Vi\xe1\xbb\x87t\n" }, .{ .header = "encoding EUC-JP\n", .author = "\xbb\xb3\xc5\xc4", .message = "\xc6\xfc\xcb\xdc \x8e\xb1 \x8f\xb0\xa1 a\x85b\n\n\xcb\xdc\xca\xb8\tx\n" }, .{ .header = "encoding ujis\n", .author = "t", .message = "cut short \x8f\xb0\n" }, .{ .header = "", .author = "\xc2\xa5\xe2\x80\xbe", .message = "\xc2\xa5100 \xe2\x80\xbe \xe4\xb8\x82\n" }, .{ .header = "encoding SJIS\n", .author = "\x95\\.\x83\\ [x]", .message = "path\\to~x \x95\\ \xb1\xb2\n\nbody \x81\x7c |\n\nSigned-off-by: \x95\\ <s@t>\n" }, .{ .header = "encoding Shift_JIS\n", .author = "t", .message = "cut \x95" }, .{ .header = "", .author = "a\\b", .message = "back\\slash ~ \xc2\xa5 \xe2\x80\xbe \xe8\xa1\xa8\n" }, .{ .header = "encoding GBK\n", .author = "\x81\\.\x81| [x]", .message = "\xd6\xd0\xce\xc4 \x81\\ \x80 |\n\nbody\n\nSigned-off-by: \x81\\ <s@t>\n" }, .{ .header = "encoding CP936\n", .author = "t", .message = "x \xa2\xe3\n" }, .{ .header = "encoding GB2312\n", .author = "\xd5\xc5", .message = "\xd6\xd0\xce\xc4\n\nbody\n" }, .{ .header = "encoding BIG5\n", .author = "\xa5\\.\xa5| [x]", .message = "\xa4\xa4\xa4\xe5 \xa5\\ |\n\nSigned-off-by: \xa5\\ <s@t>\n" }, .{ .header = "encoding GB18030\n", .author = "\x95\x32\x82\x36", .message = "\x81\x30\x81\x30 \x95\x32\x82\x36 \x80\n" }, }; var ids: std.ArrayList([]u8) = .empty; defer { for (ids.items) |id| gpa.free(id); ids.deinit(gpa); } for (commits, 0..) |c, k| { var text: Io.Writer.Allocating = .init(gpa); defer text.deinit(); try text.writer.print("tree {s}\n", .{tree}); if (k > 0) try text.writer.print("parent {s}\n", .{ids.items[k - 1]}); try text.writer.print("author {s} <a@x> 1 +0000\ncommitter C <c@x> 1 +0000\n{s}\n{s}", .{ c.author, c.header, c.message }); try ids.append(gpa, try repo.hashObject("commit", text.written())); } try repo.git(&.{ "update-ref", "HEAD", ids.items[ids.items.len - 1] }); const outputs = [_][]const u8{ "--encoding=ISO-2022-JP", "--encoding=csISO2022JP", "--encoding=UTF-8", "--encoding=latin1", "--encoding=EUC-KR", "--encoding=CP1258", "--encoding=CP1255", "--encoding=EUC-JP", "--encoding=SJIS", "--encoding=GB18030" }; const formats = [_][]const u8{ "--pretty=raw", "--pretty=medium", "--pretty=email", "--pretty=reference", "--oneline", "--format=%an|%s|%f|%e|%B", "--format=%s", "--pretty=format:%s", "--format=%w(20,2,4)%B", "--format=%<(12,trunc)%an|%>(12)%s|" }; for (outputs) |o| for (formats) |f| { try same(&repo, &.{ "log", o, f }); try same(&repo, &.{ "log", "-z", o, f }); try same(&repo, &.{ "rev-list", o, f, "HEAD" }); }; try same(&repo, &.{ "log", "--pretty=email" }); try same(&repo, &.{ "show", "-s", "--encoding=ISO-2022-JP", ids.items[0], ids.items[7] }); try same(&repo, &.{ "-c", "i18n.logOutputEncoding=ISO-2022-JP", "log", "--format=%s" }); // A search sees each commit as it is written in the output encoding. for ([_][2][]const u8{ .{ "--encoding=ISO-2022-JP", "--grep=7oL>" }, .{ "--encoding=UTF-8", "--grep=\xe4\xbb\xb6" }, .{ "--encoding=ISO-2022-JP", "--author=;3ED" }, }) |g| try same(&repo, &.{ "log", g[0], g[1], "--format=%H" }); // EUC-CN to and from GBK and Big5, which glibc converts by tables of // its own, is refused where the commit is converted whole; a format of // the user's goes through UTF-8, as it does in git. for ([_][]const u8{ "--encoding=GB2312", "--encoding=BIG5", "--encoding=GBK" }) |o| { try refused(&repo, &.{ "log", o, "--pretty=raw" }); try same(&repo, &.{ "log", o, "--format=%an|%s|%B" }); } // ISO-2022-JP-2 and -3, which glibc has and zig-charset does not. try refused(&repo, &.{ "log", "--encoding=ISO-2022-JP-2" }); try refused(&repo, &.{ "log", "--encoding=ISO-2022-JP-3" });}
test "log and show end each commit with a NUL under -z as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try writeFile(&repo, "a", "1\n2\n3\n"); try writeFile(&repo, "caf\xc3\xa9", "x\n"); try repo.git(&.{ "add", "." }); try repo.commit("base"); try repo.git(&.{ "checkout", "-q", "-b", "side" }); try writeFile(&repo, "a", "1\n2s\n3\n"); try writeFile(&repo, "caf\xc3\xa9", "x\ny\n"); try repo.git(&.{ "add", "." }); try repo.commit("side"); try repo.git(&.{ "checkout", "-q", "-" }); try writeFile(&repo, "a", "1m\n2\n3\n"); try writeFile(&repo, "caf\xc3\xa9", "z\n"); try repo.git(&.{ "add", "." }); try repo.commit("main"); // It conflicts; what is committed is what is written below. const merged = try repo.runIn(".", &.{ "merge", "-q", "--no-commit", "side" }, ""); merged.deinit(gpa); // An evil merge: a line neither parent has. try writeFile(&repo, "a", "1m\n2s\n3\nevil\n"); try writeFile(&repo, "caf\xc3\xa9", "w\n"); try repo.git(&.{ "add", "." }); try repo.commit("merge"); try repo.git(&.{ "mv", "a", "b" }); try writeFile(&repo, "bin", "\x00\x01"); try repo.git(&.{ "add", "." }); try repo.commit("rename and binary"); const formats = [_][]const u8{ "--format=%h", "--format=%h%n", "--pretty=format:%h", "--pretty=medium", "--oneline", "--pretty=raw", "--pretty=reference", "--format=", "--format=%H%x00%B%x00" }; const diffs = [_][]const []const u8{ &.{}, &.{"--raw"}, &.{"-p"}, &.{"--patch-with-raw"}, &.{"--stat"}, &.{"--name-only"}, &.{"--name-status"}, &.{"--numstat"}, &.{"--patch-with-stat"}, &.{"--summary"}, &.{"--cc"}, &.{ "--cc", "--raw" }, &.{ "-c", "--name-status" }, &.{ "-m", "--raw" }, &.{ "--cc", "--stat" }, &.{ "--cc", "--name-only" }, &.{ "--first-parent", "-p" }, &.{ "-M", "--raw" }, }; var args: std.ArrayList([]const u8) = .empty; defer args.deinit(gpa); for ([_][]const u8{ "log", "show" }) |command| for (formats) |f| for (diffs) |d| { args.clearRetainingCapacity(); try args.appendSlice(gpa, &.{ command, "-z", f }); try args.appendSlice(gpa, d); try same(&repo, args.items); }; // As grebe asks. try same(&repo, &.{ "-c", "log.showSignature=false", "log", "--parents", "--no-color", "--encoding=UTF-8", "-z", "--date=raw", "--decorate=full", "--format=%H%x00%P%x00%an%x00%ae%x00%ad%x00%s%x00%D", "--topo-order" }); for ([_][]const u8{ "HEAD", "HEAD~1", "HEAD~2" }) |rev| { try same(&repo, &.{ "-c", "log.showSignature=false", "show", "--encoding=UTF-8", "--cc", "--date=raw", "--format=%H%x00%P%x00%an%x00%ae%x00%ad%x00%cn%x00%ce%x00%cd%x00%B%x00", "--no-color", "--no-ext-diff", "--no-textconv", "-M", "--patch-with-raw", "-z", "-U3", rev, "--" }); }}
test "log on an unborn branch and with no commits says what git says" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try same(&repo, &.{"log"}); try same(&repo, &.{ "log", "--oneline" });}
/// A history of merges on four branches made with `git fast-import`, each/// commit changing a file or two, with authors and committers that differ.fn makeBranchyHistory(repo: *Repo, seed: u64, n: usize) !void { const gpa = repo.gpa; var prng: std.Random.DefaultPrng = .init(seed); const random = prng.random(); var stream: Io.Writer.Allocating = .init(gpa); defer stream.deinit(); const w = &stream.writer; const branches = [_][]const u8{ "main", "b1", "b2", "b3" }; var tips: [branches.len]?usize = @splat(null); var date: u64 = 1_790_000_000; for (1..n + 1) |mark| { const b = random.uintLessThan(usize, branches.len); date += random.uintLessThan(u64, 200); try w.print("commit refs/heads/{s}\nmark :{d}\n", .{ branches[b], mark }); const who = names[random.uintLessThan(usize, 4)]; try w.print("author {s} <a{d}@example.com> {d} {s}\ncommitter C <c@example.com> {d} +0000\n", .{ if (who.len == 0) "x" else std.mem.trim(u8, who, " "), random.uintLessThan(u8, 3), date - random.uintLessThan(u64, 1000), zones[random.uintLessThan(usize, zones.len)], date }); const msg = try std.fmt.allocPrint(gpa, "c{d} on {s}\n\nBody of {d}.\n\nSigned-off-by: S{d} <s@x>\n", .{ mark, branches[b], mark, mark % 3 }); defer gpa.free(msg); try w.print("data {d}\n{s}", .{ msg.len, msg }); if (tips[b]) |t| { try w.print("from :{d}\n", .{t}); if (random.uintLessThan(u8, 100) < 30) { const other = (b + 1 + random.uintLessThan(usize, branches.len - 1)) % branches.len; if (tips[other]) |o| try w.print("merge :{d}\n", .{o}); } } const files = [_][]const u8{ "a", "b", "d/c", "d/e" }; try w.print("M 100644 inline {s}\ndata 3\n{d}\n\n", .{ files[random.uintLessThan(usize, files.len)], mark % 100 }); tips[b] = mark; } _ = try repo.outputWithInput(&.{ "fast-import", "--quiet" }, stream.written()); try repo.git(&.{ "checkout", "-q", "-f", "main" });}
test "log walks as rev-list walks, and describes what it lists as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try makeBranchyHistory(&repo, 5, 60); const cases = [_][]const []const u8{ &.{ "log", "--oneline", "main" }, &.{ "log", "--oneline", "b1..main" }, &.{ "log", "--format=%h %p | %P", "main", "--", "d" }, &.{ "log", "--format=%h %p", "--parents", "main", "--", "d" }, &.{ "log", "--parents", "main", "--", "a" }, &.{ "log", "--pretty=fuller", "--full-history", "main", "--", "a" }, &.{ "log", "--oneline", "--first-parent", "main" }, &.{ "log", "--format=%m %h", "--left-right", "main...b2" }, &.{ "log", "--left-right", "--oneline", "main...b2" }, &.{ "log", "--left-right", "main...b2" }, &.{ "log", "--boundary", "--oneline", "b1..main" }, &.{ "log", "--boundary", "b1..main" }, &.{ "log", "--boundary", "--format=%m %h", "b1..main" }, &.{ "log", "--children", "--oneline", "-n8", "main" }, &.{ "log", "--oneline", "--topo-order", "--all" }, &.{ "log", "--oneline", "--author-date-order", "main" }, &.{ "log", "--oneline", "--date-order", "main", "b3" }, &.{ "log", "-n3", "--skip=2", "main" }, &.{ "log", "-3", "--reverse", "--format=%h %s", "main" }, &.{ "log", "--merges", "--format=%h %s", "main" }, &.{ "log", "--no-merges", "--format=%h", "--since=1790003000", "main" }, &.{ "log", "--author=Jos", "--format=%h %an", "main" }, &.{ "log", "--grep=on b1", "--format=%h %s", "--all" }, &.{ "log", "--grep=S1", "--grep=on main", "--all-match", "--format=%h %s", "--all" }, &.{ "log", "--format=%(trailers:key=Signed-off-by,valueonly)", "-n4", "main" }, &.{ "log", "--simplify-merges", "--format=%h %p", "main", "--", "d/c" }, &.{ "log", "--oneline", "--", "d/e" }, &.{ "log", "--format=%h", "nonesuch" }, &.{ "log", "--format=%h", "main", "nonesuch" }, &.{ "rev-list", "--format=%h %an", "--author=Jos", "main" }, &.{ "rev-list", "--left-right", "--pretty=oneline", "main...b2" }, &.{ "rev-list", "--boundary", "--format=%h", "b1..main" }, }; for (cases) |c| try same(&repo, c);}