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; }