source-controller/pkg/git/gogit/checkout_test.go

438 lines
10 KiB
Go

/*
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package gogit
import (
"context"
"errors"
"os"
"path/filepath"
"testing"
"time"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-billy/v5/osfs"
extgogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/cache"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/storage/filesystem"
. "github.com/onsi/gomega"
)
func TestCheckoutBranch_Checkout(t *testing.T) {
repo, path, err := initRepo()
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(path)
firstCommit, err := commitFile(repo, "branch", "init", time.Now())
if err != nil {
t.Fatal(err)
}
if err = createBranch(repo, "test"); err != nil {
t.Fatal(err)
}
secondCommit, err := commitFile(repo, "branch", "second", time.Now())
if err != nil {
t.Fatal(err)
}
tests := []struct {
name string
branch string
expectedCommit string
expectedErr string
}{
{
name: "Default branch",
branch: "master",
expectedCommit: firstCommit.String(),
},
{
name: "Other branch",
branch: "test",
expectedCommit: secondCommit.String(),
},
{
name: "Non existing branch",
branch: "invalid",
expectedErr: "couldn't find remote ref \"refs/heads/invalid\"",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
branch := CheckoutBranch{
Branch: tt.branch,
}
tmpDir, _ := os.MkdirTemp("", "test")
defer os.RemoveAll(tmpDir)
cc, err := branch.Checkout(context.TODO(), tmpDir, path, nil)
if tt.expectedErr != "" {
g.Expect(err.Error()).To(ContainSubstring(tt.expectedErr))
g.Expect(cc).To(BeNil())
return
}
g.Expect(err).To(BeNil())
g.Expect(cc.String()).To(Equal(tt.branch + "/" + tt.expectedCommit))
})
}
}
func TestCheckoutTag_Checkout(t *testing.T) {
tests := []struct {
name string
tag string
annotated bool
checkoutTag string
expectTag string
expectErr string
}{
{
name: "Tag",
tag: "tag-1",
checkoutTag: "tag-1",
expectTag: "tag-1",
},
{
name: "Annotated",
tag: "annotated",
annotated: true,
checkoutTag: "annotated",
expectTag: "annotated",
},
{
name: "Non existing tag",
tag: "tag-1",
checkoutTag: "invalid",
expectErr: "error: couldn't find remote ref \"refs/tags/invalid\"",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
repo, path, err := initRepo()
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(path)
var h plumbing.Hash
if tt.tag != "" {
h, err = commitFile(repo, "tag", tt.tag, time.Now())
if err != nil {
t.Fatal(err)
}
_, err = tag(repo, h, !tt.annotated, tt.tag, time.Now())
if err != nil {
t.Fatal(err)
}
}
tag := CheckoutTag{
Tag: tt.checkoutTag,
}
tmpDir, _ := os.MkdirTemp("", "test")
defer os.RemoveAll(tmpDir)
cc, err := tag.Checkout(context.TODO(), tmpDir, path, nil)
if tt.expectErr != "" {
g.Expect(err.Error()).To(ContainSubstring(tt.expectErr))
g.Expect(cc).To(BeNil())
return
}
g.Expect(err).To(BeNil())
g.Expect(cc.String()).To(Equal(tt.expectTag + "/" + h.String()))
g.Expect(filepath.Join(tmpDir, "tag")).To(BeARegularFile())
g.Expect(os.ReadFile(filepath.Join(tmpDir, "tag"))).To(BeEquivalentTo(tt.tag))
})
}
}
func TestCheckoutCommit_Checkout(t *testing.T) {
repo, path, err := initRepo()
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(path)
firstCommit, err := commitFile(repo, "commit", "init", time.Now())
if err != nil {
t.Fatal(err)
}
if err = createBranch(repo, "other-branch"); err != nil {
t.Fatal(err)
}
secondCommit, err := commitFile(repo, "commit", "second", time.Now())
if err != nil {
t.Fatal(err)
}
tests := []struct {
name string
commit string
branch string
expectCommit string
expectFile string
expectError string
}{
{
name: "Commit",
commit: firstCommit.String(),
expectCommit: "HEAD/" + firstCommit.String(),
expectFile: "init",
},
{
name: "Commit in specific branch",
commit: secondCommit.String(),
branch: "other-branch",
expectCommit: "other-branch/" + secondCommit.String(),
expectFile: "second",
},
{
name: "Non existing commit",
commit: "a-random-invalid-commit",
expectError: "failed to resolve commit object for 'a-random-invalid-commit': object not found",
},
{
name: "Non existing commit in specific branch",
commit: secondCommit.String(),
branch: "master",
expectError: "object not found",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
commit := CheckoutCommit{
Commit: tt.commit,
Branch: tt.branch,
}
tmpDir, err := os.MkdirTemp("", "git2go")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
cc, err := commit.Checkout(context.TODO(), tmpDir, path, nil)
if tt.expectError != "" {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(tt.expectError))
g.Expect(cc).To(BeNil())
return
}
g.Expect(err).ToNot(HaveOccurred())
g.Expect(cc).ToNot(BeNil())
g.Expect(cc.String()).To(Equal(tt.expectCommit))
g.Expect(filepath.Join(tmpDir, "commit")).To(BeARegularFile())
g.Expect(os.ReadFile(filepath.Join(tmpDir, "commit"))).To(BeEquivalentTo(tt.expectFile))
})
}
}
func TestCheckoutTagSemVer_Checkout(t *testing.T) {
now := time.Now()
tags := []struct {
tag string
annotated bool
commitTime time.Time
tagTime time.Time
}{
{
tag: "v0.0.1",
annotated: false,
commitTime: now,
},
{
tag: "v0.1.0+build-1",
annotated: true,
commitTime: now.Add(10 * time.Minute),
tagTime: now.Add(2 * time.Hour), // This should be ignored during TS comparisons
},
{
tag: "v0.1.0+build-2",
annotated: false,
commitTime: now.Add(30 * time.Minute),
},
{
tag: "v0.1.0+build-3",
annotated: true,
commitTime: now.Add(1 * time.Hour),
tagTime: now.Add(1 * time.Hour), // This should be ignored during TS comparisons
},
{
tag: "0.2.0",
annotated: true,
commitTime: now,
tagTime: now,
},
}
tests := []struct {
name string
constraint string
expectErr error
expectTag string
}{
{
name: "Orders by SemVer",
constraint: ">0.1.0",
expectTag: "0.2.0",
},
{
name: "Orders by SemVer and timestamp",
constraint: "<0.2.0",
expectTag: "v0.1.0+build-3",
},
{
name: "Errors without match",
constraint: ">=1.0.0",
expectErr: errors.New("no match found for semver: >=1.0.0"),
},
}
repo, path, err := initRepo()
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(path)
refs := make(map[string]string, len(tags))
for _, tt := range tags {
ref, err := commitFile(repo, "tag", tt.tag, tt.commitTime)
if err != nil {
t.Fatal(err)
}
_, err = tag(repo, ref, tt.annotated, tt.tag, tt.tagTime)
if err != nil {
t.Fatal(err)
}
refs[tt.tag] = ref.String()
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
semVer := CheckoutSemVer{
SemVer: tt.constraint,
}
tmpDir, _ := os.MkdirTemp("", "test")
defer os.RemoveAll(tmpDir)
cc, err := semVer.Checkout(context.TODO(), tmpDir, path, nil)
if tt.expectErr != nil {
g.Expect(err).To(Equal(tt.expectErr))
g.Expect(cc).To(BeNil())
return
}
g.Expect(err).ToNot(HaveOccurred())
g.Expect(cc.String()).To(Equal(tt.expectTag + "/" + refs[tt.expectTag]))
g.Expect(filepath.Join(tmpDir, "tag")).To(BeARegularFile())
g.Expect(os.ReadFile(filepath.Join(tmpDir, "tag"))).To(BeEquivalentTo(tt.expectTag))
})
}
}
func initRepo() (*extgogit.Repository, string, error) {
tmpDir, err := os.MkdirTemp("", "gogit")
if err != nil {
os.RemoveAll(tmpDir)
return nil, "", err
}
sto := filesystem.NewStorage(osfs.New(tmpDir), cache.NewObjectLRUDefault())
repo, err := extgogit.Init(sto, memfs.New())
if err != nil {
os.RemoveAll(tmpDir)
return nil, "", err
}
return repo, tmpDir, err
}
func createBranch(repo *extgogit.Repository, branch string) error {
wt, err := repo.Worktree()
if err != nil {
return err
}
h, err := repo.Head()
if err != nil {
return err
}
return wt.Checkout(&extgogit.CheckoutOptions{
Hash: h.Hash(),
Branch: plumbing.ReferenceName("refs/heads/" + branch),
Create: true,
})
}
func commitFile(repo *extgogit.Repository, path, content string, time time.Time) (plumbing.Hash, error) {
wt, err := repo.Worktree()
if err != nil {
return plumbing.Hash{}, err
}
f, err := wt.Filesystem.Create(path)
if err != nil {
return plumbing.Hash{}, err
}
if _, err = f.Write([]byte(content)); err != nil {
f.Close()
return plumbing.Hash{}, err
}
if err = f.Close(); err != nil {
return plumbing.Hash{}, err
}
if _, err = wt.Add(path); err != nil {
return plumbing.Hash{}, err
}
return wt.Commit("Adding: "+path, &extgogit.CommitOptions{
Author: mockSignature(time),
Committer: mockSignature(time),
})
}
func tag(repo *extgogit.Repository, commit plumbing.Hash, annotated bool, tag string, time time.Time) (*plumbing.Reference, error) {
var opts *extgogit.CreateTagOptions
if annotated {
opts = &extgogit.CreateTagOptions{
Tagger: mockSignature(time),
Message: "Annotated tag for: " + tag,
}
}
return repo.CreateTag(tag, commit, opts)
}
func mockSignature(time time.Time) *object.Signature {
return &object.Signature{
Name: "Jane Doe",
Email: "jane@example.com",
When: time,
}
}