github.com/aclements/go-misc@v0.0.0-20240129233631-2f6ede80790c/gover/cache.go (about) 1 // Copyright 2016 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 package main 6 7 import ( 8 "io/ioutil" 9 "log" 10 "os" 11 "path/filepath" 12 "regexp" 13 "strconv" 14 "strings" 15 "time" 16 ) 17 18 var hashNameRe = regexp.MustCompile(`^[0-9a-f]{7,40}(\+[0-9a-f]{1,10})?$`) 19 var fullHashRe = regexp.MustCompile("^[0-9a-f]{40}$") 20 var hashPlusRe = regexp.MustCompile(`^[0-9a-f]{40}(\+[0-9a-f]{10})?$`) 21 22 // resolveName returns the path to the root of the named build and 23 // whether or not that path exists. It will log an error and exit if 24 // name is ambiguous. If the path does not exist, the returned path is 25 // where this build should be saved. 26 func resolveName(name string) (path string, ok bool) { 27 // If the name exactly matches a saved version, return it. 28 savePath := filepath.Join(*verDir, name) 29 st, err := os.Stat(savePath) 30 if err == nil && st.IsDir() { 31 return savePath, true 32 } 33 34 // Otherwise, try to resolve it as an unambiguous hash prefix. 35 if hashNameRe.MatchString(name) { 36 nameParts := strings.SplitN(name, "+", 2) 37 builds, err := listBuilds(0) 38 if err != nil { 39 log.Fatal(err) 40 } 41 42 var fullName string 43 for _, b := range builds { 44 if !strings.HasPrefix(b.commitHash, nameParts[0]) { 45 continue 46 } 47 if (len(nameParts) == 1) != (b.deltaHash == "") { 48 continue 49 } 50 if len(nameParts) > 1 && !strings.HasPrefix(b.deltaHash, nameParts[1]) { 51 continue 52 } 53 54 // We found a match. 55 if fullName != "" { 56 log.Fatalf("ambiguous name `%s`", name) 57 } 58 fullName = b.fullName() 59 } 60 if fullName != "" { 61 return filepath.Join(*verDir, fullName), true 62 } 63 } 64 65 return savePath, false 66 } 67 68 type buildInfo struct { 69 path string 70 commitHash string 71 deltaHash string 72 names []string 73 commit *commit 74 } 75 76 func (i buildInfo) fullName() string { 77 if i.deltaHash == "" { 78 return i.commitHash 79 } 80 return i.commitHash + "+" + i.deltaHash 81 } 82 83 func (i buildInfo) shortName() string { 84 // TODO: Print more than 7 characters if necessary. 85 if i.deltaHash == "" { 86 return i.commitHash[:7] 87 } 88 return i.commitHash[:7] + "+" + i.deltaHash 89 } 90 91 type listFlags int 92 93 const ( 94 listNames listFlags = 1 << iota 95 listCommit 96 ) 97 98 func listBuilds(flags listFlags) ([]*buildInfo, error) { 99 files, err := ioutil.ReadDir(*verDir) 100 if os.IsNotExist(err) { 101 return nil, nil 102 } else if err != nil { 103 return nil, err 104 } 105 106 // Collect the saved builds. 107 builds := []*buildInfo{} 108 var baseMap map[string]*buildInfo 109 if flags&listNames != 0 { 110 baseMap = make(map[string]*buildInfo) 111 } 112 for _, file := range files { 113 if !file.IsDir() || !hashPlusRe.MatchString(file.Name()) { 114 continue 115 } 116 nameParts := strings.SplitN(file.Name(), "+", 2) 117 info := &buildInfo{path: filepath.Join(*verDir, file.Name()), commitHash: nameParts[0]} 118 if len(nameParts) > 1 { 119 info.deltaHash = nameParts[1] 120 } 121 122 builds = append(builds, info) 123 if baseMap != nil { 124 baseMap[file.Name()] = info 125 } 126 127 if flags&listCommit != 0 { 128 commit, err := ioutil.ReadFile(filepath.Join(*verDir, file.Name(), "commit")) 129 if err != nil { 130 log.Fatal(err) 131 } else { 132 info.commit = parseCommit(commit) 133 } 134 } 135 } 136 137 // Collect the names for each build. 138 if flags&listNames != 0 { 139 for _, file := range files { 140 if file.Mode()&os.ModeType == os.ModeSymlink { 141 base, err := os.Readlink(filepath.Join(*verDir, file.Name())) 142 if err != nil { 143 continue 144 } 145 if info, ok := baseMap[base]; ok { 146 info.names = append(info.names, file.Name()) 147 } 148 } 149 } 150 } 151 152 return builds, nil 153 } 154 155 type commit struct { 156 authorDate time.Time 157 topLine string 158 } 159 160 func parseCommit(obj []byte) *commit { 161 out := &commit{} 162 lines := strings.Split(string(obj), "\n") 163 for i, line := range lines { 164 if strings.HasPrefix(line, "author ") { 165 fs := strings.Fields(line) 166 secs, err := strconv.ParseInt(fs[len(fs)-2], 10, 64) 167 if err != nil { 168 log.Fatalf("malformed author in commit: %s", err) 169 } 170 out.authorDate = time.Unix(secs, 0) 171 } 172 if len(line) == 0 { 173 out.topLine = lines[i+1] 174 break 175 } 176 } 177 return out 178 }