size: 8 KiB

const std = @import("std");
const mem = std.mem;
const fs = std.fs;
const t = std.testing;

pub fn findGitDir(arena: mem.Allocator, dir: fs.Dir) ![]const u8 {
    var proc = try std.process.Child.run(.{
        .allocator = arena,
        .cwd_dir = dir,
        .argv = &.{ "git", "rev-parse", "--git-dir" },
    });
    return mem.trimEnd(u8, proc.stdout, "\n");
}

pub fn updateServerInfo(gpa: mem.Allocator, dir: fs.Dir) !void {
    var proc = try std.process.Child.run(.{
        .allocator = gpa,
        .cwd_dir = dir,
        .argv = &.{ "git", "update-server-info" },
    });
    defer gpa.free(proc.stderr);
    defer gpa.free(proc.stdout);
    std.debug.assert(proc.term.Exited == 0);
}

pub const Commit = struct {
    hash: []const u8,
    subject: []const u8,
    time: []const u8,
    author_email: []const u8,
    author_name: []const u8,
};

pub fn getCommits(arena: mem.Allocator, dir: fs.Dir) ![]Commit {
    const proc = try std.process.Child.run(.{
        .allocator = arena,
        .cwd_dir = dir,
        .argv = &.{
            "git",
            "log",
            "-z", // NUL byte is a safer delimiter than \n
            "--pretty=format:%H\n%ai\n%s\n%ae\n%an",
        },
        // 512 MiB should be enough for everyone (tm)
        .max_output_bytes = 1024 * 1024 * 512,
    });

    var commits: std.ArrayList(Commit) = try .initCapacity(arena, 128);

    var commit_texts = std.mem.splitSequence(u8, proc.stdout, "\x00");
    while (commit_texts.next()) |commit_text| {
        if (commit_text.len == 0) {
            continue;
        }
        var fields_iter = std.mem.splitSequence(u8, commit_text, "\n");
        try commits.append(arena, Commit{
            .hash = fields_iter.next().?,
            .time = fields_iter.next().?,
            .subject = fields_iter.next().?,
            .author_email = fields_iter.next().?,
            .author_name = fields_iter.next().?,
        });
    }
    return commits.items;
}

pub fn getLatestCommit(arena: mem.Allocator, dir: fs.Dir) !?Commit {
    const proc = try std.process.Child.run(.{
        .allocator = arena,
        .cwd_dir = dir,
        .argv = &.{
            "git",
            "show",
            "--summary",
            "--pretty=format:%H\n%ai\n%s\n%ae\n%an",
        },
        .max_output_bytes = 4096,
    });
    const commit_text = proc.stdout;

    if (commit_text.len == 0) {
        return null;
    }

    var fields_iter = std.mem.splitSequence(u8, commit_text, "\n");
    return Commit{
        .hash = fields_iter.next().?,
        .time = fields_iter.next().?,
        .subject = fields_iter.next().?,
        .author_email = fields_iter.next().?,
        .author_name = fields_iter.next().?,
    };
}

pub fn getDescription(arena: mem.Allocator, git_dir: fs.Dir) ![]const u8 {
    const description = git_dir.readFileAlloc(
        "description",
        arena,
        .limited(4096),
    ) catch |err| {
        switch (err) {
            error.FileNotFound => return "",
            else => return err,
        }
    };
    if (mem.startsWith(u8, description, "Unnamed repository;")) return "";
    return mem.trimEnd(u8, description, "\n");
}

/// If found, return the exact readme filename.
pub fn findReadme(arena: mem.Allocator, dir: fs.Dir) !?[]const u8 {
    var proc = try std.process.Child.run(.{
        .allocator = arena,
        .cwd_dir = dir,
        // can't run git-ls-files on bare repos, so use git-ls-tree instead:
        .argv = &.{ "git", "ls-tree", "--name-only", "HEAD" },
    });

    if (proc.stdout.len == 0) return null;

    var filenames = std.mem.splitScalar(u8, proc.stdout, '\n');
    while (filenames.next()) |name| {
        if (name.len == 0) continue;
        inline for (.{
            "readme.md",
            "readme.markdown",
            "readme.rst",
            "readme.txt",
            "readme",
        }) |matching_name| {
            if (std.ascii.eqlIgnoreCase(name, matching_name)) {
                return name;
            }
        }
    }
    return null;
}

pub fn readFileAlloc(arena: mem.Allocator, dir: fs.Dir, file_name: []const u8) ![]const u8 {
    var proc = try std.process.Child.run(.{
        .allocator = arena,
        .cwd_dir = dir,
        .argv = &.{
            "git",
            "show",
            try std.fmt.allocPrint(arena, "HEAD:{s}", .{file_name}),
        },
    });

    std.debug.assert(proc.term.Exited == 0);
    return proc.stdout;
}

pub const File = struct {
    hash: []const u8,
    size: u64,
    path: []const u8,
};

pub const Walker = struct {
    remaining: []const u8,

    pub fn next(self: *Walker) !?File {
        if (self.remaining.len == 0) return null;

        const hashEndIdx = mem.indexOfScalar(u8, self.remaining, ' ').?;
        const hash = self.remaining[0..hashEndIdx];

        const sizeEndIdx = mem.indexOfScalarPos(u8, self.remaining, hashEndIdx + 1, ' ').?;
        const sizeStr = self.remaining[hashEndIdx + 1 .. sizeEndIdx];
        const size =
            if (mem.eql(u8, sizeStr, "-")) // is a symlink
                0
            else
                try std.fmt.parseUnsigned(u64, sizeStr, 10);

        const pathEndIdx = mem.indexOfScalarPos(u8, self.remaining, sizeEndIdx + 1, '\x00').?;
        const path = self.remaining[sizeEndIdx + 1 .. pathEndIdx];

        self.remaining = self.remaining[pathEndIdx + 1 ..];
        return File{ .hash = hash, .size = size, .path = path };
    }
};

pub fn walkTree(arena: mem.Allocator, dir: fs.Dir) !Walker {
    var proc = try std.process.Child.run(.{
        .allocator = arena,
        .cwd_dir = dir,
        .max_output_bytes = 1024 * 1024 * 64,
        .argv = &.{
            "git",
            "ls-tree",
            "HEAD",
            "-r",
            "-z",
            "--format",
            "%(objectname) %(objectsize) %(path)",
        },
    });
    //empty repo without any commits would exit with non-zero status...
    //std.debug.assert(proc.term.Exited == 0);
    return Walker{ .remaining = proc.stdout };
}

//test "walkTree" {
//    var arena_impl = std.heap.ArenaAllocator.init(t.allocator_instance.allocator());
//    defer arena_impl.deinit();
//    const arena = arena_impl.allocator();
//
//    var dir = try fs.cwd().openDir("demo/khoe", .{});
//    defer dir.close();
//
//    var walker = try walkTree(arena, dir);
//    try t.expectEqualDeep(
//        File{
//            .hash = "f216b65cf70beaeda50910cf5c778d466d297bad",
//            .size = 33,
//            .path = ".gitignore",
//        },
//        (try walker.next()).?,
//    );
//}

pub const ObjectType = enum { blob, commit, tree, other };

pub fn objectType(arena: mem.Allocator, dir: fs.Dir, object_hash: []const u8) !ObjectType {
    var proc = try std.process.Child.run(.{
        .allocator = arena,
        .cwd_dir = dir,
        .max_output_bytes = 1024 * 1024 * 64,
        .argv = &.{
            "git",
            "cat-file",
            "-t",
            object_hash,
        },
    });

    const result = mem.trimEnd(u8, proc.stdout, "\n");
    if (mem.eql(u8, result, "blob")) {
        return .blob;
    } else if (mem.eql(u8, result, "commit")) {
        return .commit;
    } else if (mem.eql(u8, result, "tree")) {
        return .tree;
    }

    // I ran into a repo that had an empty folder, which `git ls-tree` listed
    // as a "commit" object, but other commands like `git show` would say that
    // same hash is invalid. I never figured out why that is, so just skip such
    // cases for now.
    return .other;
    //std.debug.panic("Unrecognized object type: {s} \"{s}\" - {s}", .{
    //    object_hash,
    //    result,
    //    proc.stderr,
    //});
}

/// Replicates git's simple heuristic: if there's a null byte in the first 8k
/// bytes, then consider the file binary.
pub fn isBinary(content: []const u8) bool {
    const first_few_bytes = 8000;
    const haystack = content[0..@min(content.len, first_few_bytes)];
    return mem.indexOfScalar(u8, haystack, '\x00') != null;
}

// TODO: instead of loading everything to memory, figure out how to stream
// instead.
pub fn catFile(arena: mem.Allocator, dir: fs.Dir, object_hash: []const u8) ![]const u8 {
    var proc = try std.process.Child.run(.{
        .allocator = arena,
        .cwd_dir = dir,
        .max_output_bytes = 1024 * 1024 * 64,
        .argv = &.{
            "git",
            "cat-file",
            "blob",
            object_hash,
        },
    });
    std.debug.assert(proc.term.Exited == 0);
    return proc.stdout;
}