Name
go-rewrite — Public 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.
- Source:
gomatic/go-rewrite - API reference: pkg.go.dev/github.com/gomatic/go-rewrite
Install
go get github.com/gomatic/go-rewriteWhat 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
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:
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:
// 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
GitandFileSysteminterfaces — no global state, no direct OS access — so every branch is reachable from an in-memory test. - Plan, then apply.
BuildPlanis a pure function from twoIdentityvalues to aPlan; onlyPlan.Applyperforms effects, and aDryRunsuppresses even those. - Ordered, raw substring replacement.
BuildPlanemitsReplacements 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.
OSFileSystemresolves every read, write, and move throughos.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 sharedgo-errormechanism, matched witherrors.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.