// Copyright 2016-2020, Pulumi Corporation. // // 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 auto import ( "context" "errors" "fmt" "path/filepath" "strings" "sync" git "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/protocol/packp/capability" "github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/go-git/go-git/v5/plumbing/transport/ssh" ) var transportMutex sync.Mutex func setupGitRepo(ctx context.Context, workDir string, repoArgs *GitRepo) (string, error) { cloneOptions := &git.CloneOptions{ RemoteName: "origin", // be explicit so we can require it in remote refs URL: repoArgs.URL, } if repoArgs.Shallow { cloneOptions.Depth = 1 cloneOptions.SingleBranch = true } if repoArgs.Auth != nil { authDetails := repoArgs.Auth // Each of the authentication options are mutually exclusive so let's check that only 1 is specified if authDetails.SSHPrivateKeyPath != "" && authDetails.Username != "" || authDetails.PersonalAccessToken != "" && authDetails.Username != "" || authDetails.PersonalAccessToken != "" && authDetails.SSHPrivateKeyPath != "" || authDetails.Username != "" && authDetails.SSHPrivateKey != "" { return "", errors.New("please specify one authentication option of `Personal Access Token`, " + "`Username\\Password`, `SSH Private Key Path` or `SSH Private Key`") } // Firstly we will try to check that an SSH Private Key Path has been specified if authDetails.SSHPrivateKeyPath != "" { publicKeys, err := ssh.NewPublicKeysFromFile("git", repoArgs.Auth.SSHPrivateKeyPath, repoArgs.Auth.Password) if err != nil { return "", fmt.Errorf("unable to use SSH Private Key Path: %w", err) } cloneOptions.Auth = publicKeys } // Then we check if the details of a SSH Private Key as passed if authDetails.SSHPrivateKey != "" { publicKeys, err := ssh.NewPublicKeys("git", []byte(repoArgs.Auth.SSHPrivateKey), repoArgs.Auth.Password) if err != nil { return "", fmt.Errorf("unable to use SSH Private Key: %w", err) } cloneOptions.Auth = publicKeys } // Then we check to see if a Personal Access Token has been specified // the username for use with a PAT can be *anything* but an empty string // so we are setting this to `git` if authDetails.PersonalAccessToken != "" { cloneOptions.Auth = &http.BasicAuth{ Username: "git", Password: repoArgs.Auth.PersonalAccessToken, } } // then we check to see if a username and a password has been specified if authDetails.Password != "" && authDetails.Username != "" { cloneOptions.Auth = &http.BasicAuth{ Username: repoArgs.Auth.Username, Password: repoArgs.Auth.Password, } } } // *Repository.Clone() will do appropriate fetching given a branch name. We must deal with // different varieties, since people have been advised to use these as a workaround while only // "refs/heads/<default>" worked. // // If a reference name is not supplied, then .Clone will fetch all refs (and all objects // referenced by those), and checking out a commit later will work as expected. if repoArgs.Branch != "" { refName := plumbing.ReferenceName(repoArgs.Branch) switch { case refName.IsRemote(): // e.g., refs/remotes/origin/branch shorter := refName.Short() // this gives "origin/branch" parts := strings.SplitN(shorter, "/", 2) if len(parts) == 2 && parts[0] == "origin" { refName = plumbing.NewBranchReferenceName(parts[1]) } else { return "", fmt.Errorf("a remote ref must begin with 'refs/remote/origin/', but got %q", repoArgs.Branch) } case refName.IsTag(): // looks like `refs/tags/v1.0.0` -- respect this even though the field is `.Branch` // nothing to do case !refName.IsBranch(): // not a remote, not refs/heads/branch; treat as a simple branch name refName = plumbing.NewBranchReferenceName(repoArgs.Branch) default: // already looks like a full branch name, so use as is } cloneOptions.ReferenceName = refName } // Azure DevOps requires multi_ack and multi_ack_detailed capabilities, which go-git doesn't implement. // But: it's possible to do a full clone by saying it's _not_ _un_supported, in which case the library // happily functions so long as it doesn't _actually_ get a multi_ack packet. See // https://github.com/go-git/go-git/blob/v5.5.1/_examples/azure_devops/main.go. repo, err := func() (*git.Repository, error) { // Because transport.UnsupportedCapabilities is a global variable, we need a global lock around the // use of this. transportMutex.Lock() defer transportMutex.Unlock() oldUnsupportedCaps := transport.UnsupportedCapabilities // This check is crude, but avoids having another dependency to parse the git URL. if strings.Contains(repoArgs.URL, "dev.azure.com") { transport.UnsupportedCapabilities = []capability.Capability{ capability.ThinPack, } } // clone repo, err := git.PlainCloneContext(ctx, workDir, false, cloneOptions) // Regardless of error we need to restore the UnsupportedCapabilities transport.UnsupportedCapabilities = oldUnsupportedCaps return repo, err }() if err != nil { return "", fmt.Errorf("unable to clone repo: %w", err) } if repoArgs.CommitHash != "" { // ensure that the commit has been fetched err := func() error { // repo.FetchContext ends up looking at the global transport.UnsupportedCapabilities, so we need a // global lock around the use of this. transportMutex.Lock() defer transportMutex.Unlock() return repo.FetchContext(ctx, &git.FetchOptions{ RemoteName: "origin", Auth: cloneOptions.Auth, Depth: cloneOptions.Depth, RefSpecs: []config.RefSpec{config.RefSpec(repoArgs.CommitHash + ":" + repoArgs.CommitHash)}, }) }() if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) && !errors.Is(err, git.ErrExactSHA1NotSupported) { return "", fmt.Errorf("fetching commit: %w", err) } // checkout commit if specified w, err := repo.Worktree() if err != nil { return "", err } hash := repoArgs.CommitHash err = w.Checkout(&git.CheckoutOptions{ Hash: plumbing.NewHash(hash), Force: true, }) if err != nil { return "", fmt.Errorf("unable to checkout commit: %w", err) } } var relPath string if repoArgs.ProjectPath != "" { relPath = repoArgs.ProjectPath } workDir = filepath.Join(workDir, relPath) return workDir, nil }