diff --git a/cmd/git-init/main.go b/cmd/git-init/main.go index 5fa50a85c62..8f428f99ed6 100644 --- a/cmd/git-init/main.go +++ b/cmd/git-init/main.go @@ -28,16 +28,16 @@ import ( var ( fetchSpec git.FetchSpec - submodules bool terminationMessagePath string ) func init() { flag.StringVar(&fetchSpec.URL, "url", "", "Git origin URL to fetch") flag.StringVar(&fetchSpec.Revision, "revision", "", "The Git revision to make the repository HEAD") + flag.StringVar(&fetchSpec.Refspec, "refspec", "", "The Git refspec to fetch the revision from (optional)") flag.StringVar(&fetchSpec.Path, "path", "", "Path of directory under which Git repository will be copied") flag.BoolVar(&fetchSpec.SSLVerify, "sslVerify", true, "Enable/Disable SSL verification in the git config") - flag.BoolVar(&submodules, "submodules", true, "Initialize and fetch Git submodules") + flag.BoolVar(&fetchSpec.Submodules, "submodules", true, "Initialize and fetch Git submodules") flag.UintVar(&fetchSpec.Depth, "depth", 1, "Perform a shallow clone to this depth") flag.StringVar(&terminationMessagePath, "terminationMessagePath", "/tekton/termination", "Location of file containing termination message") } @@ -53,15 +53,10 @@ func main() { if err := git.Fetch(logger, fetchSpec); err != nil { logger.Fatalf("Error fetching git repository: %s", err) } - if submodules { - if err := git.SubmoduleFetch(logger, fetchSpec.Path); err != nil { - logger.Fatalf("Error initializing or fetching the git submodules") - } - } - commit, err := git.Commit(logger, fetchSpec.Revision, fetchSpec.Path) + commit, err := git.ShowCommit(logger, "HEAD", fetchSpec.Path) if err != nil { - logger.Fatalf("Error parsing commit of git repository: %s", err) + logger.Fatalf("Error parsing revision %s of git repository: %s", fetchSpec.Revision, err) } resourceName := os.Getenv("TEKTON_RESOURCE_NAME") output := []v1alpha1.PipelineResourceResult{ diff --git a/docs/resources.md b/docs/resources.md index 7fba1ac1071..c786d0d357a 100644 --- a/docs/resources.md +++ b/docs/resources.md @@ -313,19 +313,53 @@ Params that can be added are the following: change the repo, e.g. [to use a fork](#using-a-fork) 1. `revision`: Git [revision][git-rev] (branch, tag, commit SHA or ref) to clone. You can use this to control what commit [or branch](#using-a-branch) - is used. _If no revision is specified, the resource will default to `latest` - from `master`._ + is used. [git checkout][git-checkout] is used to switch to the + revision, and will result in a detached HEAD in most cases. Use refspec + along with revision if you want to checkout a particular branch without a + detached HEAD. _If no revision is specified, the resource will default to `master`._ +1. `refspec`: (Optional) specify a git [refspec][git-refspec] to pass to git-fetch. + Note that if this field is specified, it must specify all refs, branches, tags, + or commits required to checkout the specified `revision`. An additional fetch + will not be run to obtain the contents of the revision field. If no refspec + is specified, the value of the `revision` field will be fetched directly. + The refspec is useful in manipulating the repository in several cases: + * when the server does not support fetches via the commit SHA (i.e. does + not have `uploadpack.allowReachableSHA1InWant` enabled) and you want + to fetch and checkout a specific commit hash from a ref chain. + * when you want to fetch several other refs alongside your revision + (for instance, tags) + * when you want to checkout a specific branch, the revision and refspec + fields can work together to be able to set the destination of the incoming + branch and switch to the branch. + + Examples: + - Check out a specified revision commit SHA1 after fetching ref (detached)
+   `revision`: cb17eba165fe7973ef9afec20e7c6971565bd72f
+   `refspec`: refs/smoke/myref
+ - Fetch all tags alongside refs/heads/master and switch to the master branch + (not detached)
+   `revision`: master
+   `refspec`: "refs/tags/\*:refs/tags/\* +refs/heads/master:refs/heads/master"
+ - Fetch the develop branch and switch to it (not detached)
+   `revision`: develop
+   `refspec`: refs/heads/develop:refs/heads/develop
+ - Fetch refs/pull/1009/head into the master branch and switch to it (not detached)
+   `revision`: master
+   `refspec`: refs/pull/1009/head:refs/heads/master
+ 1. `submodules`: defines if the resource should initialize and fetch the submodules, value is either `true` or `false`. _If not specified, this will default to true_ 1. `depth`: performs a [shallow clone][git-depth] where only the most recent - commit(s) will be fetched. If set to `'0'`, all commits will be fetched. _If - not specified, the default depth is 1._ + commit(s) will be fetched. This setting also applies to submodules. If set to + `'0'`, all commits will be fetched. _If not specified, the default depth is 1._ 1. `sslVerify`: defines if [http.sslVerify][git-http.sslVerify] should be set to `true` or `false` in the global git config. _Defaults to `true` if omitted._ [git-rev]: https://git-scm.com/docs/gitrevisions#_specifying_revisions +[git-checkout]: https://git-scm.com/docs/git-checkout +[git-refspec]: https://git-scm.com/book/en/v2/Git-Internals-The-Refspec [git-depth]: https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---depthltdepthgt [git-http.sslVerify]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpsslVerify diff --git a/pkg/apis/pipeline/v1alpha1/pipeline_resource_types.go b/pkg/apis/pipeline/v1alpha1/pipeline_resource_types.go index 7ad12816ec9..14945cdfa90 100644 --- a/pkg/apis/pipeline/v1alpha1/pipeline_resource_types.go +++ b/pkg/apis/pipeline/v1alpha1/pipeline_resource_types.go @@ -30,7 +30,7 @@ var ( ) const ( - // PipelineResourceTypeGit indicates that this source is a GitHub repo. + // PipelineResourceTypeGit indicates that this source is a Git repo. PipelineResourceTypeGit PipelineResourceType = resource.PipelineResourceTypeGit // PipelineResourceTypeStorage indicates that this source is a storage blob resource. diff --git a/pkg/apis/resource/v1alpha1/git/git_resource.go b/pkg/apis/resource/v1alpha1/git/git_resource.go index 857f297e0b4..bd117216622 100644 --- a/pkg/apis/resource/v1alpha1/git/git_resource.go +++ b/pkg/apis/resource/v1alpha1/git/git_resource.go @@ -38,10 +38,10 @@ type Resource struct { Name string `json:"name"` Type resource.PipelineResourceType `json:"type"` URL string `json:"url"` - // Git revision (branch, tag, commit SHA or ref) to clone. See - // https://git-scm.com/docs/gitrevisions#_specifying_revisions for more - // information. + // Git revision (branch, tag, commit SHA) to clone, and optionally the refspec to fetch from. + //See https://git-scm.com/docs/gitrevisions#_specifying_revisions for more information. Revision string `json:"revision"` + Refspec string `json:"refspec"` Submodules bool `json:"submodules"` Depth uint `json:"depth"` @@ -71,6 +71,8 @@ func NewResource(gitImage string, r *resource.PipelineResource) (*Resource, erro gitResource.URL = param.Value case strings.EqualFold(param.Name, "Revision"): gitResource.Revision = param.Value + case strings.EqualFold(param.Name, "Refspec"): + gitResource.Refspec = param.Value case strings.EqualFold(param.Name, "Submodules"): gitResource.Submodules = toBool(param.Value, true) case strings.EqualFold(param.Name, "Depth"): @@ -133,6 +135,8 @@ func (s *Resource) Replacements() map[string]string { "type": s.Type, "url": s.URL, "revision": s.Revision, + "refspec": s.Refspec, + "submodules": strconv.FormatBool(s.Submodules), "depth": strconv.FormatUint(uint64(s.Depth), 10), "sslVerify": strconv.FormatBool(s.SSLVerify), "httpProxy": s.HTTPProxy, @@ -149,6 +153,9 @@ func (s *Resource) GetInputTaskModifier(_ *v1alpha1.TaskSpec, path string) (v1al "-path", path, } + if s.Refspec != "" { + args = append(args, "-refspec", s.Refspec) + } if !s.Submodules { args = append(args, "-submodules=false") } diff --git a/pkg/apis/resource/v1alpha1/git/git_resource_test.go b/pkg/apis/resource/v1alpha1/git/git_resource_test.go index 3a5cf6731ef..79d065c47dc 100644 --- a/pkg/apis/resource/v1alpha1/git/git_resource_test.go +++ b/pkg/apis/resource/v1alpha1/git/git_resource_test.go @@ -52,6 +52,7 @@ func TestNewGitResource_Valid(t *testing.T) { Type: resourcev1alpha1.PipelineResourceTypeGit, URL: "git@github.com:test/test.git", Revision: "test", + Refspec: "", GitImage: "override-with-git:latest", Submodules: true, Depth: 1, @@ -72,6 +73,50 @@ func TestNewGitResource_Valid(t *testing.T) { Type: resourcev1alpha1.PipelineResourceTypeGit, URL: "git@github.com:test/test.git", Revision: "master", + Refspec: "", + GitImage: "override-with-git:latest", + Submodules: true, + Depth: 1, + SSLVerify: true, + HTTPProxy: "", + HTTPSProxy: "", + NOProxy: "", + }, + }, { + desc: "With Refspec", + pipelineResource: tb.PipelineResource("git-resource", "default", + tb.PipelineResourceSpec(resourcev1alpha1.PipelineResourceTypeGit, + tb.PipelineResourceSpecParam("URL", "git@github.com:test/test.git"), + tb.PipelineResourceSpecParam("Refspec", "refs/changes/22/222134"), + ), + ), + want: &git.Resource{ + Name: "git-resource", + Type: resourcev1alpha1.PipelineResourceTypeGit, + URL: "git@github.com:test/test.git", + Revision: "master", + Refspec: "refs/changes/22/222134", + GitImage: "override-with-git:latest", + Submodules: true, + Depth: 1, + SSLVerify: true, + HTTPProxy: "", + HTTPSProxy: "", + NOProxy: "", + }, + }, { + desc: "Without Refspec", + pipelineResource: tb.PipelineResource("git-resource", "default", + tb.PipelineResourceSpec(resourcev1alpha1.PipelineResourceTypeGit, + tb.PipelineResourceSpecParam("URL", "git@github.com:test/test.git"), + ), + ), + want: &git.Resource{ + Name: "git-resource", + Type: resourcev1alpha1.PipelineResourceTypeGit, + URL: "git@github.com:test/test.git", + Revision: "master", + Refspec: "", GitImage: "override-with-git:latest", Submodules: true, Depth: 1, @@ -93,6 +138,7 @@ func TestNewGitResource_Valid(t *testing.T) { Type: resourcev1alpha1.PipelineResourceTypeGit, URL: "git@github.com:test/test.git", Revision: "test", + Refspec: "", GitImage: "override-with-git:latest", Submodules: true, Depth: 1, @@ -115,6 +161,7 @@ func TestNewGitResource_Valid(t *testing.T) { Type: resourcev1alpha1.PipelineResourceTypeGit, URL: "git@github.com:test/test.git", Revision: "test", + Refspec: "", GitImage: "override-with-git:latest", Submodules: false, Depth: 1, @@ -137,6 +184,7 @@ func TestNewGitResource_Valid(t *testing.T) { Type: resourcev1alpha1.PipelineResourceTypeGit, URL: "git@github.com:test/test.git", Revision: "test", + Refspec: "", GitImage: "override-with-git:latest", Submodules: true, Depth: 8, @@ -159,6 +207,7 @@ func TestNewGitResource_Valid(t *testing.T) { Type: resourcev1alpha1.PipelineResourceTypeGit, URL: "git@github.com:test/test.git", Revision: "test", + Refspec: "", GitImage: "override-with-git:latest", Submodules: true, Depth: 0, @@ -182,6 +231,7 @@ func TestNewGitResource_Valid(t *testing.T) { Type: resourcev1alpha1.PipelineResourceTypeGit, URL: "git@github.com:test/test.git", Revision: "test", + Refspec: "", GitImage: "override-with-git:latest", Submodules: true, Depth: 0, @@ -205,6 +255,7 @@ func TestNewGitResource_Valid(t *testing.T) { Type: resourcev1alpha1.PipelineResourceTypeGit, URL: "git@github.com:test/test.git", Revision: "test", + Refspec: "", GitImage: "override-with-git:latest", Submodules: true, Depth: 0, @@ -228,6 +279,7 @@ func TestNewGitResource_Valid(t *testing.T) { Type: resourcev1alpha1.PipelineResourceTypeGit, URL: "git@github.com:test/test.git", Revision: "test", + Refspec: "", GitImage: "override-with-git:latest", Submodules: true, Depth: 0, @@ -251,6 +303,7 @@ func TestNewGitResource_Valid(t *testing.T) { Type: resourcev1alpha1.PipelineResourceTypeGit, URL: "git@github.com:test/test.git", Revision: "test", + Refspec: "", GitImage: "override-with-git:latest", Submodules: true, Depth: 0, @@ -279,6 +332,8 @@ func TestGitResource_Replacements(t *testing.T) { Type: resourcev1alpha1.PipelineResourceTypeGit, URL: "git@github.com:test/test.git", Revision: "master", + Refspec: "", + Submodules: false, Depth: 16, SSLVerify: false, HTTPProxy: "http-proxy.git.com", @@ -291,6 +346,8 @@ func TestGitResource_Replacements(t *testing.T) { "type": string(resourcev1alpha1.PipelineResourceTypeGit), "url": "git@github.com:test/test.git", "revision": "master", + "refspec": "", + "submodules": "false", "depth": "16", "sslVerify": "false", "httpProxy": "http-proxy.git.com", @@ -319,6 +376,7 @@ func TestGitResource_GetDownloadTaskModifier(t *testing.T) { Type: resourcev1alpha1.PipelineResourceTypeGit, URL: "git@github.com:test/test.git", Revision: "master", + Refspec: "", GitImage: "override-with-git:latest", Submodules: true, Depth: 1, @@ -354,6 +412,7 @@ func TestGitResource_GetDownloadTaskModifier(t *testing.T) { Type: resourcev1alpha1.PipelineResourceTypeGit, URL: "git@github.com:test/test.git", Revision: "master", + Refspec: "", GitImage: "override-with-git:latest", Submodules: false, Depth: 1, @@ -390,6 +449,7 @@ func TestGitResource_GetDownloadTaskModifier(t *testing.T) { Type: resourcev1alpha1.PipelineResourceTypeGit, URL: "git@github.com:test/test.git", Revision: "master", + Refspec: "", GitImage: "override-with-git:latest", Submodules: true, Depth: 8, @@ -427,6 +487,7 @@ func TestGitResource_GetDownloadTaskModifier(t *testing.T) { Type: resourcev1alpha1.PipelineResourceTypeGit, URL: "git@github.com:test/test.git", Revision: "master", + Refspec: "", GitImage: "override-with-git:latest", Submodules: false, Depth: 1, @@ -464,6 +525,7 @@ func TestGitResource_GetDownloadTaskModifier(t *testing.T) { Type: resourcev1alpha1.PipelineResourceTypeGit, URL: "git@github.com:test/test.git", Revision: "master", + Refspec: "", GitImage: "override-with-git:latest", Submodules: false, Depth: 1, @@ -499,6 +561,7 @@ func TestGitResource_GetDownloadTaskModifier(t *testing.T) { Type: resourcev1alpha1.PipelineResourceTypeGit, URL: "git@github.com:test/test.git", Revision: "master", + Refspec: "", GitImage: "override-with-git:latest", Submodules: false, Depth: 1, @@ -534,6 +597,7 @@ func TestGitResource_GetDownloadTaskModifier(t *testing.T) { Type: resourcev1alpha1.PipelineResourceTypeGit, URL: "git@github.com:test/test.git", Revision: "master", + Refspec: "", GitImage: "override-with-git:latest", Submodules: false, Depth: 1, @@ -562,6 +626,45 @@ func TestGitResource_GetDownloadTaskModifier(t *testing.T) { {Name: "HTTPS_PROXY", Value: "https-proxy.git.com"}, }, }, + }, { + desc: "With Refspec", + gitResource: &git.Resource{ + Name: "git-resource", + Type: resourcev1alpha1.PipelineResourceTypeGit, + URL: "git@github.com:test/test.git", + Revision: "master", + Refspec: "refs/tags/v1.0:refs/tags/v1.0 refs/heads/master:refs/heads/master", + GitImage: "override-with-git:latest", + Submodules: false, + Depth: 1, + SSLVerify: true, + HTTPProxy: "http-proxy.git.com", + HTTPSProxy: "https-proxy.git.com", + NOProxy: "no-proxy.git.com", + }, + want: corev1.Container{ + Name: "git-source-git-resource-l22wn", + Image: "override-with-git:latest", + Command: []string{"/ko-app/git-init"}, + Args: []string{ + "-url", + "git@github.com:test/test.git", + "-revision", + "master", + "-path", + "/test/test", + "-refspec", + "refs/tags/v1.0:refs/tags/v1.0 refs/heads/master:refs/heads/master", + "-submodules=false", + }, + WorkingDir: "/workspace", + Env: []corev1.EnvVar{ + {Name: "TEKTON_RESOURCE_NAME", Value: "git-resource"}, + {Name: "HTTP_PROXY", Value: "http-proxy.git.com"}, + {Name: "HTTPS_PROXY", Value: "https-proxy.git.com"}, + {Name: "NO_PROXY", Value: "no-proxy.git.com"}, + }, + }, }} { t.Run(tc.desc, func(t *testing.T) { ts := pipelinev1alpha1.TaskSpec{} diff --git a/pkg/git/git.go b/pkg/git/git.go index bcf14cb5ee2..8bc5b97d9fd 100644 --- a/pkg/git/git.go +++ b/pkg/git/git.go @@ -48,15 +48,17 @@ func run(logger *zap.SugaredLogger, dir string, args ...string) (string, error) type FetchSpec struct { URL string Revision string + Refspec string Path string Depth uint + Submodules bool SSLVerify bool HTTPProxy string HTTPSProxy string NOProxy string } -// Fetch fetches the specified git repository at the revision into path. +// Fetch fetches the specified git repository at the revision into path, using the refspec to fetch if provided. func Fetch(logger *zap.SugaredLogger, spec FetchSpec) error { if err := ensureHomeEnv(logger); err != nil { return err @@ -84,53 +86,95 @@ func Fetch(logger *zap.SugaredLogger, spec FetchSpec) error { return err } - fetchArgs := []string{"fetch", "--recurse-submodules=yes"} + fetchArgs := []string{"fetch"} + if spec.Submodules { + fetchArgs = append(fetchArgs, "--recurse-submodules=yes") + } if spec.Depth > 0 { fetchArgs = append(fetchArgs, fmt.Sprintf("--depth=%d", spec.Depth)) } - fetchArgs = append(fetchArgs, "origin", spec.Revision) - if _, err := run(logger, "", fetchArgs...); err != nil { - // Fetch can fail if an old commitid was used so try git pull, performing regardless of error - // as no guarantee that the same error is returned by all git servers gitlab, github etc... - if _, err := run(logger, "", "pull", "--recurse-submodules=yes", "origin"); err != nil { - logger.Warnf("Failed to pull origin : %s", err) - } - if _, err := run(logger, "", "checkout", spec.Revision); err != nil { + // Fetch the revision and verify with FETCH_HEAD + fetchParam := []string{spec.Revision} + checkoutParam := "FETCH_HEAD" + + if spec.Refspec != "" { + // if refspec is specified, fetch the refspec and verify with provided revision + fetchParam = strings.Split(spec.Refspec, " ") + checkoutParam = spec.Revision + } + + // git-init always creates and checks out an empty master branch. When the user requests + // "master" as the revision, git-fetch will refuse to update the HEAD of the branch it is + // currently on. The --update-head-ok parameter tells git-fetch that it is ok to update + // the current (empty) HEAD on initial fetch. + // The --force parameter tells git-fetch that its ok to update an existing HEAD in a + // non-fast-forward manner (though this cannot be possible on initial fetch, it can help + // when the refspec specifies the same destination twice) + fetchArgs = append(fetchArgs, "origin", "--update-head-ok", "--force") + fetchArgs = append(fetchArgs, fetchParam...) + if _, err := run(logger, spec.Path, fetchArgs...); err != nil { + return fmt.Errorf("failed to fetch %v: %v", fetchParam, err) + } + // After performing a fetch, verify that the item to checkout is actually valid + if _, err := ShowCommit(logger, checkoutParam, spec.Path); err != nil { + return fmt.Errorf("error parsing %s after fetching refspec %s", checkoutParam, spec.Refspec) + } + + if _, err := run(logger, "", "checkout", "-f", checkoutParam); err != nil { + return err + } + + commit, err := ShowCommit(logger, "HEAD", spec.Path) + if err != nil { + return err + } + ref, err := ShowRef(logger, "HEAD", spec.Path) + if err != nil { + return err + } + logger.Infof("Successfully cloned %s @ %s (%s) in path %s", trimmedURL, commit, ref, spec.Path) + if spec.Submodules { + if err := SubmoduleFetch(logger, spec); err != nil { return err } - } else if _, err := run(logger, "", "reset", "--hard", "FETCH_HEAD"); err != nil { - return err } - logger.Infof("Successfully cloned %s @ %s in path %s", trimmedURL, spec.Revision, spec.Path) return nil } -func Commit(logger *zap.SugaredLogger, revision, path string) (string, error) { - output, err := run(logger, path, "rev-parse", "HEAD") +func ShowCommit(logger *zap.SugaredLogger, revision, path string) (string, error) { + output, err := run(logger, path, "show", "-q", "--pretty=format:%H", revision) if err != nil { return "", err } return strings.TrimSuffix(output, "\n"), nil } -func SubmoduleFetch(logger *zap.SugaredLogger, path string) error { - if err := ensureHomeEnv(logger); err != nil { - return err +func ShowRef(logger *zap.SugaredLogger, revision, path string) (string, error) { + output, err := run(logger, path, "show", "-q", "--pretty=format:%D", revision) + if err != nil { + return "", err } + return strings.TrimSuffix(output, "\n"), nil +} - if path != "" { - if err := os.Chdir(path); err != nil { - return fmt.Errorf("failed to change directory with path %s; err: %w", path, err) +func SubmoduleFetch(logger *zap.SugaredLogger, spec FetchSpec) error { + if spec.Path != "" { + if err := os.Chdir(spec.Path); err != nil { + return fmt.Errorf("failed to change directory with path %s; err: %w", spec.Path, err) } } if _, err := run(logger, "", "submodule", "init"); err != nil { return err } - if _, err := run(logger, "", "submodule", "update", "--recursive"); err != nil { + updateArgs := []string{"submodule", "update", "--recursive"} + if spec.Depth > 0 { + updateArgs = append(updateArgs, "--depth", fmt.Sprintf("--depth=%d", spec.Depth)) + } + if _, err := run(logger, "", updateArgs...); err != nil { return err } - logger.Infof("Successfully initialized and updated submodules in path %s", path) + logger.Infof("Successfully initialized and updated submodules in path %s", spec.Path) return nil } diff --git a/test/git_checkout_test.go b/test/git_checkout_test.go index dc0779aec5b..52284a6df84 100644 --- a/test/git_checkout_test.go +++ b/test/git_checkout_test.go @@ -43,7 +43,7 @@ const ( func TestGitPipelineRun(t *testing.T) { t.Parallel() - revisions := []string{"master", "c15aced0e5aaee6456fbe6f7a7e95e0b5b3b2b2f", "c15aced", "release-0.1", "v0.1.0", "refs/pull/347/head"} + revisions := []string{"master", "c15aced0e5aaee6456fbe6f7a7e95e0b5b3b2b2f", "release-0.1", "v0.1.0", "refs/pull/347/head"} for _, revision := range revisions { @@ -53,7 +53,7 @@ func TestGitPipelineRun(t *testing.T) { defer tearDown(t, c, namespace) t.Logf("Creating Git PipelineResource %s", gitSourceResourceName) - if _, err := c.PipelineResourceClient.Create(getGitPipelineResource(namespace, revision, "true", "", "", "")); err != nil { + if _, err := c.PipelineResourceClient.Create(getGitPipelineResource(namespace, revision, "", "true", "", "", "")); err != nil { t.Fatalf("Failed to create Pipeline Resource `%s`: %s", gitSourceResourceName, err) } @@ -80,6 +80,57 @@ func TestGitPipelineRun(t *testing.T) { } } +// Test revision fetching with refspec specified +func TestGitPipelineRunWithRefspec(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + description string + revision string + refspec string + }{{ + description: "Fetch refs/tags/v0.1.0 alongside master and checkout the master branch", + revision: "master", + refspec: "refs/tags/v0.1.0:refs/tags/v0.1.0 refs/heads/master:refs/heads/master", + }, { + description: "Checkout specific revision from refs/pull/1009/head's commit chain", + revision: "968d5d37a61bfb85426c885dc1090c1cc4b33436", + refspec: "refs/pull/1009/head", + }, { + description: "Fetch refs/pull/1009/head into a named master branch and then check it out", + revision: "master", + refspec: "refs/pull/1009/head:refs/heads/master", + }} { + t.Run(tc.description, func(t *testing.T) { + c, namespace := setup(t) + knativetest.CleanupOnInterrupt(func() { tearDown(t, c, namespace) }, t.Logf) + defer tearDown(t, c, namespace) + + if _, err := c.PipelineResourceClient.Create(getGitPipelineResource(namespace, tc.revision, tc.refspec, "true", "", "", "")); err != nil { + t.Fatalf("Failed to create Pipeline Resource `%s`: %s", gitSourceResourceName, err) + } + + if _, err := c.TaskClient.Create(getGitCheckTask(namespace)); err != nil { + t.Fatalf("Failed to create Task `%s`: %s", gitTestTaskName, err) + } + + if _, err := c.PipelineClient.Create(getGitCheckPipeline(namespace)); err != nil { + t.Fatalf("Failed to create Pipeline `%s`: %s", gitTestPipelineName, err) + } + + if _, err := c.PipelineRunClient.Create(getGitCheckPipelineRun(namespace)); err != nil { + t.Fatalf("Failed to create Pipeline `%s`: %s", gitTestPipelineRunName, err) + } + + if err := WaitForPipelineRunState(c, gitTestPipelineRunName, timeout, PipelineRunSucceed(gitTestPipelineRunName), "PipelineRunCompleted"); err != nil { + t.Errorf("Error waiting for PipelineRun %s to finish: %s", gitTestPipelineRunName, err) + t.Fatalf("PipelineRun execution failed") + } + + }) + } +} + // TestGitPipelineRun_Disable_SSLVerify will verify the source code is retrieved even after disabling SSL certificates (sslVerify) func TestGitPipelineRun_Disable_SSLVerify(t *testing.T) { t.Parallel() @@ -89,7 +140,7 @@ func TestGitPipelineRun_Disable_SSLVerify(t *testing.T) { defer tearDown(t, c, namespace) t.Logf("Creating Git PipelineResource %s", gitSourceResourceName) - if _, err := c.PipelineResourceClient.Create(getGitPipelineResource(namespace, "master", "false", "", "", "")); err != nil { + if _, err := c.PipelineResourceClient.Create(getGitPipelineResource(namespace, "master", "", "false", "", "", "")); err != nil { t.Fatalf("Failed to create Pipeline Resource `%s`: %s", gitSourceResourceName, err) } @@ -124,7 +175,7 @@ func TestGitPipelineRunFail(t *testing.T) { defer tearDown(t, c, namespace) t.Logf("Creating Git PipelineResource %s", gitSourceResourceName) - if _, err := c.PipelineResourceClient.Create(getGitPipelineResource(namespace, "Idontexistrabbitmonkeydonkey", "true", "", "", "")); err != nil { + if _, err := c.PipelineResourceClient.Create(getGitPipelineResource(namespace, "Idontexistrabbitmonkeydonkey", "", "true", "", "", "")); err != nil { t.Fatalf("Failed to create Pipeline Resource `%s`: %s", gitSourceResourceName, err) } @@ -164,8 +215,7 @@ func TestGitPipelineRunFail(t *testing.T) { t.Fatalf("Error getting pod logs for pod `%s` and container `%s` in namespace `%s`", tr.Status.PodName, stat.Name, namespace) } // Check for failure messages from fetch and pull in the log file - if strings.Contains(strings.ToLower(string(logContent)), "couldn't find remote ref idontexistrabbitmonkeydonkey") && - strings.Contains(strings.ToLower(string(logContent)), "pathspec 'idontexistrabbitmonkeydonkey' did not match any file(s) known to git") { + if strings.Contains(strings.ToLower(string(logContent)), "couldn't find remote ref idontexistrabbitmonkeydonkey") { t.Logf("Found exepected errors when retrieving non-existent git revision") } else { t.Logf("Container `%s` log File: %s", stat.Name, logContent) @@ -192,7 +242,7 @@ func TestGitPipelineRunFail_HTTPS_PROXY(t *testing.T) { defer tearDown(t, c, namespace) t.Logf("Creating Git PipelineResource %s", gitSourceResourceName) - if _, err := c.PipelineResourceClient.Create(getGitPipelineResource(namespace, "master", "true", "", "invalid.https.proxy.com", "")); err != nil { + if _, err := c.PipelineResourceClient.Create(getGitPipelineResource(namespace, "master", "", "true", "", "invalid.https.proxy.com", "")); err != nil { t.Fatalf("Failed to create Pipeline Resource `%s`: %s", gitSourceResourceName, err) } @@ -232,8 +282,7 @@ func TestGitPipelineRunFail_HTTPS_PROXY(t *testing.T) { t.Fatalf("Error getting pod logs for pod `%s` and container `%s` in namespace `%s`", tr.Status.PodName, stat.Name, namespace) } // Check for failure messages from fetch and pull in the log file - if strings.Contains(strings.ToLower(string(logContent)), "could not resolve proxy: invalid.https.proxy.com") && - strings.Contains(strings.ToLower(string(logContent)), "pathspec 'master' did not match any file(s) known to git") { + if strings.Contains(strings.ToLower(string(logContent)), "could not resolve proxy: invalid.https.proxy.com") { t.Logf("Found exepected errors when using non-existent https proxy") } else { t.Logf("Container `%s` log File: %s", stat.Name, logContent) @@ -250,11 +299,12 @@ func TestGitPipelineRunFail_HTTPS_PROXY(t *testing.T) { } } -func getGitPipelineResource(namespace, revision, sslverify, httpproxy, httpsproxy, noproxy string) *v1alpha1.PipelineResource { +func getGitPipelineResource(namespace, revision, refspec, sslverify, httpproxy, httpsproxy, noproxy string) *v1alpha1.PipelineResource { return tb.PipelineResource(gitSourceResourceName, namespace, tb.PipelineResourceSpec( v1alpha1.PipelineResourceTypeGit, tb.PipelineResourceSpecParam("Url", "https://github.com/tektoncd/pipeline"), tb.PipelineResourceSpecParam("Revision", revision), + tb.PipelineResourceSpecParam("Refspec", refspec), tb.PipelineResourceSpecParam("sslVerify", sslverify), tb.PipelineResourceSpecParam("httpProxy", httpproxy), tb.PipelineResourceSpecParam("httpsProxy", httpsproxy), diff --git a/test/v1alpha1/git_checkout_test.go b/test/v1alpha1/git_checkout_test.go index c90444f716b..e906b18e7d4 100644 --- a/test/v1alpha1/git_checkout_test.go +++ b/test/v1alpha1/git_checkout_test.go @@ -41,7 +41,7 @@ const ( func TestGitPipelineRun(t *testing.T) { t.Parallel() - revisions := []string{"master", "c15aced0e5aaee6456fbe6f7a7e95e0b5b3b2b2f", "c15aced", "release-0.1", "v0.1.0", "refs/pull/347/head"} + revisions := []string{"master", "c15aced0e5aaee6456fbe6f7a7e95e0b5b3b2b2f", "release-0.1", "v0.1.0", "refs/pull/347/head"} for _, revision := range revisions { @@ -51,7 +51,7 @@ func TestGitPipelineRun(t *testing.T) { defer tearDown(t, c, namespace) t.Logf("Creating Git PipelineResource %s", gitSourceResourceName) - if _, err := c.PipelineResourceClient.Create(getGitPipelineResource(namespace, revision, "true", "", "", "")); err != nil { + if _, err := c.PipelineResourceClient.Create(getGitPipelineResource(namespace, revision, "", "true", "", "", "")); err != nil { t.Fatalf("Failed to create Pipeline Resource `%s`: %s", gitSourceResourceName, err) } @@ -78,6 +78,57 @@ func TestGitPipelineRun(t *testing.T) { } } +// Test revision fetching with refspec specified +func TestGitPipelineRunWithRefspec(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + description string + revision string + refspec string + }{{ + description: "Fetch refs/tags/v0.1.0 alongside master and checkout the master branch", + revision: "master", + refspec: "refs/tags/v0.1.0:refs/tags/v0.1.0 refs/heads/master:refs/heads/master", + }, { + description: "Checkout specific revision from refs/pull/1009/head's commit chain", + revision: "968d5d37a61bfb85426c885dc1090c1cc4b33436", + refspec: "refs/pull/1009/head", + }, { + description: "Fetch refs/pull/1009/head into a named master branch and then check it out", + revision: "master", + refspec: "refs/pull/1009/head:refs/heads/master", + }} { + t.Run(tc.description, func(t *testing.T) { + c, namespace := setup(t) + knativetest.CleanupOnInterrupt(func() { tearDown(t, c, namespace) }, t.Logf) + defer tearDown(t, c, namespace) + + if _, err := c.PipelineResourceClient.Create(getGitPipelineResource(namespace, tc.revision, tc.refspec, "true", "", "", "")); err != nil { + t.Fatalf("Failed to create Pipeline Resource `%s`: %s", gitSourceResourceName, err) + } + + if _, err := c.TaskClient.Create(getGitCheckTask(namespace)); err != nil { + t.Fatalf("Failed to create Task `%s`: %s", gitTestTaskName, err) + } + + if _, err := c.PipelineClient.Create(getGitCheckPipeline(namespace)); err != nil { + t.Fatalf("Failed to create Pipeline `%s`: %s", gitTestPipelineName, err) + } + + if _, err := c.PipelineRunClient.Create(getGitCheckPipelineRun(namespace)); err != nil { + t.Fatalf("Failed to create Pipeline `%s`: %s", gitTestPipelineRunName, err) + } + + if err := WaitForPipelineRunState(c, gitTestPipelineRunName, timeout, PipelineRunSucceed(gitTestPipelineRunName), "PipelineRunCompleted"); err != nil { + t.Errorf("Error waiting for PipelineRun %s to finish: %s", gitTestPipelineRunName, err) + t.Fatalf("PipelineRun execution failed") + } + + }) + } +} + // TestGitPipelineRun_Disable_SSLVerify will verify the source code is retrieved even after disabling SSL certificates (sslVerify) func TestGitPipelineRun_Disable_SSLVerify(t *testing.T) { t.Parallel() @@ -87,7 +138,7 @@ func TestGitPipelineRun_Disable_SSLVerify(t *testing.T) { defer tearDown(t, c, namespace) t.Logf("Creating Git PipelineResource %s", gitSourceResourceName) - if _, err := c.PipelineResourceClient.Create(getGitPipelineResource(namespace, "master", "false", "", "", "")); err != nil { + if _, err := c.PipelineResourceClient.Create(getGitPipelineResource(namespace, "master", "", "false", "", "", "")); err != nil { t.Fatalf("Failed to create Pipeline Resource `%s`: %s", gitSourceResourceName, err) } @@ -122,7 +173,7 @@ func TestGitPipelineRunFail(t *testing.T) { defer tearDown(t, c, namespace) t.Logf("Creating Git PipelineResource %s", gitSourceResourceName) - if _, err := c.PipelineResourceClient.Create(getGitPipelineResource(namespace, "Idontexistrabbitmonkeydonkey", "true", "", "", "")); err != nil { + if _, err := c.PipelineResourceClient.Create(getGitPipelineResource(namespace, "Idontexistrabbitmonkeydonkey", "", "true", "", "", "")); err != nil { t.Fatalf("Failed to create Pipeline Resource `%s`: %s", gitSourceResourceName, err) } @@ -162,8 +213,7 @@ func TestGitPipelineRunFail(t *testing.T) { t.Fatalf("Error getting pod logs for pod `%s` and container `%s` in namespace `%s`", tr.Status.PodName, stat.Name, namespace) } // Check for failure messages from fetch and pull in the log file - if strings.Contains(strings.ToLower(string(logContent)), "couldn't find remote ref idontexistrabbitmonkeydonkey") && - strings.Contains(strings.ToLower(string(logContent)), "pathspec 'idontexistrabbitmonkeydonkey' did not match any file(s) known to git") { + if strings.Contains(strings.ToLower(string(logContent)), "couldn't find remote ref idontexistrabbitmonkeydonkey") { t.Logf("Found exepected errors when retrieving non-existent git revision") } else { t.Logf("Container `%s` log File: %s", stat.Name, logContent) @@ -190,7 +240,7 @@ func TestGitPipelineRunFail_HTTPS_PROXY(t *testing.T) { defer tearDown(t, c, namespace) t.Logf("Creating Git PipelineResource %s", gitSourceResourceName) - if _, err := c.PipelineResourceClient.Create(getGitPipelineResource(namespace, "master", "true", "", "invalid.https.proxy.com", "")); err != nil { + if _, err := c.PipelineResourceClient.Create(getGitPipelineResource(namespace, "master", "", "true", "", "invalid.https.proxy.com", "")); err != nil { t.Fatalf("Failed to create Pipeline Resource `%s`: %s", gitSourceResourceName, err) } @@ -230,9 +280,8 @@ func TestGitPipelineRunFail_HTTPS_PROXY(t *testing.T) { t.Fatalf("Error getting pod logs for pod `%s` and container `%s` in namespace `%s`", tr.Status.PodName, stat.Name, namespace) } // Check for failure messages from fetch and pull in the log file - if strings.Contains(strings.ToLower(string(logContent)), "could not resolve proxy: invalid.https.proxy.com") && - strings.Contains(strings.ToLower(string(logContent)), "pathspec 'master' did not match any file(s) known to git") { - t.Logf("Found exepected errors when using non-existent https proxy") + if strings.Contains(strings.ToLower(string(logContent)), "could not resolve proxy: invalid.https.proxy.com") { + t.Logf("Found expected errors when using non-existent https proxy") } else { t.Logf("Container `%s` log File: %s", stat.Name, logContent) t.Fatalf("The git code extraction did not fail as expected. Expected errors not found in log file.") @@ -248,11 +297,12 @@ func TestGitPipelineRunFail_HTTPS_PROXY(t *testing.T) { } } -func getGitPipelineResource(namespace, revision, sslverify, httpproxy, httpsproxy, noproxy string) *v1alpha1.PipelineResource { +func getGitPipelineResource(namespace, revision, refspec, sslverify, httpproxy, httpsproxy, noproxy string) *v1alpha1.PipelineResource { return tb.PipelineResource(gitSourceResourceName, namespace, tb.PipelineResourceSpec( v1alpha1.PipelineResourceTypeGit, tb.PipelineResourceSpecParam("Url", "https://github.com/tektoncd/pipeline"), tb.PipelineResourceSpecParam("Revision", revision), + tb.PipelineResourceSpecParam("Refspec", refspec), tb.PipelineResourceSpecParam("sslVerify", sslverify), tb.PipelineResourceSpecParam("httpProxy", httpproxy), tb.PipelineResourceSpecParam("httpsProxy", httpsproxy),