sigs.k8s.io/release-sdk@v0.11.1-0.20240417074027-8061fb5e4952/git/git.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package git
    18  
    19  import (
    20  	"bufio"
    21  	"bytes"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"math"
    26  	"net/url"
    27  	"os"
    28  	"path/filepath"
    29  	"regexp"
    30  	"sort"
    31  	"strings"
    32  	"time"
    33  
    34  	"github.com/blang/semver/v4"
    35  	"github.com/go-git/go-git/v5"
    36  	"github.com/go-git/go-git/v5/config"
    37  	"github.com/go-git/go-git/v5/plumbing"
    38  	"github.com/go-git/go-git/v5/plumbing/object"
    39  	"github.com/go-git/go-git/v5/plumbing/storer"
    40  	"github.com/sirupsen/logrus"
    41  
    42  	"sigs.k8s.io/release-sdk/regex"
    43  	"sigs.k8s.io/release-utils/command"
    44  	"sigs.k8s.io/release-utils/util"
    45  )
    46  
    47  const (
    48  	// DefaultGithubOrg is the default GitHub org used for Kubernetes project
    49  	// repos
    50  	DefaultGithubOrg = "kubernetes"
    51  
    52  	// DefaultGithubRepo is the default git repository
    53  	DefaultGithubRepo = "kubernetes"
    54  
    55  	// DefaultGithubReleaseRepo is the default git repository used for
    56  	// SIG Release
    57  	DefaultGithubReleaseRepo = "sig-release"
    58  
    59  	// DefaultRemote is the default git remote name
    60  	DefaultRemote = "origin"
    61  
    62  	// DefaultRef is the default git reference name
    63  	DefaultRef = "HEAD"
    64  
    65  	// DefaultBranch is the default branch name
    66  	DefaultBranch = "master"
    67  
    68  	// DefaultGitUser is the default user name used for commits.
    69  	DefaultGitUser = "Kubernetes Release Robot"
    70  
    71  	// DefaultGitEmail is the default email used for commits.
    72  	DefaultGitEmail = "k8s-release-robot@users.noreply.github.com"
    73  
    74  	defaultGithubAuthRoot = "git@github.com:"
    75  	gitExecutable         = "git"
    76  	releaseBranchPrefix   = "release-"
    77  )
    78  
    79  // setVerboseTrace enables maximum verbosity output.
    80  func setVerboseTrace() error {
    81  	if err := setVerbose(5, 2, 2, 2, 2, 2, 2, 2); err != nil {
    82  		return fmt.Errorf("set verbose: %w", err)
    83  	}
    84  	return nil
    85  }
    86  
    87  // setVerboseDebug enables a higher verbosity output for git.
    88  func setVerboseDebug() error {
    89  	if err := setVerbose(2, 2, 2, 0, 0, 0, 2, 0); err != nil {
    90  		return fmt.Errorf("set verbose: %w", err)
    91  	}
    92  	return nil
    93  }
    94  
    95  // setVerbose changes the git verbosity output.
    96  func setVerbose(
    97  	merge,
    98  	curl,
    99  	trace,
   100  	tracePackAccess,
   101  	tracePacket,
   102  	tracePerformance,
   103  	traceSetup,
   104  	traceShallow uint,
   105  ) error {
   106  	// Possible values taken from:
   107  	// https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#Debugging
   108  	for key, value := range map[string]string{
   109  		// Controls the output for the recursive merge strategy. The allowed
   110  		// values are as follows:
   111  		// 0 outputs nothing, except possibly a single error message.
   112  		// 1 shows only conflicts.
   113  		// 2 also shows file changes.
   114  		// 3 shows when files are skipped because they haven’t changed.
   115  		// 4 shows all paths as they are processed.
   116  		// 5 and above show detailed debugging information.
   117  		// The default value is 2.
   118  		"GIT_MERGE_VERBOSITY": fmt.Sprint(merge),
   119  
   120  		// Git uses the curl library to do network operations over HTTP, so
   121  		// GIT_CURL_VERBOSE tells Git to emit all the messages generated by
   122  		// that library. This is similar to doing curl -v on the command line.
   123  		"GIT_CURL_VERBOSE": fmt.Sprint(curl),
   124  
   125  		// Controls general traces, which don’t fit into any specific category.
   126  		// This includes the expansion of aliases, and delegation to other
   127  		// sub-programs.
   128  		"GIT_TRACE": fmt.Sprint(trace),
   129  
   130  		// Controls tracing of packfile access. The first field is the packfile
   131  		// being accessed, the second is the offset within that file.
   132  		"GIT_TRACE_PACK_ACCESS": fmt.Sprint(tracePackAccess),
   133  
   134  		// Enables packet-level tracing for network operations.
   135  		"GIT_TRACE_PACKET": fmt.Sprint(tracePacket),
   136  
   137  		// Controls logging of performance data. The output shows how long each
   138  		// particular git invocation takes.
   139  		"GIT_TRACE_PERFORMANCE": fmt.Sprint(tracePerformance),
   140  
   141  		// Shows information about what Git is discovering about the repository
   142  		// and environment it’s interacting with.
   143  		"GIT_TRACE_SETUP": fmt.Sprint(traceSetup),
   144  
   145  		// Debugging fetching/cloning of shallow repositories.
   146  		"GIT_TRACE_SHALLOW": fmt.Sprint(traceShallow),
   147  	} {
   148  		if err := os.Setenv(key, value); err != nil {
   149  			return fmt.Errorf("unable to set %s=%s: %w", key, value, err)
   150  		}
   151  	}
   152  	return nil
   153  }
   154  
   155  // GetDefaultKubernetesRepoURL returns the default HTTPS repo URL for Kubernetes.
   156  // Expected: https://github.com/kubernetes/kubernetes
   157  func GetDefaultKubernetesRepoURL() string {
   158  	return GetKubernetesRepoURL(DefaultGithubOrg, false)
   159  }
   160  
   161  // GetKubernetesRepoURL takes a GitHub org and repo, and useSSH as a boolean and
   162  // returns a repo URL for Kubernetes.
   163  // Expected result is one of the following:
   164  // - https://github.com/<org>/kubernetes
   165  // - git@github.com:<org>/kubernetes
   166  func GetKubernetesRepoURL(org string, useSSH bool) string {
   167  	if org == "" {
   168  		org = DefaultGithubOrg
   169  	}
   170  
   171  	return GetRepoURL(org, DefaultGithubRepo, useSSH)
   172  }
   173  
   174  // GetRepoURL takes a GitHub org and repo, and useSSH as a boolean and
   175  // returns a repo URL for the specified repo.
   176  // Expected result is one of the following:
   177  // - https://github.com/<org>/<repo>
   178  // - git@github.com:<org>/<repo>
   179  func GetRepoURL(org, repo string, useSSH bool) (repoURL string) {
   180  	slug := filepath.Join(org, repo)
   181  
   182  	if useSSH {
   183  		repoURL = fmt.Sprintf("%s%s", defaultGithubAuthRoot, slug)
   184  	} else {
   185  		repoURL = (&url.URL{
   186  			Scheme: "https",
   187  			Host:   "github.com",
   188  			Path:   slug,
   189  		}).String()
   190  	}
   191  
   192  	return repoURL
   193  }
   194  
   195  // ConfigureGlobalDefaultUserAndEmail globally configures the default git
   196  // user and email.
   197  func ConfigureGlobalDefaultUserAndEmail() error {
   198  	if err := filterCommand(
   199  		"", "config", "--global", "user.name", DefaultGitUser,
   200  	).RunSuccess(); err != nil {
   201  		return fmt.Errorf("configure user name: %w", err)
   202  	}
   203  
   204  	if err := filterCommand(
   205  		"", "config", "--global", "user.email", DefaultGitEmail,
   206  	).RunSuccess(); err != nil {
   207  		return fmt.Errorf("configure user email: %w", err)
   208  	}
   209  
   210  	return nil
   211  }
   212  
   213  // ConfigureGlobalCustomUserAndEmail globally configures a custom git
   214  // user and email.
   215  func ConfigureGlobalCustomUserAndEmail(gitUser, gitEmail string) error {
   216  	if err := filterCommand(
   217  		"", "config", "--global", "user.name", gitUser,
   218  	).RunSuccess(); err != nil {
   219  		return fmt.Errorf("configure user name: %w", err)
   220  	}
   221  
   222  	if err := filterCommand(
   223  		"", "config", "--global", "user.email", gitEmail,
   224  	).RunSuccess(); err != nil {
   225  		return fmt.Errorf("configure user email: %w", err)
   226  	}
   227  
   228  	return nil
   229  }
   230  
   231  // filterCommand returns a command which automatically filters sensitive information.
   232  func filterCommand(workdir string, args ...string) *command.Command {
   233  	// Filter GitHub API keys
   234  	c, err := command.NewWithWorkDir(
   235  		workdir, gitExecutable, args...,
   236  	).Filter(`(?m)git:[0-9a-zA-Z]{35,40}`, "[REDACTED]")
   237  	if err != nil {
   238  		// should never happen
   239  		logrus.Fatalf("git command creation failed: %v", err)
   240  	}
   241  
   242  	return c
   243  }
   244  
   245  // DiscoverResult is the result of a revision discovery
   246  type DiscoverResult struct {
   247  	startSHA, startRev, endSHA, endRev string
   248  }
   249  
   250  // StartSHA returns the start SHA for the DiscoverResult
   251  func (d *DiscoverResult) StartSHA() string {
   252  	return d.startSHA
   253  }
   254  
   255  // StartRev returns the start revision for the DiscoverResult
   256  func (d *DiscoverResult) StartRev() string {
   257  	return d.startRev
   258  }
   259  
   260  // EndSHA returns the end SHA for the DiscoverResult
   261  func (d *DiscoverResult) EndSHA() string {
   262  	return d.endSHA
   263  }
   264  
   265  // EndRev returns the end revision for the DiscoverResult
   266  func (d *DiscoverResult) EndRev() string {
   267  	return d.endRev
   268  }
   269  
   270  // Remote is a representation of a git remote location
   271  type Remote struct {
   272  	name string
   273  	urls []string
   274  }
   275  
   276  // NewRemote creates a new remote for the provided name and URLs
   277  func NewRemote(name string, urls []string) *Remote {
   278  	return &Remote{name, urls}
   279  }
   280  
   281  // Name returns the name of the remote
   282  func (r *Remote) Name() string {
   283  	return r.name
   284  }
   285  
   286  // URLs returns all available URLs of the remote
   287  func (r *Remote) URLs() []string {
   288  	return r.urls
   289  }
   290  
   291  // Repo is a wrapper for a Kubernetes repository instance
   292  type Repo struct {
   293  	inner      Repository
   294  	worktree   Worktree
   295  	dir        string
   296  	dryRun     bool
   297  	maxRetries int
   298  }
   299  
   300  //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate
   301  //go:generate /usr/bin/env bash -c "cat ../scripts/boilerplate/boilerplate.generatego.txt gitfakes/fake_repository.go > gitfakes/_fake_repository.go && mv gitfakes/_fake_repository.go gitfakes/fake_repository.go"
   302  //go:generate /usr/bin/env bash -c "cat ../scripts/boilerplate/boilerplate.generatego.txt gitfakes/fake_worktree.go > gitfakes/_fake_worktree.go && mv gitfakes/_fake_worktree.go gitfakes/fake_worktree.go"
   303  
   304  // Repository is the main interface to the git.Repository functionality
   305  //
   306  //counterfeiter:generate . Repository
   307  type Repository interface {
   308  	CreateTag(string, plumbing.Hash, *git.CreateTagOptions) (*plumbing.Reference, error)
   309  	Branches() (storer.ReferenceIter, error)
   310  	CommitObject(plumbing.Hash) (*object.Commit, error)
   311  	CreateRemote(*config.RemoteConfig) (*git.Remote, error)
   312  	DeleteRemote(name string) error
   313  	Push(o *git.PushOptions) error
   314  	Head() (*plumbing.Reference, error)
   315  	Remote(string) (*git.Remote, error)
   316  	Remotes() ([]*git.Remote, error)
   317  	ResolveRevision(plumbing.Revision) (*plumbing.Hash, error)
   318  	Tags() (storer.ReferenceIter, error)
   319  }
   320  
   321  // Worktree is the main interface to the git.Worktree functionality
   322  //
   323  //counterfeiter:generate . Worktree
   324  type Worktree interface {
   325  	Add(string) (plumbing.Hash, error)
   326  	Commit(string, *git.CommitOptions) (plumbing.Hash, error)
   327  	Checkout(*git.CheckoutOptions) error
   328  	Status() (git.Status, error)
   329  }
   330  
   331  // Dir returns the directory where the repository is stored on disk
   332  func (r *Repo) Dir() string {
   333  	return r.dir
   334  }
   335  
   336  // SetDry sets the repo to dry-run mode, which does not modify any remote locations
   337  // at all.
   338  func (r *Repo) SetDry() {
   339  	r.dryRun = true
   340  }
   341  
   342  // SetWorktree can be used to manually set the repository worktree
   343  func (r *Repo) SetWorktree(worktree Worktree) {
   344  	r.worktree = worktree
   345  }
   346  
   347  // SetInnerRepo can be used to manually set the inner repository
   348  func (r *Repo) SetInnerRepo(repo Repository) {
   349  	r.inner = repo
   350  }
   351  
   352  // SetMaxRetries defines the number of times, the git client will retry
   353  // some operations when timing out or network failures. Setting it to
   354  // 0 disables retrying
   355  func (r *Repo) SetMaxRetries(numRetries int) {
   356  	r.maxRetries = numRetries
   357  }
   358  
   359  func LSRemoteExec(repoURL string, args ...string) (string, error) {
   360  	cmdArgs := append([]string{"ls-remote", "--", repoURL}, args...)
   361  	cmdStatus, err := filterCommand("", cmdArgs...).
   362  		RunSilentSuccessOutput()
   363  	if err != nil {
   364  		return "", fmt.Errorf("failed to execute the ls-remote command: %w", err)
   365  	}
   366  
   367  	return strings.TrimSpace(cmdStatus.Output()), nil
   368  }
   369  
   370  // CloneOrOpenDefaultGitHubRepoSSH clones the default Kubernetes GitHub
   371  // repository via SSH if the repoPath is empty, otherwise updates it at the
   372  // expected repoPath.
   373  func CloneOrOpenDefaultGitHubRepoSSH(repoPath string) (*Repo, error) {
   374  	return CloneOrOpenGitHubRepo(
   375  		repoPath, DefaultGithubOrg, DefaultGithubRepo, true,
   376  	)
   377  }
   378  
   379  // CleanCloneGitHubRepo creates a guaranteed fresh checkout of a given repository. The returned *Repo has a Cleanup()
   380  // method that should be used to delete the repository on-disk afterwards.
   381  func CleanCloneGitHubRepo(owner, repo string, useSSH, updateRepo bool, opts *git.CloneOptions) (*Repo, error) {
   382  	repoURL := GetRepoURL(owner, repo, useSSH)
   383  	// The use of a blank string for the repo path triggers special behaviour in CloneOrOpenRepo that causes a true
   384  	// temporary directory with a random name to be created.
   385  	return CloneOrOpenRepo("", repoURL, useSSH, updateRepo, opts)
   386  }
   387  
   388  // CloneOrOpenGitHubRepo works with a repository in the given directory, or creates one if the directory is empty. The
   389  // repo uses the provided GitHub repository via the owner and repo. If useSSH is true, then it will clone the
   390  // repository using the defaultGithubAuthRoot.
   391  func CloneOrOpenGitHubRepo(repoPath, owner, repo string, useSSH bool) (*Repo, error) {
   392  	repoURL := GetRepoURL(owner, repo, useSSH)
   393  	return CloneOrOpenRepo(repoPath, repoURL, useSSH, true, nil)
   394  }
   395  
   396  // ShallowCleanCloneGitHubRepo creates a guaranteed fresh checkout of a GitHub
   397  // repository. The returned *Repo has a Cleanup() method that should be used to
   398  // delete the repository on-disk after it is no longer needed.
   399  func ShallowCleanCloneGitHubRepo(owner, repo string, useSSH bool) (*Repo, error) {
   400  	repoURL := GetRepoURL(owner, repo, useSSH)
   401  	// Pass a blank string to ensure a temp directory gets created
   402  	return ShallowCloneOrOpenRepo("", repoURL, useSSH)
   403  }
   404  
   405  // ShallowCloneOrOpenGitHubRepo this is the *GitHub* counterpart of
   406  // ShallowCloneOrOpenRepo. It works exactly the same but it takes a
   407  // GitHub org and repo instead of a URL
   408  func ShallowCloneOrOpenGitHubRepo(owner, repoPath string, useSSH bool) (*Repo, error) {
   409  	repoURL := GetRepoURL(owner, repoPath, useSSH)
   410  	return ShallowCloneOrOpenRepo(repoPath, repoURL, useSSH)
   411  }
   412  
   413  // ShallowCloneOrOpenRepo clones performs a shallow clone of a repository (a
   414  // clone of depth 1). It is almost identical to CloneOrOpenRepo. If a
   415  // repository already exists in repoPath, then no clone is done and the function
   416  // returns the existing repository.
   417  func ShallowCloneOrOpenRepo(repoPath, repoURL string, useSSH bool) (*Repo, error) { //nolint: revive
   418  	return cloneOrOpenRepo(repoPath, repoURL, true, &git.CloneOptions{Depth: 1})
   419  }
   420  
   421  // CloneOrOpenRepo creates a temp directory containing the provided
   422  // GitHub repository via the url.
   423  //
   424  // If a repoPath is given, then the function tries to update the repository.
   425  //
   426  // The function returns the repository if cloning or updating of the repository
   427  // was successful, otherwise an error.
   428  func CloneOrOpenRepo(repoPath, repoURL string, useSSH, updateRepo bool, opts *git.CloneOptions) (*Repo, error) { //nolint: revive
   429  	return cloneOrOpenRepo(repoPath, repoURL, updateRepo, opts)
   430  }
   431  
   432  // cloneOrOpenRepo checks that the repoPath exists or creates it before running the
   433  // clone operation and connects the clone progress writer to our logging system
   434  // if needed. This function is the core of the *CloneOrOpenRepo functions.
   435  func cloneOrOpenRepo(repoPath, repoURL string, updateRepository bool, opts *git.CloneOptions) (*Repo, error) {
   436  	// Ensure we have a directory path
   437  	targetDir, preexisting, err := ensureRepoPath(repoPath)
   438  	if err != nil {
   439  		return nil, fmt.Errorf("ensuring repository path: %w", err)
   440  	}
   441  
   442  	// If the repo already exists, just update it
   443  	if preexisting {
   444  		return updateRepo(targetDir, true)
   445  	}
   446  
   447  	if opts == nil {
   448  		opts = &git.CloneOptions{}
   449  	}
   450  
   451  	progressBuffer := &bytes.Buffer{}
   452  	progressWriters := []io.Writer{progressBuffer}
   453  
   454  	// Preserve any progresswriters already defined
   455  	if opts.Progress != nil {
   456  		progressWriters = append(progressWriters, opts.Progress)
   457  	}
   458  
   459  	// Only output the clone progress on debug or trace level,
   460  	// otherwise it's too boring.
   461  	logLevel := logrus.StandardLogger().Level
   462  	if logLevel >= logrus.DebugLevel {
   463  		progressWriters = append(progressWriters, os.Stderr)
   464  	}
   465  
   466  	opts.Progress = io.MultiWriter(progressWriters...)
   467  
   468  	if err := cloneRepository(repoURL, targetDir, opts); err != nil {
   469  		if logLevel < logrus.DebugLevel {
   470  			logrus.Errorf(
   471  				"Clone repository failed. Tracked progress:\n%s",
   472  				progressBuffer.String(),
   473  			)
   474  		}
   475  		return nil, err
   476  	}
   477  
   478  	return updateRepo(targetDir, updateRepository)
   479  }
   480  
   481  // cloneRepository is a utility function that exposes the bare git clone
   482  // operation internally.
   483  func cloneRepository(repoURL, repoPath string, opts *git.CloneOptions) error {
   484  	// We always clone to the repo defined in the arguments
   485  	if opts == nil {
   486  		opts = &git.CloneOptions{}
   487  	}
   488  	opts.URL = repoURL
   489  	if _, err := git.PlainClone(repoPath, false, opts); err != nil {
   490  		return fmt.Errorf("unable to clone repo: %w", err)
   491  	}
   492  	return nil
   493  }
   494  
   495  // ensureRepoPath makes sure the repository path points to a valid directory. If
   496  // the path is an empty string, it will create a temporary directory. If it already
   497  // exists it will return exisitingDir set to true, a bool to let other functions
   498  // know its ok not to clone it again.
   499  func ensureRepoPath(repoPath string) (targetDir string, exisitingDir bool, err error) {
   500  	if repoPath != "" {
   501  		logrus.Debugf("Using existing repository path %q", repoPath)
   502  		_, err := os.Stat(repoPath)
   503  
   504  		switch {
   505  		case err == nil:
   506  			// The file or directory exists, just try to update the repo
   507  			return repoPath, true, nil
   508  		case os.IsNotExist(err):
   509  			// The directory does not exists, we still have to clone it
   510  			targetDir = repoPath
   511  		default:
   512  			// Something else bad happened
   513  			return "", false, fmt.Errorf("reading existing directory: %w", err)
   514  		}
   515  	} else {
   516  		// No repoPath given, use a random temp dir instead
   517  		t, err := os.MkdirTemp("", "k8s-")
   518  		if err != nil {
   519  			return "", false, fmt.Errorf("unable to create temp dir: %w", err)
   520  		}
   521  		targetDir = t
   522  		logrus.Debugf("Cloning to temporary directory %q", t)
   523  	}
   524  	return targetDir, false, nil
   525  }
   526  
   527  // updateRepo tries to open the provided repoPath and fetches the latest
   528  // changes from the configured remote location
   529  func updateRepo(repoPath string, updateRepository bool) (*Repo, error) {
   530  	r, err := OpenRepo(repoPath)
   531  	if err != nil {
   532  		return nil, err
   533  	}
   534  
   535  	if updateRepository {
   536  		// Update the repo
   537  		if err := filterCommand(
   538  			r.Dir(), "pull", "--rebase",
   539  		).RunSilentSuccess(); err != nil {
   540  			return nil, fmt.Errorf("unable to pull from remote: %w", err)
   541  		}
   542  	}
   543  
   544  	return r, nil
   545  }
   546  
   547  // OpenRepo tries to open the provided repoPath
   548  func OpenRepo(repoPath string) (*Repo, error) {
   549  	if !command.Available(gitExecutable) {
   550  		return nil, fmt.Errorf(
   551  			"%s executable is not available in $PATH", gitExecutable,
   552  		)
   553  	}
   554  	logLevel := logrus.StandardLogger().Level
   555  	if logLevel == logrus.DebugLevel {
   556  		logrus.Info("Setting verbose git output (debug)")
   557  		if err := setVerboseDebug(); err != nil {
   558  			return nil, fmt.Errorf("set debug output: %w", err)
   559  		}
   560  	} else if logLevel == logrus.TraceLevel {
   561  		logrus.Info("Setting verbose git output (trace)")
   562  		if err := setVerboseTrace(); err != nil {
   563  			return nil, fmt.Errorf("set trace output: %w", err)
   564  		}
   565  	}
   566  
   567  	if strings.HasPrefix(repoPath, "~/") {
   568  		repoPath = os.Getenv("HOME") + repoPath[1:]
   569  		logrus.Warnf("Normalizing repository to: %s", repoPath)
   570  	}
   571  
   572  	r, err := git.PlainOpenWithOptions(
   573  		repoPath, &git.PlainOpenOptions{DetectDotGit: true},
   574  	)
   575  	if err != nil {
   576  		return nil, fmt.Errorf("opening repo: %w", err)
   577  	}
   578  
   579  	worktree, err := r.Worktree()
   580  	if err != nil {
   581  		return nil, fmt.Errorf("getting repository worktree: %w", err)
   582  	}
   583  
   584  	return &Repo{
   585  		inner:    r,
   586  		worktree: worktree,
   587  		dir:      worktree.Filesystem.Root(),
   588  	}, nil
   589  }
   590  
   591  func (r *Repo) Cleanup() error {
   592  	logrus.Debugf("Deleting %s", r.dir)
   593  	return os.RemoveAll(r.dir)
   594  }
   595  
   596  // RevParseTag parses a git revision and returns a SHA1 on success, otherwise an
   597  // error.
   598  // If the revision does not match a tag add the remote origin in the revision.
   599  func (r *Repo) RevParseTag(rev string) (string, error) {
   600  	matched, err := regexp.MatchString(`v\d+\.\d+\.\d+.*`, rev)
   601  	if err != nil {
   602  		return "", err
   603  	}
   604  	if !matched {
   605  		// Prefix all non-tags the default remote "origin"
   606  		rev = Remotify(rev)
   607  	}
   608  
   609  	// Try to resolve the rev
   610  	ref, err := r.inner.ResolveRevision(plumbing.Revision(rev))
   611  	if err != nil {
   612  		return "", err
   613  	}
   614  
   615  	return ref.String(), nil
   616  }
   617  
   618  // RevParse parses a git revision and returns a SHA1 on success, otherwise an
   619  // error.
   620  func (r *Repo) RevParse(rev string) (string, error) {
   621  	// Try to resolve the rev
   622  	ref, err := r.inner.ResolveRevision(plumbing.Revision(rev))
   623  	if err != nil {
   624  		return "", err
   625  	}
   626  
   627  	return ref.String(), nil
   628  }
   629  
   630  // RevParseTagShort parses a git revision and returns a SHA1 trimmed to the length
   631  // 10 on success, otherwise an error.
   632  // If the revision does not match a tag add the remote origin in the revision.
   633  func (r *Repo) RevParseTagShort(rev string) (string, error) {
   634  	fullRev, err := r.RevParseTag(rev)
   635  	if err != nil {
   636  		return "", err
   637  	}
   638  
   639  	return fullRev[:10], nil
   640  }
   641  
   642  // RevParseShort parses a git revision and returns a SHA1 trimmed to the length
   643  // 10 on success, otherwise an error.
   644  func (r *Repo) RevParseShort(rev string) (string, error) {
   645  	fullRev, err := r.RevParse(rev)
   646  	if err != nil {
   647  		return "", err
   648  	}
   649  
   650  	return fullRev[:10], nil
   651  }
   652  
   653  // LatestReleaseBranchMergeBaseToLatest tries to discover the start (latest
   654  // v1.x.0 merge base) and end (release-1.(x+1) or DefaultBranch) revision inside the
   655  // repository.
   656  func (r *Repo) LatestReleaseBranchMergeBaseToLatest() (DiscoverResult, error) {
   657  	// Find the last non patch version tag, then resolve its revision
   658  	versions, err := r.latestNonPatchFinalVersions()
   659  	if err != nil {
   660  		return DiscoverResult{}, err
   661  	}
   662  	version := versions[0]
   663  	versionTag := util.SemverToTagString(version)
   664  	logrus.Debugf("Latest non patch version %s", versionTag)
   665  
   666  	base, err := r.MergeBase(
   667  		DefaultBranch,
   668  		semverToReleaseBranch(version),
   669  	)
   670  	if err != nil {
   671  		return DiscoverResult{}, err
   672  	}
   673  
   674  	// If a release branch exists for the next version, we use it. Otherwise we
   675  	// fallback to the DefaultBranch.
   676  	end, branch, err := r.releaseBranchOrMainRef(version.Major, version.Minor+1)
   677  	if err != nil {
   678  		return DiscoverResult{}, err
   679  	}
   680  
   681  	return DiscoverResult{
   682  		startSHA: base,
   683  		startRev: versionTag,
   684  		endSHA:   end,
   685  		endRev:   branch,
   686  	}, nil
   687  }
   688  
   689  func (r *Repo) LatestNonPatchFinalToMinor() (DiscoverResult, error) {
   690  	// Find the last non patch version tag, then resolve its revision
   691  	versions, err := r.latestNonPatchFinalVersions()
   692  	if err != nil {
   693  		return DiscoverResult{}, err
   694  	}
   695  	if len(versions) < 2 {
   696  		return DiscoverResult{}, errors.New("unable to find two latest non patch versions")
   697  	}
   698  
   699  	latestVersion := versions[0]
   700  	latestVersionTag := util.SemverToTagString(latestVersion)
   701  	logrus.Debugf("Latest non patch version %s", latestVersionTag)
   702  	end, err := r.RevParseTag(latestVersionTag)
   703  	if err != nil {
   704  		return DiscoverResult{}, err
   705  	}
   706  
   707  	previousVersion := versions[1]
   708  	previousVersionTag := util.SemverToTagString(previousVersion)
   709  	logrus.Debugf("Previous non patch version %s", previousVersionTag)
   710  	start, err := r.RevParseTag(previousVersionTag)
   711  	if err != nil {
   712  		return DiscoverResult{}, err
   713  	}
   714  
   715  	return DiscoverResult{
   716  		startSHA: start,
   717  		startRev: previousVersionTag,
   718  		endSHA:   end,
   719  		endRev:   latestVersionTag,
   720  	}, nil
   721  }
   722  
   723  func (r *Repo) latestNonPatchFinalVersions() ([]semver.Version, error) {
   724  	latestVersions := []semver.Version{}
   725  
   726  	tags, err := r.inner.Tags()
   727  	if err != nil {
   728  		return nil, err
   729  	}
   730  
   731  	_ = tags.ForEach(func(t *plumbing.Reference) error { //nolint: errcheck
   732  		ver, err := util.TagStringToSemver(t.Name().Short())
   733  
   734  		if err == nil {
   735  			// We're searching for the latest, non patch final tag
   736  			if ver.Patch == 0 && len(ver.Pre) == 0 {
   737  				if len(latestVersions) == 0 || ver.GT(latestVersions[0]) {
   738  					latestVersions = append([]semver.Version{ver}, latestVersions...)
   739  				}
   740  			}
   741  		}
   742  		return nil
   743  	})
   744  	if len(latestVersions) == 0 {
   745  		return nil, fmt.Errorf("unable to find latest non patch release")
   746  	}
   747  	return latestVersions, nil
   748  }
   749  
   750  func (r *Repo) releaseBranchOrMainRef(major, minor uint64) (sha, rev string, err error) {
   751  	relBranch := fmt.Sprintf("%s%d.%d", releaseBranchPrefix, major, minor)
   752  	sha, err = r.RevParseTag(relBranch)
   753  	if err == nil {
   754  		logrus.Debugf("Found release branch %s", relBranch)
   755  		return sha, relBranch, nil
   756  	}
   757  
   758  	sha, err = r.RevParseTag(DefaultBranch)
   759  	if err == nil {
   760  		logrus.Debugf("No release branch found, using %s", DefaultBranch)
   761  		return sha, DefaultBranch, nil
   762  	}
   763  
   764  	return "", "", err
   765  }
   766  
   767  // HasBranch checks if a branch exists in the repo
   768  func (r *Repo) HasBranch(branch string) (branchExists bool, err error) {
   769  	logrus.Infof("Verifying %s branch exists in the repo", branch)
   770  
   771  	branches, err := r.inner.Branches()
   772  	if err != nil {
   773  		return branchExists, fmt.Errorf("getting branches from repository: %w", err)
   774  	}
   775  
   776  	branchExists = false
   777  	if err := branches.ForEach(func(ref *plumbing.Reference) error {
   778  		if ref.Name().Short() == branch {
   779  			logrus.Infof("Branch %s found in the repository", branch)
   780  			branchExists = true
   781  		}
   782  		return nil
   783  	}); err != nil {
   784  		return branchExists, fmt.Errorf("iterating branches to check for existence: %w", err)
   785  	}
   786  	return branchExists, nil
   787  }
   788  
   789  // HasRemoteBranch takes a branch string and verifies that it exists
   790  // on the default remote
   791  func (r *Repo) HasRemoteBranch(branch string) (branchExists bool, err error) {
   792  	logrus.Infof("Verifying %s branch exists on the remote", branch)
   793  
   794  	branches, err := r.RemoteBranches()
   795  	if err != nil {
   796  		return false, fmt.Errorf("get remote branches: %w", err)
   797  	}
   798  
   799  	for _, remoteBranch := range branches {
   800  		if remoteBranch == branch {
   801  			logrus.Infof("Found branch %s", branch)
   802  			return true, nil
   803  		}
   804  	}
   805  	logrus.Infof("Branch %s not found", branch)
   806  	return false, nil
   807  }
   808  
   809  // RemoteBranches returns a list of all remotely available branches.
   810  func (r *Repo) RemoteBranches() (branches []string, err error) {
   811  	remote, err := r.inner.Remote(DefaultRemote)
   812  	if err != nil {
   813  		return nil, NewNetworkError(err)
   814  	}
   815  	var refs []*plumbing.Reference
   816  	for i := r.maxRetries + 1; i > 0; i-- {
   817  		// We can then use every Remote functions to retrieve wanted information
   818  		refs, err = remote.List(&git.ListOptions{})
   819  		if err == nil {
   820  			break
   821  		}
   822  		logrus.Warn("Could not list references on the remote repository.")
   823  		// Convert to network error to see if we can retry the push
   824  		err = NewNetworkError(err)
   825  		if !err.(NetworkError).CanRetry() || r.maxRetries == 0 || i == 1 {
   826  			return nil, err
   827  		}
   828  		waitTime := math.Pow(2, float64(r.maxRetries-i))
   829  		logrus.Errorf(
   830  			"Error listing remote references (will retry %d more times in %.0f secs): %s",
   831  			i-1, waitTime, err.Error(),
   832  		)
   833  		time.Sleep(time.Duration(waitTime) * time.Second)
   834  	}
   835  
   836  	for _, ref := range refs {
   837  		if ref.Name().IsBranch() {
   838  			branches = append(branches, ref.Name().Short())
   839  		}
   840  	}
   841  	return branches, nil
   842  }
   843  
   844  // Checkout can be used to checkout any revision inside the repository
   845  func (r *Repo) Checkout(rev string, args ...string) error {
   846  	cmdArgs := append([]string{"checkout", rev}, args...)
   847  	return command.
   848  		NewWithWorkDir(r.Dir(), gitExecutable, cmdArgs...).
   849  		RunSilentSuccess()
   850  }
   851  
   852  // IsReleaseBranch returns true if the provided branch is a Kubernetes release
   853  // branch
   854  func IsReleaseBranch(branch string) bool {
   855  	if !regex.BranchRegex.MatchString(branch) {
   856  		logrus.Warnf("%s is not a release branch", branch)
   857  		return false
   858  	}
   859  
   860  	return true
   861  }
   862  
   863  func (r *Repo) MergeBase(from, to string) (string, error) {
   864  	mainRef := Remotify(from)
   865  	releaseRef := Remotify(to)
   866  
   867  	logrus.Debugf("MainRef: %s, releaseRef: %s", mainRef, releaseRef)
   868  
   869  	commitRevs := []string{mainRef, releaseRef}
   870  	var res []*object.Commit
   871  
   872  	hashes := []*plumbing.Hash{}
   873  	for _, rev := range commitRevs {
   874  		hash, err := r.inner.ResolveRevision(plumbing.Revision(rev))
   875  		if err != nil {
   876  			return "", err
   877  		}
   878  		hashes = append(hashes, hash)
   879  	}
   880  
   881  	commits := []*object.Commit{}
   882  	for _, hash := range hashes {
   883  		commit, err := r.inner.CommitObject(*hash)
   884  		if err != nil {
   885  			return "", err
   886  		}
   887  		commits = append(commits, commit)
   888  	}
   889  
   890  	res, err := commits[0].MergeBase(commits[1])
   891  	if err != nil {
   892  		return "", err
   893  	}
   894  
   895  	if len(res) == 0 {
   896  		return "", fmt.Errorf("could not find a merge base between %s and %s", from, to)
   897  	}
   898  
   899  	mergeBase := res[0].Hash.String()
   900  	logrus.Infof("Merge base is %s", mergeBase)
   901  
   902  	return mergeBase, nil
   903  }
   904  
   905  // Remotify returns the name prepended with the default remote
   906  func Remotify(name string) string {
   907  	split := strings.Split(name, "/")
   908  	if len(split) > 1 {
   909  		return name
   910  	}
   911  	return fmt.Sprintf("%s/%s", DefaultRemote, name)
   912  }
   913  
   914  // Merge does a git merge into the current branch from the provided one
   915  func (r *Repo) Merge(from string) error {
   916  	if err := filterCommand(
   917  		r.Dir(), "merge", "-X", "ours", from,
   918  	).RunSilentSuccess(); err != nil {
   919  		return fmt.Errorf("run git merge: %w", err)
   920  	}
   921  	return nil
   922  }
   923  
   924  // Push does push the specified branch to the default remote, but only if the
   925  // repository is not in dry run mode
   926  func (r *Repo) Push(remoteBranch string) (err error) {
   927  	args := []string{"push"}
   928  	if r.dryRun {
   929  		logrus.Infof("Won't push due to dry run repository")
   930  		args = append(args, "--dry-run")
   931  	}
   932  	args = append(args, DefaultRemote, remoteBranch)
   933  
   934  	for i := r.maxRetries + 1; i > 0; i-- {
   935  		if err = filterCommand(r.Dir(), args...).RunSilentSuccess(); err == nil {
   936  			return nil
   937  		}
   938  		// Convert to network error to see if we can retry the push
   939  		err = NewNetworkError(err)
   940  		if !err.(NetworkError).CanRetry() || r.maxRetries == 0 {
   941  			return err
   942  		}
   943  		waitTime := math.Pow(2, float64(r.maxRetries-i))
   944  		logrus.Errorf(
   945  			"Error pushing %s (will retry %d more times in %.0f secs): %s",
   946  			remoteBranch, i-1, waitTime, err.Error(),
   947  		)
   948  		time.Sleep(time.Duration(waitTime) * time.Second)
   949  	}
   950  	return fmt.Errorf("trying to push %s %d times: %w", remoteBranch, r.maxRetries, err)
   951  }
   952  
   953  // Head retrieves the current repository HEAD as a string
   954  func (r *Repo) Head() (string, error) {
   955  	ref, err := r.inner.Head()
   956  	if err != nil {
   957  		return "", err
   958  	}
   959  	return ref.Hash().String(), nil
   960  }
   961  
   962  // LatestPatchToPatch tries to discover the start (latest v1.x.[x-1]) and
   963  // end (latest v1.x.x) revision inside the repository for the specified release
   964  // branch.
   965  func (r *Repo) LatestPatchToPatch(branch string) (DiscoverResult, error) {
   966  	latestTag, err := r.LatestTagForBranch(branch)
   967  	if err != nil {
   968  		return DiscoverResult{}, err
   969  	}
   970  
   971  	if len(latestTag.Pre) > 0 && latestTag.Patch > 0 {
   972  		latestTag.Patch--
   973  		latestTag.Pre = nil
   974  	}
   975  
   976  	if latestTag.Patch == 0 {
   977  		return DiscoverResult{}, fmt.Errorf(
   978  			"found non-patch version %v as latest tag on branch %s",
   979  			latestTag, branch,
   980  		)
   981  	}
   982  
   983  	prevTag := semver.Version{
   984  		Major: latestTag.Major,
   985  		Minor: latestTag.Minor,
   986  		Patch: latestTag.Patch - 1,
   987  	}
   988  
   989  	logrus.Debugf("Parsing latest tag %s%v", util.TagPrefix, latestTag)
   990  	latestVersionTag := util.SemverToTagString(latestTag)
   991  	end, err := r.RevParseTag(latestVersionTag)
   992  	if err != nil {
   993  		return DiscoverResult{}, fmt.Errorf("parsing version %v: %w", latestTag, err)
   994  	}
   995  
   996  	logrus.Debugf("Parsing previous tag %s%v", util.TagPrefix, prevTag)
   997  	previousVersionTag := util.SemverToTagString(prevTag)
   998  	start, err := r.RevParseTag(previousVersionTag)
   999  	if err != nil {
  1000  		return DiscoverResult{}, fmt.Errorf("parsing previous version %v: %w", prevTag, err)
  1001  	}
  1002  
  1003  	return DiscoverResult{
  1004  		startSHA: start,
  1005  		startRev: previousVersionTag,
  1006  		endSHA:   end,
  1007  		endRev:   latestVersionTag,
  1008  	}, nil
  1009  }
  1010  
  1011  // LatestPatchToLatest tries to discover the start (latest v1.x.x]) and
  1012  // end (release-1.x or DefaultBranch) revision inside the repository for the specified release
  1013  // branch.
  1014  func (r *Repo) LatestPatchToLatest(branch string) (DiscoverResult, error) {
  1015  	latestTag, err := r.LatestTagForBranch(branch)
  1016  	if err != nil {
  1017  		return DiscoverResult{}, err
  1018  	}
  1019  
  1020  	if len(latestTag.Pre) > 0 && latestTag.Patch > 0 {
  1021  		latestTag.Patch--
  1022  		latestTag.Pre = nil
  1023  	}
  1024  
  1025  	logrus.Debugf("Parsing latest tag %s%v", util.TagPrefix, latestTag)
  1026  	latestVersionTag := util.SemverToTagString(latestTag)
  1027  	start, err := r.RevParseTag(latestVersionTag)
  1028  	if err != nil {
  1029  		return DiscoverResult{}, fmt.Errorf("parsing version %v: %w", latestTag, err)
  1030  	}
  1031  
  1032  	// If a release branch exists for the latest version, we use it. Otherwise we
  1033  	// fallback to the DefaultBranch.
  1034  	end, branch, err := r.releaseBranchOrMainRef(latestTag.Major, latestTag.Minor)
  1035  	if err != nil {
  1036  		return DiscoverResult{}, fmt.Errorf("getting release branch for %v: %w", latestTag, err)
  1037  	}
  1038  
  1039  	return DiscoverResult{
  1040  		startSHA: start,
  1041  		startRev: latestVersionTag,
  1042  		endSHA:   end,
  1043  		endRev:   branch,
  1044  	}, nil
  1045  }
  1046  
  1047  // LatestTagForBranch returns the latest available semver tag for a given branch
  1048  func (r *Repo) LatestTagForBranch(branch string) (tag semver.Version, err error) {
  1049  	tags, err := r.TagsForBranch(branch)
  1050  	if err != nil {
  1051  		return tag, err
  1052  	}
  1053  	if len(tags) == 0 {
  1054  		return tag, errors.New("no tags found on branch")
  1055  	}
  1056  
  1057  	tag, err = util.TagStringToSemver(tags[0])
  1058  	if err != nil {
  1059  		return tag, err
  1060  	}
  1061  
  1062  	return tag, nil
  1063  }
  1064  
  1065  // PreviousTag tries to find the previous tag for a provided branch and errors
  1066  // on any failure
  1067  func (r *Repo) PreviousTag(tag, branch string) (string, error) {
  1068  	tags, err := r.TagsForBranch(branch)
  1069  	if err != nil {
  1070  		return "", err
  1071  	}
  1072  
  1073  	idx := -1
  1074  	for i, t := range tags {
  1075  		if t == tag {
  1076  			idx = i
  1077  			break
  1078  		}
  1079  	}
  1080  	if idx == -1 {
  1081  		return "", errors.New("could not find specified tag in branch")
  1082  	}
  1083  	if len(tags) < idx+1 {
  1084  		return "", errors.New("unable to find previous tag")
  1085  	}
  1086  
  1087  	return tags[idx+1], nil
  1088  }
  1089  
  1090  // TagsForBranch returns a list of tags for the provided branch sorted by
  1091  // creation date
  1092  func (r *Repo) TagsForBranch(branch string) (res []string, err error) {
  1093  	previousBranch, err := r.CurrentBranch()
  1094  	if err != nil {
  1095  		return nil, fmt.Errorf("retrieving current branch: %w", err)
  1096  	}
  1097  	if err := r.Checkout(branch); err != nil {
  1098  		return nil, fmt.Errorf("checking out %s: %w", branch, err)
  1099  	}
  1100  	defer func() { err = r.Checkout(previousBranch) }()
  1101  
  1102  	status, err := filterCommand(
  1103  		r.Dir(), "tag", "--sort=creatordate", "--merged",
  1104  	).RunSilentSuccessOutput()
  1105  	if err != nil {
  1106  		return nil, fmt.Errorf("retrieving merged tags for branch %s: %w", branch, err)
  1107  	}
  1108  
  1109  	tags := strings.Fields(status.Output())
  1110  	sort.Sort(sort.Reverse(sort.StringSlice(tags)))
  1111  
  1112  	return tags, nil
  1113  }
  1114  
  1115  // Tags returns a list of tags for the repository.
  1116  func (r *Repo) Tags() (res []string, err error) {
  1117  	tags, err := r.inner.Tags()
  1118  	if err != nil {
  1119  		return nil, fmt.Errorf("get tags: %w", err)
  1120  	}
  1121  	_ = tags.ForEach(func(t *plumbing.Reference) error { //nolint: errcheck
  1122  		res = append(res, t.Name().Short())
  1123  		return nil
  1124  	})
  1125  	return res, nil
  1126  }
  1127  
  1128  // Add adds a file to the staging area of the repo
  1129  func (r *Repo) Add(filename string) error {
  1130  	if err := filterCommand(
  1131  		r.Dir(), "add", filename,
  1132  	).RunSilentSuccess(); err != nil {
  1133  		return fmt.Errorf("adding file %s to repository: %w", filename, err)
  1134  	}
  1135  	return nil
  1136  }
  1137  
  1138  // GetUserName Reads the local user's name from the git configuration
  1139  func GetUserName() (string, error) {
  1140  	// Retrieve username from git
  1141  	userName, err := filterCommand(
  1142  		"", "config", "--get", "user.name",
  1143  	).RunSilentSuccessOutput()
  1144  	if err != nil {
  1145  		return "", fmt.Errorf("reading the user name from git: %w", err)
  1146  	}
  1147  	return userName.OutputTrimNL(), nil
  1148  }
  1149  
  1150  // GetUserEmail reads the user's name from git
  1151  func GetUserEmail() (string, error) {
  1152  	userEmail, err := filterCommand(
  1153  		"", "config", "--get", "user.email",
  1154  	).RunSilentSuccessOutput()
  1155  	if err != nil {
  1156  		return "", fmt.Errorf("reading the user's email from git: %w", err)
  1157  	}
  1158  	return userEmail.OutputTrimNL(), nil
  1159  }
  1160  
  1161  // UserCommit makes a commit using the local user's config as well as adding
  1162  // the Signed-off-by line to the commit message
  1163  func (r *Repo) UserCommit(msg string) error {
  1164  	// Retrieve username and mail
  1165  	userName, err := GetUserName()
  1166  	if err != nil {
  1167  		return fmt.Errorf("getting the user's name: %w", err)
  1168  	}
  1169  
  1170  	userEmail, err := GetUserEmail()
  1171  	if err != nil {
  1172  		return fmt.Errorf("getting the user's email: %w", err)
  1173  	}
  1174  
  1175  	// Add signed-off-by line
  1176  	msg += fmt.Sprintf("\n\nSigned-off-by: %s <%s>", userName, userEmail)
  1177  
  1178  	if err := r.CommitWithOptions(msg, &git.CommitOptions{
  1179  		Author: &object.Signature{
  1180  			Name:  userName,
  1181  			Email: userEmail,
  1182  			When:  time.Now(),
  1183  		},
  1184  	}); err != nil {
  1185  		return fmt.Errorf("commit changes: %w", err)
  1186  	}
  1187  
  1188  	return nil
  1189  }
  1190  
  1191  // Commit commits the current repository state
  1192  func (r *Repo) Commit(msg string) error {
  1193  	return r.CommitWithOptions(
  1194  		msg,
  1195  		&git.CommitOptions{
  1196  			Author: &object.Signature{
  1197  				Name:  DefaultGitUser,
  1198  				Email: DefaultGitEmail,
  1199  				When:  time.Now(),
  1200  			},
  1201  		},
  1202  	)
  1203  }
  1204  
  1205  // CommitWithOptions commits the current repository state
  1206  func (r *Repo) CommitWithOptions(msg string, options *git.CommitOptions) error {
  1207  	if _, err := r.worktree.Commit(msg, options); err != nil {
  1208  		return err
  1209  	}
  1210  	return nil
  1211  }
  1212  
  1213  // CommitEmpty commits an empty commit into the repository
  1214  func (r *Repo) CommitEmpty(msg string) error {
  1215  	return command.
  1216  		NewWithWorkDir(r.Dir(), gitExecutable,
  1217  			"commit", "--allow-empty", "-m", msg,
  1218  		).
  1219  		RunSilentSuccess()
  1220  }
  1221  
  1222  // Tag creates a new annotated tag for the provided `name` and `message`.
  1223  func (r *Repo) Tag(name, message string) error {
  1224  	head, err := r.inner.Head()
  1225  	if err != nil {
  1226  		return err
  1227  	}
  1228  	if _, err := r.inner.CreateTag(
  1229  		name,
  1230  		head.Hash(),
  1231  		&git.CreateTagOptions{
  1232  			Tagger: &object.Signature{
  1233  				Name:  DefaultGitUser,
  1234  				Email: DefaultGitEmail,
  1235  				When:  time.Now(),
  1236  			},
  1237  			Message: message,
  1238  		}); err != nil {
  1239  		return err
  1240  	}
  1241  	return nil
  1242  }
  1243  
  1244  // CurrentBranch returns the current branch of the repository or an error in
  1245  // case of any failure
  1246  func (r *Repo) CurrentBranch() (branch string, err error) {
  1247  	branches, err := r.inner.Branches()
  1248  	if err != nil {
  1249  		return "", err
  1250  	}
  1251  
  1252  	head, err := r.inner.Head()
  1253  	if err != nil {
  1254  		return "", err
  1255  	}
  1256  
  1257  	if err := branches.ForEach(func(ref *plumbing.Reference) error {
  1258  		if ref.Hash() == head.Hash() {
  1259  			branch = ref.Name().Short()
  1260  			return nil
  1261  		}
  1262  
  1263  		return nil
  1264  	}); err != nil {
  1265  		return "", err
  1266  	}
  1267  
  1268  	return branch, nil
  1269  }
  1270  
  1271  // Rm removes files from the repository
  1272  func (r *Repo) Rm(force bool, files ...string) error {
  1273  	args := []string{"rm"}
  1274  	if force {
  1275  		args = append(args, "-f")
  1276  	}
  1277  	args = append(args, files...)
  1278  
  1279  	return command.
  1280  		NewWithWorkDir(r.Dir(), gitExecutable, args...).
  1281  		RunSilentSuccess()
  1282  }
  1283  
  1284  // Remotes lists the currently available remotes for the repository
  1285  func (r *Repo) Remotes() (res []*Remote, err error) {
  1286  	remotes, err := r.inner.Remotes()
  1287  	if err != nil {
  1288  		return nil, fmt.Errorf("unable to list remotes: %w", err)
  1289  	}
  1290  
  1291  	// Sort the remotes by their name which is not always the case
  1292  	sort.Slice(remotes, func(i, j int) bool {
  1293  		return remotes[i].Config().Name < remotes[j].Config().Name
  1294  	})
  1295  
  1296  	for _, remote := range remotes {
  1297  		cfg := remote.Config()
  1298  		res = append(res, &Remote{name: cfg.Name, urls: cfg.URLs})
  1299  	}
  1300  
  1301  	return res, nil
  1302  }
  1303  
  1304  // HasRemote checks if the provided remote `name` is available and matches the
  1305  // expected `url`
  1306  func (r *Repo) HasRemote(name, expectedURL string) bool {
  1307  	remotes, err := r.Remotes()
  1308  	if err != nil {
  1309  		logrus.Warnf("Unable to get repository remotes: %v", err)
  1310  		return false
  1311  	}
  1312  
  1313  	for _, remote := range remotes {
  1314  		if remote.Name() == name {
  1315  			for _, url := range remote.URLs() {
  1316  				if url == expectedURL {
  1317  					return true
  1318  				}
  1319  			}
  1320  		}
  1321  	}
  1322  
  1323  	return false
  1324  }
  1325  
  1326  // AddRemote adds a new remote to the current working tree
  1327  func (r *Repo) AddRemote(name, owner, repo string, useSSH bool) error {
  1328  	repoURL := GetRepoURL(owner, repo, useSSH)
  1329  	args := []string{"remote", "add", name, repoURL}
  1330  	return command.
  1331  		NewWithWorkDir(r.Dir(), gitExecutable, args...).
  1332  		RunSilentSuccess()
  1333  }
  1334  
  1335  // PushToRemote push the current branch to a specified remote, but only if the
  1336  // repository is not in dry run mode
  1337  func (r *Repo) PushToRemote(remote, remoteBranch string) error {
  1338  	args := []string{"push", "--set-upstream"}
  1339  	if r.dryRun {
  1340  		logrus.Infof("Won't push due to dry run repository")
  1341  		args = append(args, "--dry-run")
  1342  	}
  1343  	args = append(args, remote, remoteBranch)
  1344  
  1345  	return filterCommand(r.Dir(), args...).RunSuccess()
  1346  }
  1347  
  1348  // PushToRemote push the current branch to a specified remote, but only if the
  1349  // repository is not in dry run mode
  1350  func (r *Repo) PushToRemoteWithOptions(pushOptions *git.PushOptions) error {
  1351  	return r.inner.Push(pushOptions)
  1352  }
  1353  
  1354  // LsRemote can be used to run `git ls-remote` with the provided args on the
  1355  // repository
  1356  func (r *Repo) LsRemote(args ...string) (output string, err error) {
  1357  	for i := r.maxRetries + 1; i > 0; i-- {
  1358  		params := []string{}
  1359  		params = append(params, "--")
  1360  		params = append(params, args...)
  1361  		output, err = r.runGitCmd("ls-remote", params...)
  1362  		if err == nil {
  1363  			return output, nil
  1364  		}
  1365  		err = NewNetworkError(err)
  1366  		if !err.(NetworkError).CanRetry() || r.maxRetries == 0 || i == 1 {
  1367  			return "", err
  1368  		}
  1369  
  1370  		waitTime := math.Pow(2, float64(r.maxRetries-i))
  1371  		logrus.Errorf(
  1372  			"Executing ls-remote (will retry %d more times in %.0f secs): %s",
  1373  			i-1, waitTime, err.Error(),
  1374  		)
  1375  		time.Sleep(time.Duration(waitTime) * time.Second)
  1376  	}
  1377  	return "", err
  1378  }
  1379  
  1380  // Branch can be used to run `git branch` with the provided args on the
  1381  // repository
  1382  func (r *Repo) Branch(args ...string) (string, error) {
  1383  	return r.runGitCmd("branch", args...)
  1384  }
  1385  
  1386  // runGitCmd runs the provided command in the repository root and appends the
  1387  // args. The command will run silently and return the captured output or an
  1388  // error in case of any failure.
  1389  func (r *Repo) runGitCmd(cmd string, args ...string) (string, error) {
  1390  	cmdArgs := append([]string{cmd}, args...)
  1391  	res, err := filterCommand(r.Dir(), cmdArgs...).RunSilentSuccessOutput()
  1392  	if err != nil {
  1393  		return "", fmt.Errorf("running git %s: %w", cmd, err)
  1394  	}
  1395  	return res.OutputTrimNL(), nil
  1396  }
  1397  
  1398  // IsDirty returns true if the worktree status is not clean. It can also error
  1399  // if the worktree status is not retrievable.
  1400  func (r *Repo) IsDirty() (bool, error) {
  1401  	status, err := r.Status()
  1402  	if err != nil {
  1403  		return false, fmt.Errorf("retrieving worktree status: %w", err)
  1404  	}
  1405  	return !status.IsClean(), nil
  1406  }
  1407  
  1408  // RemoteTags return the tags that currently exist in the
  1409  func (r *Repo) RemoteTags() (tags []string, err error) {
  1410  	logrus.Debug("Listing remote tags with ls-remote")
  1411  	output, err := r.LsRemote(DefaultRemote)
  1412  	if err != nil {
  1413  		return tags, fmt.Errorf("while listing tags using ls-remote: %w", err)
  1414  	}
  1415  	const gitTagPreRef = "refs/tags/"
  1416  	tags = make([]string, 0)
  1417  	scanner := bufio.NewScanner(strings.NewReader(output))
  1418  	scanner.Split(bufio.ScanWords)
  1419  	for scanner.Scan() {
  1420  		if strings.HasPrefix(scanner.Text(), gitTagPreRef) {
  1421  			tags = append(tags, strings.TrimPrefix(scanner.Text(), gitTagPreRef))
  1422  		}
  1423  	}
  1424  	logrus.Debugf("Remote repository contains %d tags", len(tags))
  1425  	return tags, nil
  1426  }
  1427  
  1428  // HasRemoteTag Checks if the default remote already has a tag
  1429  func (r *Repo) HasRemoteTag(tag string) (hasTag bool, err error) {
  1430  	remoteTags, err := r.RemoteTags()
  1431  	if err != nil {
  1432  		return hasTag, fmt.Errorf("getting tags to check if tag exists: %w", err)
  1433  	}
  1434  	for _, remoteTag := range remoteTags {
  1435  		if tag == remoteTag {
  1436  			logrus.Infof("Tag %s found in default remote", tag)
  1437  			return true, nil
  1438  		}
  1439  	}
  1440  	return false, nil
  1441  }
  1442  
  1443  // SetURL can be used to overwrite the URL for a remote
  1444  func (r *Repo) SetURL(remote, newURL string) error {
  1445  	if err := r.inner.DeleteRemote(remote); err != nil {
  1446  		return fmt.Errorf("delete remote: %w", err)
  1447  	}
  1448  	if _, err := r.inner.CreateRemote(&config.RemoteConfig{
  1449  		Name: remote,
  1450  		URLs: []string{newURL},
  1451  	}); err != nil {
  1452  		return fmt.Errorf("create remote: %w", err)
  1453  	}
  1454  	return nil
  1455  }
  1456  
  1457  // Status reads and returns the Status object from the repository
  1458  func (r *Repo) Status() (*git.Status, error) {
  1459  	status, err := r.worktree.Status()
  1460  	if err != nil {
  1461  		return nil, fmt.Errorf("getting the repository status: %w", err)
  1462  	}
  1463  	return &status, nil
  1464  }
  1465  
  1466  // ShowLastCommit is a simple function that runs git show and returns the
  1467  // last commit in the log
  1468  func (r *Repo) ShowLastCommit() (logData string, err error) {
  1469  	logData, err = r.runGitCmd("show")
  1470  	if err != nil {
  1471  		return logData, fmt.Errorf("getting last commit log: %w", err)
  1472  	}
  1473  	return logData, nil
  1474  }
  1475  
  1476  // LastCommitSha returns the sha of the last commit in the repository
  1477  func (r *Repo) LastCommitSha() (string, error) {
  1478  	shaval, err := r.runGitCmd("log", "--pretty=format:'%H'", "-n1")
  1479  	if err != nil {
  1480  		return "", fmt.Errorf("trying to retrieve the last commit sha: %w", err)
  1481  	}
  1482  	return shaval, nil
  1483  }
  1484  
  1485  // FetchRemote gets the objects from the specified remote. It returns true as
  1486  // first argument if something has been fetched remotely.
  1487  func (r *Repo) FetchRemote(remoteName string) (bool, error) {
  1488  	if remoteName == "" {
  1489  		return false, errors.New("error fetching, remote repository name is empty")
  1490  	}
  1491  	// Verify the remote exists
  1492  	remotes, err := r.Remotes()
  1493  	if err != nil {
  1494  		return false, fmt.Errorf("getting repository remotes: %w", err)
  1495  	}
  1496  
  1497  	remoteExists := false
  1498  	for _, remote := range remotes {
  1499  		if remote.Name() == remoteName {
  1500  			remoteExists = true
  1501  			break
  1502  		}
  1503  	}
  1504  	if !remoteExists {
  1505  		return false, errors.New("cannot fetch repository, the specified remote does not exist")
  1506  	}
  1507  
  1508  	res, err := filterCommand(r.Dir(), "fetch", remoteName).RunSilentSuccessOutput()
  1509  	if err != nil {
  1510  		return false, fmt.Errorf("fetching objects from %s: %w", remoteName, err)
  1511  	}
  1512  	// git fetch outputs on stderr
  1513  	output := strings.TrimSpace(res.Error())
  1514  	logrus.Debugf("Fetch result: %s", output)
  1515  	return output != "", nil
  1516  }
  1517  
  1518  // Rebase calls rebase on the current repo to the specified branch
  1519  func (r *Repo) Rebase(branch string) error {
  1520  	if branch == "" {
  1521  		return errors.New("cannot rebase repository, branch is empty")
  1522  	}
  1523  	logrus.Infof("Rebasing repository to %s", branch)
  1524  	_, err := r.runGitCmd("rebase", branch)
  1525  	// If we get an error, try to interpret it to make more sense
  1526  	if err != nil {
  1527  		return fmt.Errorf("rebasing repository: %w", err)
  1528  	}
  1529  	return nil
  1530  }
  1531  
  1532  // ParseRepoSlug parses a repository string and return the organization and repository name/
  1533  func ParseRepoSlug(repoSlug string) (org, repo string, err error) {
  1534  	match, err := regexp.MatchString(`(?i)^[a-z0-9-/]+$`, repoSlug)
  1535  	if err != nil {
  1536  		return "", "", fmt.Errorf("checking repository slug: %w", err)
  1537  	}
  1538  	if !match {
  1539  		return "", "", errors.New("repository slug contains invalid characters")
  1540  	}
  1541  
  1542  	parts := strings.Split(repoSlug, "/")
  1543  	if len(parts) > 2 {
  1544  		return "", "", errors.New("string is not a well formed org/repo slug")
  1545  	}
  1546  	org = parts[0]
  1547  	if len(parts) > 1 {
  1548  		repo = parts[1]
  1549  	}
  1550  	return org, repo, nil
  1551  }
  1552  
  1553  // NewNetworkError creates a new NetworkError
  1554  func NewNetworkError(err error) NetworkError {
  1555  	gerror := NetworkError{
  1556  		error: err,
  1557  	}
  1558  	return gerror
  1559  }
  1560  
  1561  // NetworkError is a wrapper for the error class
  1562  type NetworkError struct {
  1563  	error
  1564  }
  1565  
  1566  // CanRetry tells if an error can be retried
  1567  func (e NetworkError) CanRetry() bool {
  1568  	// We consider these strings as part of errors we can retry
  1569  	retryMessages := []string{
  1570  		"dial tcp", "read udp", "connection refused",
  1571  		"ssh: connect to host", "Could not read from remote",
  1572  	}
  1573  
  1574  	// If any of them are in the error message, we consider it temporary
  1575  	for _, message := range retryMessages {
  1576  		if strings.Contains(e.Error(), message) {
  1577  			return true
  1578  		}
  1579  	}
  1580  
  1581  	// Otherwise permanent
  1582  	return false
  1583  }
  1584  
  1585  // LatestReleaseBranch determines the latest release-x.y branch of the repo.
  1586  func (r *Repo) LatestReleaseBranch() (string, error) {
  1587  	branches, err := r.RemoteBranches()
  1588  	if err != nil {
  1589  		return "", fmt.Errorf("get remote branches: %w", err)
  1590  	}
  1591  
  1592  	var latest semver.Version
  1593  	for _, branch := range branches {
  1594  		if strings.HasPrefix(branch, releaseBranchPrefix) {
  1595  			version := strings.TrimPrefix(branch, releaseBranchPrefix) + ".0"
  1596  
  1597  			parsed, err := semver.Parse(version)
  1598  			if err != nil {
  1599  				logrus.Debugf("Unable to parse semver for %s: %v", version, err)
  1600  				continue
  1601  			}
  1602  
  1603  			if parsed.GT(latest) {
  1604  				latest = parsed
  1605  			}
  1606  		}
  1607  	}
  1608  
  1609  	if latest.EQ(semver.Version{}) {
  1610  		return "", errors.New("no latest release branch found")
  1611  	}
  1612  
  1613  	return semverToReleaseBranch(latest), nil
  1614  }
  1615  
  1616  func semverToReleaseBranch(v semver.Version) string {
  1617  	return fmt.Sprintf("%s%d.%d", releaseBranchPrefix, v.Major, v.Minor)
  1618  }