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  }