diff options
author | Pavlo Strokov <pstrokov@gitlab.com> | 2020-11-02 20:43:15 +0300 |
---|---|---|
committer | Paul Okstad <pokstad@gitlab.com> | 2020-11-02 20:43:15 +0300 |
commit | e51ae2232edaa0201c65b94b66e437249156cd06 (patch) | |
tree | e79fc287f7ae72ab6fd1597af49a29d7ead7dcfe | |
parent | 98fe9f19fcecf4b9d27ab60dfaf6d127447a0350 (diff) |
Extending of git.Repository with fetch command
In order to support migration of functionality from Ruby to Go implementation
it makes sense to extend existing git.Repository interface with new methods
in order to re-use them during code porting.
This change adds partial implementation of the git fetch sub-command.
Currently it is used only in tests, but would be in use in the future
to omit code duplication.
Part of: https://gitlab.com/gitlab-org/gitaly/-/issues/1466
-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) + }) +} |