2016-01-29 20:53:56 +01:00
package getter
import (
2019-02-21 09:52:47 +01:00
"context"
2017-01-26 21:33:22 +01:00
"encoding/base64"
2016-01-29 20:53:56 +01:00
"fmt"
"io/ioutil"
"net/url"
"os"
"os/exec"
"path/filepath"
2019-02-21 09:52:47 +01:00
"runtime"
"strconv"
2017-01-26 21:33:22 +01:00
"strings"
2016-01-29 20:53:56 +01:00
urlhelper "github.com/hashicorp/go-getter/helper/url"
2019-02-21 09:52:47 +01:00
safetemp "github.com/hashicorp/go-safetemp"
version "github.com/hashicorp/go-version"
2016-01-29 20:53:56 +01:00
)
// GitGetter is a Getter implementation that will download a module from
// a git repository.
2019-02-21 09:52:47 +01:00
type GitGetter struct {
getter
}
2016-01-29 20:53:56 +01:00
2017-01-26 21:33:22 +01:00
func ( g * GitGetter ) ClientMode ( _ * url . URL ) ( ClientMode , error ) {
return ClientModeDir , nil
}
2016-01-29 20:53:56 +01:00
func ( g * GitGetter ) Get ( dst string , u * url . URL ) error {
2019-02-21 09:52:47 +01:00
ctx := g . Context ( )
2016-01-29 20:53:56 +01:00
if _ , err := exec . LookPath ( "git" ) ; err != nil {
return fmt . Errorf ( "git must be available and on the PATH" )
}
2019-05-14 01:28:06 +02:00
// The port number must be parseable as an integer. If not, the user
// was probably trying to use a scp-style address, in which case the
// ssh:// prefix must be removed to indicate that.
2019-09-09 14:04:58 +02:00
//
// This is not necessary in versions of Go which have patched
// CVE-2019-14809 (e.g. Go 1.12.8+)
2019-05-14 01:28:06 +02:00
if portStr := u . Port ( ) ; portStr != "" {
if _ , err := strconv . ParseUint ( portStr , 10 , 16 ) ; err != nil {
return fmt . Errorf ( "invalid port number %q; if using the \"scp-like\" git address scheme where a colon introduces the path instead, remove the ssh:// portion and use just the git:: prefix" , portStr )
}
}
2016-01-29 20:53:56 +01:00
// Extract some query parameters we use
2017-01-26 21:33:22 +01:00
var ref , sshKey string
2019-02-21 09:52:47 +01:00
var depth int
2016-01-29 20:53:56 +01:00
q := u . Query ( )
if len ( q ) > 0 {
ref = q . Get ( "ref" )
q . Del ( "ref" )
2017-01-26 21:33:22 +01:00
sshKey = q . Get ( "sshkey" )
q . Del ( "sshkey" )
2019-02-21 09:52:47 +01:00
if n , err := strconv . Atoi ( q . Get ( "depth" ) ) ; err == nil {
depth = n
}
q . Del ( "depth" )
2016-01-29 20:53:56 +01:00
// Copy the URL
var newU url . URL = * u
u = & newU
u . RawQuery = q . Encode ( )
}
2017-01-26 21:33:22 +01:00
var sshKeyFile string
if sshKey != "" {
// Check that the git version is sufficiently new.
if err := checkGitVersion ( "2.3" ) ; err != nil {
return fmt . Errorf ( "Error using ssh key: %v" , err )
}
// We have an SSH key - decode it.
raw , err := base64 . StdEncoding . DecodeString ( sshKey )
if err != nil {
return err
}
// Create a temp file for the key and ensure it is removed.
fh , err := ioutil . TempFile ( "" , "go-getter" )
if err != nil {
return err
}
sshKeyFile = fh . Name ( )
defer os . Remove ( sshKeyFile )
// Set the permissions prior to writing the key material.
if err := os . Chmod ( sshKeyFile , 0600 ) ; err != nil {
return err
}
// Write the raw key into the temp file.
_ , err = fh . Write ( raw )
fh . Close ( )
if err != nil {
return err
}
}
// Clone or update the repository
2016-01-29 20:53:56 +01:00
_ , err := os . Stat ( dst )
if err != nil && ! os . IsNotExist ( err ) {
return err
}
if err == nil {
2019-02-21 09:52:47 +01:00
err = g . update ( ctx , dst , sshKeyFile , ref , depth )
2016-01-29 20:53:56 +01:00
} else {
2019-02-21 09:52:47 +01:00
err = g . clone ( ctx , dst , sshKeyFile , u , depth )
2016-01-29 20:53:56 +01:00
}
if err != nil {
return err
}
// Next: check out the proper tag/branch if it is specified, and checkout
2017-01-26 21:33:22 +01:00
if ref != "" {
if err := g . checkout ( dst , ref ) ; err != nil {
return err
}
2016-01-29 20:53:56 +01:00
}
2017-01-26 21:33:22 +01:00
// Lastly, download any/all submodules.
2019-02-21 09:52:47 +01:00
return g . fetchSubmodules ( ctx , dst , sshKeyFile , depth )
2016-01-29 20:53:56 +01:00
}
// GetFile for Git doesn't support updating at this time. It will download
// the file every time.
func ( g * GitGetter ) GetFile ( dst string , u * url . URL ) error {
2018-04-10 18:03:44 +02:00
td , tdcloser , err := safetemp . Dir ( "" , "getter" )
2016-01-29 20:53:56 +01:00
if err != nil {
return err
}
2018-04-10 18:03:44 +02:00
defer tdcloser . Close ( )
2016-01-29 20:53:56 +01:00
// Get the filename, and strip the filename from the URL so we can
// just get the repository directly.
filename := filepath . Base ( u . Path )
u . Path = filepath . Dir ( u . Path )
// Get the full repository
if err := g . Get ( td , u ) ; err != nil {
return err
}
// Copy the single file
u , err = urlhelper . Parse ( fmtFileURL ( filepath . Join ( td , filename ) ) )
if err != nil {
return err
}
fg := & FileGetter { Copy : true }
return fg . GetFile ( dst , u )
}
func ( g * GitGetter ) checkout ( dst string , ref string ) error {
cmd := exec . Command ( "git" , "checkout" , ref )
cmd . Dir = dst
return getRunCommand ( cmd )
}
2019-02-21 09:52:47 +01:00
func ( g * GitGetter ) clone ( ctx context . Context , dst , sshKeyFile string , u * url . URL , depth int ) error {
args := [ ] string { "clone" }
if depth > 0 {
args = append ( args , "--depth" , strconv . Itoa ( depth ) )
}
args = append ( args , u . String ( ) , dst )
cmd := exec . CommandContext ( ctx , "git" , args ... )
2017-01-26 21:33:22 +01:00
setupGitEnv ( cmd , sshKeyFile )
2016-01-29 20:53:56 +01:00
return getRunCommand ( cmd )
}
2019-02-21 09:52:47 +01:00
func ( g * GitGetter ) update ( ctx context . Context , dst , sshKeyFile , ref string , depth int ) error {
2016-01-29 20:53:56 +01:00
// Determine if we're a branch. If we're NOT a branch, then we just
// switch to master prior to checking out
2019-02-21 09:52:47 +01:00
cmd := exec . CommandContext ( ctx , "git" , "show-ref" , "-q" , "--verify" , "refs/heads/" + ref )
2016-01-29 20:53:56 +01:00
cmd . Dir = dst
2017-01-26 21:33:22 +01:00
2016-01-29 20:53:56 +01:00
if getRunCommand ( cmd ) != nil {
// Not a branch, switch to master. This will also catch non-existent
// branches, in which case we want to switch to master and then
// checkout the proper branch later.
ref = "master"
}
// We have to be on a branch to pull
if err := g . checkout ( dst , ref ) ; err != nil {
return err
}
2019-02-21 09:52:47 +01:00
if depth > 0 {
cmd = exec . Command ( "git" , "pull" , "--depth" , strconv . Itoa ( depth ) , "--ff-only" )
} else {
cmd = exec . Command ( "git" , "pull" , "--ff-only" )
}
2016-01-29 20:53:56 +01:00
cmd . Dir = dst
2017-01-26 21:33:22 +01:00
setupGitEnv ( cmd , sshKeyFile )
2016-01-29 20:53:56 +01:00
return getRunCommand ( cmd )
}
2017-01-26 21:33:22 +01:00
// fetchSubmodules downloads any configured submodules recursively.
2019-02-21 09:52:47 +01:00
func ( g * GitGetter ) fetchSubmodules ( ctx context . Context , dst , sshKeyFile string , depth int ) error {
args := [ ] string { "submodule" , "update" , "--init" , "--recursive" }
if depth > 0 {
args = append ( args , "--depth" , strconv . Itoa ( depth ) )
}
cmd := exec . CommandContext ( ctx , "git" , args ... )
2017-01-26 21:33:22 +01:00
cmd . Dir = dst
setupGitEnv ( cmd , sshKeyFile )
return getRunCommand ( cmd )
}
// setupGitEnv sets up the environment for the given command. This is used to
// pass configuration data to git and ssh and enables advanced cloning methods.
func setupGitEnv ( cmd * exec . Cmd , sshKeyFile string ) {
2017-09-14 16:19:31 +02:00
const gitSSHCommand = "GIT_SSH_COMMAND="
var sshCmd [ ] string
// If we have an existing GIT_SSH_COMMAND, we need to append our options.
// We will also remove our old entry to make sure the behavior is the same
// with versions of Go < 1.9.
env := os . Environ ( )
for i , v := range env {
2019-02-21 09:52:47 +01:00
if strings . HasPrefix ( v , gitSSHCommand ) && len ( v ) > len ( gitSSHCommand ) {
2017-09-14 16:19:31 +02:00
sshCmd = [ ] string { v }
env [ i ] , env [ len ( env ) - 1 ] = env [ len ( env ) - 1 ] , env [ i ]
env = env [ : len ( env ) - 1 ]
break
}
}
if len ( sshCmd ) == 0 {
sshCmd = [ ] string { gitSSHCommand + "ssh" }
}
2017-01-26 21:33:22 +01:00
if sshKeyFile != "" {
// We have an SSH key temp file configured, tell ssh about this.
2019-02-21 09:52:47 +01:00
if runtime . GOOS == "windows" {
sshKeyFile = strings . Replace ( sshKeyFile , ` \ ` , ` / ` , - 1 )
}
2017-09-14 16:19:31 +02:00
sshCmd = append ( sshCmd , "-i" , sshKeyFile )
2017-01-26 21:33:22 +01:00
}
2017-09-14 16:19:31 +02:00
env = append ( env , strings . Join ( sshCmd , " " ) )
cmd . Env = env
2017-01-26 21:33:22 +01:00
}
// checkGitVersion is used to check the version of git installed on the system
// against a known minimum version. Returns an error if the installed version
// is older than the given minimum.
func checkGitVersion ( min string ) error {
want , err := version . NewVersion ( min )
if err != nil {
return err
}
out , err := exec . Command ( "git" , "version" ) . Output ( )
if err != nil {
return err
}
fields := strings . Fields ( string ( out ) )
2019-02-21 09:52:47 +01:00
if len ( fields ) < 3 {
2017-01-26 21:33:22 +01:00
return fmt . Errorf ( "Unexpected 'git version' output: %q" , string ( out ) )
}
2019-02-21 09:52:47 +01:00
v := fields [ 2 ]
if runtime . GOOS == "windows" && strings . Contains ( v , ".windows." ) {
// on windows, git version will return for example:
// git version 2.20.1.windows.1
// Which does not follow the semantic versionning specs
// https://semver.org. We remove that part in order for
// go-version to not error.
v = v [ : strings . Index ( v , ".windows." ) ]
}
2017-01-26 21:33:22 +01:00
2019-02-21 09:52:47 +01:00
have , err := version . NewVersion ( v )
2017-01-26 21:33:22 +01:00
if err != nil {
return err
}
if have . LessThan ( want ) {
return fmt . Errorf ( "Required git version = %s, have %s" , want , have )
}
return nil
}