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

     1  package git
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/mail"
     9  	"os"
    10  	"os/exec"
    11  	"path"
    12  	"path/filepath"
    13  	"regexp"
    14  	"strings"
    15  	"sync"
    16  	"testing"
    17  	"time"
    18  
    19  	log "github.com/sirupsen/logrus"
    20  
    21  	"github.com/go-git/go-git/v5/plumbing"
    22  	"github.com/go-git/go-git/v5/plumbing/transport"
    23  	githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
    24  	"github.com/stretchr/testify/assert"
    25  	"github.com/stretchr/testify/require"
    26  
    27  	"github.com/argoproj/argo-cd/v3/util/workloadidentity"
    28  	"github.com/argoproj/argo-cd/v3/util/workloadidentity/mocks"
    29  )
    30  
    31  func runCmd(workingDir string, name string, args ...string) error {
    32  	cmd := exec.Command(name, args...)
    33  	cmd.Dir = workingDir
    34  	cmd.Stdout = os.Stdout
    35  	cmd.Stderr = os.Stderr
    36  	return cmd.Run()
    37  }
    38  
    39  func outputCmd(workingDir string, name string, args ...string) ([]byte, error) {
    40  	cmd := exec.Command(name, args...)
    41  	cmd.Dir = workingDir
    42  	cmd.Stderr = os.Stderr
    43  	return cmd.Output()
    44  }
    45  
    46  func _createEmptyGitRepo() (string, error) {
    47  	tempDir, err := os.MkdirTemp("", "")
    48  	if err != nil {
    49  		return tempDir, err
    50  	}
    51  
    52  	err = runCmd(tempDir, "git", "init")
    53  	if err != nil {
    54  		return tempDir, err
    55  	}
    56  
    57  	err = runCmd(tempDir, "git", "commit", "-m", "Initial commit", "--allow-empty")
    58  	return tempDir, err
    59  }
    60  
    61  func Test_nativeGitClient_Fetch(t *testing.T) {
    62  	tempDir, err := _createEmptyGitRepo()
    63  	require.NoError(t, err)
    64  
    65  	client, err := NewClient("file://"+tempDir, NopCreds{}, true, false, "", "")
    66  	require.NoError(t, err)
    67  
    68  	err = client.Init()
    69  	require.NoError(t, err)
    70  
    71  	err = client.Fetch("")
    72  	require.NoError(t, err)
    73  }
    74  
    75  func Test_nativeGitClient_Fetch_Prune(t *testing.T) {
    76  	tempDir, err := _createEmptyGitRepo()
    77  	require.NoError(t, err)
    78  
    79  	client, err := NewClient("file://"+tempDir, NopCreds{}, true, false, "", "")
    80  	require.NoError(t, err)
    81  
    82  	err = client.Init()
    83  	require.NoError(t, err)
    84  
    85  	err = runCmd(tempDir, "git", "branch", "test/foo")
    86  	require.NoError(t, err)
    87  
    88  	err = client.Fetch("")
    89  	require.NoError(t, err)
    90  
    91  	err = runCmd(tempDir, "git", "branch", "-d", "test/foo")
    92  	require.NoError(t, err)
    93  	err = runCmd(tempDir, "git", "branch", "test/foo/bar")
    94  	require.NoError(t, err)
    95  
    96  	err = client.Fetch("")
    97  	require.NoError(t, err)
    98  }
    99  
   100  func Test_IsAnnotatedTag(t *testing.T) {
   101  	tempDir := t.TempDir()
   102  	client, err := NewClient("file://"+tempDir, NopCreds{}, true, false, "", "")
   103  	require.NoError(t, err)
   104  
   105  	err = client.Init()
   106  	require.NoError(t, err)
   107  
   108  	p := path.Join(client.Root(), "README")
   109  	f, err := os.Create(p)
   110  	require.NoError(t, err)
   111  	_, err = f.WriteString("Hello.")
   112  	require.NoError(t, err)
   113  	err = f.Close()
   114  	require.NoError(t, err)
   115  
   116  	err = runCmd(client.Root(), "git", "add", "README")
   117  	require.NoError(t, err)
   118  
   119  	err = runCmd(client.Root(), "git", "commit", "-m", "Initial commit", "-a")
   120  	require.NoError(t, err)
   121  
   122  	atag := client.IsAnnotatedTag("master")
   123  	assert.False(t, atag)
   124  
   125  	err = runCmd(client.Root(), "git", "tag", "some-tag", "-a", "-m", "Create annotated tag")
   126  	require.NoError(t, err)
   127  	atag = client.IsAnnotatedTag("some-tag")
   128  	assert.True(t, atag)
   129  
   130  	// Tag effectually points to HEAD, so it's considered the same
   131  	atag = client.IsAnnotatedTag("HEAD")
   132  	assert.True(t, atag)
   133  
   134  	err = runCmd(client.Root(), "git", "rm", "README")
   135  	require.NoError(t, err)
   136  	err = runCmd(client.Root(), "git", "commit", "-m", "remove README", "-a")
   137  	require.NoError(t, err)
   138  
   139  	// We moved on, so tag doesn't point to HEAD anymore
   140  	atag = client.IsAnnotatedTag("HEAD")
   141  	assert.False(t, atag)
   142  }
   143  
   144  func Test_resolveTagReference(t *testing.T) {
   145  	// Setup
   146  	commitHash := plumbing.NewHash("0123456789abcdef0123456789abcdef01234567")
   147  	tagRef := plumbing.NewReferenceFromStrings("refs/tags/v1.0.0", "sometaghash")
   148  
   149  	// Test single function
   150  	resolvedRef := plumbing.NewHashReference(tagRef.Name(), commitHash)
   151  
   152  	// Verify
   153  	assert.Equal(t, commitHash, resolvedRef.Hash())
   154  	assert.Equal(t, tagRef.Name(), resolvedRef.Name())
   155  }
   156  
   157  func Test_ChangedFiles(t *testing.T) {
   158  	tempDir := t.TempDir()
   159  
   160  	client, err := NewClientExt("file://"+tempDir, tempDir, NopCreds{}, true, false, "", "")
   161  	require.NoError(t, err)
   162  
   163  	err = client.Init()
   164  	require.NoError(t, err)
   165  
   166  	err = runCmd(client.Root(), "git", "commit", "-m", "Initial commit", "--allow-empty")
   167  	require.NoError(t, err)
   168  
   169  	// Create a tag to have a second ref
   170  	err = runCmd(client.Root(), "git", "tag", "some-tag")
   171  	require.NoError(t, err)
   172  
   173  	p := path.Join(client.Root(), "README")
   174  	f, err := os.Create(p)
   175  	require.NoError(t, err)
   176  	_, err = f.WriteString("Hello.")
   177  	require.NoError(t, err)
   178  	err = f.Close()
   179  	require.NoError(t, err)
   180  
   181  	err = runCmd(client.Root(), "git", "add", "README")
   182  	require.NoError(t, err)
   183  
   184  	err = runCmd(client.Root(), "git", "commit", "-m", "Changes", "-a")
   185  	require.NoError(t, err)
   186  
   187  	previousSHA, err := client.LsRemote("some-tag")
   188  	require.NoError(t, err)
   189  
   190  	commitSHA, err := client.LsRemote("HEAD")
   191  	require.NoError(t, err)
   192  
   193  	// Invalid commits, error
   194  	_, err = client.ChangedFiles("0000000000000000000000000000000000000000", "1111111111111111111111111111111111111111")
   195  	require.Error(t, err)
   196  
   197  	// Not SHAs, error
   198  	_, err = client.ChangedFiles(previousSHA, "HEAD")
   199  	require.Error(t, err)
   200  
   201  	// Same commit, no changes
   202  	changedFiles, err := client.ChangedFiles(commitSHA, commitSHA)
   203  	require.NoError(t, err)
   204  	assert.ElementsMatch(t, []string{}, changedFiles)
   205  
   206  	// Different ref, with changes
   207  	changedFiles, err = client.ChangedFiles(previousSHA, commitSHA)
   208  	require.NoError(t, err)
   209  	assert.ElementsMatch(t, []string{"README"}, changedFiles)
   210  }
   211  
   212  func Test_SemverTags(t *testing.T) {
   213  	tempDir := t.TempDir()
   214  
   215  	client, err := NewClientExt("file://"+tempDir, tempDir, NopCreds{}, true, false, "", "")
   216  	require.NoError(t, err)
   217  
   218  	err = client.Init()
   219  	require.NoError(t, err)
   220  
   221  	mapTagRefs := map[string]string{}
   222  	for _, tag := range []string{
   223  		"v1.0.0-rc1",
   224  		"v1.0.0-rc2",
   225  		"v1.0.0",
   226  		"v1.0",
   227  		"v1.0.1",
   228  		"v1.1.0",
   229  		"2024-apple",
   230  		"2024-banana",
   231  	} {
   232  		err = runCmd(client.Root(), "git", "commit", "-m", tag+" commit", "--allow-empty")
   233  		require.NoError(t, err)
   234  
   235  		// Create an rc semver tag
   236  		err = runCmd(client.Root(), "git", "tag", tag)
   237  		require.NoError(t, err)
   238  
   239  		sha, err := client.LsRemote("HEAD")
   240  		require.NoError(t, err)
   241  
   242  		mapTagRefs[tag] = sha
   243  	}
   244  
   245  	for _, tc := range []struct {
   246  		name     string
   247  		ref      string
   248  		expected string
   249  		error    bool
   250  	}{{
   251  		name:     "pinned rc version",
   252  		ref:      "v1.0.0-rc1",
   253  		expected: mapTagRefs["v1.0.0-rc1"],
   254  	}, {
   255  		name:     "lt rc constraint",
   256  		ref:      "< v1.0.0-rc3",
   257  		expected: mapTagRefs["v1.0.0-rc2"],
   258  	}, {
   259  		name:     "pinned major version",
   260  		ref:      "v1.0.0",
   261  		expected: mapTagRefs["v1.0.0"],
   262  	}, {
   263  		name:     "pinned patch version",
   264  		ref:      "v1.0.1",
   265  		expected: mapTagRefs["v1.0.1"],
   266  	}, {
   267  		name:     "pinned minor version",
   268  		ref:      "v1.1.0",
   269  		expected: mapTagRefs["v1.1.0"],
   270  	}, {
   271  		name:     "patch wildcard constraint",
   272  		ref:      "v1.0.*",
   273  		expected: mapTagRefs["v1.0.1"],
   274  	}, {
   275  		name:     "patch tilde constraint",
   276  		ref:      "~v1.0.0",
   277  		expected: mapTagRefs["v1.0.1"],
   278  	}, {
   279  		name:     "minor wildcard constraint",
   280  		ref:      "v1.*",
   281  		expected: mapTagRefs["v1.1.0"],
   282  	}, {
   283  		// The semver library allows for using both * and x as the wildcard modifier.
   284  		name:     "alternative minor wildcard constraint",
   285  		ref:      "v1.x",
   286  		expected: mapTagRefs["v1.1.0"],
   287  	}, {
   288  		name:     "minor gte constraint",
   289  		ref:      ">= v1.0.0",
   290  		expected: mapTagRefs["v1.1.0"],
   291  	}, {
   292  		name:     "multiple constraints",
   293  		ref:      "> v1.0.0 < v1.1.0",
   294  		expected: mapTagRefs["v1.0.1"],
   295  	}, {
   296  		// We treat non-specific semver versions as regular tags, rather than constraints.
   297  		name:     "non-specific version",
   298  		ref:      "v1.0",
   299  		expected: mapTagRefs["v1.0"],
   300  	}, {
   301  		// Which means a missing tag will raise an error.
   302  		name:  "missing non-specific version",
   303  		ref:   "v1.1",
   304  		error: true,
   305  	}, {
   306  		// This is NOT a semver constraint, so it should always resolve to itself - because specifying a tag should
   307  		// return the commit for that tag.
   308  		// semver/v3 has the unfortunate semver-ish behaviour where any tag starting with a number is considered to be
   309  		// "semver-ish", where that number is the semver major version, and the rest then gets coerced into a beta
   310  		// version string. This can cause unexpected behaviour with constraints logic.
   311  		// In this case, if the tag is being incorrectly coerced into semver (for being semver-ish), it will incorrectly
   312  		// return the commit for the 2024-banana tag; which we want to avoid.
   313  		name:     "apple non-semver tag",
   314  		ref:      "2024-apple",
   315  		expected: mapTagRefs["2024-apple"],
   316  	}, {
   317  		name:     "banana non-semver tag",
   318  		ref:      "2024-banana",
   319  		expected: mapTagRefs["2024-banana"],
   320  	}, {
   321  		// A semver version (without constraints) should ONLY match itself.
   322  		// We do not want "2024-apple" to get "semver-ish'ed" into matching "2024.0.0-apple"; they're different tags.
   323  		name:  "no semver tag coercion",
   324  		ref:   "2024.0.0-apple",
   325  		error: true,
   326  	}, {
   327  		// No minor versions are specified, so we would expect a major version of 2025 or more.
   328  		// This is because if we specify > 11 in semver, we would not expect 11.1.0 to pass; it should be 12.0.0 or more.
   329  		// Similarly, if we were to specify > 11.0, we would expect 11.1.0 or more.
   330  		name:  "semver constraints on non-semver tags",
   331  		ref:   "> 2024-apple",
   332  		error: true,
   333  	}, {
   334  		// However, if one specifies the minor/patch versions, semver constraints can be used to match non-semver tags.
   335  		// 2024-banana is considered as "2024.0.0-banana" in semver-ish, and banana > apple, so it's a match.
   336  		// Note: this is more for documentation and future reference than real testing, as it seems like quite odd behaviour.
   337  		name:     "semver constraints on semver tags",
   338  		ref:      "> 2024.0.0-apple",
   339  		expected: mapTagRefs["2024-banana"],
   340  	}} {
   341  		t.Run(tc.name, func(t *testing.T) {
   342  			commitSHA, err := client.LsRemote(tc.ref)
   343  			if tc.error {
   344  				require.Error(t, err)
   345  				return
   346  			}
   347  			require.NoError(t, err)
   348  			assert.True(t, IsCommitSHA(commitSHA))
   349  			assert.Equal(t, tc.expected, commitSHA)
   350  		})
   351  	}
   352  }
   353  
   354  func Test_nativeGitClient_Submodule(t *testing.T) {
   355  	tempDir, err := os.MkdirTemp("", "")
   356  	require.NoError(t, err)
   357  
   358  	foo := filepath.Join(tempDir, "foo")
   359  	err = os.Mkdir(foo, 0o755)
   360  	require.NoError(t, err)
   361  
   362  	err = runCmd(foo, "git", "init")
   363  	require.NoError(t, err)
   364  
   365  	bar := filepath.Join(tempDir, "bar")
   366  	err = os.Mkdir(bar, 0o755)
   367  	require.NoError(t, err)
   368  
   369  	err = runCmd(bar, "git", "init")
   370  	require.NoError(t, err)
   371  
   372  	err = runCmd(bar, "git", "commit", "-m", "Initial commit", "--allow-empty")
   373  	require.NoError(t, err)
   374  
   375  	// Embed repository bar into repository foo
   376  	t.Setenv("GIT_ALLOW_PROTOCOL", "file")
   377  	err = runCmd(foo, "git", "submodule", "add", bar)
   378  	require.NoError(t, err)
   379  
   380  	err = runCmd(foo, "git", "commit", "-m", "Initial commit")
   381  	require.NoError(t, err)
   382  
   383  	tempDir, err = os.MkdirTemp("", "")
   384  	require.NoError(t, err)
   385  
   386  	// Clone foo
   387  	err = runCmd(tempDir, "git", "clone", foo)
   388  	require.NoError(t, err)
   389  
   390  	client, err := NewClient("file://"+foo, NopCreds{}, true, false, "", "")
   391  	require.NoError(t, err)
   392  
   393  	err = client.Init()
   394  	require.NoError(t, err)
   395  
   396  	err = client.Fetch("")
   397  	require.NoError(t, err)
   398  
   399  	commitSHA, err := client.LsRemote("HEAD")
   400  	require.NoError(t, err)
   401  
   402  	// Call Checkout() with submoduleEnabled=false.
   403  	_, err = client.Checkout(commitSHA, false)
   404  	require.NoError(t, err)
   405  
   406  	// Check if submodule url does not exist in .git/config
   407  	err = runCmd(client.Root(), "git", "config", "submodule.bar.url")
   408  	require.Error(t, err)
   409  
   410  	// Call Submodule() via Checkout() with submoduleEnabled=true.
   411  	_, err = client.Checkout(commitSHA, true)
   412  	require.NoError(t, err)
   413  
   414  	// Check if the .gitmodule URL is reflected in .git/config
   415  	cmd := exec.Command("git", "config", "submodule.bar.url")
   416  	cmd.Dir = client.Root()
   417  	result, err := cmd.Output()
   418  	require.NoError(t, err)
   419  	assert.Equal(t, bar+"\n", string(result))
   420  
   421  	// Change URL of submodule bar
   422  	err = runCmd(client.Root(), "git", "config", "--file=.gitmodules", "submodule.bar.url", bar+"baz")
   423  	require.NoError(t, err)
   424  
   425  	// Call Submodule()
   426  	err = client.Submodule()
   427  	require.NoError(t, err)
   428  
   429  	// Check if the URL change in .gitmodule is reflected in .git/config
   430  	cmd = exec.Command("git", "config", "submodule.bar.url")
   431  	cmd.Dir = client.Root()
   432  	result, err = cmd.Output()
   433  	require.NoError(t, err)
   434  	assert.Equal(t, bar+"baz\n", string(result))
   435  }
   436  
   437  func TestNewClient_invalidSSHURL(t *testing.T) {
   438  	client, err := NewClient("ssh://bitbucket.org:org/repo", NopCreds{}, false, false, "", "")
   439  	assert.Nil(t, client)
   440  	assert.ErrorIs(t, err, ErrInvalidRepoURL)
   441  }
   442  
   443  func Test_IsRevisionPresent(t *testing.T) {
   444  	tempDir := t.TempDir()
   445  
   446  	client, err := NewClientExt("file://"+tempDir, tempDir, NopCreds{}, true, false, "", "")
   447  	require.NoError(t, err)
   448  
   449  	err = client.Init()
   450  	require.NoError(t, err)
   451  
   452  	p := path.Join(client.Root(), "README")
   453  	f, err := os.Create(p)
   454  	require.NoError(t, err)
   455  	_, err = f.WriteString("Hello.")
   456  	require.NoError(t, err)
   457  	err = f.Close()
   458  	require.NoError(t, err)
   459  
   460  	err = runCmd(client.Root(), "git", "add", "README")
   461  	require.NoError(t, err)
   462  
   463  	err = runCmd(client.Root(), "git", "commit", "-m", "Initial Commit", "-a")
   464  	require.NoError(t, err)
   465  
   466  	commitSHA, err := client.LsRemote("HEAD")
   467  	require.NoError(t, err)
   468  
   469  	// Ensure revision for HEAD is present locally.
   470  	revisionPresent := client.IsRevisionPresent(commitSHA)
   471  	assert.True(t, revisionPresent)
   472  
   473  	// Ensure invalid revision is not returned.
   474  	revisionPresent = client.IsRevisionPresent("invalid-revision")
   475  	assert.False(t, revisionPresent)
   476  }
   477  
   478  func Test_nativeGitClient_RevisionMetadata(t *testing.T) {
   479  	tempDir := t.TempDir()
   480  	client, err := NewClient("file://"+tempDir, NopCreds{}, true, false, "", "")
   481  	require.NoError(t, err)
   482  
   483  	err = client.Init()
   484  	require.NoError(t, err)
   485  
   486  	p := path.Join(client.Root(), "README")
   487  	f, err := os.Create(p)
   488  	require.NoError(t, err)
   489  	_, err = f.WriteString("Hello.")
   490  	require.NoError(t, err)
   491  	err = f.Close()
   492  	require.NoError(t, err)
   493  
   494  	err = runCmd(client.Root(), "git", "config", "user.name", "FooBar ||| something\nelse")
   495  	require.NoError(t, err)
   496  	err = runCmd(client.Root(), "git", "config", "user.email", "foo@foo.com")
   497  	require.NoError(t, err)
   498  
   499  	err = runCmd(client.Root(), "git", "add", "README")
   500  	require.NoError(t, err)
   501  	now := time.Now()
   502  	err = runCmd(client.Root(), "git", "commit", "--date=\"Sat Jun 5 20:00:00 2021 +0000 UTC\"", "-m", `| Initial commit |
   503  
   504  
   505  (╯°□°)╯︵ ┻━┻
   506  		`, "-a",
   507  		"--trailer", "Argocd-reference-commit-author: test-author <test@email.com>",
   508  		"--trailer", "Argocd-reference-commit-date: "+now.Format(time.RFC3339),
   509  		"--trailer", "Argocd-reference-commit-subject: chore: make a change",
   510  		"--trailer", "Argocd-reference-commit-sha: abc123",
   511  		"--trailer", "Argocd-reference-commit-repourl: https://git.example.com/test/repo.git",
   512  	)
   513  	require.NoError(t, err)
   514  
   515  	metadata, err := client.RevisionMetadata("HEAD")
   516  	require.NoError(t, err)
   517  	require.Equal(t, &RevisionMetadata{
   518  		Author: `FooBar ||| somethingelse <foo@foo.com>`,
   519  		Date:   time.Date(2021, time.June, 5, 20, 0, 0, 0, time.UTC).Local(),
   520  		Tags:   []string{},
   521  		Message: fmt.Sprintf(`| Initial commit |
   522  
   523  (╯°□°)╯︵ ┻━┻
   524  
   525  Argocd-reference-commit-author: test-author <test@email.com>
   526  Argocd-reference-commit-date: %s
   527  Argocd-reference-commit-subject: chore: make a change
   528  Argocd-reference-commit-sha: abc123
   529  Argocd-reference-commit-repourl: https://git.example.com/test/repo.git`, now.Format(time.RFC3339)),
   530  		References: []RevisionReference{
   531  			{
   532  				Commit: &CommitMetadata{
   533  					Author: mail.Address{
   534  						Name:    "test-author",
   535  						Address: "test@email.com",
   536  					},
   537  					Date:    now.Format(time.RFC3339),
   538  					Subject: "chore: make a change",
   539  					SHA:     "abc123",
   540  					RepoURL: "https://git.example.com/test/repo.git",
   541  				},
   542  			},
   543  		},
   544  	}, metadata)
   545  }
   546  
   547  func Test_nativeGitClient_SetAuthor(t *testing.T) {
   548  	expectedName := "Tester"
   549  	expectedEmail := "test@example.com"
   550  
   551  	tempDir, err := _createEmptyGitRepo()
   552  	require.NoError(t, err)
   553  
   554  	client, err := NewClient("file://"+tempDir, NopCreds{}, true, false, "", "")
   555  	require.NoError(t, err)
   556  
   557  	err = client.Init()
   558  	require.NoError(t, err)
   559  
   560  	out, err := client.SetAuthor(expectedName, expectedEmail)
   561  	require.NoError(t, err, "error output: ", out)
   562  
   563  	// Check git user.name
   564  	gitUserName, err := outputCmd(client.Root(), "git", "config", "--local", "user.name")
   565  	require.NoError(t, err)
   566  	actualName := strings.TrimSpace(string(gitUserName))
   567  	require.Equal(t, expectedName, actualName)
   568  
   569  	// Check git user.email
   570  	gitUserEmail, err := outputCmd(client.Root(), "git", "config", "--local", "user.email")
   571  	require.NoError(t, err)
   572  	actualEmail := strings.TrimSpace(string(gitUserEmail))
   573  	require.Equal(t, expectedEmail, actualEmail)
   574  }
   575  
   576  func Test_nativeGitClient_CheckoutOrOrphan(t *testing.T) {
   577  	t.Run("checkout to an existing branch", func(t *testing.T) {
   578  		// not main or master
   579  		expectedBranch := "feature"
   580  
   581  		tempDir, err := _createEmptyGitRepo()
   582  		require.NoError(t, err)
   583  
   584  		client, err := NewClientExt("file://"+tempDir, tempDir, NopCreds{}, true, false, "", "")
   585  		require.NoError(t, err)
   586  
   587  		err = client.Init()
   588  		require.NoError(t, err)
   589  
   590  		// set the author for the initial commit of the orphan branch
   591  		out, err := client.SetAuthor("test", "test@example.com")
   592  		require.NoError(t, err, "error output: %s", out)
   593  
   594  		// get base branch
   595  		gitCurrentBranch, err := outputCmd(tempDir, "git", "rev-parse", "--abbrev-ref", "HEAD")
   596  		require.NoError(t, err)
   597  		baseBranch := strings.TrimSpace(string(gitCurrentBranch))
   598  
   599  		// get base commit
   600  		gitCurrentCommitHash, err := outputCmd(tempDir, "git", "rev-parse", "HEAD")
   601  		require.NoError(t, err)
   602  		expectedCommitHash := strings.TrimSpace(string(gitCurrentCommitHash))
   603  
   604  		// make expected branch
   605  		err = runCmd(tempDir, "git", "checkout", "-b", expectedBranch)
   606  		require.NoError(t, err)
   607  
   608  		// checkout to base branch, ready to test
   609  		err = runCmd(tempDir, "git", "checkout", baseBranch)
   610  		require.NoError(t, err)
   611  
   612  		out, err = client.CheckoutOrOrphan(expectedBranch, false)
   613  		require.NoError(t, err, "error output: ", out)
   614  
   615  		// get current branch, verify current branch
   616  		gitCurrentBranch, err = outputCmd(tempDir, "git", "rev-parse", "--abbrev-ref", "HEAD")
   617  		require.NoError(t, err)
   618  		actualBranch := strings.TrimSpace(string(gitCurrentBranch))
   619  		require.Equal(t, expectedBranch, actualBranch)
   620  
   621  		// get current commit hash, verify current commit hash
   622  		// equal -> not orphan
   623  		gitCurrentCommitHash, err = outputCmd(tempDir, "git", "rev-parse", "HEAD")
   624  		require.NoError(t, err)
   625  		actualCommitHash := strings.TrimSpace(string(gitCurrentCommitHash))
   626  		require.Equal(t, expectedCommitHash, actualCommitHash)
   627  	})
   628  
   629  	t.Run("orphan", func(t *testing.T) {
   630  		// not main or master
   631  		expectedBranch := "feature"
   632  
   633  		// make origin git repository
   634  		tempDir, err := _createEmptyGitRepo()
   635  		require.NoError(t, err)
   636  		originGitRepoURL := "file://" + tempDir
   637  		err = runCmd(tempDir, "git", "commit", "-m", "Second commit", "--allow-empty")
   638  		require.NoError(t, err)
   639  
   640  		// get base branch
   641  		gitCurrentBranch, err := outputCmd(tempDir, "git", "rev-parse", "--abbrev-ref", "HEAD")
   642  		require.NoError(t, err)
   643  		baseBranch := strings.TrimSpace(string(gitCurrentBranch))
   644  
   645  		// make test dir
   646  		tempDir, err = os.MkdirTemp("", "")
   647  		require.NoError(t, err)
   648  
   649  		client, err := NewClientExt(originGitRepoURL, tempDir, NopCreds{}, true, false, "", "")
   650  		require.NoError(t, err)
   651  
   652  		err = client.Init()
   653  		require.NoError(t, err)
   654  
   655  		// set the author for the initial commit of the orphan branch
   656  		out, err := client.SetAuthor("test", "test@example.com")
   657  		require.NoError(t, err, "error output: %s", out)
   658  
   659  		err = client.Fetch("")
   660  		require.NoError(t, err)
   661  
   662  		// checkout to origin base branch
   663  		err = runCmd(tempDir, "git", "checkout", baseBranch)
   664  		require.NoError(t, err)
   665  
   666  		// get base commit
   667  		gitCurrentCommitHash, err := outputCmd(tempDir, "git", "rev-parse", "HEAD")
   668  		require.NoError(t, err)
   669  		baseCommitHash := strings.TrimSpace(string(gitCurrentCommitHash))
   670  
   671  		out, err = client.CheckoutOrOrphan(expectedBranch, false)
   672  		require.NoError(t, err, "error output: ", out)
   673  
   674  		// get current branch, verify current branch
   675  		gitCurrentBranch, err = outputCmd(tempDir, "git", "rev-parse", "--abbrev-ref", "HEAD")
   676  		require.NoError(t, err)
   677  		actualBranch := strings.TrimSpace(string(gitCurrentBranch))
   678  		require.Equal(t, expectedBranch, actualBranch)
   679  
   680  		// check orphan branch
   681  
   682  		// get current commit hash, verify current commit hash
   683  		// not equal -> orphan
   684  		gitCurrentCommitHash, err = outputCmd(tempDir, "git", "rev-parse", "HEAD")
   685  		require.NoError(t, err)
   686  		currentCommitHash := strings.TrimSpace(string(gitCurrentCommitHash))
   687  		require.NotEqual(t, baseCommitHash, currentCommitHash)
   688  
   689  		// get commit count on current branch, verify 1 -> orphan
   690  		gitCommitCount, err := outputCmd(tempDir, "git", "rev-list", "--count", actualBranch)
   691  		require.NoError(t, err)
   692  		require.Equal(t, "1", strings.TrimSpace(string(gitCommitCount)))
   693  	})
   694  }
   695  
   696  func Test_nativeGitClient_CheckoutOrNew(t *testing.T) {
   697  	t.Run("checkout to an existing branch", func(t *testing.T) {
   698  		// Example status
   699  		// * 57aef63 (feature) Second commit
   700  		// * a4fad22 (main) Initial commit
   701  
   702  		// Test scenario
   703  		// given : main branch (w/ Initial commit)
   704  		// when  : try to check out [main -> feature]
   705  		// then  : feature branch (w/ Second commit)
   706  
   707  		// not main or master
   708  		expectedBranch := "feature"
   709  
   710  		tempDir, err := _createEmptyGitRepo()
   711  		require.NoError(t, err)
   712  
   713  		client, err := NewClientExt("file://"+tempDir, tempDir, NopCreds{}, true, false, "", "")
   714  		require.NoError(t, err)
   715  
   716  		err = client.Init()
   717  		require.NoError(t, err)
   718  
   719  		out, err := client.SetAuthor("test", "test@example.com")
   720  		require.NoError(t, err, "error output: %s", out)
   721  
   722  		// get base branch
   723  		gitCurrentBranch, err := outputCmd(tempDir, "git", "rev-parse", "--abbrev-ref", "HEAD")
   724  		require.NoError(t, err)
   725  		baseBranch := strings.TrimSpace(string(gitCurrentBranch))
   726  
   727  		// make expected branch
   728  		err = runCmd(tempDir, "git", "checkout", "-b", expectedBranch)
   729  		require.NoError(t, err)
   730  
   731  		// make expected commit
   732  		err = runCmd(tempDir, "git", "commit", "-m", "Second commit", "--allow-empty")
   733  		require.NoError(t, err)
   734  
   735  		// get expected commit
   736  		expectedCommitHash, err := client.CommitSHA()
   737  		require.NoError(t, err)
   738  
   739  		// checkout to base branch, ready to test
   740  		err = runCmd(tempDir, "git", "checkout", baseBranch)
   741  		require.NoError(t, err)
   742  
   743  		out, err = client.CheckoutOrNew(expectedBranch, baseBranch, false)
   744  		require.NoError(t, err, "error output: ", out)
   745  
   746  		// get current branch, verify current branch
   747  		gitCurrentBranch, err = outputCmd(tempDir, "git", "rev-parse", "--abbrev-ref", "HEAD")
   748  		require.NoError(t, err)
   749  		actualBranch := strings.TrimSpace(string(gitCurrentBranch))
   750  		require.Equal(t, expectedBranch, actualBranch)
   751  
   752  		// get current commit hash, verify current commit hash
   753  		actualCommitHash, err := client.CommitSHA()
   754  		require.NoError(t, err)
   755  		require.Equal(t, expectedCommitHash, actualCommitHash)
   756  	})
   757  
   758  	t.Run("new", func(t *testing.T) {
   759  		// Test scenario
   760  		// given : main branch (w/ Initial commit)
   761  		// 	 * a4fad22 (main) Initial commit
   762  		// when  : try to check out [main -> feature]
   763  		// then  : feature branch (w/ Initial commit)
   764  		// 	 * a4fad22 (feature, main) Initial commit
   765  
   766  		// not main or master
   767  		expectedBranch := "feature"
   768  
   769  		tempDir, err := _createEmptyGitRepo()
   770  		require.NoError(t, err)
   771  
   772  		client, err := NewClientExt("file://"+tempDir, tempDir, NopCreds{}, true, false, "", "")
   773  		require.NoError(t, err)
   774  
   775  		err = client.Init()
   776  		require.NoError(t, err)
   777  
   778  		out, err := client.SetAuthor("test", "test@example.com")
   779  		require.NoError(t, err, "error output: %s", out)
   780  
   781  		// get base branch
   782  		gitCurrentBranch, err := outputCmd(tempDir, "git", "rev-parse", "--abbrev-ref", "HEAD")
   783  		require.NoError(t, err)
   784  		baseBranch := strings.TrimSpace(string(gitCurrentBranch))
   785  
   786  		// get expected commit
   787  		expectedCommitHash, err := client.CommitSHA()
   788  		require.NoError(t, err)
   789  
   790  		out, err = client.CheckoutOrNew(expectedBranch, baseBranch, false)
   791  		require.NoError(t, err, "error output: ", out)
   792  
   793  		// get current branch, verify current branch
   794  		gitCurrentBranch, err = outputCmd(tempDir, "git", "rev-parse", "--abbrev-ref", "HEAD")
   795  		require.NoError(t, err)
   796  		actualBranch := strings.TrimSpace(string(gitCurrentBranch))
   797  		require.Equal(t, expectedBranch, actualBranch)
   798  
   799  		// get current commit hash, verify current commit hash
   800  		actualCommitHash, err := client.CommitSHA()
   801  		require.NoError(t, err)
   802  		require.Equal(t, expectedCommitHash, actualCommitHash)
   803  	})
   804  }
   805  
   806  func Test_nativeGitClient_RemoveContents_SpecificPath(t *testing.T) {
   807  	// given
   808  	tempDir, err := _createEmptyGitRepo()
   809  	require.NoError(t, err)
   810  
   811  	client, err := NewClient("file://"+tempDir, NopCreds{}, true, false, "", "")
   812  	require.NoError(t, err)
   813  
   814  	err = client.Init()
   815  	require.NoError(t, err)
   816  
   817  	_, err = client.SetAuthor("test", "test@example.com")
   818  	require.NoError(t, err)
   819  
   820  	err = runCmd(client.Root(), "touch", "README.md")
   821  	require.NoError(t, err)
   822  
   823  	err = runCmd(client.Root(), "mkdir", "scripts")
   824  	require.NoError(t, err)
   825  	err = runCmd(client.Root(), "touch", "scripts/startup.sh")
   826  	require.NoError(t, err)
   827  
   828  	err = runCmd(client.Root(), "git", "add", "--all")
   829  	require.NoError(t, err)
   830  	err = runCmd(client.Root(), "git", "commit", "-m", "Make files")
   831  	require.NoError(t, err)
   832  
   833  	// when: remove only "scripts" directory
   834  	_, err = client.RemoveContents([]string{"scripts"})
   835  	require.NoError(t, err)
   836  
   837  	// then: "scripts" should be gone, "README.md" should still exist
   838  	_, err = os.Stat(filepath.Join(client.Root(), "README.md"))
   839  	require.NoError(t, err, "README.md should not be removed")
   840  
   841  	_, err = os.Stat(filepath.Join(client.Root(), "scripts"))
   842  	require.Error(t, err, "scripts directory should be removed")
   843  
   844  	// and: listing should only show README.md
   845  	ls, err := outputCmd(client.Root(), "ls")
   846  	require.NoError(t, err)
   847  	require.Equal(t, "README.md", strings.TrimSpace(string(ls)))
   848  }
   849  
   850  func Test_nativeGitClient_CommitAndPush(t *testing.T) {
   851  	tempDir, err := _createEmptyGitRepo()
   852  	require.NoError(t, err)
   853  
   854  	// config receive.denyCurrentBranch updateInstead
   855  	// because local git init make a non-bare repository which cannot be pushed normally
   856  	err = runCmd(tempDir, "git", "config", "--local", "receive.denyCurrentBranch", "updateInstead")
   857  	require.NoError(t, err)
   858  
   859  	// get branch
   860  	gitCurrentBranch, err := outputCmd(tempDir, "git", "rev-parse", "--abbrev-ref", "HEAD")
   861  	require.NoError(t, err)
   862  	branch := strings.TrimSpace(string(gitCurrentBranch))
   863  
   864  	client, err := NewClient("file://"+tempDir, NopCreds{}, true, false, "", "")
   865  	require.NoError(t, err)
   866  
   867  	err = client.Init()
   868  	require.NoError(t, err)
   869  
   870  	out, err := client.SetAuthor("test", "test@example.com")
   871  	require.NoError(t, err, "error output: ", out)
   872  
   873  	err = client.Fetch(branch)
   874  	require.NoError(t, err)
   875  
   876  	out, err = client.Checkout(branch, false)
   877  	require.NoError(t, err, "error output: ", out)
   878  
   879  	// make a file then commit and push
   880  	err = runCmd(client.Root(), "touch", "README.md")
   881  	require.NoError(t, err)
   882  
   883  	out, err = client.CommitAndPush(branch, "docs: README")
   884  	require.NoError(t, err, "error output: %s", out)
   885  
   886  	// get current commit hash of the cloned repository
   887  	expectedCommitHash, err := client.CommitSHA()
   888  	require.NoError(t, err)
   889  
   890  	// get origin repository's current commit hash
   891  	gitCurrentCommitHash, err := outputCmd(tempDir, "git", "rev-parse", "HEAD")
   892  	require.NoError(t, err)
   893  	actualCommitHash := strings.TrimSpace(string(gitCurrentCommitHash))
   894  	require.Equal(t, expectedCommitHash, actualCommitHash)
   895  }
   896  
   897  func Test_newAuth_AzureWorkloadIdentity(t *testing.T) {
   898  	tokenprovider := new(mocks.TokenProvider)
   899  	tokenprovider.On("GetToken", azureDevopsEntraResourceId).Return(&workloadidentity.Token{AccessToken: "accessToken"}, nil)
   900  
   901  	creds := AzureWorkloadIdentityCreds{store: NoopCredsStore{}, tokenProvider: tokenprovider}
   902  
   903  	auth, err := newAuth("", creds)
   904  	require.NoError(t, err)
   905  	_, ok := auth.(*githttp.TokenAuth)
   906  	require.Truef(t, ok, "expected TokenAuth but got %T", auth)
   907  }
   908  
   909  func TestNewAuth(t *testing.T) {
   910  	tests := []struct {
   911  		name     string
   912  		repoURL  string
   913  		creds    Creds
   914  		expected transport.AuthMethod
   915  		wantErr  bool
   916  	}{
   917  		{
   918  			name:    "HTTPSCreds with bearer token",
   919  			repoURL: "https://github.com/org/repo.git",
   920  			creds: HTTPSCreds{
   921  				bearerToken: "test-token",
   922  			},
   923  			expected: &githttp.TokenAuth{Token: "test-token"},
   924  			wantErr:  false,
   925  		},
   926  		{
   927  			name:    "HTTPSCreds with basic auth",
   928  			repoURL: "https://github.com/org/repo.git",
   929  			creds: HTTPSCreds{
   930  				username: "test-user",
   931  				password: "test-password",
   932  			},
   933  			expected: &githttp.BasicAuth{Username: "test-user", Password: "test-password"},
   934  			wantErr:  false,
   935  		},
   936  		{
   937  			name:    "HTTPSCreds with basic auth no username",
   938  			repoURL: "https://github.com/org/repo.git",
   939  			creds: HTTPSCreds{
   940  				password: "test-password",
   941  			},
   942  			expected: &githttp.BasicAuth{Username: "x-access-token", Password: "test-password"},
   943  			wantErr:  false,
   944  		},
   945  	}
   946  
   947  	for _, tt := range tests {
   948  		t.Run(tt.name, func(t *testing.T) {
   949  			auth, err := newAuth(tt.repoURL, tt.creds)
   950  			if (err != nil) != tt.wantErr {
   951  				t.Errorf("newAuth() error = %v, wantErr %v", err, tt.wantErr)
   952  				return
   953  			}
   954  			assert.Equal(t, tt.expected, auth)
   955  		})
   956  	}
   957  }
   958  
   959  func Test_nativeGitClient_runCredentialedCmd(t *testing.T) {
   960  	tests := []struct {
   961  		name         string
   962  		creds        Creds
   963  		environ      []string
   964  		expectedArgs []string
   965  		expectedEnv  []string
   966  		expectedErr  bool
   967  	}{
   968  		{
   969  			name: "basic auth header set",
   970  			creds: &mockCreds{
   971  				environ: []string{forceBasicAuthHeaderEnv + "=Basic dGVzdDp0ZXN0"},
   972  			},
   973  			expectedArgs: []string{"--config-env", "http.extraHeader=" + forceBasicAuthHeaderEnv, "status"},
   974  			expectedEnv:  []string{forceBasicAuthHeaderEnv + "=Basic dGVzdDp0ZXN0"},
   975  			expectedErr:  false,
   976  		},
   977  		{
   978  			name: "bearer auth header set",
   979  			creds: &mockCreds{
   980  				environ: []string{bearerAuthHeaderEnv + "=Bearer test-token"},
   981  			},
   982  			expectedArgs: []string{"--config-env", "http.extraHeader=" + bearerAuthHeaderEnv, "status"},
   983  			expectedEnv:  []string{bearerAuthHeaderEnv + "=Bearer test-token"},
   984  			expectedErr:  false,
   985  		},
   986  		{
   987  			name: "no auth header set",
   988  			creds: &mockCreds{
   989  				environ: []string{},
   990  			},
   991  			expectedArgs: []string{"status"},
   992  			expectedEnv:  []string{},
   993  			expectedErr:  false,
   994  		},
   995  		{
   996  			name: "error getting environment",
   997  			creds: &mockCreds{
   998  				environErr: true,
   999  			},
  1000  			expectedArgs: []string{},
  1001  			expectedEnv:  []string{},
  1002  			expectedErr:  true,
  1003  		},
  1004  	}
  1005  
  1006  	for _, tt := range tests {
  1007  		t.Run(tt.name, func(t *testing.T) {
  1008  			client := &nativeGitClient{
  1009  				creds: tt.creds,
  1010  			}
  1011  
  1012  			err := client.runCredentialedCmd("status")
  1013  			if (err != nil) != tt.expectedErr {
  1014  				t.Errorf("runCredentialedCmd() error = %v, expectedErr %v", err, tt.expectedErr)
  1015  				return
  1016  			}
  1017  
  1018  			if tt.expectedErr {
  1019  				return
  1020  			}
  1021  
  1022  			cmd := exec.Command("git", tt.expectedArgs...)
  1023  			cmd.Env = append(os.Environ(), tt.expectedEnv...)
  1024  			output, err := cmd.CombinedOutput()
  1025  			if err != nil {
  1026  				t.Errorf("runCredentialedCmd() command error = %v, output = %s", err, output)
  1027  			}
  1028  		})
  1029  	}
  1030  }
  1031  
  1032  func Test_LsFiles_RaceCondition(t *testing.T) {
  1033  	// Create two temporary directories and initialize them as git repositories
  1034  	tempDir1 := t.TempDir()
  1035  	tempDir2 := t.TempDir()
  1036  
  1037  	client1, err := NewClient("file://"+tempDir1, NopCreds{}, true, false, "", "")
  1038  	require.NoError(t, err)
  1039  	client2, err := NewClient("file://"+tempDir2, NopCreds{}, true, false, "", "")
  1040  	require.NoError(t, err)
  1041  
  1042  	err = client1.Init()
  1043  	require.NoError(t, err)
  1044  	err = client2.Init()
  1045  	require.NoError(t, err)
  1046  
  1047  	// Add different files to each repository
  1048  	file1 := filepath.Join(client1.Root(), "file1.txt")
  1049  	err = os.WriteFile(file1, []byte("content1"), 0o644)
  1050  	require.NoError(t, err)
  1051  	err = runCmd(client1.Root(), "git", "add", "file1.txt")
  1052  	require.NoError(t, err)
  1053  	err = runCmd(client1.Root(), "git", "commit", "-m", "Add file1")
  1054  	require.NoError(t, err)
  1055  
  1056  	file2 := filepath.Join(client2.Root(), "file2.txt")
  1057  	err = os.WriteFile(file2, []byte("content2"), 0o644)
  1058  	require.NoError(t, err)
  1059  	err = runCmd(client2.Root(), "git", "add", "file2.txt")
  1060  	require.NoError(t, err)
  1061  	err = runCmd(client2.Root(), "git", "commit", "-m", "Add file2")
  1062  	require.NoError(t, err)
  1063  
  1064  	// Assert that LsFiles returns the correct files when called sequentially
  1065  	files1, err := client1.LsFiles("*", true)
  1066  	require.NoError(t, err)
  1067  	require.Contains(t, files1, "file1.txt")
  1068  
  1069  	files2, err := client2.LsFiles("*", true)
  1070  	require.NoError(t, err)
  1071  	require.Contains(t, files2, "file2.txt")
  1072  
  1073  	// Define a function to call LsFiles multiple times in parallel
  1074  	var wg sync.WaitGroup
  1075  	callLsFiles := func(client Client, expectedFile string) {
  1076  		defer wg.Done()
  1077  		for i := 0; i < 100; i++ {
  1078  			files, err := client.LsFiles("*", true)
  1079  			require.NoError(t, err)
  1080  			require.Contains(t, files, expectedFile)
  1081  		}
  1082  	}
  1083  
  1084  	// Call LsFiles in parallel for both clients
  1085  	wg.Add(2)
  1086  	go callLsFiles(client1, "file1.txt")
  1087  	go callLsFiles(client2, "file2.txt")
  1088  	wg.Wait()
  1089  }
  1090  
  1091  type mockCreds struct {
  1092  	environ    []string
  1093  	environErr bool
  1094  }
  1095  
  1096  func (m *mockCreds) Environ() (io.Closer, []string, error) {
  1097  	if m.environErr {
  1098  		return nil, nil, errors.New("error getting environment")
  1099  	}
  1100  	return io.NopCloser(nil), m.environ, nil
  1101  }
  1102  
  1103  func (m *mockCreds) GetUserInfo(_ context.Context) (string, string, error) {
  1104  	return "", "", nil
  1105  }
  1106  
  1107  func Test_GetReferences(t *testing.T) {
  1108  	t.Parallel()
  1109  
  1110  	now := time.Now()
  1111  
  1112  	tests := []struct {
  1113  		name               string
  1114  		input              string
  1115  		expectedReferences []RevisionReference
  1116  		expectedMessage    string
  1117  	}{
  1118  		{
  1119  			name:               "No trailers",
  1120  			input:              "This is a commit message without trailers.",
  1121  			expectedReferences: nil,
  1122  			expectedMessage:    "This is a commit message without trailers.\n",
  1123  		},
  1124  		{
  1125  			name: "Invalid trailers",
  1126  			input: `Argocd-reference-commit-repourl: % invalid %
  1127  Argocd-reference-commit-date: invalid-date
  1128  Argocd-reference-commit-sha: xyz123
  1129  Argocd-reference-commit-body: this isn't json
  1130  Argocd-reference-commit-author: % not email %
  1131  Argocd-reference-commit-bogus:`,
  1132  			expectedReferences: nil,
  1133  			expectedMessage: `Argocd-reference-commit-repourl: % invalid %
  1134  Argocd-reference-commit-date: invalid-date
  1135  Argocd-reference-commit-sha: xyz123
  1136  Argocd-reference-commit-body: this isn't json
  1137  Argocd-reference-commit-author: % not email %
  1138  Argocd-reference-commit-bogus:
  1139  `,
  1140  		},
  1141  		{
  1142  			name:               "Unknown trailers",
  1143  			input:              "Argocd-reference-commit-unknown: foobar",
  1144  			expectedReferences: nil,
  1145  			expectedMessage:    "Argocd-reference-commit-unknown: foobar\n",
  1146  		},
  1147  		{
  1148  			name: "Some valid and Invalid trailers",
  1149  			input: `Argocd-reference-commit-sha: abc123
  1150  Argocd-reference-commit-repourl: % invalid %
  1151  Argocd-reference-commit-date: invalid-date`,
  1152  			expectedReferences: []RevisionReference{
  1153  				{
  1154  					Commit: &CommitMetadata{
  1155  						SHA: "abc123",
  1156  					},
  1157  				},
  1158  			},
  1159  			expectedMessage: `Argocd-reference-commit-repourl: % invalid %
  1160  Argocd-reference-commit-date: invalid-date
  1161  `,
  1162  		},
  1163  		{
  1164  			name: "Valid trailers",
  1165  			input: fmt.Sprintf(`Argocd-reference-commit-repourl: https://github.com/org/repo.git
  1166  Argocd-reference-commit-author: John Doe <john.doe@example.com>
  1167  Argocd-reference-commit-date: %s
  1168  Argocd-reference-commit-subject: Fix bug
  1169  Argocd-reference-commit-body: "Fix bug\n\nSome: trailer"
  1170  Argocd-reference-commit-sha: abc123`, now.Format(time.RFC3339)),
  1171  			expectedReferences: []RevisionReference{
  1172  				{
  1173  					Commit: &CommitMetadata{
  1174  						Author: mail.Address{
  1175  							Name:    "John Doe",
  1176  							Address: "john.doe@example.com",
  1177  						},
  1178  						Date:    now.Format(time.RFC3339),
  1179  						Body:    "Fix bug\n\nSome: trailer",
  1180  						Subject: "Fix bug",
  1181  						SHA:     "abc123",
  1182  						RepoURL: "https://github.com/org/repo.git",
  1183  					},
  1184  				},
  1185  			},
  1186  			expectedMessage: "",
  1187  		},
  1188  		{
  1189  			name: "Duplicate trailers",
  1190  			input: `Argocd-reference-commit-repourl: https://github.com/org/repo.git
  1191  Argocd-reference-commit-repourl: https://github.com/another/repo.git`,
  1192  			expectedReferences: []RevisionReference{
  1193  				{
  1194  					Commit: &CommitMetadata{
  1195  						RepoURL: "https://github.com/another/repo.git",
  1196  					},
  1197  				},
  1198  			},
  1199  			expectedMessage: "",
  1200  		},
  1201  	}
  1202  
  1203  	for _, tt := range tests {
  1204  		t.Run(tt.name, func(t *testing.T) {
  1205  			t.Parallel()
  1206  
  1207  			logCtx := log.WithFields(log.Fields{})
  1208  			result, message := GetReferences(logCtx, tt.input)
  1209  			assert.Equal(t, tt.expectedReferences, result)
  1210  			assert.Equal(t, tt.expectedMessage, message)
  1211  		})
  1212  	}
  1213  }
  1214  
  1215  func Test_BuiltinConfig(t *testing.T) {
  1216  	tempDir := t.TempDir()
  1217  	for _, enabled := range []bool{false, true} {
  1218  		client, err := NewClientExt("file://"+tempDir, tempDir, NopCreds{}, true, false, "", "", WithBuiltinGitConfig(enabled))
  1219  		require.NoError(t, err)
  1220  		native := client.(*nativeGitClient)
  1221  
  1222  		configOut, err := native.config("--list", "--show-origin")
  1223  		require.NoError(t, err)
  1224  		for k, v := range builtinGitConfig {
  1225  			r := regexp.MustCompile(fmt.Sprintf("(?m)^command line:\\s+%s=%s$", strings.ToLower(k), regexp.QuoteMeta(v)))
  1226  			matches := r.FindString(configOut)
  1227  			if enabled {
  1228  				assert.NotEmpty(t, matches, "missing builtin configuration option: %s=%s", k, v)
  1229  			} else {
  1230  				assert.Empty(t, matches, "unexpected builtin configuration when builtin config is disabled: %s=%s", k, v)
  1231  			}
  1232  		}
  1233  	}
  1234  }
  1235  
  1236  func Test_GitNoDetachedMaintenance(t *testing.T) {
  1237  	tempDir := t.TempDir()
  1238  	ctx := t.Context()
  1239  
  1240  	client, err := NewClientExt("file://"+tempDir, tempDir, NopCreds{}, true, false, "", "")
  1241  	require.NoError(t, err)
  1242  	native := client.(*nativeGitClient)
  1243  
  1244  	err = client.Init()
  1245  	require.NoError(t, err)
  1246  
  1247  	cmd := exec.CommandContext(ctx, "git", "fetch")
  1248  	// trace execution of Git subcommands and their arguments to stderr
  1249  	cmd.Env = append(cmd.Env, "GIT_TRACE=true")
  1250  	// Ignore system config in case it disables auto maintenance
  1251  	cmd.Env = append(cmd.Env, "GIT_CONFIG_NOSYSTEM=true")
  1252  	output, err := native.runCmdOutput(cmd, runOpts{CaptureStderr: true})
  1253  	require.NoError(t, err)
  1254  
  1255  	lines := strings.Split(output, "\n")
  1256  	for _, line := range lines {
  1257  		if strings.Contains(line, "git maintenance run") {
  1258  			assert.NotContains(t, output, "--detach", "Unexpected --detach when running git maintenance")
  1259  			return
  1260  		}
  1261  	}
  1262  	assert.Fail(t, "Expected to see `git maintenance` run after `git fetch`")
  1263  }