diff.v

4.8 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// changes lists the paths that differ between two trees and how, in path36// order, from the hashes alone: no file is read and diff(1) never runs. A37// preview needs no more than this.38pub fn changes(from []FileEntry, to []FileEntry) []Delta {39	from_map := index_files(from)40	to_map := index_files(to)41	mut paths := from_map.keys()42	for path in to_map.keys() {43		if path !in from_map {44			paths << path45		}46	}47	paths.sort()48	mut deltas := []Delta{}49	for path in paths {50		before := from_map[path] or {51			deltas << Delta{52				path: path53				kind: 'added'54			}55			continue56		}57		after := to_map[path] or {58			deltas << Delta{59				path: path60				kind: 'removed'61			}62			continue63		}64		if before.hash != after.hash || before.executable != after.executable {65			deltas << Delta{66				path: path67				kind: 'modified'68			}69		}70	}71	return deltas72}7374// diff_trees reports every path whose content differs, in path order, with75// its unified diff.76pub fn diff_trees(store &storage.Store, from Tree, to Tree) ![]Delta {77	from_map := index_files(from.files)78	to_map := index_files(to.files)79	mut deltas := []Delta{}80	work := temp_workspace()!81	defer {82		os.rmdir_all(work) or {}83	}84	for change in changes(from.files, to.files) {85		// A change of the executable bit alone has no text to show.86		if change.kind == 'modified' && from_map[change.path].hash == to_map[change.path].hash {87			continue88		}89		mut from_bytes := []u8{}90		if change.kind != 'added' {91			from_bytes = bytes_of(store, from, from_map[change.path])!92		}93		mut to_bytes := []u8{}94		if change.kind != 'removed' {95			to_bytes = bytes_of(store, to, to_map[change.path])!96		}97		deltas << diff_in(work, change.path, change.kind, from_bytes, to_bytes)!98	}99	return deltas100}101102// diff_bytes compares two versions of one file held in memory, for callers103// whose bytes are not in the store yet.104pub fn diff_bytes(path string, kind string, from_bytes []u8, to_bytes []u8) !Delta {105	work := temp_workspace()!106	defer {107		os.rmdir_all(work) or {}108	}109	return diff_in(work, path, kind, from_bytes, to_bytes)!110}111112fn diff_in(work string, path string, kind string, from_bytes []u8, to_bytes []u8) !Delta {113	mut delta := Delta{114		path: path115		kind: kind116	}117	if looks_binary(from_bytes) || looks_binary(to_bytes) {118		delta.binary = true119		delta.patch = binary_notice120	} else {121		delta.patch = run_diff(work, path, from_bytes, to_bytes)!122		delta.added, delta.removed = count_lines(delta.patch)123	}124	return delta125}126127pub fn index_files(files []FileEntry) map[string]FileEntry {128	mut out := map[string]FileEntry{}129	for file in files {130		out[file.path] = file131	}132	return out133}134135fn bytes_of(store &storage.Store, tree Tree, entry FileEntry) ![]u8 {136	if tree.root == '' {137		return store.get_object(entry.hash)!138	}139	return os.read_bytes(os.join_path(tree.root, entry.path))!140}141142fn looks_binary(data []u8) bool {143	limit := if data.len < 8000 { data.len } else { 8000 }144	for i in 0 .. limit {145		if data[i] == 0 {146			return true147		}148	}149	return false150}151152fn count_lines(patch string) (int, int) {153	mut added := 0154	mut removed := 0155	for line in patch.split_into_lines() {156		if line.starts_with('+') && !line.starts_with('+++') {157			added++158		} else if line.starts_with('-') && !line.starts_with('---') {159			removed++160		}161	}162	return added, removed163}164165fn temp_workspace() !string {166	dir := os.join_path(os.temp_dir(), 'koh-diff-${os.getpid()}-${time.now().unix_nano()}')167	os.mkdir_all(dir, os.MkdirParams{})!168	return dir169}170171fn run_diff(work string, path string, from_bytes []u8, to_bytes []u8) !string {172	from_path := os.join_path(work, 'before')173	to_path := os.join_path(work, 'after')174	os.write_file_array(from_path, from_bytes)!175	os.write_file_array(to_path, to_bytes)!176	command := 'diff -u -L ${shell_quote('a/' + path)} -L ${shell_quote('b/' + path)} ${shell_quote(from_path)} ${shell_quote(to_path)}'177	result := os.execute(command)178	if result.exit_code < 0 {179		return error('cannot run diff(1)')180	}181	return result.output182}183184fn shell_quote(text string) string {185	return "'" + text.replace("'", "'\\''") + "'"186}