Skip to content

Commit

Permalink
Add git info to $build (#77)
Browse files Browse the repository at this point in the history
* Add first steps

* Fix type structure

* Add git buildin in Build

* Fix structs for template

* Determine if folder is a git repo

* Get branch and commithash

* Get tag of current commit

* Parse commit metadata

* Cleanup and use ArenaAllocator

* Parse commit_date correctly

* Add git? builtin

* Add builtins for tag and branch

* Add check for zero parameters

* Fix branch path on windows
  • Loading branch information
Sc3l3t0n authored Oct 23, 2024
1 parent a43e86e commit cada8ea
Show file tree
Hide file tree
Showing 6 changed files with 321 additions and 3 deletions.
3 changes: 3 additions & 0 deletions src/context.zig
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ pub const Template = @import("context/Template.zig");
pub const Site = @import("context/Site.zig");
pub const Page = @import("context/Page.zig");
pub const Build = @import("context/Build.zig");
pub const Git = @import("context/Git.zig");
pub const Asset = @import("context/Asset.zig");
pub const DateTime = @import("context/DateTime.zig");
pub const String = @import("context/String.zig");
Expand All @@ -84,6 +85,7 @@ pub const Value = union(enum) {
alternative: Page.Alternative,
content_section: Page.ContentSection,
build: *const Build,
git: Git,
asset: Asset,
map: Map,
// slice: Slice,
Expand Down Expand Up @@ -173,6 +175,7 @@ pub const Value = union(enum) {
Page.Alternative => .{ .alternative = v },
Page.ContentSection => .{ .content_section = v },
*const Build => .{ .build = v },
Git => .{ .git = v },
Ctx(Value) => .{ .ctx = v },
Asset => .{ .asset = v },
DateTime => .{ .date = v },
Expand Down
55 changes: 53 additions & 2 deletions src/context/Build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,26 @@ const scripty = @import("scripty");
const utils = @import("utils.zig");
const context = @import("../context.zig");
const Value = context.Value;
const Optional = context.Optional;
const Signature = @import("doctypes.zig").Signature;
const uninitialized = utils.uninitialized;

pub const dot = scripty.defaultDot(Build, Value, false);
pub const PassByRef = true;

generated: context.DateTime,
_git: context.Git,

pub fn init() Build {
pub fn init(arena: Allocator) Build {
return .{
.generated = context.DateTime.initNow(),
._git = context.Git.init(arena),
};
}

pub const description =
\\Gives you access to build-time assets and other build related info.
// \\When inside of a git repository it also gives git-related metadata.
\\When inside of a git repository it also gives git-related metadata.
;

pub const Fields = struct {
Expand Down Expand Up @@ -68,4 +71,52 @@ pub const Builtins = struct {
return context.assetFind(ref, .{ .build = null });
}
};

pub const git = struct {
pub const signature: Signature = .{ .ret = .Git };
pub const description =
\\Returns git-related metadata if you are inside a git repository.
\\If you are not or the parsing failes, it will return an error.
\\Packed object are not supported, commit anything to get the metadata.
;
pub const examples =
\\<div :text="$build.git()..."></div>
;
pub fn call(
build: *const Build,
_: Allocator,
args: []const Value,
) Value {
const bad_arg = .{
.err = "expected 0 arguments",
};
if (args.len != 0) return bad_arg;

return if (build._git._in_repo) .{ .git = build._git } else .{ .err = "Not in a git repository" };
}
};

pub const @"git?" = struct {
pub const signature: Signature = .{ .ret = .Git };
pub const description =
\\Returns git-related metadata if you are inside a git repository.
\\If you are not or the parsing failes, it will return null.
\\Packed object are not supported, commit anything to get the metadata.
;
pub const examples =
\\<div :if="$build.git?()">...</div>
;
pub fn call(
build: *const Build,
gpa: Allocator,
args: []const Value,
) !Value {
const bad_arg = .{
.err = "expected 0 arguments",
};
if (args.len != 0) return bad_arg;

return if (build._git._in_repo) Optional.init(gpa, build._git) else Optional.Null;
}
};
};
7 changes: 7 additions & 0 deletions src/context/DateTime.zig
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ pub fn init(iso8601: []const u8) !DateTime {
};
}

pub fn initUnix(timestamp: i64) !DateTime {
const date = try zeit.instant(.{
.source = .{ .unix_timestamp = timestamp },
});
return .{ ._inst = date };
}

pub fn initNow() DateTime {
const date = zeit.instant(.{}) catch unreachable;
return .{ ._inst = date };
Expand Down
255 changes: 255 additions & 0 deletions src/context/Git.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
const Git = @This();

const std = @import("std");
const builtin = @import("builtin");
const scripty = @import("scripty");
const context = @import("../context.zig");
const Signature = @import("doctypes.zig").Signature;
const Allocator = std.mem.Allocator;
const DateTime = context.DateTime;
const String = context.String;
const Optional = context.Optional;
const Bool = context.Bool;
const Value = context.Value;

pub const dot = scripty.defaultDot(Git, Value, false);

pub const gitCommitHashLen = 40;

_in_repo: bool = false,

commit_hash: []const u8 = undefined,
commit_date: DateTime = undefined,
commit_message: []const u8 = undefined,
author_name: []const u8 = undefined,
author_email: []const u8 = undefined,

_tag: ?[]const u8 = null,
_branch: ?[]const u8 = null,

pub fn init(arena: Allocator) Git {
var git = Git{};

const git_dir = std.fs.cwd().openDir(".git", .{}) catch {
return git;
};
git._in_repo = true;

const head = readHead(arena, git_dir) catch return Git{};
switch (head) {
.commit_hash => |hash| git.commit_hash = hash,
.branch => |branch| {
git._branch = branch;
git.commit_hash = readCommitOfBranch(arena, git_dir, branch) catch return Git{};
},
}

git._tag = getTagForCommitHash(arena, git_dir, git.commit_hash) catch return Git{};

git.setAdditionalMetadata(arena, git_dir) catch return Git{};
return git;
}

fn readHead(arena: Allocator, git_dir: std.fs.Dir) !union(enum) { commit_hash: []const u8, branch: []const u8 } {
var head_file = try git_dir.openFile("HEAD", .{});
defer head_file.close();
const buf = try head_file.readToEndAlloc(arena, 4096);

if (std.mem.startsWith(u8, buf, "ref:")) {
return .{ .branch = buf[16 .. buf.len - 1] };
} else {
return .{ .commit_hash = buf[0 .. buf.len - 1] };
}
}

fn readCommitOfBranch(arena: Allocator, git_dir: std.fs.Dir, branch: []const u8) ![]const u8 {
const rel_path = switch (builtin.os.tag) {
.windows => win: {
const duped_branch = try arena.dupe(u8, branch);
defer arena.free(duped_branch);
std.mem.replaceScalar(u8, duped_branch, '/', '\\');
break :win try std.fs.path.join(arena, &.{ "refs", "heads", duped_branch });
},
else => try std.fs.path.join(arena, &.{ "refs", "heads", branch }),
};
defer arena.free(rel_path);

const content = try git_dir.readFileAlloc(arena, rel_path, gitCommitHashLen + 1);
return content[0..gitCommitHashLen];
}

fn getTagForCommitHash(arena: Allocator, git_dir: std.fs.Dir, commit_hash: []const u8) !?[]const u8 {
const rel_path = try std.fs.path.join(arena, &.{ "refs", "tags" });
defer arena.free(rel_path);

var tags = try git_dir.openDir(rel_path, .{ .iterate = true });
defer tags.close();

var iter = tags.iterate();
while (try iter.next()) |tag| {
const content = try tags.readFileAlloc(arena, tag.name, gitCommitHashLen + 1);
const tag_hash = content[0..gitCommitHashLen];

if (std.mem.eql(u8, tag_hash, commit_hash)) {
return try arena.dupe(u8, tag.name);
}
}
return null;
}

// NOTE: Does not support packed objects
fn setAdditionalMetadata(git: *Git, arena: Allocator, git_dir: std.fs.Dir) !void {
const commit_path = try std.fs.path.join(arena, &.{ "objects", git.commit_hash[0..2], git.commit_hash[2..] });
defer arena.free(commit_path);

const content = try git_dir.openFile(commit_path, .{});
var decompressed = std.compress.zlib.decompressor(content.reader());
const reader = decompressed.reader();
const data = try reader.readAllAlloc(arena, 100000);

var attributes = std.mem.splitScalar(u8, data, '\n');

_ = attributes.next(); // tree hash
_ = attributes.next(); // parent commit hash
_ = attributes.next(); // author

if (attributes.next()) |committer| {
const @"<_index" = std.mem.indexOfScalar(u8, committer, '<').?;
const @">_index" = std.mem.indexOfScalar(u8, committer, '>').?;

git.author_name = committer[10 .. @"<_index" - 1];
git.author_email = committer[@"<_index" + 1 .. @">_index"];

const unix_time = try std.fmt.parseInt(i64, committer[@">_index" + 2 .. committer.len - 6], 10);
const offset_hour = try std.fmt.parseInt(i64, committer[committer.len - 4 .. committer.len - 2], 10);
const offset = switch (committer[committer.len - 5]) {
'-' => -offset_hour,
'+' => offset_hour,
else => unreachable,
};
git.commit_date = try DateTime.initUnix(unix_time + offset * 3600);
}

_ = attributes.next(); // empty line
git.commit_message = attributes.rest();
}

pub const description =
\\Information about the current git repository.
;

pub const Fields = struct {
pub const commit_hash =
\\The current commit hash.
;
pub const commit_date =
\\The date of the current commit.
;
pub const commit_message =
\\The commit message of the current commit.
;
pub const author_name =
\\The name of the author of the current commit.
;
pub const author_email =
\\The email of the author of the current commit.
;
};

pub const Builtins = struct {
pub const tag = struct {
pub const signature: Signature = .{ .ret = .String };
pub const description =
\\Returns the tag of the current commit.
\\If the current commit does not have a tag, an error is returned.
;
pub const examples =
\\<div :text="$build.git().tag()"></div>
\\<div :if="$build.git?()"><span :text="$if.tag()"></span></div>
;
pub fn call(
git: Git,
gpa: Allocator,
args: []const Value,
) !Value {
const bad_arg = .{
.err = "expected 0 arguments",
};
if (args.len != 0) return bad_arg;

return if (git._tag) |_tag| Value.from(gpa, _tag) else .{ .err = "No tag for this commit" };
}
};

pub const @"tag?" = struct {
pub const signature: Signature = .{ .ret = .String };
pub const description =
\\Returns the tag of the current commit.
\\If the current commit does not have a tag, null is returned.
;
pub const examples =
\\<div :if="$build.git().tag?()"><span :text="$if"></span></div>
\\<div :if="$build.git?()"><span :if="$if.tag?()"><span :text="$if"></span></span></div>
;
pub fn call(
git: Git,
gpa: Allocator,
args: []const Value,
) !Value {
const bad_arg = .{
.err = "expected 0 arguments",
};
if (args.len != 0) return bad_arg;

return if (git._tag) |_tag| Optional.init(gpa, _tag) else Optional.Null;
}
};

pub const branch = struct {
pub const signature: Signature = .{ .ret = .String };
pub const description =
\\Returns the branch of the current commit.
\\If the current commit does not have a branch, an error is returned.
;
pub const examples =
\\<div :text="$build.git().branch()"></div>
\\<div :if="$build.git?()"><span :text="$if.branch()"></span></div>
;
pub fn call(
git: Git,
gpa: Allocator,
args: []const Value,
) !Value {
const bad_arg = .{
.err = "expected 0 arguments",
};
if (args.len != 0) return bad_arg;

return if (git._branch) |_branch| Value.from(gpa, _branch) else .{ .err = "No branch for this commit" };
}
};

pub const @"branch?" = struct {
pub const signature: Signature = .{ .ret = .String };
pub const description =
\\Returns the branch of the current commit.
\\If the current commit does not have a branch, null is returned.
;
pub const examples =
\\<div :if="$build.git().branch?()"><span :text="$if"></span></div>
\\<div :if="$build.git?()"><span :if="$if.branch?()"><span :text="$if"></span></span></div>
;
pub fn call(
git: Git,
gpa: Allocator,
args: []const Value,
) !Value {
const bad_arg = .{
.err = "expected 0 arguments",
};
if (args.len != 0) return bad_arg;

return if (git._branch) |_branch| Optional.init(gpa, _branch) else Optional.Null;
}
};
};
2 changes: 2 additions & 0 deletions src/context/doctypes.zig
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub const ScriptyParam = union(enum) {
Site,
Page,
Build,
Git,
Asset,
Alternative,
ContentSection,
Expand Down Expand Up @@ -71,6 +72,7 @@ pub const ScriptyParam = union(enum) {
context.Page, *const context.Page => .Page,
context.Site, *const context.Site => .Site,
context.Build => .Build,
context.Git => .Git,
superhtml.utils.Ctx(context.Value) => .Ctx,
context.Page.Alternative => .Alternative,
context.Page.ContentSection => .ContentSection,
Expand Down
Loading

0 comments on commit cada8ea

Please sign in to comment.