tree.v

2.7 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}2627fn walk_dir(store &storage.Store, root string, rel string, ig Ignore, cache map[string]storage.CacheRow, mut out []FileEntry) ! {28	dir := if rel == '' { root } else { os.join_path(root, rel) }29	for name in os.ls(dir)! {30		child := if rel == '' { name } else { '${rel}/${name}' }31		if ig.matches(child) {32			continue33		}34		st := lstat(os.join_path(root, child)) or { continue }35		match st.kind {36			.dir {37				walk_dir(store, root, child, ig, cache, mut out)!38			}39			.file {40				if st.size > storage.max_file_size {41					return error('${child} is ${st.size} bytes; koh stores code and text, not files this large')42				}43				hash := hash_file(store, root, child, st, cache)!44				out << FileEntry{45					path:       child46					hash:       hash47					size:       st.size48					executable: is_executable(st.mode)49				}50			}51			else {}52		}53	}54}5556fn hash_file(store &storage.Store, root string, rel string, st Stat, cache map[string]storage.CacheRow) !string {57	if row := cache[rel] {58		if row.size == st.size && row.mtime_ns == st.mtime_ns && row.ctime_ns == st.ctime_ns {59			return row.hash60		}61	}62	data := os.read_bytes(os.join_path(root, rel))!63	hash := storage.hash_hex(data)64	store.save_cache(storage.CacheRow{65		path:     rel66		size:     st.size67		mtime_ns: st.mtime_ns68		ctime_ns: st.ctime_ns69		hash:     hash70	})!71	return hash72}7374// store_objects writes the bytes of every entry the database does not have75// yet, so a save only pays for content it has never seen.76pub fn store_objects(store &storage.Store, root string, entries []FileEntry) ! {77	for entry in entries {78		if store.has_object(entry.hash)! {79			continue80		}81		data := os.read_bytes(os.join_path(root, entry.path))!82		stored := store.put_object(data)!83		if stored != entry.hash {84			return error('${entry.path} changed while saving')85		}86	}87}8889// same_tree compares two sorted entry lists by path, hash and executable bit.90pub fn same_tree(a []FileEntry, b []FileEntry) bool {91	if a.len != b.len {92		return false93	}94	for i in 0 .. a.len {95		if a[i].path != b[i].path || a[i].hash != b[i].hash || a[i].executable != b[i].executable {96			return false97		}98	}99	return true100}