tree.v

4.0 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	walk_dir(store, root, '', ig, cache, mut entries)!27	entries.sort(a.path < b.path)28	return entries29}3031// walk_raw hashes the same files without a store: no cache, nothing written.32// `migrate` uses it to check a working tree against a manifest before it has a33// database to check it with.34pub fn walk_raw(root string, ig Ignore) ![]FileEntry {35	mut entries := []FileEntry{}36	walk_dir(none, root, '', ig, map[string]storage.CacheRow{}, mut entries)!37	entries.sort(a.path < b.path)38	return entries39}4041fn walk_dir(store ?&storage.Store, root string, rel string, ig Ignore, cache map[string]storage.CacheRow, mut out []FileEntry) ! {42	dir := if rel == '' { root } else { os.join_path(root, rel) }43	for name in os.ls(dir)! {44		child := if rel == '' { name } else { '${rel}/${name}' }45		st := lstat(os.join_path(root, child)) or { continue }46		// The kind is known by now, so a pattern ending in `/` can be applied47		// to directories only.48		if ig.matches(child, st.kind == .dir) {49			continue50		}51		match st.kind {52			.dir {53				walk_dir(store, root, child, ig, cache, mut out)!54			}55			.file {56				if st.size > storage.max_file_size {57					return error('${child} is ${st.size} bytes; koh stores code and text, not files this large')58				}59				hash := hash_file(store, root, child, st, cache)!60				out << FileEntry{61					path:       child62					hash:       hash63					size:       st.size64					executable: is_executable(st.mode)65				}66			}67			else {}68		}69	}70}7172fn hash_file(store ?&storage.Store, root string, rel string, st Stat, cache map[string]storage.CacheRow) !string {73	if s := store {74		if row := cache[rel] {75			if row.size == st.size && row.mtime_ns == st.mtime_ns && row.ctime_ns == st.ctime_ns {76				return row.hash77			}78		}79		data := os.read_bytes(os.join_path(root, rel))!80		hash := storage.hash_hex(data)81		if !cacheable(st, time.now().unix_nano()) {82			return hash83		}84		s.save_cache(storage.CacheRow{85			path:     rel86			size:     st.size87			mtime_ns: st.mtime_ns88			ctime_ns: st.ctime_ns89			hash:     hash90		})!91		return hash92	}93	return storage.hash_hex(os.read_bytes(os.join_path(root, rel))!)94}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}