go-rewrite(docs) gomatic manual

Name

go-rewritePublic documentation for gomatic/go-rewrite — the rebrand-on-clone engine that rewrites a Go project's identity tokens and moves its command directory.

go-rewrite is the gomatic ecosystem’s rebrand-on-clone engine for Go. It turns a clone of a template repository into a distinct project: it Discovers the project’s current identity (the module path from go.mod, the command name from the tracked cmd/<name> directory) and target identity (the module path from the git origin remote), BuildPlans the token replacements and the one command-directory move that turn the current identity into the target, and applies them with Plan.Apply through an injected FileSystem. The Git and FileSystem seams keep the engine pure and testable; OSGit and OSFileSystem back them with the real repository, the latter confining all file access beneath the project root via os.OpenRoot.

Install

sh
go get github.com/gomatic/go-rewrite

What it rewrites

A project’s identity lives in two places: its content (every file that names the old module path or command) and its structure (the cmd/<name> directory named for the command). go-rewrite handles both. Discover reads the current identity from the working tree and the target identity from the git origin remote; BuildPlan reduces the difference to a Plan of token Replacements plus one directory move; Plan.Apply carries it out and reports which files changed.

The engine is a library, not a CLI — it holds no orchestration or argument-parsing logic. A caller supplies the Git and FileSystem seams (real ones in production, in-memory ones in tests) and decides how to surface the result.

Usage

Discover, plan, and apply

go
package main

import (
	"log"

	rewrite "github.com/gomatic/go-rewrite"
)

func main() {
	git := rewrite.OSGit{Dir: "."}
	fs := rewrite.OSFileSystem{Root: ".", Lister: git.Files}

	// Discover the current identity (from go.mod and cmd/<name>) and the target
	// identity (from the origin remote). Pass an override name instead of "" to
	// force a specific command name.
	current, target, err := rewrite.Discover(git, fs, "")
	if err != nil {
		log.Fatal(err)
	}

	// Build the plan, then apply it. Pass true for a dry run that reports the
	// would-be changes without writing or moving anything.
	changed, err := rewrite.BuildPlan(current, target).Apply(fs, false)
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("rewrote %d files", len(changed))
}

Dry run first

Plan.Apply takes a DryRun flag: when true it computes and returns the Changed set exactly as a real run would, but writes nothing and moves nothing — so you can preview the impact before committing to it:

go
plan := rewrite.BuildPlan(current, target)

// Preview without touching the working tree.
changed, err := plan.Apply(fs, true)
if err != nil {
	log.Fatal(err)
}
for _, file := range changed {
	log.Printf("would rewrite %s", file)
}

// Then apply for real.
if _, err := plan.Apply(fs, false); err != nil {
	log.Fatal(err)
}

The Git and FileSystem seams

The engine depends only on two narrow interfaces, so it never touches the OS directly. Git supplies the origin remote; FileSystem lists, reads, writes, and moves project files. In production, OSGit shells out to git and OSFileSystem backs the seam with the repository; in tests, an in-memory map implements the same interface:

go
// OSGit shells out to git in a working directory: Remote() runs
// "git remote get-url origin", and Files() runs "git ls-files".
git := rewrite.OSGit{Dir: "."}

// OSFileSystem reads, writes, and moves files beneath Root, enumerating the
// project through Lister — typically OSGit.Files, so only tracked files are
// rewritten. Every path operation is confined beneath Root via os.OpenRoot,
// so the adapter can never read or write a sibling of the project.
fs := rewrite.OSFileSystem{Root: ".", Lister: git.Files}

Because the seams are interfaces, a test substitutes its own Git and a map-backed FileSystem and asserts on the returned Changed set without any git repository or disk access.

Design

  • Pure engine, injected seams. The discovery and planning logic depends only on the Git and FileSystem interfaces — no global state, no direct OS access — so every branch is reachable from an in-memory test.
  • Plan, then apply. BuildPlan is a pure function from two Identity values to a Plan; only Plan.Apply performs effects, and a DryRun suppresses even those.
  • Ordered, raw substring replacement. BuildPlan emits Replacements most-specific-first (full module path, then the environment prefix, then the identifier, then the bare name) and each is applied as an unanchored substring — so order is load-bearing and callers must supply distinctive identity tokens (a full module path, a namespaced command name) for matches to be the project’s identity and never coincidence.
  • Root-confined file access. OSFileSystem resolves every read, write, and move through os.OpenRoot (Go 1.24+), so a path that would escape the project root fails rather than resolving outside it. Writes preserve an existing file’s permission bits.
  • Constant sentinel errors. Every failure the package can emit — ErrGitCommand, ErrNotFound, ErrOpenFile, ErrWriteFile, ErrMoveFile — is a constant on the shared go-error mechanism, matched with errors.Is.

Who uses it

go-rewrite is the reusable engine behind the rename command that rebrands a freshly cloned gomatic/template.cli, and it composes the other gomatic/go-* libraries — go-module for module-path parsing and go-error for its sentinels.