///| Git repository materialization (.git + index)
///|
pub fn materialize_clone_to_fs(
store : ObjectStore,
commit_id : ObjectId,
refname : String,
remote_url : String,
fs : &FileSystem,
root : String,
) -> Unit raise GitError {
checkout_commit_to_fs(store, commit_id, fs, root)
write_git_metadata(store, commit_id, refname, remote_url, fs, root)
}
///|
pub fn write_git_metadata(
store : ObjectStore,
commit_id : ObjectId,
refname : String,
remote_url : String,
fs : &FileSystem,
root : String,
) -> Unit raise GitError {
let git_dir = join_path(root, ".git")
fs.mkdir_p(git_dir)
fs.mkdir_p(join_path(git_dir, "objects"))
fs.mkdir_p(join_path(git_dir, "objects/info"))
fs.mkdir_p(join_path(git_dir, "objects/pack"))
fs.mkdir_p(join_path(git_dir, "refs/heads"))
fs.mkdir_p(join_path(git_dir, "refs/tags"))
// Note: loose objects are not written here - pack file is used instead
write_head_and_refs(fs, git_dir, refname, commit_id)
write_config(fs, git_dir, remote_url)
write_packed_refs(fs, git_dir)
write_alternates(fs, git_dir)
write_index_v2(store, fs, git_dir, commit_id)
}
///|
fn write_head_and_refs(
fs : &FileSystem,
git_dir : String,
refname : String,
commit_id : ObjectId,
) -> Unit raise GitError {
let head_path = join_path(git_dir, "HEAD")
if refname == "HEAD" {
write_text(fs, head_path, commit_id.to_hex() + "\n")
return
}
write_text(fs, head_path, "ref: \{refname}\n")
let ref_path = join_path(git_dir, refname)
let dir = parent_dir(ref_path)
fs.mkdir_p(dir)
write_text(fs, ref_path, commit_id.to_hex() + "\n")
}
///|
fn write_config(
fs : &FileSystem,
git_dir : String,
remote_url : String,
) -> Unit raise GitError {
let config_path = join_path(git_dir, "config")
let base =
#|[core]
#| repositoryformatversion = 0
#| filemode = true
#| bare = false
#| logallrefupdates = true
let remote_block =
$|[remote "origin"]
$| url = \{remote_url}
$| fetch = +refs/heads/*:refs/remotes/origin/*
$|
let config = if remote_url.length() == 0 {
base + "\n"
} else {
base + "\n" + remote_block
}
write_text(fs, config_path, config)
}
///|
fn write_packed_refs(fs : &FileSystem, git_dir : String) -> Unit raise GitError {
let path = join_path(git_dir, "packed-refs")
write_text(fs, path, "# pack-refs with: peeled fully-peeled\n")
}
///|
fn write_alternates(fs : &FileSystem, git_dir : String) -> Unit raise GitError {
let path = join_path(git_dir, "objects/info/alternates")
write_text(fs, path, "")
}
///|
/// Write a packfile and its index under .git/objects/pack.
pub fn write_packfile_with_index(
fs : &FileSystem,
git_dir : String,
pack : Bytes,
objects : Array[PackObject],
) -> Unit raise GitError {
if pack.length() < 20 {
raise GitError::PackfileError("Packfile too short")
}
let pack_dir = join_path(git_dir, "objects/pack")
fs.mkdir_p(pack_dir)
let trailer_offset = pack.length() - 20
let pack_id = read_trailer_id(pack, trailer_offset)
let base = "pack-\{pack_id.to_hex()}"
let pack_path = join_path(pack_dir, base + ".pack")
let idx_path = join_path(pack_dir, base + ".idx")
fs.write_file(pack_path, pack)
write_pack_index_from_objects(fs, idx_path, pack, objects)
}
///|
fn write_index_v2(
store : ObjectStore,
fs : &FileSystem,
git_dir : String,
commit_id : ObjectId,
) -> Unit raise GitError {
let entries = collect_commit_entries(store, commit_id)
entries.sort_by(fn(a, b) { String::compare(a.path, b.path) })
let out : Array[Byte] = []
// Header: "DIRC" + version(2) + count
out.push(b'D')
out.push(b'I')
out.push(b'R')
out.push(b'C')
push_u32_be(out, 2)
push_u32_be(out, entries.length())
for e in entries {
// Git index v2 entry format (62 bytes fixed + variable name):
// ctime_sec (4) + ctime_nsec (4)
push_u32_be(out, 0)
push_u32_be(out, 0)
// mtime_sec (4) + mtime_nsec (4)
push_u32_be(out, 0)
push_u32_be(out, 0)
// dev (4) + ino (4)
push_u32_be(out, 0)
push_u32_be(out, 0)
// mode (4) - comes BEFORE uid/gid
push_u32_be(out, e.mode)
// uid (4) + gid (4)
push_u32_be(out, 0)
push_u32_be(out, 0)
// file_size (4)
push_u32_be(out, e.size)
// object id
for b in e.id.bytes {
out.push(b)
}
// Convert path to UTF-8 bytes
let path_bytes = @utf8.encode(e.path)
let name_byte_len = path_bytes.length()
let name_len = if name_byte_len < 0x0fff { name_byte_len } else { 0x0fff }
push_u16_be(out, name_len)
for i = 0; i < path_bytes.length(); i = i + 1 {
out.push(path_bytes[i])
}
out.push(b'\x00')
// Pad to 8-byte alignment from entry start
// Entry fixed part = 62 bytes, then name bytes + NUL + padding
// Total entry size must be multiple of 8
let entry_len = 62 + name_byte_len + 1 // fixed + name bytes + NUL
let padded_len = (entry_len + 7) & (0xFFFFFFFF - 7) // round up to 8
let padding = padded_len - entry_len
for _i = 0; _i < padding; _i = _i + 1 {
out.push(b'\x00')
}
}
let content = Bytes::from_array(
FixedArray::makei(out.length(), fn(i) { out[i] }),
)
let checksum = sha1(content)
for b in checksum.bytes {
out.push(b)
}
let index_bytes = Bytes::from_array(
FixedArray::makei(out.length(), fn(i) { out[i] }),
)
let index_path = join_path(git_dir, "index")
fs.write_file(index_path, index_bytes)
}
///|
priv struct IndexEntryInfo {
path : String
id : ObjectId
size : Int
mode : Int
}
///|
fn collect_commit_entries(
store : ObjectStore,
commit_id : ObjectId,
) -> Array[IndexEntryInfo] raise GitError {
let commit_obj = store.get(commit_id)
match commit_obj {
None => raise GitError::InvalidObject("Missing commit object")
Some(obj) => {
if obj.obj_type != ObjectType::Commit {
raise GitError::InvalidObject("Object is not a commit")
}
let info = parse_commit(obj.data)
let out : Array[IndexEntryInfo] = []
collect_tree_entries(store, info.tree, "", out)
out
}
}
}
///|
fn collect_tree_entries(
store : ObjectStore,
tree_id : ObjectId,
prefix : String,
out : Array[IndexEntryInfo],
) -> Unit raise GitError {
let tree_obj = store.get(tree_id)
match tree_obj {
None => raise GitError::InvalidObject("Missing tree object")
Some(obj) => {
if obj.obj_type != ObjectType::Tree {
raise GitError::InvalidObject("Object is not a tree")
}
let entries = parse_tree(obj.data)
for entry in entries {
let path = if prefix.length() == 0 {
entry.name
} else {
prefix + "/" + entry.name
}
if is_tree_mode(entry.mode) {
collect_tree_entries(store, entry.id, path, out)
} else {
let blob = store.get(entry.id)
match blob {
None => raise GitError::InvalidObject("Missing blob object")
Some(b) => {
if b.obj_type != ObjectType::Blob {
raise GitError::InvalidObject("Object is not a blob")
}
let mode = parse_octal(entry.mode)
out.push({ path, id: entry.id, size: b.data.length(), mode })
}
}
}
}
}
}
}
///|
fn parse_octal(s : String) -> Int {
let mut result = 0
for c in s {
if c < '0' || c > '7' {
continue
}
result = result * 8 + (c.to_int() - '0'.to_int())
}
result
}
///|
fn push_u32_be(out : Array[Byte], v : Int) -> Unit {
out.push(((v >> 24) & 0xff).to_byte())
out.push(((v >> 16) & 0xff).to_byte())
out.push(((v >> 8) & 0xff).to_byte())
out.push((v & 0xff).to_byte())
}
///|
fn push_u16_be(out : Array[Byte], v : Int) -> Unit {
out.push(((v >> 8) & 0xff).to_byte())
out.push((v & 0xff).to_byte())
}
///|
fn write_text(
fs : &FileSystem,
path : String,
content : String,
) -> Unit raise GitError {
fs.write_string(path, content)
}
///|