//! Comparing trees: which files changed, how much, and the unified diff. //! //! Both sides of a comparison are `Tree`s — the working tree (files on disk) //! or a save (files in the object store) — so every diff Koh offers, working //! tree against a save, save against save, is the same call. //! //! The unified diff itself comes from `diff(1)`: it is present on both target //! platforms, handles the edge cases, and keeps a thousand lines of line-matching //! code out of Koh. Koh only parses its output for the +/- counts. module snapshot import os import time import src.storage pub struct Tree { pub: root string // '' when the tree's bytes come from the object store files []FileEntry } pub struct Delta { pub mut: path string kind string // added, removed or modified added int removed int binary bool patch string } pub const binary_notice = 'Binary files differ' // diff_trees reports every path whose content differs, in path order. pub fn diff_trees(store &storage.Store, from Tree, to Tree) ![]Delta { from_map := index_files(from.files) to_map := index_files(to.files) mut paths := []string{} for path in from_map.keys() { paths << path } for path in to_map.keys() { if path !in from_map { paths << path } } paths.sort() mut deltas := []Delta{} work := temp_workspace()! defer { os.rmdir_all(work) or {} } for path in paths { has_before := path in from_map has_after := path in to_map if has_before && has_after && from_map[path].hash == to_map[path].hash { continue } kind := if !has_before { 'added' } else if !has_after { 'removed' } else { 'modified' } mut from_bytes := []u8{} if has_before { from_bytes = bytes_of(store, from, from_map[path])! } mut to_bytes := []u8{} if has_after { to_bytes = bytes_of(store, to, to_map[path])! } mut delta := Delta{ path: path kind: kind } if looks_binary(from_bytes) || looks_binary(to_bytes) { delta.binary = true delta.patch = binary_notice } else { delta.patch = run_diff(work, path, from_bytes, to_bytes)! delta.added, delta.removed = count_lines(delta.patch) } deltas << delta } return deltas } pub fn index_files(files []FileEntry) map[string]FileEntry { mut out := map[string]FileEntry{} for file in files { out[file.path] = file } return out } fn bytes_of(store &storage.Store, tree Tree, entry FileEntry) ![]u8 { if tree.root == '' { return store.get_object(entry.hash)! } return os.read_bytes(os.join_path(tree.root, entry.path))! } fn looks_binary(data []u8) bool { limit := if data.len < 8000 { data.len } else { 8000 } for i in 0 .. limit { if data[i] == 0 { return true } } return false } fn count_lines(patch string) (int, int) { mut added := 0 mut removed := 0 for line in patch.split_into_lines() { if line.starts_with('+') && !line.starts_with('+++') { added++ } else if line.starts_with('-') && !line.starts_with('---') { removed++ } } return added, removed } fn temp_workspace() !string { dir := os.join_path(os.temp_dir(), 'koh-diff-${os.getpid()}-${time.now().unix_nano()}') os.mkdir_all(dir, os.MkdirParams{})! return dir } fn run_diff(work string, path string, from_bytes []u8, to_bytes []u8) !string { from_path := os.join_path(work, 'before') to_path := os.join_path(work, 'after') os.write_file_array(from_path, from_bytes)! os.write_file_array(to_path, to_bytes)! command := 'diff -u -L ${shell_quote('a/' + path)} -L ${shell_quote('b/' + path)} ${shell_quote(from_path)} ${shell_quote(to_path)}' result := os.execute(command) if result.exit_code < 0 { return error('cannot run diff(1)') } return result.output } fn shell_quote(text string) string { return "'" + text.replace("'", "'\\''") + "'" }