diff options
author | Paul Okstad <pokstad@gitlab.com> | 2020-11-02 20:43:16 +0300 |
---|---|---|
committer | Paul Okstad <pokstad@gitlab.com> | 2020-11-02 20:43:16 +0300 |
commit | 38542329a8d1e62410e184beb3347b3916ed8b03 (patch) | |
tree | 8d73bf4844ea0038fbc461f801e79e8c00264c24 | |
parent | d2ce8e3feb84b1a74121a52be4bb7d20fc860391 (diff) | |
parent | e51ae2232edaa0201c65b94b66e437249156cd06 (diff) |
Merge branch 'ps-repository-fetch-cmd' into 'master'
Extending of git.Repository with fetch command
See merge request gitlab-org/gitaly!2721
-rw-r--r-- | internal/git/repository.go | 82 | ||||
-rw-r--r-- | internal/git/repository_test.go | 150 |
2 files changed, 232 insertions, 0 deletions
diff --git a/internal/git/repository.go b/internal/git/repository.go index 2157b3cc6..a8a23099b 100644 --- a/internal/git/repository.go +++ b/internal/git/repository.go @@ -32,6 +32,61 @@ var ( ErrNotFound = errors.New("not found") ) +// FetchOptsTags controls what tags needs to be imported on fetch. +type FetchOptsTags string + +func (t FetchOptsTags) String() string { + return string(t) +} + +var ( + // FetchOptsTagsDefault enables importing of tags only on fetched branches. + FetchOptsTagsDefault = FetchOptsTags("") + // FetchOptsTagsAll enables importing of every tag from the remote repository. + FetchOptsTagsAll = FetchOptsTags("--tags") + // FetchOptsTagsNone disables importing of tags from the remote repository. + FetchOptsTagsNone = FetchOptsTags("--no-tags") +) + +// FetchOpts is used to configure invocation of the 'FetchRemote' command. +type FetchOpts struct { + // Env is a list of env vars to pass to the cmd. + Env []string + // Global is a list of global flags to use with 'git' command. + Global []Option + // Prune if set fetch removes any remote-tracking references that no longer exist on the remote. + // https://git-scm.com/docs/git-fetch#Documentation/git-fetch.txt---prune + Prune bool + // Force if set fetch overrides local references with values from remote that's + // doesn't have the previous commit as an ancestor. + // https://git-scm.com/docs/git-fetch#Documentation/git-fetch.txt---force + Force bool + // Tags controls whether tags will be fetched as part of the remote or not. + // https://git-scm.com/docs/git-fetch#Documentation/git-fetch.txt---tags + // https://git-scm.com/docs/git-fetch#Documentation/git-fetch.txt---no-tags + Tags FetchOptsTags + // Stderr if set it would be used to redirect stderr stream into it. + Stderr io.Writer +} + +func (opts FetchOpts) buildFlags() []Option { + flags := []Option{Flag{Name: "--quiet"}} + + if opts.Prune { + flags = append(flags, Flag{Name: "--prune"}) + } + + if opts.Force { + flags = append(flags, Flag{Name: "--force"}) + } + + if opts.Tags != FetchOptsTagsDefault { + flags = append(flags, Flag{Name: opts.Tags.String()}) + } + + return flags +} + // Repository represents a Git repository. type Repository interface { // ResolveRef resolves the given refish to its object ID. This uses the @@ -77,6 +132,9 @@ type Repository interface { // is returned if the oid does not refer to a valid object. ReadObject(ctx context.Context, oid string) ([]byte, error) + // FetchRemote fetches changes from the specified remote. + FetchRemote(ctx context.Context, remoteName string, opts FetchOpts) error + // Config returns executor of the 'config' sub-command. Config() Config @@ -128,6 +186,10 @@ func (UnimplementedRepo) ReadObject(context.Context, string) ([]byte, error) { return nil, ErrUnimplemented } +func (UnimplementedRepo) FetchRemote(context.Context, string, FetchOpts) error { + return ErrUnimplemented +} + func (UnimplementedRepo) Remote() Remote { return UnimplementedRemote{} } @@ -341,6 +403,26 @@ func (repo *localRepository) UpdateRef(ctx context.Context, reference, newrev, o return nil } +func (repo *localRepository) FetchRemote(ctx context.Context, remoteName string, opts FetchOpts) error { + if err := validateNotBlank(remoteName, "remoteName"); err != nil { + return err + } + + cmd, err := SafeCmdWithEnv(ctx, opts.Env, repo.repo, opts.Global, + SubCmd{ + Name: "fetch", + Flags: opts.buildFlags(), + Args: []string{remoteName}, + }, + WithStderr(opts.Stderr), + ) + if err != nil { + return err + } + + return cmd.Wait() +} + func (repo *localRepository) Config() Config { return RepositoryConfig{repo: repo.repo} } diff --git a/internal/git/repository_test.go b/internal/git/repository_test.go index 78ee145ff..fbd5c6b7b 100644 --- a/internal/git/repository_test.go +++ b/internal/git/repository_test.go @@ -1,11 +1,13 @@ package git_test import ( + "bytes" "errors" "fmt" "io" "io/ioutil" "os" + "os/exec" "path/filepath" "strings" "testing" @@ -13,6 +15,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitaly/client" + "gitlab.com/gitlab-org/gitaly/internal/command" "gitlab.com/gitlab-org/gitaly/internal/git" "gitlab.com/gitlab-org/gitaly/internal/gitaly/config" "gitlab.com/gitlab-org/gitaly/internal/helper" @@ -514,3 +517,150 @@ func TestLocalRepository_UpdateRef(t *testing.T) { }) } } + +func TestLocalRepository_FetchRemote(t *testing.T) { + ctx, cancel := testhelper.Context() + defer cancel() + + initBareWithRemote := func(t *testing.T, remote string) (git.Repository, string, testhelper.Cleanup) { + t.Helper() + + testRepo, testRepoPath, cleanup := testhelper.InitBareRepo(t) + + cmd := exec.Command(command.GitPath(), "-C", testRepoPath, "remote", "add", remote, testhelper.GitlabTestStoragePath()+"/gitlab-test.git") + err := cmd.Run() + if err != nil { + cleanup() + t.Log(err) + t.FailNow() + } + + return git.NewRepository(testRepo), testRepoPath, cleanup + } + + t.Run("invalid name", func(t *testing.T) { + repo := git.NewRepository(nil) + + err := repo.FetchRemote(ctx, " ", git.FetchOpts{}) + require.True(t, errors.Is(err, git.ErrInvalidArg)) + require.Contains(t, err.Error(), `"remoteName" is blank or empty`) + }) + + t.Run("unknown remote", func(t *testing.T) { + testRepo, _, cleanup := testhelper.InitBareRepo(t) + defer cleanup() + + repo := git.NewRepository(testRepo) + var stderr bytes.Buffer + err := repo.FetchRemote(ctx, "stub", git.FetchOpts{Stderr: &stderr}) + require.Error(t, err) + require.Contains(t, stderr.String(), "'stub' does not appear to be a git repository") + }) + + t.Run("ok", func(t *testing.T) { + repo, testRepoPath, cleanup := initBareWithRemote(t, "origin") + defer cleanup() + + var stderr bytes.Buffer + require.NoError(t, repo.FetchRemote(ctx, "origin", git.FetchOpts{Stderr: &stderr})) + + require.Empty(t, stderr.String(), "it should not produce output as it is called with --quite flag by default") + + fetchHeadData, err := ioutil.ReadFile(filepath.Join(testRepoPath, "FETCH_HEAD")) + require.NoError(t, err, "it should create FETCH_HEAD with info about fetch") + + fetchHead := string(fetchHeadData) + require.Contains(t, fetchHead, "e56497bb5f03a90a51293fc6d516788730953899 not-for-merge branch ''test''") + require.Contains(t, fetchHead, "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b not-for-merge tag 'v1.1.0'") + + sha, err := repo.ResolveRefish(ctx, "refs/remotes/origin/master^{commit}") + require.NoError(t, err, "the object from remote should exists in local after fetch done") + require.Equal(t, "1e292f8fedd741b75372e19097c76d327140c312", sha) + }) + + t.Run("with env", func(t *testing.T) { + _, sourceRepoPath, sourceCleanup := testhelper.NewTestRepo(t) + defer sourceCleanup() + + testRepo, testRepoPath, testCleanup := testhelper.NewTestRepo(t) + defer testCleanup() + + repo := git.NewRepository(testRepo) + testhelper.MustRunCommand(t, nil, "git", "-C", testRepoPath, "remote", "add", "source", sourceRepoPath) + + var stderr bytes.Buffer + require.NoError(t, repo.FetchRemote(ctx, "source", git.FetchOpts{Stderr: &stderr, Env: []string{"GIT_TRACE=1"}})) + require.Contains(t, stderr.String(), "trace: built-in: git fetch --quiet source --end-of-options") + }) + + t.Run("with globals", func(t *testing.T) { + _, sourceRepoPath, sourceCleanup := testhelper.NewTestRepo(t) + defer sourceCleanup() + + testRepo, testRepoPath, testCleanup := testhelper.NewTestRepo(t) + defer testCleanup() + + repo := git.NewRepository(testRepo) + testhelper.MustRunCommand(t, nil, "git", "-C", testRepoPath, "remote", "add", "source", sourceRepoPath) + + require.NoError(t, repo.FetchRemote(ctx, "source", git.FetchOpts{})) + + testhelper.MustRunCommand(t, nil, "git", "-C", testRepoPath, "branch", "--track", "testing-fetch-prune", "refs/remotes/source/markdown") + testhelper.MustRunCommand(t, nil, "git", "-C", sourceRepoPath, "branch", "-D", "markdown") + + require.NoError(t, repo.FetchRemote( + ctx, + "source", + git.FetchOpts{ + Global: []git.Option{git.ValueFlag{Name: "-c", Value: "fetch.prune=true"}}, + }), + ) + + contains, err := repo.ContainsRef(ctx, "refs/remotes/source/markdown") + require.NoError(t, err) + require.False(t, contains, "remote tracking branch should be pruned as it no longer exists on the remote") + }) + + t.Run("with prune", func(t *testing.T) { + _, sourceRepoPath, sourceCleanup := testhelper.NewTestRepo(t) + defer sourceCleanup() + + testRepo, testRepoPath, testCleanup := testhelper.NewTestRepo(t) + defer testCleanup() + + repo := git.NewRepository(testRepo) + + testhelper.MustRunCommand(t, nil, "git", "-C", testRepoPath, "remote", "add", "source", sourceRepoPath) + require.NoError(t, repo.FetchRemote(ctx, "source", git.FetchOpts{})) + + testhelper.MustRunCommand(t, nil, "git", "-C", testRepoPath, "branch", "--track", "testing-fetch-prune", "refs/remotes/source/markdown") + testhelper.MustRunCommand(t, nil, "git", "-C", sourceRepoPath, "branch", "-D", "markdown") + + require.NoError(t, repo.FetchRemote(ctx, "source", git.FetchOpts{Prune: true})) + + contains, err := repo.ContainsRef(ctx, "refs/remotes/source/markdown") + require.NoError(t, err) + require.False(t, contains, "remote tracking branch should be pruned as it no longer exists on the remote") + }) + + t.Run("with no tags", func(t *testing.T) { + repo, testRepoPath, cleanup := initBareWithRemote(t, "origin") + defer cleanup() + + tagsBefore := testhelper.MustRunCommand(t, nil, "git", "-C", testRepoPath, "tag", "--list") + require.Empty(t, tagsBefore) + + require.NoError(t, repo.FetchRemote(ctx, "origin", git.FetchOpts{Tags: git.FetchOptsTagsNone, Force: true})) + + tagsAfter := testhelper.MustRunCommand(t, nil, "git", "-C", testRepoPath, "tag", "--list") + require.Empty(t, tagsAfter) + + containsBranches, err := repo.ContainsRef(ctx, "'test'") + require.NoError(t, err) + require.False(t, containsBranches) + + containsTags, err := repo.ContainsRef(ctx, "v1.1.0") + require.NoError(t, err) + require.False(t, containsTags) + }) +} |