├── go.mod ├── README.md ├── LICENSE └── main.go /go.mod: -------------------------------------------------------------------------------- 1 | module rsc.io/grepdiff 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go get [-u] rsc.io/grepdiff 2 | 3 | https://godoc.org/rsc.io/grepdiff 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Grepdiff greps diffs. 6 | // 7 | // Usage: 8 | // 9 | // grepdiff regexp [file.diff ...] 10 | // 11 | // Grepdiff reads diffs from the named files (or standard input) 12 | // and prints a reduced diff containing only the hunks matching 13 | // the regular expression. 14 | // 15 | // The diffs are expected to be in unified diff format, 16 | // as produced by commands like ``git diff'' or ``hg diff''. 17 | // 18 | // The regular expression syntax is that of the Go regexp package, 19 | // which matches RE2 and roughly matches PCRE, Perl, and most 20 | // other languages. 21 | // For details, see ``go doc regexp/syntax'' or https://godoc.org/regexp/syntax. 22 | // 23 | // Unlike grep, the regexp search considers the entire hunk 24 | // starting with the @@ line, not individual lines. 25 | // It is therefore possible to search for multiline matches. 26 | // As a nod to grep, however, by default the regexp search allows ^ and $ to 27 | // match the start and end of each line, not just the start and end of the hunk. 28 | // (To restrict ^ and $ to the start and end of the entire hunk, 29 | // begin prefix the regexp passed to grepdiff with ``(?-m)''.) 30 | // 31 | // Grepdiff exits with status 0 if it found any matches, 1 if it found no matches, and 2 if an error occurred. 32 | // 33 | // Examples 34 | // 35 | // Diff two Git revisions and extract just the hunks mentioning foo: 36 | // 37 | // git diff rev1 rev2 | grepdiff foo 38 | // 39 | // For the adventurous, apply those changes: 40 | // 41 | // git diff rev1 rev2 | grepdiff foo | git apply 42 | // 43 | // Extract changes in func New: 44 | // 45 | // git diff rev1 HEAD | grepdiff ' @@ func New\(' 46 | // 47 | // Search for new TODOs before sending out for review: 48 | // 49 | // git diff origin...HEAD | grepdiff '^\+.*TODO' 50 | // 51 | package main 52 | 53 | import ( 54 | "bytes" 55 | "flag" 56 | "fmt" 57 | "io/ioutil" 58 | "log" 59 | "os" 60 | "regexp" 61 | ) 62 | 63 | func usage() { 64 | fmt.Fprintf(os.Stderr, "usage: grepdiff regexp [file.diff ...]\n") 65 | // fmt.Fprintf(os.Stderr, "options:\n") 66 | // flag.PrintDefaults() 67 | os.Exit(2) 68 | } 69 | 70 | var exitStatus = 1 71 | 72 | func main() { 73 | log.SetFlags(0) 74 | log.SetPrefix("grepdiff: ") 75 | flag.Usage = usage 76 | flag.Parse() 77 | if flag.NArg() < 1 { 78 | usage() 79 | } 80 | 81 | reStr := flag.Arg(0) 82 | files := flag.Args()[1:] 83 | 84 | // Compile regexp twice: once unmodified for reporting errors, 85 | // and again with (?m) for real use. If somehow the (?m) does cause 86 | // a problem, handle it gracefully. 87 | re, err := regexp.Compile(reStr) 88 | if err != nil { 89 | log.Fatal(err) 90 | } 91 | re, err = regexp.Compile("(?m)" + reStr) 92 | if err != nil { 93 | log.Fatal(err) 94 | } 95 | 96 | if len(files) == 0 { 97 | grepDiff(re, os.Stdin) 98 | } else { 99 | for _, file := range files { 100 | f, err := os.Open(file) 101 | if err != nil { 102 | log.Print(err) 103 | exitStatus = 2 104 | continue 105 | } 106 | grepDiff(re, f) 107 | f.Close() 108 | } 109 | } 110 | 111 | os.Exit(exitStatus) 112 | } 113 | 114 | func grepDiff(re *regexp.Regexp, file *os.File) { 115 | data, err := ioutil.ReadAll(file) 116 | grepDiffData(re, data, file.Name()) 117 | if err != nil { 118 | log.Print("%v", err) 119 | exitStatus = 2 120 | } 121 | } 122 | 123 | var ( 124 | diffLine = []byte("\ndiff ") 125 | hunkLine = []byte("\n@@ ") 126 | ) 127 | 128 | func grepDiffData(re *regexp.Regexp, data []byte, name string) { 129 | forEach(data, "diff ", func(diff []byte) { 130 | var hdr []byte 131 | forEach(diff, "@@ ", func(hunk []byte) { 132 | if hdr == nil { 133 | hdr = diff[:cap(diff)-cap(hunk)] 134 | } 135 | if re.Match(hunk) { 136 | os.Stdout.Write(hdr) 137 | hdr = hdr[:0] // not nil 138 | os.Stdout.Write(hunk) 139 | if exitStatus == 1 { 140 | exitStatus = 0 141 | } 142 | } 143 | }) 144 | }) 145 | } 146 | 147 | func forEach(x []byte, prefix string, f func([]byte)) { 148 | b := []byte("\n" + prefix) 149 | start := 0 150 | if !bytes.HasPrefix(x, b[1:]) { 151 | start = bytes.Index(x, b) + 1 152 | if start == 0 { 153 | return 154 | } 155 | } 156 | for start < len(x) { 157 | i := bytes.Index(x[start:], b) + start + 1 158 | if i == start { 159 | i = len(x) 160 | } 161 | if start >= 0 { 162 | f(x[start:i]) 163 | } 164 | start = i 165 | } 166 | } 167 | --------------------------------------------------------------------------------