github.com/ETCDEVTeam/janus@v0.2.4-0.20180611132348-f6c8fba730fa/gitvv/gitvv.go (about) 1 package gitvv 2 3 import ( 4 "errors" 5 "log" 6 "os/exec" 7 "regexp" 8 "strconv" 9 "strings" 10 ) 11 12 type gitDescription int 13 14 const defaultHashLength = 7 15 16 var ( 17 cacheLastTagName string 18 cacheCommitCountFromTagName string 19 cacheCommitCount string 20 cacheHEADHash string 21 ) 22 23 func isHash(s string) bool { 24 // Strip 'g' prefix for SHA1 25 if strings.HasPrefix(s, "g") { 26 s = s[1:] 27 } 28 // Must be 0-9 V a-f 29 r := regexp.MustCompile(`\b([^g-z\W]\w+)\b`) 30 if r.MatchString(s) { 31 return true 32 } 33 return false 34 } 35 36 // getTagOnHEADCommit gets the tag on the current commit, else 37 // returns "" if no tag on current commit 38 func getTagIfTagOnHEADCommit(dir string) (string, bool) { 39 //git describe --exact-match --abbrev=0 40 41 c, e := exec.Command("git", "-C", dir, "describe", "--exact-match", "--abbrev=0").CombinedOutput() 42 if e != nil { 43 //log.Println(e) 44 return "", false 45 } 46 tag := strings.TrimSpace(string(c)) 47 if tag == "" { 48 return tag, false 49 } 50 return tag, true 51 } 52 53 func getCommitCountFrom(fromTag, dir string) string { 54 if cacheCommitCount != "" && cacheCommitCountFromTagName == fromTag { 55 return cacheCommitCount 56 } 57 58 reference := "HEAD" 59 if fromTag != "" { 60 reference = fromTag + "..HEAD" 61 } 62 c, e := exec.Command("git", "-C", dir, "rev-list", reference, "--count").Output() 63 if e != nil { 64 // TODO: handle error better 65 //log.Println(e) 66 return "0" 67 } 68 69 // Save caches 70 cacheCommitCount = strings.TrimSpace(string(c)) 71 cacheCommitCountFromTagName = fromTag 72 73 return cacheCommitCount 74 } 75 76 func getHEADHash(length int, dir string) string { 77 if cacheHEADHash != "" { 78 return cacheHEADHash[:length] 79 } 80 c, e := exec.Command("git", "-C", dir, "rev-parse", "HEAD").Output() 81 // > b9d3d5da740b4ed748734565614b8fe7885d9714 82 if e != nil { 83 log.Fatalln(e) 84 return "???????" 85 } 86 87 sha1 := strings.TrimSpace(string(c)) 88 cacheHEADHash = sha1 // cache 89 90 if length > len(cacheHEADHash) { 91 length = len(cacheHEADHash) 92 } 93 return cacheHEADHash[:length] 94 } 95 96 func getLastTag(dir string) (string, bool) { 97 if cacheLastTagName != "" { 98 return cacheLastTagName, true 99 } 100 vOut, verErr := exec.Command("git", "-C", dir, "describe", "--tags", "--abbrev=0").CombinedOutput() 101 if verErr != nil { 102 //log.Println(verErr) 103 return "", false 104 } 105 106 tag := strings.TrimSpace(string(vOut)) 107 108 // Has no tags 109 if tag == "" { 110 return tag, false 111 } 112 113 cacheLastTagName = tag 114 return cacheLastTagName, true 115 } 116 117 // Assumes using semver format for tags, eg v3.5.0 or 3.4.0 118 func parseSemverFromTag(s string) []string { 119 tag := strings.TrimPrefix(s, "v") 120 vers := strings.Split(tag, ".") 121 return vers 122 } 123 124 // parseHashLength parses desired hash length output with default for none set 125 // eg. 126 // %S8 -> 8 127 // %S123 -> 123 128 // %S -> defaultLen 129 // NOTE: only compatible with single use #TODO? 130 func parseHashLength(s string) (int, error) { 131 re := regexp.MustCompile(`%S(\d+)`) 132 m := re.MatchString(s) 133 // no digits following %S, use default 134 if !m { 135 return defaultHashLength, nil 136 } 137 f := re.FindAllString(s, 1) 138 if f == nil || len(f) == 0 { 139 return 0, errors.New("regex return match but no matching string(s) found") 140 } 141 ff := f[0] 142 ff = strings.TrimPrefix(ff, "%S") 143 i, e := strconv.Atoi(ff) 144 if e != nil { 145 return 0, e 146 } 147 148 return i, nil 149 } 150 151 // getB gets the semi-semver/mod patch number 152 func getB(dir string) (string, bool) { 153 t, exists := getLastTag(dir) 154 if !exists { 155 return "", false 156 } 157 semvers := parseSemverFromTag(t) 158 if len(semvers) != 3 { 159 return "", false 160 } 161 p := semvers[2] 162 163 c := getCommitCountFrom(t, dir) 164 165 pi, e := strconv.Atoi(p) 166 if e != nil { 167 log.Println(e) 168 return "", false 169 } 170 pi = pi * 100 171 172 ci, e := strconv.Atoi(c) 173 if e != nil { 174 log.Println(e) 175 return "", false 176 } 177 178 bi := pi + ci 179 b := strconv.Itoa(bi) 180 181 return b, true 182 } 183 184 // GetVersion gets formatted git version 185 // It assumes tags are by semver standards 186 // format: 187 // %M, _M - major version 188 // %m, _m - minor version 189 // %P, _P - patch version 190 // %C, _C - commit count since last tag 191 // %S, _S - HEAD sha1 192 // %B - hybrid patch number [semver_minor_version*100 + commit_count] 193 func GetVersion(format, dir string) string { 194 195 var ( 196 lastTag string 197 commitCount string = "0" 198 semvers = []string{} 199 sha string 200 ) 201 semvers = nil 202 203 // Set current dir as default in case flag not set. 204 if dir == "" { 205 dir = "." 206 } 207 208 // Set default format. 209 if format == "" { 210 // v3.5.0+66-bbb06b1 211 format = "v%M.%m.%P-%S" 212 } 213 214 // Need to get commit count 215 lastTag, _ = getTagIfTagOnHEADCommit(dir) 216 // Is not 0 217 if lastTag == "" { 218 lastTag, _ = getLastTag(dir) 219 // Either from init (entire branch) or lastTag 220 221 } 222 223 commitCount = getCommitCountFrom(lastTag, dir) 224 if lastTag != "" { 225 semvers = parseSemverFromTag(lastTag) 226 } 227 228 // Convention alert: 229 // Want: when commit count is 0 (ie HEAD is on a tag), should yield only semver, eg v3.5.0 230 // when commit count is >0 (ie HEAD is above a tag), should yield full "nightly" version name, eg v3.5.0+14-adfe123 231 // This syntax allows to signify tagged builds vs running builds. 232 // -- The point of this is just to be able to shift some logic out of CI scripts. 233 if format == "TAG_OR_NIGHTLY" { 234 format = "v%M.%m.%P+%C-%S" 235 if commitCount == "0" { 236 format = "v%M.%m.%P-%S" 237 } 238 } 239 240 sha = getHEADHash(defaultHashLength, dir) 241 if strings.Index(format, "%S") >= 0 { 242 l, e := parseHashLength(format) 243 if e != nil { 244 log.Println(e) 245 } 246 if l != defaultHashLength { 247 cacheHEADHash = "" 248 sha = getHEADHash(l, dir) 249 } 250 } 251 252 out := format 253 254 if semvers != nil { 255 // -1 to replace indefinitely. Allows maximum user-decision-making. 256 out = strings.Replace(out, "%M", semvers[0], -1) 257 out = strings.Replace(out, "_M", semvers[0], -1) 258 out = strings.Replace(out, "%m", semvers[1], -1) 259 out = strings.Replace(out, "_m", semvers[1], -1) 260 out = strings.Replace(out, "%P", semvers[2], -1) 261 out = strings.Replace(out, "_P", semvers[2], -1) 262 } else { 263 out = strings.Replace(out, "%M", "?", -1) 264 out = strings.Replace(out, "_M", "?", -1) 265 out = strings.Replace(out, "%m", "?", -1) 266 out = strings.Replace(out, "_m", "?", -1) 267 out = strings.Replace(out, "%P", "?", -1) 268 out = strings.Replace(out, "_P", "?", -1) 269 } 270 271 out = strings.Replace(out, "%C", commitCount, -1) 272 out = strings.Replace(out, "_C", commitCount, -1) 273 274 re1 := regexp.MustCompile(`(%S(\d+|))`) 275 re2 := regexp.MustCompile(`(_S(\d+|))`) 276 out = re1.ReplaceAllLiteralString(out, sha) 277 out = re2.ReplaceAllLiteralString(out, sha) 278 279 b, ok := getB(dir) 280 if ok { 281 out = strings.Replace(out, "%B", b, -1) 282 out = strings.Replace(out, "_B", b, -1) 283 } else { 284 out = strings.Replace(out, "%B", "?", -1) 285 out = strings.Replace(out, "_B", "?", -1) 286 } 287 288 return out 289 }