From cb080a20664196e61b0586b7ef7ecdb3a1090c88 Mon Sep 17 00:00:00 2001 From: Ian Howell Date: Wed, 28 Jul 2021 13:05:23 -0500 Subject: [PATCH] Implement fetching remote refs for document pull This commit adds the `fetch` and `fetch.remoteRefSpec` fields to the configuration for repositories, allowing the capability to fetch references from a remote. This is useful for pulling specific gerrit patchsets. This also changes `checkout.remoteRefSpec` to `checkout.refSpec`, and implements the logic required to checkout to an arbitrary ref. Closes: #616 Change-Id: Ie21a6c2a7a7ac92ed3c05fef7e5683203cd62e45 --- pkg/config/errors.go | 2 +- pkg/config/options.go | 6 ----- pkg/config/repo.go | 44 +++++++++++++++++++++++++++------- pkg/document/pull/pull.go | 9 ++++--- pkg/document/repo/repo.go | 19 +++++++++++++++ pkg/document/repo/repo_test.go | 3 ++- 6 files changed, 62 insertions(+), 21 deletions(-) diff --git a/pkg/config/errors.go b/pkg/config/errors.go index 887c93676..f71b62387 100644 --- a/pkg/config/errors.go +++ b/pkg/config/errors.go @@ -67,7 +67,7 @@ type ErrMutuallyExclusiveCheckout struct { } func (e ErrMutuallyExclusiveCheckout) Error() string { - return "Checkout mutually exclusive, use either: commit-hash, branch or tag." + return "Checkout mutually exclusive, use either: commit-hash, branch, tag, or ref." } // ErrRepositoryNotFound is returned if repository is empty diff --git a/pkg/config/options.go b/pkg/config/options.go index a1368a960..8c5044e21 100644 --- a/pkg/config/options.go +++ b/pkg/config/options.go @@ -22,8 +22,6 @@ import ( "k8s.io/cli-runtime/pkg/printers" "sigs.k8s.io/yaml" - - "opendev.org/airship/airshipctl/pkg/errors" ) // ContextOptions holds all configurable options for context @@ -44,7 +42,6 @@ type ManifestOptions struct { Branch string CommitHash string Tag string - RemoteRef string Force bool IsPhase bool TargetPath string @@ -152,9 +149,6 @@ func (o *ManifestOptions) Validate() error { if o.Name == "" { return ErrMissingManifestName{} } - if o.RemoteRef != "" { - return errors.ErrNotImplemented{What: "repository checkout by RemoteRef"} - } if o.IsPhase && o.RepoName == "" { return ErrMissingRepositoryName{} } diff --git a/pkg/config/repo.go b/pkg/config/repo.go index 6cd0e9658..7d8ee6505 100644 --- a/pkg/config/repo.go +++ b/pkg/config/repo.go @@ -18,6 +18,7 @@ import ( "fmt" "github.com/go-git/go-git/v5" + gitconfig "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/plumbing/transport/http" @@ -49,6 +50,8 @@ type Repository struct { Auth *RepoAuth `json:"auth,omitempty"` // CheckoutOptions holds options to checkout repository CheckoutOptions *RepoCheckout `json:"checkout,omitempty"` + // FetchOptions holds options for fetching remote refs + FetchOptions *RepoFetch `json:"fetch,omitempty"` } // RepoAuth struct describes method of authentication against given repository @@ -77,17 +80,27 @@ type RepoCheckout struct { Branch string `json:"branch"` // Tag is the tag name to checkout Tag string `json:"tag"` - // RemoteRef is not supported currently TODO - // RemoteRef is used for remote checkouts such as gerrit change requests/github pull request + // Ref is the ref to checkout // for example refs/changes/04/691202/5 - // TODO Add support for fetching remote refs - RemoteRef string `json:"remoteRef,omitempty"` + Ref string `json:"ref,omitempty"` // ForceCheckout is a boolean to indicate whether to use the `--force` option when checking out ForceCheckout bool `json:"force"` // LocalBranch is a boolean to indicate whether the Branch is local one. False by default LocalBranch bool `json:"localBranch"` } +// RepoFetch holds information on which remote ref to fetch +type RepoFetch struct { + // RemoteRefSpec is used for remote fetches such as gerrit change + // requests and github pull requests. The format of the refspec is an + // optional +, followed by :, where is the pattern for + // references on the remote side and is where those references + // will be written locally. The + tells Git to update the reference + // even if it isn't a fast-forward. + // eg.: refs/changes/04/691202/5:refs/changes/04/691202/5 + RemoteRefSpec string `json:"remoteRefSpec,omitempty"` +} + // RepoCheckout methods func (c *RepoCheckout) String() string { @@ -102,7 +115,7 @@ func (c *RepoCheckout) String() string { // repository checkout and returns Error for incorrect values // returns nil when there are no errors func (c *RepoCheckout) Validate() error { - possibleValues := []string{c.CommitHash, c.Branch, c.Tag, c.RemoteRef} + possibleValues := []string{c.CommitHash, c.Branch, c.Tag, c.Ref} var count int for _, val := range possibleValues { if val != "" { @@ -112,8 +125,14 @@ func (c *RepoCheckout) Validate() error { if count > 1 { return ErrMutuallyExclusiveCheckout{} } - if c.RemoteRef != "" { - return errors.ErrNotImplemented{What: "repository checkout by RemoteRef"} + return nil +} + +// Validate verifies that the remote refspec is valid. If a remote refspec was +// not supplied, Validate does nothing. +func (rf *RepoFetch) Validate() error { + if rf.RemoteRefSpec != "" { + return gitconfig.RefSpec(rf.RemoteRefSpec).Validate() } return nil } @@ -238,6 +257,8 @@ func (repo *Repository) ToCheckoutOptions() *git.CheckoutOptions { co.Branch = plumbing.NewTagReferenceName(repo.CheckoutOptions.Tag) case repo.CheckoutOptions.CommitHash != "": co.Hash = plumbing.NewHash(repo.CheckoutOptions.CommitHash) + case repo.CheckoutOptions.Ref != "": + co.Branch = plumbing.ReferenceName(repo.CheckoutOptions.Ref) } } return co @@ -257,7 +278,14 @@ func (repo *Repository) ToCloneOptions(auth transport.AuthMethod) *git.CloneOpti // ToFetchOptions returns an instance of git.FetchOptions for given authentication // FetchOptions describes how a fetch should be performed func (repo *Repository) ToFetchOptions(auth transport.AuthMethod) *git.FetchOptions { - return &git.FetchOptions{Auth: auth} + var refSpecs []gitconfig.RefSpec + if repo.FetchOptions != nil && repo.FetchOptions.RemoteRefSpec != "" { + refSpecs = []gitconfig.RefSpec{gitconfig.RefSpec(repo.FetchOptions.RemoteRefSpec)} + } + return &git.FetchOptions{ + Auth: auth, + RefSpecs: refSpecs, + } } // URL returns the repository URL in a string format diff --git a/pkg/document/pull/pull.go b/pkg/document/pull/pull.go index 284aeba7c..fdda273c8 100644 --- a/pkg/document/pull/pull.go +++ b/pkg/document/pull/pull.go @@ -31,7 +31,6 @@ func Pull(cfgFactory config.Factory, noCheckout bool) error { } func cloneRepositories(cfg *config.Config, noCheckout bool) error { - // Clone main repository currentManifest, err := cfg.CurrentContextManifest() log.Debugf("Reading current context manifest information from %s", cfg.LoadedConfigPath()) if err != nil { @@ -39,17 +38,17 @@ func cloneRepositories(cfg *config.Config, noCheckout bool) error { } // Clone repositories - for repoName, extraRepoConfig := range currentManifest.Repositories { - err := extraRepoConfig.Validate() + for repoName, repoConfig := range currentManifest.Repositories { + err := repoConfig.Validate() if err != nil { return err } - repository, err := repo.NewRepository(currentManifest.GetTargetPath(), extraRepoConfig) + repository, err := repo.NewRepository(currentManifest.GetTargetPath(), repoConfig) if err != nil { return err } log.Printf("Downloading %s repository %s from %s into %s", - repoName, repository.Name, extraRepoConfig.URL(), currentManifest.GetTargetPath()) + repoName, repository.Name, repoConfig.URL(), currentManifest.GetTargetPath()) err = repository.Download(noCheckout) if err != nil { return err diff --git a/pkg/document/repo/repo.go b/pkg/document/repo/repo.go index ef4a68a69..04431b9ba 100644 --- a/pkg/document/repo/repo.go +++ b/pkg/document/repo/repo.go @@ -115,6 +115,19 @@ func (repo *Repository) Checkout() error { return tree.Checkout(co) } +// Fetch fetches remote refs +func (repo *Repository) Fetch() error { + if !repo.Driver.IsOpen() { + return ErrNoOpenRepo{} + } + auth, err := repo.ToAuth() + if err != nil { + return fmt.Errorf("failed to build auth options for repository %v: %w", repo.Name, err) + } + fo := repo.ToFetchOptions(auth) + return repo.Driver.Fetch(fo) +} + // Open the repository func (repo *Repository) Open() error { log.Debugf("Attempting to open repository %s", repo.Name) @@ -155,5 +168,11 @@ func (repo *Repository) Download(noCheckout bool) error { if noCheckout { return nil } + + err := repo.Fetch() + if err != nil && err != git.NoErrAlreadyUpToDate { + return fmt.Errorf("failed to fetch refs for repository %v: %w", repo.Name, err) + } + return repo.Checkout() } diff --git a/pkg/document/repo/repo_test.go b/pkg/document/repo/repo_test.go index 815cc87bd..0c4e4a62b 100644 --- a/pkg/document/repo/repo_test.go +++ b/pkg/document/repo/repo_test.go @@ -63,7 +63,8 @@ func TestDownload(t *testing.T) { CloneOptions: &git.CloneOptions{ URL: fx.DotGit().Root(), }, - URLString: fx.DotGit().Root(), + FetchOptions: &git.FetchOptions{Auth: nil}, + URLString: fx.DotGit().Root(), } fs := memfs.New()