review

review patchsets using your default editor
Log | Files | Refs

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 }