├── .gitignore ├── README └── gitbrute.go /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | gitbrute brute-forces a pair of author+committer timestamps such that 2 | the resulting git commit has your desired prefix. 3 | 4 | It will find the most recent time that satisfies your prefix. 5 | 6 | Shorter prefixes match more quickly, of course. The author & 7 | committer timestamp are not kept in sync. 8 | 9 | Example: https://github.com/bradfitz/deadbeef 10 | 11 | Usage: 12 | 13 | go run gitbrute.go --prefix 000000 14 | 15 | This amends the last commit of the current repository. 16 | -------------------------------------------------------------------------------- /gitbrute.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // The gitbrute command brute-forces a git commit hash prefix. 18 | package main 19 | 20 | import ( 21 | "bytes" 22 | "crypto/sha1" 23 | "flag" 24 | "fmt" 25 | "log" 26 | "os" 27 | "os/exec" 28 | "regexp" 29 | "runtime" 30 | "strconv" 31 | "strings" 32 | "time" 33 | ) 34 | 35 | var ( 36 | prefix = flag.String("prefix", "bf", "Desired prefix") 37 | force = flag.Bool("force", false, "Re-run, even if current hash matches prefix") 38 | cpu = flag.Int("cpus", runtime.NumCPU(), "Number of CPUs to use. Defaults to number of processors.") 39 | ) 40 | 41 | var ( 42 | start = time.Now() 43 | startUnix = start.Unix() 44 | ) 45 | 46 | func main() { 47 | flag.Parse() 48 | runtime.GOMAXPROCS(*cpu) 49 | if _, err := strconv.ParseInt(*prefix, 16, 64); err != nil { 50 | log.Fatalf("Prefix %q isn't hex.", *prefix) 51 | } 52 | 53 | hash := curHash() 54 | if strings.HasPrefix(hash, *prefix) && !*force { 55 | return 56 | } 57 | 58 | obj, err := exec.Command("git", "cat-file", "-p", hash).Output() 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | i := bytes.Index(obj, []byte("\n\n")) 63 | if i < 0 { 64 | log.Fatalf("No \\n\\n found in %q", obj) 65 | } 66 | msg := obj[i+2:] 67 | 68 | possibilities := make(chan try, 512) 69 | go explore(possibilities) 70 | 71 | winner := make(chan solution) 72 | done := make(chan struct{}) 73 | 74 | for i := 0; i < *cpu; i++ { 75 | go bruteForce(obj, winner, possibilities, done) 76 | } 77 | 78 | w := <-winner 79 | close(done) 80 | 81 | cmd := exec.Command("git", "commit", "--allow-empty", "--amend", "--date="+w.author.String(), "--file=-") 82 | cmd.Env = append(os.Environ(), "GIT_COMMITTER_DATE="+w.committer.String()) 83 | cmd.Stdout = os.Stdout 84 | cmd.Stdin = bytes.NewReader(msg) 85 | if err := cmd.Run(); err != nil { 86 | log.Fatalf("amend: %v", err) 87 | } 88 | } 89 | 90 | type solution struct { 91 | author, committer date 92 | } 93 | 94 | var ( 95 | authorDateRx = regexp.MustCompile(`(?m)^author.+> (.+)`) 96 | committerDateRx = regexp.MustCompile(`(?m)^committer.+> (.+)`) 97 | ) 98 | 99 | func bruteForce(obj []byte, winner chan<- solution, possibilities <-chan try, done <-chan struct{}) { 100 | // blob is the blob to mutate in-place repeatedly while testing 101 | // whether we have a match. 102 | blob := []byte(fmt.Sprintf("commit %d\x00%s", len(obj), obj)) 103 | authorDate, adatei := getDate(blob, authorDateRx) 104 | commitDate, cdatei := getDate(blob, committerDateRx) 105 | 106 | s1 := sha1.New() 107 | wantHexPrefix := []byte(strings.ToLower(*prefix)) 108 | hexBuf := make([]byte, 0, sha1.Size*2) 109 | 110 | for t := range possibilities { 111 | select { 112 | case <-done: 113 | return 114 | default: 115 | ad := date{startUnix - int64(t.authorBehind), authorDate.tz} 116 | cd := date{startUnix - int64(t.commitBehind), commitDate.tz} 117 | strconv.AppendInt(blob[:adatei], ad.n, 10) 118 | strconv.AppendInt(blob[:cdatei], cd.n, 10) 119 | s1.Reset() 120 | s1.Write(blob) 121 | if !bytes.HasPrefix(hexInPlace(s1.Sum(hexBuf[:0])), wantHexPrefix) { 122 | continue 123 | } 124 | 125 | winner <- solution{ad, cd} 126 | return 127 | } 128 | } 129 | } 130 | 131 | // try is a pair of seconds behind now to brute force, looking for a 132 | // matching commit. 133 | type try struct { 134 | commitBehind int 135 | authorBehind int 136 | } 137 | 138 | // explore yields the sequence: 139 | // (0, 0) 140 | // 141 | // (0, 1) 142 | // (1, 0) 143 | // (1, 1) 144 | // 145 | // (0, 2) 146 | // (1, 2) 147 | // (2, 0) 148 | // (2, 1) 149 | // (2, 2) 150 | // 151 | // ... 152 | func explore(c chan<- try) { 153 | for max := 0; ; max++ { 154 | for i := 0; i <= max-1; i++ { 155 | c <- try{i, max} 156 | } 157 | for j := 0; j <= max; j++ { 158 | c <- try{max, j} 159 | } 160 | } 161 | } 162 | 163 | // date is a git date. 164 | type date struct { 165 | n int64 // unix seconds 166 | tz string 167 | } 168 | 169 | func (d date) String() string { return fmt.Sprintf("%d %s", d.n, d.tz) } 170 | 171 | // getDate parses out a date from a git header (or blob with a header 172 | // following the size and null byte). It returns the date and index 173 | // that the unix seconds begins at within h. 174 | func getDate(h []byte, rx *regexp.Regexp) (d date, idx int) { 175 | m := rx.FindSubmatchIndex(h) 176 | if m == nil { 177 | log.Fatalf("Failed to match %s in %q", rx, h) 178 | } 179 | v := string(h[m[2]:m[3]]) 180 | space := strings.Index(v, " ") 181 | if space < 0 { 182 | log.Fatalf("unexpected date %q", v) 183 | } 184 | n, err := strconv.ParseInt(v[:space], 10, 64) 185 | if err != nil { 186 | log.Fatalf("unexpected date %q", v) 187 | } 188 | return date{n, v[space+1:]}, m[2] 189 | } 190 | 191 | func curHash() string { 192 | all, err := exec.Command("git", "rev-parse", "HEAD").Output() 193 | if err != nil { 194 | log.Fatal(err) 195 | } 196 | h := string(all) 197 | if i := strings.Index(h, "\n"); i > 0 { 198 | h = h[:i] 199 | } 200 | return h 201 | } 202 | 203 | // hexInPlace takes a slice of binary data and returns the same slice with double 204 | // its length, hex-ified in-place. 205 | func hexInPlace(v []byte) []byte { 206 | const hex = "0123456789abcdef" 207 | h := v[:len(v)*2] 208 | for i := len(v) - 1; i >= 0; i-- { 209 | b := v[i] 210 | h[i*2+0] = hex[b>>4] 211 | h[i*2+1] = hex[b&0xf] 212 | } 213 | return h 214 | } 215 | --------------------------------------------------------------------------------