//! The Git identity rule, pinned by vectors: a fixed manifest in, a fixed
//! commit ID out. A Git commit ID hashes the author and committer lines, so
//! the rule that writes them is part of every exported commit's name, and a
//! change to it would rename every commit a mirror has published.
//!
//! Each vector check-in carries no files, so its tree is Git's empty tree
//! and the commit object is the identity lines plus the message, exactly as
//! `git fast-import` writes it. The expected IDs were computed outside zit,
//! with `git hash-object -t commit` over those bytes, and cross-checked by
//! feeding the same commit to a real `git fast-import`.
const std = @import("std");
const artifact = @import("../artifact.zig");
const export_impl = @import("git_export_impl.zig");
const testing = std.testing;
/// Strip the leading `#` guard block, as `rfc_vectors.zig` does: on disk
/// each vector stays inert, since Fossil crosslinks any blob that parses as
/// an artifact into every clone of this repository.
fn vector(comptime raw: []const u8) []const u8 {
comptime {
var i: usize = 0;
while (i < raw.len and raw[i] == '#') {
while (i < raw.len and raw[i] != '\n') i += 1;
if (i < raw.len) i += 1;
}
return raw[i..];
}
}
const guarded = [_][]const u8{
@embedFile("identity_vectors/contact_login.txt"),
@embedFile("identity_vectors/plain_login.txt"),
@embedFile("identity_vectors/no_user.txt"),
@embedFile("identity_vectors/no_date.txt"),
@embedFile("identity_vectors/fractional_second.txt"),
};
const contact_login = vector(guarded[0]);
const plain_login = vector(guarded[1]);
const no_user = vector(guarded[2]);
const no_date = vector(guarded[3]);
const fractional_second = vector(guarded[4]);
const empty_tree = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
/// The commit ID Git gives a parentless commit of the empty tree.
fn commitId(identity: anytype, message: []const u8, out: *[40]u8) !void {
var buf: [512]u8 = undefined;
const line = try std.fmt.bufPrint(buf[256..], "{s} <{s}> {d} +0000", .{ identity.name, identity.email, identity.timestamp });
const body = try std.fmt.bufPrint(buf[0..256], "tree {s}\nauthor {s}\ncommitter {s}\n\n{s}", .{ empty_tree, line, line, message });
var header: [32]u8 = undefined;
const prefix = try std.fmt.bufPrint(&header, "commit {d}\x00", .{body.len});
var sha = std.crypto.hash.Sha1.init(.{});
sha.update(prefix);
sha.update(body);
var digest: [20]u8 = undefined;
sha.final(&digest);
out.* = std.fmt.bytesToHex(digest, .lower);
}
fn expectCommitId(
text: []const u8,
rule: export_impl.IdentityRule,
contacts: ?*const std.StringHashMapUnmanaged([]const u8),
want: []const u8,
) !void {
const gpa = testing.allocator;
var m = try artifact.parse(gpa, text);
defer m.deinit();
try expectManifestCommitId(&m, rule, contacts, want);
}
fn expectManifestCommitId(
m: *const artifact.Manifest,
rule: export_impl.IdentityRule,
contacts: ?*const std.StringHashMapUnmanaged([]const u8),
want: []const u8,
) !void {
var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
defer arena_state.deinit();
const identity = try export_impl.identityOf(arena_state.allocator(), m, rule, contacts);
try testing.expectEqual(@as(i16, 0), identity.timezone_offset_minutes);
const message: []const u8 = if (m.c) |c| c.comment else "";
var got: [40]u8 = undefined;
try commitId(identity, message, &got);
try testing.expectEqualStrings(want, &got);
}
const repository_rule: export_impl.IdentityRule = .{ .domain = "example.com" };
test "git identity vectors: every vector file is inert to Fossil on disk" {
for (guarded) |raw| try testing.expect(artifact.frame(raw) == null);
}
test "git identity vectors: a login with a contact is still login@DOMAIN; contacts are not an input" {
var contacts: std.StringHashMapUnmanaged([]const u8) = .empty;
defer contacts.deinit(testing.allocator);
try contacts.put(testing.allocator, "alice", "[email protected]");
// author alice <[email protected]> 1704164645 +0000
try expectCommitId(contact_login, repository_rule, &contacts, "dfd25fc0d2bfcedd78385a3e2066fdf71786b9d7");
try expectCommitId(contact_login, repository_rule, null, "dfd25fc0d2bfcedd78385a3e2066fdf71786b9d7");
}
test "git identity vectors: a mirror recorded as built from contacts keeps taking the contact's address" {
var contacts: std.StringHashMapUnmanaged([]const u8) = .empty;
defer contacts.deinit(testing.allocator);
try contacts.put(testing.allocator, "alice", "[email protected]");
// author alice <[email protected]> 1704164645 +0000
try expectCommitId(contact_login, .{ .domain = "example.com", .contacts = true }, &contacts, "c7c7c707df42de2882ffa9ad05d4f8a60ab38c17");
}
test "git identity vectors: a login with no contact is login@DOMAIN" {
// author bob <[email protected]> 1704164645 +0000
try expectCommitId(plain_login, repository_rule, null, "ac3a0d4bf4ff939f6c9ce6382f4b691104b5fb18");
// The same under a recorded contacts rule: there is no contact to take.
var contacts: std.StringHashMapUnmanaged([]const u8) = .empty;
defer contacts.deinit(testing.allocator);
try contacts.put(testing.allocator, "alice", "[email protected]");
try expectCommitId(plain_login, .{ .domain = "example.com", .contacts = true }, &contacts, "ac3a0d4bf4ff939f6c9ce6382f4b691104b5fb18");
}
test "git identity vectors: a manifest with no U card is the login unknown, under the default domain" {
// author unknown <[email protected]> 1704164645 +0000
try expectCommitId(no_user, .{}, null, "d0ba58be6c4f99f617abd61883f2748f20d88a86");
}
test "git identity vectors: a manifest with no D card is time zero" {
const gpa = testing.allocator;
// Without a D card the cards classify as a submanifest, which may not
// carry C or U, so no stored check-in lacks one ...
try testing.expectError(error.DisallowedCard, artifact.parse(gpa, no_date));
// ... but the rule still answers for one: time zero.
var m = try artifact.parse(gpa, plain_login);
defer m.deinit();
m.d = null;
// author bob <[email protected]> 0 +0000
try expectManifestCommitId(&m, repository_rule, null, "27e84ab4fc283cee1dde7d88a9701bd0037d7209");
}
test "git identity vectors: a fractional second is truncated to the whole second" {
// author dave <[email protected]> 1704164645 +0000
try expectCommitId(fractional_second, repository_rule, null, "ce23a5a8a55162f26785e6d60388d7b3058b4975");
}
test "a contact's email address is the bracketed part, or else the first word with an @" {
try testing.expectEqualStrings("[email protected]", export_impl.contactAddress("Dan Mestas <[email protected]>").?);
try testing.expectEqualStrings("[email protected]", export_impl.contactAddress("[email protected]").?);
try testing.expectEqualStrings("[email protected]", export_impl.contactAddress("mail [email protected], phone 555").?);
try testing.expect(export_impl.contactAddress("") == null);
try testing.expect(export_impl.contactAddress("just a name") == null);
try testing.expect(export_impl.contactAddress("@ alone") == null);
}
test "an email domain is refused where it could break an identity line" {
try testing.expect(export_impl.validEmailDomain("example.org"));
try testing.expect(export_impl.validEmailDomain("users.noreply.github.com"));
try testing.expect(export_impl.validEmailDomain("zit.invalid"));
try testing.expect(!export_impl.validEmailDomain(""));
try testing.expect(!export_impl.validEmailDomain("a b"));
try testing.expect(!export_impl.validEmailDomain("x> 0 +0000\ncommitter"));
try testing.expect(!export_impl.validEmailDomain("a@b"));
try testing.expect(!export_impl.validEmailDomain(&@as([export_impl.max_email_domain + 1]u8, @splat('a'))));
}