templateupdate

package
v0.38.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 8, 2026 License: MIT Imports: 28 Imported by: 0

Documentation

Overview

Package templateupdate provides historical template rendering for template lifecycle operations (update, diff, check). It renders a template at a specific git commit without side effects (no hooks, no metadata files).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CleanOldBackups

func CleanOldBackups(projectDir string, maxAge time.Duration) error

CleanOldBackups removes backup directories older than maxAge.

func ClearConflictStatus

func ClearConflictStatus(projectRoot string) error

ClearConflictStatus removes the conflict status file.

func CollectNewHooks

func CollectNewHooks(changes []HookChange) *types.HooksConfig

CollectNewHooks returns the new hook commands from added/modified hooks, organised into a HooksConfig for execution.

func CreateBackup

func CreateBackup(projectDir string, filePaths []string) (string, error)

CreateBackup copies the specified files from projectDir into .tag/backup/{timestamp}/ for restoration on --abort. Returns the backup directory path.

func CreateBackupFromResults

func CreateBackupFromResults(projectDir string, results []MergeResult, fromCommit, toCommit string) (string, error)

CreateBackupFromResults creates a manifest-aware backup from merge results. Files being modified or deleted are copied to the backup directory; files being added are tracked in the manifest so they can be removed on rollback.

func FindLatestBackup

func FindLatestBackup(projectDir string) (string, error)

FindLatestBackup returns the path of the most recent backup directory, or empty string if none exists.

func FormatCleanSummary

func FormatCleanSummary(w io.Writer, report *ConflictReport)

FormatCleanSummary writes a success summary to the given writer.

func FormatConflictSummary

func FormatConflictSummary(w io.Writer, report *ConflictReport)

FormatConflictSummary writes a human-readable conflict summary to the given writer. This is intended for stderr output.

func FormatDiff

func FormatDiff(results []MergeResult, source, oldSHA, newSHA string, opts FormatOptions)

FormatDiff writes the diff output for the given merge results.

func FormatHookChanges

func FormatHookChanges(changes []HookChange) []string

FormatHookChanges returns a human-readable summary of hook changes.

func FormatVarChanges

func FormatVarChanges(changes []VarChange, userVars map[string]any) []string

FormatVarChanges returns a human-readable summary of variable changes.

func HasExecutableChanges

func HasExecutableChanges(changes []HookChange) bool

HasExecutableChanges returns true if any hook changes would require execution.

func ReadProjectFiles

func ReadProjectFiles(projectDir string) (map[string]*RenderedFile, error)

ReadProjectFiles walks the project directory and returns an in-memory snapshot of all files as a map of forward-slash paths to RenderedFile pointers.

It skips the .tag/ directory, .git/, and the .tagconfig.json file itself. Binary detection uses the same heuristic as template rendering.

func RemoveBackup

func RemoveBackup(backupPath string) error

RemoveBackup removes a backup directory.

func ResolveNewVariables

func ResolveNewVariables(changes []VarChange, vars map[string]any, overrides map[string]string) []string

ResolveNewVariables applies defaults for new optional variables and returns the names of new required variables that still need values.

func RestoreBackup

func RestoreBackup(projectDir, backupPath string) error

RestoreBackup restores files from a backup directory to the project.

func RestoreFromManifest

func RestoreFromManifest(projectDir, backupPath string) error

RestoreFromManifest restores a project using the manifest for precise rollback. If no manifest exists (legacy backup), falls back to RestoreBackup.

func ToPointerMap

func ToPointerMap(m map[string]RenderedFile) map[string]*RenderedFile

ToPointerMap converts a value map to a pointer map for MergeTrees compatibility.

func WriteConflictStatus

func WriteConflictStatus(projectRoot string, status *ConflictStatus) error

WriteConflictStatus atomically writes the conflict status file to <projectRoot>/.tag/conflicts.json.

func WriteManifest

func WriteManifest(backupPath string, manifest *BackupManifest) error

WriteManifest writes the manifest to the backup directory.

Types

type BackupManifest

type BackupManifest struct {
	CreatedAt  time.Time       `json:"created_at"`
	FromCommit string          `json:"from_commit"`
	ToCommit   string          `json:"to_commit"`
	Files      []ManifestEntry `json:"files"`
}

BackupManifest records metadata about a backup for reliable rollback.

func ReadManifest

func ReadManifest(backupPath string) (*BackupManifest, error)

ReadManifest reads the manifest from a backup directory. Returns nil, nil if no manifest exists (legacy backup).

type CheckOptions

type CheckOptions struct {
	ProjectDir string // Project directory (default: ".")
	Ref        string // Override the template ref to check against
}

CheckOptions configures the check operation.

type CheckResult

type CheckResult struct {
	UpToDate   bool   // true if project matches latest template commit
	CurrentSHA string // commit SHA stored in .tagconfig.json
	LatestSHA  string // latest commit SHA from remote
	Source     string // template source string
}

CheckResult contains the outcome of a template freshness check.

type Checker

type Checker struct {
	// contains filtered or unexported fields
}

Checker checks whether a project's template is up to date with upstream.

func NewChecker

func NewChecker(resolver remote.LatestCommitResolver) *Checker

NewChecker creates a Checker with the given commit resolver.

func (*Checker) Check

func (c *Checker) Check(ctx context.Context, opts CheckOptions) (*CheckResult, error)

Check reads the project's .tagconfig.json, resolves the latest upstream commit, and compares them.

type CommitFetcher

type CommitFetcher interface {
	FetchAtCommit(ctx context.Context, url, commitSHA, destDir string) (string, error)
}

CommitFetcher checks out a git repository at a specific commit SHA.

type ConflictReport

type ConflictReport struct {
	// Conflicts lists files that could not be cleanly merged.
	Conflicts []ConflictedFile
	// Clean lists files that were merged or updated without conflicts.
	Clean []MergeResult
	// Prompts lists files that need an explicit user decision.
	Prompts []MergeResult
	// Skipped lists paths excluded by ignore patterns.
	Skipped []string
}

ConflictReport summarises the outcome of a tree merge for display and downstream processing.

func NewConflictReport

func NewConflictReport(results []MergeResult, skipped []string) *ConflictReport

NewConflictReport builds a ConflictReport from merge results and skipped paths.

func (*ConflictReport) HasConflicts

func (r *ConflictReport) HasConflicts() bool

HasConflicts reports whether any files have unresolved conflicts or prompts.

type ConflictStatus

type ConflictStatus struct {
	SchemaVersion   int       `json:"schema_version"`
	UpdateCommit    string    `json:"update_commit"`
	ConflictedFiles []string  `json:"conflicted_files"`
	PromptFiles     []string  `json:"prompt_files,omitempty"`
	ResolvedFiles   []string  `json:"resolved_files"`
	StartedAt       time.Time `json:"started_at"`
}

ConflictStatus is persisted to .tag/conflicts.json when an update leaves unresolved conflicts. It allows resumption with --continue or --abort.

func NewConflictStatus

func NewConflictStatus(report *ConflictReport, updateCommit string) *ConflictStatus

NewConflictStatus creates a ConflictStatus from a ConflictReport.

func ReadConflictStatus

func ReadConflictStatus(projectRoot string) (*ConflictStatus, error)

ReadConflictStatus reads the conflict status from <projectRoot>/.tag/conflicts.json. Returns nil if the file does not exist.

type ConflictedFile

type ConflictedFile struct {
	Path          string
	MarkerCount   int
	BaseContent   []byte
	OursContent   []byte
	TheirsContent []byte
	MergedContent []byte
	Mode          os.FileMode
}

ConflictedFile provides details about a single conflicted file.

type DiffOptions

type DiffOptions struct {
	ProjectDir string // Project directory (default: ".")
	Ref        string // Override template ref
}

DiffOptions configures the diff operation.

type DiffResult

type DiffResult struct {
	OldSHA  string        // commit SHA from .tagconfig.json
	NewSHA  string        // latest commit SHA from remote
	Source  string        // template source string
	Results []MergeResult // merge results from dry-run
	Skipped []string      // paths skipped by ignore rules
}

DiffResult contains the outcome of a template diff operation.

type Differ

type Differ struct {
	// contains filtered or unexported fields
}

Differ performs a dry-run 3-way merge to show what would change on update.

func NewDiffer

func NewDiffer(renderer *HistoricalRenderer, resolver remote.LatestCommitResolver) *Differ

NewDiffer creates a Differ with the given renderer and resolver.

func (*Differ) Diff

func (d *Differ) Diff(ctx context.Context, opts DiffOptions) (*DiffResult, error)

Diff computes the differences between the current project and the latest template version. It performs a full 3-way merge in dry-run mode.

type FileAction

type FileAction string

FileAction describes what happened to a file during an update.

const (
	// FileModified means the file existed and was changed (back up, restore on rollback).
	FileModified FileAction = "modified"
	// FileDeleted means the file was removed by the merge (back up, restore on rollback).
	FileDeleted FileAction = "deleted"
	// FileAdded means the file was newly created by the merge (remove on rollback).
	FileAdded FileAction = "added"
)

type FormatOptions

type FormatOptions struct {
	Color  bool // Enable ANSI color output
	Stat   bool // Show diffstat instead of full diff
	Writer io.Writer
}

FormatOptions controls diff output formatting.

type GitMerger

type GitMerger struct{}

GitMerger implements TextMerger by shelling out to git merge-file.

func (*GitMerger) Merge3

func (g *GitMerger) Merge3(ctx context.Context, path string, base, ours, theirs []byte) ([]byte, bool, error)

Merge3 writes base, ours, and theirs to temporary files and runs git merge-file to produce a 3-way merge. Exit code 0 means clean merge, exit code >0 with output means conflicts, and any other error is fatal.

type HistoricalRenderer

type HistoricalRenderer struct {
	// contains filtered or unexported fields
}

HistoricalRenderer renders a template at a specific git commit using provided variables. It produces an in-memory snapshot of the rendered output without any side effects.

func NewHistoricalRenderer

func NewHistoricalRenderer(fetcher CommitFetcher) *HistoricalRenderer

NewHistoricalRenderer creates a renderer that uses the given fetcher to check out templates at historical commits.

func (*HistoricalRenderer) LoadConfigAtCommit

func (r *HistoricalRenderer) LoadConfigAtCommit(
	ctx context.Context,
	templateURL string,
	commitSHA string,
) (*tmplconfig.TemplateConfig, error)

LoadConfigAtCommit fetches the template at the given commit and returns the parsed TemplateConfig (tag.template.json). The checkout is created in a temporary directory and cleaned up before returning.

func (*HistoricalRenderer) RenderAt

func (r *HistoricalRenderer) RenderAt(
	ctx context.Context,
	templateURL string,
	commitSHA string,
	vars map[string]any,
) (map[string]RenderedFile, error)

RenderAt checks out the template at commitSHA and renders it with vars. It returns a map of relative paths (using forward slashes) to rendered files. The caller-provided vars are used as-is; no variable collection or prompting occurs.

The method creates a temporary directory for the checkout and cleans it up before returning, regardless of success or failure.

func (*HistoricalRenderer) RenderPair

func (r *HistoricalRenderer) RenderPair(
	ctx context.Context,
	templateURL string,
	oldSHA string,
	newSHA string,
	vars map[string]any,
) (base, theirs map[string]*RenderedFile, err error)

RenderPair renders the template at two commits (old and new) and returns pointer maps suitable for MergeTrees. This provides a clean API for future single-clone optimization while currently calling RenderAt twice.

type HookChange

type HookChange struct {
	Phase    string // "pre_scaffold" or "post_scaffold"
	Type     HookChangeType
	OldHooks []string // nil for HookAdded
	NewHooks []string // nil for HookRemoved
}

HookChange describes a single hook difference between template versions.

func DetectHookChanges

func DetectHookChanges(oldConfig, newConfig *tmplconfig.TemplateConfig) []HookChange

DetectHookChanges compares hook definitions between two template configs and returns a sorted list of changes.

type HookChangeType

type HookChangeType int

HookChangeType classifies how a hook changed between template versions.

const (
	// HookAdded means the hook exists in the new config but not the old.
	HookAdded HookChangeType = iota
	// HookRemoved means the hook existed in the old config but not the new.
	HookRemoved
	// HookModified means the hook commands changed between versions.
	HookModified
)

func (HookChangeType) String

func (t HookChangeType) String() string

String returns a human-readable label for a HookChangeType.

type IgnoreMatcher

type IgnoreMatcher struct {
	// contains filtered or unexported fields
}

IgnoreMatcher determines whether a file should be skipped during template updates. It merges patterns from four sources in priority order:

  1. Built-in defaults (lowest priority)
  2. .tagignore file (gitignore-style)
  3. .tagconfig.json skip_patterns
  4. CLI --skip flags (highest priority)

All sources are combined into a single gitignore-style matcher. Because the go-git matcher evaluates patterns in order and the last matching pattern wins, we append sources from lowest to highest priority so that higher-priority sources can override lower ones (including negation patterns).

func NewIgnoreMatcher

func NewIgnoreMatcher(opts IgnoreMatcherOptions) (*IgnoreMatcher, error)

NewIgnoreMatcher creates an IgnoreMatcher by merging patterns from all configured sources.

func (*IgnoreMatcher) ShouldSkip

func (m *IgnoreMatcher) ShouldSkip(relPath string, isDir bool) bool

ShouldSkip reports whether the given relative path should be excluded from template update operations.

type IgnoreMatcherOptions

type IgnoreMatcherOptions struct {
	// ProjectRoot is the path to the project directory. Used to read
	// .tagignore and .tagconfig.json. May be empty if patterns are
	// supplied directly.
	ProjectRoot string

	// TagignorePatterns are patterns parsed from a .tagignore file.
	// If nil and ProjectRoot is set, patterns are loaded from the file.
	TagignorePatterns []string

	// TagconfigPatterns are patterns from .tagconfig.json skip_patterns.
	// If nil and ProjectRoot is set, patterns are loaded from the file.
	TagconfigPatterns []string

	// CLIPatterns are patterns from --skip CLI flags.
	CLIPatterns []string
}

IgnoreMatcherOptions configures the pattern sources for an IgnoreMatcher.

type ManifestEntry

type ManifestEntry struct {
	Path   string     `json:"path"`
	Action FileAction `json:"action"`
}

ManifestEntry records a single file's role in a backup.

func BuildManifestEntries

func BuildManifestEntries(results []MergeResult) []ManifestEntry

BuildManifestEntries categorises merge results into manifest entries.

type MergeEngine

type MergeEngine struct {
	// contains filtered or unexported fields
}

MergeEngine reconciles three file trees: base (old template render), ours (user's current project), and theirs (new template render).

func NewMergeEngine

func NewMergeEngine(tm TextMerger, ignoreFn func(path string) bool) *MergeEngine

NewMergeEngine creates a MergeEngine with the given text merger and optional ignore function. If ignoreFn is nil, no files are skipped.

func (*MergeEngine) MergeFile

func (m *MergeEngine) MergeFile(
	ctx context.Context, path string, base, ours, theirs *RenderedFile,
) (MergeResult, error)

MergeFile merges a single file path across the three trees using the decision matrix defined in the design document.

func (*MergeEngine) MergeTrees

func (m *MergeEngine) MergeTrees(
	ctx context.Context,
	base, ours, theirs map[string]*RenderedFile,
) ([]MergeResult, []string, error)

MergeTrees merges three file trees and returns a sorted list of merge results plus a list of skipped paths.

type MergeOp

type MergeOp int

MergeOp classifies the action to take for a file during a 3-way merge.

const (
	// MergeKeep means the file needs no changes (user's version is current).
	MergeKeep MergeOp = iota
	// MergeAdd means a new file from the template should be added.
	MergeAdd
	// MergeDelete means the file was removed by both sides.
	MergeDelete
	// MergeUpdate means a clean merge was applied automatically.
	MergeUpdate
	// MergeConflict means overlapping changes require user resolution.
	MergeConflict
	// MergeUserAdded means the file exists only in the user's project.
	MergeUserAdded
	// MergePrompt means the situation needs an explicit user decision
	// (e.g. user deleted but template changed, or binary conflict).
	MergePrompt
)

func (MergeOp) String

func (op MergeOp) String() string

String returns a human-readable label for a MergeOp.

type MergeResult

type MergeResult struct {
	// Path is the relative file path within the project.
	Path string
	// Op is the merge operation classification.
	Op MergeOp
	// Content holds the merged content. For conflicts it contains conflict markers.
	Content []byte
	// Mode is the file permission mode for the merged file.
	Mode os.FileMode
	// Conflicted is true when Content contains unresolved conflict markers.
	Conflicted bool
	// IsBinary is true when the file was detected as binary.
	IsBinary bool
	// PromptReason describes why a user prompt is needed (non-empty only for MergePrompt).
	PromptReason string
	// BaseContent is the original base version (populated for conflicts/prompts to enable resolution).
	BaseContent []byte
	// OursContent is the user's version (populated for conflicts/prompts).
	OursContent []byte
	// TheirsContent is the template's version (populated for conflicts/prompts).
	TheirsContent []byte
	// OursSHA256 is the SHA256 hex digest of the user's binary content (for display).
	OursSHA256 string
	// TheirsSHA256 is the SHA256 hex digest of the template's binary content (for display).
	TheirsSHA256 string
}

MergeResult describes the outcome of merging a single file path.

func ResolveConflicts

func ResolveConflicts(report *ConflictReport, mode ResolveMode) []MergeResult

ResolveConflicts applies the given resolution mode to all conflicts and prompts (including binary file conflicts) in the report. Returns a new set of clean MergeResults that replace the conflicted/prompt entries.

type RenderedFile

type RenderedFile struct {
	Content  []byte
	Mode     os.FileMode
	IsBinary bool
}

RenderedFile represents a single file produced by rendering a template.

type ResolveMode

type ResolveMode int

ResolveMode specifies how to auto-resolve conflicts.

const (
	// ResolveNone does not auto-resolve; conflict markers remain.
	ResolveNone ResolveMode = iota
	// ResolveOurs replaces conflicted content with the user's version.
	ResolveOurs
	// ResolveTheirs replaces conflicted content with the template's version.
	ResolveTheirs
)

type TextMerger

type TextMerger interface {
	// Merge3 performs a 3-way merge of base, ours, and theirs content.
	// Returns the merged content, whether conflicts were found, and any error.
	Merge3(ctx context.Context, path string, base, ours, theirs []byte) (merged []byte, conflicted bool, err error)
}

TextMerger performs 3-way text merging of file contents.

type UpdateMode

type UpdateMode int

UpdateMode defines the operational mode for the update command.

const (
	// UpdateModeApply is the default mode: compute and apply changes.
	UpdateModeApply UpdateMode = iota
	// UpdateModeContinue resumes after manual conflict resolution.
	UpdateModeContinue
	// UpdateModeAbort restores from backup and cancels in-progress update.
	UpdateModeAbort
)

type UpdateOptions

type UpdateOptions struct {
	ProjectDir   string
	Ref          string
	VarOverrides map[string]string
	ResolveMode  ResolveMode
	SkipPatterns []string
	DryRun       bool
	Backup       bool
	SkipHooks    bool
	AcceptHooks  bool
	Mode         UpdateMode
}

UpdateOptions configures the update operation.

type UpdateResult

type UpdateResult struct {
	OldSHA       string
	NewSHA       string
	Applied      []MergeResult
	Conflicts    *ConflictReport
	VarChanges   []VarChange
	HookChanges  []HookChange
	NewFiles     int
	UpdatedFiles int
	DeletedFiles int
}

UpdateResult contains the outcome of an update operation.

type Updater

type Updater struct {
	// contains filtered or unexported fields
}

Updater applies upstream template changes to the user's project.

func NewUpdater

func NewUpdater(renderer *HistoricalRenderer, resolver remote.LatestCommitResolver) *Updater

NewUpdater creates an Updater with the given renderer and resolver.

func (*Updater) Update

func (u *Updater) Update(ctx context.Context, opts UpdateOptions) (*UpdateResult, error)

Update performs the template update operation.

type VarChange

type VarChange struct {
	Name   string
	Type   VarChangeType
	OldDef *tmplconfig.VariableDef // nil for VarAdded
	NewDef *tmplconfig.VariableDef // nil for VarRemoved
}

VarChange describes a single variable difference between template versions.

func DetectVarChanges

func DetectVarChanges(oldConfig, newConfig *tmplconfig.TemplateConfig) []VarChange

DetectVarChanges compares variable definitions between two template configs and returns a sorted list of changes. Private variables (starting with _) are included since they may affect rendering.

func (VarChange) NeedsPrompt

func (vc VarChange) NeedsPrompt() bool

NeedsPrompt returns true if the variable change requires user input.

type VarChangeType

type VarChangeType int

VarChangeType classifies how a template variable changed between versions.

const (
	// VarAdded means the variable exists in the new config but not the old.
	VarAdded VarChangeType = iota
	// VarRemoved means the variable existed in the old config but not the new.
	VarRemoved
	// VarDefaultChanged means the variable's default value changed.
	VarDefaultChanged
	// VarTypeChanged means the variable's type changed.
	VarTypeChanged
)

func (VarChangeType) String

func (t VarChangeType) String() string

String returns a human-readable label for a VarChangeType.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL