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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Paths as git writes them in its output when `core.quotePath` is true,//! as it is by default: unchanged if nothing in them needs it, and//! otherwise in double quotes with C's escapes. The bytes that need it are//! the control characters, DEL, `"`, `\` and every byte of 0x80 and above;//! the seven with a letter escape get it, and the rest are written in//! three octal digits. A space does not need it. These are what `git//! cat-file -p` printed for a tree with a name for every byte.
const std = @import("std");const Io = std.Io;
/// Whether git would quote `path`.pub fn needsQuoting(path: []const u8) bool { return needsQuotingWith(path, .{});}
/// How paths are quoted.pub const Options = struct { /// `core.quotePath`: whether bytes of 0x80 and above are escaped too. /// When it is false, git writes them as they are, but still escapes /// the control characters, DEL, `"` and `\`. quote_path: bool = true,};
/// Whether git would quote `path` with `core.quotePath` as `options` says.pub fn needsQuotingWith(path: []const u8, options: Options) bool { for (path) |c| if (needsEscapeWith(c, options)) return true; return false;}
fn needsEscapeWith(c: u8, options: Options) bool { return c < 0x20 or c == '"' or c == '\\' or c == 0x7f or (c > 0x7f and options.quote_path);}
/// Writes `path` as git writes it.pub fn writePath(w: *Io.Writer, path: []const u8) Io.Writer.Error!void { return writePathWith(w, path, .{});}
/// Writes `path` as git writes it with `core.quotePath` as `options`/// says.pub fn writePathWith(w: *Io.Writer, path: []const u8, options: Options) Io.Writer.Error!void { if (!needsQuotingWith(path, options)) return w.writeAll(path); try w.writeByte('"'); try writeQuotedBody(w, path, options); try w.writeByte('"');}
/// Writes `text` escaped as inside git's double quotes, without the/// quotes, so that a quoted path can be written in pieces.pub fn writeQuotedBody(w: *Io.Writer, text: []const u8, options: Options) Io.Writer.Error!void { for (text) |c| { if (!needsEscapeWith(c, options)) { try w.writeByte(c); continue; } const letter: ?u8 = switch (c) { 0x07 => 'a', 0x08 => 'b', '\t' => 't', '\n' => 'n', 0x0b => 'v', 0x0c => 'f', '\r' => 'r', '"' => '"', '\\' => '\\', else => null, }; if (letter) |l| { try w.writeAll(&.{ '\\', l }); } else { try w.print("\\{o:0>3}", .{c}); } }}
test "a path is quoted only if it needs to be" { var buffer: [64]u8 = undefined; var w: Io.Writer = .fixed(&buffer); try writePath(&w, "plain name.txt"); try std.testing.expectEqualStrings("plain name.txt", w.buffered()); w.end = 0; try writePath(&w, "caf\xc3\xa9\t\"q\"\\\x01\x7f"); try std.testing.expectEqualStrings("\"caf\\303\\251\\t\\\"q\\\"\\\\\\001\\177\"", w.buffered()); w.end = 0; try writePathWith(&w, "caf\xc3\xa9", .{ .quote_path = false }); try std.testing.expectEqualStrings("caf\xc3\xa9", w.buffered()); w.end = 0; try writePathWith(&w, "caf\xc3\xa9\x7f", .{ .quote_path = false }); try std.testing.expectEqualStrings("\"caf\xc3\xa9\\177\"", w.buffered());}
/// What reading back a C-quoted string took of its input.pub const Unquoted = struct { /// How many bytes of the input the quoted string took. len: usize,};
/// Reads the double-quoted string at the start of `text`, written with/// the escapes `writePath` uses (and `\a`, `\b`, `\f`, `\v` and octal for/// any byte), into `out`, as git reads a quoted path in/// `objects/info/alternates` or `GIT_ALTERNATE_OBJECT_DIRECTORIES`./// Returns null, having written nothing it promises, if `text` does not/// begin with a well-formed quoted string; git then takes it literally.pub fn unquote(gpa: std.mem.Allocator, text: []const u8, out: *std.ArrayList(u8)) std.mem.Allocator.Error!?Unquoted { if (text.len == 0 or text[0] != '"') return null; const start = out.items.len; var i: usize = 1; while (i < text.len) { const c = text[i]; i += 1; switch (c) { '"' => return .{ .len = i }, '\\' => { if (i >= text.len) break; const e = text[i]; i += 1; const byte: u8 = switch (e) { 'a' => 0x07, 'b' => 0x08, 't' => '\t', 'n' => '\n', 'v' => 0x0b, 'f' => 0x0c, 'r' => '\r', '"' => '"', '\\' => '\\', '0'...'3' => octal: { if (text.len - i < 2) break; const d1 = text[i]; const d2 = text[i + 1]; if (d1 < '0' or d1 > '7' or d2 < '0' or d2 > '7') break; i += 2; break :octal (e - '0') << 6 | (d1 - '0') << 3 | (d2 - '0'); }, else => break, }; try out.append(gpa, byte); }, else => try out.append(gpa, c), } } out.shrinkRetainingCapacity(start); return null;}
test "a quoted path reads back as it was written" { const gpa = std.testing.allocator; var out: std.ArrayList(u8) = .empty; defer out.deinit(gpa); const got = (try unquote(gpa, "\"caf\\303\\251\\t\\\"q\\\"\\\\\\001\\177\":rest", &out)).?; try std.testing.expectEqualStrings("caf\xc3\xa9\t\"q\"\\\x01\x7f", out.items); try std.testing.expectEqualStrings(":rest", "\"caf\\303\\251\\t\\\"q\\\"\\\\\\001\\177\":rest"[got.len..]); out.clearRetainingCapacity(); try std.testing.expectEqual(null, try unquote(gpa, "\"unterminated", &out)); try std.testing.expectEqual(null, try unquote(gpa, "\"bad \\q escape\"", &out)); try std.testing.expectEqual(null, try unquote(gpa, "plain", &out)); try std.testing.expectEqual(0, out.items.len);}
/// Writes `text` as git quotes a word for a shell in the commands it/// suggests, such as the `safe.directory` one: as it is if every byte is a/// letter, a digit or one of `@+,=:^-_./`, and otherwise in single quotes,/// with `'` written `'\''` and `!` written `'\!'`. Found by asking git/// about directories named with each punctuation character.pub fn writeShellQuoted(w: *Io.Writer, text: []const u8) Io.Writer.Error!void { const plain = for (text) |c| { if (!std.ascii.isAlphanumeric(c) and std.mem.findScalar(u8, "@+,=:^-_./", c) == null) break false; } else text.len != 0; if (plain) return w.writeAll(text); try w.writeByte('\''); for (text) |c| switch (c) { '\'' => try w.writeAll("'\\''"), '!' => try w.writeAll("'\\!'"), else => try w.writeByte(c), }; try w.writeByte('\'');}
test "words are quoted for a shell as git quotes them" { var buffer: [64]u8 = undefined; for ([_][2][]const u8{ .{ "/a/b-c_d.e@f+g,h=i:j^k", "/a/b-c_d.e@f+g,h=i:j^k" }, .{ "a b", "'a b'" }, .{ "a'b", "'a'\\''b'" }, .{ "a!b", "'a'\\!'b'" }, .{ "a~b", "'a~b'" }, }) |case| { var w: Io.Writer = .fixed(&buffer); try writeShellQuoted(&w, case[0]); try std.testing.expectEqualStrings(case[1], w.buffered()); }}