///| Git index (v2/v3) reader/writer
///|
pub struct IndexEntry {
path : String
id : @bit.ObjectId
mode : Int
size : Int
mtime_sec : Int
mtime_nsec : Int
intent_to_add : Bool
dev : Int
ino : Int
uid : Int
gid : Int
}
///|
pub struct IndexStageEntry {
entry : IndexEntry
stage : Int
}
///|
pub struct ResolveUndoStage {
mode : Int
id : @bit.ObjectId
}
///|
pub struct ResolveUndoEntry {
path : String
base : ResolveUndoStage?
ours : ResolveUndoStage?
theirs : ResolveUndoStage?
}
///|
priv struct IndexCacheTree {
name : String
entry_count : Int
oid : @bit.ObjectId?
subtrees : Array[IndexCacheTree]
}
///|
pub fn IndexEntry::new(
path : String,
id : @bit.ObjectId,
mode : Int,
size : Int,
mtime_sec? : Int = 0,
mtime_nsec? : Int = 0,
intent_to_add? : Bool = false,
dev? : Int = 0,
ino? : Int = 0,
uid? : Int = 0,
gid? : Int = 0,
) -> IndexEntry {
{
path,
id,
mode,
size,
mtime_sec,
mtime_nsec,
intent_to_add,
dev,
ino,
uid,
gid,
}
}
///|
pub fn IndexStageEntry::new(entry : IndexEntry, stage : Int) -> IndexStageEntry {
{ entry, stage }
}
///|
fn index_repo_hash_size(fs : &@bit.RepoFileSystem, git_dir : String) -> Int {
match
read_config_value(fs, git_dir + "/config", "extensions", "objectformat") {
Some(raw) =>
if config_strip_quotes(raw).to_lower() == "sha256" {
32
} else {
20
}
None => 20
}
}
///|
fn index_hash_size_for_entries(
entries : Array[IndexEntry],
hash_size : Int,
) -> Int {
if entries.length() > 0 {
entries[0].id.hash_size()
} else {
hash_size
}
}
///|
fn index_hash_size_for_stage_entries(
entries : Array[IndexStageEntry],
hash_size : Int,
) -> Int {
if entries.length() > 0 {
entries[0].entry.id.hash_size()
} else {
hash_size
}
}
///|
pub fn read_index_entries(
fs : &@bit.RepoFileSystem,
git_dir : String,
) -> Array[IndexEntry] raise @bit.GitError {
index_entries_from_stage_entries(read_index_stage_entries(fs, git_dir))
}
///|
fn index_entries_from_stage_entries(
staged_entries : Array[IndexStageEntry],
) -> Array[IndexEntry] {
let mut has_conflicts = false
for item in staged_entries {
if item.stage != 0 {
has_conflicts = true
break
}
}
if has_conflicts {
let selected : Map[String, (Int, IndexEntry)] = Map([])
for item in staged_entries {
let prio = index_stage_priority(item.stage)
let entry = item.entry
match selected.get(entry.path) {
Some((prev_prio, _)) =>
if prio > prev_prio {
selected[entry.path] = (prio, entry)
}
None => selected[entry.path] = (prio, entry)
}
}
let deduped : Array[IndexEntry] = Array::new(capacity=selected.length())
for _, value in selected {
deduped.push(value.1)
}
deduped.sort_by((a, b) => index_path_compare_git_order(a.path, b.path))
deduped
} else {
let entries : Array[IndexEntry] = []
for item in staged_entries {
entries.push(item.entry)
}
entries
}
}
///|
/// Read the index entries and optional TREE extension from one index snapshot.
/// A missing or malformed extension falls back to the ordinary staged-tree walk.
fn read_index_entries_with_cache_tree(
fs : &@bit.RepoFileSystem,
git_dir : String,
) -> (Array[IndexEntry], IndexCacheTree?) raise @bit.GitError {
let path = join_path(git_dir, "index")
if !fs.is_file(path) {
return ([], None)
}
let data = fs.read_file(path)
let hash_size = index_repo_hash_size(fs, git_dir)
let staged_entries = index_read_stage_entries_from_bytes(data, hash_size~)
let index_entries = index_entries_from_stage_entries(staged_entries)
let cache_tree = if staged_entries.any(fn(item) { item.stage != 0 }) {
None
} else {
index_read_cache_tree_from_bytes(data, hash_size~) catch {
_ => None
}
}
(index_entries, cache_tree)
}
///|
fn index_entry_is_sparse_dir(entry : IndexEntry) -> Bool {
entry.mode == 0o040000 && entry.path.has_suffix("/")
}
///|
fn index_collect_tree_entries(
db : ObjectDb,
fs : &@bit.RepoFileSystem,
tree_id : @bit.ObjectId,
prefix : String,
out : Array[IndexEntry],
) -> Unit raise @bit.GitError {
let tree_obj = db.get(fs, tree_id)
match tree_obj {
None =>
raise @bit.GitError::InvalidObject(
"Missing tree object: " + tree_id.to_hex(),
)
Some(obj) => {
if obj.obj_type != @bit.ObjectType::Tree {
raise @bit.GitError::InvalidObject("Object is not a tree")
}
let entries = @bit.parse_tree(obj.data)
for entry in entries {
let path = if prefix == "" {
entry.name
} else {
prefix + "/" + entry.name
}
if entry.mode == "40000" {
index_collect_tree_entries(db, fs, entry.id, path, out)
continue
}
if entry.mode == "160000" {
out.push(IndexEntry::new(path, entry.id, 0o160000, 0))
continue
}
let blob_obj = db.get(fs, entry.id)
match blob_obj {
None =>
raise @bit.GitError::InvalidObject(
"Missing blob object: " + entry.id.to_hex(),
)
Some(blob) => {
if blob.obj_type != @bit.ObjectType::Blob {
raise @bit.GitError::InvalidObject("Object is not a blob")
}
out.push(
IndexEntry::new(
path,
entry.id,
index_parse_octal_value(entry.mode),
blob.data.length(),
),
)
}
}
}
}
}
}
///|
pub fn expand_sparse_index_entries(
fs : &@bit.RepoFileSystem,
git_dir : String,
entries : Array[IndexEntry],
) -> Array[IndexEntry] raise @bit.GitError {
let has_sparse_dir = entries.any(index_entry_is_sparse_dir)
if !has_sparse_dir {
return entries
}
let db = ObjectDb::load(fs, git_dir)
let expanded : Array[IndexEntry] = []
for entry in entries {
if index_entry_is_sparse_dir(entry) {
let prefix = String::unsafe_substring(
entry.path,
start=0,
end=entry.path.length() - 1,
)
index_collect_tree_entries(db, fs, entry.id, prefix, expanded)
} else {
expanded.push(entry)
}
}
expanded.sort_by((a, b) => index_path_compare_git_order(a.path, b.path))
expanded
}
///|
pub fn expand_sparse_skip_worktree_paths(
fs : &@bit.RepoFileSystem,
git_dir : String,
entries : Array[IndexEntry],
skip_worktree_paths : Map[String, Bool],
) -> Map[String, Bool] raise @bit.GitError {
let has_sparse_dir = entries.any(index_entry_is_sparse_dir)
if !has_sparse_dir {
return skip_worktree_paths
}
let db = ObjectDb::load(fs, git_dir)
let expanded : Map[String, Bool] = Map([])
for path in skip_worktree_paths.keys() {
expanded[path] = true
}
for entry in entries {
if !skip_worktree_paths.contains(entry.path) {
continue
}
if index_entry_is_sparse_dir(entry) {
let prefix = String::unsafe_substring(
entry.path,
start=0,
end=entry.path.length() - 1,
)
let children : Array[IndexEntry] = []
index_collect_tree_entries(db, fs, entry.id, prefix, children)
for child in children {
expanded[child.path] = true
}
} else {
expanded[entry.path] = true
}
}
expanded
}
///|
pub fn read_index_stage_entries(
fs : &@bit.RepoFileSystem,
git_dir : String,
) -> Array[IndexStageEntry] raise @bit.GitError {
let path = join_path(git_dir, "index")
if !fs.is_file(path) {
return []
}
let data = fs.read_file(path)
index_read_stage_entries_from_bytes(
data,
hash_size=index_repo_hash_size(fs, git_dir),
)
}
///|
pub fn read_resolve_undo_entries(
fs : &@bit.RepoFileSystem,
git_dir : String,
) -> Array[ResolveUndoEntry] raise @bit.GitError {
let path = join_path(git_dir, "index")
if !fs.is_file(path) {
return []
}
let data = fs.read_file(path)
index_read_resolve_undo_entries_from_bytes(
data,
hash_size=index_repo_hash_size(fs, git_dir),
)
}
///|
fn index_read_stage_entries_from_bytes(
data : Bytes,
hash_size? : Int = 20,
) -> Array[IndexStageEntry] raise @bit.GitError {
if data.length() < 32 {
raise @bit.GitError::InvalidObject("Index file too short")
}
if !(data[0] == b'D' && data[1] == b'I' && data[2] == b'R' && data[3] == b'C') {
raise @bit.GitError::InvalidObject("Invalid index header")
}
let version = index_read_u32_be(data, 4)
if version != 2 && version != 3 {
raise @bit.GitError::InvalidObject("Unsupported index version: \{version}")
}
let count = index_read_u32_be(data, 8)
let mut offset = 12
let entries : Array[IndexStageEntry] = Array::new(capacity=count)
for _ in 0.. data.length() {
raise @bit.GitError::InvalidObject("Index entry truncated")
}
// Index entry format (40 bytes stat data):
// ctime_seconds, ctime_nanoseconds, mtime_seconds, mtime_nanoseconds,
// dev, ino, mode, uid, gid, file_size
offset += 8 // skip ctime_sec, ctime_nsec
let mtime_sec = index_read_u32_be(data, offset)
offset += 4
let mtime_nsec = index_read_u32_be(data, offset)
offset += 4
let dev = index_read_u32_be(data, offset)
offset += 4
let ino = index_read_u32_be(data, offset)
offset += 4
let mode = index_read_u32_be(data, offset)
offset += 4
let uid = index_read_u32_be(data, offset)
offset += 4
let gid = index_read_u32_be(data, offset)
offset += 4
let size = index_read_u32_be(data, offset)
offset += 4
let id = index_read_object_id(data, offset, hash_size~)
offset += hash_size
// flags (u16)
let flags = index_read_u16_be(data, offset)
offset += 2
let stage = (flags >> 12) & 0x3
let has_extended = (flags & 0x4000) != 0
let ext_flags = if has_extended {
index_read_u16_be(data, offset)
} else {
0
}
if has_extended {
offset += 2
}
// Read path directly from bytes (avoid intermediate Array[Byte] + StringBuilder)
let fixed_size = 40 + hash_size + 2 + (if has_extended { 2 } else { 0 })
let path_start = entry_start + fixed_size
let mut path_end = path_start
while path_end < data.length() && data[path_end] != b'\x00' {
path_end += 1
}
let path_str = @utf8.decode_lossy(data[path_start:path_end])
let path_len = path_end - path_start
let entry_size = (fixed_size + path_len + 1 + 7) / 8 * 8
offset = entry_start + entry_size
entries.push({
entry: {
path: path_str,
id,
mode,
size,
mtime_sec,
mtime_nsec,
intent_to_add: index_ext_flags_has_intent_to_add(ext_flags),
dev,
ino,
uid,
gid,
},
stage,
})
}
entries
}
///|
fn index_read_resolve_undo_entries_from_bytes(
data : Bytes,
hash_size? : Int = 20,
) -> Array[ResolveUndoEntry] raise @bit.GitError {
if data.length() < 32 {
return []
}
if !(data[0] == b'D' && data[1] == b'I' && data[2] == b'R' && data[3] == b'C') {
raise @bit.GitError::InvalidObject("Invalid index header")
}
let version = index_read_u32_be(data, 4)
if version != 2 && version != 3 {
raise @bit.GitError::InvalidObject("Unsupported index version: \{version}")
}
let count = index_read_u32_be(data, 8)
let checksum_start = data.length() - hash_size
let mut offset = 12
for _ in 0.. checksum_start {
raise @bit.GitError::InvalidObject("Index entry truncated")
}
let flags = index_read_u16_be(data, offset + 40 + hash_size)
let has_extended = (flags & 0x4000) != 0
let fixed_size = 40 + hash_size + 2 + (if has_extended { 2 } else { 0 })
let path_start = entry_start + fixed_size
let mut path_end = path_start
while path_end < checksum_start && data[path_end] != b'\x00' {
path_end += 1
}
if path_end >= checksum_start {
raise @bit.GitError::InvalidObject("Index entry truncated")
}
let path_len = path_end - path_start
let entry_size = (fixed_size + path_len + 1 + 7) / 8 * 8
offset = entry_start + entry_size
}
while offset + 8 <= checksum_start {
let signature = @utf8.decode_lossy(data[offset:offset + 4])
let chunk_size = index_read_u32_be(data, offset + 4)
let body_start = offset + 8
let body_end = body_start + chunk_size
if body_end > checksum_start {
raise @bit.GitError::InvalidObject("Index extension truncated")
}
if signature == "REUC" {
let chunk_view = data[body_start:body_end]
return index_parse_resolve_undo_chunk(
Bytes::from_array(
FixedArray::makei(chunk_view.length(), fn(i) { chunk_view[i] }),
),
hash_size~,
)
}
offset = body_end
}
[]
}
///|
fn index_parse_resolve_undo_chunk(
data : Bytes,
hash_size? : Int = 20,
) -> Array[ResolveUndoEntry] raise @bit.GitError {
let entries : Array[ResolveUndoEntry] = []
let mut offset = 0
while offset < data.length() {
let (path, next_offset) = index_read_nul_terminated_string(data, offset)
let (base_mode, base_mode_end) = index_read_nul_terminated_octal(
data, next_offset,
)
let (ours_mode, ours_mode_end) = index_read_nul_terminated_octal(
data, base_mode_end,
)
let (theirs_mode, theirs_mode_end) = index_read_nul_terminated_octal(
data, ours_mode_end,
)
let mut cursor = theirs_mode_end
let base = if base_mode == 0 {
None
} else {
let id = index_read_object_id(data, cursor, hash_size~)
cursor += hash_size
let stage : ResolveUndoStage = { mode: base_mode, id }
Some(stage)
}
let ours = if ours_mode == 0 {
None
} else {
let id = index_read_object_id(data, cursor, hash_size~)
cursor += hash_size
let stage : ResolveUndoStage = { mode: ours_mode, id }
Some(stage)
}
let theirs = if theirs_mode == 0 {
None
} else {
let id = index_read_object_id(data, cursor, hash_size~)
cursor += hash_size
let stage : ResolveUndoStage = { mode: theirs_mode, id }
Some(stage)
}
entries.push({ path, base, ours, theirs })
offset = cursor
}
entries
}
///|
fn index_read_entries_end_offset(
data : Bytes,
count : Int,
hash_size? : Int = 20,
) -> Int raise @bit.GitError {
let checksum_start = data.length() - hash_size
let mut offset = 12
for _ in 0.. checksum_start {
raise @bit.GitError::InvalidObject("Index entry truncated")
}
let flags = index_read_u16_be(data, offset + 40 + hash_size)
let has_extended = (flags & 0x4000) != 0
let fixed_size = 40 + hash_size + 2 + (if has_extended { 2 } else { 0 })
let path_start = entry_start + fixed_size
let mut path_end = path_start
while path_end < checksum_start && data[path_end] != b'\x00' {
path_end += 1
}
if path_end >= checksum_start {
raise @bit.GitError::InvalidObject("Index entry truncated")
}
let path_len = path_end - path_start
let entry_size = (fixed_size + path_len + 1 + 7) / 8 * 8
offset = entry_start + entry_size
}
offset
}
///|
fn index_find_extension_chunk(
data : Bytes,
signature : String,
hash_size? : Int = 20,
) -> Bytes? raise @bit.GitError {
if data.length() < 32 {
return None
}
let count = index_read_u32_be(data, 8)
let checksum_start = data.length() - hash_size
let mut offset = index_read_entries_end_offset(data, count, hash_size~)
while offset + 8 <= checksum_start {
let current_sig = @utf8.decode_lossy(data[offset:offset + 4])
let chunk_size = index_read_u32_be(data, offset + 4)
let body_start = offset + 8
let body_end = body_start + chunk_size
if body_end > checksum_start {
raise @bit.GitError::InvalidObject("Index extension truncated")
}
if current_sig == signature {
let chunk_view = data[body_start:body_end]
return Some(
Bytes::from_array(
FixedArray::makei(chunk_view.length(), fn(i) { chunk_view[i] }),
),
)
}
offset = body_end
}
None
}
///|
fn index_parse_signed_decimal(
data : Bytes,
start : Int,
) -> (Int, Int) raise @bit.GitError {
if start >= data.length() {
raise @bit.GitError::InvalidObject("Invalid cache-tree extension")
}
let mut sign = 1
let mut cursor = start
if data[cursor] == b'-' {
sign = -1
cursor += 1
}
if cursor >= data.length() || data[cursor] < b'0' || data[cursor] > b'9' {
raise @bit.GitError::InvalidObject("Invalid cache-tree extension")
}
let mut value = 0
while cursor < data.length() && data[cursor] >= b'0' && data[cursor] <= b'9' {
value = value * 10 + (data[cursor].to_int() - b'0'.to_int())
cursor += 1
}
(sign * value, cursor)
}
///|
fn index_parse_cache_tree_node(
data : Bytes,
start : Int,
hash_size? : Int = 20,
) -> (IndexCacheTree, Int) raise @bit.GitError {
let (name, after_name) = index_read_nul_terminated_string(data, start)
let (entry_count, after_entry_count) = index_parse_signed_decimal(
data, after_name,
)
if after_entry_count >= data.length() || data[after_entry_count] != b' ' {
raise @bit.GitError::InvalidObject("Invalid cache-tree extension")
}
let (subtree_nr, after_subtree_nr) = index_parse_signed_decimal(
data,
after_entry_count + 1,
)
if after_subtree_nr >= data.length() || data[after_subtree_nr] != b'\n' {
raise @bit.GitError::InvalidObject("Invalid cache-tree extension")
}
let mut cursor = after_subtree_nr + 1
let oid = if entry_count >= 0 {
let parsed = index_read_object_id(data, cursor, hash_size~)
cursor += hash_size
Some(parsed)
} else {
None
}
let subtrees : Array[IndexCacheTree] = []
for _ in 0.. IndexCacheTree? raise @bit.GitError {
match index_find_extension_chunk(data, "TREE", hash_size~) {
Some(chunk) => {
let (tree, end_offset) = index_parse_cache_tree_node(chunk, 0, hash_size~)
if end_offset != chunk.length() {
raise @bit.GitError::InvalidObject("Invalid cache-tree extension")
}
Some(tree)
}
None => None
}
}
///|
fn index_append_ascii_bytes(target : Array[Byte], text : String) -> Unit {
for c in text {
target.push(c.to_int().to_byte())
}
}
///|
fn index_append_u32_be(target : Array[Byte], value : Int) -> Unit {
target.push(((value >> 24) & 0xFF).to_byte())
target.push(((value >> 16) & 0xFF).to_byte())
target.push(((value >> 8) & 0xFF).to_byte())
target.push((value & 0xFF).to_byte())
}
///|
fn index_append_cache_tree_node(
target : Array[Byte],
tree : IndexCacheTree,
) -> Unit {
index_append_ascii_bytes(target, tree.name)
target.push(b'\x00')
index_append_ascii_bytes(target, tree.entry_count.to_string())
target.push(b' ')
index_append_ascii_bytes(target, tree.subtrees.length().to_string())
target.push(b'\n')
match tree.oid {
Some(id) if tree.entry_count >= 0 =>
for b in id.bytes {
target.push(b)
}
_ => ()
}
for subtree in tree.subtrees {
index_append_cache_tree_node(target, subtree)
}
}
///|
fn index_cache_tree_extension_bytes(tree : IndexCacheTree) -> Array[Byte] {
let body : Array[Byte] = []
index_append_cache_tree_node(body, tree)
let extension : Array[Byte] = [b'T', b'R', b'E', b'E']
index_append_u32_be(extension, body.length())
extension.append(body)
extension
}
///|
fn index_cache_tree_invalidate_path(
tree : IndexCacheTree,
path : String,
) -> IndexCacheTree {
let invalidated_entry_count = -1
let slash_idx = index_find_first_slash(path)
match slash_idx {
None => {
let kept : Array[IndexCacheTree] = []
for subtree in tree.subtrees {
if subtree.name != path {
kept.push(subtree)
}
}
{
name: tree.name,
entry_count: invalidated_entry_count,
oid: None,
subtrees: kept,
}
}
Some(idx) => {
let head = String::unsafe_substring(path, start=0, end=idx)
let tail = String::unsafe_substring(
path,
start=idx + 1,
end=path.length(),
)
let updated : Array[IndexCacheTree] = []
for subtree in tree.subtrees {
if subtree.name == head {
updated.push(index_cache_tree_invalidate_path(subtree, tail))
} else {
updated.push(subtree)
}
}
{
name: tree.name,
entry_count: invalidated_entry_count,
oid: None,
subtrees: updated,
}
}
}
}
///|
fn index_find_first_slash(path : String) -> Int? {
for i = 0; i < path.length(); i = i + 1 {
if path[i] == '/' {
return Some(i)
}
}
None
}
///|
fn index_entry_signature(entry : IndexEntry) -> String {
entry.id.to_hex() + ":" + entry.mode.to_string()
}
///|
fn index_collect_changed_paths(
old_entries : Array[IndexEntry],
new_entries : Array[IndexEntry],
) -> Array[String] {
let old_by_path : Map[String, String] = Map([])
for entry in old_entries {
old_by_path[entry.path] = index_entry_signature(entry)
}
let new_by_path : Map[String, String] = Map([])
for entry in new_entries {
new_by_path[entry.path] = index_entry_signature(entry)
}
let changed : Array[String] = []
let seen : Map[String, Bool] = Map([])
for path, old_sig in old_by_path {
match new_by_path.get(path) {
Some(new_sig) if new_sig == old_sig => ()
_ =>
if !seen.contains(path) {
seen[path] = true
changed.push(path)
}
}
}
for path in new_by_path.keys() {
if !old_by_path.contains(path) && !seen.contains(path) {
seen[path] = true
changed.push(path)
}
}
changed
}
///|
fn index_preserve_cache_tree(
fs : &@bit.RepoFileSystem,
git_dir : String,
sorted : Array[IndexEntry],
) -> IndexCacheTree? {
let index_path = join_path(git_dir, "index")
let raw = fs.read_file(index_path) catch { _ => return None }
let previous_tree = index_read_cache_tree_from_bytes(
raw,
hash_size=index_repo_hash_size(fs, git_dir),
) catch {
_ => return None
}
guard previous_tree is Some(tree) else { return None }
let staged = index_read_stage_entries_from_bytes(raw) catch {
_ => return None
}
let old_entries : Array[IndexEntry] = []
for item in staged {
if item.stage == 0 {
old_entries.push(item.entry)
} else {
return None
}
}
let changed_paths = index_collect_changed_paths(old_entries, sorted)
let mut preserved = tree
for path in changed_paths {
preserved = index_cache_tree_invalidate_path(preserved, path)
}
Some(preserved)
}
///|
fn index_build_cache_tree_from_entries(
entries : Array[IndexEntry],
name? : String = "",
) -> IndexCacheTree {
let file_entries : Array[@bit.TreeEntry] = []
let dir_map : Map[String, Array[IndexEntry]] = Map([])
let subtrees : Array[IndexCacheTree] = []
let mut entry_count = 0
for entry in entries {
if worktree_is_zero_object_id(entry.id) {
continue
}
match split_first(entry.path) {
(entry_name, None) => {
entry_count += 1
file_entries.push(
@bit.TreeEntry::new(
@string_utils.mode_to_string(entry.mode),
entry_name,
entry.id,
),
)
}
(entry_name, Some(rest)) =>
match dir_map.get(entry_name) {
Some(list) =>
list.push({
path: rest,
id: entry.id,
mode: entry.mode,
size: entry.size,
mtime_sec: entry.mtime_sec,
mtime_nsec: entry.mtime_nsec,
intent_to_add: entry.intent_to_add,
dev: entry.dev,
ino: entry.ino,
uid: entry.uid,
gid: entry.gid,
})
None =>
dir_map[entry_name] = [
{
path: rest,
id: entry.id,
mode: entry.mode,
size: entry.size,
mtime_sec: entry.mtime_sec,
mtime_nsec: entry.mtime_nsec,
intent_to_add: entry.intent_to_add,
dev: entry.dev,
ino: entry.ino,
uid: entry.uid,
gid: entry.gid,
},
]
}
}
}
for dir_name, list in dir_map {
let subtree = index_build_cache_tree_from_entries(list, name=dir_name)
file_entries.push(
@bit.TreeEntry::new("40000", dir_name, subtree.oid.unwrap()),
)
subtrees.push(subtree)
entry_count += subtree.entry_count
}
file_entries.sort_by(fn(a, b) {
let a_key = if a.mode == "40000" { a.name + "/" } else { a.name }
let b_key = if b.mode == "40000" { b.name + "/" } else { b.name }
compare_strings_lexicographic(a_key, b_key)
})
subtrees.sort_by(fn(a, b) { compare_strings_lexicographic(a.name, b.name) })
let (tree_id, _) = @bit.create_tree(file_entries)
{ name, entry_count, oid: Some(tree_id), subtrees }
}
///|
fn index_prepare_cache_tree(
fs : &@bit.RepoFileSystem,
git_dir : String,
sorted : Array[IndexEntry],
) -> IndexCacheTree {
match index_preserve_cache_tree(fs, git_dir, sorted) {
Some(tree) => tree
None => index_build_cache_tree_from_entries(sorted)
}
}
///|
fn index_read_nul_terminated_string(
data : Bytes,
start : Int,
) -> (String, Int) raise @bit.GitError {
let mut end = start
while end < data.length() && data[end] != b'\x00' {
end += 1
}
if end >= data.length() {
raise @bit.GitError::InvalidObject("Unexpected end of resolve-undo data")
}
(@utf8.decode_lossy(data[start:end]), end + 1)
}
///|
fn index_read_nul_terminated_octal(
data : Bytes,
start : Int,
) -> (Int, Int) raise @bit.GitError {
let (text, next) = index_read_nul_terminated_string(data, start)
(index_parse_octal_value(text), next)
}
///|
fn index_parse_octal_value(text : String) -> Int raise @bit.GitError {
if text.length() == 0 {
raise @bit.GitError::InvalidObject("Invalid resolve-undo mode")
}
let mut value = 0
for c in text {
if c < '0' || c > '7' {
raise @bit.GitError::InvalidObject("Invalid resolve-undo mode")
}
value = value * 8 + (c.to_int() - '0'.to_int())
}
value
}
///|
/// Read skip-worktree paths from both bit sidecar and index extended flags.
pub fn read_skip_worktree_paths(
fs : &@bit.RepoFileSystem,
git_dir : String,
hash_size? : Int = 20,
) -> Map[String, Bool] {
let effective_hash_size = if hash_size == 20 {
index_repo_hash_size(fs, git_dir)
} else {
hash_size
}
let out : Map[String, Bool] = Map([])
let sidecar_path = join_path(git_dir, "bit-skip-worktree")
if fs.is_file(sidecar_path) {
let text = @utf8.decode_lossy(
(fs.read_file(sidecar_path) catch { _ => Default::default() })[:],
)
for line_view in text.split("\n") {
let line = line_view.to_owned()
if line.length() > 0 {
out[line] = true
}
}
}
let index_path = join_path(git_dir, "index")
if !fs.is_file(index_path) {
return out
}
let data = fs.read_file(index_path) catch { _ => return out }
if data.length() < 32 {
return out
}
if !(data[0] == b'D' && data[1] == b'I' && data[2] == b'R' && data[3] == b'C') {
return out
}
let count = index_read_u32_be(data, 8) catch { _ => return out }
let mut offset = 12
for _ in 0.. data.length() {
return out
}
let flags = index_read_u16_be(data, offset + 40 + effective_hash_size) catch {
_ => return out
}
let stage = (flags >> 12) & 0x3
let has_extended = (flags & 0x4000) != 0
let ext_flags = if has_extended {
index_read_u16_be(data, offset + 40 + effective_hash_size + 2) catch {
_ => return out
}
} else {
0
}
let fixed_size = 40 +
effective_hash_size +
2 +
(if has_extended { 2 } else { 0 })
let path_start = offset + fixed_size
let mut i = path_start
while i < data.length() && data[i] != b'\x00' {
i += 1
}
if i >= data.length() {
return out
}
if stage == 0 && has_extended && (ext_flags & 0x4000) != 0 {
out[@utf8.decode_lossy(data[path_start:i])] = true
}
let path_len = i - path_start
let entry_size = (fixed_size + path_len + 1 + 7) / 8 * 8
offset += entry_size
}
out
}
///|
pub fn read_assume_unchanged_paths(
fs : &@bit.RepoFileSystem,
git_dir : String,
hash_size? : Int = 20,
) -> Map[String, Bool] {
let effective_hash_size = if hash_size == 20 {
index_repo_hash_size(fs, git_dir)
} else {
hash_size
}
let out : Map[String, Bool] = Map([])
let index_path = join_path(git_dir, "index")
if !fs.is_file(index_path) {
return out
}
let data = fs.read_file(index_path) catch { _ => return out }
if data.length() < 32 {
return out
}
if !(data[0] == b'D' && data[1] == b'I' && data[2] == b'R' && data[3] == b'C') {
return out
}
let count = index_read_u32_be(data, 8) catch { _ => return out }
let mut offset = 12
for _ in 0.. data.length() {
return out
}
let flags = index_read_u16_be(data, offset + 40 + effective_hash_size) catch {
_ => return out
}
let stage = (flags >> 12) & 0x3
let has_extended = (flags & 0x4000) != 0
let fixed_size = 40 +
effective_hash_size +
2 +
(if has_extended { 2 } else { 0 })
let path_start = offset + fixed_size
let mut i = path_start
while i < data.length() && data[i] != b'\x00' {
i += 1
}
if i >= data.length() {
return out
}
if stage == 0 && (flags & 0x8000) != 0 {
out[@utf8.decode_lossy(data[path_start:i])] = true
}
let path_len = i - path_start
let entry_size = (fixed_size + path_len + 1 + 7) / 8 * 8
offset += entry_size
}
out
}
///|
pub fn read_unmerged_paths(
fs : &@bit.RepoFileSystem,
git_dir : String,
) -> Map[String, Bool] {
let out : Map[String, Bool] = Map([])
let entries = read_index_stage_entries(fs, git_dir) catch { _ => return out }
for item in entries {
if item.stage != 0 {
out[item.entry.path] = true
}
}
out
}
///|
pub fn write_skip_worktree_paths(
fs : &@bit.FileSystem,
git_dir : String,
paths : Array[String],
) -> Unit raise @bit.GitError {
let state_path = join_path(git_dir, "bit-skip-worktree")
let by_path : Map[String, Bool] = Map([])
for path in paths {
if path.length() > 0 {
by_path[path] = true
}
}
let sorted = by_path.keys().to_array()
sorted.sort_by((a, b) => String::compare(a, b))
if sorted.length() == 0 {
fs.remove_file(state_path) catch {
_ => ()
}
return
}
fs.write_string(state_path, sorted.join("\n") + "\n")
}
///|
fn index_entry_buf_size(
path_len : Int,
has_extended : Bool,
hash_size? : Int = 20,
) -> Int {
let fixed_size = 40 + hash_size + 2 + (if has_extended { 2 } else { 0 })
(fixed_size + path_len + 1 + 7) / 8 * 8
}
///|
fn write_index_entry_to_buf(
buf : FixedArray[Byte],
pos : Int,
e : IndexEntry,
stage : Int,
skip_worktree : Bool,
assume_unchanged : Bool,
hash_size? : Int = 20,
) -> Int {
let entry_start = pos
let mut pos = pos
fa_set_u32_be(buf, pos, e.mtime_sec) // ctime_sec
fa_set_u32_be(buf, pos + 4, e.mtime_nsec) // ctime_nsec
fa_set_u32_be(buf, pos + 8, e.mtime_sec) // mtime_sec
fa_set_u32_be(buf, pos + 12, e.mtime_nsec) // mtime_nsec
fa_set_u32_be(buf, pos + 16, e.dev)
fa_set_u32_be(buf, pos + 20, e.ino)
fa_set_u32_be(buf, pos + 24, e.mode) // mode
fa_set_u32_be(buf, pos + 28, e.uid)
fa_set_u32_be(buf, pos + 32, e.gid)
fa_set_u32_be(buf, pos + 36, e.size) // file_size
pos += 40
// object id
for i in 0.. FixedArray[Byte] {
// Pre-calculate total buffer size
let mut total_size = 12 // header: "DIRC" + version + count
let mut version = 2
for e in sorted {
let is_skip = match skip_worktree_paths {
Some(m) => m.contains(e.path)
None => false
}
let has_extended = index_extended_flags(e.intent_to_add, is_skip) != 0
if has_extended {
version = 3
}
total_size += index_entry_buf_size(
@utf8.encode(e.path).length(),
has_extended,
hash_size~,
)
}
let cache_tree_extension = match cache_tree {
Some(tree) => index_cache_tree_extension_bytes(tree)
None => []
}
total_size += cache_tree_extension.length()
total_size += hash_size // hash checksum
let buf : FixedArray[Byte] = FixedArray::make(total_size, b'\x00')
// Header
buf[0] = b'D'
buf[1] = b'I'
buf[2] = b'R'
buf[3] = b'C'
fa_set_u32_be(buf, 4, version)
fa_set_u32_be(buf, 8, sorted.length()) // count
let mut pos = 12
for e in sorted {
let is_skip = match skip_worktree_paths {
Some(m) => m.contains(e.path)
None => false
}
let is_assume = match assume_unchanged_paths {
Some(m) => m.contains(e.path)
None => false
}
pos = write_index_entry_to_buf(
buf,
pos,
e,
0,
is_skip,
is_assume,
hash_size~,
)
}
for i in 0.. (Map[String, Bool], Map[String, Bool]) {
let existing_skip = read_skip_worktree_paths(fs, git_dir, hash_size~)
let existing_assume = read_assume_unchanged_paths(fs, git_dir, hash_size~)
let skip_worktree_paths : Map[String, Bool] = Map([])
let assume_unchanged_paths : Map[String, Bool] = Map([])
for entry in entries {
if existing_skip.contains(entry.path) {
skip_worktree_paths[entry.path] = true
}
if existing_assume.contains(entry.path) {
assume_unchanged_paths[entry.path] = true
}
}
(skip_worktree_paths, assume_unchanged_paths)
}
///|
pub fn write_index_entries(
fs : &@bit.FileSystem,
git_dir : String,
entries : Array[IndexEntry],
hash_size? : Int = 20,
) -> Unit raise @bit.GitError {
let sorted = entries.copy()
sorted.sort_by((a, b) => index_path_compare_git_order(a.path, b.path))
let effective_hash_size = index_hash_size_for_entries(sorted, hash_size)
let buf = write_index_buf(sorted, None, None, hash_size=effective_hash_size)
let index_path = join_path(git_dir, "index")
fs.write_file(index_path, Bytes::from_array(buf[:]))
}
///|
pub fn write_index_entries_preserving_cache_tree(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
entries : Array[IndexEntry],
) -> Unit raise @bit.GitError {
let hash_size = index_repo_hash_size(rfs, git_dir)
let sorted = entries.copy()
sorted.sort_by((a, b) => index_path_compare_git_order(a.path, b.path))
let cache_tree = index_prepare_cache_tree(rfs, git_dir, sorted)
let (skip_worktree_paths, assume_unchanged_paths) = index_preserved_flag_paths(
rfs,
git_dir,
sorted,
hash_size~,
)
let buf = write_index_buf(
sorted,
Some(skip_worktree_paths),
Some(assume_unchanged_paths),
cache_tree=Some(cache_tree),
hash_size~,
)
let index_path = join_path(git_dir, "index")
fs.write_file(index_path, Bytes::from_array(buf[:]))
}
///|
pub fn write_index_entries_with_tree(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
entries : Array[IndexEntry],
tree_id : @bit.ObjectId,
) -> Unit raise @bit.GitError {
let hash_size = index_repo_hash_size(rfs, git_dir)
let sorted = entries.copy()
sorted.sort_by((a, b) => index_path_compare_git_order(a.path, b.path))
let cache_tree = index_build_cache_tree_from_entries(sorted)
match cache_tree.oid {
Some(id) if id == tree_id => ()
_ => ()
}
let (skip_worktree_paths, assume_unchanged_paths) = index_preserved_flag_paths(
rfs,
git_dir,
sorted,
hash_size~,
)
let buf = write_index_buf(
sorted,
Some(skip_worktree_paths),
Some(assume_unchanged_paths),
cache_tree=Some(cache_tree),
hash_size~,
)
let index_path = join_path(git_dir, "index")
fs.write_file(index_path, Bytes::from_array(buf[:]))
}
///|
fn index_read_u32_be(data : Bytes, start : Int) -> Int raise @bit.GitError {
if start < 0 || start + 4 > data.length() {
raise @bit.GitError::InvalidObject("Unexpected end of index data")
}
(data[start].to_int() << 24) |
(data[start + 1].to_int() << 16) |
(data[start + 2].to_int() << 8) |
data[start + 3].to_int()
}
///|
fn index_read_u16_be(data : Bytes, start : Int) -> Int raise @bit.GitError {
if start < 0 || start + 2 > data.length() {
raise @bit.GitError::InvalidObject("Unexpected end of index data")
}
(data[start].to_int() << 8) | data[start + 1].to_int()
}
///|
pub fn write_index_entries_with_skip_worktree(
fs : &@bit.FileSystem,
git_dir : String,
entries : Array[IndexEntry],
skip_worktree_paths : Map[String, Bool],
hash_size? : Int = 20,
) -> Unit raise @bit.GitError {
let sorted = entries.copy()
sorted.sort_by((a, b) => index_path_compare_git_order(a.path, b.path))
let effective_hash_size = index_hash_size_for_entries(sorted, hash_size)
let buf = write_index_buf(
sorted,
Some(skip_worktree_paths),
None,
hash_size=effective_hash_size,
)
let index_path = join_path(git_dir, "index")
fs.write_file(index_path, Bytes::from_array(buf[:]))
}
///|
pub fn write_index_entries_with_flags(
fs : &@bit.FileSystem,
git_dir : String,
entries : Array[IndexEntry],
skip_worktree_paths : Map[String, Bool],
assume_unchanged_paths : Map[String, Bool],
hash_size? : Int = 20,
) -> Unit raise @bit.GitError {
let sorted = entries.copy()
sorted.sort_by((a, b) => index_path_compare_git_order(a.path, b.path))
let effective_hash_size = index_hash_size_for_entries(sorted, hash_size)
let buf = write_index_buf(
sorted,
Some(skip_worktree_paths),
Some(assume_unchanged_paths),
hash_size=effective_hash_size,
)
let index_path = join_path(git_dir, "index")
fs.write_file(index_path, Bytes::from_array(buf[:]))
}
///|
fn write_index_stage_buf(
entries : Array[IndexStageEntry],
hash_size? : Int = 20,
) -> FixedArray[Byte] {
let mut total_size = 12
let mut version = 2
for item in entries {
let has_extended = index_extended_flags(item.entry.intent_to_add, false) !=
0
if has_extended {
version = 3
}
total_size += index_entry_buf_size(
@utf8.encode(item.entry.path).length(),
has_extended,
hash_size~,
)
}
total_size += hash_size
let buf : FixedArray[Byte] = FixedArray::make(total_size, b'\x00')
buf[0] = b'D'
buf[1] = b'I'
buf[2] = b'R'
buf[3] = b'C'
fa_set_u32_be(buf, 4, version)
fa_set_u32_be(buf, 8, entries.length())
let mut pos = 12
for item in entries {
pos = write_index_entry_to_buf(
buf,
pos,
item.entry,
item.stage,
false,
false,
hash_size~,
)
}
let content_bytes = Bytes::from_array(buf[:pos])
let checksum = @bit.hash_prefix(content_bytes, pos, hash_size~)
for i in 0.. Unit raise @bit.GitError {
let sorted = entries.copy()
sorted.sort_by(fn(a, b) {
let path_cmp = index_path_compare_git_order(a.entry.path, b.entry.path)
if path_cmp != 0 {
path_cmp
} else {
Int::compare(a.stage, b.stage)
}
})
let effective_hash_size = index_hash_size_for_stage_entries(sorted, hash_size)
let buf = write_index_stage_buf(sorted, hash_size=effective_hash_size)
let index_path = join_path(git_dir, "index")
fs.write_file(index_path, Bytes::from_array(buf[:]))
}
///|
fn index_read_object_id(
data : Bytes,
start : Int,
hash_size? : Int = 20,
) -> @bit.ObjectId raise @bit.GitError {
if start + hash_size > data.length() {
raise @bit.GitError::InvalidObject("Unexpected end of index data")
}
let bytes : FixedArray[Byte] = FixedArray::make(hash_size, b'\x00')
for i in 0.. Int {
let mut flags = 0
if intent_to_add {
flags = flags | 0x2000
}
if skip_worktree {
flags = flags | 0x4000
}
flags
}
///|
fn index_ext_flags_has_intent_to_add(ext_flags : Int) -> Bool {
(ext_flags & 0x2000) != 0
}
///|
fn fa_set_u32_be(buf : FixedArray[Byte], offset : Int, v : Int) -> Unit {
buf[offset] = ((v >> 24) & 0xff).to_byte()
buf[offset + 1] = ((v >> 16) & 0xff).to_byte()
buf[offset + 2] = ((v >> 8) & 0xff).to_byte()
buf[offset + 3] = (v & 0xff).to_byte()
}
///|
fn fa_set_u16_be(buf : FixedArray[Byte], offset : Int, v : Int) -> Unit {
buf[offset] = ((v >> 8) & 0xff).to_byte()
buf[offset + 1] = (v & 0xff).to_byte()
}
///|
fn index_path_compare_git_order(a : String, b : String) -> Int {
let a_len = a.length()
let b_len = b.length()
let min_len = if a_len < b_len { a_len } else { b_len }
for i in 0.. bv {
return 1
}
}
if a_len < b_len {
-1
} else if a_len > b_len {
1
} else {
0
}
}
///|
fn index_stage_priority(stage : Int) -> Int {
if stage == 0 {
4
} else if stage == 2 {
3
} else if stage == 1 {
2
} else {
1
}
}