// Package version tells the editor what build of itself it is. // // The number comes from whichever of three sources knows it, in this order: // the linker, the Go build system, and a constant of last resort. Nothing here // reads a file or runs a command, so it costs nothing to ask. package version import ( "runtime/debug" "strings" "time" ) // The build stamps these through the linker: // // go build -ldflags "-X 'codeberg.org/turbo-editors/turbo-core/version.stamp=v0.2.0'" . // // The Makefile and scripts/install.sh both do it, filling stamp from // `git describe --tags --dirty`. They are unexported because nothing but the // linker should ever write them, and package-level strings are what -X can set. var ( stamp string commit string built string ) // unknownNumber is what a build reports when no source could name it. It is // deliberately not a version number: a stale one printed as fact is the defect // this package exists to remove. const unknownNumber = "unknown" // develVersion is what the Go build system calls a binary built from a working // tree when it can read no version at all. const develVersion = "(devel)" // pseudoTailLength is the width of the tail every Go pseudo-version ends with: // a dash, a 14-digit UTC timestamp, a dash, and twelve hex characters. const pseudoTailLength = 1 + 14 + 1 + 12 // shortCommitLength is how much of a revision hash is worth showing. Seven // characters is what git itself abbreviates to, and what a reader will paste // back into `git show`. const shortCommitLength = 7 // Info is what a binary knows about its own build. // // Commit and Built are empty when nothing recorded them, which is normal: a // binary from `go install …@v0.2.0` knows its version and nothing else. A // caller shows the fields it has and says nothing about the rest. type Info struct { // Number is the version, without a leading "v": "0.2.0" for a release, // "0.1.0-14-g88a4c38" for a build between two of them. Number string // Commit is the abbreviated revision, when it is known separately from // Number. Commit string // Built is when the binary was linked, in RFC 3339, when it is known. Built string } // Current returns what this binary knows about itself. // // info := version.Current() // fmt.Println(info.Number) // "0.1.0-14-g88a4c38" func Current() Info { return resolve(stamp, commit, built, readBuildInfo()) } // String renders the whole of what is known on one line, for `-version`. // // One line rather than several because the installer prints it beside a path, // and because a caller that wants the parts has the fields. // // version.Info{Number: "0.2.0", Commit: "88a4c38"}.String() // // "0.2.0 (88a4c38)" func (i Info) String() string { switch { case i.Commit != "" && i.Built != "": return i.Number + " (" + i.Commit + ", built " + i.Built + ")" case i.Commit != "": return i.Number + " (" + i.Commit + ")" case i.Built != "": return i.Number + " (built " + i.Built + ")" } return i.Number } // resolve turns the raw build facts into an Info. // // It is separate from Current so that every combination can be tested without // linker flags: the interesting cases are precisely the ones a test binary // cannot be built into. func resolve(stamp, commit, built string, info *debug.BuildInfo) Info { if stamp != "" { return Info{Number: trimV(stamp), Commit: shorten(commit), Built: built} } if info == nil { return Info{Number: unknownNumber} } return fromBuildInfo(info) } // fromBuildInfo reads what the Go build system recorded. // // Two quite different builds arrive here. `go install @v0.2.0` records // the module version and **no** VCS information at all, because there was no // repository to read. A plain `go build .` in a checkout records the reverse: // the version is "(devel)" and the revision is known. Neither can name a tag — // the build system does not read them — which is why a build that wants to // show one has to be stamped. func fromBuildInfo(info *debug.BuildInfo) Info { settings := settingsOf(info) out := Info{ Number: trimV(info.Main.Version), Commit: shorten(settings["vcs.revision"]), } // A pseudo-version is the Go tool naming a commit that no tag names — // "0.1.1-0.20260831165958-88a4c3859bf3+dirty". It is unreadable in a // dialog, and its "0.1.1" is a patch release that does not exist, so it is // reported for what it means rather than for what it says. if out.Number == "" || out.Number == develVersion || isPseudoVersion(out.Number) { out.Number = develNumber(settings) } // vcs.time is when the *commit* was made, not when this binary was linked, // so it is not a build date and is not reported as one. return out } // develNumber names a build made from a working tree, where no tag is // available: "devel", and "devel-dirty" when the tree had uncommitted changes. func develNumber(settings map[string]string) string { if settings["vcs.modified"] == "true" { return "devel-dirty" } if _, built := settings["vcs.revision"]; built { return "devel" } return unknownNumber } // isPseudoVersion reports whether a module version is one the Go tool invented // for a commit that no tag names. // // Every form of one ends the same way, whatever base version precedes it, and // build metadata such as "+dirty" is not part of that tail. // // isPseudoVersion("0.1.1-0.20260831165958-88a4c3859bf3+dirty") // true // isPseudoVersion("0.2.0") // false func isPseudoVersion(number string) bool { number = withoutBuildMetadata(number) if len(number) <= pseudoTailLength { return false // nothing left for the base version it was derived from } return hasPseudoTail(number[len(number)-pseudoTailLength:]) } // hasPseudoTail reports whether a tail of exactly pseudoTailLength characters // is the "-yyyymmddhhmmss-abcdefabcdef" a pseudo-version ends with. func hasPseudoTail(tail string) bool { if !isAll(tail[1:15], isDigit) || !isAll(tail[16:], isHex) { return false } // The character before the timestamp is a dash when no tag precedes the // commit ("v0.0.0--") and a dot when one does, because the base // then ends in "-0." or "-pre.0.". separatesTimestamp := tail[0] == '-' || tail[0] == '.' return separatesTimestamp && tail[15] == '-' } // withoutBuildMetadata drops the "+dirty" or "+incompatible" a version may // carry, which is not part of the pseudo-version tail. func withoutBuildMetadata(number string) string { if plus := strings.IndexByte(number, '+'); plus >= 0 { return number[:plus] } return number } // isAll reports whether every byte of s satisfies want. func isAll(s string, want func(byte) bool) bool { for i := 0; i < len(s); i++ { if !want(s[i]) { return false } } return true } func isDigit(c byte) bool { return c >= '0' && c <= '9' } func isHex(c byte) bool { return isDigit(c) || (c >= 'a' && c <= 'f') } // settingsOf turns a build's settings into a map, which is how every caller // here wants them. func settingsOf(info *debug.BuildInfo) map[string]string { out := make(map[string]string, len(info.Settings)) for _, setting := range info.Settings { out[setting.Key] = setting.Value } return out } // trimV drops the leading "v" a git tag carries. The tag is `v0.2.0`; what a // person reads in an About box is `0.2.0`. func trimV(number string) string { if len(number) > 1 && number[0] == 'v' && number[1] >= '0' && number[1] <= '9' { return number[1:] } return number } // shorten abbreviates a revision hash the way git does. func shorten(revision string) string { if len(revision) <= shortCommitLength { return revision } return revision[:shortCommitLength] } // BuiltAt renders Built for a person: "2026-08-31 18:04 UTC". // // A stamp that does not parse is shown as it is rather than dropped — a build // whose date is malformed should say so, not appear to have none. // // version.Info{Built: "2026-08-31T18:04:05Z"}.BuiltAt() // "2026-08-31 18:04 UTC" func (i Info) BuiltAt() string { if i.Built == "" { return "" } moment, err := time.Parse(time.RFC3339, i.Built) if err != nil { return i.Built } return moment.UTC().Format("2006-01-02 15:04") + " UTC" } // readBuildInfo is debug.ReadBuildInfo, with the "ok" folded into a nil. func readBuildInfo() *debug.BuildInfo { info, ok := debug.ReadBuildInfo() if !ok { return nil } return info }