Lindenii Project Forge
Login

hare-git

Git library for Hare
Commit info
ID
6d7fa8544b613e573ce0ab19da5be19c4284032b
Author
Runxi Yu <me@runxiyu.org>
Author date
Sun, 21 Sep 2025 14:34:34 +0800
Committer
Runxi Yu <me@runxiyu.org>
Committer date
Sun, 21 Sep 2025 14:34:34 +0800
Actions
Write objects
use bytes;
use compress::zlib;
use crypto::sha256;
use encoding::utf8;
use errors;
use fmt;
use fs;
use hash;
use io;
use memio;
use path;
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_path_resolve(id: oid) (str | nomem) = {
	const hex = oid_stringify(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 loose_read(
	r: repo,
	id: oid,
) (object | fs::error | io::error | errors::invalid | strconv::invalid | strconv::overflow | nomem | utf8::invalid) = {
	const rel = loose_path_resolve(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(header[..sp])?;
	const szs = strings::fromutf8(header[sp + 1z ..])?;
	const expect = strconv::stoz(szs)?;

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

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

	if (ty == "blob") {
		const b = blob_parse(body)?;
		return (b: object);
	} else if (ty == "tree") {
		const t = tree_parse(body)?;
		return (t: object);
	} else if (ty == "commit") {
		const c = commit_parse(body)?;
		return (c: object);
	} else if (ty == "tag") {
		const g = tag_parse(body)?;
		return (g: object);
	};

	return errors::invalid;
};

// Reads a loose object from the repository by its ID,
// returning its type and raw data.
export fn loose_read_typed(
	r: repo,
	id: oid,
) ((objtype, []u8) | fs::error | io::error | errors::invalid | errors::noentry | strconv::invalid | strconv::overflow | nomem) = {
	const rel = loose_path_resolve(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(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);
};

fn loose_body_slice(raw: []u8) ([]u8 | errors::invalid) = {
	let mnul = bytes::index(raw, 0u8);
	if (mnul is void) {
		return errors::invalid;
	};
	let nul = mnul: size;
	if (nul + 1z > len(raw)) {
		return errors::invalid;
	};
	return raw[nul + 1z ..];
};

// Writes a loose object given its type tag and raw body, returning the new ID.
export fn loose_write_typed(
	r: repo,
	ty: objtype,
	body: []u8,
) (oid | fs::error | io::error | errors::invalid | nomem) = {
	let tyname: const str = switch (ty) {
	case objtype::OBJ_BLOB => yield "blob";
	case objtype::OBJ_TREE => yield "tree";
	case objtype::OBJ_COMMIT => yield "commit";
	case objtype::OBJ_TAG => yield "tag";
	case =>
		return errors::invalid;
	};
	const tynameb = strings::toutf8(tyname);
	const size_str = strconv::ztos(len(body));
	const sizeb = strings::toutf8(size_str);

	let rawlen = len(tynameb) + 1z + len(sizeb) + 1z + len(body);
	let raw = alloc([0u8...], rawlen)?;
	defer free(raw);

	let pos = 0z;
	raw[pos .. pos + len(tynameb)] = tynameb;
	pos += len(tynameb);
	raw[pos] = ' ';
	pos += 1z;
	raw[pos .. pos + len(sizeb)] = sizeb;
	pos += len(sizeb);
	raw[pos] = 0u8;
	pos += 1z;
	raw[pos .. pos + len(body)] = body;

	let st = sha256::sha256();
	defer hash::close(&st);
	hash::write(&st, raw);

	let id: oid = [0...];
	hash::sum(&st, id);

	let src = memio::fixed(raw);
	let zw = zlib::compress(&src);
	let compressed: []u8 = [];
	match (io::drain(&zw.vtable)) {
	case let buf: []u8 =>
		compressed = buf;
	case let err: io::error => {
		match (io::close(&zw.vtable)) {
		case void =>
			return err;
		case let cerr: io::error =>
			return cerr;
		};
	};
	};
	defer free(compressed);

	match (io::close(&zw.vtable)) {
	case void => void;
	case let err: io::error =>
		return err;
	};

	let rel = loose_path_resolve(id)?;
	defer free(rel);

	const dir = path::dirname(rel);
	match (fs::mkdirs(r.root, dir, (0o755: fs::mode))) {
	case void => void;
	case let err: fs::error =>
		return err;
	};

	let file = match (fs::create(r.root, rel, (0o644: fs::mode))) {
	case let fh: io::handle =>
		yield fh;
	case let err: fs::error =>
		return err;
	};

	let write_res = io::writeall(file, compressed);
	let close_res = io::close(file);

	match (write_res) {
	case size => void;
	case let err: io::error =>
		return err;
	};

	match (close_res) {
	case void => void;
	case let err: io::error =>
		return err;
	};

	return id;
};

// Writes a loose object, returning the new ID.
export fn loose_write(
	r: repo,
	o: object,
) (oid | fs::error | io::error | errors::invalid | nomem) = {
	match (o) {
	case let b: blob =>
		return loose_write_typed(r, objtype::OBJ_BLOB, b.data);
	case let t: tree => {
		let serialized = tree_serialize(t)?;
		defer free(serialized);
		let body = loose_body_slice(serialized)?;
		return loose_write_typed(r, objtype::OBJ_TREE, body);
	};
	case let c: commit => {
		let serialized = commit_serialize(c)?;
		defer free(serialized);
		let body = loose_body_slice(serialized)?;
		return loose_write_typed(r, objtype::OBJ_COMMIT, body);
	};
	case let g: tag => {
		let serialized = tag_serialize(g)?;
		defer free(serialized);
		let body = loose_body_slice(serialized)?;
		return loose_write_typed(r, objtype::OBJ_TAG, body);
	};
	case =>
		abort("Unknown object variant written to loose storage");
	};
};
use strconv;
use strings;

// A simple Git blob with its object ID and raw data.
export type blob = struct {
	data: []u8,
};

// Frees resources associated with a [[blob]].
export fn blob_finish(b: blob) void = {
	free(b.data);
};

// Parses a blob from its raw data.
// The data is copied and the resulting blob
// must be finished with [[blob_finish]].
export fn blob_parse(body: []u8) (blob | nomem) = {
	let data = alloc(body...)?;
	return blob { data = data };
};


// Serializes a blob into the on-disk format.
// Serializes a blob into the uncompressed on-disk format.
export fn blob_serialize(b: blob) ([]u8 | nomem) = {
	const sizes = strconv::ztos(len(b.data));
	const ty = strings::toutf8("blob ");
	const sizesb = strings::toutf8(sizes);

	let hlen = len(ty) + len(sizesb) + 1z;
	let out = alloc([0u8...], hlen + len(b.data))?;
	let pos = 0z;

	out[pos .. pos + len(ty)] = ty;
	pos += len(ty);

	out[pos .. pos + len(sizesb)] = sizesb;
	pos += len(sizesb);

	out[pos] = 0u8;
	pos += 1z;

	out[pos .. pos + len(b.data)] = b.data;
	return 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 commit_parse(
	body: []u8,
) (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(line[5..])?;
			match (oid_parse(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(line[7..])?;
			match (oid_parse(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 = ident_parse(line[7..])?;
			ident_finish(c.author);
			c.author = per;
		} else if (bytes::hasprefix(line, strings::toutf8("committer "))) {
			const per = ident_parse(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;
};

// Serializes a commit into its on-disk format.
// Serializes a commit into its uncompressed on-disk format.
export fn commit_serialize(c: commit) ([]u8 | nomem) = {
	const treehex = oid_stringify(c.tree)?;
	defer free(treehex);

	let parenthex: []const str = [];
	for (let i = 0z; i < len(c.parents); i += 1z) {
		const hex = oid_stringify(c.parents[i])?;
		append(parenthex, hex)!;
	};

	let authorb = ident_serialize(c.author)?;
	defer free(authorb);

	let committerb = ident_serialize(c.committer)?;
	defer free(committerb);

	let bodylen = 0z;
	bodylen += 5z + len(treehex) + 1z;
	for (let i = 0z; i < len(parenthex); i += 1z) {
		bodylen += 7z + len(parenthex[i]) + 1z;
	};
	bodylen += 7z + len(authorb) + 1z;
	bodylen += 10z + len(committerb) + 1z;
	bodylen += 1z + len(c.message);

	const sizes = strconv::ztos(bodylen);
	const ty = strings::toutf8("commit ");
	const sizesb = strings::toutf8(sizes);
	let hlen = len(ty) + len(sizesb) + 1z;

	let out = alloc([0u8...], hlen + bodylen)?;
	let pos = 0z;

	out[pos .. pos + len(ty)] = ty;
	pos += len(ty);
	out[pos .. pos + len(sizesb)] = sizesb;
	pos += len(sizesb);
	out[pos] = 0u8;
	pos += 1z;

	{
		const pre = strings::toutf8("tree ");
		out[pos .. pos + len(pre)] = pre; pos += len(pre);
		const hb = strings::toutf8(treehex);
		out[pos .. pos + len(hb)] = hb; pos += len(hb);
		out[pos] = '\n'; pos += 1z;
	};

	for (let i = 0z; i < len(parenthex); i += 1z) {
		const pre = strings::toutf8("parent ");
		out[pos .. pos + len(pre)] = pre; pos += len(pre);
		const hb = strings::toutf8(parenthex[i]);
		out[pos .. pos + len(hb)] = hb; pos += len(hb);
		out[pos] = '\n'; pos += 1z;
	};

	{
		const pre = strings::toutf8("author ");
		out[pos .. pos + len(pre)] = pre; pos += len(pre);
		out[pos .. pos + len(authorb)] = authorb; pos += len(authorb);
		out[pos] = '\n'; pos += 1z;
	};

	{
		const pre = strings::toutf8("committer ");
		out[pos .. pos + len(pre)] = pre; pos += len(pre);
		out[pos .. pos + len(committerb)] = committerb; pos += len(committerb);
		out[pos] = '\n'; pos += 1z;
	};

	out[pos] = '\n';
	pos += 1z;
	out[pos .. pos + len(c.message)] = c.message;

	for (let i = 0z; i < len(parenthex); i += 1z) {
		free(parenthex[i]);
	};

	return out;
};
use bytes;
use encoding::utf8;
use errors;
use strconv;
use strings;

// A Git annotated tag object.
export type tag = struct {
	target: oid,
	target_type: objtype,
	name: []u8,
	tagger: (void | ident),
	message: []u8,
};

// Frees resources associated with a [[tag]].
export fn tag_finish(t: tag) void = {
	free(t.name);
	match (t.tagger) {
	case let id: ident =>
		ident_finish(id);
	case void =>
		void;
	};
	free(t.message);
};

// Parses a tag from its raw data.
export fn tag_parse(
	body: []u8,
) (tag | errors::invalid | strconv::invalid | strconv::overflow | utf8::invalid | nomem) = {
	let t = tag {
		target = [0...],
		target_type = objtype::OBJ_INVALID,
		name = [],
		tagger = void,
		message = [],
	};

	let have_target = false;
	let have_type = false;

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

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

		if (bytes::hasprefix(line, strings::toutf8("object "))) {
			const hex = strings::fromutf8(line[7..])?;
			match (oid_parse(hex)) {
			case let o: oid =>
				t.target = o;
				have_target = true;
			case =>
				return errors::invalid;
			};
		} else if (bytes::hasprefix(line, strings::toutf8("type "))) {
			const ty = strings::fromutf8(line[5..])?;
			if (ty == "commit") {
				t.target_type = objtype::OBJ_COMMIT;
			} else if (ty == "tree") {
				t.target_type = objtype::OBJ_TREE;
			} else if (ty == "blob") {
				t.target_type = objtype::OBJ_BLOB;
			} else if (ty == "tag") {
				t.target_type = objtype::OBJ_TAG;
			} else {
				return errors::invalid;
			};
			have_type = true;
		} else if (bytes::hasprefix(line, strings::toutf8("tag "))) {
			const name_b = line[4..];
			let name = alloc(name_b...)?;
			free(t.name);
			t.name = name;
		} else if (bytes::hasprefix(line, strings::toutf8("tagger "))) {
			const per = ident_parse(line[7..])?;
			match (t.tagger) {
			case let old: ident =>
				ident_finish(old);
			case void =>
				void;
			};
			t.tagger = 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;
	};

	if (!have_target || !have_type) {
		return errors::invalid;
	};

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

// Serializes a tag into the on-disk format.
// Serializes a commit into its uncompressed on-disk format.
export fn tag_serialize(t: tag) ([]u8 | errors::invalid | nomem) = {
	let tyname: (const str | void) = void;
	switch (t.target_type) {
	case objtype::OBJ_COMMIT => tyname = "commit";
	case objtype::OBJ_TREE   => tyname = "tree";
	case objtype::OBJ_BLOB   => tyname = "blob";
	case objtype::OBJ_TAG    => tyname = "tag";
	case => return errors::invalid;
	};
	const tynameb = strings::toutf8((tyname: const str));

	const objhex = oid_stringify(t.target)?;
	defer free(objhex);

	let taggerb: ([]u8 | void) = void;
	match (t.tagger) {
	case let id: ident =>
		taggerb = ident_serialize(id)?;
	case void =>
		void;
	};

	let bodylen = 0z;
	bodylen += 7z + len(objhex) + 1z;
	bodylen += 5z + len(tynameb) + 1z;
	bodylen += 4z + len(t.name) + 1z;
	match (taggerb) {
	case let tb: []u8 =>
		bodylen += 7z + len(tb) + 1z;
	case void =>
		void;
	};
	bodylen += 1z + len(t.message);

	const sizes = strconv::ztos(bodylen);
	const ty = strings::toutf8("tag ");
	const sizesb = strings::toutf8(sizes);
	let hlen = len(ty) + len(sizesb) + 1z;

	let out = alloc([0u8...], hlen + bodylen)?;
	let pos = 0z;

	out[pos .. pos + len(ty)] = ty; pos += len(ty);
	out[pos .. pos + len(sizesb)] = sizesb; pos += len(sizesb);
	out[pos] = 0u8; pos += 1z;

	{
		const pre = strings::toutf8("object ");
		out[pos .. pos + len(pre)] = pre; pos += len(pre);
		const hb = strings::toutf8(objhex);
		out[pos .. pos + len(hb)] = hb; pos += len(hb);
		out[pos] = '\n'; pos += 1z;
	};

	{
		const pre = strings::toutf8("type ");
		out[pos .. pos + len(pre)] = pre; pos += len(pre);
		out[pos .. pos + len(tynameb)] = tynameb; pos += len(tynameb);
		out[pos] = '\n'; pos += 1z;
	};

	{
		const pre = strings::toutf8("tag ");
		out[pos .. pos + len(pre)] = pre; pos += len(pre);
		out[pos .. pos + len(t.name)] = t.name; pos += len(t.name);
		out[pos] = '\n'; pos += 1z;
	};

	match (taggerb) {
	case let tb: []u8 =>
		const pre = strings::toutf8("tagger ");
		out[pos .. pos + len(pre)] = pre; pos += len(pre);
		out[pos .. pos + len(tb)] = tb; pos += len(tb);
		out[pos] = '\n'; pos += 1z;
	case void =>
		void;
	};

	out[pos] = '\n'; pos += 1z;
	out[pos .. pos + len(t.message)] = t.message;

	return out;
};
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 tree_parse(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(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_lookup_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_resolve_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_lookup_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 (object_read(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 (object_read(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;
};

// Serializes a tree into the on-disk format.
// Serializes a tree into its uncompressed on-disk format.
export fn tree_serialize(t: tree) ([]u8 | nomem) = {
	let bodylen = 0z;
	for (let e .. t.entries) {
		const modes = strconv::u32tos(e.mode, strconv::base::OCT);
		bodylen += len(strings::toutf8(modes));
		bodylen += 1z;
		bodylen += len(e.name);
		bodylen += 1z;
		bodylen += (sha256::SZ: size);
	};

	const sizes = strconv::ztos(bodylen);
	const ty = strings::toutf8("tree ");
	const sizesb = strings::toutf8(sizes);

	let hlen = len(ty) + len(sizesb) + 1z;
	let out = alloc([0u8...], hlen + bodylen)?;
	let pos = 0z;

	out[pos .. pos + len(ty)] = ty;
	pos += len(ty);
	out[pos .. pos + len(sizesb)] = sizesb;
	pos += len(sizesb);
	out[pos] = 0u8;
	pos += 1z;

	for (let e .. t.entries) {
		const modes = strconv::u32tos(e.mode, strconv::base::OCT);
		const modeb = strings::toutf8(modes);

		out[pos .. pos + len(modeb)] = modeb;
		pos += len(modeb);

		out[pos] = ' ';
		pos += 1z;

		out[pos .. pos + len(e.name)] = e.name;
		pos += len(e.name);

		out[pos] = 0u8;
		pos += 1z;

		out[pos .. pos + (sha256::SZ: size)] = e.oid[..];
		pos += (sha256::SZ: size);
	};

	return out;
};
use bytes;
use crypto::sha256;
use errors;
use fmt;
use fs;
use hash;
use io;
use strconv;
use strings;

// Object/pack type tags.
//
// These are not typically used as we could represent objects with tagged
// unions. However, they may be useful in scenarios where a full object is
// undesirable or unavailable.
export type objtype = enum u8 {
	OBJ_INVALID   = 0u8,
	OBJ_COMMIT    = 1u8,
	OBJ_TREE      = 2u8,
	OBJ_BLOB      = 3u8,
	OBJ_TAG       = 4u8,
	OBJ_FUTURE    = 5u8,
	OBJ_OFS_DELTA = 6u8,
	OBJ_REF_DELTA = 7u8,
};

// Any Git object.
export type object = (blob | tree | commit | tag);

// Frees resources associated with any Git object.
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 g: tag =>
		tag_finish(g);
	case =>
		abort("Unknown object type being freed...");
	};
};

// Serializes an object into its on-disk representation.
export fn object_serialize(o: object)
	([]u8 | errors::invalid | nomem) = {
	match (o) {
	case let b: blob =>
		return blob_serialize(b);
	case let t: tree =>
		return tree_serialize(t);
	case let c: commit =>
		return commit_serialize(c);
	case let g: tag =>
		return tag_serialize(g);
	case =>
		abort("Unknown object type being serialized...");
	};
};

// Verifies that the given buffer (which must be the exact on-disk format
// structured as "type size\0body") matches the given object ID.
export fn object_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);

	return bytes::equal(got[..], want[..]);
};

// Verifies that the given typed body matches the given object ID.
export fn object_verify_typed(ty: objtype, body: []u8, want: oid) bool = {
	let st = sha256::sha256();
	defer hash::close(&st);

	switch (ty) {
	case objtype::OBJ_BLOB =>
		hash::write(&st, strings::toutf8("blob"));
	case objtype::OBJ_TREE =>
		hash::write(&st, strings::toutf8("tree"));
	case objtype::OBJ_COMMIT =>
		hash::write(&st, strings::toutf8("commit"));
	case objtype::OBJ_TAG =>
		hash::write(&st, strings::toutf8("tag"));
	case =>
		return false;
	};

	hash::write(&st, strings::toutf8(" "));
	let szs = strconv::ztos(len(body));
	hash::write(&st, strings::toutf8(szs));
	hash::write(&st, strings::toutf8("\x00"));

	hash::write(&st, body);

	let got: oid = [0...];
	hash::sum(&st, got);

	return bytes::equal(got[..], want[..]);
};

// Reads a Git object from the repository by its ID.
export fn object_read(
	r: repo,
	id: oid,
) (object | fs::error | io::error | errors::invalid | strconv::invalid | strconv::overflow | nomem) = {
	match (loose_read(r, id)) {
	case let o: object =>
		return o;
	case let fe: fs::error =>
		if (fe is errors::noentry) {
			void;
		} else {
			return fe;
		};
	case let e: (io::error | errors::invalid | strconv::invalid | strconv::overflow | nomem) =>
		return e;
	};

	match (pack_read(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;
	};
};