///|
pub(all) enum FingerprintMode {
Fast
GitCompat
} derive(Eq, Debug)
///|
pub impl Show for FingerprintMode with fn output(self, logger) {
match self {
Fast => logger.write_string("Fast")
GitCompat => logger.write_string("GitCompat")
}
}
///|
pub fn parse_fingerprint_mode(raw : String) -> FingerprintMode? {
match raw {
"fast" => Some(FingerprintMode::Fast)
"git" | "git-compat" | "bit-compat" | "bit_compat" =>
Some(FingerprintMode::GitCompat)
_ => None
}
}
///|
pub fn fingerprint_mode_name(mode : FingerprintMode) -> String {
match mode {
FingerprintMode::Fast => "fast"
FingerprintMode::GitCompat => "git"
}
}
///|
fn fingerprint_ignore_entry(
name : String,
ignore_entries : Array[String],
) -> Bool {
for candidate in ignore_entries {
if name == candidate {
return true
}
}
false
}
///|
fn collect_fast_hash_files(
fs : &@bit.RepoFileSystem,
root : String,
rel : String,
ignore_entries : Array[String],
out : Array[String],
) -> Unit {
let dir_path = if rel.length() == 0 { root } else { root + "/" + rel }
let entries = fs.readdir(dir_path) catch { _ => [] }
entries.sort_by((a, b) => String::lexical_compare(a, b))
for entry in entries {
if fingerprint_ignore_entry(entry, ignore_entries) {
continue
}
let child_rel = if rel.length() == 0 { entry } else { rel + "/" + entry }
let child_path = root + "/" + child_rel
if fs.is_dir(child_path) {
collect_fast_hash_files(fs, root, child_rel, ignore_entries, out)
} else if fs.is_file(child_path) {
out.push(child_rel)
}
}
}
///|
pub fn fast_directory_fingerprint(
fs : &@bit.RepoFileSystem,
root : String,
ignore_entries? : Array[String] = [".", "..", ".git", ".bit"],
) -> String {
if !fs.is_dir(root) {
return @bit.hash_blob_string("missing").to_hex()
}
let files : Array[String] = []
collect_fast_hash_files(fs, root, "", ignore_entries, files)
let buf = StringBuilder::new()
for rel in files {
let file_path = root + "/" + rel
let content = fs.read_file(file_path) catch { _ => Bytes::from_array([]) }
let file_hash = @bit.sha1(content).to_hex()
buf.write_string(rel)
buf.write_string("\t")
buf.write_string(file_hash)
buf.write_string("\n")
}
@bit.hash_blob_string(buf.to_string()).to_hex()
}
///|
fn fingerprint_path_ignored(
rel_path : String,
ignore_entries : Array[String],
) -> Bool {
for entry in ignore_entries {
if entry == "." || entry == ".." {
continue
}
if rel_path == entry || rel_path.has_prefix(entry + "/") {
return true
}
}
false
}
///|
fn decode_bytes(data : Bytes) -> String {
@string_utils.decode_bytes(data)
}
///|
fn trim_string(s : String) -> String {
let mut start = 0
let mut end = s.length()
while start < end {
let c = s.unsafe_get(start)
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
start += 1
} else {
break
}
}
while end > start {
let c = s.unsafe_get(end - 1)
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
end -= 1
} else {
break
}
}
if start == 0 && end == s.length() {
s
} else {
String::unsafe_substring(s, start~, end~)
}
}
///|
fn resolve_git_dir(fs : &@bit.RepoFileSystem, root : String) -> String {
let bit_path = root + "/.git"
if fs.is_file(bit_path) {
let bytes = fs.read_file(bit_path) catch { _ => return bit_path }
let text = decode_bytes(bytes)
for line_view in text.split("\n") {
let line = trim_string(line_view.to_owned())
if !line.has_prefix("gitdir:") {
continue
}
let raw = trim_string(
String::unsafe_substring(line, start=7, end=line.length()),
)
if raw.length() == 0 {
return bit_path
}
if raw.has_prefix("/") {
return raw
}
return root + "/" + raw
}
bit_path
} else {
bit_path
}
}
///|
fn split_first(path : String) -> (String, String?) {
match path.find("/") {
None => (path, None)
Some(idx) => {
let name = String::unsafe_substring(path, start=0, end=idx)
let rest = String::unsafe_substring(
path,
start=idx + 1,
end=path.length(),
)
(name, Some(rest))
}
}
}
///|
fn compare_strings_lexicographic(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.. b_char {
return 1
}
}
if a_len < b_len {
-1
} else if a_len > b_len {
1
} else {
0
}
}
///|
fn to_octal_string(value : Int) -> String {
if value == 0 {
return "0"
}
let digits : Array[Char] = []
let mut v = value
while v > 0 {
let d = v % 8
digits.push((d + '0'.to_int()).unsafe_to_char())
v = v / 8
}
let rev = digits.rev()
let sb = StringBuilder::new()
for c in rev {
sb.write_char(c)
}
sb.to_string()
}
///|
fn mode_to_string(mode : Int) -> String {
if mode == 33188 {
"100644"
} else {
to_octal_string(mode)
}
}
///|
fn tree_id_from_entries(entries : Array[@bitlib.IndexEntry]) -> @bit.ObjectId {
if entries.length() == 0 {
return @bit.create_tree([]).0
}
let file_entries : Array[@bit.TreeEntry] = []
let dir_map : Map[String, Array[@bitlib.IndexEntry]] = Map([])
for e in entries {
match split_first(e.path) {
(name, None) => {
let mode = mode_to_string(e.mode)
file_entries.push(@bit.TreeEntry::new(mode, name, e.id))
}
(name, Some(rest)) =>
match dir_map.get(name) {
Some(list) =>
list.push(
@bitlib.IndexEntry::new(
rest,
e.id,
e.mode,
e.size,
mtime_sec=e.mtime_sec,
mtime_nsec=e.mtime_nsec,
),
)
None =>
dir_map[name] = [
@bitlib.IndexEntry::new(
rest,
e.id,
e.mode,
e.size,
mtime_sec=e.mtime_sec,
mtime_nsec=e.mtime_nsec,
),
]
}
}
}
for dir_name, list in dir_map {
let sub_id = tree_id_from_entries(list)
file_entries.push(@bit.TreeEntry::new("40000", dir_name, sub_id))
}
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)
})
@bit.create_tree(file_entries).0
}
///|
fn index_entries_after_add_all(
fs : &@bit.RepoFileSystem,
root : String,
ignore_entries : Array[String],
) -> Array[@bitlib.IndexEntry] {
let git_dir = resolve_git_dir(fs, root)
let existing = @bitlib.read_index_entries(fs, git_dir) catch { _ => [] }
let staged : Map[String, @bitlib.IndexEntry] = Map([])
for e in existing {
if fingerprint_path_ignored(e.path, ignore_entries) {
continue
}
staged[e.path] = e
}
let working_files = @bitlib.list_working_files(fs, root) catch { _ => [] }
let present : Map[String, Bool] = Map([])
for rel in working_files {
if fingerprint_path_ignored(rel, ignore_entries) {
continue
}
let abs = root + "/" + rel
if fs.is_file(abs) {
let content = fs.read_file(abs) catch { _ => Bytes::from_array([]) }
let id = @bit.hash_blob(content)
let mode = staged.get(rel).map(e => e.mode).unwrap_or(33188)
staged[rel] = @bitlib.IndexEntry::new(rel, id, mode, content.length())
present[rel] = true
}
}
let removed_paths : Array[String] = []
for path, _ in staged {
if present.get(path) is None {
removed_paths.push(path)
}
}
for path in removed_paths {
staged.remove(path)
}
let out : Array[@bitlib.IndexEntry] = []
for _, entry in staged {
out.push(entry)
}
out
}
///|
pub fn bit_compat_directory_fingerprint(
fs : &@bit.RepoFileSystem,
root : String,
ignore_entries? : Array[String] = [".", "..", ".git", ".bit"],
) -> String {
if !fs.is_dir(root) {
return @bit.hash_blob_string("missing").to_hex()
}
let entries = index_entries_after_add_all(fs, root, ignore_entries)
tree_id_from_entries(entries).to_hex()
}
///|
pub fn directory_fingerprint(
fs : &@bit.RepoFileSystem,
root : String,
mode : FingerprintMode,
ignore_entries? : Array[String] = [".", "..", ".git", ".bit"],
) -> String {
match mode {
FingerprintMode::Fast =>
fast_directory_fingerprint(fs, root, ignore_entries~)
FingerprintMode::GitCompat =>
bit_compat_directory_fingerprint(fs, root, ignore_entries~)
}
}