Lindenii Project Forge
Login

hare-git

Git library for Hare
Commit info
ID
5698bc4d1af0ed8286189295288fb144e27db325
Author
Runxi Yu <me@runxiyu.org>
Author date
Sun, 21 Sep 2025 03:15:38 +0800
Committer
Runxi Yu <me@runxiyu.org>
Committer date
Sun, 21 Sep 2025 03:15:48 +0800
Actions
Stop using _is gihg
use bytes;
use errors;
use encoding::utf8;
use strings;
use strconv;

// Author/committer identity and its associated timestamp
// and timezone offset.
export type ident = struct {
	name: []u8,
	email: []u8,
	when: i64,
	ofs: i32,
};

// Frees resources associated with an [[ident]].
export fn ident_finish(p: ident) void = {
	free(p.name);
	free(p.email);
};

// Parses an [[ident]] from its canonical byte-slice representation.
fn parse_ident(
	line: []u8,
) (ident | errors::invalid | strconv::invalid | strconv::overflow | nomem) = {
) (ident | errors::invalid | strconv::invalid | strconv::overflow | utf8::invalid | 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 when_s = strings::fromutf8(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 hh = strconv::stou32(strings::fromutf8(tz_b[1..3])?, strconv::base::DEC)?;
	const mm = strconv::stou32(strings::fromutf8(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 encoding::utf8;
use errors;
use fmt;
use fs;
use io;
use strconv;
use strings;
use encoding::utf8;

// Find the path to a loose object with the given ID,
// relative to the repository root.
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);
};

// Reads a loose object from the repository by its ID.
export fn read_loose(
	r: repo,
	id: oid,
) (object | fs::error | io::error | errors::invalid | strconv::invalid | strconv::overflow | nomem | utf8::invalid) = {
	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 ty = strings::fromutf8(header[..sp])?;
	const szs = strings::fromutf8(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(body)?;
		return (b: object);
	} else if (ty == "tree") {
		const t = parse_tree(body)?;
		return (t: object);
	} else if (ty == "commit") {
		const c = parse_commit(body)?;
		return (c: object);
	} else if (ty == "tag") {
		const g = parse_tag(body)?;
		return (g: object);
	} else {
		return errors::invalid;
	};
};

// Reads a loose object from the repository by its ID,
// returning its type and raw data.
export fn read_loose_typed(
	r: repo,
	id: oid,
) ((objtype, []u8) | fs::error | io::error | errors::invalid | errors::noentry | strconv::invalid | strconv::overflow | nomem) = {
	const rel = loose_relpath(id)?;
	defer free(rel);

	let 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 ty = strings::fromutf8(header[..sp])?;
	const szs = strings::fromutf8(header[sp + 1z ..])?;
	const expect = strconv::stoz(szs)?;
	if (expect != len(body)) {
		return errors::invalid;
	};

	let code: objtype = objtype::OBJ_INVALID;
	if (ty == "blob") {
		code = objtype::OBJ_BLOB;
	} else if (ty == "tree") {
		code = objtype::OBJ_TREE;
	} else if (ty == "commit") {
		code = objtype::OBJ_COMMIT;
	} else if (ty == "tag") {
		code = objtype::OBJ_TAG;
	} else {
		return errors::invalid;
	};

	let out = alloc(body...)?;
	return (code, out);
};
use bytes;
use encoding::utf8;
use errors;
use strconv;
use strings;

// A Git commit object.
export type commit = struct {
	tree: oid,
	parents: []oid,
	author: ident,
	committer: ident,
	message: []u8,
	// other raw headers?
};

// Frees resources associated with a [[commit]].
export fn commit_finish(c: commit) void = {
	free(c.parents);
	ident_finish(c.author);
	ident_finish(c.committer);
	free(c.message);
};

// Parses a commit from its raw data and object ID.
export fn parse_commit(
	body: []u8,
) (commit | errors::invalid | strconv::invalid | strconv::overflow | nomem) = {
) (commit | errors::invalid | strconv::invalid | strconv::overflow | utf8::invalid | nomem) = {
	let c = commit {
		tree = [0...],
		parents = [],
		author = ident { name = [], email = [], when = 0, ofs = 0 },
		committer = ident { name = [], email = [], when = 0, ofs = 0 },
		message = [],
	};

	let i = 0z;
	for (true) {
		let mrel = bytes::index(body[i..], '\n');
		if (mrel is void) {
			return errors::invalid;
		};
		let rel = mrel: size;

		const line = body[i .. i + rel];

		if (len(line) == 0) {
			i += rel + 1z;
			break;
		};

		if (bytes::hasprefix(line, strings::toutf8("tree "))) {
			const hex = strings::fromutf8_unsafe(line[5..]);
			const hex = strings::fromutf8(line[5..])?;
			match (parse_oid(hex)) {
			case let o: oid =>
				c.tree = o;
			case nomem =>
				return nomem;
			case =>
				return errors::invalid;
			};
		} else if (bytes::hasprefix(line, strings::toutf8("parent "))) {
			const hex = strings::fromutf8_unsafe(line[7..]);
			const hex = strings::fromutf8(line[7..])?;
			match (parse_oid(hex)) {
			case let o: oid =>
				append(c.parents, o)!;
			case nomem =>
				return nomem;
			case =>
				return errors::invalid;
			};
		} else if (bytes::hasprefix(line, strings::toutf8("author "))) {
			const per = parse_ident(line[7..])?;
			ident_finish(c.author);
			c.author = per;
		} else if (bytes::hasprefix(line, strings::toutf8("committer "))) {
			const per = parse_ident(line[10..])?;
			ident_finish(c.committer);
			c.committer = per;
		} else if (
			bytes::hasprefix(line, strings::toutf8("gpgsig ")) ||
			bytes::hasprefix(line, strings::toutf8("gpgsig-sha256 "))
		) {
			i += rel + 1z;
			for (true) {
				if (i >= len(body)) {
					return errors::invalid;
				};
				let mnext = bytes::index(body[i..], '\n');
				if (mnext is void) {
					return errors::invalid;
				};
				let next = mnext: size;
				if (body[i] != ' ') {
					break;
				};
				i += next + 1z;
			};
			continue;
		};

		i += rel + 1z;
	};

	c.message = alloc(body[i..]...)?;
	return c;
};

use bytes;
use crypto::sha256;
use encoding::utf8;
use errors;
use fs;
use io;
use strconv;
use strings;

// A Git tree object.
export type tree = struct {
	entries: []tree_entry,
};

// Frees resources associated with a [[tree]].
export fn tree_finish(t: tree) void = {
	for (let entry .. t.entries) {
		tree_entry_finish(entry);
	};
	free(t.entries);
};

// A single entry in a Git tree. In general, the oid
// either refers to a blob (file) or another tree (directory).
export type tree_entry = struct {
	mode: u32,
	name: []u8,
	oid: oid,
};

// Frees resources associated with a [[tree_entry]].
export fn tree_entry_finish(te: tree_entry) void = {
	free(te.name);
};

// Parses a tree from its raw data and object ID.
export fn parse_tree(body: []u8) (tree | errors::invalid | strconv::invalid | strconv::overflow | nomem) = {
export fn parse_tree(body: []u8) (tree | errors::invalid | strconv::invalid | strconv::overflow | utf8::invalid | nomem) = {
	let entries: []tree_entry = [];
	let i = 0z;

	for (i < len(body)) {
		const sp = match (bytes::index(body[i..], ' ')) {
		case let j: size => yield j;
		case void => return errors::invalid;
		};
		const mode_b = body[i .. i+sp];
		i += sp + 1z;

		const nul = match (bytes::index(body[i..], 0u8)) {
		case let j: size => yield j;
		case void => return errors::invalid;
		};
		const name_b = body[i .. i+nul];
		i += nul + 1z;

		if (i + sha256::SZ > len(body)) return errors::invalid;
		let child: oid = [0...];
		child[..] = body[i .. i+sha256::SZ];
		i += sha256::SZ;

		const mode_s = strings::fromutf8_unsafe(mode_b);
		const mode_s = strings::fromutf8(mode_b)?;
		const mode = strconv::stou32(mode_s, strconv::base::OCT)?;

		const name = alloc(name_b...)?;
		append(entries, tree_entry { mode = mode, name = name, oid = child })!;
	};

	return tree { entries = entries };
};

// Looks up a tree entry by name.
fn tree_entry_by_name_raw(t: *const tree, name: []const u8) (*const tree_entry | void) = {
	for (let i = 0z; i < len(t.entries); i += 1z) {
		if (bytes::equal(t.entries[i].name, name)) {
			return &t.entries[i];
		};
	};
	return void;
};

// Recursively looks up a tree or blob at the given path,
export fn tree_at_path(
	r: repo,
	root: const tree,
	path: const []u8,
) (tree | blob | errors::invalid | fs::error | io::error | strconv::invalid | strconv::overflow | nomem) = {
	if (len(path) == 0) {
		return root;
	};

	let owned_root = false;
	defer if (owned_root) {
		tree_finish(root);
	};

	let i = 0z;
	for (i < len(path)) {
		let j = match (bytes::index(path[i..], '/')) {
		case let k: size => yield i + k;
		case void => yield len(path);
		};
		if (j == i) {
			return errors::invalid;
		};
		let comp = path[i..j];

		let entp = tree_entry_by_name_raw(&root, comp);
		let ent: *const tree_entry = match (entp) {
		case let p: *const tree_entry =>
			yield p;
		case void =>
			return errors::invalid;
		};

		let last = (j == len(path));
		if (last) {
			match (read_object(r, ent.oid)) {
			case let t: tree =>
				if (owned_root) {
					tree_finish(root);
				};
				return t;
			case let b: blob =>
				if (owned_root) {
					tree_finish(root);
				};
				return b;
			case =>
				if (owned_root) {
					tree_finish(root);
				};
				return errors::invalid;
			};
		} else {
			match (read_object(r, ent.oid)) {
			case let t: tree =>
				if (owned_root) {
					tree_finish(root);
				};
				root = t;
				owned_root = true;
			case =>
				if (owned_root) {
					tree_finish(root);
				};
				return errors::invalid;
			};
			i = j + 1z;
			if (i >= len(path)) {
				return errors::invalid;
			};
		};
	};

	return errors::invalid;
};