Skip to content

Commit

Permalink
git: add support for specifying refspecs for a push
Browse files Browse the repository at this point in the history
Add `PushConfig` for configuring a push operation. Users can use
`PushConfig.Refspecs` to specify the refspecs when using `Push()`.

Furthermore, fix a bug related to `Push()` where all refs were pushed to
origin, since we did not specify a refspec and the default refspec used
by gogit is `refs/heads/*:/refs/heads/*`.

Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
  • Loading branch information
aryan9600 committed May 15, 2023
1 parent e0c94db commit 88da1a6
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 46 deletions.
21 changes: 20 additions & 1 deletion git/gogit/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ func (g *Client) Commit(info git.Commit, commitOpts ...repository.CommitOption)
return commit.String(), nil
}

func (g *Client) Push(ctx context.Context) error {
func (g *Client) Push(ctx context.Context, cfg repository.PushConfig) error {
if g.repository == nil {
return git.ErrNoGitRepository
}
Expand All @@ -361,7 +361,26 @@ func (g *Client) Push(ctx context.Context) error {
return fmt.Errorf("failed to construct auth method with options: %w", err)
}

var refspecs []config.RefSpec
for _, ref := range cfg.Refspecs {
refspecs = append(refspecs, config.RefSpec(ref))
}

// If no refspecs were provided, we need to push the current ref HEAD points to.
// The format of a refspec for a Git push is generally something like
// "refs/heads/branch:refs/heads/branch".
if len(refspecs) == 0 {
head, err := g.repository.Head()
if err != nil {
return err
}

headRefspec := config.RefSpec(fmt.Sprintf("%s:%[1]s", head.Name()))
refspecs = append(refspecs, headRefspec)
}

return g.repository.PushContext(ctx, &extgogit.PushOptions{
RefSpecs: refspecs,
Force: g.forcePush,
RemoteName: extgogit.DefaultRemoteName,
Auth: authMethod,
Expand Down
177 changes: 140 additions & 37 deletions git/gogit/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,20 +176,12 @@ func TestCommit(t *testing.T) {
func TestPush(t *testing.T) {
g := NewWithT(t)

server, err := gittestserver.NewTempGitServer()
server, repoURL, err := setupGitServer(true)
g.Expect(err).ToNot(HaveOccurred())
defer os.RemoveAll(server.Root())
server.Auth("test-user", "test-pass")

err = server.InitRepo("../testdata/git/repo", git.DefaultBranch, "test.git")
g.Expect(err).ToNot(HaveOccurred())

err = server.StartHTTP()
g.Expect(err).ToNot(HaveOccurred())
defer server.StopHTTP()

tmp := t.TempDir()
repoURL := server.HTTPAddressWithCredentials() + "/" + "test.git"
auth, err := transportAuth(&git.AuthOptions{
Transport: git.HTTP,
Username: "test-user",
Expand All @@ -209,10 +201,29 @@ func TestPush(t *testing.T) {
g.Expect(err).ToNot(HaveOccurred())
ggc.repository = repo

// make a commit on master and push it.
cc, err := commitFile(repo, "test", "testing gogit push", time.Now())
g.Expect(err).ToNot(HaveOccurred())
err = ggc.Push(context.TODO(), repository.PushConfig{})
g.Expect(err).ToNot(HaveOccurred())

// make a dummy commit on master. this helps us make sure we don't push all
// refs and push only the ref HEAD points to.
dummyCC, err := commitFile(repo, "test", "dummy commit", time.Now())
g.Expect(err).ToNot(HaveOccurred())

// switch HEAD to a different branch
wt, err := repo.Worktree()
g.Expect(err).ToNot(HaveOccurred())
err = wt.Checkout(&extgogit.CheckoutOptions{
Branch: plumbing.NewBranchReferenceName("test"),
Create: true,
})
g.Expect(err).ToNot(HaveOccurred())

err = ggc.Push(context.TODO())
testCC, err := commitFile(repo, "test", "testing gogit push on test branch", time.Now())
g.Expect(err).ToNot(HaveOccurred())
err = ggc.Push(context.TODO(), repository.PushConfig{})
g.Expect(err).ToNot(HaveOccurred())

repo, err = extgogit.PlainClone(t.TempDir(), false, &extgogit.CloneOptions{
Expand All @@ -223,35 +234,109 @@ func TestPush(t *testing.T) {
ref, err := repo.Head()
g.Expect(err).ToNot(HaveOccurred())
g.Expect(ref.Hash().String()).To(Equal(cc.String()))
// this assertion is not required but highlights the fact that we
// indeed only push the ref HEAD points to.
g.Expect(ref.Hash().String()).ToNot(Equal(dummyCC.String()))

ref, err = repo.Reference(plumbing.NewRemoteReferenceName(git.DefaultRemote, "test"), true)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(ref.Hash().String()).To(Equal(testCC.String()))
}

func TestForcePush(t *testing.T) {
func TestPush_pushConfig_refspecs(t *testing.T) {
g := NewWithT(t)

server, err := gittestserver.NewTempGitServer()
server, repoURL, err := setupGitServer(false)
g.Expect(err).ToNot(HaveOccurred())
defer os.RemoveAll(server.Root())
server.Auth("test-user", "test-pass")
defer server.StopHTTP()
g.Expect(err).ToNot(HaveOccurred())

err = server.InitRepo("../testdata/git/repo", git.DefaultBranch, "test.git")
tmp := t.TempDir()
repo, err := extgogit.PlainClone(tmp, false, &extgogit.CloneOptions{
URL: repoURL,
RemoteName: git.DefaultRemote,
Tags: extgogit.NoTags,
})
g.Expect(err).ToNot(HaveOccurred())

err = server.StartHTTP()
ggc, err := NewClient(tmp, nil)
g.Expect(err).ToNot(HaveOccurred())
defer server.StopHTTP()
ggc.repository = repo

repoURL := server.HTTPAddressWithCredentials() + "/" + "test.git"
auth, err := transportAuth(&git.AuthOptions{
Transport: git.HTTP,
Username: "test-user",
Password: "test-pass",
}, false)
head, err := repo.Head()
g.Expect(err).ToNot(HaveOccurred())
_, err = tag(repo, head.Hash(), false, "v0.1.0", time.Now())
g.Expect(err).ToNot(HaveOccurred())

wt, err := repo.Worktree()
g.Expect(err).ToNot(HaveOccurred())
err = wt.Checkout(&extgogit.CheckoutOptions{
Branch: plumbing.NewBranchReferenceName("feature/refspecs"),
Create: true,
})
g.Expect(err).ToNot(HaveOccurred())
headOnFeature, err := commitFile(repo, "test", "testing it on feature/refspecs", time.Now())
g.Expect(err).ToNot(HaveOccurred())

// Create an extra tag to check later that we push only using the provided refspec,
_, err = tag(repo, headOnFeature, false, "v0.2.0", time.Now())
g.Expect(err).ToNot(HaveOccurred())

err = ggc.Push(context.TODO(), repository.PushConfig{
Refspecs: []string{
"refs/heads/feature/refspecs:refs/heads/feature/refspecs",
},
})
g.Expect(err).ToNot(HaveOccurred())

err = ggc.Push(context.TODO(), repository.PushConfig{
Refspecs: []string{
"refs/heads/feature/refspecs:refs/heads/prod/refspecs",
},
})

err = ggc.Push(context.TODO(), repository.PushConfig{
Refspecs: []string{
"refs/tags/v0.1.0:refs/tags/v0.1.0",
},
})

repo, err = extgogit.PlainClone(t.TempDir(), false, &extgogit.CloneOptions{
URL: repoURL,
})
g.Expect(err).ToNot(HaveOccurred())

remRefName := plumbing.NewRemoteReferenceName(extgogit.DefaultRemoteName, "feature/refspecs")
remRef, err := repo.Reference(remRefName, true)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(remRef.Hash().String()).To(Equal(headOnFeature.String()))

remRefName = plumbing.NewRemoteReferenceName(extgogit.DefaultRemoteName, "prod/refspecs")
remRef, err = repo.Reference(remRefName, true)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(remRef.Hash().String()).To(Equal(headOnFeature.String()))

tagRef, err := repo.Reference(plumbing.NewTagReferenceName("v0.1.0"), true)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(tagRef.Hash().String()).To(Equal(head.Hash().String()))

tagRef, err = repo.Reference(plumbing.NewTagReferenceName("v0.2.0"), true)
g.Expect(err).To(HaveOccurred())
}

func TestForcePush(t *testing.T) {
g := NewWithT(t)

server, repoURL, err := setupGitServer(false)
g.Expect(err).ToNot(HaveOccurred())
defer os.RemoveAll(server.Root())
defer server.StopHTTP()
g.Expect(err).ToNot(HaveOccurred())

tmp1 := t.TempDir()
repo1, err := extgogit.PlainClone(tmp1, false, &extgogit.CloneOptions{
URL: repoURL,
Auth: auth,
RemoteName: git.DefaultRemote,
Tags: extgogit.NoTags,
})
Expand All @@ -267,7 +352,6 @@ func TestForcePush(t *testing.T) {
tmp2 := t.TempDir()
repo2, err := extgogit.PlainClone(tmp2, false, &extgogit.CloneOptions{
URL: repoURL,
Auth: auth,
RemoteName: git.DefaultRemote,
Tags: extgogit.NoTags,
})
Expand All @@ -281,23 +365,22 @@ func TestForcePush(t *testing.T) {
ggc2.repository = repo2

// First push from ggc1 should work.
err = ggc1.Push(context.TODO())
err = ggc1.Push(context.TODO(), repository.PushConfig{})
g.Expect(err).ToNot(HaveOccurred())

// Force push from ggc2 should override ggc1.
err = ggc2.Push(context.TODO())
err = ggc2.Push(context.TODO(), repository.PushConfig{})
g.Expect(err).ToNot(HaveOccurred())

// Follow-up push from ggc1 errors.
_, err = commitFile(repo1, "test", "amend file again", time.Now())
g.Expect(err).ToNot(HaveOccurred())

err = ggc1.Push(context.TODO())
err = ggc1.Push(context.TODO(), repository.PushConfig{})
g.Expect(err).To(HaveOccurred())

repo, err := extgogit.PlainClone(t.TempDir(), false, &extgogit.CloneOptions{
URL: repoURL,
Auth: auth,
URL: repoURL,
})
g.Expect(err).ToNot(HaveOccurred())
ref, err := repo.Head()
Expand Down Expand Up @@ -554,23 +637,16 @@ func TestSwitchBranch(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)

server, err := gittestserver.NewTempGitServer()
server, repoURL, err := setupGitServer(false)
g.Expect(err).ToNot(HaveOccurred())
defer os.RemoveAll(server.Root())

err = server.InitRepo("../testdata/git/repo", git.DefaultBranch, "test.git")
g.Expect(err).ToNot(HaveOccurred())

err = server.StartHTTP()
g.Expect(err).ToNot(HaveOccurred())
defer server.StopHTTP()

var expectedHash string
if tt.setupFunc != nil {
expectedHash = tt.setupFunc(g, filepath.Join(server.Root(), "test.git"))
}

repoURL := server.HTTPAddressWithCredentials() + "/" + "test.git"
tmp := t.TempDir()
repo, err := extgogit.PlainClone(tmp, false, &extgogit.CloneOptions{
URL: repoURL,
Expand Down Expand Up @@ -756,3 +832,30 @@ func TestValidateUrl(t *testing.T) {
})
}
}

// setupGitServer sets up, starts an HTTP Git server. It initialzes
// a repo on the server and then returns the server and the URL of the
// initialized repository. The auth argument can be set to true to enable
// basic auth.
func setupGitServer(auth bool) (*gittestserver.GitServer, string, error) {
server, err := gittestserver.NewTempGitServer()
if err != nil {
return nil, "", err
}
if auth {
server.Auth("test-user", "test-pass")
}

err = server.InitRepo("../testdata/git/repo", git.DefaultBranch, "test.git")
if err != nil {
return nil, "", err
}

err = server.StartHTTP()
if err != nil {
return nil, "", err
}

repoURL := server.HTTPAddressWithCredentials() + "/" + "test.git"
return server, repoURL, nil
}
12 changes: 6 additions & 6 deletions git/internal/e2e/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func testUsingClone(g *WithT, client repository.Client, repoURL *url.URL, upstre
)
g.Expect(err).ToNot(HaveOccurred(), "first commit")

err = client.Push(context.TODO())
err = client.Push(context.TODO(), repository.PushConfig{})
g.Expect(err).ToNot(HaveOccurred())

headCommit, _, err := headCommitWithBranch(upstreamRepo.url, "main", upstreamRepo.username, upstreamRepo.password)
Expand All @@ -82,7 +82,7 @@ func testUsingClone(g *WithT, client repository.Client, repoURL *url.URL, upstre
)
g.Expect(err).ToNot(HaveOccurred(), "second commit")

err = client.Push(context.TODO())
err = client.Push(context.TODO(), repository.PushConfig{})
g.Expect(err).ToNot(HaveOccurred())
headCommit, branch, err := headCommitWithBranch(upstreamRepo.url, "new", upstreamRepo.username, upstreamRepo.password)
g.Expect(err).ToNot(HaveOccurred())
Expand All @@ -100,7 +100,7 @@ func testUsingClone(g *WithT, client repository.Client, repoURL *url.URL, upstre
}),
)
g.Expect(err).ToNot(HaveOccurred(), "third commit")
err = client.Push(context.TODO())
err = client.Push(context.TODO(), repository.PushConfig{})
g.Expect(err).ToNot(HaveOccurred())
headCommit, _, err = headCommitWithBranch(upstreamRepo.url, "new", upstreamRepo.username, upstreamRepo.password)
g.Expect(err).ToNot(HaveOccurred())
Expand All @@ -120,7 +120,7 @@ func testUsingInit(g *WithT, client repository.Client, repoURL *url.URL, upstrea
)
g.Expect(err).ToNot(HaveOccurred(), "first commit")

err = client.Push(context.TODO())
err = client.Push(context.TODO(), repository.PushConfig{})
g.Expect(err).ToNot(HaveOccurred())

headCommit, _, err := headCommitWithBranch(upstreamRepo.url, "main", upstreamRepo.username, upstreamRepo.password)
Expand All @@ -138,7 +138,7 @@ func testUsingInit(g *WithT, client repository.Client, repoURL *url.URL, upstrea
)
g.Expect(err).ToNot(HaveOccurred(), "second commit")

err = client.Push(context.TODO())
err = client.Push(context.TODO(), repository.PushConfig{})
g.Expect(err).ToNot(HaveOccurred())
headCommit, branch, err := headCommitWithBranch(upstreamRepo.url, "new", upstreamRepo.username, upstreamRepo.password)
g.Expect(err).ToNot(HaveOccurred())
Expand All @@ -155,7 +155,7 @@ func testUsingInit(g *WithT, client repository.Client, repoURL *url.URL, upstrea
}),
)
g.Expect(err).ToNot(HaveOccurred(), "third commit")
err = client.Push(context.TODO())
err = client.Push(context.TODO(), repository.PushConfig{})
g.Expect(err).ToNot(HaveOccurred())
headCommit, _, err = headCommitWithBranch(upstreamRepo.url, "new", upstreamRepo.username, upstreamRepo.password)
g.Expect(err).ToNot(HaveOccurred())
Expand Down
6 changes: 4 additions & 2 deletions git/repository/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ type Writer interface {
// Init initializes a repository at the configured path with the remote
// origin set to url on the provided branch.
Init(ctx context.Context, url, branch string) error
// Push pushes the current branch of the repository to origin.
Push(ctx context.Context) error
// Push performs a Git push to origin. By default, it pushes the
// reference pointed to by the HEAD, to its equivalent destination at
// the origin, but this is configurable via PushConfig.
Push(ctx context.Context, cfg PushConfig) error
// SwitchBranch switches from the current branch of the repository to the
// provided branch. If the branch doesn't exist, it is created.
SwitchBranch(ctx context.Context, branch string) error
Expand Down
8 changes: 8 additions & 0 deletions git/repository/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ type CloneOptions struct {
ShallowClone bool
}

// PushConfig provides configuration options for a Git push.
type PushConfig struct {
// Refspecs is a list of refspecs to use for the push operation.
// For details about Git Refspecs, please see:
// https://git-scm.com/book/en/v2/Git-Internals-The-Refspec
Refspecs []string
}

// CheckoutStrategy provides options to checkout a repository to a target.
type CheckoutStrategy struct {
// Branch to checkout. If supported by the client, it can be combined
Expand Down

0 comments on commit 88da1a6

Please sign in to comment.