1234567891011module snapshot1213import os14import time15import src.storage1617pub struct Tree {18pub:19 root string 20 files []FileEntry21}2223pub struct Delta {24pub mut:25 path string26 kind string 27 added int28 removed int29 binary bool30 patch string31}3233pub const binary_notice = 'Binary files differ'3435363738pub 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}73747576pub 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 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}101102103104pub 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}