size: 9 KiB

1const std = @import("std");
2const mem = std.mem;
3const fs = std.fs;
4const Io = std.Io;
5const t = std.testing;
6
7pub fn findGitDir(arena: mem.Allocator, dir: fs.Dir) ![]const u8 {
8 var proc = try std.process.Child.run(.{
9 .allocator = arena,
10 .cwd_dir = dir,
11 .argv = &.{ "git", "rev-parse", "--git-dir" },
12 });
13 return mem.trimEnd(u8, proc.stdout, "\n");
14}
15
16pub fn updateServerInfo(gpa: mem.Allocator, dir: fs.Dir) !void {
17 var proc = try std.process.Child.run(.{
18 .allocator = gpa,
19 .cwd_dir = dir,
20 .argv = &.{ "git", "update-server-info" },
21 });
22 defer gpa.free(proc.stderr);
23 defer gpa.free(proc.stdout);
24 std.debug.assert(proc.term.Exited == 0);
25}
26
27pub const Commit = struct {
28 hash: []const u8,
29 subject: []const u8,
30 time: []const u8,
31 author_email: []const u8,
32 author_name: []const u8,
33};
34
35pub fn getCommits(arena: mem.Allocator, dir: fs.Dir) ![]Commit {
36 const proc = try std.process.Child.run(.{
37 .allocator = arena,
38 .cwd_dir = dir,
39 .argv = &.{
40 "git",
41 "log",
42 "-z", // NUL byte is a safer delimiter than \n
43 "--pretty=format:%H\n%ai\n%s\n%ae\n%an",
44 },
45 // 512 MiB should be enough for everyone (tm)
46 .max_output_bytes = 1024 * 1024 * 512,
47 });
48
49 var commits: std.ArrayList(Commit) = try .initCapacity(arena, 128);
50
51 var commit_texts = std.mem.splitSequence(u8, proc.stdout, "\x00");
52 while (commit_texts.next()) |commit_text| {
53 if (commit_text.len == 0) {
54 continue;
55 }
56 var fields_iter = std.mem.splitSequence(u8, commit_text, "\n");
57 try commits.append(arena, Commit{
58 .hash = fields_iter.next().?,
59 .time = fields_iter.next().?,
60 .subject = fields_iter.next().?,
61 .author_email = fields_iter.next().?,
62 .author_name = fields_iter.next().?,
63 });
64 }
65 return commits.items;
66}
67
68pub fn getLatestCommit(arena: mem.Allocator, dir: fs.Dir) !?Commit {
69 const proc = try std.process.Child.run(.{
70 .allocator = arena,
71 .cwd_dir = dir,
72 .argv = &.{
73 "git",
74 "show",
75 "--summary",
76 "--pretty=format:%H\n%ai\n%s\n%ae\n%an",
77 },
78 .max_output_bytes = 4096,
79 });
80 const commit_text = proc.stdout;
81
82 if (commit_text.len == 0) {
83 return null;
84 }
85
86 var fields_iter = std.mem.splitSequence(u8, commit_text, "\n");
87 return Commit{
88 .hash = fields_iter.next().?,
89 .time = fields_iter.next().?,
90 .subject = fields_iter.next().?,
91 .author_email = fields_iter.next().?,
92 .author_name = fields_iter.next().?,
93 };
94}
95
96pub fn getDescription(arena: mem.Allocator, git_dir: fs.Dir) ![]const u8 {
97 const description = git_dir.readFileAlloc(
98 "description",
99 arena,
100 .limited(4096),
101 ) catch |err| {
102 switch (err) {
103 error.FileNotFound => return "",
104 else => return err,
105 }
106 };
107 if (mem.startsWith(u8, description, "Unnamed repository;")) return "";
108 return mem.trimEnd(u8, description, "\n");
109}
110
111/// If found, return the exact readme filename.
112pub fn findReadme(arena: mem.Allocator, dir: fs.Dir) !?[]const u8 {
113 var proc = try std.process.Child.run(.{
114 .allocator = arena,
115 .cwd_dir = dir,
116 // can't run git-ls-files on bare repos, so use git-ls-tree instead:
117 .argv = &.{ "git", "ls-tree", "--name-only", "HEAD" },
118 });
119
120 if (proc.stdout.len == 0) return null;
121
122 var filenames = std.mem.splitScalar(u8, proc.stdout, '\n');
123 while (filenames.next()) |name| {
124 if (name.len == 0) continue;
125 inline for (.{
126 "readme.md",
127 "readme.markdown",
128 "readme.rst",
129 "readme.txt",
130 "readme",
131 }) |matching_name| {
132 if (std.ascii.eqlIgnoreCase(name, matching_name)) {
133 return name;
134 }
135 }
136 }
137 return null;
138}
139
140pub fn readFileAlloc(arena: mem.Allocator, dir: fs.Dir, file_name: []const u8) ![]const u8 {
141 var proc = try std.process.Child.run(.{
142 .allocator = arena,
143 .cwd_dir = dir,
144 .argv = &.{
145 "git",
146 "show",
147 try std.fmt.allocPrint(arena, "HEAD:{s}", .{file_name}),
148 },
149 });
150
151 std.debug.assert(proc.term.Exited == 0);
152 return proc.stdout;
153}
154
155pub const File = struct {
156 hash: []const u8,
157 size: u64,
158 path: []const u8,
159};
160
161pub const Walker = struct {
162 remaining: []const u8,
163
164 pub fn next(self: *Walker) !?File {
165 if (self.remaining.len == 0) return null;
166
167 const hashEndIdx = mem.indexOfScalar(u8, self.remaining, ' ').?;
168 const hash = self.remaining[0..hashEndIdx];
169
170 const sizeEndIdx = mem.indexOfScalarPos(u8, self.remaining, hashEndIdx + 1, ' ').?;
171 const sizeStr = self.remaining[hashEndIdx + 1 .. sizeEndIdx];
172 const size =
173 if (mem.eql(u8, sizeStr, "-")) // is a symlink
174 0
175 else
176 try std.fmt.parseUnsigned(u64, sizeStr, 10);
177
178 const pathEndIdx = mem.indexOfScalarPos(u8, self.remaining, sizeEndIdx + 1, '\x00').?;
179 const path = self.remaining[sizeEndIdx + 1 .. pathEndIdx];
180
181 self.remaining = self.remaining[pathEndIdx + 1 ..];
182 return File{ .hash = hash, .size = size, .path = path };
183 }
184};
185
186pub fn walkTree(arena: mem.Allocator, dir: fs.Dir, tree_ref: []const u8) !Walker {
187 var proc = try std.process.Child.run(.{
188 .allocator = arena,
189 .cwd_dir = dir,
190 .max_output_bytes = 1024 * 1024 * 64,
191 .argv = &.{
192 "git",
193 "ls-tree",
194 tree_ref,
195 "-r",
196 "-z",
197 "--format",
198 "%(objectname) %(objectsize) %(path)",
199 },
200 });
201 //empty repo without any commits would exit with non-zero status...
202 //std.debug.assert(proc.term.Exited == 0);
203 return Walker{ .remaining = proc.stdout };
204}
205
206//test "walkTree" {
207// var arena_impl = std.heap.ArenaAllocator.init(t.allocator_instance.allocator());
208// defer arena_impl.deinit();
209// const arena = arena_impl.allocator();
210//
211// var dir = try fs.cwd().openDir("demo/khoe", .{});
212// defer dir.close();
213//
214// var walker = try walkTree(arena, dir);
215// try t.expectEqualDeep(
216// File{
217// .hash = "f216b65cf70beaeda50910cf5c778d466d297bad",
218// .size = 33,
219// .path = ".gitignore",
220// },
221// (try walker.next()).?,
222// );
223//}
224
225pub const ObjectType = enum { blob, commit, tree, other };
226
227pub fn objectType(arena: mem.Allocator, dir: fs.Dir, object_hash: []const u8) !ObjectType {
228 var proc = try std.process.Child.run(.{
229 .allocator = arena,
230 .cwd_dir = dir,
231 .max_output_bytes = 1024 * 1024 * 64,
232 .argv = &.{
233 "git",
234 "cat-file",
235 "-t",
236 object_hash,
237 },
238 });
239
240 const result = mem.trimEnd(u8, proc.stdout, "\n");
241 if (mem.eql(u8, result, "blob")) {
242 return .blob;
243 } else if (mem.eql(u8, result, "commit")) {
244 return .commit;
245 } else if (mem.eql(u8, result, "tree")) {
246 return .tree;
247 }
248
249 // I ran into a repo that had an empty folder, which `git ls-tree` listed
250 // as a "commit" object, but other commands like `git show` would say that
251 // same hash is invalid. I never figured out why that is, so just skip such
252 // cases for now.
253 return .other;
254 //std.debug.panic("Unrecognized object type: {s} \"{s}\" - {s}", .{
255 // object_hash,
256 // result,
257 // proc.stderr,
258 //});
259}
260
261/// Replicates git's simple heuristic: if there's a null byte in the first 8k
262/// bytes, then consider the file binary.
263pub fn isBinary(content: []const u8) bool {
264 const first_few_bytes = 8000;
265 const haystack = content[0..@min(content.len, first_few_bytes)];
266 return mem.indexOfScalar(u8, haystack, '\x00') != null;
267}
268
269// TODO: instead of loading everything to memory, figure out how to stream
270// instead.
271pub fn catFile(arena: mem.Allocator, dir: fs.Dir, object_hash: []const u8) ![]const u8 {
272 var proc = try std.process.Child.run(.{
273 .allocator = arena,
274 .cwd_dir = dir,
275 .max_output_bytes = 1024 * 1024 * 64,
276 .argv = &.{
277 "git",
278 "cat-file",
279 "blob",
280 object_hash,
281 },
282 });
283 std.debug.assert(proc.term.Exited == 0);
284 return proc.stdout;
285}
286
287pub fn show(arena: mem.Allocator, dir: fs.Dir, commit_hash: []const u8) ![]const u8 {
288 // First get git's terminal output with color codes intact
289 var git_proc = try std.process.Child.run(.{
290 .allocator = arena,
291 .cwd_dir = dir,
292 .max_output_bytes = 1024 * 1024 * 64,
293 .argv = &.{ "git", "show", commit_hash },
294 });
295 std.debug.assert(git_proc.term.Exited == 0);
296 return git_proc.stdout;
297}
298
299pub fn formatPatch(arena: mem.Allocator, dir: fs.Dir, commit_hash: []const u8) ![]const u8 {
300 var proc = try std.process.Child.run(.{
301 .allocator = arena,
302 .cwd_dir = dir,
303 .max_output_bytes = 1024 * 1024 * 1024,
304 .argv = &.{
305 "git",
306 "format-patch",
307 "-1",
308 "--stdout",
309 commit_hash,
310 },
311 });
312 std.debug.assert(proc.term.Exited == 0);
313 return proc.stdout;
314}