Lindenii Project Forge
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; }; };