srctree

Gregory Mullen parent b05f619c d03bb222
add git blame

src/endpoints/repos.zig added: 284, removed: 83, total 201
@@ -14,6 +14,8 @@ const DOM = Endpoint.DOM;
const Template = Endpoint.Template;
const Error = Endpoint.Error;
const UriIter = Endpoint.Router.UriIter;
const ROUTE = Endpoint.Router.ROUTE;
const MatchRouter = Endpoint.Router.MatchRouter;
 
const Bleach = @import("../bleach.zig");
const Humanize = @import("../humanize.zig");
@@ -31,7 +33,7 @@ const Types = @import("../types.zig");
 
const gitweb = @import("../gitweb.zig");
 
const endpoints = [_]Endpoint.Router.MatchRouter{
const endpoints = [_]MatchRouter{
.{ .name = "", .match = .{ .call = treeBlob } },
.{ .name = "blob", .match = .{ .call = treeBlob } },
.{ .name = "commits", .match = .{ .simple = &Commits.routes } },
@@ -39,6 +41,7 @@ const endpoints = [_]Endpoint.Router.MatchRouter{
.{ .name = "tree", .match = .{ .call = treeBlob } },
.{ .name = "diffs", .match = .{ .route = &Diffs.router } },
.{ .name = "issues", .match = .{ .route = &Issues.router } },
ROUTE("blame", blame),
} ++ gitweb.endpoints;
 
pub const RouteData = struct {
@@ -290,6 +293,202 @@ fn guessLang(name: []const u8) ?[]const u8 {
return null;
}
 
const BlameCommit = struct {
sha: []const u8,
parent: []const u8,
title: []const u8,
filename: []const u8,
author: struct {
name: []const u8,
time: i64,
tz: i32,
},
};
 
const BlameLine = struct {
commit: BlameCommit,
line: []const u8,
};
 
fn parseBlame(a: Allocator, blame_txt: []const u8) !struct {
map: std.StringHashMap(BlameCommit),
lines: []BlameLine,
} {
var map = std.StringHashMap(BlameCommit).init(a);
 
const count = std.mem.count(u8, blame_txt, "\n\t");
const lines = try a.alloc(BlameLine, count);
var in_lines = std.mem.split(u8, blame_txt, "\n");
for (lines) |*blm| {
const line = in_lines.next() orelse break;
if (line.len < 40) break;
const gp = try map.getOrPut(line[0..40]);
const cmt = gp.value_ptr;
if (!gp.found_existing) {
cmt.*.sha = line[0..40];
cmt.*.parent = "";
while (true) {
const next = in_lines.next() orelse return error.UnexpectedEndOfBlame;
if (next[0] == '\t') {
blm.*.line = next[1..];
break;
}
 
if (std.mem.startsWith(u8, next, "author ")) {
cmt.*.author.name = next["author ".len..];
} else if (std.mem.startsWith(u8, next, "author-time ")) {
cmt.*.author.time = try std.fmt.parseInt(i64, next["author-time ".len..], 10);
} else if (std.mem.startsWith(u8, next, "author-tz ")) {
cmt.*.author.tz = try std.fmt.parseInt(i32, next["author-tz ".len..], 10);
} else if (std.mem.startsWith(u8, next, "summary ")) {
cmt.*.title = next["summary ".len..];
} else if (std.mem.startsWith(u8, next, "previous ")) {
cmt.*.parent = next["previous ".len..][0..40];
} else if (std.mem.startsWith(u8, next, "filename ")) {
cmt.*.filename = next["filename ".len..];
} else {
continue;
}
}
} else {
blm.line = in_lines.next().?[1..];
}
blm.commit = cmt.*;
}
 
return .{
.map = map,
.lines = lines,
};
}
 
fn blame(ctx: *Context) Error!void {
const rd = RouteData.make(&ctx.uri) orelse return error.Unrouteable;
std.debug.assert(std.mem.eql(u8, rd.verb orelse "", "blame"));
_ = ctx.uri.next();
const blame_file = ctx.uri.rest();
 
var cwd = std.fs.cwd();
const fname = try aPrint(ctx.alloc, "./repos/{s}", .{rd.name});
const dir = cwd.openDir(fname, .{}) catch return error.Unknown;
var repo = git.Repo.init(dir) catch return error.Unknown;
defer repo.raze(ctx.alloc);
 
var actions = repo.getActions(ctx.alloc);
const git_blame = actions.blame(blame_file) catch unreachable;
 
const parsed = parseBlame(ctx.alloc, git_blame) catch unreachable;
var source_lines = std.ArrayList(u8).init(ctx.alloc);
for (parsed.lines) |line| {
try source_lines.appendSlice(line.line);
try source_lines.append('\n');
}
 
const formatted = if (guessLang(blame_file)) |lang| fmt: {
var pre = try highlight(ctx.alloc, lang, source_lines.items);
break :fmt pre[28..][0 .. pre.len - 38];
} else Bleach.sanitizeAlloc(ctx.alloc, source_lines.items, .{}) catch return error.Unknown;
 
const tctx = try wrapLineNumbersBlame(ctx.alloc, formatted, parsed.lines);
 
var tmpl = Template.find("blame.html");
tmpl.init(ctx.alloc);
 
try tmpl.ctx.?.putBlock("blame_lines", tctx);
 
tmpl.addVar("filename", blame_file) catch return error.Unknown;
ctx.response.status = .ok;
 
try ctx.sendTemplate(&tmpl);
}
 
fn highlight(a: Allocator, lang: []const u8, text: []const u8) ![]u8 {
var child = std.ChildProcess.init(&[_][]const u8{ "pygmentize", "-f", "html", "-l", lang }, a);
child.stdin_behavior = .Pipe;
child.stdout_behavior = .Pipe;
child.stderr_behavior = .Ignore;
child.expand_arg0 = .no_expand;
child.spawn() catch unreachable;
 
const err_mask = std.os.POLL.ERR | std.os.POLL.NVAL | std.os.POLL.HUP;
var poll_fd = [_]std.os.pollfd{
.{
.fd = child.stdout.?.handle,
.events = std.os.POLL.IN,
.revents = undefined,
},
};
_ = std.os.write(child.stdin.?.handle, text) catch unreachable;
std.os.close(child.stdin.?.handle);
child.stdin = null;
var buf = std.ArrayList(u8).init(a);
const abuf = try a.alloc(u8, 0xffffff);
while (true) {
const events_len = std.os.poll(&poll_fd, std.math.maxInt(i32)) catch unreachable;
if (events_len == 0) continue;
if (poll_fd[0].revents & std.os.POLL.IN != 0) {
const amt = std.os.read(poll_fd[0].fd, abuf) catch unreachable;
if (amt == 0) break;
try buf.appendSlice(abuf[0..amt]);
} else if (poll_fd[0].revents & err_mask != 0) {
break;
}
}
a.free(abuf);
 
_ = child.wait() catch unreachable;
return try buf.toOwnedSlice();
}
 
fn wrapLineNumbersBlame(
a: Allocator,
text: []const u8,
blames: []BlameLine,
) ![]Template.Context {
const count = std.mem.count(u8, text, "\n");
var litr = std.mem.split(u8, text, "\n");
var tctx = try a.alloc(Template.Context, count + 1);
for (0..count + 1) |i| {
var ctx = &tctx[i];
ctx.* = Template.Context.init(a);
if (i < count) {
try ctx.put("sha", blames[i].commit.sha[0..8]);
try ctx.put("author", blames[i].commit.author.name);
} else {
try ctx.put("sha", blames[i - 1].commit.sha[0..8]);
try ctx.put("author", blames[i - 1].commit.author.name);
}
const b = std.fmt.allocPrint(a, "#L{}", .{i + 1}) catch unreachable;
try ctx.put("num", b[2..]);
try ctx.put("id", b[1..]);
try ctx.put("href", b);
try ctx.put("line", litr.next().?);
}
return tctx;
}
 
fn wrapLineNumbers(a: Allocator, root_dom: *DOM, text: []const u8) !*DOM {
var dom = root_dom;
dom = dom.open(HTML.element("code", null, null));
// TODO
 
const count = std.mem.count(u8, text, "\n");
var litr = std.mem.split(u8, text, "\n");
for (0..count + 1) |i| {
var pbuf: [12]u8 = undefined;
const b = std.fmt.bufPrint(&pbuf, "#L{}", .{i + 1}) catch unreachable;
const attrs = try HTML.Attribute.alloc(
a,
&[_][]const u8{ "num", "id", "href" },
&[_]?[]const u8{ b[2..], b[1..], b },
);
const line = litr.next().?;
dom.push(HTML.element("ln", line, attrs));
}
 
return dom.close();
}
 
fn blob(ctx: *Context, repo: *git.Repo, pfiles: git.Tree) Error!void {
var tmpl = Template.find("blob.html");
tmpl.init(ctx.alloc);
@@ -311,92 +510,37 @@ fn blob(ctx: *Context, repo: *git.Repo, pfiles: git.Tree) Error!void {
} else return error.InvalidURI;
}
 
var dom = DOM.new(ctx.alloc);
 
var resolve = repo.blob(ctx.alloc, &blb.hash) catch return error.Unknown;
var reader = resolve.reader();
 
var formatted: []const u8 = undefined;
 
const d2 = reader.readAllAlloc(ctx.alloc, 0xffffff) catch unreachable;
 
if (guessLang(blb.name)) |lang| {
var child = std.ChildProcess.init(&[_][]const u8{
"pygmentize",
"-f",
"html",
"-l",
lang,
}, ctx.alloc);
child.stdin_behavior = .Pipe;
child.stdout_behavior = .Pipe;
child.stderr_behavior = .Ignore;
child.expand_arg0 = .no_expand;
child.spawn() catch unreachable;
 
const err_mask = std.os.POLL.ERR | std.os.POLL.NVAL | std.os.POLL.HUP;
var poll_fd = [_]std.os.pollfd{
.{
.fd = child.stdout.?.handle,
.events = std.os.POLL.IN,
.revents = undefined,
},
};
_ = std.os.write(child.stdin.?.handle, d2) catch unreachable;
std.os.close(child.stdin.?.handle);
child.stdin = null;
var buf = std.ArrayList(u8).init(ctx.alloc);
const abuf = try ctx.alloc.alloc(u8, 0xffffff);
while (true) {
const events_len = std.os.poll(&poll_fd, std.math.maxInt(i32)) catch unreachable;
if (events_len == 0) continue;
if (poll_fd[0].revents & std.os.POLL.IN != 0) {
const amt = std.os.read(poll_fd[0].fd, abuf) catch unreachable;
if (amt == 0) break;
try buf.appendSlice(abuf[0..amt]);
} else if (poll_fd[0].revents & err_mask != 0) {
break;
}
}
ctx.alloc.free(abuf);
 
_ = child.wait() catch unreachable;
formatted = try buf.toOwnedSlice();
const pre = try highlight(ctx.alloc, lang, d2);
formatted = pre[28..][0 .. pre.len - 38];
} else {
formatted = d2;
formatted = Bleach.sanitizeAlloc(ctx.alloc, d2, .{}) catch return error.Unknown;
}
 
dom = dom.open(HTML.element("code", null, null));
// TODO
var litr = if (guessLang(blb.name)) |_|
std.mem.split(u8, formatted[28..][0 .. formatted.len - 38], "\n")
else
std.mem.split(u8, d2, "\n");
 
const count = std.mem.count(u8, formatted, "\n");
for (0..count + 1) |i| {
var pbuf: [12]u8 = undefined;
const b = std.fmt.bufPrint(&pbuf, "#L{}", .{i + 1}) catch unreachable;
const attrs = try HTML.Attribute.alloc(ctx.alloc, &[_][]const u8{
"num",
"id",
"href",
}, &[_]?[]const u8{
b[2..],
b[1..],
b,
});
const line = litr.next().?;
dom.push(HTML.element("ln", line, attrs));
}
 
dom = dom.close();
var dom = DOM.new(ctx.alloc);
dom = try wrapLineNumbers(ctx.alloc, dom, formatted);
const data = dom.done();
 
const filestr = try aPrint(
ctx.alloc,
"{pretty}",
.{HTML.div(data, &HTML.Attr.class("code-block"))},
);
tmpl.addVar("files", filestr) catch return error.Unknown;
tmpl.addVar("blob", filestr) catch return error.Unknown;
tmpl.addVar("filename", blb.name) catch return error.Unknown;
ctx.uri.reset();
_ = ctx.uri.next();
tmpl.addVar("repo", ctx.uri.next() orelse "unknown") catch return error.Unknown;
_ = ctx.uri.next();
tmpl.addVar("uri_filename", ctx.uri.rest()) catch return error.Unknown;
 
ctx.response.status = .ok;
 
try ctx.sendTemplate(&tmpl);
@@ -421,8 +565,7 @@ fn htmlReadme(a: Allocator, readme: []const u8) ![]HTML.E {
dom = dom.open(HTML.element("code", null, null));
var litr = std.mem.split(u8, readme, "\n");
while (litr.next()) |dirty| {
var clean = try a.alloc(u8, dirty.len * 2);
clean = Bleach.sanitize(dirty, clean, .{}) catch return error.Unknown;
const clean = Bleach.sanitizeAlloc(a, dirty, .{}) catch return error.Unknown;
dom.push(HTML.element("ln", clean, null));
}
dom = dom.close();
@@ -487,7 +630,7 @@ fn drawTree(a: Allocator, ddom: *DOM, rname: []const u8, base: []const u8, obj:
}
 
fn tree(ctx: *Context, repo: *git.Repo, files: *git.Tree) Error!void {
var tmpl = Template.find("repo.html");
var tmpl = Template.find("tree.html");
tmpl.init(ctx.alloc);
 
const head = if (repo.head) |h| switch (h) {
@@ -574,4 +717,3 @@ fn tree(ctx: *Context, repo: *git.Repo, files: *git.Tree) Error!void {
 
ctx.sendTemplate(&tmpl) catch return error.Unknown;
}
// this is the end
 
src/git.zig added: 284, removed: 83, total 201
@@ -673,6 +673,7 @@ pub const Repo = struct {
}
 
pub fn getActions(self: *const Repo, a: Allocator) Actions {
// FIXME if (!self.bare) provide_working_dir_to_git
return Actions{
.alloc = a,
.repo = self,
@@ -888,7 +889,13 @@ pub const Commit = struct {
a.free(self.sha);
a.free(self.blob);
}
pub fn format(self: Commit, comptime _: []const u8, _: std.fmt.FormatOptions, out: anytype) !void {
 
pub fn format(
self: Commit,
comptime _: []const u8,
_: std.fmt.FormatOptions,
out: anytype,
) !void {
try out.print(
\\Commit{{
\\commit {s}
@@ -1238,6 +1245,16 @@ pub const Actions = struct {
});
}
 
pub fn blame(self: Actions, name: []const u8) ![]u8 {
std.debug.print("{s}\n", .{name});
return try self.exec(&[_][]const u8{
"git",
"blame",
"--porcelain",
name,
});
}
 
fn execCustom(self: Actions, argv: []const []const u8) !std.ChildProcess.RunResult {
std.debug.assert(std.mem.eql(u8, argv[0], "git"));
const cwd = if (self.cwd != null and self.cwd.?.fd != std.fs.cwd().fd) self.cwd else null;
 
static/main.css added: 284, removed: 83, total 201
@@ -1,5 +1,5 @@
html {
/*background: #201B22;*/
background: #222222;
background-image: linear-gradient(0deg, #290015 0%, #222222 6%);
color-scheme: light dark;
color: #FFFFEB;
@@ -112,7 +112,21 @@ code {
text-align: left;
white-space: pre;
width: 100%;
> ln {
.blame-line {
display: block;
.blame-header {
sha {
width: 5em;
display: inline-block;
}
author {
width: 8em;
display: inline-block;
}
}
}
 
ln {
width: 100%;
display: inline-block;
&:before {
@@ -510,6 +524,30 @@ comment {
}
}
 
blob {
text-align: left;
display: block;
margin-bottom: 1em;
 
 
header {
background: #333;
border-radius: 3px 3px 0 0;
border: 1px gray solid;
border-bottom: 0px;
display: flex;
justify-content: space-between;
height: 1.5em;
padding: 4px 12px;
}
.code-block {
padding: 4px 12px;
border: 1px gray solid;
border-bottom: 0px;
border-top: 0px;
}
}
 
.commit-flex {
width: 1050px;
margin: auto;
 
templates/blame.html added: 284, removed: 83, total 201
@@ -10,7 +10,11 @@
<blob>
<a class="btn" href="<!-- blame_uri ORNULL -->/blame">blame</a>
<header><!-- filename --></header>
<!-- blame -->
<div class="code-block">
<code>
<!-- FOREACH blame_lines --><span class="blame-line"><span class="blame-header"><sha><!-- sha --></sha><author><!-- author --></author><time><!-- time ORNULL --></time></span><ln num="<!-- num -->" id="<!-- id -->" href="<!-- href -->"><!-- line --></ln></span><!-- END !-->
</code>
</div>
</blob>
</content>
</body>