mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-05-17 18:25:26 +00:00
391 lines
9.8 KiB
Go
391 lines
9.8 KiB
Go
// Copyright 2022 The Kubernetes Authors.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
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
|
|
// Indicate local execution to adapt with path.
|
|
localFlag bool
|
|
// Run commands, or merely print commands.
|
|
verbosity Verbosity
|
|
}
|
|
|
|
func NewLoud(wd string, doIt bool, localFlag bool) *Runner {
|
|
return newRunner(wd, doIt, High, localFlag)
|
|
}
|
|
|
|
func NewQuiet(wd string, doIt bool, localFlag bool) *Runner {
|
|
return newRunner(wd, doIt, Low, localFlag)
|
|
}
|
|
|
|
func newRunner(wd string, doIt bool, v Verbosity, localFlag bool) *Runner {
|
|
return &Runner{workDir: wd, doIt: doIt, verbosity: v, localFlag: localFlag}
|
|
}
|
|
|
|
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) {
|
|
var out string
|
|
|
|
// Update latest tags from upstream
|
|
gr.comment("updating tags from upstream")
|
|
_, err = gr.run(noHarmDone, "fetch", "-t", string(remoteUpstream), string(mainBranch))
|
|
if err != nil {
|
|
// Handle if repo is not a fork
|
|
_, err = gr.run(noHarmDone, "fetch", "-t", string(mainBranch))
|
|
if err != nil {
|
|
_ = fmt.Errorf("failed to fetch tags from %s", string(mainBranch))
|
|
}
|
|
}
|
|
|
|
gr.comment("loading remote tags")
|
|
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")
|
|
err := gr.runNoOut(noHarmDone, "fetch", string(remote))
|
|
if err != nil {
|
|
// If current repo is fork
|
|
err = gr.runNoOut(noHarmDone, "fetch", string(remoteUpstream))
|
|
if err != nil {
|
|
return fmt.Errorf("%w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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 remotely. Create or reset it locally.
|
|
out, err := gr.run(noHarmDone, "checkout", "-B", branch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Expected strings: "Switched to a new branch" or "Switched to and reset branch"
|
|
if !strings.Contains(out, "Switched to") {
|
|
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)
|
|
}
|
|
|
|
func (gr *Runner) GetLatestTag(releaseBranch string) (string, error) {
|
|
var latestTag string
|
|
// Assuming release branch has this format: release-path/to/module-vX.Y.Z
|
|
// and each release branch maintains tags, extract version from latest `releaseBranch`
|
|
gr.comment("extract version from latest release branch")
|
|
filteredBranchList, err := gr.run(noHarmDone, "branch", "-a", "--list", "*"+releaseBranch+"*", "--sort=-committerdate")
|
|
if len(filteredBranchList) < 1 {
|
|
_ = fmt.Errorf("latest tag not found for %s", releaseBranch)
|
|
return "", err
|
|
}
|
|
newestBranch := strings.Split(strings.ReplaceAll(filteredBranchList, "\r\n", "\n"), "\n")
|
|
split := strings.Split(newestBranch[0], "-")
|
|
latestTag = split[len(split)-1]
|
|
if err != nil {
|
|
_ = fmt.Errorf("error getting latest tag for %s", releaseBranch)
|
|
}
|
|
|
|
return latestTag, nil
|
|
}
|
|
|
|
func (gr *Runner) GetMainBranch() string {
|
|
return string(mainBranch)
|
|
}
|