Replace bash release helper scripts with Go progam

This commit is contained in:
jregan
2020-10-08 10:45:12 -07:00
parent 4052cd4fd8
commit 0c169e96e5
31 changed files with 2130 additions and 176 deletions

View File

@@ -48,8 +48,8 @@ $(MYGOBIN)/golangci-lint-kustomize:
)
$(MYGOBIN)/gorepomod:
cd api; \
go install github.com/monopole/gorepomod
cd cmd/gorepomod; \
go install .
$(MYGOBIN)/mdrip:
cd api; \

15
cmd/gorepomod/Makefile Normal file
View File

@@ -0,0 +1,15 @@
MYGOBIN := $(shell go env GOPATH)/bin
$(MYGOBIN)/gorepomod: usage.go
go install .
.PHONY: test
test: $(MYGOBIN)/gorepomod
go test ./...
usage.go: README.md $(MYGOBIN)/goimports
go generate . \
$(MYGOBIN)/goimports -w usage.go
$(MYGOBIN)/goimports:
go install golang.org/x/tools/cmd/goimports

103
cmd/gorepomod/README.md Normal file
View File

@@ -0,0 +1,103 @@
# gorepomod
Helps when you have a git repository with multiple Go modules.
It handles tasks one might otherwise attempt with
```
find ./ -name "go.mod" | xargs {some hack}
```
Run it from a git repository root.
It walks the repository, reads `go.mod` files, builds
a model of Go modules and intra-repo module
dependencies, then performs some operation.
Install:
```
go get github.com/monopole/gorepomod
```
## Usage
_Commands that change things (everything but `list`)
do nothing but log commands
unless you add the `--doIt` flag,
allowing the change._
#### `gorepomod list`
Lists modules and intra-repo dependencies.
Use this to get module names for use in other commands.
#### `gorepomod tidy`
Creates a change with mechanical updates
to `go.mod` and `go.sum` files.
#### `gorepomod unpin {module}`
Creates a change to `go.mod` files.
For each module _m_ in the repository,
if _m_ depends on a _{module}_,
then _m_'s dependency on it will be replaced by
a relative path to the in-repo module.
#### `gorepomod pin {module} [{version}]`
Creates a change to `go.mod` files.
The opposite of `unpin`.
The change removes replacements and pins _m_ to a
specific, previously tagged and released version of _{module}_.
The argument _{version}_ defaults to recent version of _{module}_.
_{version}_ should be in semver form, e.g. `v1.2.3`.
#### `gorepomod release {module} [patch|minor|major]`
Computes a new version for the module, tags the repo
with that version, and pushes the tag to the remote.
The value of the 2nd argument, either `patch` (the default),
`minor` or `major`, determines the new version.
If the existing version is _v1.2.7_, then the new version will be:
- `patch` -> _v1.2.8_
- `minor` -> _v1.3.0_
- `major` -> _v2.0.0_
After establishing the the version, the command looks for a branch named
> _release-{module}/-v{major}.{minor}_
If the branch doesn't exist, the command creates it and pushes it to the remote.
The command then creates a new tag in the form
> _{module}/v{major}.{minor}.{patch}_
The command pushes this tag to the remote. This typically triggers
cloud activity to create release artifacts.
#### `gorepomod unrelease {module}`
This undoes the work of `release`, by deleting the
most recent tag both locally and at the remote.
You can then fix whatever, and re-release.
This, however, must be done almost immediately.
If there's a chance someone (or some cloud robot) already
imported the module at the given tag, then don't do this,
because it will confuse module caches.
Do a new patch release instead.

5
cmd/gorepomod/go.mod Normal file
View File

@@ -0,0 +1,5 @@
module sigs.k8s.io/kustomize/cmd/gorepomod
go 1.15
require golang.org/x/mod v0.3.0

14
cmd/gorepomod/go.sum Normal file
View File

@@ -0,0 +1,14 @@
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -0,0 +1,195 @@
package arguments
import (
"fmt"
"os"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/misc"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/semver"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/utils"
)
const (
doItFlag = "--doIt"
cmdPin = "pin"
cmdUnPin = "unpin"
cmdTidy = "tidy"
cmdList = "list"
cmdRelease = "release"
cmdUnRelease = "unrelease"
cmdDebug = "debug"
)
var (
commands = []string{
cmdPin, cmdUnPin, cmdTidy, cmdList, cmdRelease, cmdUnRelease, cmdDebug}
// TODO: make this a PATH-like flag
// e.g.: --excludes ".git:.idea:site:docs"
excSlice = []string{
".git",
".github",
".idea",
"docs",
"examples",
"hack",
"site",
"releasing",
}
)
type Command int
const (
Tidy Command = iota
UnPin
Pin
List
Release
UnRelease
Debug
)
type Args struct {
cmd Command
moduleName misc.ModuleShortName
version semver.SemVer
bump semver.SvBump
doIt bool
}
func (a *Args) GetCommand() Command {
return a.cmd
}
func (a *Args) Bump() semver.SvBump {
return a.bump
}
func (a *Args) Version() semver.SemVer {
return a.version
}
func (a *Args) ModuleName() misc.ModuleShortName {
return a.moduleName
}
func (a *Args) Exclusions() (result []string) {
// Make sure the list has no repeats.
for k := range utils.SliceToSet(excSlice) {
result = append(result, k)
}
return
}
func (a *Args) DoIt() bool {
return a.doIt
}
type myArgs struct {
args []string
doIt bool
}
func (a *myArgs) next() (result string) {
if !a.more() {
panic("no args left")
}
result = a.args[0]
a.args = a.args[1:]
return
}
func (a *myArgs) more() bool {
return len(a.args) > 0
}
func newArgs() *myArgs {
result := &myArgs{}
for _, a := range os.Args[1:] {
if a == doItFlag {
result.doIt = true
} else {
result.args = append(result.args, a)
}
}
return result
}
func Parse() (result *Args, err error) {
result = &Args{}
clArgs := newArgs()
result.doIt = clArgs.doIt
result.moduleName = misc.ModuleUnknown
if !clArgs.more() {
return nil, fmt.Errorf("command needs at least one arg")
}
command := clArgs.next()
switch command {
case cmdPin:
if !clArgs.more() {
return nil, fmt.Errorf("pin needs a moduleName to pin")
}
result.moduleName = misc.ModuleShortName(clArgs.next())
if clArgs.more() {
result.version, err = semver.Parse(clArgs.next())
if err != nil {
return nil, err
}
} else {
result.version = semver.Zero()
}
result.cmd = Pin
case cmdUnPin:
if !clArgs.more() {
return nil, fmt.Errorf("unpin needs a moduleName to unpin")
}
result.moduleName = misc.ModuleShortName(clArgs.next())
result.cmd = UnPin
case cmdTidy:
result.cmd = Tidy
case cmdList:
result.cmd = List
case cmdRelease:
if !clArgs.more() {
return nil, fmt.Errorf("specify {module} to release")
}
result.moduleName = misc.ModuleShortName(clArgs.next())
bump := "patch"
if clArgs.more() {
bump = clArgs.next()
}
switch bump {
case "major":
result.bump = semver.Major
case "minor":
result.bump = semver.Minor
case "patch":
result.bump = semver.Patch
default:
return nil, fmt.Errorf(
"unknown bump %s; specify one of 'major', 'minor' or 'patch'", bump)
}
result.cmd = Release
case cmdUnRelease:
if !clArgs.more() {
return nil, fmt.Errorf("specify {module} to unrelease")
}
result.moduleName = misc.ModuleShortName(clArgs.next())
result.cmd = UnRelease
case cmdDebug:
if !clArgs.more() {
return nil, fmt.Errorf("specify {module} to debug")
}
result.moduleName = misc.ModuleShortName(clArgs.next())
result.cmd = Debug
default:
return nil, fmt.Errorf(
"unknown command %q; must be one of %v", command, commands)
}
if clArgs.more() {
return nil, fmt.Errorf("unknown extra args: %v", clArgs.args)
}
return
}

View File

@@ -0,0 +1,82 @@
package edit
import (
"fmt"
"os/exec"
"strings"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/misc"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/semver"
)
// Editor runs `go mod` commands on an instance of Module.
// If doIt is false, the command is printed, but not run.
type Editor struct {
module misc.LaModule
doIt bool
}
func New(m misc.LaModule, doIt bool) *Editor {
return &Editor{
doIt: doIt,
module: m,
}
}
func (e *Editor) run(args ...string) error {
c := exec.Command(
"go",
append([]string{"mod"}, args...)...)
c.Dir = string(e.module.ShortName())
if e.doIt {
out, err := c.CombinedOutput()
if err != nil {
return fmt.Errorf("%s out=%q", err.Error(), out)
}
} else {
fmt.Printf("in %-60s; %s\n", c.Dir, c.String())
}
return nil
}
func upstairs(depth int) string {
var b strings.Builder
for i := 0; i < depth; i++ {
b.WriteString("../")
}
return b.String()
}
func (e *Editor) Tidy() error {
return e.run("tidy")
}
func (e *Editor) Pin(target misc.LaModule, oldV, newV semver.SemVer) error {
err := e.run(
"edit",
"-dropreplace="+target.ImportPath()+"@"+oldV.String(),
"-require="+target.ImportPath()+"@"+newV.String(),
)
if err != nil {
return err
}
return e.run("tidy")
}
func (e *Editor) UnPin(target misc.LaModule, oldV semver.SemVer) error {
var r strings.Builder
r.WriteString(target.ImportPath())
r.WriteString("@")
r.WriteString(oldV.String())
r.WriteString("=")
r.WriteString(upstairs(e.module.ShortName().Depth()))
r.WriteString(string(target.ShortName()))
err := e.run(
"edit",
"-replace="+r.String(),
)
if err != nil {
return err
}
return e.run("tidy")
}

View File

@@ -0,0 +1,32 @@
package edit
import (
"testing"
)
func TestUpstairs(t *testing.T) {
var testCases = map[string]struct {
depth int
expected string
}{
"zero": {
depth: 0,
expected: "",
},
"one": {
depth: 1,
expected: "../",
},
"five": {
depth: 5,
expected: "../../../../../",
},
}
for n, tc := range testCases {
if tc.expected != upstairs(tc.depth) {
t.Fatalf(
"%s: for depth %d, expected %q, got %q",
n, tc.depth, tc.expected, upstairs(tc.depth))
}
}
}

View File

@@ -0,0 +1,75 @@
package main
import (
"bufio"
"log"
"os"
"strings"
)
const (
inputFile = "README.md"
outputFile = "usage.go"
)
// Convert README.md to a usage function.
func main() {
inFile, err := os.Open(inputFile)
if err != nil {
log.Fatal(err)
}
defer inFile.Close()
scanner := bufio.NewScanner(inFile)
w := NewWriter(outputFile)
w.prLn("// Code generated by internal/gen/main.go; DO NOT EDIT.")
w.prLn("package main")
w.prLn("")
w.prLn("")
w.prLn("const (")
w.prLn(" usageMsg = `")
// Skip the first two lines.
scanner.Scan()
scanner.Scan()
for scanner.Scan() {
line := scanner.Text()
line = strings.Replace(line, "`", "'", -1)
line = strings.Replace(line, "\\[", "[", -1)
line = strings.Replace(line, "\\]", "]", -1)
w.prLn(line)
}
w.prLn("`")
w.prLn(")")
w.close()
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
}
type writer struct {
f *os.File
}
func NewWriter(n string) *writer {
f, err := os.Create(n)
if err != nil {
log.Fatalf("unable to create `%s`; %v", n, err)
}
return &writer{f: f}
}
func (w *writer) prLn(line string) {
_, err := w.f.WriteString(line + "\n")
if err != nil {
log.Printf("Trouble writing: %s", line)
log.Fatal(err)
}
}
func (w *writer) close() {
if err := w.f.Close(); err != nil {
log.Fatal(err)
}
}

View File

@@ -0,0 +1,340 @@
package git
import (
"fmt"
"os/exec"
"sort"
"strings"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/misc"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/semver"
)
const (
refsTags = "refs/tags/"
pathSep = "/"
remoteOrigin = misc.TrackedRepo("origin")
remoteUpstream = misc.TrackedRepo("upstream")
mainBranch = "master"
indent = " "
doing = " [x] "
faking = " [ ] "
)
type safetyLevel int
const (
// Commands that don't hurt, e.g. checking out an existing branch.
noHarmDone safetyLevel = iota
// Commands that write, and could be hard to undo.
undoPainful
)
type Verbosity int
const (
Low Verbosity = iota
High
)
var recognizedRemotes = []misc.TrackedRepo{remoteUpstream, remoteOrigin}
// Runner runs specific git tasks using the git CLI.
type Runner struct {
// From which directory do we run the commands.
workDir string
// Run commands, or merely print commands.
doIt bool
// Run commands, or merely print commands.
verbosity Verbosity
}
func NewLoud(wd string, doIt bool) *Runner {
return newRunner(wd, doIt, High)
}
func NewQuiet(wd string, doIt bool) *Runner {
return newRunner(wd, doIt, Low)
}
func newRunner(wd string, doIt bool, v Verbosity) *Runner {
return &Runner{workDir: wd, doIt: doIt, verbosity: v}
}
func (gr *Runner) comment(f string) {
if gr.verbosity == Low {
return
}
fmt.Print(indent)
fmt.Println(f)
}
func (gr *Runner) doing(s string) {
if gr.verbosity == Low {
return
}
fmt.Print(indent)
fmt.Print(doing)
fmt.Println(s)
}
func (gr *Runner) faking(s string) {
if gr.verbosity == Low {
return
}
fmt.Print(indent)
fmt.Print(faking)
fmt.Println(s)
}
func (gr *Runner) run(sl safetyLevel, args ...string) (string, error) {
c := exec.Command("git", args...)
c.Dir = gr.workDir
if gr.doIt || sl == noHarmDone {
gr.doing(c.String())
out, err := c.CombinedOutput()
if err != nil {
return "", fmt.Errorf(
"%s out=%q", err.Error(), strings.TrimSpace(string(out)))
}
return string(out), nil
}
gr.faking(c.String())
return "", nil
}
func (gr *Runner) runNoOut(s safetyLevel, args ...string) error {
_, err := gr.run(s, args...)
return err
}
// TODO: allow for other remote names.
func (gr *Runner) DetermineRemoteToUse() (misc.TrackedRepo, error) {
gr.comment("determining remote to use")
out, err := gr.run(noHarmDone, "remote")
if err != nil {
return "", err
}
remotes := strings.Split(out, "\n")
if len(remotes) < 1 {
return "", fmt.Errorf("need at least one remote")
}
for _, n := range recognizedRemotes {
if contains(remotes, n) {
return n, nil
}
}
return "", fmt.Errorf(
"unable to find recognized remote %v", recognizedRemotes)
}
func contains(list []string, item misc.TrackedRepo) bool {
for _, n := range list {
if n == string(item) {
return true
}
}
return false
}
func (gr *Runner) LoadLocalTags() (result misc.VersionMap, err error) {
gr.comment("loading local tags")
var out string
out, err = gr.run(noHarmDone, "tag", "-l")
if err != nil {
return nil, err
}
result = make(misc.VersionMap)
lines := strings.Split(out, "\n")
for _, l := range lines {
n, v, err := parseModuleSpec(l)
if err != nil {
// ignore it
continue
}
result[n] = append(result[n], v)
}
for _, versions := range result {
sort.Sort(versions)
}
return
}
func (gr *Runner) LoadRemoteTags(
remote misc.TrackedRepo) (result misc.VersionMap, err error) {
gr.comment("loading remote tags")
var out string
out, err = gr.run(noHarmDone, "ls-remote", "--ref", string(remote))
if err != nil {
return nil, err
}
result = make(misc.VersionMap)
lines := strings.Split(out, "\n")
for _, l := range lines {
fields := strings.Fields(l)
if len(fields) < 2 {
// ignore it
continue
}
if !strings.HasPrefix(fields[1], refsTags) {
// ignore it
continue
}
path := fields[1][len(refsTags):]
n, v, err := parseModuleSpec(path)
if err != nil {
// ignore it
continue
}
result[n] = append(result[n], v)
}
for _, versions := range result {
sort.Sort(versions)
}
return
}
func parseModuleSpec(
path string) (n misc.ModuleShortName, v semver.SemVer, err error) {
fields := strings.Split(path, pathSep)
v, err = semver.Parse(fields[len(fields)-1])
if err != nil {
// Silently ignore versions we don't understand.
return "", v, err
}
n = misc.ModuleAtTop
if len(fields) > 1 {
n = misc.ModuleShortName(
strings.Join(fields[:len(fields)-1], pathSep))
}
return
}
func (gr *Runner) Debug(remote misc.TrackedRepo) error {
return nil // gr.CheckoutMainBranch(remote)
}
func (gr *Runner) AssureCleanWorkspace() error {
gr.comment("assuring a clean workspace")
out, err := gr.run(noHarmDone, "status")
if err != nil {
return err
}
if !strings.Contains(out, "nothing to commit, working tree clean") {
return fmt.Errorf("the workspace isn't clean")
}
return nil
}
func (gr *Runner) AssureOnMainBranch() error {
gr.comment("assuring main branch checked out")
out, err := gr.run(noHarmDone, "status")
if err != nil {
return err
}
if !strings.Contains(out, "On branch "+mainBranch) {
return fmt.Errorf("expected to be on branch %q", mainBranch)
}
return nil
}
// CheckoutMainBranch does that.
func (gr *Runner) CheckoutMainBranch() error {
gr.comment("checking out main branch")
return gr.runNoOut(noHarmDone, "checkout", mainBranch)
}
// FetchRemote does that.
func (gr *Runner) FetchRemote(remote misc.TrackedRepo) error {
gr.comment("fetching remote")
return gr.runNoOut(noHarmDone, "fetch", string(remote))
}
// MergeFromRemoteMain does a fast forward only merge with main branch.
func (gr *Runner) MergeFromRemoteMain(remote misc.TrackedRepo) error {
remo := strings.Join(
[]string{string(remote), mainBranch}, pathSep)
gr.comment("merging from remote")
return gr.runNoOut(undoPainful, "merge", "--ff-only", remo)
}
// CheckoutReleaseBranch attempts to checkout or create a branch.
// If it's on the remote already, fail if we cannot check it out locally.
func (gr *Runner) CheckoutReleaseBranch(
remote misc.TrackedRepo, branch string) error {
yes, err := gr.doesRemoteBranchExist(remote, branch)
if err != nil {
return err
}
if yes {
gr.comment("checking out branch")
if out, err := gr.run(noHarmDone, "checkout", branch); err != nil {
fmt.Printf("error with checkout: %q", err.Error())
fmt.Printf("out: %q", out)
return fmt.Errorf(
"branch %q exists on remote %q, but isn't present locally",
branch, string(remote))
}
return nil
}
gr.comment("creating branch")
// The branch doesn't exist. Create it.
out, err := gr.run(noHarmDone, "checkout", "-b", branch)
if err != nil {
return err
}
if !strings.Contains(out, "Switched to a new branch ") {
return fmt.Errorf("unexpected branch creation output: %q", out)
}
return nil
}
func (gr *Runner) doesRemoteBranchExist(
remote misc.TrackedRepo, branch string) (bool, error) {
gr.comment("looking for branch on remote")
out, err := gr.run(noHarmDone, "branch", "-r")
if err != nil {
return false, err
}
lookFor := strings.Join([]string{string(remote), branch}, pathSep)
lines := strings.Split(out, "\n")
for _, l := range lines {
if strings.TrimSpace(l) == lookFor {
return true, nil
}
}
return false, nil
}
func (gr *Runner) PushBranchToRemote(
remote misc.TrackedRepo, branch string) error {
gr.comment("pushing branch to remote")
return gr.runNoOut(undoPainful, "push", "-f", string(remote), branch)
}
func (gr *Runner) CreateLocalReleaseTag(tag, branch string) error {
msg := fmt.Sprintf("\"Release %s on branch %s\"", tag, branch)
gr.comment("creating local release tag")
return gr.runNoOut(
undoPainful,
"tag", "-a",
"-m", msg,
tag)
}
func (gr *Runner) DeleteLocalTag(tag string) error {
gr.comment("deleting local tag")
return gr.runNoOut(undoPainful, "tag", "--delete", tag)
}
func (gr *Runner) PushTagToRemote(
remote misc.TrackedRepo, tag string) error {
gr.comment("pushing tag to remote")
return gr.runNoOut(undoPainful, "push", string(remote), tag)
}
func (gr *Runner) DeleteTagFromRemote(
remote misc.TrackedRepo, tag string) error {
gr.comment("deleting tags from remote")
return gr.runNoOut(undoPainful, "push", string(remote), ":"+refsTags+tag)
}

View File

@@ -0,0 +1,126 @@
package misc
import (
"fmt"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/semver"
)
// ModFunc is a function accepting a module, and returning an error.
type ModFunc func(LaModule) error
type LaRepository interface {
// RepoPath is the import of the of repository,
// e.g. github.com/kubernetes-sigs/kustomize
// The directory {srcRoot}/{importPath} should contain a
// dotGit directory.
// This directory might be a Go module, or contain directories
// that are Go modules, or both.
RepoPath() string
// AbsPath is the full local filesystem path.
AbsPath() string
// FindModule returns a module or nil.
FindModule(ModuleShortName) LaModule
}
type LaModule interface {
// ShortName is the module's name without the repo.
ShortName() ModuleShortName
// ImportPath is the relative path below the Go src root,
// which is the same path as would be used to
// import the module.
ImportPath() string
// AbsPath is the absolute path to the module's
// go.mod file on the local file system.
AbsPath() string
// Latest version tagged locally.
VersionLocal() semver.SemVer
// Latest version tagged remotely.
VersionRemote() semver.SemVer
// Does this module depend on the argument, and
// if so at what version?
DependsOn(LaModule) (bool, semver.SemVer)
// GetReplacements returns a list of replacements.
GetReplacements() []string
}
// VersionMap holds the versions associated with modules.
type VersionMap map[ModuleShortName]semver.Versions
func (m VersionMap) Report() {
for n, versions := range m {
fmt.Println(n)
for _, v := range versions {
fmt.Print(" ")
fmt.Println(v)
}
}
}
func (m VersionMap) Latest(
n ModuleShortName) semver.SemVer {
versions := m[n]
if versions == nil {
return semver.Zero()
}
return versions[0]
}
type LesModules []LaModule
func (s LesModules) LenLongestName() (ans int) {
for _, m := range s {
l := len(m.ShortName())
if l > ans {
ans = l
}
}
return
}
func (s LesModules) Apply(f ModFunc) error {
for _, m := range s {
err := f(m)
if err != nil {
return err
}
}
return nil
}
func (s LesModules) Find(target ModuleShortName) LaModule {
for _, m := range s {
if m.ShortName() == target {
return m
}
}
return nil
}
func (s LesModules) GetAllThatDependOn(
target LaModule) (result TaggedModules) {
for _, m := range s {
if yes, v := m.DependsOn(target); yes {
result = append(result, TaggedModule{M: m, V: v})
}
}
return
}
func (s LesModules) InternalDeps(
target LaModule) (result TaggedModules) {
for _, m := range s {
if yes, v := target.DependsOn(m); yes {
result = append(result, TaggedModule{M: m, V: v})
}
}
return
}

View File

@@ -0,0 +1,23 @@
package misc
import (
"path/filepath"
"strings"
)
// ModuleShortName is the in-repo path to the directory holding the module
// (holding the go.mod file). It's the unique in-repo name of the module.
// It's the name used to tag the repo at a particular module version.
// E.g. "" (empty), "kyaml", "cmd/config", "plugin/example/whatever".
type ModuleShortName string
// Never used in a tag.
const ModuleAtTop = ModuleShortName("{top}")
const ModuleUnknown = ModuleShortName("{unknown}")
func (m ModuleShortName) Depth() int {
if m == ModuleAtTop || m == ModuleUnknown {
return 0
}
return strings.Count(string(m), string(filepath.Separator)) + 1
}

View File

@@ -0,0 +1,36 @@
package misc_test
import (
"testing"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/misc"
)
func TestDepth(t *testing.T) {
var testCases = map[string]struct {
path string
expectedDepth int
}{
"zero": {
path: "{top}",
expectedDepth: 0,
},
"one": {
path: "one",
expectedDepth: 1,
},
"three": {
path: "one/two/three",
expectedDepth: 3,
},
}
for n, tc := range testCases {
m := misc.ModuleShortName(tc.path)
d := m.Depth()
if d != tc.expectedDepth {
t.Fatalf(
"%s: %s, expected %d, got %d",
n, tc.path, tc.expectedDepth, d)
}
}
}

View File

@@ -0,0 +1,42 @@
package misc
import (
"fmt"
"strings"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/semver"
)
// TaggedModule is a module known to be tagged with the given version.
type TaggedModule struct {
M LaModule
V semver.SemVer
}
func (p TaggedModule) String() string {
if p.V.IsZero() {
return string(p.M.ShortName())
}
return string(p.M.ShortName()) + "/" + p.V.String()
}
type TaggedModules []TaggedModule
func (s TaggedModules) String() string {
// format := "%-"+strconv.Itoa(s.LenLongestString()+2)+"s"
var b strings.Builder
for i := range s {
b.WriteString(fmt.Sprintf("%-15s", s[i]))
}
return b.String()
}
func (s TaggedModules) LenLongestString() (ans int) {
for _, m := range s {
l := len(m.String())
if l > ans {
ans = l
}
}
return
}

View File

@@ -0,0 +1,4 @@
package misc
// TrackedRepo identifies a git remote repository.
type TrackedRepo string

View File

@@ -0,0 +1,79 @@
package mod
import (
"fmt"
"path/filepath"
"golang.org/x/mod/modfile"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/misc"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/semver"
)
// Module is an immutable representation of a Go module.
type Module struct {
repo misc.LaRepository
shortName misc.ModuleShortName
mf *modfile.File
vLocal semver.SemVer
vRemote semver.SemVer
}
func New(
repo misc.LaRepository,
shortName misc.ModuleShortName,
mf *modfile.File,
vl semver.SemVer,
vr semver.SemVer) *Module {
return &Module{
repo: repo,
shortName: shortName,
mf: mf,
vLocal: vl,
vRemote: vr,
}
}
func (m *Module) GitRepo() misc.LaRepository {
return m.repo
}
func (m *Module) VersionLocal() semver.SemVer {
return m.vLocal
}
func (m *Module) VersionRemote() semver.SemVer {
return m.vRemote
}
func (m *Module) ShortName() misc.ModuleShortName {
return m.shortName
}
func (m *Module) ImportPath() string {
return filepath.Join(m.repo.RepoPath(), string(m.ShortName()))
}
func (m *Module) AbsPath() string {
return filepath.Join(m.repo.AbsPath(), string(m.ShortName()))
}
func (m *Module) DependsOn(target misc.LaModule) (bool, semver.SemVer) {
for _, r := range m.mf.Require {
if r.Mod.Path == target.ImportPath() {
v, err := semver.Parse(r.Mod.Version)
if err != nil {
panic(err)
}
return true, v
}
}
return false, semver.Zero()
}
func (m *Module) GetReplacements() (result []string) {
for _, r := range m.mf.Replace {
result = append(
result, fmt.Sprintf("%s => %s", r.Old.String(), r.New.String()))
}
return
}

View File

@@ -0,0 +1 @@
package mod_test

View File

@@ -0,0 +1,134 @@
package repo
import (
"fmt"
"path/filepath"
"strings"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/git"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/misc"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/utils"
)
const (
dotGitFileName = ".git"
srcHint = "/src/"
goModFile = "go.mod"
)
// DotGitData holds basic information about a local .git file
type DotGitData struct {
// srcPath is the absolute path to the local Go src directory.
// This used to be $GOPATH/src.
// It's the directory containing git repository clones.
srcPath string
// The path below srcPath to a particular repository
// directory, a directory containing a .git directory.
// Typically {repoOrg}/{repoUserName}, e.g. sigs.k8s.io/cli-utils
repoPath string
}
func (dg *DotGitData) SrcPath() string {
return dg.srcPath
}
func (dg *DotGitData) RepoPath() string {
return dg.repoPath
}
func (dg *DotGitData) AbsPath() string {
return filepath.Join(dg.srcPath, dg.repoPath)
}
// NewDotGitDataFromPath wants the incoming path to hold dotGit
// E.g.
// ~/gopath/src/sigs.k8s.io/kustomize
// ~/gopath/src/github.com/monopole/gorepomod
func NewDotGitDataFromPath(path string) (*DotGitData, error) {
if !utils.DirExists(filepath.Join(path, dotGitFileName)) {
return nil, fmt.Errorf(
"%q doesn't have a %q file", path, dotGitFileName)
}
// This is an attempt to figure out where the user has cloned
// their repos. In the old days, it was an import path under
// $GOPATH/src. If we cannot guess it, we may need to ask for it,
// or maybe proceed without knowing it.
index := strings.Index(path, srcHint)
if index < 0 {
return nil, fmt.Errorf(
"path %q doesn't contain %q", path, srcHint)
}
return &DotGitData{
srcPath: path[:index+len(srcHint)-1],
repoPath: path[index+len(srcHint):],
}, nil
}
// It's a factory factory.
func (dg *DotGitData) NewRepoFactory(
exclusions []string) (*ManagerFactory, error) {
modules, err := loadProtoModules(dg.AbsPath(), exclusions)
if err != nil {
return nil, err
}
err = dg.checkModules(modules)
if err != nil {
return nil, err
}
runner := git.NewQuiet(dg.AbsPath(), true)
remoteName, err := runner.DetermineRemoteToUse()
if err != nil {
return nil, err
}
// Some tags might exist for modules that
// have been renamed or deleted; ignore those.
// There might be newer tags locally than remote,
// so report both.
localTags, err := runner.LoadLocalTags()
if err != nil {
return nil, err
}
remoteTags, err := runner.LoadRemoteTags(remoteName)
if err != nil {
return nil, err
}
return &ManagerFactory{
dg: dg,
modules: modules,
remoteName: remoteName,
versionMapLocal: localTags,
versionMapRemote: remoteTags,
}, nil
}
func (dg *DotGitData) checkModules(modules []*protoModule) error {
for _, pm := range modules {
file := filepath.Join(pm.PathToGoMod(), goModFile)
// Do the paths make sense?
if !strings.HasPrefix(pm.FullPath(), dg.RepoPath()) {
return fmt.Errorf(
"module %q doesn't start with the repository name %q",
pm.FullPath(), dg.RepoPath())
}
shortName := pm.ShortName(dg.RepoPath())
if shortName == misc.ModuleAtTop {
if pm.PathToGoMod() != dg.AbsPath() {
return fmt.Errorf("in %q, problem with top module", file)
}
} else {
// Do the relative path and short name make sense?
if !strings.HasSuffix(pm.PathToGoMod(), string(shortName)) {
return fmt.Errorf(
"in %q, the module name %q doesn't match the file's pathToGoMod %q",
file, shortName, pm.PathToGoMod())
}
}
}
return nil
}

View File

@@ -0,0 +1,6 @@
package repo
import "testing"
func TestLoadTags(t *testing.T) {
}

View File

@@ -0,0 +1,194 @@
package repo
import (
"fmt"
"strconv"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/edit"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/git"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/misc"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/semver"
)
// Manager manages a git repo.
// All data already loaded and validated, it's ready to go.
type Manager struct {
// Underlying file system facts.
dg *DotGitData
// The remote used for fetching tags, pushing tags,
// and pushing release branches.
remoteName misc.TrackedRepo
// The list of known Go modules in the repo.
modules misc.LesModules
}
func (mgr *Manager) AbsPath() string {
return mgr.dg.AbsPath()
}
func (mgr *Manager) RepoPath() string {
return mgr.dg.RepoPath()
}
func (mgr *Manager) FindModule(
target misc.ModuleShortName) misc.LaModule {
return mgr.modules.Find(target)
}
func (mgr *Manager) Tidy(doIt bool) error {
return mgr.modules.Apply(func(m misc.LaModule) error {
return edit.New(m, doIt).Tidy()
})
}
func (mgr *Manager) Pin(
doIt bool, target misc.LaModule, newV semver.SemVer) error {
return mgr.modules.Apply(func(m misc.LaModule) error {
if yes, oldVersion := m.DependsOn(target); yes {
return edit.New(m, doIt).Pin(target, oldVersion, newV)
}
return nil
})
}
func (mgr *Manager) UnPin(doIt bool, target misc.LaModule) error {
return mgr.modules.Apply(func(m misc.LaModule) error {
if yes, oldVersion := m.DependsOn(target); yes {
return edit.New(m, doIt).UnPin(target, oldVersion)
}
return nil
})
}
func hasUnPinnedDeps(m misc.LaModule) string {
if len(m.GetReplacements()) > 0 {
return "yes"
}
return ""
}
func (mgr *Manager) List() error {
fmt.Printf(" src path: %s\n", mgr.dg.SrcPath())
fmt.Printf(" repo path: %s\n", mgr.RepoPath())
fmt.Printf(" remote: %s\n", mgr.remoteName)
format := "%-" +
strconv.Itoa(mgr.modules.LenLongestName()+2) +
"s%-11s%-11s%17s %s\n"
fmt.Printf(
format, "NAME", "LOCAL", "REMOTE",
"HAS-UNPINNED-DEPS", "INTRA-REPO-DEPENDENCIES")
return mgr.modules.Apply(func(m misc.LaModule) error {
fmt.Printf(
format, m.ShortName(),
m.VersionLocal().Pretty(),
m.VersionRemote().Pretty(),
hasUnPinnedDeps(m),
mgr.modules.InternalDeps(m))
return nil
})
}
func determineBranchAndTag(
m misc.LaModule, v semver.SemVer) (string, string) {
if m.ShortName() == misc.ModuleAtTop {
return fmt.Sprintf("release-%s", v.BranchLabel()), v.String()
}
return fmt.Sprintf(
"release-%s-%s", m.ShortName(), v.BranchLabel()),
string(m.ShortName()) + "/" + v.String()
}
func (mgr *Manager) Debug(_ misc.LaModule, doIt bool) error {
gr := git.NewLoud(mgr.AbsPath(), doIt)
return gr.Debug(mgr.remoteName)
}
// Release supports a gitlab flow style release process.
//
// * All development happens in the branch named "master".
// * Each minor release gets its own branch.
// *
func (mgr *Manager) Release(
target misc.LaModule, bump semver.SvBump, doIt bool) error {
if reps := target.GetReplacements(); len(reps) > 0 {
return fmt.Errorf(
"to release %q, first pin these replacements: %v",
target.ShortName(), reps)
}
newVersion := target.VersionLocal().Bump(bump)
if newVersion.Equals(target.VersionRemote()) {
return fmt.Errorf(
"version %s already exists on remote - delete it first", newVersion)
}
if newVersion.LessThan(target.VersionRemote()) {
fmt.Printf(
"version %s is less than the most recent remote version (%s)",
newVersion, target.VersionRemote())
}
gr := git.NewLoud(mgr.AbsPath(), doIt)
relBranch, relTag := determineBranchAndTag(target, newVersion)
fmt.Printf(
"Releasing %s, stepping from %s to %s\n",
target.ShortName(), target.VersionLocal(), newVersion)
if err := gr.AssureCleanWorkspace(); err != nil {
return err
}
if err := gr.FetchRemote(mgr.remoteName); err != nil {
return err
}
if err := gr.CheckoutMainBranch(); err != nil {
return err
}
if err := gr.MergeFromRemoteMain(mgr.remoteName); err != nil {
return err
}
if err := gr.AssureCleanWorkspace(); err != nil {
return err
}
if err := gr.CheckoutReleaseBranch(mgr.remoteName, relBranch); err != nil {
return err
}
if err := gr.MergeFromRemoteMain(mgr.remoteName); err != nil {
return err
}
if err := gr.PushBranchToRemote(mgr.remoteName, relBranch); err != nil {
return err
}
if err := gr.CreateLocalReleaseTag(relTag, relBranch); err != nil {
return err
}
if err := gr.PushTagToRemote(mgr.remoteName, relTag); err != nil {
return err
}
if err := gr.CheckoutMainBranch(); err != nil {
return err
}
return nil
}
func (mgr *Manager) UnRelease(target misc.LaModule, doIt bool) error {
fmt.Printf(
"Unreleasing %s/%s\n",
target.ShortName(), target.VersionRemote())
_, tag := determineBranchAndTag(target, target.VersionRemote())
gr := git.NewLoud(mgr.AbsPath(), doIt)
if err := gr.DeleteTagFromRemote(mgr.remoteName, tag); err != nil {
return err
}
if err := gr.DeleteLocalTag(tag); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,35 @@
package repo
import (
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/misc"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/mod"
)
// ManagerFactory is a collection of clean data needed to build
// clean, fully wired up instances of Manager.
type ManagerFactory struct {
dg *DotGitData
modules []*protoModule
remoteName misc.TrackedRepo
versionMapLocal misc.VersionMap
versionMapRemote misc.VersionMap
}
func (mf *ManagerFactory) NewRepoManager() *Manager {
result := &Manager{
dg: mf.dg,
remoteName: mf.remoteName,
}
var modules misc.LesModules
for _, pm := range mf.modules {
shortName := pm.ShortName(mf.dg.RepoPath())
modules = append(
modules,
mod.New(
result, shortName, pm.mf,
mf.versionMapLocal.Latest(shortName),
mf.versionMapRemote.Latest(shortName)))
}
result.modules = modules
return result
}

View File

@@ -0,0 +1,101 @@
package repo
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"golang.org/x/mod/modfile"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/misc"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/utils"
)
const (
dotDir = "."
)
// protoModule holds parts being collected to represent a module.
type protoModule struct {
pathToGoMod string
mf *modfile.File
}
func (pm *protoModule) FullPath() string {
return pm.mf.Module.Mod.Path
}
func (pm *protoModule) PathToGoMod() string {
return pm.pathToGoMod
}
// Represents the trailing version label in a module name.
// See https://blog.golang.org/v2-go-modules
var trailingVersionPattern = regexp.MustCompile("/v\\d+$")
func (pm *protoModule) ShortName(
repoImportPath string) misc.ModuleShortName {
fp := pm.FullPath()
if fp == repoImportPath {
return misc.ModuleAtTop
}
p := fp[len(repoImportPath)+1:]
stripped := trailingVersionPattern.ReplaceAllString(p, "")
return misc.ModuleShortName(stripped)
}
func loadProtoModules(
repoRoot string, exclusions []string) (result []*protoModule, err error) {
var paths []string
paths, err = getPathsToModules(repoRoot, exclusions)
if err != nil {
return
}
for _, p := range paths {
var pm *protoModule
pm, err = loadProtoModule(p)
if err != nil {
return
}
result = append(result, pm)
}
return
}
func loadProtoModule(path string) (*protoModule, error) {
mPath := filepath.Join(path, goModFile)
content, err := ioutil.ReadFile(mPath)
if err != nil {
return nil, fmt.Errorf("error reading %q: %v\n", mPath, err)
}
f, err := modfile.Parse(mPath, content, nil)
if err != nil {
return nil, err
}
return &protoModule{pathToGoMod: path, mf: f}, nil
}
func getPathsToModules(
repoRoot string, exclusions []string) (result []string, err error) {
exclusionMap := utils.SliceToSet(exclusions)
err = filepath.Walk(
repoRoot,
func(path string, info os.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("trouble at pathToGoMod %q: %v\n", path, err)
}
if info.IsDir() {
if _, ok := exclusionMap[info.Name()]; ok {
return filepath.SkipDir
}
return nil
}
if info.Name() == goModFile {
result = append(result, path[:len(path)-len(goModFile)-1])
return filepath.SkipDir
}
return nil
})
return
}

View File

@@ -0,0 +1,47 @@
package repo
import (
"testing"
"golang.org/x/mod/modfile"
"golang.org/x/mod/module"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/misc"
)
func TestShortName(t *testing.T) {
var testCases = map[string]struct {
name misc.ModuleShortName
modFile *modfile.File
}{
"one": {
name: misc.ModuleShortName("garage"),
modFile: &modfile.File{
Module: &modfile.Module{
Mod: module.Version{
Path: "gh.com/micheal/garage",
Version: "v2.3.4",
},
},
},
},
"three": {
name: misc.ModuleShortName("fruit/yellow/banana"),
modFile: &modfile.File{
Module: &modfile.Module{
Mod: module.Version{
Path: "gh.com/micheal/fruit/yellow/banana",
Version: "v2.3.4",
},
},
},
},
}
for n, tc := range testCases {
m := protoModule{pathToGoMod: "irrelevant", mf: tc.modFile}
actual := m.ShortName("gh.com/micheal")
if actual != tc.name {
t.Errorf(
"%s: expected %s, got %s", n, tc.name, actual)
}
}
}

View File

@@ -0,0 +1,98 @@
package semver
import (
"fmt"
"strconv"
"strings"
)
// SemVer is the immutable semantic version per https://semver.org
type SemVer struct {
major int
minor int
patch int
}
func New(major, minor, patch int) SemVer {
return SemVer{
major: major,
minor: minor,
patch: patch,
}
}
var zero = New(0, 0, 0)
func Zero() SemVer {
return zero
}
// Versions implements sort.Interface to get decreasing order.
type Versions []SemVer
func (v Versions) Len() int { return len(v) }
func (v Versions) Less(i, j int) bool { return v[j].LessThan(v[i]) }
func (v Versions) Swap(i, j int) { v[i], v[j] = v[j], v[i] }
func Parse(raw string) (SemVer, error) {
if len(raw) < 6 {
// e.g. minimal length is 6, e.g. "v1.2.3"
return zero, fmt.Errorf("%q too short to be a version", raw)
}
if raw[0] != 'v' {
return zero, fmt.Errorf("%q must start with letter 'v'", raw)
}
fields := strings.Split(raw[1:], ".")
if len(fields) < 3 {
return zero, fmt.Errorf("%q doesn't have the form v1.2.3", raw)
}
n := make([]int, 3)
for i := 0; i < 3; i++ {
var err error
n[i], err = strconv.Atoi(fields[i])
if err != nil {
return zero, err
}
}
return New(n[0], n[1], n[2]), nil
}
func (v SemVer) Bump(b SvBump) SemVer {
switch b {
case Major:
return New(v.major+1, 0, 0)
case Minor:
return New(v.major, v.minor+1, 0)
default:
return New(v.major, v.minor, v.patch+1)
}
}
func (v SemVer) BranchLabel() string {
return fmt.Sprintf("v%d.%d", v.major, v.minor)
}
func (v SemVer) String() string {
return fmt.Sprintf("v%d.%d.%d", v.major, v.minor, v.patch)
}
func (v SemVer) Pretty() string {
if v.IsZero() {
return ""
}
return v.String()
}
func (v SemVer) Equals(o SemVer) bool {
return v.major == o.major && v.minor == o.minor && v.patch == o.patch
}
func (v SemVer) LessThan(o SemVer) bool {
return v.major < o.major ||
(v.major == o.major && v.minor < o.minor) ||
(v.major == o.major && v.minor == o.minor && v.patch < o.patch)
}
func (v SemVer) IsZero() bool {
return v.Equals(zero)
}

View File

@@ -0,0 +1,105 @@
package semver
import (
"testing"
)
func TestParse(t *testing.T) {
var testCases = map[string]struct {
raw string
v SemVer
errMsg string
}{
"one": {
raw: "v1.2.3",
v: SemVer{major: 1, minor: 2, patch: 3},
errMsg: "",
},
"two": {
raw: "v2.0.9999",
v: SemVer{major: 2, minor: 0, patch: 9999},
errMsg: "",
},
"three": {
raw: "pizza",
v: zero,
errMsg: "\"pizza\" too short to be a version",
},
"non-digit": {
raw: "v1.x.222",
v: zero,
errMsg: "strconv.Atoi: parsing \"x\": invalid syntax",
},
"bad fields": {
raw: "v1.222",
v: zero,
errMsg: "\"v1.222\" doesn't have the form v1.2.3",
},
}
for n, tc := range testCases {
v, err := Parse(tc.raw)
if err == nil {
if tc.errMsg != "" {
t.Errorf(
"%s: no error, but expected err %q", n, tc.errMsg)
}
if !v.Equals(tc.v) {
t.Errorf(
"%s: expected %v, got %v", n, tc.v, v)
}
} else {
if tc.errMsg == "" {
t.Errorf(
"%s: unexpected error %v", n, err)
} else {
if tc.errMsg != err.Error() {
t.Errorf(
"%s: expected err msg %q, but got %q",
n, tc.errMsg, err.Error())
}
}
}
}
}
func TestLessThan(t *testing.T) {
var testCases = map[string]struct {
v1 SemVer
v2 SemVer
expected bool
}{
"one": {
v1: SemVer{major: 2, minor: 2, patch: 3},
v2: SemVer{major: 1, minor: 2, patch: 3},
expected: false,
},
"two": {
v1: SemVer{major: 1, minor: 3, patch: 3},
v2: SemVer{major: 1, minor: 2, patch: 3},
expected: false,
},
"three": {
v1: SemVer{major: 1, minor: 2, patch: 4},
v2: SemVer{major: 1, minor: 2, patch: 3},
expected: false,
},
"eq": {
v1: SemVer{major: 2, minor: 2, patch: 3},
v2: SemVer{major: 2, minor: 2, patch: 3},
expected: false,
},
"four": {
v1: zero,
v2: SemVer{major: 0, minor: 0, patch: 1},
expected: true,
},
}
for n, tc := range testCases {
actual := tc.v1.LessThan(tc.v2)
if actual != tc.expected {
t.Errorf(
"%s: expected %v, got %v for %s LessThan %s",
n, tc.expected, actual, tc.v1.String(), tc.v2.String())
}
}
}

View File

@@ -0,0 +1,17 @@
package semver
type SvBump int
const (
Patch SvBump = iota
Minor
Major
)
func (b SvBump) String() string {
return map[SvBump]string{
Patch: "Patch",
Minor: "Minor",
Major: "Major",
}[b]
}

View File

@@ -0,0 +1,26 @@
package utils
import (
"log"
"os"
)
func DirExists(name string) bool {
info, err := os.Stat(name)
if os.IsNotExist(err) {
return false
}
return info.IsDir()
}
func SliceToSet(slice []string) map[string]bool {
result := make(map[string]bool)
for _, x := range slice {
if _, ok := result[x]; ok {
log.Fatalf("programmer error - repeated value: %s", x)
} else {
result[x] = true
}
}
return result
}

84
cmd/gorepomod/main.go Normal file
View File

@@ -0,0 +1,84 @@
package main
import (
"fmt"
"os"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/arguments"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/misc"
"sigs.k8s.io/kustomize/cmd/gorepomod/internal/repo"
)
//go:generate go run internal/gen/main.go
func loadRepoManager(args *arguments.Args) (*repo.Manager, error) {
path, err := os.Getwd()
if err != nil {
return nil, err
}
dg, err := repo.NewDotGitDataFromPath(path)
if err != nil {
return nil, err
}
pr, err := dg.NewRepoFactory(args.Exclusions())
if err != nil {
return nil, err
}
return pr.NewRepoManager(), nil
}
func actualMain() error {
args, err := arguments.Parse()
if err != nil {
return err
}
mgr, err := loadRepoManager(args)
if err != nil {
return err
}
var targetModule misc.LaModule = nil
if args.ModuleName() != misc.ModuleUnknown {
targetModule = mgr.FindModule(args.ModuleName())
if targetModule == nil {
return fmt.Errorf(
"cannot find module %q in repo %s",
args.ModuleName(), mgr.RepoPath())
}
}
switch args.GetCommand() {
case arguments.List:
return mgr.List()
case arguments.Tidy:
return mgr.Tidy(args.DoIt())
case arguments.Pin:
v := args.Version()
if v.IsZero() {
v = targetModule.VersionLocal()
}
return mgr.Pin(args.DoIt(), targetModule, v)
case arguments.UnPin:
return mgr.UnPin(args.DoIt(), targetModule)
case arguments.Release:
return mgr.Release(targetModule, args.Bump(), args.DoIt())
case arguments.UnRelease:
return mgr.UnRelease(targetModule, args.DoIt())
case arguments.Debug:
return mgr.Debug(targetModule, args.DoIt())
default:
return fmt.Errorf("cannot handle cmd %v", args.GetCommand())
}
}
func main() {
if len(os.Args) < 2 {
fmt.Print(usageMsg)
return
}
if err := actualMain(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

109
cmd/gorepomod/usage.go Normal file
View File

@@ -0,0 +1,109 @@
// Code generated by internal/gen/main.go; DO NOT EDIT.
package main
const (
usageMsg = `
Helps when you have a git repository with multiple Go modules.
It handles tasks one might otherwise attempt with
'''
find ./ -name "go.mod" | xargs {some hack}
'''
Run it from a git repository root.
It walks the repository, reads 'go.mod' files, builds
a model of Go modules and intra-repo module
dependencies, then performs some operation.
Install:
'''
go get github.com/monopole/gorepomod
'''
## Usage
_Commands that change things (everything but 'list')
do nothing but log commands
unless you add the '--doIt' flag,
allowing the change._
#### 'gorepomod list'
Lists modules and intra-repo dependencies.
Use this to get module names for use in other commands.
#### 'gorepomod tidy'
Creates a change with mechanical updates
to 'go.mod' and 'go.sum' files.
#### 'gorepomod unpin {module}'
Creates a change to 'go.mod' files.
For each module _m_ in the repository,
if _m_ depends on a _{module}_,
then _m_'s dependency on it will be replaced by
a relative path to the in-repo module.
#### 'gorepomod pin {module} [{version}]'
Creates a change to 'go.mod' files.
The opposite of 'unpin'.
The change removes replacements and pins _m_ to a
specific, previously tagged and released version of _{module}_.
The argument _{version}_ defaults to recent version of _{module}_.
_{version}_ should be in semver form, e.g. 'v1.2.3'.
#### 'gorepomod release {module} [patch|minor|major]'
Computes a new version for the module, tags the repo
with that version, and pushes the tag to the remote.
The value of the 2nd argument, either 'patch' (the default),
'minor' or 'major', determines the new version.
If the existing version is _v1.2.7_, then the new version will be:
- 'patch' -> _v1.2.8_
- 'minor' -> _v1.3.0_
- 'major' -> _v2.0.0_
After establishing the the version, the command looks for a branch named
> _release-{module}/-v{major}.{minor}_
If the branch doesn't exist, the command creates it and pushes it to the remote.
The command then creates a new tag in the form
> _{module}/v{major}.{minor}.{patch}_
The command pushes this tag to the remote. This typically triggers
cloud activity to create release artifacts.
#### 'gorepomod unrelease {module}'
This undoes the work of 'release', by deleting the
most recent tag both locally and at the remote.
You can then fix whatever, and re-release.
This, however, must be done almost immediately.
If there's a chance someone (or some cloud robot) already
imported the module at the given tag, then don't do this,
because it will confuse module caches.
Do a new patch release instead.
`
)

View File

@@ -1,67 +0,0 @@
#!/usr/bin/env bash
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
# In general, pin modules to a specific version of the
# kustomize API before a release of that module, and
# unpin the module after the module release so that
# development proceeds against the API's HEAD.
#
# E.g. for the kustomize CLI module, do this before
# releasing the CLI:
#
# ./hack/pinUnpin.sh pin kustomize v0.3.1
#
# where v0.3.1 is the most recently released version of
# the API, and do the following afterwards:
#
# ./hack/pinUnpin.sh unPin kustomize
set -o nounset
set -o pipefail
if [ "$#" -lt 2 ]; then
echo "usage:"
echo " ./hack/pinUnpin.sh pin kustomize v0.3.1"
echo " or "
echo " ./hack/pinUnpin.sh unPin kustomize"
exit 1
fi
operation=$1
if [[ ("$operation" != "pin") && ("$operation" != "unPin") ]]; then
echo "unknown operation $operation"
exit 1
fi
module=$2
if [ ! -d "$module" ]; then
echo "directory $module doesn't exist"
exit 1
fi
version="unnecessary"
if [ "$operation" == "pin" ]; then
if [ "$#" -le 2 ]; then
echo "Specify version to pin, e.g. '$0 $module pin v0.2.0'"
exit 1
fi
version=$3
fi
function unPin {
oldV=$(grep -m 1 sigs.k8s.io/kustomize/api go.mod | awk '{print $NF}')
go mod edit -replace=sigs.k8s.io/kustomize/api@${oldV}=../api
go mod tidy
}
function pin {
oldV=$(grep -m 1 sigs.k8s.io/kustomize/api go.mod | awk '{print $NF}')
go mod edit -dropreplace=sigs.k8s.io/kustomize/api@${oldV}
go mod edit -require=sigs.k8s.io/kustomize/api@$version
go mod tidy
}
pushd $module >& /dev/null
$operation
popd >& /dev/null

View File

@@ -1,107 +0,0 @@
#!/usr/bin/env bash
# Copyright 2019 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
#
# To fix all plugin dependence to a particular
# released version of the kustomize API,
# run this from the repo root:
#
# ./hack/pinUnpinPluginApiDep.sh pin api v0.2.0
#
# To replace fixed dependence with
# dependence on local filesystem (HEAD)
# run this from the repo root:
#
# ./hack/pinUnpinPluginApiDep.sh unPin api
#
# All plugins, even plugins not written in Go,
# have a unit test written in Go that depends
# on a particular version of the api for a test
# harness. The plugins written in Go, either
# as exec or Go-plugin style plugins,
# will likely depend directly on the kustomize
# API, and any number of other 3rd party packages.
#
# The Go plugins in the `builtin` directory
# are in practice converted to static libraries
# in the API, so should remain unpinned (dependent
# on HEAD). The other example plugins can be pinned
# or unpinned on a case by case basis, since
# they are just examples - but likely should
# remain unpinned too. Nothing in the outside
# world should depend on these plugin modules,
# so there's no reason for them to be pinned.
set -o errexit
set -o nounset
#set -o pipefail
function doUnPin {
oldV=$(grep -m 1 sigs.k8s.io/kustomize/${module} go.mod | awk '{print $NF}')
if [ ! -z $oldV ]; then
go mod edit -replace=sigs.k8s.io/kustomize/${module}@${oldV}=$1
fi
go mod tidy
}
function doPin {
oldV=$(grep -m 1 sigs.k8s.io/kustomize/${module} go.mod | awk '{print $NF}')
if [ ! -z $oldV ]; then
go mod edit -dropreplace=sigs.k8s.io/kustomize/${module}@${oldV}
go mod edit -require=sigs.k8s.io/kustomize/${module}@$1
fi
go mod tidy
}
function forEachGoMod {
for goMod in $(find $2 -name 'go.mod'); do
d=$(dirname "${goMod}")
echo "$1 $d"
(cd $d; $1 $3)
done
}
function unPin {
echo "Unpinning $module"
forEachGoMod doUnPin ./plugin/builtin ../../../${module}
forEachGoMod doUnPin ./plugin/someteam.example.com/v1 ../../../../${module}
}
function pin {
echo "Pinning $module to $version"
forEachGoMod doPin ./plugin/builtin ${version}
forEachGoMod doPin ./plugin/someteam.example.com/v1 ${version}
}
if [ "$#" -eq 0 ]; then
echo "Pin or unpin plugins, e.g."
echo " "
echo " ./hack/pinUnpinPluginApiDep.sh pin api v0.2.0"
echo " "
echo " ./hack/pinUnpinPluginApiDep.sh unPin api"
echo " "
exit 1
fi
operation=$1
if [[ ("$operation" != "pin") && ("$operation" != "unPin") ]]; then
echo "unknown operation $operation"
exit 1
fi
module=$2
if [[ ("$module" != "api") && ("$module" != "kyaml") ]]; then
echo "unknown module $module"
exit 1
fi
version="unnecessary"
if [ "$operation" == "pin" ]; then
if [ "$#" -le 2 ]; then
echo "Specify version to pin, e.g. '$0 pin v0.2.0'"
exit 1
fi
version=$3
fi
$operation