tree.v

3.3 KB · raw

1//! Walking the working tree: ignore rules, hashing, and the file cache.23module snapshot45import os6import src.storage78// One tracked file in one tree.9pub struct FileEntry {10pub:11	path       string12	hash       string13	size       i6414	executable bool15}1617// walk hashes every tracked file under `root` and returns them sorted by path.18// Symlinks are skipped, as Koh 2 skipped them, so migration stays lossless.19pub fn walk(store &storage.Store, root string, ig Ignore) ![]FileEntry {20	cache := store.load_cache()!21	mut entries := []FileEntry{}22	walk_dir(store, root, '', ig, cache, mut entries)!23	entries.sort(a.path < b.path)24	return entries25}2627// walk_raw hashes the same files without a store: no cache, nothing written.28// `migrate` uses it to check a working tree against a manifest before it has a29// database to check it with.30pub fn walk_raw(root string, ig Ignore) ![]FileEntry {31	mut entries := []FileEntry{}32	walk_dir(none, root, '', ig, map[string]storage.CacheRow{}, mut entries)!33	entries.sort(a.path < b.path)34	return entries35}3637fn walk_dir(store ?&storage.Store, root string, rel string, ig Ignore, cache map[string]storage.CacheRow, mut out []FileEntry) ! {38	dir := if rel == '' { root } else { os.join_path(root, rel) }39	for name in os.ls(dir)! {40		child := if rel == '' { name } else { '${rel}/${name}' }41		st := lstat(os.join_path(root, child)) or { continue }42		// The kind is known by now, so a pattern ending in `/` can be applied43		// to directories only.44		if ig.matches(child, st.kind == .dir) {45			continue46		}47		match st.kind {48			.dir {49				walk_dir(store, root, child, ig, cache, mut out)!50			}51			.file {52				if st.size > storage.max_file_size {53					return error('${child} is ${st.size} bytes; koh stores code and text, not files this large')54				}55				hash := hash_file(store, root, child, st, cache)!56				out << FileEntry{57					path:       child58					hash:       hash59					size:       st.size60					executable: is_executable(st.mode)61				}62			}63			else {}64		}65	}66}6768fn hash_file(store ?&storage.Store, root string, rel string, st Stat, cache map[string]storage.CacheRow) !string {69	if s := store {70		if row := cache[rel] {71			if row.size == st.size && row.mtime_ns == st.mtime_ns && row.ctime_ns == st.ctime_ns {72				return row.hash73			}74		}75		data := os.read_bytes(os.join_path(root, rel))!76		hash := storage.hash_hex(data)77		s.save_cache(storage.CacheRow{78			path:     rel79			size:     st.size80			mtime_ns: st.mtime_ns81			ctime_ns: st.ctime_ns82			hash:     hash83		})!84		return hash85	}86	return storage.hash_hex(os.read_bytes(os.join_path(root, rel))!)87}8889// store_objects writes the bytes of every entry the database does not have90// yet, so a save only pays for content it has never seen.91pub fn store_objects(store &storage.Store, root string, entries []FileEntry) ! {92	for entry in entries {93		if store.has_object(entry.hash)! {94			continue95		}96		data := os.read_bytes(os.join_path(root, entry.path))!97		stored := store.put_object(data)!98		if stored != entry.hash {99			return error('${entry.path} changed while saving')100		}101	}102}103104// same_tree compares two sorted entry lists by path, hash and executable bit.105pub fn same_tree(a []FileEntry, b []FileEntry) bool {106	if a.len != b.len {107		return false108	}109	for i in 0 .. a.len {110		if a[i].path != b[i].path || a[i].hash != b[i].hash || a[i].executable != b[i].executable {111			return false112		}113	}114	return true115}