github.com/argoproj/argo-cd/v3@v3.2.1/util/git/client.go (about)

     1  package git
     2  
     3  import (
     4  	"bufio"
     5  	"crypto/tls"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"math"
    10  	"net/http"
    11  	"net/mail"
    12  	"net/url"
    13  	"os"
    14  	"os/exec"
    15  	"path/filepath"
    16  	"regexp"
    17  	"sort"
    18  	"strconv"
    19  	"strings"
    20  	"syscall"
    21  	"time"
    22  	"unicode/utf8"
    23  
    24  	"github.com/bmatcuk/doublestar/v4"
    25  	"github.com/go-git/go-git/v5"
    26  	"github.com/go-git/go-git/v5/config"
    27  	"github.com/go-git/go-git/v5/plumbing"
    28  	"github.com/go-git/go-git/v5/plumbing/transport"
    29  	githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
    30  	"github.com/go-git/go-git/v5/storage/memory"
    31  	"github.com/google/uuid"
    32  	log "github.com/sirupsen/logrus"
    33  	"golang.org/x/crypto/ssh"
    34  	"golang.org/x/crypto/ssh/knownhosts"
    35  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    36  	utilnet "k8s.io/apimachinery/pkg/util/net"
    37  
    38  	"github.com/argoproj/argo-cd/v3/common"
    39  	certutil "github.com/argoproj/argo-cd/v3/util/cert"
    40  	"github.com/argoproj/argo-cd/v3/util/env"
    41  	executil "github.com/argoproj/argo-cd/v3/util/exec"
    42  	"github.com/argoproj/argo-cd/v3/util/proxy"
    43  	"github.com/argoproj/argo-cd/v3/util/versions"
    44  )
    45  
    46  var ErrInvalidRepoURL = errors.New("repo URL is invalid")
    47  
    48  // builtinGitConfig configuration contains statements that are needed
    49  // for correct ArgoCD operation. These settings will override any
    50  // user-provided configuration of same options.
    51  var builtinGitConfig = map[string]string{
    52  	"maintenance.autoDetach": "false",
    53  	"gc.autoDetach":          "false",
    54  }
    55  
    56  // BuiltinGitConfigEnv contains builtin git configuration in the
    57  // format acceptable by Git.
    58  var BuiltinGitConfigEnv []string
    59  
    60  // CommitMetadata contains metadata about a commit that is related in some way to another commit.
    61  type CommitMetadata struct {
    62  	// Author is the author of the commit.
    63  	// Comes from the Argocd-reference-commit-author trailer.
    64  	Author mail.Address
    65  	// Date is the date of the commit, formatted as by `git show -s --format=%aI`.
    66  	// May be an empty string if the date is unknown.
    67  	// Comes from the Argocd-reference-commit-date trailer.
    68  	Date string
    69  	// Subject is the commit message subject, i.e. `git show -s --format=%s`.
    70  	// Comes from the Argocd-reference-commit-subject trailer.
    71  	Subject string
    72  	// Body is the commit message body, excluding the subject, i.e. `git show -s --format=%b`.
    73  	// Comes from the Argocd-reference-commit-body trailer.
    74  	Body string
    75  	// SHA is the commit hash.
    76  	// Comes from the Argocd-reference-commit-sha trailer.
    77  	SHA string
    78  	// RepoURL is the URL of the repository where the commit is located.
    79  	// Comes from the Argocd-reference-commit-repourl trailer.
    80  	// This value is not validated beyond confirming that it's a URL, and it should not be used to construct UI links
    81  	// unless it is properly validated and/or sanitized first.
    82  	RepoURL string
    83  }
    84  
    85  // RevisionReference contains a reference to a some information that is related in some way to another commit. For now,
    86  // it supports only references to a commit. In the future, it may support other types of references.
    87  type RevisionReference struct {
    88  	// Commit contains metadata about the commit that is related in some way to another commit.
    89  	Commit *CommitMetadata
    90  }
    91  
    92  type RevisionMetadata struct {
    93  	// Author is the author of the commit. Corresponds to the output of `git log -n 1 --pretty='format:%an <%ae>'`.
    94  	Author string
    95  	// Date is the date of the commit. Corresponds to the output of `git log -n 1 --pretty='format:%ad'`.
    96  	Date time.Time
    97  	Tags []string
    98  	// Message is the commit message.
    99  	Message string
   100  	// References contains metadata about information that is related in some way to this commit. This data comes from
   101  	// git commit trailers starting with "Argocd-reference-". We currently only support a single reference to a commit,
   102  	// but we return an array to allow for future expansion.
   103  	References []RevisionReference
   104  }
   105  
   106  // this should match reposerver/repository/repository.proto/RefsList
   107  type Refs struct {
   108  	Branches []string
   109  	Tags     []string
   110  	// heads and remotes are also refs, but are not needed at this time.
   111  }
   112  
   113  type gitRefCache interface {
   114  	SetGitReferences(repo string, references []*plumbing.Reference) error
   115  	GetOrLockGitReferences(repo string, lockId string, references *[]*plumbing.Reference) (string, error)
   116  	UnlockGitReferences(repo string, lockId string) error
   117  }
   118  
   119  // Client is a generic git client interface
   120  type Client interface {
   121  	Root() string
   122  	Init() error
   123  	Fetch(revision string) error
   124  	Submodule() error
   125  	Checkout(revision string, submoduleEnabled bool) (string, error)
   126  	LsRefs() (*Refs, error)
   127  	LsRemote(revision string) (string, error)
   128  	LsFiles(path string, enableNewGitFileGlobbing bool) ([]string, error)
   129  	LsLargeFiles() ([]string, error)
   130  	CommitSHA() (string, error)
   131  	RevisionMetadata(revision string) (*RevisionMetadata, error)
   132  	VerifyCommitSignature(string) (string, error)
   133  	IsAnnotatedTag(string) bool
   134  	ChangedFiles(revision string, targetRevision string) ([]string, error)
   135  	IsRevisionPresent(revision string) bool
   136  	// SetAuthor sets the author name and email in the git configuration.
   137  	SetAuthor(name, email string) (string, error)
   138  	// CheckoutOrOrphan checks out the branch. If the branch does not exist, it creates an orphan branch.
   139  	CheckoutOrOrphan(branch string, submoduleEnabled bool) (string, error)
   140  	// CheckoutOrNew checks out the given branch. If the branch does not exist, it creates an empty branch based on
   141  	// the base branch.
   142  	CheckoutOrNew(branch, base string, submoduleEnabled bool) (string, error)
   143  	// RemoveContents removes all files from the given paths in the git repository.
   144  	RemoveContents(paths []string) (string, error)
   145  	// CommitAndPush commits and pushes changes to the target branch.
   146  	CommitAndPush(branch, message string) (string, error)
   147  }
   148  
   149  type EventHandlers struct {
   150  	OnLsRemote func(repo string) func()
   151  	OnFetch    func(repo string) func()
   152  	OnPush     func(repo string) func()
   153  }
   154  
   155  // nativeGitClient implements Client interface using git CLI
   156  type nativeGitClient struct {
   157  	EventHandlers
   158  
   159  	// URL of the repository
   160  	repoURL string
   161  	// Root path of repository
   162  	root string
   163  	// Authenticator credentials for private repositories
   164  	creds Creds
   165  	// Whether to connect insecurely to repository, e.g. don't verify certificate
   166  	insecure bool
   167  	// Whether the repository is LFS enabled
   168  	enableLfs bool
   169  	// gitRefCache knows how to cache git refs
   170  	gitRefCache gitRefCache
   171  	// indicates if client allowed to load refs from cache
   172  	loadRefFromCache bool
   173  	// HTTP/HTTPS proxy used to access repository
   174  	proxy string
   175  	// list of targets that shouldn't use the proxy, applies only if the proxy is set
   176  	noProxy string
   177  	// git configuration environment variables
   178  	gitConfigEnv []string
   179  }
   180  
   181  type runOpts struct {
   182  	SkipErrorLogging bool
   183  	CaptureStderr    bool
   184  }
   185  
   186  var (
   187  	maxAttemptsCount = 1
   188  	maxRetryDuration time.Duration
   189  	retryDuration    time.Duration
   190  	factor           int64
   191  )
   192  
   193  func init() {
   194  	if countStr := os.Getenv(common.EnvGitAttemptsCount); countStr != "" {
   195  		cnt, err := strconv.Atoi(countStr)
   196  		if err != nil {
   197  			panic(fmt.Sprintf("Invalid value in %s env variable: %v", common.EnvGitAttemptsCount, err))
   198  		}
   199  		maxAttemptsCount = int(math.Max(float64(cnt), 1))
   200  	}
   201  
   202  	maxRetryDuration = env.ParseDurationFromEnv(common.EnvGitRetryMaxDuration, common.DefaultGitRetryMaxDuration, 0, math.MaxInt64)
   203  	retryDuration = env.ParseDurationFromEnv(common.EnvGitRetryDuration, common.DefaultGitRetryDuration, 0, math.MaxInt64)
   204  	factor = env.ParseInt64FromEnv(common.EnvGitRetryFactor, common.DefaultGitRetryFactor, 0, math.MaxInt64)
   205  
   206  	BuiltinGitConfigEnv = append(BuiltinGitConfigEnv, fmt.Sprintf("GIT_CONFIG_COUNT=%d", len(builtinGitConfig)))
   207  	idx := 0
   208  	for k, v := range builtinGitConfig {
   209  		BuiltinGitConfigEnv = append(BuiltinGitConfigEnv, fmt.Sprintf("GIT_CONFIG_KEY_%d=%s", idx, k))
   210  		BuiltinGitConfigEnv = append(BuiltinGitConfigEnv, fmt.Sprintf("GIT_CONFIG_VALUE_%d=%s", idx, v))
   211  		idx++
   212  	}
   213  }
   214  
   215  type ClientOpts func(c *nativeGitClient)
   216  
   217  // WithCache sets git revisions cacher as well as specifies if client should tries to use cached resolved revision
   218  func WithCache(cache gitRefCache, loadRefFromCache bool) ClientOpts {
   219  	return func(c *nativeGitClient) {
   220  		c.gitRefCache = cache
   221  		c.loadRefFromCache = loadRefFromCache
   222  	}
   223  }
   224  
   225  func WithBuiltinGitConfig(enable bool) ClientOpts {
   226  	return func(c *nativeGitClient) {
   227  		if enable {
   228  			c.gitConfigEnv = BuiltinGitConfigEnv
   229  		} else {
   230  			c.gitConfigEnv = nil
   231  		}
   232  	}
   233  }
   234  
   235  // WithEventHandlers sets the git client event handlers
   236  func WithEventHandlers(handlers EventHandlers) ClientOpts {
   237  	return func(c *nativeGitClient) {
   238  		c.EventHandlers = handlers
   239  	}
   240  }
   241  
   242  func NewClient(rawRepoURL string, creds Creds, insecure bool, enableLfs bool, proxy string, noProxy string, opts ...ClientOpts) (Client, error) {
   243  	r := regexp.MustCompile(`([/:])`)
   244  	normalizedGitURL := NormalizeGitURL(rawRepoURL)
   245  	if normalizedGitURL == "" {
   246  		return nil, fmt.Errorf("repository %q cannot be initialized: %w", rawRepoURL, ErrInvalidRepoURL)
   247  	}
   248  	root := filepath.Join(os.TempDir(), r.ReplaceAllString(normalizedGitURL, "_"))
   249  	if root == os.TempDir() {
   250  		return nil, fmt.Errorf("repository %q cannot be initialized, because its root would be system temp at %s", rawRepoURL, root)
   251  	}
   252  	return NewClientExt(rawRepoURL, root, creds, insecure, enableLfs, proxy, noProxy, opts...)
   253  }
   254  
   255  func NewClientExt(rawRepoURL string, root string, creds Creds, insecure bool, enableLfs bool, proxy string, noProxy string, opts ...ClientOpts) (Client, error) {
   256  	client := &nativeGitClient{
   257  		repoURL:      rawRepoURL,
   258  		root:         root,
   259  		creds:        creds,
   260  		insecure:     insecure,
   261  		enableLfs:    enableLfs,
   262  		proxy:        proxy,
   263  		noProxy:      noProxy,
   264  		gitConfigEnv: BuiltinGitConfigEnv,
   265  	}
   266  	for i := range opts {
   267  		opts[i](client)
   268  	}
   269  	return client, nil
   270  }
   271  
   272  var gitClientTimeout = env.ParseDurationFromEnv("ARGOCD_GIT_REQUEST_TIMEOUT", 15*time.Second, 0, math.MaxInt64)
   273  
   274  // Returns a HTTP client object suitable for go-git to use using the following
   275  // pattern:
   276  //   - If insecure is true, always returns a client with certificate verification
   277  //     turned off.
   278  //   - If one or more custom certificates are stored for the repository, returns
   279  //     a client with those certificates in the list of root CAs used to verify
   280  //     the server's certificate.
   281  //   - Otherwise (and on non-fatal errors), a default HTTP client is returned.
   282  func GetRepoHTTPClient(repoURL string, insecure bool, creds Creds, proxyURL string, noProxy string) *http.Client {
   283  	// Default HTTP client
   284  	customHTTPClient := &http.Client{
   285  		// 15 second timeout by default
   286  		Timeout: gitClientTimeout,
   287  		// don't follow redirect
   288  		CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
   289  			return http.ErrUseLastResponse
   290  		},
   291  	}
   292  
   293  	proxyFunc := proxy.GetCallback(proxyURL, noProxy)
   294  
   295  	// Callback function to return any configured client certificate
   296  	// We never return err, but an empty cert instead.
   297  	clientCertFunc := func(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) {
   298  		var err error
   299  		cert := tls.Certificate{}
   300  
   301  		// If we aren't called with GenericHTTPSCreds, then we just return an empty cert
   302  		httpsCreds, ok := creds.(GenericHTTPSCreds)
   303  		if !ok {
   304  			return &cert, nil
   305  		}
   306  
   307  		// If the creds contain client certificate data, we return a TLS.Certificate
   308  		// populated with the cert and its key.
   309  		if httpsCreds.HasClientCert() {
   310  			cert, err = tls.X509KeyPair([]byte(httpsCreds.GetClientCertData()), []byte(httpsCreds.GetClientCertKey()))
   311  			if err != nil {
   312  				log.Errorf("Could not load Client Certificate: %v", err)
   313  				return &cert, nil
   314  			}
   315  		}
   316  
   317  		return &cert, nil
   318  	}
   319  	transport := &http.Transport{
   320  		Proxy: proxyFunc,
   321  		TLSClientConfig: &tls.Config{
   322  			GetClientCertificate: clientCertFunc,
   323  		},
   324  		DisableKeepAlives: true,
   325  	}
   326  	customHTTPClient.Transport = transport
   327  	if insecure {
   328  		transport.TLSClientConfig.InsecureSkipVerify = true
   329  		return customHTTPClient
   330  	}
   331  	parsedURL, err := url.Parse(repoURL)
   332  	if err != nil {
   333  		return customHTTPClient
   334  	}
   335  	serverCertificatePem, err := certutil.GetCertificateForConnect(parsedURL.Host)
   336  	if err != nil {
   337  		return customHTTPClient
   338  	}
   339  	if len(serverCertificatePem) > 0 {
   340  		certPool := certutil.GetCertPoolFromPEMData(serverCertificatePem)
   341  		transport.TLSClientConfig.RootCAs = certPool
   342  	}
   343  	return customHTTPClient
   344  }
   345  
   346  func newAuth(repoURL string, creds Creds) (transport.AuthMethod, error) {
   347  	switch creds := creds.(type) {
   348  	case SSHCreds:
   349  		var sshUser string
   350  		if isSSH, user := IsSSHURL(repoURL); isSSH {
   351  			sshUser = user
   352  		}
   353  		signer, err := ssh.ParsePrivateKey([]byte(creds.sshPrivateKey))
   354  		if err != nil {
   355  			return nil, err
   356  		}
   357  		auth := &PublicKeysWithOptions{}
   358  		auth.User = sshUser
   359  		auth.Signer = signer
   360  		if creds.insecure {
   361  			auth.HostKeyCallback = ssh.InsecureIgnoreHostKey()
   362  		} else {
   363  			// Set up validation of SSH known hosts for using our ssh_known_hosts
   364  			// file.
   365  			auth.HostKeyCallback, err = knownhosts.New(certutil.GetSSHKnownHostsDataPath())
   366  			if err != nil {
   367  				log.Errorf("Could not set-up SSH known hosts callback: %v", err)
   368  			}
   369  		}
   370  		return auth, nil
   371  	case HTTPSCreds:
   372  		if creds.bearerToken != "" {
   373  			return &githttp.TokenAuth{Token: creds.bearerToken}, nil
   374  		}
   375  		auth := githttp.BasicAuth{Username: creds.username, Password: creds.password}
   376  		if auth.Username == "" {
   377  			auth.Username = "x-access-token"
   378  		}
   379  		return &auth, nil
   380  	case GitHubAppCreds:
   381  		token, err := creds.getAccessToken()
   382  		if err != nil {
   383  			return nil, err
   384  		}
   385  		auth := githttp.BasicAuth{Username: "x-access-token", Password: token}
   386  		return &auth, nil
   387  	case GoogleCloudCreds:
   388  		username, err := creds.getUsername()
   389  		if err != nil {
   390  			return nil, fmt.Errorf("failed to get username from creds: %w", err)
   391  		}
   392  		token, err := creds.getAccessToken()
   393  		if err != nil {
   394  			return nil, fmt.Errorf("failed to get access token from creds: %w", err)
   395  		}
   396  
   397  		auth := githttp.BasicAuth{Username: username, Password: token}
   398  		return &auth, nil
   399  	case AzureWorkloadIdentityCreds:
   400  		token, err := creds.GetAzureDevOpsAccessToken()
   401  		if err != nil {
   402  			return nil, fmt.Errorf("failed to get access token from creds: %w", err)
   403  		}
   404  
   405  		auth := githttp.TokenAuth{Token: token}
   406  		return &auth, nil
   407  	}
   408  
   409  	return nil, nil
   410  }
   411  
   412  func (m *nativeGitClient) Root() string {
   413  	return m.root
   414  }
   415  
   416  // Init initializes a local git repository and sets the remote origin
   417  func (m *nativeGitClient) Init() error {
   418  	_, err := git.PlainOpen(m.root)
   419  	if err == nil {
   420  		return nil
   421  	}
   422  	if !errors.Is(err, git.ErrRepositoryNotExists) {
   423  		return err
   424  	}
   425  	log.Infof("Initializing %s to %s", m.repoURL, m.root)
   426  	err = os.RemoveAll(m.root)
   427  	if err != nil {
   428  		return fmt.Errorf("unable to clean repo at %s: %w", m.root, err)
   429  	}
   430  	err = os.MkdirAll(m.root, 0o755)
   431  	if err != nil {
   432  		return err
   433  	}
   434  	repo, err := git.PlainInit(m.root, false)
   435  	if err != nil {
   436  		return err
   437  	}
   438  	_, err = repo.CreateRemote(&config.RemoteConfig{
   439  		Name: git.DefaultRemoteName,
   440  		URLs: []string{m.repoURL},
   441  	})
   442  	return err
   443  }
   444  
   445  // IsLFSEnabled returns true if the repository is LFS enabled
   446  func (m *nativeGitClient) IsLFSEnabled() bool {
   447  	return m.enableLfs
   448  }
   449  
   450  func (m *nativeGitClient) fetch(revision string) error {
   451  	var err error
   452  	if revision != "" {
   453  		err = m.runCredentialedCmd("fetch", "origin", revision, "--tags", "--force", "--prune")
   454  	} else {
   455  		err = m.runCredentialedCmd("fetch", "origin", "--tags", "--force", "--prune")
   456  	}
   457  	return err
   458  }
   459  
   460  // IsRevisionPresent checks to see if the given revision already exists locally.
   461  func (m *nativeGitClient) IsRevisionPresent(revision string) bool {
   462  	if revision == "" {
   463  		return false
   464  	}
   465  
   466  	cmd := exec.Command("git", "cat-file", "-t", revision)
   467  	out, err := m.runCmdOutput(cmd, runOpts{SkipErrorLogging: true})
   468  	if out == "commit" && err == nil {
   469  		return true
   470  	}
   471  	return false
   472  }
   473  
   474  // Fetch fetches latest updates from origin
   475  func (m *nativeGitClient) Fetch(revision string) error {
   476  	if m.OnFetch != nil {
   477  		done := m.OnFetch(m.repoURL)
   478  		defer done()
   479  	}
   480  
   481  	err := m.fetch(revision)
   482  
   483  	// When we have LFS support enabled, check for large files and fetch them too.
   484  	if err == nil && m.IsLFSEnabled() {
   485  		largeFiles, err := m.LsLargeFiles()
   486  		if err == nil && len(largeFiles) > 0 {
   487  			err = m.runCredentialedCmd("lfs", "fetch", "--all")
   488  			if err != nil {
   489  				return err
   490  			}
   491  		}
   492  	}
   493  
   494  	return err
   495  }
   496  
   497  // LsFiles lists the local working tree, including only files that are under source control
   498  func (m *nativeGitClient) LsFiles(path string, enableNewGitFileGlobbing bool) ([]string, error) {
   499  	if enableNewGitFileGlobbing {
   500  		// This is the new way with safer globbing
   501  
   502  		// evaluating the root path for symlinks
   503  		realRoot, err := filepath.EvalSymlinks(m.root)
   504  		if err != nil {
   505  			return nil, err
   506  		}
   507  		// searching for the pattern inside the root path
   508  		allFiles, err := doublestar.FilepathGlob(filepath.Join(realRoot, path))
   509  		if err != nil {
   510  			return nil, err
   511  		}
   512  		var files []string
   513  		for _, file := range allFiles {
   514  			link, err := filepath.EvalSymlinks(file)
   515  			if err != nil {
   516  				return nil, err
   517  			}
   518  			absPath, err := filepath.Abs(link)
   519  			if err != nil {
   520  				return nil, err
   521  			}
   522  
   523  			if strings.HasPrefix(absPath, realRoot) {
   524  				// removing the repository root prefix from the file path
   525  				relativeFile, err := filepath.Rel(realRoot, file)
   526  				if err != nil {
   527  					return nil, err
   528  				}
   529  				files = append(files, relativeFile)
   530  			} else {
   531  				log.Warnf("Absolute path for %s is outside of repository, ignoring it", file)
   532  			}
   533  		}
   534  		return files, nil
   535  	}
   536  	// This is the old and default way
   537  	out, err := m.runCmd("ls-files", "--full-name", "-z", "--", path)
   538  	if err != nil {
   539  		return nil, err
   540  	}
   541  	// remove last element, which is blank regardless of whether we're using nullbyte or newline
   542  	ss := strings.Split(out, "\000")
   543  	return ss[:len(ss)-1], nil
   544  }
   545  
   546  // LsLargeFiles lists all files that have references to LFS storage
   547  func (m *nativeGitClient) LsLargeFiles() ([]string, error) {
   548  	out, err := m.runCmd("lfs", "ls-files", "-n")
   549  	if err != nil {
   550  		return nil, err
   551  	}
   552  	ss := strings.Split(out, "\n")
   553  	return ss, nil
   554  }
   555  
   556  // Submodule embed other repositories into this repository
   557  func (m *nativeGitClient) Submodule() error {
   558  	if err := m.runCredentialedCmd("submodule", "sync", "--recursive"); err != nil {
   559  		return err
   560  	}
   561  	return m.runCredentialedCmd("submodule", "update", "--init", "--recursive")
   562  }
   563  
   564  // Checkout checks out the specified revision
   565  func (m *nativeGitClient) Checkout(revision string, submoduleEnabled bool) (string, error) {
   566  	if revision == "" || revision == "HEAD" {
   567  		revision = "origin/HEAD"
   568  	}
   569  	if out, err := m.runCmd("checkout", "--force", revision); err != nil {
   570  		return out, fmt.Errorf("failed to checkout %s: %w", revision, err)
   571  	}
   572  	// We must populate LFS content by using lfs checkout, if we have at least
   573  	// one LFS reference in the current revision.
   574  	if m.IsLFSEnabled() {
   575  		largeFiles, err := m.LsLargeFiles()
   576  		if err != nil {
   577  			return "", fmt.Errorf("failed to list LFS files: %w", err)
   578  		}
   579  		if len(largeFiles) > 0 {
   580  			if out, err := m.runCmd("lfs", "checkout"); err != nil {
   581  				return out, fmt.Errorf("failed to checkout LFS files: %w", err)
   582  			}
   583  		}
   584  	}
   585  	if _, err := os.Stat(m.root + "/.gitmodules"); !os.IsNotExist(err) {
   586  		if submoduleEnabled {
   587  			if err := m.Submodule(); err != nil {
   588  				return "", fmt.Errorf("failed to update submodules: %w", err)
   589  			}
   590  		}
   591  	}
   592  	// NOTE
   593  	// The double “f” in the arguments is not a typo: the first “f” tells
   594  	// `git clean` to delete untracked files and directories, and the second “f”
   595  	// tells it to clean untracked nested Git repositories (for example a
   596  	// submodule which has since been removed).
   597  	if out, err := m.runCmd("clean", "-ffdx"); err != nil {
   598  		return out, fmt.Errorf("failed to clean: %w", err)
   599  	}
   600  	return "", nil
   601  }
   602  
   603  func (m *nativeGitClient) getRefs() ([]*plumbing.Reference, error) {
   604  	myLockUUID, err := uuid.NewRandom()
   605  	myLockId := ""
   606  	if err != nil {
   607  		log.Debug("Error generating git references cache lock id: ", err)
   608  	} else {
   609  		myLockId = myLockUUID.String()
   610  	}
   611  	// Prevent an additional get call to cache if we know our state isn't stale
   612  	needsUnlock := true
   613  	if m.gitRefCache != nil && m.loadRefFromCache {
   614  		var res []*plumbing.Reference
   615  		foundLockId, err := m.gitRefCache.GetOrLockGitReferences(m.repoURL, myLockId, &res)
   616  		isLockOwner := myLockId == foundLockId
   617  		if !isLockOwner && err == nil {
   618  			// Valid value already in cache
   619  			return res, nil
   620  		} else if !isLockOwner && err != nil {
   621  			// Error getting value from cache
   622  			log.Debugf("Error getting git references from cache: %v", err)
   623  			return nil, err
   624  		}
   625  		// Defer a soft reset of the cache lock, if the value is set this call will be ignored
   626  		defer func() {
   627  			if needsUnlock {
   628  				err := m.gitRefCache.UnlockGitReferences(m.repoURL, myLockId)
   629  				if err != nil {
   630  					log.Debugf("Error unlocking git references from cache: %v", err)
   631  				}
   632  			}
   633  		}()
   634  	}
   635  
   636  	if m.OnLsRemote != nil {
   637  		done := m.OnLsRemote(m.repoURL)
   638  		defer done()
   639  	}
   640  
   641  	repo, err := git.Init(memory.NewStorage(), nil)
   642  	if err != nil {
   643  		return nil, err
   644  	}
   645  	remote, err := repo.CreateRemote(&config.RemoteConfig{
   646  		Name: git.DefaultRemoteName,
   647  		URLs: []string{m.repoURL},
   648  	})
   649  	if err != nil {
   650  		return nil, err
   651  	}
   652  	auth, err := newAuth(m.repoURL, m.creds)
   653  	if err != nil {
   654  		return nil, err
   655  	}
   656  	res, err := listRemote(remote, &git.ListOptions{Auth: auth}, m.insecure, m.creds, m.proxy, m.noProxy)
   657  	if err == nil && m.gitRefCache != nil {
   658  		if err := m.gitRefCache.SetGitReferences(m.repoURL, res); err != nil {
   659  			log.Warnf("Failed to store git references to cache: %v", err)
   660  		} else {
   661  			// Since we successfully overwrote the lock with valid data, we don't need to unlock
   662  			needsUnlock = false
   663  		}
   664  		return res, nil
   665  	}
   666  	return res, err
   667  }
   668  
   669  func (m *nativeGitClient) LsRefs() (*Refs, error) {
   670  	refs, err := m.getRefs()
   671  	if err != nil {
   672  		return nil, err
   673  	}
   674  
   675  	sortedRefs := &Refs{
   676  		Branches: []string{},
   677  		Tags:     []string{},
   678  	}
   679  
   680  	for _, revision := range refs {
   681  		if revision.Name().IsBranch() {
   682  			sortedRefs.Branches = append(sortedRefs.Branches, revision.Name().Short())
   683  		} else if revision.Name().IsTag() {
   684  			sortedRefs.Tags = append(sortedRefs.Tags, revision.Name().Short())
   685  		}
   686  	}
   687  
   688  	log.Debugf("LsRefs resolved %d branches and %d tags on repository", len(sortedRefs.Branches), len(sortedRefs.Tags))
   689  
   690  	// Would prefer to sort by last modified date but that info does not appear to be available without resolving each ref
   691  	sort.Strings(sortedRefs.Branches)
   692  	sort.Strings(sortedRefs.Tags)
   693  
   694  	return sortedRefs, nil
   695  }
   696  
   697  // LsRemote resolves the commit SHA of a specific branch, tag (with semantic versioning or not),
   698  // or HEAD. If the supplied revision does not resolve, and "looks" like a 7+ hexadecimal commit SHA,
   699  // it will return the revision string. Otherwise, it returns an error indicating that the revision could
   700  // not be resolved. This method runs with in-memory storage and is safe to run concurrently,
   701  // or to be run without a git repository locally cloned.
   702  func (m *nativeGitClient) LsRemote(revision string) (res string, err error) {
   703  	for attempt := 0; attempt < maxAttemptsCount; attempt++ {
   704  		res, err = m.lsRemote(revision)
   705  		if err == nil {
   706  			return
   707  		} else if apierrors.IsInternalError(err) || apierrors.IsTimeout(err) || apierrors.IsServerTimeout(err) ||
   708  			apierrors.IsTooManyRequests(err) || utilnet.IsProbableEOF(err) || utilnet.IsConnectionReset(err) {
   709  			// Formula: timeToWait = duration * factor^retry_number
   710  			// Note that timeToWait should equal to duration for the first retry attempt.
   711  			// When timeToWait is more than maxDuration retry should be performed at maxDuration.
   712  			timeToWait := float64(retryDuration) * (math.Pow(float64(factor), float64(attempt)))
   713  			if maxRetryDuration > 0 {
   714  				timeToWait = math.Min(float64(maxRetryDuration), timeToWait)
   715  			}
   716  			time.Sleep(time.Duration(timeToWait))
   717  		}
   718  	}
   719  	return
   720  }
   721  
   722  func getGitTags(refs []*plumbing.Reference) []string {
   723  	var tags []string
   724  	for _, ref := range refs {
   725  		if ref.Name().IsTag() {
   726  			tags = append(tags, ref.Name().Short())
   727  		}
   728  	}
   729  	return tags
   730  }
   731  
   732  func (m *nativeGitClient) lsRemote(revision string) (string, error) {
   733  	if IsCommitSHA(revision) {
   734  		return revision, nil
   735  	}
   736  
   737  	refs, err := m.getRefs()
   738  	if err != nil {
   739  		return "", fmt.Errorf("failed to list refs: %w", err)
   740  	}
   741  
   742  	if revision == "" {
   743  		revision = "HEAD"
   744  	}
   745  
   746  	maxV, err := versions.MaxVersion(revision, getGitTags(refs))
   747  	if err == nil {
   748  		revision = maxV
   749  	}
   750  
   751  	// refToHash keeps a maps of remote refs to their hash
   752  	// (e.g. refs/heads/master -> a67038ae2e9cb9b9b16423702f98b41e36601001)
   753  	refToHash := make(map[string]string)
   754  
   755  	// refToResolve remembers ref name of the supplied revision if we determine the revision is a
   756  	// symbolic reference (like HEAD), in which case we will resolve it from the refToHash map
   757  	refToResolve := ""
   758  
   759  	for _, ref := range refs {
   760  		refName := ref.Name().String()
   761  		hash := ref.Hash().String()
   762  		if ref.Type() == plumbing.HashReference {
   763  			refToHash[refName] = hash
   764  		}
   765  		// log.Debugf("%s\t%s", hash, refName)
   766  		if ref.Name().Short() == revision || refName == revision {
   767  			if ref.Type() == plumbing.HashReference {
   768  				log.Debugf("revision '%s' resolved to '%s'", revision, hash)
   769  				return hash, nil
   770  			}
   771  			if ref.Type() == plumbing.SymbolicReference {
   772  				refToResolve = ref.Target().String()
   773  			}
   774  		}
   775  	}
   776  
   777  	if refToResolve != "" {
   778  		// If refToResolve is non-empty, we are resolving symbolic reference (e.g. HEAD).
   779  		// It should exist in our refToHash map
   780  		if hash, ok := refToHash[refToResolve]; ok {
   781  			log.Debugf("symbolic reference '%s' (%s) resolved to '%s'", revision, refToResolve, hash)
   782  			return hash, nil
   783  		}
   784  	}
   785  
   786  	// We support the ability to use a truncated commit-SHA (e.g. first 7 characters of a SHA)
   787  	if IsTruncatedCommitSHA(revision) {
   788  		log.Debugf("revision '%s' assumed to be commit sha", revision)
   789  		return revision, nil
   790  	}
   791  
   792  	// If we get here, revision string had non hexadecimal characters (indicating its a branch, tag,
   793  	// or symbolic ref) and we were unable to resolve it to a commit SHA.
   794  	return "", fmt.Errorf("unable to resolve '%s' to a commit SHA", revision)
   795  }
   796  
   797  // CommitSHA returns current commit sha from `git rev-parse HEAD`
   798  func (m *nativeGitClient) CommitSHA() (string, error) {
   799  	out, err := m.runCmd("rev-parse", "HEAD")
   800  	if err != nil {
   801  		return "", err
   802  	}
   803  	return strings.TrimSpace(out), nil
   804  }
   805  
   806  // RevisionMetadata returns the meta-data for the commit
   807  func (m *nativeGitClient) RevisionMetadata(revision string) (*RevisionMetadata, error) {
   808  	out, err := m.runCmd("show", "-s", "--format=%an <%ae>%n%at%n%B", revision)
   809  	if err != nil {
   810  		return nil, err
   811  	}
   812  	segments := strings.SplitN(out, "\n", 3)
   813  	if len(segments) != 3 {
   814  		return nil, fmt.Errorf("expected 3 segments, got %v", segments)
   815  	}
   816  	author := segments[0]
   817  	authorDateUnixTimestamp, _ := strconv.ParseInt(segments[1], 10, 64)
   818  	message := strings.TrimSpace(segments[2])
   819  
   820  	cmd := exec.Command("git", "interpret-trailers", "--parse")
   821  	cmd.Stdin = strings.NewReader(message)
   822  	out, err = m.runCmdOutput(cmd, runOpts{})
   823  	if err != nil {
   824  		return nil, fmt.Errorf("failed to interpret trailers for revision %q in repo %q: %w", revision, m.repoURL, err)
   825  	}
   826  	relatedCommits, _ := GetReferences(log.WithFields(log.Fields{"repo": m.repoURL, "revision": revision}), out)
   827  
   828  	out, err = m.runCmd("tag", "--points-at", revision)
   829  	if err != nil {
   830  		return nil, err
   831  	}
   832  	tags := strings.Fields(out)
   833  
   834  	return &RevisionMetadata{
   835  		Author:     author,
   836  		Date:       time.Unix(authorDateUnixTimestamp, 0),
   837  		Tags:       tags,
   838  		Message:    message,
   839  		References: relatedCommits,
   840  	}, nil
   841  }
   842  
   843  func truncate(str string) string {
   844  	if utf8.RuneCountInString(str) > 100 {
   845  		return string([]rune(str)[0:97]) + "..."
   846  	}
   847  	return str
   848  }
   849  
   850  var shaRegex = regexp.MustCompile(`^[0-9a-f]{5,40}$`)
   851  
   852  // GetReferences extracts related commit metadata from the commit message trailers. If referenced commit
   853  // metadata is present, we return a slice containing a single metadata object. If no related commit metadata is found,
   854  // we return a nil slice.
   855  //
   856  // If a trailer fails validation, we log an error and skip that trailer. We truncate the trailer values to 100
   857  // characters to avoid excessively long log messages.
   858  //
   859  // We also return the commit message body with all valid Argocd-reference-commit-* trailers removed.
   860  func GetReferences(logCtx *log.Entry, commitMessageBody string) ([]RevisionReference, string) {
   861  	unrelatedLines := strings.Builder{}
   862  	var relatedCommit CommitMetadata
   863  	scanner := bufio.NewScanner(strings.NewReader(commitMessageBody))
   864  	for scanner.Scan() {
   865  		line := scanner.Text()
   866  		updated := updateCommitMetadata(logCtx, &relatedCommit, line)
   867  		if !updated {
   868  			unrelatedLines.WriteString(line + "\n")
   869  		}
   870  	}
   871  	var relatedCommits []RevisionReference
   872  	if relatedCommit != (CommitMetadata{}) {
   873  		relatedCommits = append(relatedCommits, RevisionReference{
   874  			Commit: &relatedCommit,
   875  		})
   876  	}
   877  	return relatedCommits, unrelatedLines.String()
   878  }
   879  
   880  // updateCommitMetadata checks if the line is a valid Argocd-reference-commit-* trailer. If so, it updates
   881  // the relatedCommit object and returns true. If the line is not a valid trailer, it returns false.
   882  func updateCommitMetadata(logCtx *log.Entry, relatedCommit *CommitMetadata, line string) bool {
   883  	if !strings.HasPrefix(line, "Argocd-reference-commit-") {
   884  		return false
   885  	}
   886  	parts := strings.SplitN(line, ": ", 2)
   887  	if len(parts) != 2 {
   888  		return false
   889  	}
   890  	trailerKey := parts[0]
   891  	trailerValue := parts[1]
   892  	switch trailerKey {
   893  	case "Argocd-reference-commit-repourl":
   894  		_, err := url.Parse(trailerValue)
   895  		if err != nil {
   896  			logCtx.Errorf("failed to parse repo URL %q: %v", truncate(trailerValue), err)
   897  			return false
   898  		}
   899  		relatedCommit.RepoURL = trailerValue
   900  	case "Argocd-reference-commit-author":
   901  		address, err := mail.ParseAddress(trailerValue)
   902  		if err != nil || address == nil {
   903  			logCtx.Errorf("failed to parse author email %q: %v", truncate(trailerValue), err)
   904  			return false
   905  		}
   906  		relatedCommit.Author = *address
   907  	case "Argocd-reference-commit-date":
   908  		// Validate that it's the correct date format.
   909  		t, err := time.Parse(time.RFC3339, trailerValue)
   910  		if err != nil {
   911  			logCtx.Errorf("failed to parse date %q with RFC3339 format: %v", truncate(trailerValue), err)
   912  			return false
   913  		}
   914  		relatedCommit.Date = t.Format(time.RFC3339)
   915  	case "Argocd-reference-commit-subject":
   916  		relatedCommit.Subject = trailerValue
   917  	case "Argocd-reference-commit-body":
   918  		body := ""
   919  		err := json.Unmarshal([]byte(trailerValue), &body)
   920  		if err != nil {
   921  			logCtx.Errorf("failed to parse body %q as JSON: %v", truncate(trailerValue), err)
   922  			return false
   923  		}
   924  		relatedCommit.Body = body
   925  	case "Argocd-reference-commit-sha":
   926  		if !shaRegex.MatchString(trailerValue) {
   927  			logCtx.Errorf("invalid commit SHA %q in trailer %s: must be a lowercase hex string 5-40 characters long", truncate(trailerValue), trailerKey)
   928  			return false
   929  		}
   930  		relatedCommit.SHA = trailerValue
   931  	default:
   932  		return false
   933  	}
   934  	return true
   935  }
   936  
   937  // VerifyCommitSignature Runs verify-commit on a given revision and returns the output
   938  func (m *nativeGitClient) VerifyCommitSignature(revision string) (string, error) {
   939  	out, err := m.runGnuPGWrapper("git-verify-wrapper.sh", revision)
   940  	if err != nil {
   941  		log.Errorf("error verifying commit signature: %v", err)
   942  		return "", errors.New("permission denied")
   943  	}
   944  	return out, nil
   945  }
   946  
   947  // IsAnnotatedTag returns true if the revision points to an annotated tag
   948  func (m *nativeGitClient) IsAnnotatedTag(revision string) bool {
   949  	cmd := exec.Command("git", "describe", "--exact-match", revision)
   950  	out, err := m.runCmdOutput(cmd, runOpts{SkipErrorLogging: true})
   951  	if out != "" && err == nil {
   952  		return true
   953  	}
   954  	return false
   955  }
   956  
   957  // ChangedFiles returns a list of files changed between two revisions
   958  func (m *nativeGitClient) ChangedFiles(revision string, targetRevision string) ([]string, error) {
   959  	if revision == targetRevision {
   960  		return []string{}, nil
   961  	}
   962  
   963  	if !IsCommitSHA(revision) || !IsCommitSHA(targetRevision) {
   964  		return []string{}, errors.New("invalid revision provided, must be SHA")
   965  	}
   966  
   967  	out, err := m.runCmd("diff", "--name-only", fmt.Sprintf("%s..%s", revision, targetRevision))
   968  	if err != nil {
   969  		return nil, fmt.Errorf("failed to diff %s..%s: %w", revision, targetRevision, err)
   970  	}
   971  
   972  	if out == "" {
   973  		return []string{}, nil
   974  	}
   975  
   976  	files := strings.Split(out, "\n")
   977  	return files, nil
   978  }
   979  
   980  // config runs a git config command.
   981  func (m *nativeGitClient) config(args ...string) (string, error) {
   982  	args = append([]string{"config"}, args...)
   983  	out, err := m.runCmd(args...)
   984  	if err != nil {
   985  		return out, fmt.Errorf("failed to run git config: %w", err)
   986  	}
   987  	return out, nil
   988  }
   989  
   990  // SetAuthor sets the author name and email in the git configuration.
   991  func (m *nativeGitClient) SetAuthor(name, email string) (string, error) {
   992  	if name != "" {
   993  		out, err := m.config("--local", "user.name", name)
   994  		if err != nil {
   995  			return out, err
   996  		}
   997  	}
   998  	if email != "" {
   999  		out, err := m.config("--local", "user.email", email)
  1000  		if err != nil {
  1001  			return out, err
  1002  		}
  1003  	}
  1004  	return "", nil
  1005  }
  1006  
  1007  // CheckoutOrOrphan checks out the branch. If the branch does not exist, it creates an orphan branch.
  1008  func (m *nativeGitClient) CheckoutOrOrphan(branch string, submoduleEnabled bool) (string, error) {
  1009  	out, err := m.Checkout(branch, submoduleEnabled)
  1010  	if err != nil {
  1011  		// If the branch doesn't exist, create it as an orphan branch.
  1012  		if !strings.Contains(err.Error(), "did not match any file(s) known to git") {
  1013  			return out, fmt.Errorf("failed to checkout branch: %w", err)
  1014  		}
  1015  		out, err = m.runCmd("switch", "--orphan", branch)
  1016  		if err != nil {
  1017  			return out, fmt.Errorf("failed to create orphan branch: %w", err)
  1018  		}
  1019  
  1020  		// Make an empty initial commit.
  1021  		out, err = m.runCmd("commit", "--allow-empty", "-m", "Initial commit")
  1022  		if err != nil {
  1023  			return out, fmt.Errorf("failed to commit initial commit: %w", err)
  1024  		}
  1025  
  1026  		// Push the commit.
  1027  		err = m.runCredentialedCmd("push", "origin", branch)
  1028  		if err != nil {
  1029  			return "", fmt.Errorf("failed to push to branch: %w", err)
  1030  		}
  1031  	}
  1032  	return "", nil
  1033  }
  1034  
  1035  // CheckoutOrNew checks out the given branch. If the branch does not exist, it creates an empty branch based on
  1036  // the base branch.
  1037  func (m *nativeGitClient) CheckoutOrNew(branch, base string, submoduleEnabled bool) (string, error) {
  1038  	out, err := m.Checkout(branch, submoduleEnabled)
  1039  	if err != nil {
  1040  		if !strings.Contains(err.Error(), "did not match any file(s) known to git") {
  1041  			return out, fmt.Errorf("failed to checkout branch: %w", err)
  1042  		}
  1043  		// If the branch does not exist, create any empty branch based on the sync branch
  1044  		// First, checkout the sync branch.
  1045  		out, err = m.Checkout(base, submoduleEnabled)
  1046  		if err != nil {
  1047  			return out, fmt.Errorf("failed to checkout sync branch: %w", err)
  1048  		}
  1049  
  1050  		out, err = m.runCmd("checkout", "-b", branch)
  1051  		if err != nil {
  1052  			return out, fmt.Errorf("failed to create branch: %w", err)
  1053  		}
  1054  	}
  1055  	return "", nil
  1056  }
  1057  
  1058  // RemoveContents removes all files from the path of git repository.
  1059  func (m *nativeGitClient) RemoveContents(paths []string) (string, error) {
  1060  	if len(paths) == 0 {
  1061  		return "", nil
  1062  	}
  1063  	args := append([]string{"rm", "-r", "--ignore-unmatch", "--"}, paths...)
  1064  	out, err := m.runCmd(args...)
  1065  	if err != nil {
  1066  		return out, fmt.Errorf("failed to clear paths %v: %w", paths, err)
  1067  	}
  1068  	return "", nil
  1069  }
  1070  
  1071  // CommitAndPush commits and pushes changes to the target branch.
  1072  func (m *nativeGitClient) CommitAndPush(branch, message string) (string, error) {
  1073  	out, err := m.runCmd("add", ".")
  1074  	if err != nil {
  1075  		return out, fmt.Errorf("failed to add files: %w", err)
  1076  	}
  1077  
  1078  	out, err = m.runCmd("commit", "-m", message)
  1079  	if err != nil {
  1080  		if strings.Contains(out, "nothing to commit, working tree clean") {
  1081  			return out, nil
  1082  		}
  1083  		return out, fmt.Errorf("failed to commit: %w", err)
  1084  	}
  1085  
  1086  	if m.OnPush != nil {
  1087  		done := m.OnPush(m.repoURL)
  1088  		defer done()
  1089  	}
  1090  
  1091  	err = m.runCredentialedCmd("push", "origin", branch)
  1092  	if err != nil {
  1093  		return "", fmt.Errorf("failed to push: %w", err)
  1094  	}
  1095  
  1096  	return "", nil
  1097  }
  1098  
  1099  // runWrapper runs a custom command with all the semantics of running the Git client
  1100  func (m *nativeGitClient) runGnuPGWrapper(wrapper string, args ...string) (string, error) {
  1101  	cmd := exec.Command(wrapper, args...)
  1102  	cmd.Env = append(cmd.Env, "GNUPGHOME="+common.GetGnuPGHomePath(), "LANG=C")
  1103  	return m.runCmdOutput(cmd, runOpts{})
  1104  }
  1105  
  1106  // runCmd is a convenience function to run a command in a given directory and return its output
  1107  func (m *nativeGitClient) runCmd(args ...string) (string, error) {
  1108  	cmd := exec.Command("git", args...)
  1109  	return m.runCmdOutput(cmd, runOpts{})
  1110  }
  1111  
  1112  // runCredentialedCmd is a convenience function to run a git command with username/password credentials
  1113  func (m *nativeGitClient) runCredentialedCmd(args ...string) error {
  1114  	closer, environ, err := m.creds.Environ()
  1115  	if err != nil {
  1116  		return err
  1117  	}
  1118  	defer func() { _ = closer.Close() }()
  1119  
  1120  	// If a basic auth header is explicitly set, tell Git to send it to the
  1121  	// server to force use of basic auth instead of negotiating the auth scheme
  1122  	for _, e := range environ {
  1123  		if strings.HasPrefix(e, forceBasicAuthHeaderEnv+"=") {
  1124  			args = append([]string{"--config-env", "http.extraHeader=" + forceBasicAuthHeaderEnv}, args...)
  1125  		} else if strings.HasPrefix(e, bearerAuthHeaderEnv+"=") {
  1126  			args = append([]string{"--config-env", "http.extraHeader=" + bearerAuthHeaderEnv}, args...)
  1127  		}
  1128  	}
  1129  
  1130  	cmd := exec.Command("git", args...)
  1131  	cmd.Env = append(cmd.Env, environ...)
  1132  	_, err = m.runCmdOutput(cmd, runOpts{})
  1133  	return err
  1134  }
  1135  
  1136  func (m *nativeGitClient) runCmdOutput(cmd *exec.Cmd, ropts runOpts) (string, error) {
  1137  	cmd.Dir = m.root
  1138  	cmd.Env = append(os.Environ(), cmd.Env...)
  1139  	// Set $HOME to nowhere, so we can execute Git regardless of any external
  1140  	// authentication keys (e.g. in ~/.ssh) -- this is especially important for
  1141  	// running tests on local machines and/or CircleCI.
  1142  	cmd.Env = append(cmd.Env, "HOME=/dev/null")
  1143  	// Skip LFS for most Git operations except when explicitly requested
  1144  	cmd.Env = append(cmd.Env, "GIT_LFS_SKIP_SMUDGE=1")
  1145  	// Disable Git terminal prompts in case we're running with a tty
  1146  	cmd.Env = append(cmd.Env, "GIT_TERMINAL_PROMPT=false")
  1147  	// Add Git configuration options that are essential for ArgoCD operation
  1148  	cmd.Env = append(cmd.Env, m.gitConfigEnv...)
  1149  
  1150  	// For HTTPS repositories, we need to consider insecure repositories as well
  1151  	// as custom CA bundles from the cert database.
  1152  	if IsHTTPSURL(m.repoURL) {
  1153  		if m.insecure {
  1154  			cmd.Env = append(cmd.Env, "GIT_SSL_NO_VERIFY=true")
  1155  		} else {
  1156  			parsedURL, err := url.Parse(m.repoURL)
  1157  			// We don't fail if we cannot parse the URL, but log a warning in that
  1158  			// case. And we execute the command in a verbatim way.
  1159  			if err != nil {
  1160  				log.Warnf("runCmdOutput: Could not parse repo URL '%s'", m.repoURL)
  1161  			} else {
  1162  				caPath, err := certutil.GetCertBundlePathForRepository(parsedURL.Host)
  1163  				if err == nil && caPath != "" {
  1164  					cmd.Env = append(cmd.Env, "GIT_SSL_CAINFO="+caPath)
  1165  				}
  1166  			}
  1167  		}
  1168  	}
  1169  	cmd.Env = proxy.UpsertEnv(cmd, m.proxy, m.noProxy)
  1170  	opts := executil.ExecRunOpts{
  1171  		TimeoutBehavior: executil.TimeoutBehavior{
  1172  			Signal:     syscall.SIGTERM,
  1173  			ShouldWait: true,
  1174  		},
  1175  		SkipErrorLogging: ropts.SkipErrorLogging,
  1176  		CaptureStderr:    ropts.CaptureStderr,
  1177  	}
  1178  	return executil.RunWithExecRunOpts(cmd, opts)
  1179  }