diff.v

4.1 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		deltas << diff_in(work, path, kind, from_bytes, to_bytes)!76	}77	return deltas78}7980// diff_bytes compares two versions of one file held in memory, for callers81// whose bytes are not in the store yet.82pub fn diff_bytes(path string, kind string, from_bytes []u8, to_bytes []u8) !Delta {83	work := temp_workspace()!84	defer {85		os.rmdir_all(work) or {}86	}87	return diff_in(work, path, kind, from_bytes, to_bytes)!88}8990fn diff_in(work string, path string, kind string, from_bytes []u8, to_bytes []u8) !Delta {91	mut delta := Delta{92		path: path93		kind: kind94	}95	if looks_binary(from_bytes) || looks_binary(to_bytes) {96		delta.binary = true97		delta.patch = binary_notice98	} else {99		delta.patch = run_diff(work, path, from_bytes, to_bytes)!100		delta.added, delta.removed = count_lines(delta.patch)101	}102	return delta103}104105pub fn index_files(files []FileEntry) map[string]FileEntry {106	mut out := map[string]FileEntry{}107	for file in files {108		out[file.path] = file109	}110	return out111}112113fn bytes_of(store &storage.Store, tree Tree, entry FileEntry) ![]u8 {114	if tree.root == '' {115		return store.get_object(entry.hash)!116	}117	return os.read_bytes(os.join_path(tree.root, entry.path))!118}119120fn looks_binary(data []u8) bool {121	limit := if data.len < 8000 { data.len } else { 8000 }122	for i in 0 .. limit {123		if data[i] == 0 {124			return true125		}126	}127	return false128}129130fn count_lines(patch string) (int, int) {131	mut added := 0132	mut removed := 0133	for line in patch.split_into_lines() {134		if line.starts_with('+') && !line.starts_with('+++') {135			added++136		} else if line.starts_with('-') && !line.starts_with('---') {137			removed++138		}139	}140	return added, removed141}142143fn temp_workspace() !string {144	dir := os.join_path(os.temp_dir(), 'koh-diff-${os.getpid()}-${time.now().unix_nano()}')145	os.mkdir_all(dir, os.MkdirParams{})!146	return dir147}148149fn run_diff(work string, path string, from_bytes []u8, to_bytes []u8) !string {150	from_path := os.join_path(work, 'before')151	to_path := os.join_path(work, 'after')152	os.write_file_array(from_path, from_bytes)!153	os.write_file_array(to_path, to_bytes)!154	command := 'diff -u -L ${shell_quote('a/' + path)} -L ${shell_quote('b/' + path)} ${shell_quote(from_path)} ${shell_quote(to_path)}'155	result := os.execute(command)156	if result.exit_code < 0 {157		return error('cannot run diff(1)')158	}159	return result.output160}161162fn shell_quote(text string) string {163	return "'" + text.replace("'", "'\\''") + "'"164}