github.com/henvic/wedeploycli@v1.7.6-0.20200319005353-3630f582f284/deployment/transport/git/git.go (about)

     1  package git
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"regexp"
    13  	"runtime"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/hashicorp/errwrap"
    18  	"github.com/henvic/wedeploycli/deployment/internal/groupuid"
    19  	"github.com/henvic/wedeploycli/deployment/transport"
    20  	"github.com/henvic/wedeploycli/envs"
    21  	"github.com/henvic/wedeploycli/services"
    22  	"github.com/henvic/wedeploycli/userhome"
    23  	"github.com/henvic/wedeploycli/verbose"
    24  )
    25  
    26  var errStream io.Writer = os.Stderr
    27  
    28  // Transport using go-git.
    29  type Transport struct {
    30  	ctx      context.Context
    31  	settings transport.Settings
    32  
    33  	start time.Time
    34  	end   time.Time
    35  
    36  	gitEnvCache []string
    37  	gitVersion  string
    38  }
    39  
    40  // Stage files.
    41  func (t *Transport) Stage(s services.ServiceInfoList) (err error) {
    42  	verbose.Debug("Staging files")
    43  
    44  	for _, service := range s {
    45  		if err = t.stageService(filepath.Base(service.Location)); err != nil {
    46  			return err
    47  		}
    48  	}
    49  
    50  	return nil
    51  }
    52  
    53  func (t *Transport) stageService(dest string) error {
    54  	var params = []string{"add", dest}
    55  	verbose.Debug(fmt.Sprintf("Running git %v", strings.Join(params, " ")))
    56  	var cmd = exec.CommandContext(t.ctx, "git", params...) // #nosec
    57  	cmd.Env = t.getConfigEnvs()
    58  	cmd.Dir = t.settings.WorkDir
    59  	cmd.Stderr = errStream
    60  
    61  	return cmd.Run()
    62  }
    63  
    64  // Commit adds all files and commits
    65  func (t *Transport) Commit(message string) (commit string, err error) {
    66  	var params = []string{
    67  		"commit",
    68  		"--no-verify",
    69  		"--allow-empty",
    70  		"--message",
    71  		message,
    72  	}
    73  
    74  	verbose.Debug(fmt.Sprintf("Running git %v", strings.Join(params, " ")))
    75  	var cmd = exec.CommandContext(t.ctx, "git", params...) // #nosec
    76  	cmd.Env = t.getConfigEnvs()
    77  	cmd.Dir = t.settings.WorkDir
    78  
    79  	if verbose.Enabled {
    80  		cmd.Stderr = errStream
    81  	}
    82  
    83  	err = cmd.Run()
    84  
    85  	if err != nil {
    86  		return "", errwrap.Wrapf("can't commit: {{err}}", err)
    87  	}
    88  
    89  	commit, err = t.getLastCommit()
    90  
    91  	if err != nil {
    92  		return "", err
    93  	}
    94  
    95  	verbose.Debug("commit", commit)
    96  	return commit, nil
    97  }
    98  
    99  func (t *Transport) getLastCommit() (commit string, err error) {
   100  	var params = []string{"rev-parse", "HEAD"}
   101  	verbose.Debug(fmt.Sprintf("Running git %v", strings.Join(params, " ")))
   102  	var cmd = exec.CommandContext(t.ctx, "git", params...) // #nosec
   103  	cmd.Env = t.getConfigEnvs()
   104  	var buf bytes.Buffer
   105  	cmd.Dir = t.settings.WorkDir
   106  	cmd.Stderr = errStream
   107  	cmd.Stdout = &buf
   108  
   109  	err = cmd.Run()
   110  
   111  	if err != nil {
   112  		return "", errwrap.Wrapf("can't get last commit: {{err}}", err)
   113  	}
   114  
   115  	commit = strings.TrimSpace(buf.String())
   116  	return commit, nil
   117  }
   118  
   119  // Push deployment to the Liferay Cloud remote
   120  func (t *Transport) Push() (groupUID string, err error) {
   121  	t.start = time.Now()
   122  	defer func() {
   123  		t.end = time.Now()
   124  	}()
   125  
   126  	if t.useCredentialHack() {
   127  		return t.pushHack()
   128  	}
   129  
   130  	var params = []string{"push", t.getGitRemote(), "master", "--force", "--no-verify"}
   131  
   132  	if verbose.Enabled {
   133  		params = append(params, "--verbose")
   134  	}
   135  
   136  	var wectx = t.settings.ConfigContext
   137  
   138  	verbose.Debug(fmt.Sprintf("Running git %v", strings.Join(params, " ")))
   139  	var cmd = exec.CommandContext(t.ctx, "git", params...) // #nosec
   140  	cmd.Env = append(t.getConfigEnvs(),
   141  		"GIT_TERMINAL_PROMPT=0",
   142  		envs.GitCredentialRemoteToken+"="+wectx.Token(),
   143  	)
   144  	cmd.Dir = t.settings.WorkDir
   145  
   146  	var bufErr = copyErrStreamAndVerbose(cmd)
   147  	err = cmd.Run()
   148  
   149  	if err != nil {
   150  		bs := bufErr.String()
   151  		switch {
   152  		case strings.Contains(bs, "fatal: Authentication failed for"),
   153  			strings.Contains(bs, "could not read Username"):
   154  			return "", errors.New("invalid credentials when pushing deployment")
   155  		case strings.Contains(bs, "error: "):
   156  			return "", getGitErrors(bs)
   157  		default:
   158  			return "", err
   159  		}
   160  	}
   161  
   162  	return groupuid.Extract(bufErr.String())
   163  }
   164  
   165  // UploadDuration for deployment (only correct after it finishes)
   166  func (t *Transport) UploadDuration() time.Duration {
   167  	return t.end.Sub(t.start)
   168  }
   169  
   170  // Setup as a git repo
   171  func (t *Transport) Setup(ctx context.Context, settings transport.Settings) error {
   172  	t.ctx = ctx
   173  	t.settings = settings
   174  
   175  	if hasGit := existsDependency("git"); !hasGit {
   176  		return errors.New("git was not found on your system: please visit https://git-scm.com/")
   177  	}
   178  
   179  	// preload the config envs
   180  	_ = t.getConfigEnvs()
   181  
   182  	if err := t.getGitVersion(); err != nil {
   183  		return err
   184  	}
   185  
   186  	return nil
   187  }
   188  
   189  func existsDependency(cmd string) bool {
   190  	_, err := exec.LookPath(cmd)
   191  	return err == nil
   192  }
   193  
   194  func (t *Transport) getGitVersion() error {
   195  	var params = []string{"version"}
   196  	verbose.Debug(fmt.Sprintf("Running git %v", strings.Join(params, " ")))
   197  	var cmd = exec.CommandContext(t.ctx, "git", params...) // #nosec
   198  	cmd.Env = t.getConfigEnvs()
   199  	cmd.Dir = t.settings.WorkDir
   200  	var buf bytes.Buffer
   201  	cmd.Stderr = errStream
   202  	cmd.Stdout = &buf
   203  
   204  	if err := cmd.Run(); err != nil {
   205  		return err
   206  	}
   207  
   208  	verbose.Debug(buf.String())
   209  
   210  	// filter using semver partially
   211  	r := regexp.MustCompile(`(\d+.\d+.\d+)(-[0-9A-Za-z-]*.\d*)?`)
   212  	var b = r.FindStringSubmatch(buf.String())
   213  
   214  	switch len(b) {
   215  	case 0:
   216  		t.gitVersion = buf.String()
   217  	default:
   218  		t.gitVersion = b[0]
   219  	}
   220  
   221  	return nil
   222  }
   223  
   224  // Init repository
   225  func (t *Transport) Init() (err error) {
   226  	var params = []string{"init"}
   227  	verbose.Debug(fmt.Sprintf("Running git %v", strings.Join(params, " ")))
   228  	var cmd = exec.CommandContext(t.ctx, "git", params...) // #nosec
   229  	cmd.Env = t.getConfigEnvs()
   230  	cmd.Dir = t.settings.WorkDir
   231  	cmd.Stderr = errStream
   232  
   233  	if err := cmd.Run(); err != nil {
   234  		return err
   235  	}
   236  
   237  	if err := t.setKeepLineEndings(); err != nil {
   238  		return err
   239  	}
   240  
   241  	if err := t.setStopLineEndingsWarnings(); err != nil {
   242  		return err
   243  	}
   244  
   245  	return t.setGitAuthor()
   246  }
   247  
   248  func (t *Transport) setKeepLineEndings() error {
   249  	var params = []string{"config", "core.autocrlf", "false", "--local"}
   250  	verbose.Debug(fmt.Sprintf("Running git %v", strings.Join(params, " ")))
   251  	var cmd = exec.CommandContext(t.ctx, "git", params...) // #nosec
   252  	cmd.Env = t.getConfigEnvs()
   253  	cmd.Dir = t.settings.WorkDir
   254  	cmd.Stderr = errStream
   255  
   256  	return cmd.Run()
   257  }
   258  
   259  func (t *Transport) setStopLineEndingsWarnings() error {
   260  	var params = []string{"config", "core.safecrlf", "false", "--local"}
   261  	verbose.Debug(fmt.Sprintf("Running git %v", strings.Join(params, " ")))
   262  	var cmd = exec.CommandContext(t.ctx, "git", params...) // #nosec
   263  	cmd.Env = t.getConfigEnvs()
   264  	cmd.Dir = t.settings.WorkDir
   265  	cmd.Stderr = errStream
   266  
   267  	return cmd.Run()
   268  }
   269  
   270  func (t *Transport) setGitAuthor() error {
   271  	if err := t.setGitAuthorName(); err != nil {
   272  		return err
   273  	}
   274  
   275  	return t.setGitAuthorEmail()
   276  }
   277  
   278  func (t *Transport) setGitAuthorName() error {
   279  	var params = []string{"config", "user.name", "Liferay Cloud user", "--local"}
   280  	verbose.Debug(fmt.Sprintf("Running git %v", strings.Join(params, " ")))
   281  	var cmd = exec.CommandContext(t.ctx, "git", params...) // #nosec
   282  	cmd.Env = t.getConfigEnvs()
   283  	cmd.Dir = t.settings.WorkDir
   284  	cmd.Stderr = errStream
   285  
   286  	return cmd.Run()
   287  }
   288  
   289  func (t *Transport) setGitAuthorEmail() error {
   290  	var params = []string{"config", "user.email", "user@deployment", "--local"}
   291  	verbose.Debug(fmt.Sprintf("Running git %v", strings.Join(params, " ")))
   292  	var cmd = exec.CommandContext(t.ctx, "git", params...) // #nosec
   293  	cmd.Env = t.getConfigEnvs()
   294  	cmd.Dir = t.settings.WorkDir
   295  	cmd.Stderr = errStream
   296  
   297  	return cmd.Run()
   298  }
   299  
   300  func (t *Transport) getGitRemote() string {
   301  	var remote = t.settings.ConfigContext.Remote()
   302  
   303  	// always add a "wedeploy-" prefix to all deployment remote endpoints, but "lcp"
   304  	if remote != "lcp" {
   305  		remote = "lcp" + "-" + remote
   306  	}
   307  
   308  	return remote
   309  }
   310  
   311  // ProcessIgnored gets what file should be ignored.
   312  func (t *Transport) ProcessIgnored() (map[string]struct{}, error) {
   313  	var params = []string{"status", "--ignored", "--untracked-files=all", "--porcelain", "--", "."}
   314  	verbose.Debug(fmt.Sprintf("Running git %v", strings.Join(params, " ")))
   315  	var cmd = exec.CommandContext(t.ctx, "git", params...) // #nosec
   316  	cmd.Env = append(t.getConfigEnvs(), "GIT_WORK_TREE="+t.settings.Path)
   317  	cmd.Dir = t.settings.Path
   318  	cmd.Stderr = errStream
   319  
   320  	var out = &bytes.Buffer{}
   321  	cmd.Stdout = out
   322  	var list = map[string]struct{}{}
   323  
   324  	if err := cmd.Run(); err != nil {
   325  		return nil, err
   326  	}
   327  
   328  	const ignorePattern = "!! "
   329  
   330  	for _, w := range bytes.Split(out.Bytes(), []byte("\n")) {
   331  		if bytes.HasPrefix(w, []byte(ignorePattern)) {
   332  			p := filepath.Join(t.settings.Path,
   333  				string(bytes.TrimPrefix(w, []byte(ignorePattern))))
   334  			list[p] = struct{}{}
   335  		}
   336  	}
   337  
   338  	if len(list) != 0 {
   339  		verbose.Debug(fmt.Sprintf(
   340  			"Ignoring %d files and directories found on .gitignore files",
   341  			len(list)))
   342  
   343  	}
   344  
   345  	return list, nil
   346  }
   347  
   348  // AddRemote on project
   349  func (t *Transport) AddRemote() (err error) {
   350  	if t.useCredentialHack() {
   351  		return t.addRemoteHack()
   352  	}
   353  
   354  	wectx := t.settings.ConfigContext
   355  
   356  	var gitServer = fmt.Sprintf("https://git.%v/%v.git",
   357  		wectx.InfrastructureDomain(),
   358  		t.settings.ProjectID)
   359  
   360  	var params = []string{"remote", "add", t.getGitRemote(), gitServer}
   361  	verbose.Debug(fmt.Sprintf("Running git %v", strings.Join(params, " ")))
   362  	var cmd = exec.CommandContext(t.ctx, "git", params...) // #nosec
   363  	cmd.Env = t.getConfigEnvs()
   364  	cmd.Dir = t.settings.WorkDir
   365  	cmd.Stderr = errStream
   366  
   367  	if err = cmd.Run(); err != nil {
   368  		return err
   369  	}
   370  
   371  	return t.addCredentialHelper()
   372  }
   373  
   374  func (t *Transport) addEmptyCredentialHelper() (err error) {
   375  	// If credential.helper is configured to the empty string, this resets the helper list to empty
   376  	// (so you may override a helper set by a lower-priority config file by configuring the empty-string helper,
   377  	// followed by whatever set of helpers you would like).
   378  	// https://www.kernel.org/pub/software/scm/git/docs/gitcredentials.html
   379  	var params = []string{"config", "--add", "credential.helper", ""}
   380  	verbose.Debug("Resetting credential helpers")
   381  	verbose.Debug(fmt.Sprintf("Running git %v", strings.Join(params, " ")))
   382  	var cmd = exec.CommandContext(t.ctx, "git", params...) // #nosec
   383  	cmd.Env = t.getConfigEnvs()
   384  	cmd.Dir = t.settings.WorkDir
   385  	cmd.Stderr = errStream
   386  	return cmd.Run()
   387  }
   388  
   389  func (t *Transport) addCredentialHelper() error {
   390  	if t.useCredentialHack() {
   391  		verbose.Debug("Skipping adding git credential helper")
   392  		return nil
   393  	}
   394  
   395  	if err := t.addEmptyCredentialHelper(); err != nil {
   396  		return err
   397  	}
   398  
   399  	bin, err := getWeExecutable()
   400  
   401  	if err != nil {
   402  		return err
   403  	}
   404  
   405  	// Windows... Really? Really? Really? Really.
   406  	// See issue #323
   407  	if runtime.GOOS == "windows" {
   408  		bin = strings.Replace(bin, `\`, `/`, -1)
   409  		bin = strings.Replace(bin, ` `, `\ `, -1)
   410  	}
   411  
   412  	var credentialHelper = bin + " git-credential-helper"
   413  
   414  	var params = []string{"config", "--add", "credential.helper", credentialHelper}
   415  	verbose.Debug(fmt.Sprintf("Running git %v", strings.Join(params, " ")))
   416  	var cmd = exec.CommandContext(t.ctx, "git", params...) // #nosec
   417  	cmd.Env = t.getConfigEnvs()
   418  	cmd.Dir = t.settings.WorkDir
   419  	cmd.Stderr = errStream
   420  	return cmd.Run()
   421  }
   422  
   423  func getWeExecutable() (string, error) {
   424  	var exec, err = os.Executable()
   425  
   426  	if err != nil {
   427  		verbose.Debug(fmt.Sprintf("%v; falling back to os.Args[0]", err))
   428  		return filepath.Abs(os.Args[0])
   429  	}
   430  
   431  	return exec, nil
   432  }
   433  
   434  // // filter using semver partially
   435  var semverMatcher = regexp.MustCompile(`(\d+.\d+.\d+)(-[0-9A-Za-z-]*.\d*)?`)
   436  
   437  // UserAgent of the transport layer.
   438  func (t *Transport) UserAgent() string {
   439  	var params = []string{"version"}
   440  	verbose.Debug(fmt.Sprintf("Running git %v", strings.Join(params, " ")))
   441  	var cmd = exec.CommandContext(t.ctx, "git", params...) // #nosec
   442  	cmd.Env = t.getConfigEnvs()
   443  	cmd.Dir = t.settings.WorkDir
   444  	var buf bytes.Buffer
   445  	cmd.Stderr = errStream
   446  	cmd.Stdout = &buf
   447  
   448  	if err := cmd.Run(); err != nil {
   449  		verbose.Debug(err)
   450  		return "unknown"
   451  	}
   452  
   453  	var v = buf.String()
   454  	verbose.Debug(v)
   455  
   456  	var b = semverMatcher.FindStringSubmatch(v)
   457  
   458  	if len(b) != 0 {
   459  		return b[0]
   460  	}
   461  
   462  	return v
   463  }
   464  
   465  func (t *Transport) getConfigEnvs() (es []string) {
   466  	if len(t.gitEnvCache) != 0 {
   467  		return t.gitEnvCache
   468  	}
   469  
   470  	var originals = os.Environ()
   471  	var vars = map[string]string{}
   472  
   473  	for _, o := range originals {
   474  		if e := strings.SplitN(o, "=", 2); len(e) == 2 {
   475  			vars[e[0]] = e[1]
   476  		}
   477  	}
   478  
   479  	if v, ok := vars[envs.SkipTLSVerification]; ok {
   480  		vars["GIT_SSL_NO_VERIFY"] = v
   481  	}
   482  
   483  	var gitDir = filepath.Join(t.settings.WorkDir, ".git")
   484  
   485  	vars["GIT_DIR"] = gitDir
   486  
   487  	switch runtime.GOOS {
   488  	case "windows":
   489  		verbose.Debug("Microsoft Windows detected: using git system config")
   490  	default:
   491  		vars["GIT_CONFIG_NOSYSTEM"] = "true"
   492  	}
   493  
   494  	var sandboxHome = filepath.Join(userhome.GetHomeDir(), ".wedeploy", "git-sandbox")
   495  	vars["HOME"] = sandboxHome
   496  	vars["XDG_CONFIG_HOME"] = sandboxHome
   497  	vars["GIT_CONFIG"] = filepath.Join(gitDir, "config")
   498  	vars["GIT_WORK_TREE"] = t.settings.WorkDir
   499  
   500  	for key, value := range vars {
   501  		if !strings.HasPrefix(key, fmt.Sprintf("%s=", key)) {
   502  			es = append(es, fmt.Sprintf("%s=%s", key, value))
   503  		}
   504  	}
   505  
   506  	t.gitEnvCache = es
   507  	return es
   508  }
   509  
   510  func copyErrStreamAndVerbose(cmd *exec.Cmd) *bytes.Buffer {
   511  	var bufErr bytes.Buffer
   512  	cmd.Stderr = &bufErr
   513  
   514  	switch {
   515  	case verbose.Enabled && verbose.IsUnsafeMode():
   516  		cmd.Stderr = io.MultiWriter(&bufErr, os.Stderr)
   517  	case verbose.Enabled:
   518  		verbose.Debug(fmt.Sprintf(
   519  			"Use %v=true to override security protection (see wedeploy/cli #327)",
   520  			envs.UnsafeVerbose))
   521  	}
   522  
   523  	return &bufErr
   524  }
   525  
   526  func getGitErrors(s string) error {
   527  	var parts = strings.Split(s, "\n")
   528  	var list = []string{}
   529  	for _, p := range parts {
   530  		if strings.Contains(p, "error: ") {
   531  			list = append(list, p)
   532  		}
   533  	}
   534  
   535  	if len(list) == 0 {
   536  		return nil
   537  	}
   538  
   539  	return fmt.Errorf("push: %v", strings.Join(list, "\n"))
   540  }