mirror of https://github.com/pulumi/pulumi.git
195 lines
6.9 KiB
Go
195 lines
6.9 KiB
Go
// 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
|
|
}
|