diff.v

3.7 KB · raw

1//! Comparing trees: which files changed, how much, and the unified diff.2//!3//! Both sides of a comparison are `Tree`s — the working tree (files on disk)4//! or a save (files in the object store) — so every diff Koh offers, working5//! tree against a save, save against save, is the same call.6//!7//! The unified diff itself comes from `diff(1)`: it is present on both target8//! platforms, handles the edge cases, and keeps a thousand lines of line-matching9//! code out of Koh. Koh only parses its output for the +/- counts.1011module snapshot1213import os14import time15import src.storage1617pub struct Tree {18pub:19	root  string // '' when the tree's bytes come from the object store20	files []FileEntry21}2223pub struct Delta {24pub mut:25	path    string26	kind    string // added, removed or modified27	added   int28	removed int29	binary  bool30	patch   string31}3233pub const binary_notice = 'Binary files differ'3435// diff_trees reports every path whose content differs, in path order.36pub fn diff_trees(store &storage.Store, from Tree, to Tree) ![]Delta {37	from_map := index_files(from.files)38	to_map := index_files(to.files)39	mut paths := []string{}40	for path in from_map.keys() {41		paths << path42	}43	for path in to_map.keys() {44		if path !in from_map {45			paths << path46		}47	}48	paths.sort()49	mut deltas := []Delta{}50	work := temp_workspace()!51	defer {52		os.rmdir_all(work) or {}53	}54	for path in paths {55		has_before := path in from_map56		has_after := path in to_map57		if has_before && has_after && from_map[path].hash == to_map[path].hash {58			continue59		}60		kind := if !has_before {61			'added'62		} else if !has_after {63			'removed'64		} else {65			'modified'66		}67		mut from_bytes := []u8{}68		if has_before {69			from_bytes = bytes_of(store, from, from_map[path])!70		}71		mut to_bytes := []u8{}72		if has_after {73			to_bytes = bytes_of(store, to, to_map[path])!74		}75		mut delta := Delta{76			path: path77			kind: kind78		}79		if looks_binary(from_bytes) || looks_binary(to_bytes) {80			delta.binary = true81			delta.patch = binary_notice82		} else {83			delta.patch = run_diff(work, path, from_bytes, to_bytes)!84			delta.added, delta.removed = count_lines(delta.patch)85		}86		deltas << delta87	}88	return deltas89}9091pub fn index_files(files []FileEntry) map[string]FileEntry {92	mut out := map[string]FileEntry{}93	for file in files {94		out[file.path] = file95	}96	return out97}9899fn bytes_of(store &storage.Store, tree Tree, entry FileEntry) ![]u8 {100	if tree.root == '' {101		return store.get_object(entry.hash)!102	}103	return os.read_bytes(os.join_path(tree.root, entry.path))!104}105106fn looks_binary(data []u8) bool {107	limit := if data.len < 8000 { data.len } else { 8000 }108	for i in 0 .. limit {109		if data[i] == 0 {110			return true111		}112	}113	return false114}115116fn count_lines(patch string) (int, int) {117	mut added := 0118	mut removed := 0119	for line in patch.split_into_lines() {120		if line.starts_with('+') && !line.starts_with('+++') {121			added++122		} else if line.starts_with('-') && !line.starts_with('---') {123			removed++124		}125	}126	return added, removed127}128129fn temp_workspace() !string {130	dir := os.join_path(os.temp_dir(), 'koh-diff-${os.getpid()}-${time.now().unix_nano()}')131	os.mkdir_all(dir, os.MkdirParams{})!132	return dir133}134135fn run_diff(work string, path string, from_bytes []u8, to_bytes []u8) !string {136	from_path := os.join_path(work, 'before')137	to_path := os.join_path(work, 'after')138	os.write_file_array(from_path, from_bytes)!139	os.write_file_array(to_path, to_bytes)!140	command := 'diff -u -L ${shell_quote('a/' + path)} -L ${shell_quote('b/' + path)} ${shell_quote(from_path)} ${shell_quote(to_path)}'141	result := os.execute(command)142	if result.exit_code < 0 {143		return error('cannot run diff(1)')144	}145	return result.output146}147148fn shell_quote(text string) string {149	return "'" + text.replace("'", "'\\''") + "'"150}