Skip to content
zit Artifact [1a19e4f49f]
Login

Artifact [1a19e4f49f]

Artifact 1a19e4f49f51ea845e388fdc9706de940a1ee362f2f66e07406965b5de019f92:


const std = @import("std");
const fast_export = @import("git_fast_export.zig");

const Limits = fast_export.Limits;
const Reader = fast_export.Reader;
const Event = fast_export.Event;

const oid = "0123456789abcdef0123456789abcdef01234567";
const second_oid = "89abcdef0123456789abcdef0123456789abcdef";

var stream_bytes: []const u8 = &.{};
var stream_split: usize = 0;
var stream_offset: usize = 0;

fn splitStream(_: *std.Io.Reader, writer: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize {
    if (stream_offset == stream_bytes.len) return error.EndOfStream;

    const boundary = if (stream_offset < stream_split) stream_split else stream_bytes.len;
    const available = boundary - stream_offset;
    const count = @min(available, @intFromEnum(limit));
    if (count == 0) return error.EndOfStream;

    try writer.writeAll(stream_bytes[stream_offset..][0..count]);
    stream_offset += count;
    return count;
}

fn initSplitInput(bytes: []const u8, split: usize, buffer: *[1]u8) std.Io.Reader {
    stream_bytes = bytes;
    stream_split = split;
    stream_offset = 0;
    return .{
        .vtable = &.{ .stream = splitStream },
        .buffer = buffer,
        .seek = 0,
        .end = 0,
    };
}

fn defaultLimits() Limits {
    return .{
        .max_line = 4096,
        .max_data = 4096,
        .max_path = 4096,
    };
}

fn expectBlobThenDone(reader: *Reader) !void {
    var blob_lease = (try reader.next()).?;
    defer blob_lease.deinit();

    switch (blob_lease.value) {
        .blob => |blob| {
            try std.testing.expectEqual(@as(u64, 1), blob.mark);
            try std.testing.expectEqualStrings(oid, blob.original_oid.?);
            try std.testing.expectEqualSlices(u8, "a\n\x00bc", blob.data);
        },
        else => return error.TestUnexpectedResult,
    }

    blob_lease.deinit();
    var done_lease = (try reader.next()).?;
    defer done_lease.deinit();
    switch (done_lease.value) {
        .done => {},
        else => return error.TestUnexpectedResult,
    }
    done_lease.deinit();
    try std.testing.expect((try reader.next()) == null);
}

test "git fast-export blob framing survives every split position" {
    const stream = "feature done\n" ++
        "blob\n" ++
        "mark :1\n" ++
        "original-oid " ++ oid ++ "\n" ++
        "data 5\n" ++
        "a\n\x00bc\n" ++
        "done\n";

    for (0..stream.len + 1) |split| {
        var input_buffer: [1]u8 = undefined;
        var reader = Reader.init(std.testing.allocator, initSplitInput(stream, split, &input_buffer), defaultLimits());
        defer reader.deinit();
        try expectBlobThenDone(&reader);
    }
}

test "git fast-export leases block advancement and release event storage" {
    const stream = "feature done\nblob\nmark :1\ndata 1\nx\ndone\n";
    const input = std.Io.Reader.fixed(stream);
    var reader = Reader.init(std.testing.allocator, input, defaultLimits());
    defer reader.deinit();

    var lease = (try reader.next()).?;
    try std.testing.expectError(error.ActiveLease, reader.next());
    lease.deinit();

    var done_lease = (try reader.next()).?;
    defer done_lease.deinit();
    switch (done_lease.value) {
        .done => {},
        else => return error.TestUnexpectedResult,
    }
}

test "git fast-export skips top-level progress directives" {
    const stream = "feature done\n" ++
        "progress before blob\n" ++
        "blob\nmark :1\ndata 0\n\n" ++
        "progress after blob\n" ++
        "done\n";
    const input = std.Io.Reader.fixed(stream);
    var reader = Reader.init(std.testing.allocator, input, defaultLimits());
    defer reader.deinit();

    var blob_lease = (try reader.next()).?;
    switch (blob_lease.value) {
        .blob => {},
        else => return error.TestUnexpectedResult,
    }
    blob_lease.deinit();

    var done_lease = (try reader.next()).?;
    defer done_lease.deinit();
    switch (done_lease.value) {
        .done => {},
        else => return error.TestUnexpectedResult,
    }
    done_lease.deinit();
    try std.testing.expect((try reader.next()) == null);
}

test "git fast-export commit decodes identities modes paths and ordered commands" {
    const stream = "feature done\n" ++
        "commit refs/heads/main\n" ++
        "mark :2\n" ++
        "original-oid " ++ oid ++ "\n" ++
        "author Alice Example <[email protected]> 1700000000 -0230\n" ++
        "committer Bob Example <[email protected]> 1700000001 +0530\n" ++
        "data 3\n" ++
        "msg\n" ++
        "from :1\n" ++
        "merge " ++ second_oid ++ "\n" ++
        "R \"old\\\\path\" \"space path\\t\\377\"\n" ++
        "M 100755 :2 \"space path\\t\\377\"\n" ++
        "M 100644 :1 plain\n" ++
        "M 120000 :3 link\n" ++
        "D -leading-dash\n" ++
        "D \"line\\nfeed\"\n" ++
        "D \"bells\\a\\v\"\n" ++
        "C plain copy\n" ++
        "deleteall\n" ++
        "\n" ++
        "done\n";

    const input = std.Io.Reader.fixed(stream);
    var reader = Reader.init(std.testing.allocator, input, defaultLimits());
    defer reader.deinit();

    var lease = (try reader.next()).?;
    defer lease.deinit();
    switch (lease.value) {
        .commit => |commit| {
            try std.testing.expectEqualStrings("refs/heads/main", commit.ref);
            try std.testing.expectEqual(@as(u64, 2), commit.mark);
            try std.testing.expectEqualStrings(oid, commit.original_oid.?);
            try std.testing.expectEqualStrings("Alice Example", commit.author.name);
            try std.testing.expectEqualStrings("[email protected]", commit.author.email);
            try std.testing.expectEqual(@as(i64, 1700000000), commit.author.timestamp);
            try std.testing.expectEqual(@as(i16, -150), commit.author.timezone_offset_minutes);
            try std.testing.expectEqualStrings("Bob Example", commit.committer.name);
            try std.testing.expectEqual(@as(i16, 330), commit.committer.timezone_offset_minutes);
            try std.testing.expectEqualSlices(u8, "msg", commit.message);
            try std.testing.expectEqual(@as(u64, 1), commit.from.?.mark);
            try std.testing.expectEqual(@as(usize, 1), commit.merges.len);
            try std.testing.expectEqualStrings(second_oid, commit.merges[0].oid);
            try std.testing.expectEqual(@as(usize, 9), commit.commands.len);

            switch (commit.commands[0]) {
                .rename => |rename| {
                    try std.testing.expectEqualStrings("old\\path", rename.old);
                    try std.testing.expectEqualSlices(u8, "space path\t\xff", rename.new);
                },
                else => return error.TestUnexpectedResult,
            }
            switch (commit.commands[1]) {
                .modify => |modify| {
                    try std.testing.expectEqual(.executable, modify.mode);
                    try std.testing.expectEqual(@as(u64, 2), modify.data.mark);
                    try std.testing.expectEqualSlices(u8, "space path\t\xff", modify.path);
                },
                else => return error.TestUnexpectedResult,
            }
            switch (commit.commands[5]) {
                .delete => |path| try std.testing.expectEqualSlices(u8, "line\nfeed", path),
                else => return error.TestUnexpectedResult,
            }
            switch (commit.commands[6]) {
                .delete => |path| try std.testing.expectEqualSlices(u8, "bells\x07\x0b", path),
                else => return error.TestUnexpectedResult,
            }
            switch (commit.commands[8]) {
                .delete_all => {},
                else => return error.TestUnexpectedResult,
            }
        },
        else => return error.TestUnexpectedResult,
    }
}

test "git fast-export reset and tag retain metadata" {
    const stream = "feature done\n" ++
        "reset refs/heads/main\n" ++
        "from :2\n\n" ++
        "tag v1.0\n" ++
        "mark :3\n" ++
        "from :2\n" ++
        "original-oid " ++ oid ++ "\n" ++
        "tagger Release Bot <[email protected]> 1700000002 +0000\n" ++
        "data 3\n" ++
        "tag\n" ++
        "done\n";

    const input = std.Io.Reader.fixed(stream);
    var reader = Reader.init(std.testing.allocator, input, defaultLimits());
    defer reader.deinit();

    var reset_lease = (try reader.next()).?;
    defer reset_lease.deinit();
    switch (reset_lease.value) {
        .reset => |reset| {
            try std.testing.expectEqualStrings("refs/heads/main", reset.ref);
            try std.testing.expectEqual(@as(u64, 2), reset.target.?.mark);
        },
        else => return error.TestUnexpectedResult,
    }
    reset_lease.deinit();

    var tag_lease = (try reader.next()).?;
    defer tag_lease.deinit();
    switch (tag_lease.value) {
        .tag => |tag| {
            try std.testing.expectEqualStrings("v1.0", tag.name);
            try std.testing.expectEqual(@as(u64, 3), tag.mark.?);
            try std.testing.expectEqual(@as(u64, 2), tag.target.mark);
            try std.testing.expectEqualStrings(oid, tag.original_oid.?);
            try std.testing.expectEqualStrings("Release Bot", tag.tagger.name);
            try std.testing.expectEqualSlices(u8, "tag", tag.message);
        },
        else => return error.TestUnexpectedResult,
    }
}

fn expectNextError(bytes: []const u8, limits: Limits, expected: anyerror) !void {
    const input = std.Io.Reader.fixed(bytes);
    var reader = Reader.init(std.testing.allocator, input, limits);
    defer reader.deinit();
    try std.testing.expectError(expected, reader.next());
}

test "git fast-export rejects malformed framing and configured bounds" {
    try expectNextError("feature done\nblob\nmark :1\ndata -1\n", defaultLimits(), error.Malformed);
    try expectNextError("feature done\nblob\nmark :1\ndata 18446744073709551616\n", defaultLimits(), error.Malformed);
    try expectNextError("feature done\nblob\nmark :1\ndata 1\nx", defaultLimits(), error.UnexpectedEof);
    try expectNextError("feature done\nblob\nmark :1\ndata 1\nxx", defaultLimits(), error.Malformed);
    try expectNextError("feature done\nblob\nmark :x\ndata 0\n\n", defaultLimits(), error.Malformed);
    try expectNextError("feature relative-marks\n", defaultLimits(), error.UnsupportedFeature);
    try expectNextError("feature done\nblob\nmark :1\ndata 2\nxx\n", .{ .max_line = 64, .max_data = 1, .max_path = 64 }, error.LimitExceeded);
    try expectNextError("feature done\n", .{ .max_line = 3, .max_data = 64, .max_path = 64 }, error.LimitExceeded);
    try expectNextError(
        "feature done\ncommit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +0000\ncommitter C <[email protected]> 2 +0000\ndata 0\n\nD \"\\400\"\n\n",
        defaultLimits(),
        error.Malformed,
    );
    try expectNextError(
        "feature done\ncommit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +0000\ncommitter C <[email protected]> 2 +0000\ndata 0\n\nD four\n\n",
        .{ .max_line = 128, .max_data = 64, .max_path = 3 },
        error.LimitExceeded,
    );

    const trailing = "feature done\ndone\ntrailing";
    const input = std.Io.Reader.fixed(trailing);
    var reader = Reader.init(std.testing.allocator, input, defaultLimits());
    defer reader.deinit();
    var done_lease = (try reader.next()).?;
    done_lease.deinit();
    try std.testing.expectError(error.TrailingData, reader.next());
}

test "git fast-export requires a terminal done followed by eof" {
    const stream = "feature done\nblob\nmark :1\ndata 0\n\n";
    const input = std.Io.Reader.fixed(stream);
    var reader = Reader.init(std.testing.allocator, input, defaultLimits());
    defer reader.deinit();

    var blob_lease = (try reader.next()).?;
    blob_lease.deinit();
    try std.testing.expectError(error.MissingDone, reader.next());
}

test "git fast-export rejects invalid commit semantics at the grammar boundary" {
    const prefix = "feature done\ncommit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +0000\ncommitter C <[email protected]> 2 +0000\ndata 0\n\n";
    try expectNextError(prefix ++ "M 160000 :1 submodule\n\n", defaultLimits(), error.UnsupportedMode);
    try expectNextError(prefix ++ "M 100600 :1 unknown\n\n", defaultLimits(), error.UnknownMode);
    try expectNextError(prefix ++ "M 100644 nonsense file\n\n", defaultLimits(), error.Malformed);
    try expectNextError("feature done\ncommit refs/heads/main\nmark :2\nauthor A <[email protected]> 999999999999999999999999 +0000\n", defaultLimits(), error.Malformed);
    try expectNextError("feature done\ncommit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +2460\n", defaultLimits(), error.Malformed);
    try expectNextError("feature done\ncommit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +0000\ncommitter C <[email protected]> 2 +0000\n\n", defaultLimits(), error.Malformed);
    try expectNextError("feature done\ncommit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +0000\ncommitter C <[email protected]> 2 +0000\ndata 0\n", defaultLimits(), error.UnexpectedEof);
}

test "git fast-export leaves mark declaration and reference resolution to the importer" {
    const stream = "feature done\n" ++
        "blob\nmark :1\ndata 0\n\n" ++
        "blob\nmark :1\ndata 0\n\n" ++
        "commit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +0000\ncommitter C <[email protected]> 2 +0000\ndata 0\n\nfrom :999\n\n" ++
        "done\n";
    const input = std.Io.Reader.fixed(stream);
    var reader = Reader.init(std.testing.allocator, input, defaultLimits());
    defer reader.deinit();

    for (0..2) |_| {
        var blob_lease = (try reader.next()).?;
        switch (blob_lease.value) {
            .blob => {},
            else => return error.TestUnexpectedResult,
        }
        blob_lease.deinit();
    }
    var commit_lease = (try reader.next()).?;
    defer commit_lease.deinit();
    switch (commit_lease.value) {
        .commit => |commit| try std.testing.expectEqual(@as(u64, 999), commit.from.?.mark),
        else => return error.TestUnexpectedResult,
    }
}