// 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, ", "))
}
| # | Change | User | Description | Committed | |
|---|---|---|---|---|---|
| #3 | 32824 | C. Thomas Tyler |
Rename Go module path from github.com/rcowham/p4lf to workshop.perforce.com/p4lf. The rcowham prefix was a carryover from early research into go-libtail (which is hosted at github.com/rcowham/go-libtail). p4lf is hosted on the Perforce Public Depot / Workshop, not GitHub, so workshop.perforce.com is the correct canonical module path. Updated: go.mod, all *.go files with internal imports, Makefile (MODULE and LDFLAGS). Session logs retain the old path as accurate historical record. |
||
| #2 | 32820 | C. Thomas Tyler |
Fix P4 keyword expansion in regex pattern strings. Use 'const dollar = "\x24"' (hex for dollar sign) so that the idRe and changeRe patterns are built via string concatenation. This prevents Perforce from expanding the regex patterns themselves when syncing the text+k file, while still expanding the P4ID/P4Change/P4DateTime constants that are intended to carry the stamped values. Verified: 'p4lf -version' shows correct output; 'strings p4lf' shows all three stamps plus unexpanded regex patterns. |
||
| #1 | 32819 | C. Thomas Tyler |
Add version stamping via Perforce keyword expansion. internal/version/version.go (type text+k): - SemVer var: hand-maintained Major.Minor.Patch, injectable via -ldflags - P4ID/P4Change/P4DateTime constants: expanded by Perforce on each submit/sync - Get() parses expanded keywords into an Info struct (stream, CL, date, revision) - String() produces: 'p4lf 0.1.0 (CL 32819, //p4lf/dev#3, 2026/06/25)' - Gracefully handles unexpanded state (pre-first-submit, non-P4 builds) internal/version/version_test.go: - Tests stream/revision parsing for dev, main, release streams - Tests Change and DateTime regex parsing cmd/p4lf/main.go: - Uses version.String() and version.SemVer instead of hardcoded const Makefile: - VERSION now read from internal/version/version.go - LDFLAGS target github.com/rcowham/p4lf/internal/version.SemVer |