Lindenii Project Forge
Function documentation
use fs; use io; use errors; use strconv; use encoding::utf8;
// Errors possible while handling Git repositories.
export type error = ( fs::error | io::error | errors::invalid | strconv::invalid | strconv::overflow | utf8::invalid | nomem );
use bytes; use errors; 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) = { 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;
// 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) = { 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; }; };
// 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, ) ((u8, []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 expect = strconv::stoz(szs)?; if (expect != len(body)) { return errors::invalid; }; let code: u8 = 0u8; if (ty == "blob") { code = OBJ_BLOB; } else if (ty == "tree") { code = OBJ_TREE; } else if (ty == "commit") { code = OBJ_COMMIT; } else { return errors::invalid; }; let out = alloc(body...)?; return (code, out); };
// A simple Git blob with its object ID and raw data.
export type blob = struct { oid: oid, 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 and object ID. // The data is copied and the resulting blob // must be finished with [[blob_finish]].
export fn parse_blob(id: oid, body: []u8) (blob | nomem) = { let data = alloc(body...)?; return blob { oid = id, data = data }; };
use bytes; use errors; use strconv; use strings;
// A Git commit object.
export type commit = struct { oid: oid, 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( id: oid, body: []u8, ) (commit | errors::invalid | strconv::invalid | strconv::overflow | nomem) = { let c = commit { oid = id, 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..]); 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..]); 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 errors; use fs; use io; use strconv; use strings;
// A Git tree object.
export type tree = struct { oid: oid, 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(id: oid, body: []u8) (tree | errors::invalid | strconv::invalid | strconv::overflow | 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 = strconv::stou32(mode_s, strconv::base::OCT)?; const name = alloc(name_b...)?; append(entries, tree_entry { mode = mode, name = name, oid = child })!; }; return tree { oid = id, entries = entries }; };
fn tree_entry_by_name_raw(t: *const tree, name: []const u8) (*const tree_entry | void) = {
// 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) { match (read_object(r, root.oid)) { case let t: tree => return t; case => return errors::invalid; }; }; let cur: tree = *root; let owned_cur = false; defer if (owned_cur) { tree_finish(cur); }; 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(&cur, 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_cur) { tree_finish(cur); }; return t; case let b: blob => if (owned_cur) { tree_finish(cur); }; return b; case => if (owned_cur) { tree_finish(cur); }; return errors::invalid; }; } else { match (read_object(r, ent.oid)) { case let t: tree => if (owned_cur) { tree_finish(cur); }; cur = t; owned_cur = true; case => if (owned_cur) { tree_finish(cur); }; return errors::invalid; }; i = j + 1z; if (i >= len(path)) { return errors::invalid; }; }; }; return errors::invalid; };
use bytes; use crypto::sha256; use errors; use fmt; use fs; use hash; use io; use strconv; use strings;
// Any Git object. [[tag]] will be supported in the future.
export type object = (blob | tree | commit);
// TODO: These should be enums instead of u8 constants.
export def OBJ_COMMIT: u8 = 1u8; export def OBJ_TREE: u8 = 2u8; export def OBJ_BLOB: u8 = 3u8; export def OBJ_TAG: u8 = 4u8; export def OBJ_OFS_DELTA: u8 = 6u8; export def OBJ_REF_DELTA: u8 = 7u8;
// 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 => abort("Unknown object type being freed...");
}; };
// Formats a Git object ID as a hexadecimal string.
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; }; };
// Verifies that the given body matches the given object ID.
export fn verify_typed(ty: str, body: []u8, want: oid) bool = { let st = sha256::sha256(); defer hash::close(&st); if (ty == "blob") { hash::write(&st, strings::toutf8("blob")); } else if (ty == "tree") { hash::write(&st, strings::toutf8("tree")); } else if (ty == "commit") { hash::write(&st, strings::toutf8("commit")); } else { 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); if (bytes::equal(got[..], want[..])) { return true; } else { return false; }; };
// Reads a Git object from the repository by its ID.
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; } 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 compress::zlib; use crypto::sha256; use endian; use errors; use fmt; use fs; use io; use strconv; use strings; def IDX_MAGIC: u32 = 0xff744f63u32; def IDX_V2: u32 = 2u32; def PACK_MAGIC: u32 = 0x5041434bu32; def PACK_V2: u32 = 2u32; type pack_loc = struct { pack_rel: str, ofs: u64, }; fn cmp_oid(a: []u8, b: oid) i32 = { for (let i = 0z; i < sha256::SZ; i += 1z) { let av = a[i]; let bv = b[i]; if (av < bv) { return -1; }; if (av > bv) { return 1; }; }; return 0; }; fn count_large_before(off32: []u8, idx: size) size = { let n = 0z; for (let i = 0z; i < idx; i += 1z) { let o32 = endian::begetu32(off32[i*4z .. i*4z + 4z]); if ((o32 & 0x8000_0000u32) != 0u32) { n += 1z; }; }; return n; };
// Reads a packed object by its ID from the given repository.
export fn read_packed( r: repo, id: oid, ) (object | fs::error | io::error | errors::invalid | strconv::invalid | strconv::overflow | errors::noentry | nomem) = { let loc = find_in_indexes(r, id)?; return read_from_pack_at(r, loc, id); }; fn find_in_indexes( r: repo, id: oid, ) (pack_loc | errors::noentry | fs::error | io::error | errors::invalid | nomem) = { const dir = "objects/pack"; let it = fs::iter(r.root, dir)?; defer fs::finish(it); for (true) { match (fs::next(it)) { case let de: fs::dirent => if (!strings::hassuffix(de.name, ".idx")) { continue; }; { let rel = fmt::asprintf("{}/{}", dir, de.name)?; match (idx_lookup(r, rel, id)) { case let pl: pack_loc => free(rel); return pl; case errors::noentry => free(rel); continue; case let fe: fs::error => free(rel); return fe; case let ioe: io::error => free(rel); return ioe; case let inv: errors::invalid => free(rel); return inv; case nomem => free(rel); return nomem; }; }; case done => break; case let fe: fs::error => return fe; }; }; return errors::noentry; }; fn idx_lookup( r: repo, idx_rel: const str, id: oid, ) (pack_loc | errors::noentry | fs::error | io::error | errors::invalid | nomem) = { let h = fs::open(r.root, idx_rel)?; defer io::close(h)!; let buf = io::drain(h)?; defer free(buf); if (len(buf) < 8z + 256z*4z) { return errors::invalid; }; let off = 0z; let magic = endian::begetu32(buf[off..off+4]); off += 4z; if (magic != IDX_MAGIC) { return errors::invalid; }; let ver = endian::begetu32(buf[off..off+4]); off += 4z; if (ver != IDX_V2) { return errors::invalid; }; let fanout: [256]u32 = [0...]; for (let i = 0z; i < 256z; i += 1z) { fanout[i] = endian::begetu32(buf[off..off+4]); off += 4z; }; let nobj = fanout[255]: size; let need = off + nobj * sha256::SZ + nobj * 4z + nobj * 4z + 2z * sha256::SZ; if (need > len(buf)) { return errors::invalid; }; let names_off = off; let crcs_off = names_off + nobj * sha256::SZ; let off32_off = crcs_off + nobj * 4z; let large_count = 0z; for (let i = 0z; i < nobj; i += 1z) { let o32 = endian::begetu32(buf[off32_off + i*4z .. off32_off + i*4z + 4z]); if ((o32 & 0x8000_0000u32) != 0u32) { large_count += 1z; }; }; let off64_off = off32_off + nobj * 4z; let trailer_off = off64_off + large_count * 8z; if (trailer_off + 2z * sha256::SZ > len(buf)) { return errors::invalid; }; let first = (id[0]: u8): size; let lo: size = if (first == 0u8) { yield 0z; } else { yield fanout[first - 1z]: size; }; let hi: size = fanout[first]: size; let found = false; let idx = 0z; let l = lo; let h = hi; for (l < h) { let m = l + (h - l) / 2z; let cand = buf[names_off + m*sha256::SZ .. names_off + (m+1z)*sha256::SZ]; let c = cmp_oid(cand, id); if (c == 0) { found = true; idx = m; break; } else if (c < 0) { l = m + 1z; } else { h = m; }; }; if (!found) { return errors::noentry; }; let o32 = endian::begetu32(buf[off32_off + idx*4z .. off32_off + idx*4z + 4z]); let ofs: u64 = 0u64; if ((o32 & 0x8000_0000u32) == 0u32) { ofs = (o32: u64); } else { let nlarge_before = count_large_before(buf[off32_off..], idx); let p = off64_off + nlarge_before * 8z; let o64be = endian::begetu64(buf[p .. p + 8z]); ofs = o64be; }; if (!strings::hassuffix(idx_rel, ".idx")) { return errors::invalid; }; let stem = strings::bytesub(idx_rel, 0z, len(idx_rel) - 4z)!; let packpath = fmt::asprintf("{}{}", stem, ".pack")?; return pack_loc { pack_rel = packpath, ofs = ofs }; }; fn read_from_pack_at( r: repo, loc: pack_loc, want: oid, ) (object | fs::error | io::error | errors::invalid | strconv::invalid | strconv::overflow | errors::noentry | nomem) = { defer free(loc.pack_rel); let h = fs::open(r.root, loc.pack_rel)?; defer io::close(h)!; let header: [12]u8 = [0...]; match (io::readall(h, header)) { case size => void; case io::EOF => return errors::invalid; case let ioe: io::error => return ioe; }; let magic = endian::begetu32(header[..4]); let ver = endian::begetu32(header[4..8]); if (magic != PACK_MAGIC || ver != PACK_V2) { return errors::invalid; }; io::seek(h, (loc.ofs: i64), io::whence::SET)?; let ty: u8 = 0u8; match (read_obj_header(h)) { case let t: (u8, size, size) => ty = t.0; case let ioe: io::error => return ioe; case => return errors::invalid; }; let full_ty: u8 = 0u8; let body: []u8 = []; defer if (len(body) != 0) { free(body); }; switch (ty) { case OBJ_COMMIT => body = inflate_section(h)?; full_ty = OBJ_COMMIT; case OBJ_TREE => body = inflate_section(h)?; full_ty = OBJ_TREE; case OBJ_BLOB => body = inflate_section(h)?; full_ty = OBJ_BLOB; case OBJ_REF_DELTA => match (resolve_ref_delta(r, h)) { case let t: (u8, []u8) => full_ty = t.0; body = t.1; case let e: (fs::error | io::error | errors::invalid | errors::noentry | nomem) => return e; }; case OBJ_OFS_DELTA => match (resolve_ofs_delta(r, h, loc)) { case let t: (u8, []u8) => full_ty = t.0; body = t.1; case let e: (fs::error | io::error | errors::invalid | errors::noentry | nomem) => return e; }; case => return errors::invalid; }; let tystr = if (full_ty == OBJ_BLOB) { yield "blob"; } else if (full_ty == OBJ_TREE) { yield "tree"; } else if (full_ty == OBJ_COMMIT) { yield "commit"; } else { yield ""; }; if (tystr == "" || !verify_typed(tystr, body, want)) { return errors::invalid; }; if (full_ty == OBJ_BLOB) { const b = parse_blob(want, body)?; return (b: object); }; if (full_ty == OBJ_TREE) { const t = parse_tree(want, body)?; return (t: object); }; if (full_ty == OBJ_COMMIT) { const c = parse_commit(want, body)?; return (c: object); }; return errors::invalid; }; fn read_obj_header(h: io::handle) ((u8, size, size) | io::error | errors::invalid) = { let consumed = 0z; let b0: [1]u8 = [0]; match (io::readall(h, b0)) { case size => void; case io::EOF => return errors::invalid; case let ioe: io::error => return ioe; }; consumed += 1z; let ty = (b0[0] >> 4) & 0x07u8; let sz: size = (b0[0] & 0x0fu8): size; let shift = 4z; if ((b0[0] & 0x80u8) != 0u8) { for (true) { let bb: [1]u8 = [0]; match (io::readall(h, bb)) { case size => void; case io::EOF => return errors::invalid; case let ioe: io::error => return ioe; }; consumed += 1z; let v = (bb[0] & 0x7fu8): size; sz += v << shift; if ((bb[0] & 0x80u8) == 0u8) { break; }; shift += 7z; }; }; return (ty, sz, consumed); }; fn inflate_section(h: io::handle) ([]u8 | io::error | nomem) = { let zr = zlib::decompress(h)?; defer io::close(&zr.vtable)!; let out = io::drain(&zr.vtable)?; return out; }; fn resolve_ref_delta( r: repo, h: io::handle, ) ((u8, []u8) | fs::error | io::error | errors::invalid | errors::noentry | strconv::invalid | strconv::overflow | nomem) = { let base: oid = [0...]; match (io::readall(h, base)) { case size => void; case io::EOF => return errors::invalid; case let ioe: io::error => return ioe; }; let delta = inflate_section(h)?; defer free(delta); let bt = read_resolved_body_by_id(r, base)?; let out = apply_delta(bt.1, delta)?; return (bt.0, out); }; fn read_ofs_distance(h: io::handle) (u64 | io::error | errors::invalid) = { let b: [1]u8 = [0]; match (io::readall(h, b)) { case size => void; case io::EOF => return errors::invalid; case let ioe: io::error => return ioe; }; let dist: u64 = (b[0] & 0x7fu8): u64; if ((b[0] & 0x80u8) != 0u8) { for (true) { match (io::readall(h, b)) { case size => void; case io::EOF => return errors::invalid; case let ioe: io::error => return ioe; }; dist = ((dist + 1u64) << 7u64) + ((b[0] & 0x7fu8): u64); if ((b[0] & 0x80u8) == 0u8) { break; }; }; }; return dist; }; fn resolve_ofs_delta( r: repo, h: io::handle, loc: pack_loc, ) ((u8, []u8) | fs::error | io::error | errors::invalid | errors::noentry | strconv::invalid | strconv::overflow | nomem) = { let dist = read_ofs_distance(h)?; let base_ofs: u64 = if (loc.ofs > dist) { yield loc.ofs - dist; } else { yield 0u64; }; if (base_ofs == 0u64) { return errors::invalid; }; let bt = read_resolved_body_at_ofs(r, loc.pack_rel, base_ofs)?; let delta = inflate_section(h)?; defer free(delta); let out = apply_delta(bt.1, delta)?; return (bt.0, out); }; fn read_resolved_body_by_id( r: repo, id: oid, ) ((u8, []u8) | fs::error | io::error | errors::invalid | errors::noentry | strconv::invalid | strconv::overflow | nomem) = { match (find_in_indexes(r, id)) { case let pl: pack_loc => let res = read_resolved_body_at_ofs(r, pl.pack_rel, pl.ofs); free(pl.pack_rel); return res; case errors::noentry => return read_loose_typed(r, id); case let fe: fs::error => return fe; case let ioe: io::error => return ioe; case let inv: errors::invalid => return inv; case nomem => return nomem; }; }; fn read_resolved_body_at_ofs( r: repo, pack_rel: str, ofs: u64, ) ((u8, []u8) | fs::error | io::error | errors::invalid | errors::noentry | strconv::invalid | strconv::overflow | nomem) = { let h = fs::open(r.root, pack_rel)?; defer io::close(h)!; let header: [12]u8 = [0...]; match (io::readall(h, header)) { case size => void; case io::EOF => return errors::invalid; case let ioe: io::error => return ioe; }; let magic = endian::begetu32(header[..4]); let ver = endian::begetu32(header[4..8]); if (magic != PACK_MAGIC || ver != PACK_V2) { return errors::invalid; }; io::seek(h, (ofs: i64), io::whence::SET)?; match (read_obj_header(h)) { case let t: (u8, size, size) => switch (t.0) { case OBJ_COMMIT => let body = inflate_section(h)?; return (OBJ_COMMIT, body); case OBJ_TREE => let body = inflate_section(h)?; return (OBJ_TREE, body); case OBJ_BLOB => let body = inflate_section(h)?; return (OBJ_BLOB, body); case OBJ_REF_DELTA => let base: oid = [0...]; match (io::readall(h, base)) { case size => void; case io::EOF => return errors::invalid; case let ioe: io::error => return ioe; }; let delta = inflate_section(h)?; defer free(delta); let bt = read_resolved_body_by_id(r, base)?; let out = apply_delta(bt.1, delta)?; return (bt.0, out); case OBJ_OFS_DELTA => let dist = read_ofs_distance(h)?; let base_ofs: u64 = if (ofs > dist) { yield ofs - dist; } else { yield 0u64; }; if (base_ofs == 0u64) { return errors::invalid; }; let delta = inflate_section(h)?; defer free(delta); let bt = read_resolved_body_at_ofs(r, pack_rel, base_ofs)?; let out = apply_delta(bt.1, delta)?; return (bt.0, out); case => return errors::invalid; }; case let ioe: io::error => return ioe; case => return errors::invalid; }; }; fn apply_delta(base: []u8, delta: []u8) ([]u8 | errors::invalid | nomem) = { let i = 0z; let srcsz = read_varint(delta, &i)?; let dstsz = read_varint(delta, &i)?; if (srcsz != len(base)) { return errors::invalid; }; let out: []u8 = alloc([0u8...], dstsz)?; let outpos = 0z; for (i < len(delta)) { let op = delta[i]; i += 1z; if ((op & 0x80u8) != 0u8) { let off = 0z; let n = 0z; if ((op & 0x01u8) != 0u8) { if (i >= len(delta)) { return errors::invalid; }; off |= (delta[i]: size); i += 1z; }; if ((op & 0x02u8) != 0u8) { if (i >= len(delta)) { return errors::invalid; }; off |= (delta[i]: size) << 8z; i += 1z; }; if ((op & 0x04u8) != 0u8) { if (i >= len(delta)) { return errors::invalid; }; off |= (delta[i]: size) << 16z; i += 1z; }; if ((op & 0x08u8) != 0u8) { if (i >= len(delta)) { return errors::invalid; }; off |= (delta[i]: size) << 24z; i += 1z; }; if ((op & 0x10u8) != 0u8) { if (i >= len(delta)) { return errors::invalid; }; n |= (delta[i]: size); i += 1z; }; if ((op & 0x20u8) != 0u8) { if (i >= len(delta)) { return errors::invalid; }; n |= (delta[i]: size) << 8z; i += 1z; }; if ((op & 0x40u8) != 0u8) { if (i >= len(delta)) { return errors::invalid; }; n |= (delta[i]: size) << 16z; i += 1z; }; if (n == 0z) { n = 0x10000z; }; if (off + n > len(base) || outpos + n > len(out)) { return errors::invalid; }; out[outpos .. outpos + n] = base[off .. off + n]; outpos += n; } else if (op != 0u8) { let n = (op: size); if (i + n > len(delta) || outpos + n > len(out)) { return errors::invalid; }; out[outpos .. outpos + n] = delta[i .. i + n]; i += n; outpos += n; } else { return errors::invalid; }; }; if (outpos != len(out)) { return errors::invalid; }; return out; }; fn read_varint(buf: []u8, ip: *size) (size | errors::invalid) = { let res = 0z; let shift = 0z; for (true) { if (*ip >= len(buf)) { return errors::invalid; }; let b = buf[*ip]; *ip += 1z; res |= ((b & 0x7fu8): size) << shift; if ((b & 0x80u8) == 0u8) { break; }; shift += 7z; }; return res; };
use bytes; use errors; use fs; use io; use strings;
// Resolve a Git ref to its object ID from its fully qualified ref name.
export fn resolve_ref(r: repo, refname: const str) (oid | error) = { { match (fs::open(r.root, refname)) { case let fh: io::handle => defer io::close(fh)!; let b = io::drain(fh)?; defer free(b); let n = if (len(b) > 0 && b[len(b)-1] == '\n') { yield len(b) - 1z; } else { yield len(b); }; let s = strings::fromutf8(b[..n])?; return parse_oid(s)?; case let fe: fs::error => if (!(fe is errors::noentry)) { return fe; }; }; }; match (fs::open(r.root, "packed-refs")) { case let fh: io::handle => defer io::close(fh)!; let pr = io::drain(fh)?; defer free(pr); let want = strings::toutf8(refname); let i = 0z; for (i < len(pr)) { let e = match (bytes::index(pr[i..], '\n')) { case let j: size => yield i + j; case void => yield len(pr); }; let line = pr[i..e]; if (len(line) >= 1 && (line[0] == '#' || line[0] == '^')) { void; } else if (len(line) >= 66z) { let sp = bytes::index(line, ' '); if (sp is size) { let k = (sp: size); if (k == 64z && k + 1z < len(line)) { let name_b = line[k + 1z..]; if (bytes::equal(name_b, want)) { let hexs = strings::fromutf8(line[..k])?; return parse_oid(hexs)?; }; }; }; }; i = if (e < len(pr) && pr[e] == '\n') { yield e + 1z; } else { yield e; }; }; return errors::invalid; case let fe: fs::error => if (fe is errors::noentry) { return errors::invalid; }; return fe; }; };
// Reads and resolves the HEAD ref to its object ID.
export fn head_oid(r: repo) (oid | error) = { let fh = fs::open(r.root, "HEAD")?; defer io::close(fh)!; let b = io::drain(fh)?; defer free(b); let n = if (len(b) > 0 && b[len(b)-1] == '\n') { yield len(b) - 1z; } else { yield len(b); }; let line = b[..n]; const pfx = strings::toutf8("ref: "); if (len(line) >= len(pfx) && bytes::hasprefix(line, pfx)) { let rn = strings::fromutf8(line[len(pfx)..])?; return resolve_ref(r, rn); }; let s = strings::fromutf8(line)?; return parse_oid(s)?; };
use fs; use os;
// A Git repository.
export type repo = struct { root: *fs::fs, };
// Open a repository at the given path.
export fn open(path: const str) (repo | fs::error) = { return repo { root = os::diropen(path)?, }; };
// Close a repository, freeing its resources.
export fn close(r: repo) void = { fs::close(r.root); };
use errors; use fs; use io; use strconv;
// List recent commits starting from the given commit ID, following first // parents only. // TODO: Consider cases with merge commits.
export fn recent_commits(r: repo, start: oid, limit: int) ([]commit | error) = { if (limit <= 0) { let empty: []commit = []; return empty; }; let out: []commit = []; let cur = start; for (let n = 0; n < limit; n += 1) { match (read_object(r, cur)) { case let c: commit => append(out, c)!; if (len(c.parents) == 0) { return out; }; cur = c.parents[0]; case let b: blob => blob_finish(b); return errors::invalid; case let t: tree => tree_finish(t); return errors::invalid; case let e: (fs::error | io::error | errors::invalid | strconv::invalid | strconv::overflow | nomem) => return (e: error); }; }; return out; };