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 }