Lindenii Project Forge
Login

hare-git

Git library for Hare
Commit info
ID
8f2b1263c64201039ece5b50b4e31c8e3f060c91
Author
Runxi Yu <me@runxiyu.org>
Author date
Sun, 14 Sep 2025 04:19:46 +0800
Committer
Runxi Yu <me@runxiyu.org>
Committer date
Sun, 14 Sep 2025 04:35:22 +0800
Actions
Basic loose object parsing
/hare-git
use bytes;
use errors;
use strings;
use strconv;

export type ident = struct {
	name: []u8,
	email: []u8,
	when: i64,
	ofs: i32,
};

export fn ident_finish(p: ident) void = {
	free(p.name);
	free(p.email);
};

fn parse_ident(
	line: []u8,
) (ident | errors::invalid | strconv::invalid | strconv::overflow | nomem) = {
	let mlt = bytes::index(line, '<');
	if (mlt is void) {
		return errors::invalid;
	};
	let lt = mlt: size;

	let mgt_rel = bytes::index(line[lt + 1z..], '>');
	if (mgt_rel is void) {
		return errors::invalid;
	};
	let gt_rel = mgt_rel: size;

	const gt = lt + 1z + gt_rel;

	const name_b = line[..lt];
	const email_b = line[lt + 1z .. gt];

	let rest = line[gt + 1z..];
	if (len(rest) == 0 || rest[0] != ' ') {
		return errors::invalid;
	};
	rest = rest[1..];

	let msp = bytes::index(rest, ' ');
	if (msp is void) {
		return errors::invalid;
	};
	let sp = msp: size;

	const when_s = strings::fromutf8_unsafe(rest[..sp]);
	const tz_b = rest[sp + 1z..];
	if (len(tz_b) < 5) {
		return errors::invalid;
	};

	const when = strconv::stoi64(when_s)?;

	let sign: i32 = 1;
	if (tz_b[0] == '-') {
		sign = -1;
	};

	const hh = strconv::stou32(strings::fromutf8_unsafe(tz_b[1..3]), strconv::base::DEC)?;
	const mm = strconv::stou32(strings::fromutf8_unsafe(tz_b[3..5]), strconv::base::DEC)?;
	const mins: i32 = (hh: i32) * 60 + (mm: i32);
	const ofs: i32 = sign * mins;

	let name = alloc(name_b...)?;
	let email = alloc(email_b...)?;
	return ident { name = name, email = email, when = when, ofs = ofs };
};
use bytes;
use compress::zlib;
use errors;
use fmt;
use fs;
use io;
use strconv;
use strings;

fn loose_relpath(id: oid) (str | nomem) = {
	const hex = oid_string(id)?;
	defer free(hex);

	const dir = strings::bytesub(hex, 0z, 2z)!;
	const file = strings::bytesub(hex, 2z, strings::end)!;

	return fmt::asprintf("objects/{}/{}", dir, file);
};

export fn read_loose(
	r: repo,
	id: oid,
) (object | fs::error | io::error | errors::invalid | strconv::invalid | strconv::overflow | nomem) = {
	const rel = loose_relpath(id)?;
	defer free(rel);

	const fh = fs::open(r.root, rel)?;
	defer io::close(fh)!;

	let zr = zlib::decompress(fh)?;
	defer io::close(&zr.vtable)!;

	let buf = io::drain(&zr.vtable)?;
	defer free(buf);

	let mnul = bytes::index(buf, 0u8);
	if (mnul is void) {
		return errors::invalid;
	};
	let nul = mnul: size;

	const header = buf[..nul];
	const body = buf[nul + 1z ..];

	let msp = bytes::index(header, ' ');
	if (msp is void) {
		return errors::invalid;
	};
	let sp = msp: size;

	const ty = strings::fromutf8_unsafe(header[..sp]);
	const szs = strings::fromutf8_unsafe(header[sp + 1z ..]);
	const expect = strconv::stoz(szs)?;

	if (expect != len(body)) {
		return errors::invalid;
	};

	if (!verify_oid(buf, id)) {
		return errors::invalid;
	};

	if (ty == "blob") {
		const b = parse_blob(id, body)?;
		return (b: object);
	} else if (ty == "tree") {
		const t = parse_tree(id, body)?;
		return (t: object);
	} else if (ty == "commit") {
		const c = parse_commit(id, body)?;
		return (c: object);
	} else {
		return errors::invalid;
	};
};
use bytes;
use crypto::sha256;
use errors;
use fs;
use hash;
use io;
use strconv;

export type object = (blob | tree | commit);

export fn object_finish(o: object) void = {
	match (o) {
		case let b: blob => blob_finish(b);
		case let t: tree => tree_finish(t);
		case let c: commit => commit_finish(c);
	case let b: blob =>
		blob_finish(b);
	case let t: tree =>
		tree_finish(t);
	case let c: commit =>
		commit_finish(c);
	};
};

export fn verify_oid(buf: []u8, want: oid) bool = {
	let st = sha256::sha256();
	hash::write(&st, buf);
	let got: oid = [0...];
	hash::sum(&st, got);
	hash::close(&st);

	if (bytes::equal(got[..], want[..])) {
		return true;
	} else {
		return false;
	};
};

export fn read_object(
	r: repo,
	id: oid,
) (object | fs::error | io::error | errors::invalid | strconv::invalid | strconv::overflow | nomem) = {
	match (read_loose(r, id)) {
	case let o: object =>
		return o;
	case let fe: fs::error =>
		if (fe is errors::noentry) {
			void; // fallthrough to the packfile thing
		} else {
			return fe;
		};
	case let e: (io::error | errors::invalid | strconv::invalid | strconv::overflow | nomem) =>
		return e;
	};

	match (read_packed(r, id)) {
	case let o: object =>
		return o;
	case let fe: fs::error =>
		if (fe is errors::noentry) {
			return errors::invalid;
		} else {
			return fe;
		};
	case let e: (io::error | errors::invalid | strconv::invalid | strconv::overflow | nomem) =>
		return e;
	};
};
use errors;
use fs;
use io;
use strconv;

export fn read_packed(
	r: repo,
	id: oid,
) (object | fs::error | io::error | errors::invalid | strconv::invalid | strconv::overflow | errors::noentry | nomem) = {
	return errors::noentry;
};
use fmt;
use fs;
use getopt;
use io;
use os;
use strings;

use git;

fn usage(cmd: *getopt::command) void = {
	getopt::printusage(os::stderr, os::args[0], cmd.help)!;
};

fn print_blob(b: git::blob) void = {
	io::write(os::stdout, b.data)!;
};

fn print_tree(t: git::tree) void = {
	for (let i = 0z; i < len(t.entries); i += 1z) {
		const ent = t.entries[i];

		const hex = git::oid_string(ent.oid)!;
		defer free(hex);

		const name = strings::fromutf8_unsafe(ent.name);
		fmt::printfln("{:o} {} {}", ent.mode, hex, name)!;
	};
};

fn print_ident(label: str, id: git::ident) void = {
	const name = strings::fromutf8_unsafe(id.name);
	const email = strings::fromutf8_unsafe(id.email);
	fmt::printfln("{} {} <{}> {} {}", label, name, email, id.when, id.ofs)!;
};

fn print_commit(c: git::commit) void = {
	const treehex = git::oid_string(c.tree)!;
	defer free(treehex);
	fmt::printfln("tree {}", treehex)!;

	for (let i = 0z; i < len(c.parents); i += 1z) {
		const phex = git::oid_string(c.parents[i])!;
		defer free(phex);
		fmt::printfln("parent {}", phex)!;
	};

	print_ident("author", c.author);
	print_ident("committer", c.committer);

	fmt::println("")!;

	const msg = strings::fromutf8_unsafe(c.message);
	fmt::println(msg)!;
};

export fn main() void = {
	let repo_path: (str | void) = void;

	let cmd = getopt::parse(
		os::args,
		"Print a Git object by OID",
		('r', "repo", "PATH to repo or .git"),
		"OID",
	);
	defer getopt::finish(&cmd);

	for (let opt .. cmd.opts) {
		switch (opt.0) {
		case 'r' =>
			repo_path = opt.1;
		case =>
			abort();
		};
	};

	if (len(cmd.args) != 1) {
		usage(&cmd);
		os::exit(2);
	};

	if (repo_path is void) {
		usage(&cmd);
		os::exit(2);
	};

	const rp = repo_path: str;

	let r = match (git::open(rp)) {
	case let rr: git::repo =>
		yield rr;
	case let fe: fs::error =>
		fmt::errorfln("open repo: {}", fs::strerror(fe))!;
		os::exit(1);
	};
	defer git::close(r);

	const oidhex = cmd.args[0];
	let id = match (git::parse_oid(oidhex)) {
	case let o: git::oid =>
		yield o;
	case =>
		fmt::errorfln("invalid oid: {}", oidhex)!;
		os::exit(1);
	};

	let obj = match (git::read_object(r, id)) {
	case let o: git::object =>
		yield o;
	case let fe: fs::error =>
		fmt::errorfln("fs error: {}", fs::strerror(fe))!;
		os::exit(1);
	case let ioe: io::error =>
		fmt::errorfln("io error: {}", io::strerror(ioe))!;
		os::exit(1);
	case =>
		fmt::errorfln("not a valid object: {}", oidhex)!;
		os::exit(1);
	};

	match (obj) {
	case let b: git::blob =>
		print_blob(b);
		git::blob_finish(b);
	case let t: git::tree =>
		print_tree(t);
		git::tree_finish(t);
	case let c: git::commit =>
		print_commit(c);
		git::commit_finish(c);
	};
};