tree.v

4.2 KB · raw

1//! Walking the working tree: ignore rules, hashing, and the file cache.23module snapshot45import os6import time7import src.storage89// FAT filesystems keep mtime in 2-second steps; three covers them too.10const racy_window_ns = i64(3_000_000_000)1112// One tracked file in one tree.13pub struct FileEntry {14pub:15	path       string16	hash       string17	size       i6418	executable bool19}2021// walk hashes every tracked file under `root` and returns them sorted by path.22// Symlinks are skipped, as Koh 2 skipped them, so migration stays lossless.23pub fn walk(store &storage.Store, root string, ig Ignore) ![]FileEntry {24	cache := store.load_cache()!25	mut entries := []FileEntry{}26	mut fresh := []storage.CacheRow{}27	walk_dir(root, '', ig.banning(store.stripped_paths()!), cache, mut entries, mut fresh)!28	// Skipped while another command writes, or inside a caller's transaction:29	// the next walk hashes those files again, which is all a missing row costs.30	store.save_cache_rows(fresh) or {}31	entries.sort(a.path < b.path)32	return entries33}3435// walk_raw hashes the same files without a store: no cache, nothing written.36// `migrate` uses it to check a working tree against a manifest before it has a37// database to check it with.38pub fn walk_raw(root string, ig Ignore) ![]FileEntry {39	mut entries := []FileEntry{}40	mut unused := []storage.CacheRow{}41	walk_dir(root, '', ig, map[string]storage.CacheRow{}, mut entries, mut unused)!42	entries.sort(a.path < b.path)43	return entries44}4546fn walk_dir(root string, rel string, ig Ignore, cache map[string]storage.CacheRow, mut out []FileEntry, mut fresh []storage.CacheRow) ! {47	dir := if rel == '' { root } else { os.join_path(root, rel) }48	for name in os.ls(dir)! {49		child := if rel == '' { name } else { '${rel}/${name}' }50		st := lstat(os.join_path(root, child)) or { continue }51		// The kind is known by now, so a pattern ending in `/` can be applied52		// to directories only.53		if ig.matches(child, st.kind == .dir) {54			continue55		}56		match st.kind {57			.dir {58				walk_dir(root, child, ig, cache, mut out, mut fresh)!59			}60			.file {61				if st.size > storage.max_file_size {62					return error('${child} is ${st.size} bytes; koh stores code and text, not files this large')63				}64				hash := hash_file(root, child, st, cache, mut fresh)!65				out << FileEntry{66					path:       child67					hash:       hash68					size:       st.size69					executable: is_executable(st.mode)70				}71			}72			else {}73		}74	}75}7677fn hash_file(root string, rel string, st Stat, cache map[string]storage.CacheRow, mut fresh []storage.CacheRow) !string {78	if row := cache[rel] {79		if row.size == st.size && row.mtime_ns == st.mtime_ns && row.ctime_ns == st.ctime_ns {80			return row.hash81		}82	}83	hash := storage.hash_hex(os.read_bytes(os.join_path(root, rel))!)84	if cacheable(st, time.now().unix_nano()) {85		fresh << storage.CacheRow{86			path:     rel87			size:     st.size88			mtime_ns: st.mtime_ns89			ctime_ns: st.ctime_ns90			hash:     hash91		}92	}93	return hash94}9596// cacheable reports whether a hash may be cached. Linux stamps files from a97// clock that ticks every few milliseconds, so a same-size rewrite in the tick98// of the hash would look unchanged; a file changed that recently is hashed99// again next time instead. `cp -p` and the like keep an old mtime while100// rewriting, so the newer of mtime and ctime decides.101fn cacheable(st Stat, now_ns i64) bool {102	changed := if st.ctime_ns > st.mtime_ns { st.ctime_ns } else { st.mtime_ns }103	return now_ns - changed >= racy_window_ns104}105106// store_objects writes the bytes of every entry the database does not have107// yet, so a save only pays for content it has never seen.108pub fn store_objects(store &storage.Store, root string, entries []FileEntry) ! {109	for entry in entries {110		if store.has_object(entry.hash)! {111			continue112		}113		data := os.read_bytes(os.join_path(root, entry.path))!114		stored := store.put_object(data)!115		if stored != entry.hash {116			return error('${entry.path} changed while saving')117		}118	}119}120121// same_tree compares two sorted entry lists by path, hash and executable bit.122pub fn same_tree(a []FileEntry, b []FileEntry) bool {123	if a.len != b.len {124		return false125	}126	for i in 0 .. a.len {127		if a[i].path != b[i].path || a[i].hash != b[i].hash || a[i].executable != b[i].executable {128			return false129		}130	}131	return true132}