//! Walking the working tree: ignore rules, hashing, and the file cache. module snapshot import os import src.storage // One tracked file in one tree. pub struct FileEntry { pub: path string hash string size i64 executable bool } // walk hashes every tracked file under `root` and returns them sorted by path. // Symlinks are skipped, as Koh 2 skipped them, so migration stays lossless. pub fn walk(store &storage.Store, root string, ig Ignore) ![]FileEntry { cache := store.load_cache()! mut entries := []FileEntry{} walk_dir(store, root, '', ig, cache, mut entries)! entries.sort(a.path < b.path) return entries } // walk_raw hashes the same files without a store: no cache, nothing written. // `migrate` uses it to check a working tree against a manifest before it has a // database to check it with. pub fn walk_raw(root string, ig Ignore) ![]FileEntry { mut entries := []FileEntry{} walk_dir(none, root, '', ig, map[string]storage.CacheRow{}, mut entries)! entries.sort(a.path < b.path) return entries } fn walk_dir(store ?&storage.Store, root string, rel string, ig Ignore, cache map[string]storage.CacheRow, mut out []FileEntry) ! { dir := if rel == '' { root } else { os.join_path(root, rel) } for name in os.ls(dir)! { child := if rel == '' { name } else { '${rel}/${name}' } st := lstat(os.join_path(root, child)) or { continue } // The kind is known by now, so a pattern ending in `/` can be applied // to directories only. if ig.matches(child, st.kind == .dir) { continue } match st.kind { .dir { walk_dir(store, root, child, ig, cache, mut out)! } .file { if st.size > storage.max_file_size { return error('${child} is ${st.size} bytes; koh stores code and text, not files this large') } hash := hash_file(store, root, child, st, cache)! out << FileEntry{ path: child hash: hash size: st.size executable: is_executable(st.mode) } } else {} } } } fn hash_file(store ?&storage.Store, root string, rel string, st Stat, cache map[string]storage.CacheRow) !string { if s := store { if row := cache[rel] { if row.size == st.size && row.mtime_ns == st.mtime_ns && row.ctime_ns == st.ctime_ns { return row.hash } } data := os.read_bytes(os.join_path(root, rel))! hash := storage.hash_hex(data) s.save_cache(storage.CacheRow{ path: rel size: st.size mtime_ns: st.mtime_ns ctime_ns: st.ctime_ns hash: hash })! return hash } return storage.hash_hex(os.read_bytes(os.join_path(root, rel))!) } // store_objects writes the bytes of every entry the database does not have // yet, so a save only pays for content it has never seen. pub fn store_objects(store &storage.Store, root string, entries []FileEntry) ! { for entry in entries { if store.has_object(entry.hash)! { continue } data := os.read_bytes(os.join_path(root, entry.path))! stored := store.put_object(data)! if stored != entry.hash { return error('${entry.path} changed while saving') } } } // same_tree compares two sorted entry lists by path, hash and executable bit. pub fn same_tree(a []FileEntry, b []FileEntry) bool { if a.len != b.len { return false } for i in 0 .. a.len { if a[i].path != b[i].path || a[i].hash != b[i].hash || a[i].executable != b[i].executable { return false } } return true }