| 🛟 Updated. 28d5985 k33g 20h ago | 1 | // Package version tells the editor what build of itself it is. |
| 2 | // |
| 3 | // The number comes from whichever of three sources knows it, in this order: |
| 4 | // the linker, the Go build system, and a constant of last resort. Nothing here |
| 5 | // reads a file or runs a command, so it costs nothing to ask. |
| 6 | package version |
| 7 | |
| 8 | import ( |
| 9 | "runtime/debug" |
| 10 | "strings" |
| 11 | "time" |
| 12 | ) |
| 13 | |
| 14 | // The build stamps these through the linker: |
| 15 | // |
| 📦 Turbo Core f3ade8d k33g 12h ago | 16 | // go build -ldflags "-X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.2.0'" . |
| 🛟 Updated. 28d5985 k33g 20h ago | 17 | // |
| 18 | // The Makefile and scripts/install.sh both do it, filling stamp from |
| 19 | // `git describe --tags --dirty`. They are unexported because nothing but the |
| 20 | // linker should ever write them, and package-level strings are what -X can set. |
| 21 | var ( |
| 22 | stamp string |
| 23 | commit string |
| 24 | built string |
| 25 | ) |
| 26 | |
| 27 | // unknownNumber is what a build reports when no source could name it. It is |
| 28 | // deliberately not a version number: a stale one printed as fact is the defect |
| 29 | // this package exists to remove. |
| 30 | const unknownNumber = "unknown" |
| 31 | |
| 32 | // develVersion is what the Go build system calls a binary built from a working |
| 33 | // tree when it can read no version at all. |
| 34 | const develVersion = "(devel)" |
| 35 | |
| 36 | // pseudoTailLength is the width of the tail every Go pseudo-version ends with: |
| 37 | // a dash, a 14-digit UTC timestamp, a dash, and twelve hex characters. |
| 38 | const pseudoTailLength = 1 + 14 + 1 + 12 |
| 39 | |
| 40 | // shortCommitLength is how much of a revision hash is worth showing. Seven |
| 41 | // characters is what git itself abbreviates to, and what a reader will paste |
| 42 | // back into `git show`. |
| 43 | const shortCommitLength = 7 |
| 44 | |
| 45 | // Info is what a binary knows about its own build. |
| 46 | // |
| 47 | // Commit and Built are empty when nothing recorded them, which is normal: a |
| 48 | // binary from `go install …@v0.2.0` knows its version and nothing else. A |
| 49 | // caller shows the fields it has and says nothing about the rest. |
| 50 | type Info struct { |
| 51 | // Number is the version, without a leading "v": "0.2.0" for a release, |
| 52 | // "0.1.0-14-g88a4c38" for a build between two of them. |
| 53 | Number string |
| 54 | // Commit is the abbreviated revision, when it is known separately from |
| 55 | // Number. |
| 56 | Commit string |
| 57 | // Built is when the binary was linked, in RFC 3339, when it is known. |
| 58 | Built string |
| 59 | } |
| 60 | |
| 61 | // Current returns what this binary knows about itself. |
| 62 | // |
| 63 | // info := version.Current() |
| 64 | // fmt.Println(info.Number) // "0.1.0-14-g88a4c38" |
| 65 | func Current() Info { |
| 66 | return resolve(stamp, commit, built, readBuildInfo()) |
| 67 | } |
| 68 | |
| 69 | // String renders the whole of what is known on one line, for `-version`. |
| 70 | // |
| 71 | // One line rather than several because the installer prints it beside a path, |
| 72 | // and because a caller that wants the parts has the fields. |
| 73 | // |
| 74 | // version.Info{Number: "0.2.0", Commit: "88a4c38"}.String() |
| 75 | // // "0.2.0 (88a4c38)" |
| 76 | func (i Info) String() string { |
| 77 | switch { |
| 78 | case i.Commit != "" && i.Built != "": |
| 79 | return i.Number + " (" + i.Commit + ", built " + i.Built + ")" |
| 80 | case i.Commit != "": |
| 81 | return i.Number + " (" + i.Commit + ")" |
| 82 | case i.Built != "": |
| 83 | return i.Number + " (built " + i.Built + ")" |
| 84 | } |
| 85 | return i.Number |
| 86 | } |
| 87 | |
| 88 | // resolve turns the raw build facts into an Info. |
| 89 | // |
| 90 | // It is separate from Current so that every combination can be tested without |
| 91 | // linker flags: the interesting cases are precisely the ones a test binary |
| 92 | // cannot be built into. |
| 93 | func resolve(stamp, commit, built string, info *debug.BuildInfo) Info { |
| 94 | if stamp != "" { |
| 95 | return Info{Number: trimV(stamp), Commit: shorten(commit), Built: built} |
| 96 | } |
| 97 | if info == nil { |
| 98 | return Info{Number: unknownNumber} |
| 99 | } |
| 100 | return fromBuildInfo(info) |
| 101 | } |
| 102 | |
| 103 | // fromBuildInfo reads what the Go build system recorded. |
| 104 | // |
| 105 | // Two quite different builds arrive here. `go install <module>@v0.2.0` records |
| 106 | // the module version and **no** VCS information at all, because there was no |
| 107 | // repository to read. A plain `go build .` in a checkout records the reverse: |
| 108 | // the version is "(devel)" and the revision is known. Neither can name a tag — |
| 109 | // the build system does not read them — which is why a build that wants to |
| 110 | // show one has to be stamped. |
| 111 | func fromBuildInfo(info *debug.BuildInfo) Info { |
| 112 | settings := settingsOf(info) |
| 113 | out := Info{ |
| 114 | Number: trimV(info.Main.Version), |
| 115 | Commit: shorten(settings["vcs.revision"]), |
| 116 | } |
| 117 | |
| 118 | // A pseudo-version is the Go tool naming a commit that no tag names — |
| 119 | // "0.1.1-0.20260831165958-88a4c3859bf3+dirty". It is unreadable in a |
| 120 | // dialog, and its "0.1.1" is a patch release that does not exist, so it is |
| 121 | // reported for what it means rather than for what it says. |
| 122 | if out.Number == "" || out.Number == develVersion || isPseudoVersion(out.Number) { |
| 123 | out.Number = develNumber(settings) |
| 124 | } |
| 125 | // vcs.time is when the *commit* was made, not when this binary was linked, |
| 126 | // so it is not a build date and is not reported as one. |
| 127 | return out |
| 128 | } |
| 129 | |
| 130 | // develNumber names a build made from a working tree, where no tag is |
| 131 | // available: "devel", and "devel-dirty" when the tree had uncommitted changes. |
| 132 | func develNumber(settings map[string]string) string { |
| 133 | if settings["vcs.modified"] == "true" { |
| 134 | return "devel-dirty" |
| 135 | } |
| 136 | if _, built := settings["vcs.revision"]; built { |
| 137 | return "devel" |
| 138 | } |
| 139 | return unknownNumber |
| 140 | } |
| 141 | |
| 142 | // isPseudoVersion reports whether a module version is one the Go tool invented |
| 143 | // for a commit that no tag names. |
| 144 | // |
| 145 | // Every form of one ends the same way, whatever base version precedes it, and |
| 146 | // build metadata such as "+dirty" is not part of that tail. |
| 147 | // |
| 148 | // isPseudoVersion("0.1.1-0.20260831165958-88a4c3859bf3+dirty") // true |
| 149 | // isPseudoVersion("0.2.0") // false |
| 150 | func isPseudoVersion(number string) bool { |
| 151 | number = withoutBuildMetadata(number) |
| 152 | if len(number) <= pseudoTailLength { |
| 153 | return false // nothing left for the base version it was derived from |
| 154 | } |
| 155 | return hasPseudoTail(number[len(number)-pseudoTailLength:]) |
| 156 | } |
| 157 | |
| 158 | // hasPseudoTail reports whether a tail of exactly pseudoTailLength characters |
| 159 | // is the "-yyyymmddhhmmss-abcdefabcdef" a pseudo-version ends with. |
| 160 | func hasPseudoTail(tail string) bool { |
| 161 | if !isAll(tail[1:15], isDigit) || !isAll(tail[16:], isHex) { |
| 162 | return false |
| 163 | } |
| 164 | // The character before the timestamp is a dash when no tag precedes the |
| 165 | // commit ("v0.0.0-<ts>-<hash>") and a dot when one does, because the base |
| 166 | // then ends in "-0." or "-pre.0.". |
| 167 | separatesTimestamp := tail[0] == '-' || tail[0] == '.' |
| 168 | return separatesTimestamp && tail[15] == '-' |
| 169 | } |
| 170 | |
| 171 | // withoutBuildMetadata drops the "+dirty" or "+incompatible" a version may |
| 172 | // carry, which is not part of the pseudo-version tail. |
| 173 | func withoutBuildMetadata(number string) string { |
| 174 | if plus := strings.IndexByte(number, '+'); plus >= 0 { |
| 175 | return number[:plus] |
| 176 | } |
| 177 | return number |
| 178 | } |
| 179 | |
| 180 | // isAll reports whether every byte of s satisfies want. |
| 181 | func isAll(s string, want func(byte) bool) bool { |
| 182 | for i := 0; i < len(s); i++ { |
| 183 | if !want(s[i]) { |
| 184 | return false |
| 185 | } |
| 186 | } |
| 187 | return true |
| 188 | } |
| 189 | |
| 190 | func isDigit(c byte) bool { return c >= '0' && c <= '9' } |
| 191 | |
| 192 | func isHex(c byte) bool { return isDigit(c) || (c >= 'a' && c <= 'f') } |
| 193 | |
| 194 | // settingsOf turns a build's settings into a map, which is how every caller |
| 195 | // here wants them. |
| 196 | func settingsOf(info *debug.BuildInfo) map[string]string { |
| 197 | out := make(map[string]string, len(info.Settings)) |
| 198 | for _, setting := range info.Settings { |
| 199 | out[setting.Key] = setting.Value |
| 200 | } |
| 201 | return out |
| 202 | } |
| 203 | |
| 204 | // trimV drops the leading "v" a git tag carries. The tag is `v0.2.0`; what a |
| 205 | // person reads in an About box is `0.2.0`. |
| 206 | func trimV(number string) string { |
| 207 | if len(number) > 1 && number[0] == 'v' && number[1] >= '0' && number[1] <= '9' { |
| 208 | return number[1:] |
| 209 | } |
| 210 | return number |
| 211 | } |
| 212 | |
| 213 | // shorten abbreviates a revision hash the way git does. |
| 214 | func shorten(revision string) string { |
| 215 | if len(revision) <= shortCommitLength { |
| 216 | return revision |
| 217 | } |
| 218 | return revision[:shortCommitLength] |
| 219 | } |
| 220 | |
| 221 | // BuiltAt renders Built for a person: "2026-08-31 18:04 UTC". |
| 222 | // |
| 223 | // A stamp that does not parse is shown as it is rather than dropped — a build |
| 224 | // whose date is malformed should say so, not appear to have none. |
| 225 | // |
| 226 | // version.Info{Built: "2026-08-31T18:04:05Z"}.BuiltAt() // "2026-08-31 18:04 UTC" |
| 227 | func (i Info) BuiltAt() string { |
| 228 | if i.Built == "" { |
| 229 | return "" |
| 230 | } |
| 231 | moment, err := time.Parse(time.RFC3339, i.Built) |
| 232 | if err != nil { |
| 233 | return i.Built |
| 234 | } |
| 235 | return moment.UTC().Format("2006-01-02 15:04") + " UTC" |
| 236 | } |
| 237 | |
| 238 | // readBuildInfo is debug.ReadBuildInfo, with the "ok" folded into a nil. |
| 239 | func readBuildInfo() *debug.BuildInfo { |
| 240 | info, ok := debug.ReadBuildInfo() |
| 241 | if !ok { |
| 242 | return nil |
| 243 | } |
| 244 | return info |
| 245 | } |