Files
go2spec/check_version.go
2026-02-28 11:18:38 +00:00

180 lines
5.8 KiB
Go

package main
import (
"fmt"
"log"
"os"
"os/exec"
"regexp"
"slices"
"strconv"
"strings"
"time"
"unicode"
)
var (
// describeRegexp parses the count and revision part of the “git describe --long” output.
describeRegexp = regexp.MustCompile(`-\d+-g([0-9a-f]+)\s*$`)
// semverRegexp checks if a string is a valid Go semver,
// from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
// with leading "v" added.
semverRegexp = regexp.MustCompile(`^v(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`)
// uversionPrereleaseRegexp checks for upstream pre-release
// so that '-' can be replaced with '~' in pkgVersionFromGit.
// To be kept in sync with the regexp portion of uversionmanglePattern in spec.go
uversionPrereleaseRegexp = regexp.MustCompile(`(\d)[_\.\-\+]?(RC|rc|pre|dev|beta|alpha)[.]?(\d*)$`)
)
// pkgVersionFromGit determines the actual version to be packaged
// from the git repository status and user preference.
// Besides returning the upstream version, the "upstream" struct
// struct fields u.version, u.commitIsh, u.hasRelease and u.isRelease
// are also set.
// `preferredRev` should be empty if there are no user preferences.
// TODO: also support other VCS
func pkgVersionFromGit(gitdir string, u *upstream, preferredRev string, forcePrerelease bool) (string, error) {
var latestTag string
var commitsAhead int
var cmd *exec.Cmd // the temporary shell commands we execute
// If the user specifies a valid tag as the preferred revision, that tag should be used without additional heuristics.
if u.rr != nil {
if out, err := u.rr.VCS.Tags(gitdir); err == nil && slices.Contains(out, preferredRev) {
latestTag = preferredRev
}
}
// Find @latest version tag (whether annotated or not) when the user
// (1) does not specify a version tag, or
// (2) specifies an invalid version tag.
if len(latestTag) == 0 {
cmd = exec.Command("git", "describe", "--abbrev=0", "--tags", "--exclude", "*/v*")
cmd.Dir = gitdir
if out, err := cmd.Output(); err == nil {
latestTag = strings.TrimSpace(string(out))
}
}
if len(latestTag) > 0 {
u.hasRelease = true
u.tag = latestTag
log.Printf("Found latest tag %q", latestTag)
if !semverRegexp.MatchString(latestTag) {
log.Printf("WARNING: Latest tag %q is not a valid SemVer version\n", latestTag)
// TODO: Enforce strict sementic versioning with leading "v"?
}
// Count number of commits since @latest version
cmd = exec.Command("git", "rev-list", "--count", latestTag+"..HEAD")
cmd.Dir = gitdir
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("git rev-list: %w", err)
}
commitsAhead, err = strconv.Atoi(strings.TrimSpace(string(out)))
if err != nil {
return "", fmt.Errorf("parse commits ahead: %w", err)
}
if commitsAhead == 0 {
// Equivalent to "git describe --exact-match --tags"
log.Printf("Latest tag %q matches master", latestTag)
} else {
log.Printf("INFO: master is ahead of %q by %v commits", latestTag, commitsAhead)
}
u.commitIsh = latestTag
// Mangle latestTag into upstream_version
// TODO: Move to function and write unit test?
u.version = strings.TrimLeftFunc(
uversionPrereleaseRegexp.ReplaceAllString(latestTag, "$1~$2$3"),
func(r rune) bool {
return !unicode.IsNumber(r)
},
)
if forcePrerelease {
log.Printf("INFO: Force packaging master (prerelease) as requested by user")
// Fallthrough to package @master (prerelease)
} else {
u.isRelease = true
return u.version, nil
}
}
// Packaging @master (prerelease)
mainVer := ""
//if u.hasRelease {
// mainVer = u.version + "+"
//}
// Find committer date, UNIX timestamp
cmd = exec.Command("git", "log", "--pretty=format:%ct", "-n1", "--no-show-signature")
cmd.Dir = gitdir
lastCommitUnixBytes, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("git log: %w", err)
}
lastCommitUnix, err := strconv.ParseInt(strings.TrimSpace(string(lastCommitUnixBytes)), 0, 64)
if err != nil {
return "", fmt.Errorf("parse last commit date: %w", err)
}
dateStr := time.Unix(lastCommitUnix, 0).UTC().Format("20060102")
// Fetch commit hash
cmd = exec.Command("git", "describe", "--long", "--tags")
cmd.Dir = gitdir
lastCommitHash := ""
describeBytes, err := cmd.Output()
if err != nil {
// In case there are no tags at all, we just use the sha of the current commit
cmd = exec.Command("git", "rev-parse", "--short", "HEAD")
cmd.Dir = gitdir
cmd.Stderr = os.Stderr
revparseBytes, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("git rev-parse: %w", err)
}
lastCommitHash = strings.TrimSpace(string(revparseBytes))
u.commitIsh = lastCommitHash
} else {
submatches := describeRegexp.FindSubmatch(describeBytes)
if submatches == nil {
return "", fmt.Errorf("git describe output %q does not match expected format", string(describeBytes))
}
lastCommitHash = string(submatches[1])
u.commitIsh = strings.TrimSpace(string(describeBytes))
}
// Fetch full commit hash
cmd = exec.Command("git", "rev-parse", "HEAD")
cmd.Dir = gitdir
fullCommitHashBytes, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("git rev-parse HEAD: %w", err)
}
fullCommitHash := strings.TrimSpace(string(fullCommitHashBytes))
u.version = fmt.Sprintf("%sgit%s.%s",
mainVer,
time.Unix(lastCommitUnix, 0).UTC().Format("20060102"),
lastCommitHash)
if u.hasRelease {
// have tag: 1.2.3.20250101+git9f107c8
u.version = fmt.Sprintf("%s.%s+git%s", u.version, dateStr, lastCommitHash)
} else {
// without tag: 0.git20250101.96ee002 96ee0021ea0fb9174681b8004d8deba3c499d7f5
u.version = fmt.Sprintf("0+git%s.%s\n%%define commit_id %s", dateStr, lastCommitHash, fullCommitHash)
}
return u.version, nil
}