internal/git/version.go (132 lines of code) (raw):

package git import ( "bytes" "fmt" "strconv" "strings" ) // minimumVersion is the minimum required Git version. If updating this version, be sure to // also update the following locations: // - https://gitlab.com/gitlab-org/gitaly/blob/master/README.md#installation // - https://gitlab.com/gitlab-org/gitaly/blob/master/.gitlab-ci.yml // - https://docs.gitlab.com/install/installation/#software-requirements // - https://docs.gitlab.com/update/ (see e.g. https://docs.gitlab.com/update/versions/gitlab_17_changes/) var minimumVersion = Version{ versionString: "2.47.0", major: 2, minor: 47, patch: 0, rc: false, // gl is the GitLab patch level. gl: 0, } // Version represents the version of git itself. type Version struct { // versionString is the string representation of the version. The string representation is // not directly derived from the parsed version information because we do not extract all // information from the original version string. Deriving it from parsed information would // thus potentially lose information. versionString string major, minor, patch uint32 rc bool gl uint32 } // NewVersion constructs a new Git version from the given components. func NewVersion(major, minor, patch, gl uint32) Version { return Version{ versionString: fmt.Sprintf("%d.%d.%d.gl%d", major, minor, patch, gl), major: major, minor: minor, patch: patch, gl: gl, } } // NewRCVersion constructs a new Git RC version from the given components. func NewRCVersion(major, minor, patch, gl uint32) Version { return Version{ versionString: fmt.Sprintf("%d.%d.%d.gl%d.RC", major, minor, patch, gl), major: major, minor: minor, patch: patch, gl: gl, rc: true, } } // ParseVersionOutput parses output returned by git-version(1). It is expected to be in the format // "git version 2.39.1.gl1". func ParseVersionOutput(versionOutput []byte) (Version, error) { trimmedVersionOutput := string(bytes.Trim(versionOutput, " \n")) versionString := strings.SplitN(trimmedVersionOutput, " ", 3) if len(versionString) != 3 { return Version{}, fmt.Errorf("invalid version format: %q", string(versionOutput)) } version, err := ParseVersion(versionString[2]) if err != nil { return Version{}, fmt.Errorf("cannot parse git version: %w", err) } return version, nil } // String returns the string representation of the version. func (v Version) String() string { return v.versionString } // IsSupported checks if a version string corresponds to a Git version // supported by Gitaly. func (v Version) IsSupported() bool { return !v.LessThan(minimumVersion) } // IsCatfileObjectTypeFilterSupported checks whether the current Git version supports the `git cat-file --filter=` // option. func (v Version) IsCatfileObjectTypeFilterSupported() bool { return v.GreaterOrEqual(NewVersion(2, 49, 0, 2)) } // LessThan determines whether the version is older than another version. func (v Version) LessThan(other Version) bool { switch { case v.major < other.major: return true case v.major > other.major: return false case v.minor < other.minor: return true case v.minor > other.minor: return false case v.patch < other.patch: return true case v.patch > other.patch: return false case v.rc && !other.rc: return true case !v.rc && other.rc: return false case v.gl < other.gl: return true case v.gl > other.gl: return false default: // this should only be reachable when versions are equal return false } } // Equal determines whether the version is the same as another version. func (v Version) Equal(other Version) bool { return v == other } // GreaterOrEqual determines whether the version is newer than or equal to another version. func (v Version) GreaterOrEqual(other Version) bool { return !v.LessThan(other) } // ParseVersion parses a git version string. func ParseVersion(versionStr string) (Version, error) { versionSplit := strings.SplitN(versionStr, ".", 4) if len(versionSplit) < 3 { return Version{}, fmt.Errorf("expected major.minor.patch in %q", versionStr) } ver := Version{ versionString: versionStr, } for i, v := range []*uint32{&ver.major, &ver.minor, &ver.patch} { var n64 uint64 if versionSplit[i] == "GIT" { // Git falls back to vx.x.GIT if it's unable to describe the current version // or if there's a version file. We should just treat this as "0", even // though it may have additional commits on top. n64 = 0 } else { rcSplit := strings.SplitN(versionSplit[i], "-", 2) var err error n64, err = strconv.ParseUint(rcSplit[0], 10, 32) if err != nil { return Version{}, err } if len(rcSplit) == 2 && strings.HasPrefix(rcSplit[1], "rc") { ver.rc = true } } *v = uint32(n64) } if len(versionSplit) == 4 { if strings.HasPrefix(versionSplit[3], "rc") { ver.rc = true } else if strings.HasPrefix(versionSplit[3], "gl") { gitlabPatchLevel := versionSplit[3][2:] gl, err := strconv.ParseUint(gitlabPatchLevel, 10, 32) if err != nil { return Version{}, err } ver.gl = uint32(gl) } } return ver, nil }