tree.odin (4911B)
1 /* 2 Package tree reads the repository as it stands at the end of a change. For 3 the staged change and a bare revision that is the working tree, read in 4 place; for a range it is a tree git holds and the filesystem does not, so 5 the range's end is written once to a scratch directory and read from there. 6 Every reader after that reads files, which is what a sidecar can be handed. 7 */ 8 package tree 9 10 import "core:os" 11 import "core:path/filepath" 12 import "core:slice" 13 import "core:strings" 14 import "jm:sh" 15 import "jm:tar" 16 17 import "../git" 18 19 // Tree is the repository's files at the end of the change. 20 Tree :: struct { 21 // dir is where the files are read from: the repository root, or a 22 // scratch copy of the revision. 23 dir: string, 24 // root is the repository, which is what git is asked about. 25 root: string, 26 // rev is the revision materialised, or empty for the working tree. 27 rev: string, 28 } 29 30 // at is the tree the change arrives at. A range's end is materialised; 31 // the working tree is read in place. 32 at :: proc(root, rev: string, allocator := context.allocator) -> (t: Tree, ok: bool) { 33 after, ranged := ends(rev) 34 if !ranged { 35 return Tree{dir = root, root = root}, true 36 } 37 dir := materialise(root, after, allocator) or_return 38 return Tree{dir = dir, root = root, rev = after}, true 39 } 40 41 // close removes what materialising left behind. 42 close :: proc(t: Tree) { 43 if t.rev != "" && t.dir != t.root { 44 os.remove_all(t.dir) 45 } 46 } 47 48 // materialise writes the revision's files to a scratch directory, out of 49 // git's own archive of it: one ask, however many files, unpacked here 50 // rather than by a tar program the platform may not have. 51 materialise :: proc(root, rev: string, allocator := context.allocator) -> (dir: string, ok: bool) { 52 temp := os.temp_directory(context.temp_allocator) or_else "" 53 scratch, err := os.make_directory_temp(temp, "review-tree-*", allocator) 54 if err != nil { 55 return "", false 56 } 57 r := sh.exec({"git", "archive", "--format=tar", rev}, {dir = root}, context.temp_allocator) 58 if !r.ok { 59 os.remove_all(scratch) 60 return "", false 61 } 62 if _, unpacked := tar.extract(transmute([]byte)r.stdout, scratch); unpacked != .None { 63 os.remove_all(scratch) 64 return "", false 65 } 66 return scratch, true 67 } 68 69 // files lists the tracked paths at the end of the change. The staged 70 // change is tracked by the index, which is what ls-files reads. 71 files :: proc(t: Tree, allocator := context.allocator) -> (out: []string, ok: bool) { 72 if t.rev == "" { 73 return git.lines(t.root, {"ls-files"}, allocator) 74 } 75 return git.lines(t.root, {"ls-tree", "-r", "--name-only", t.rev}, allocator) 76 } 77 78 // path is where a tracked file can be read by a program that reads files. 79 path :: proc(t: Tree, name: string, allocator := context.allocator) -> string { 80 joined, err := filepath.join({t.dir, name}, allocator) 81 if err != nil { 82 return name 83 } 84 return joined 85 } 86 87 // read is a tracked file's contents at the end of the change. 88 read :: proc(t: Tree, name: string, allocator := context.allocator) -> (data: []byte, ok: bool) { 89 contents, err := os.read_entire_file_from_path( 90 path(t, name, context.temp_allocator), 91 allocator, 92 ) 93 return contents, err == nil 94 } 95 96 // sources reads every tracked file that is text, so that a search over 97 // the repository is a loop over memory rather than a program run. Binary 98 // files are left out by the cheapest test there is: a NUL byte in the 99 // first kilobyte. 100 sources :: proc(t: Tree, allocator := context.allocator) -> (out: map[string][]byte, ok: bool) { 101 tracked := files(t, context.temp_allocator) or_return 102 out = make(map[string][]byte, allocator) 103 for name in tracked { 104 data, readable := read(t, name, allocator) 105 if !readable { 106 continue // A tracked path the tree cannot read is a link or gone. 107 } 108 head := data[:min(len(data), 1024)] 109 if slice.contains(head, 0) { 110 delete(data, allocator) 111 continue 112 } 113 out[strings.clone(name, allocator)] = data 114 } 115 return out, true 116 } 117 118 // line is one line of a file at the end of the change, trimmed, or empty 119 // where the file or the line is not there. 120 line :: proc(t: Tree, name: string, number: int, allocator := context.allocator) -> string { 121 if number < 1 { 122 return "" 123 } 124 data, ok := read(t, name, context.temp_allocator) 125 if !ok { 126 return "" 127 } 128 rest := string(data) 129 n := 0 130 for text in strings.split_lines_iterator(&rest) { 131 n += 1 132 if n == number { 133 return strings.clone(strings.trim_space(text), allocator) 134 } 135 } 136 return "" 137 } 138 139 // exists reports whether the tree holds the path as a file. 140 exists :: proc(t: Tree, name: string) -> bool { 141 return os.is_file(path(t, name, context.temp_allocator)) 142 } 143 144 // ends reports the revision a range arrives at, and whether it is a range 145 // at all. Both A..B and A...B are reviewed as B. 146 ends :: proc(rev: string) -> (after: string, ranged: bool) { 147 i := strings.index(rev, "..") 148 if i < 0 { 149 return rev, false 150 } 151 return strings.trim_prefix(rev[i + 2:], "."), true 152 }