version.go #3

  • //
  • p4lf/
  • dev/
  • internal/
  • version/
  • version.go
  • View
  • Commits
  • Open Download .zip Download (4 KB)
// 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