// Package version provides build-time and Perforce-stamped version information // for p4lf. // // This file is stored in Perforce as type 'text+k', which causes Perforce to // expand the $Id: //p4lf/dev/internal/version/version.go#3 $, $Change: 32824 $, and $DateTime: 2026/06/25 13:26:01 $ keywords on each submit/sync. // The expanded strings are compiled directly into the binary, so: // // strings p4lf | grep '\$' # reveals all three stamps without running the binary // p4lf -version # parses and displays them cleanly // // SemVer follows Semantic Versioning (Major.Minor.Patch). Increment it manually // before submitting a notable release point. It is declared as a var so it can // also be overridden at build time via -ldflags for CI/release pipelines: // // go build -ldflags "-X workshop.perforce.com/p4lf/internal/version.SemVer=1.0.0" package version import ( "fmt" "regexp" "strings" ) // SemVer is the hand-maintained semantic version (Major.Minor.Patch). // Increment this manually before submitting a release point. var SemVer = "0.1.0" // P4ID is expanded by Perforce to the depot path and file revision. // Example after submit: "$Id: //p4lf/dev/internal/version/version.go#3 $" const P4ID = "$Id: //p4lf/dev/internal/version/version.go#3 $" // P4Change is expanded by Perforce to the changelist number that last // submitted this file. // Example after submit: "$Change: 32824 $" const P4Change = "$Change: 32824 $" // P4DateTime is expanded by Perforce to the submit date and time. // Example after submit: "$DateTime: 2026/06/25 13:26:01 $" const P4DateTime = "$DateTime: 2026/06/25 13:26:01 $" // idRe, changeRe, and dateTimeRe are built via string concatenation using // dollar ("\x24") to prevent Perforce keyword expansion in the pattern text. // dollar is "$" written in hex so P4 does not interpret it as a keyword prefix. const dollar = "\x24" var idRe = regexp.MustCompile(`\` + dollar + `Id:\s*(//[^\s#]+)(#\d+)\s*\` + dollar) var changeRe = regexp.MustCompile(`\` + dollar + `Change:\s*(\d+)\s*\` + dollar) var dateTimeRe = regexp.MustCompile(`\` + dollar + `DateTime:\s*(\d{4}/\d{2}/\d{2})`) // Info holds parsed version fields. type Info struct { SemVer string // e.g. "0.1.0" Stream string // e.g. "//p4lf/dev" Revision string // e.g. "#3" Change string // e.g. "32819" Date string // e.g. "2026/06/25" } // Get returns the parsed version information. func Get() Info { info := Info{SemVer: SemVer} if m := idRe.FindStringSubmatch(P4ID); m != nil { depotPath := m[1] // e.g. //p4lf/dev/internal/version/version.go info.Revision = m[2] // Extract stream: first two path components (//stream-depot/stream-name). parts := strings.SplitN(strings.TrimPrefix(depotPath, "//"), "/", 3) if len(parts) >= 2 { info.Stream = "//" + parts[0] + "/" + parts[1] } } if m := changeRe.FindStringSubmatch(P4Change); m != nil { info.Change = m[1] } if m := dateTimeRe.FindStringSubmatch(P4DateTime); m != nil { info.Date = m[1] } return info } // String returns the single-line version string used in -version output and // startup log messages. // // With P4 keywords expanded: // // p4lf 0.1.0 (CL 32819, //p4lf/dev#3, 2026/06/25) // // Without expansion (unexpanded keywords, e.g. before first submit): // // p4lf 0.1.0 (P4 keywords not expanded) func String() string { info := Get() if info.Change == "" && info.Stream == "" { return fmt.Sprintf("p4lf %s (P4 keywords not expanded)", info.SemVer) } parts := []string{} if info.Change != "" { parts = append(parts, "CL "+info.Change) } if info.Stream != "" { s := info.Stream if info.Revision != "" { s += info.Revision } parts = append(parts, s) } if info.Date != "" { parts = append(parts, info.Date) } return fmt.Sprintf("p4lf %s (%s)", info.SemVer, strings.Join(parts, ", ")) }