Commit 5217da57fc

Ian Johnson <ian@ianjohnson.dev>
2024-11-20 06:47:00
zig fetch: support SHA-256 Git repositories
Closes #21888
1 parent 62cc81a
src/Package/Fetch/git/testdata/testrepo.idx → src/Package/Fetch/git/testdata/testrepo-sha1.idx
File renamed without changes
src/Package/Fetch/git/testdata/testrepo.pack → src/Package/Fetch/git/testdata/testrepo-sha1.pack
File renamed without changes
src/Package/Fetch/git/testdata/testrepo-sha256.idx
Binary file
src/Package/Fetch/git/testdata/testrepo-sha256.pack
Binary file
src/Package/Fetch/git.zig
@@ -9,32 +9,133 @@ const mem = std.mem;
 const testing = std.testing;
 const Allocator = mem.Allocator;
 const Sha1 = std.crypto.hash.Sha1;
+const Sha256 = std.crypto.hash.sha2.Sha256;
 const assert = std.debug.assert;
 
-pub const oid_length = Sha1.digest_length;
-pub const fmt_oid_length = 2 * oid_length;
-/// The ID of a Git object (an SHA-1 hash).
-pub const Oid = [oid_length]u8;
+/// The ID of a Git object.
+pub const Oid = union(Format) {
+    sha1: [Sha1.digest_length]u8,
+    sha256: [Sha256.digest_length]u8,
 
-pub fn parseOid(s: []const u8) !Oid {
-    if (s.len != fmt_oid_length) return error.InvalidOid;
-    var oid: Oid = undefined;
-    for (&oid, 0..) |*b, i| {
-        b.* = std.fmt.parseUnsigned(u8, s[2 * i ..][0..2], 16) catch return error.InvalidOid;
+    pub const max_formatted_length = len: {
+        var max: usize = 0;
+        for (std.enums.values(Format)) |f| {
+            max = @max(max, f.formattedLength());
+        }
+        break :len max;
+    };
+
+    pub const Format = enum {
+        sha1,
+        sha256,
+
+        pub fn byteLength(f: Format) usize {
+            return switch (f) {
+                .sha1 => Sha1.digest_length,
+                .sha256 => Sha256.digest_length,
+            };
+        }
+
+        pub fn formattedLength(f: Format) usize {
+            return 2 * f.byteLength();
+        }
+    };
+
+    const Hasher = union(Format) {
+        sha1: Sha1,
+        sha256: Sha256,
+
+        fn init(oid_format: Format) Hasher {
+            return switch (oid_format) {
+                .sha1 => .{ .sha1 = Sha1.init(.{}) },
+                .sha256 => .{ .sha256 = Sha256.init(.{}) },
+            };
+        }
+
+        // Must be public for use from HashedReader and HashedWriter.
+        pub fn update(hasher: *Hasher, b: []const u8) void {
+            switch (hasher.*) {
+                inline else => |*inner| inner.update(b),
+            }
+        }
+
+        fn finalResult(hasher: *Hasher) Oid {
+            return switch (hasher.*) {
+                inline else => |*inner, tag| @unionInit(Oid, @tagName(tag), inner.finalResult()),
+            };
+        }
+    };
+
+    pub fn fromBytes(oid_format: Format, bytes: []const u8) Oid {
+        assert(bytes.len == oid_format.byteLength());
+        return switch (oid_format) {
+            inline else => |tag| @unionInit(Oid, @tagName(tag), bytes[0..comptime tag.byteLength()].*),
+        };
     }
-    return oid;
-}
 
-test parseOid {
-    try testing.expectEqualSlices(
-        u8,
-        &.{ 0xCE, 0x91, 0x9C, 0xCF, 0x45, 0x95, 0x18, 0x56, 0xA7, 0x62, 0xFF, 0xDB, 0x8E, 0xF8, 0x50, 0x30, 0x1C, 0xD8, 0xC5, 0x88 },
-        &try parseOid("ce919ccf45951856a762ffdb8ef850301cd8c588"),
-    );
-    try testing.expectError(error.InvalidOid, parseOid("ce919ccf"));
-    try testing.expectError(error.InvalidOid, parseOid("master"));
-    try testing.expectError(error.InvalidOid, parseOid("HEAD"));
-}
+    pub fn readBytes(oid_format: Format, reader: anytype) @TypeOf(reader).NoEofError!Oid {
+        return switch (oid_format) {
+            inline else => |tag| @unionInit(Oid, @tagName(tag), try reader.readBytesNoEof(tag.byteLength())),
+        };
+    }
+
+    pub fn parse(oid_format: Format, s: []const u8) error{InvalidOid}!Oid {
+        switch (oid_format) {
+            inline else => |tag| {
+                if (s.len != tag.formattedLength()) return error.InvalidOid;
+                var bytes: [tag.byteLength()]u8 = undefined;
+                for (&bytes, 0..) |*b, i| {
+                    b.* = std.fmt.parseUnsigned(u8, s[2 * i ..][0..2], 16) catch return error.InvalidOid;
+                }
+                return @unionInit(Oid, @tagName(tag), bytes);
+            },
+        }
+    }
+
+    test parse {
+        try testing.expectEqualSlices(
+            u8,
+            &.{ 0xCE, 0x91, 0x9C, 0xCF, 0x45, 0x95, 0x18, 0x56, 0xA7, 0x62, 0xFF, 0xDB, 0x8E, 0xF8, 0x50, 0x30, 0x1C, 0xD8, 0xC5, 0x88 },
+            &(try parse(.sha1, "ce919ccf45951856a762ffdb8ef850301cd8c588")).sha1,
+        );
+        try testing.expectError(error.InvalidOid, parse(.sha256, "ce919ccf45951856a762ffdb8ef850301cd8c588"));
+        try testing.expectError(error.InvalidOid, parse(.sha1, "7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a"));
+        try testing.expectEqualSlices(
+            u8,
+            &.{ 0x7F, 0x44, 0x4A, 0x92, 0xBD, 0x45, 0x72, 0xEE, 0x4A, 0x28, 0xB2, 0xC6, 0x30, 0x59, 0x92, 0x4A, 0x9C, 0xA1, 0x82, 0x91, 0x38, 0x55, 0x3E, 0xF3, 0xE7, 0xC4, 0x1E, 0xE1, 0x59, 0xAF, 0xAE, 0x7A },
+            &(try parse(.sha256, "7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a")).sha256,
+        );
+        try testing.expectError(error.InvalidOid, parse(.sha1, "ce919ccf"));
+        try testing.expectError(error.InvalidOid, parse(.sha256, "ce919ccf"));
+        try testing.expectError(error.InvalidOid, parse(.sha1, "master"));
+        try testing.expectError(error.InvalidOid, parse(.sha256, "master"));
+        try testing.expectError(error.InvalidOid, parse(.sha1, "HEAD"));
+        try testing.expectError(error.InvalidOid, parse(.sha256, "HEAD"));
+    }
+
+    pub fn parseAny(s: []const u8) error{InvalidOid}!Oid {
+        return for (std.enums.values(Format)) |f| {
+            if (s.len == f.formattedLength()) break parse(f, s);
+        } else error.InvalidOid;
+    }
+
+    pub fn format(
+        oid: Oid,
+        comptime fmt: []const u8,
+        options: std.fmt.FormatOptions,
+        writer: anytype,
+    ) @TypeOf(writer).Error!void {
+        _ = fmt;
+        _ = options;
+        try writer.print("{}", .{std.fmt.fmtSliceHexLower(oid.slice())});
+    }
+
+    pub fn slice(oid: *const Oid) []const u8 {
+        return switch (oid.*) {
+            inline else => |*bytes| bytes,
+        };
+    }
+};
 
 pub const Diagnostics = struct {
     allocator: Allocator,
@@ -72,8 +173,8 @@ pub const Diagnostics = struct {
 pub const Repository = struct {
     odb: Odb,
 
-    pub fn init(allocator: Allocator, pack_file: std.fs.File, index_file: std.fs.File) !Repository {
-        return .{ .odb = try Odb.init(allocator, pack_file, index_file) };
+    pub fn init(allocator: Allocator, format: Oid.Format, pack_file: std.fs.File, index_file: std.fs.File) !Repository {
+        return .{ .odb = try Odb.init(allocator, format, pack_file, index_file) };
     }
 
     pub fn deinit(repository: *Repository) void {
@@ -92,7 +193,7 @@ pub const Repository = struct {
         const tree_oid = tree_oid: {
             const commit_object = try repository.odb.readObject();
             if (commit_object.type != .commit) return error.NotACommit;
-            break :tree_oid try getCommitTree(commit_object.data);
+            break :tree_oid try getCommitTree(repository.odb.format, commit_object.data);
         };
         try repository.checkoutTree(worktree, tree_oid, "", diagnostics);
     }
@@ -114,7 +215,11 @@ pub const Repository = struct {
         const tree_data = try repository.odb.allocator.dupe(u8, tree_object.data);
         defer repository.odb.allocator.free(tree_data);
 
-        var tree_iter: TreeIterator = .{ .data = tree_data };
+        var tree_iter: TreeIterator = .{
+            .format = repository.odb.format,
+            .data = tree_data,
+            .pos = 0,
+        };
         while (try tree_iter.next()) |entry| {
             switch (entry.type) {
                 .directory => {
@@ -170,19 +275,20 @@ pub const Repository = struct {
 
     /// Returns the ID of the tree associated with the given commit (provided as
     /// raw object data).
-    fn getCommitTree(commit_data: []const u8) !Oid {
+    fn getCommitTree(format: Oid.Format, commit_data: []const u8) !Oid {
         if (!mem.startsWith(u8, commit_data, "tree ") or
-            commit_data.len < "tree ".len + fmt_oid_length + "\n".len or
-            commit_data["tree ".len + fmt_oid_length] != '\n')
+            commit_data.len < "tree ".len + format.formattedLength() + "\n".len or
+            commit_data["tree ".len + format.formattedLength()] != '\n')
         {
             return error.InvalidCommit;
         }
-        return try parseOid(commit_data["tree ".len..][0..fmt_oid_length]);
+        return try .parse(format, commit_data["tree ".len..][0..format.formattedLength()]);
     }
 
     const TreeIterator = struct {
+        format: Oid.Format,
         data: []const u8,
-        pos: usize = 0,
+        pos: usize,
 
         const Entry = struct {
             type: Type,
@@ -220,8 +326,9 @@ pub const Repository = struct {
             const name = iterator.data[iterator.pos..name_end :0];
             iterator.pos = name_end + 1;
 
+            const oid_length = iterator.format.byteLength();
             if (iterator.pos + oid_length > iterator.data.len) return error.InvalidTree;
-            const oid = iterator.data[iterator.pos..][0..oid_length].*;
+            const oid: Oid = .fromBytes(iterator.format, iterator.data[iterator.pos..][0..oid_length]);
             iterator.pos += oid_length;
 
             return .{ .type = @"type", .executable = executable, .name = name, .oid = oid };
@@ -235,6 +342,7 @@ pub const Repository = struct {
 /// The format of the packfile and its associated index are documented in
 /// [pack-format](https://git-scm.com/docs/pack-format).
 const Odb = struct {
+    format: Oid.Format,
     pack_file: std.fs.File,
     index_header: IndexHeader,
     index_file: std.fs.File,
@@ -242,11 +350,12 @@ const Odb = struct {
     allocator: Allocator,
 
     /// Initializes the database from open pack and index files.
-    fn init(allocator: Allocator, pack_file: std.fs.File, index_file: std.fs.File) !Odb {
+    fn init(allocator: Allocator, format: Oid.Format, pack_file: std.fs.File, index_file: std.fs.File) !Odb {
         try pack_file.seekTo(0);
         try index_file.seekTo(0);
         const index_header = try IndexHeader.read(index_file.reader());
         return .{
+            .format = format,
             .pack_file = pack_file,
             .index_header = index_header,
             .index_file = index_file,
@@ -268,7 +377,7 @@ const Odb = struct {
         const base_object = while (true) {
             if (odb.cache.get(base_offset)) |base_object| break base_object;
 
-            base_header = try EntryHeader.read(odb.pack_file.reader());
+            base_header = try EntryHeader.read(odb.format, odb.pack_file.reader());
             switch (base_header) {
                 .ofs_delta => |ofs_delta| {
                     try delta_offsets.append(odb.allocator, base_offset);
@@ -292,6 +401,7 @@ const Odb = struct {
 
         const base_data = try resolveDeltaChain(
             odb.allocator,
+            odb.format,
             odb.pack_file,
             base_object,
             delta_offsets.items,
@@ -303,14 +413,15 @@ const Odb = struct {
 
     /// Seeks to the beginning of the object with the given ID.
     fn seekOid(odb: *Odb, oid: Oid) !void {
-        const key = oid[0];
+        const oid_length = odb.format.byteLength();
+        const key = oid.slice()[0];
         var start_index = if (key > 0) odb.index_header.fan_out_table[key - 1] else 0;
         var end_index = odb.index_header.fan_out_table[key];
         const found_index = while (start_index < end_index) {
             const mid_index = start_index + (end_index - start_index) / 2;
             try odb.index_file.seekTo(IndexHeader.size + mid_index * oid_length);
-            const mid_oid = try odb.index_file.reader().readBytesNoEof(oid_length);
-            switch (mem.order(u8, &mid_oid, &oid)) {
+            const mid_oid = try Oid.readBytes(odb.format, odb.index_file.reader());
+            switch (mem.order(u8, mid_oid.slice(), oid.slice())) {
                 .lt => start_index = mid_index + 1,
                 .gt => end_index = mid_index,
                 .eq => break mid_index,
@@ -495,6 +606,7 @@ pub const Session = struct {
     location: Location,
     supports_agent: bool,
     supports_shallow: bool,
+    object_format: Oid.Format,
     allocator: Allocator,
 
     const agent = "zig/" ++ @import("builtin").zig_version_string;
@@ -513,6 +625,7 @@ pub const Session = struct {
             .location = try .init(allocator, uri),
             .supports_agent = false,
             .supports_shallow = false,
+            .object_format = .sha1,
             .allocator = allocator,
         };
         errdefer session.deinit();
@@ -528,6 +641,10 @@ pub const Session = struct {
                         session.supports_shallow = true;
                     }
                 }
+            } else if (mem.eql(u8, capability.key, "object-format")) {
+                if (std.meta.stringToEnum(Oid.Format, capability.value orelse continue)) |format| {
+                    session.object_format = format;
+                }
             }
         }
         return session;
@@ -708,6 +825,11 @@ pub const Session = struct {
         if (session.supports_agent) {
             try Packet.write(.{ .data = agent_capability }, body_writer);
         }
+        {
+            const object_format_packet = try std.fmt.allocPrint(session.allocator, "object-format={s}\n", .{@tagName(session.object_format)});
+            defer session.allocator.free(object_format_packet);
+            try Packet.write(.{ .data = object_format_packet }, body_writer);
+        }
         try Packet.write(.delimiter, body_writer);
         for (options.ref_prefixes) |ref_prefix| {
             const ref_prefix_packet = try std.fmt.allocPrint(session.allocator, "ref-prefix {s}\n", .{ref_prefix});
@@ -739,10 +861,14 @@ pub const Session = struct {
         try request.wait();
         if (request.response.status != .ok) return error.ProtocolError;
 
-        return .{ .request = request };
+        return .{
+            .format = session.object_format,
+            .request = request,
+        };
     }
 
     pub const RefIterator = struct {
+        format: Oid.Format,
         request: std.http.Client.Request,
         buf: [Packet.max_data_length]u8 = undefined,
 
@@ -764,7 +890,7 @@ pub const Session = struct {
                 .data => |data| {
                     const ref_data = Packet.normalizeText(data);
                     const oid_sep_pos = mem.indexOfScalar(u8, ref_data, ' ') orelse return error.InvalidRefPacket;
-                    const oid = parseOid(data[0..oid_sep_pos]) catch return error.InvalidRefPacket;
+                    const oid = Oid.parse(iterator.format, data[0..oid_sep_pos]) catch return error.InvalidRefPacket;
 
                     const name_sep_pos = mem.indexOfScalarPos(u8, ref_data, oid_sep_pos + 1, ' ') orelse ref_data.len;
                     const name = ref_data[oid_sep_pos + 1 .. name_sep_pos];
@@ -778,7 +904,7 @@ pub const Session = struct {
                         if (mem.startsWith(u8, attribute, "symref-target:")) {
                             symref_target = attribute["symref-target:".len..];
                         } else if (mem.startsWith(u8, attribute, "peeled:")) {
-                            peeled = parseOid(attribute["peeled:".len..]) catch return error.InvalidRefPacket;
+                            peeled = Oid.parse(iterator.format, attribute["peeled:".len..]) catch return error.InvalidRefPacket;
                         }
                         last_sep_pos = next_sep_pos;
                     }
@@ -814,6 +940,11 @@ pub const Session = struct {
         if (session.supports_agent) {
             try Packet.write(.{ .data = agent_capability }, body_writer);
         }
+        {
+            const object_format_packet = try std.fmt.allocPrint(session.allocator, "object-format={s}\n", .{@tagName(session.object_format)});
+            defer session.allocator.free(object_format_packet);
+            try Packet.write(.{ .data = object_format_packet }, body_writer);
+        }
         try Packet.write(.delimiter, body_writer);
         // Our packfile parser supports the OFS_DELTA object type
         try Packet.write(.{ .data = "ofs-delta\n" }, body_writer);
@@ -997,7 +1128,7 @@ const EntryHeader = union(Type) {
         };
     }
 
-    fn read(reader: anytype) !EntryHeader {
+    fn read(format: Oid.Format, reader: anytype) !EntryHeader {
         const InitialByte = packed struct { len: u4, type: u3, has_next: bool };
         const initial: InitialByte = @bitCast(reader.readByte() catch |e| switch (e) {
             error.EndOfStream => return error.InvalidFormat,
@@ -1016,7 +1147,7 @@ const EntryHeader = union(Type) {
                 .uncompressed_length = uncompressed_length,
             } },
             .ref_delta => .{ .ref_delta = .{
-                .base_object = reader.readBytesNoEof(oid_length) catch |e| switch (e) {
+                .base_object = Oid.readBytes(format, reader) catch |e| switch (e) {
                     error.EndOfStream => return error.InvalidFormat,
                     else => |other| return other,
                 },
@@ -1081,7 +1212,7 @@ const IndexEntry = struct {
 
 /// Writes out a version 2 index for the given packfile, as documented in
 /// [pack-format](https://git-scm.com/docs/pack-format).
-pub fn indexPack(allocator: Allocator, pack: std.fs.File, index_writer: anytype) !void {
+pub fn indexPack(allocator: Allocator, format: Oid.Format, pack: std.fs.File, index_writer: anytype) !void {
     try pack.seekTo(0);
 
     var index_entries: std.AutoHashMapUnmanaged(Oid, IndexEntry) = .empty;
@@ -1089,7 +1220,7 @@ pub fn indexPack(allocator: Allocator, pack: std.fs.File, index_writer: anytype)
     var pending_deltas: std.ArrayListUnmanaged(IndexEntry) = .empty;
     defer pending_deltas.deinit(allocator);
 
-    const pack_checksum = try indexPackFirstPass(allocator, pack, &index_entries, &pending_deltas);
+    const pack_checksum = try indexPackFirstPass(allocator, format, pack, &index_entries, &pending_deltas);
 
     var cache: ObjectCache = .{};
     defer cache.deinit(allocator);
@@ -1099,7 +1230,7 @@ pub fn indexPack(allocator: Allocator, pack: std.fs.File, index_writer: anytype)
         while (i > 0) {
             i -= 1;
             const delta = pending_deltas.items[i];
-            if (try indexPackHashDelta(allocator, pack, delta, index_entries, &cache)) |oid| {
+            if (try indexPackHashDelta(allocator, format, pack, delta, index_entries, &cache)) |oid| {
                 try index_entries.put(allocator, oid, delta);
                 _ = pending_deltas.swapRemove(i);
             }
@@ -1117,7 +1248,7 @@ pub fn indexPack(allocator: Allocator, pack: std.fs.File, index_writer: anytype)
     }
     mem.sortUnstable(Oid, oids.items, {}, struct {
         fn lessThan(_: void, o1: Oid, o2: Oid) bool {
-            return mem.lessThan(u8, &o1, &o2);
+            return mem.lessThan(u8, o1.slice(), o2.slice());
         }
     }.lessThan);
 
@@ -1125,15 +1256,16 @@ pub fn indexPack(allocator: Allocator, pack: std.fs.File, index_writer: anytype)
     var count: u32 = 0;
     var fan_out_index: u8 = 0;
     for (oids.items) |oid| {
-        if (oid[0] > fan_out_index) {
-            @memset(fan_out_table[fan_out_index..oid[0]], count);
-            fan_out_index = oid[0];
+        const key = oid.slice()[0];
+        if (key > fan_out_index) {
+            @memset(fan_out_table[fan_out_index..key], count);
+            fan_out_index = key;
         }
         count += 1;
     }
     @memset(fan_out_table[fan_out_index..], count);
 
-    var index_hashed_writer = std.compress.hashedWriter(index_writer, Sha1.init(.{}));
+    var index_hashed_writer = std.compress.hashedWriter(index_writer, Oid.Hasher.init(format));
     const writer = index_hashed_writer.writer();
     try writer.writeAll(IndexHeader.signature);
     try writer.writeInt(u32, IndexHeader.supported_version, .big);
@@ -1142,7 +1274,7 @@ pub fn indexPack(allocator: Allocator, pack: std.fs.File, index_writer: anytype)
     }
 
     for (oids.items) |oid| {
-        try writer.writeAll(&oid);
+        try writer.writeAll(oid.slice());
     }
 
     for (oids.items) |oid| {
@@ -1165,9 +1297,9 @@ pub fn indexPack(allocator: Allocator, pack: std.fs.File, index_writer: anytype)
         try writer.writeInt(u64, offset, .big);
     }
 
-    try writer.writeAll(&pack_checksum);
+    try writer.writeAll(pack_checksum.slice());
     const index_checksum = index_hashed_writer.hasher.finalResult();
-    try index_writer.writeAll(&index_checksum);
+    try index_writer.writeAll(index_checksum.slice());
 }
 
 /// Performs the first pass over the packfile data for index construction.
@@ -1176,13 +1308,14 @@ pub fn indexPack(allocator: Allocator, pack: std.fs.File, index_writer: anytype)
 /// format).
 fn indexPackFirstPass(
     allocator: Allocator,
+    format: Oid.Format,
     pack: std.fs.File,
     index_entries: *std.AutoHashMapUnmanaged(Oid, IndexEntry),
     pending_deltas: *std.ArrayListUnmanaged(IndexEntry),
-) ![Sha1.digest_length]u8 {
+) !Oid {
     var pack_buffered_reader = std.io.bufferedReader(pack.reader());
     var pack_counting_reader = std.io.countingReader(pack_buffered_reader.reader());
-    var pack_hashed_reader = std.compress.hashedReader(pack_counting_reader.reader(), Sha1.init(.{}));
+    var pack_hashed_reader = std.compress.hashedReader(pack_counting_reader.reader(), Oid.Hasher.init(format));
     const pack_reader = pack_hashed_reader.reader();
 
     const pack_header = try PackHeader.read(pack_reader);
@@ -1191,12 +1324,12 @@ fn indexPackFirstPass(
     while (current_entry < pack_header.total_objects) : (current_entry += 1) {
         const entry_offset = pack_counting_reader.bytes_read;
         var entry_crc32_reader = std.compress.hashedReader(pack_reader, std.hash.Crc32.init());
-        const entry_header = try EntryHeader.read(entry_crc32_reader.reader());
+        const entry_header = try EntryHeader.read(format, entry_crc32_reader.reader());
         switch (entry_header) {
             .commit, .tree, .blob, .tag => |object| {
                 var entry_decompress_stream = std.compress.zlib.decompressor(entry_crc32_reader.reader());
                 var entry_counting_reader = std.io.countingReader(entry_decompress_stream.reader());
-                var entry_hashed_writer = std.compress.hashedWriter(std.io.null_writer, Sha1.init(.{}));
+                var entry_hashed_writer = std.compress.hashedWriter(std.io.null_writer, Oid.Hasher.init(format));
                 const entry_writer = entry_hashed_writer.writer();
                 // The object header is not included in the pack data but is
                 // part of the object's ID
@@ -1229,8 +1362,8 @@ fn indexPackFirstPass(
     }
 
     const pack_checksum = pack_hashed_reader.hasher.finalResult();
-    const recorded_checksum = try pack_buffered_reader.reader().readBytesNoEof(Sha1.digest_length);
-    if (!mem.eql(u8, &pack_checksum, &recorded_checksum)) {
+    const recorded_checksum = try Oid.readBytes(format, pack_buffered_reader.reader());
+    if (!mem.eql(u8, pack_checksum.slice(), recorded_checksum.slice())) {
         return error.CorruptedPack;
     }
     _ = pack_reader.readByte() catch |e| switch (e) {
@@ -1245,6 +1378,7 @@ fn indexPackFirstPass(
 /// delta and we do not yet know the offset of the base object).
 fn indexPackHashDelta(
     allocator: Allocator,
+    format: Oid.Format,
     pack: std.fs.File,
     delta: IndexEntry,
     index_entries: std.AutoHashMapUnmanaged(Oid, IndexEntry),
@@ -1259,7 +1393,7 @@ fn indexPackHashDelta(
         if (cache.get(base_offset)) |base_object| break base_object;
 
         try pack.seekTo(base_offset);
-        base_header = try EntryHeader.read(pack.reader());
+        base_header = try EntryHeader.read(format, pack.reader());
         switch (base_header) {
             .ofs_delta => |ofs_delta| {
                 try delta_offsets.append(allocator, base_offset);
@@ -1279,9 +1413,9 @@ fn indexPackHashDelta(
         }
     };
 
-    const base_data = try resolveDeltaChain(allocator, pack, base_object, delta_offsets.items, cache);
+    const base_data = try resolveDeltaChain(allocator, format, pack, base_object, delta_offsets.items, cache);
 
-    var entry_hasher = Sha1.init(.{});
+    var entry_hasher: Oid.Hasher = .init(format);
     var entry_hashed_writer = std.compress.hashedWriter(std.io.null_writer, &entry_hasher);
     try entry_hashed_writer.writer().print("{s} {}\x00", .{ @tagName(base_object.type), base_data.len });
     entry_hasher.update(base_data);
@@ -1294,6 +1428,7 @@ fn indexPackHashDelta(
 /// to obtain the final object.
 fn resolveDeltaChain(
     allocator: Allocator,
+    format: Oid.Format,
     pack: std.fs.File,
     base_object: Object,
     delta_offsets: []const u64,
@@ -1306,7 +1441,7 @@ fn resolveDeltaChain(
 
         const delta_offset = delta_offsets[i];
         try pack.seekTo(delta_offset);
-        const delta_header = try EntryHeader.read(pack.reader());
+        const delta_header = try EntryHeader.read(format, pack.reader());
         const delta_data = try readObjectRaw(allocator, pack.reader(), delta_header.uncompressedLength());
         defer allocator.free(delta_data);
         var delta_stream = std.io.fixedBufferStream(delta_data);
@@ -1394,16 +1529,22 @@ fn expandDelta(base_object: anytype, delta_reader: anytype, writer: anytype) !vo
     }
 }
 
-test "packfile indexing and checkout" {
-    // To verify the contents of this packfile without using the code in this
-    // file:
-    //
-    // 1. Create a new empty Git repository (`git init`)
-    // 2. `git unpack-objects <path/to/testdata.pack`
-    // 3. `git fsck` -> note the "dangling commit" ID (which matches the commit
-    //    checked out below)
-    // 4. `git checkout dd582c0720819ab7130b103635bd7271b9fd4feb`
-    const testrepo_pack = @embedFile("git/testdata/testrepo.pack");
+/// Runs the packfile indexing and checkout test.
+///
+/// The two testrepo repositories under testdata contain identical commit
+/// histories and contents.
+///
+/// To verify the contents of the packfiles using Git alone, run the
+/// following commands in an empty directory:
+///
+/// 1. `git init --object-format=(sha1|sha256)`
+/// 2. `git unpack-objects <path/to/testrepo.pack`
+/// 3. `git fsck` - will print one "dangling commit":
+///    - SHA-1: `dd582c0720819ab7130b103635bd7271b9fd4feb`
+///    - SHA-256: `7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a`
+/// 4. `git checkout $commit`
+fn runRepositoryTest(comptime format: Oid.Format, head_commit: []const u8) !void {
+    const testrepo_pack = @embedFile("git/testdata/testrepo-" ++ @tagName(format) ++ ".pack");
 
     var git_dir = testing.tmpDir(.{});
     defer git_dir.cleanup();
@@ -1413,27 +1554,27 @@ test "packfile indexing and checkout" {
 
     var index_file = try git_dir.dir.createFile("testrepo.idx", .{ .read = true });
     defer index_file.close();
-    try indexPack(testing.allocator, pack_file, index_file.writer());
+    try indexPack(testing.allocator, format, pack_file, index_file.writer());
 
     // Arbitrary size limit on files read while checking the repository contents
-    // (all files in the test repo are known to be much smaller than this)
-    const max_file_size = 4096;
+    // (all files in the test repo are known to be smaller than this)
+    const max_file_size = 8192;
 
     const index_file_data = try git_dir.dir.readFileAlloc(testing.allocator, "testrepo.idx", max_file_size);
     defer testing.allocator.free(index_file_data);
     // testrepo.idx is generated by Git. The index created by this file should
     // match it exactly. Running `git verify-pack -v testrepo.pack` can verify
     // this.
-    const testrepo_idx = @embedFile("git/testdata/testrepo.idx");
+    const testrepo_idx = @embedFile("git/testdata/testrepo-" ++ @tagName(format) ++ ".idx");
     try testing.expectEqualSlices(u8, testrepo_idx, index_file_data);
 
-    var repository = try Repository.init(testing.allocator, pack_file, index_file);
+    var repository = try Repository.init(testing.allocator, format, pack_file, index_file);
     defer repository.deinit();
 
     var worktree = testing.tmpDir(.{ .iterate = true });
     defer worktree.cleanup();
 
-    const commit_id = try parseOid("dd582c0720819ab7130b103635bd7271b9fd4feb");
+    const commit_id = try Oid.parse(format, head_commit);
 
     var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
     defer diagnostics.deinit();
@@ -1497,6 +1638,14 @@ test "packfile indexing and checkout" {
     try testing.expectEqualStrings(expected_file_contents, actual_file_contents);
 }
 
+test "SHA-1 packfile indexing and checkout" {
+    try runRepositoryTest(.sha1, "dd582c0720819ab7130b103635bd7271b9fd4feb");
+}
+
+test "SHA-256 packfile indexing and checkout" {
+    try runRepositoryTest(.sha256, "7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a");
+}
+
 /// Checks out a commit of a packfile. Intended for experimenting with and
 /// benchmarking possible optimizations to the indexing and checkout behavior.
 pub fn main() !void {
@@ -1504,14 +1653,16 @@ pub fn main() !void {
 
     const args = try std.process.argsAlloc(allocator);
     defer std.process.argsFree(allocator, args);
-    if (args.len != 4) {
-        return error.InvalidArguments; // Arguments: packfile commit worktree
+    if (args.len != 5) {
+        return error.InvalidArguments; // Arguments: format packfile commit worktree
     }
 
-    var pack_file = try std.fs.cwd().openFile(args[1], .{});
+    const format = std.meta.stringToEnum(Oid.Format, args[1]) orelse return error.InvalidFormat;
+
+    var pack_file = try std.fs.cwd().openFile(args[2], .{});
     defer pack_file.close();
-    const commit = try parseOid(args[2]);
-    var worktree = try std.fs.cwd().makeOpenPath(args[3], .{});
+    const commit = try Oid.parse(format, args[3]);
+    var worktree = try std.fs.cwd().makeOpenPath(args[4], .{});
     defer worktree.close();
 
     var git_dir = try worktree.makeOpenPath(".git", .{});
@@ -1521,12 +1672,12 @@ pub fn main() !void {
     var index_file = try git_dir.createFile("idx", .{ .read = true });
     defer index_file.close();
     var index_buffered_writer = std.io.bufferedWriter(index_file.writer());
-    try indexPack(allocator, pack_file, index_buffered_writer.writer());
+    try indexPack(allocator, format, pack_file, index_buffered_writer.writer());
     try index_buffered_writer.flush();
     try index_file.sync();
 
     std.debug.print("Starting checkout...\n", .{});
-    var repository = try Repository.init(allocator, pack_file, index_file);
+    var repository = try Repository.init(allocator, format, pack_file, index_file);
     defer repository.deinit();
     var diagnostics: Diagnostics = .{ .allocator = allocator };
     defer diagnostics.deinit();
src/Package/Fetch.zig
@@ -814,7 +814,7 @@ const Resource = union(enum) {
     const Git = struct {
         session: git.Session,
         fetch_stream: git.Session.FetchStream,
-        want_oid: [git.oid_length]u8,
+        want_oid: git.Oid,
     };
 
     fn deinit(resource: *Resource) void {
@@ -976,7 +976,7 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re
         const want_oid = want_oid: {
             const want_ref =
                 if (uri.fragment) |fragment| try fragment.toRawMaybeAlloc(arena) else "HEAD";
-            if (git.parseOid(want_ref)) |oid| break :want_oid oid else |_| {}
+            if (git.Oid.parseAny(want_ref)) |oid| break :want_oid oid else |_| {}
 
             const want_ref_head = try std.fmt.allocPrint(arena, "refs/heads/{s}", .{want_ref});
             const want_ref_tag = try std.fmt.allocPrint(arena, "refs/tags/{s}", .{want_ref});
@@ -1018,17 +1018,13 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re
             });
             const notes_start = try eb.reserveNotes(notes_len);
             eb.extra.items[notes_start] = @intFromEnum(try eb.addErrorMessage(.{
-                .msg = try eb.printString("try .url = \"{;+/}#{}\",", .{
-                    uri, std.fmt.fmtSliceHexLower(&want_oid),
-                }),
+                .msg = try eb.printString("try .url = \"{;+/}#{}\",", .{ uri, want_oid }),
             }));
             return error.FetchFailed;
         }
 
-        var want_oid_buf: [git.fmt_oid_length]u8 = undefined;
-        _ = std.fmt.bufPrint(&want_oid_buf, "{}", .{
-            std.fmt.fmtSliceHexLower(&want_oid),
-        }) catch unreachable;
+        var want_oid_buf: [git.Oid.max_formatted_length]u8 = undefined;
+        _ = std.fmt.bufPrint(&want_oid_buf, "{}", .{want_oid}) catch unreachable;
         var fetch_stream = session.fetch(&.{&want_oid_buf}, server_header_buffer) catch |err| {
             return f.fail(f.location_tok, try eb.printString(
                 "unable to create fetch stream: {s}",
@@ -1163,7 +1159,7 @@ fn unpackResource(
             });
             return try unpackTarball(f, tmp_directory.handle, dcp.reader());
         },
-        .git_pack => return unpackGitPack(f, tmp_directory.handle, resource) catch |err| switch (err) {
+        .git_pack => return unpackGitPack(f, tmp_directory.handle, &resource.git) catch |err| switch (err) {
             error.FetchFailed => return error.FetchFailed,
             error.OutOfMemory => return error.OutOfMemory,
             else => |e| return f.fail(f.location_tok, try eb.printString(
@@ -1298,11 +1294,10 @@ fn unzip(f: *Fetch, out_dir: fs.Dir, reader: anytype) RunError!UnpackResult {
     return res;
 }
 
-fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource) anyerror!UnpackResult {
+fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource.Git) anyerror!UnpackResult {
     const arena = f.arena.allocator();
     const gpa = f.arena.child_allocator;
-    const want_oid = resource.git.want_oid;
-    const reader = resource.git.fetch_stream.reader();
+    const object_format: git.Oid.Format = resource.want_oid;
 
     var res: UnpackResult = .{};
     // The .git directory is used to store the packfile and associated index, but
@@ -1314,7 +1309,7 @@ fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource) anyerror!Unpac
         var pack_file = try pack_dir.createFile("pkg.pack", .{ .read = true });
         defer pack_file.close();
         var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
-        try fifo.pump(reader, pack_file.writer());
+        try fifo.pump(resource.fetch_stream.reader(), pack_file.writer());
         try pack_file.sync();
 
         var index_file = try pack_dir.createFile("pkg.idx", .{ .read = true });
@@ -1323,7 +1318,7 @@ fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource) anyerror!Unpac
             const index_prog_node = f.prog_node.start("Index pack", 0);
             defer index_prog_node.end();
             var index_buffered_writer = std.io.bufferedWriter(index_file.writer());
-            try git.indexPack(gpa, pack_file, index_buffered_writer.writer());
+            try git.indexPack(gpa, object_format, pack_file, index_buffered_writer.writer());
             try index_buffered_writer.flush();
             try index_file.sync();
         }
@@ -1331,10 +1326,10 @@ fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource) anyerror!Unpac
         {
             const checkout_prog_node = f.prog_node.start("Checkout", 0);
             defer checkout_prog_node.end();
-            var repository = try git.Repository.init(gpa, pack_file, index_file);
+            var repository = try git.Repository.init(gpa, object_format, pack_file, index_file);
             defer repository.deinit();
             var diagnostics: git.Diagnostics = .{ .allocator = arena };
-            try repository.checkout(out_dir, want_oid, &diagnostics);
+            try repository.checkout(out_dir, resource.want_oid, &diagnostics);
 
             if (diagnostics.errors.items.len > 0) {
                 try res.allocErrors(arena, diagnostics.errors.items.len, "unable to unpack packfile");
src/main.zig
@@ -7037,8 +7037,8 @@ fn cmdFetch(
 
     var saved_path_or_url = path_or_url;
 
-    if (fetch.latest_commit) |*latest_commit| resolved: {
-        const latest_commit_hex = try std.fmt.allocPrint(arena, "{}", .{std.fmt.fmtSliceHexLower(latest_commit)});
+    if (fetch.latest_commit) |latest_commit| resolved: {
+        const latest_commit_hex = try std.fmt.allocPrint(arena, "{}", .{latest_commit});
 
         var uri = try std.Uri.parse(path_or_url);