//! 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' // changes lists the paths that differ between two trees and how, in path // order, from the hashes alone: no file is read and diff(1) never runs. A // preview needs no more than this. pub fn changes(from []FileEntry, to []FileEntry) []Delta { from_map := index_files(from) to_map := index_files(to) mut paths := from_map.keys() for path in to_map.keys() { if path !in from_map { paths << path } } paths.sort() mut deltas := []Delta{} for path in paths { before := from_map[path] or { deltas << Delta{ path: path kind: 'added' } continue } after := to_map[path] or { deltas << Delta{ path: path kind: 'removed' } continue } if before.hash != after.hash || before.executable != after.executable { deltas << Delta{ path: path kind: 'modified' } } } return deltas } // diff_trees reports every path whose content differs, in path order, with // its unified diff. 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 deltas := []Delta{} work := temp_workspace()! defer { os.rmdir_all(work) or {} } for change in changes(from.files, to.files) { // A change of the executable bit alone has no text to show. if change.kind == 'modified' && from_map[change.path].hash == to_map[change.path].hash { continue } mut from_bytes := []u8{} if change.kind != 'added' { from_bytes = bytes_of(store, from, from_map[change.path])! } mut to_bytes := []u8{} if change.kind != 'removed' { to_bytes = bytes_of(store, to, to_map[change.path])! } deltas << diff_in(work, change.path, change.kind, from_bytes, to_bytes)! } return deltas } // diff_bytes compares two versions of one file held in memory, for callers // whose bytes are not in the store yet. pub fn diff_bytes(path string, kind string, from_bytes []u8, to_bytes []u8) !Delta { work := temp_workspace()! defer { os.rmdir_all(work) or {} } return diff_in(work, path, kind, from_bytes, to_bytes)! } fn diff_in(work string, path string, kind string, from_bytes []u8, to_bytes []u8) !Delta { 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) } return delta } 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("'", "'\\''") + "'" }