Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions server/events/markdown_renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,14 +228,17 @@ var multiProjectApplyTmpl = template.Must(template.New("").Funcs(sprig.TxtFuncMa
var planSuccessUnwrappedTmpl = template.Must(template.New("").Parse(
"```diff\n" +
"{{.TerraformOutput}}\n" +
"```\n\n" + planNextSteps))
"```\n\n" + planNextSteps +
"{{ if .HasDiverged }}\n\n:warning: The branch we're merging into is ahead, it is recommended to pull new commits first.{{end}}"))

var planSuccessWrappedTmpl = template.Must(template.New("").Parse(
"<details><summary>Show Output</summary>\n\n" +
"```diff\n" +
"{{.TerraformOutput}}\n" +
"```\n\n" +
planNextSteps + "\n" +
"</details>"))
"</details>" +
"{{ if .HasDiverged }}\n\n:warning: The branch we're merging into is ahead, it is recommended to pull new commits first.{{end}}"))

// planNextSteps are instructions appended after successful plans as to what
// to do next.
Expand Down
36 changes: 36 additions & 0 deletions server/events/markdown_renderer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,42 @@ $$$
* :repeat: To **plan** this project again, comment:
* $atlantis plan -d path -w workspace$

---
* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
* $atlantis apply$
`,
},
{
"single successful plan with master ahead",
models.PlanCommand,
[]models.ProjectResult{
{
PlanSuccess: &models.PlanSuccess{
TerraformOutput: "terraform-output",
LockURL: "lock-url",
RePlanCmd: "atlantis plan -d path -w workspace",
ApplyCmd: "atlantis apply -d path -w workspace",
HasDiverged: true,
},
Workspace: "workspace",
RepoRelDir: "path",
},
},
models.Github,
`Ran Plan for dir: $path$ workspace: $workspace$

$$$diff
terraform-output
$$$

* :arrow_forward: To **apply** this plan, comment:
* $atlantis apply -d path -w workspace$
* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
* :repeat: To **plan** this project again, comment:
* $atlantis plan -d path -w workspace$

:warning: The branch we're merging into is ahead, it is recommended to pull new commits first.

---
* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
* $atlantis apply$
Expand Down
9 changes: 5 additions & 4 deletions server/events/mock_workingdir_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions server/events/mocks/mock_working_dir.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions server/events/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,10 @@ type PlanSuccess struct {
RePlanCmd string
// ApplyCmd is the command that users should run to apply this plan.
ApplyCmd string
// HasDiverged is true if we're using the checkout merge strategy and the
// branch we're merging into has been updated since we cloned and merged
// it.
HasDiverged bool
}

// PullStatus is the current status of a pull request that is in progress.
Expand Down
4 changes: 2 additions & 2 deletions server/events/project_command_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext,
}
ctx.Log.Debug("%d files were modified in this pull request", len(modifiedFiles))

repoDir, err := p.WorkingDir.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, workspace)
repoDir, _, err := p.WorkingDir.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, workspace)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -176,7 +176,7 @@ func (p *DefaultProjectCommandBuilder) buildProjectPlanCommand(ctx *CommandConte
defer unlockFn()

ctx.Log.Debug("cloning repository")
repoDir, err := p.WorkingDir.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, workspace)
repoDir, _, err := p.WorkingDir.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, workspace)
if err != nil {
return pcc, err
}
Expand Down
3 changes: 2 additions & 1 deletion server/events/project_command_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ func (p *DefaultProjectCommandRunner) doPlan(ctx models.ProjectCommandContext) (
defer unlockFn()

// Clone is idempotent so okay to run even if the repo was already cloned.
repoDir, cloneErr := p.WorkingDir.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, ctx.Workspace)
repoDir, hasDiverged, cloneErr := p.WorkingDir.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, ctx.Workspace)
if cloneErr != nil {
if unlockErr := lockAttempt.UnlockFn(); unlockErr != nil {
ctx.Log.Err("error unlocking state after plan error: %v", unlockErr)
Expand All @@ -176,6 +176,7 @@ func (p *DefaultProjectCommandRunner) doPlan(ctx models.ProjectCommandContext) (
TerraformOutput: strings.Join(outputs, "\n"),
RePlanCmd: ctx.RePlanCmd,
ApplyCmd: ctx.ApplyCmd,
HasDiverged: hasDiverged,
}, "", nil
}

Expand Down
5 changes: 3 additions & 2 deletions server/events/project_command_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
package events_test

import (
"github.com/hashicorp/go-version"
"github.com/runatlantis/atlantis/server/events/runtime"
"os"
"testing"

"github.com/hashicorp/go-version"
"github.com/runatlantis/atlantis/server/events/runtime"

. "github.com/petergtz/pegomock"
"github.com/runatlantis/atlantis/server/events"
"github.com/runatlantis/atlantis/server/events/mocks"
Expand Down
74 changes: 60 additions & 14 deletions server/events/working_dir.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ const workingDirPrefix = "repos"
// WorkingDir handles the workspace on disk for running commands.
type WorkingDir interface {
// Clone git clones headRepo, checks out the branch and then returns the
// absolute path to the root of the cloned repo.
Clone(log *logging.SimpleLogger, baseRepo models.Repo, headRepo models.Repo, p models.PullRequest, workspace string) (string, error)
// absolute path to the root of the cloned repo. It also returns
// a boolean indicating if we should warn users that the branch we're
// merging into has been updated since we cloned it.
Clone(log *logging.SimpleLogger, baseRepo models.Repo, headRepo models.Repo, p models.PullRequest, workspace string) (string, bool, error)
// GetWorkingDir returns the path to the workspace for this repo and pull.
// If workspace does not exist on disk, error will be of type os.IsNotExist.
GetWorkingDir(r models.Repo, p models.PullRequest, workspace string) (string, error)
Expand Down Expand Up @@ -70,7 +72,7 @@ func (w *FileWorkspace) Clone(
baseRepo models.Repo,
headRepo models.Repo,
p models.PullRequest,
workspace string) (string, error) {
workspace string) (string, bool, error) {
cloneDir := w.cloneDir(baseRepo, p, workspace)

// If the directory already exists, check if it's at the right commit.
Expand All @@ -89,40 +91,84 @@ func (w *FileWorkspace) Clone(
}
revParseCmd := exec.Command("git", "rev-parse", pullHead) // #nosec
revParseCmd.Dir = cloneDir
output, err := revParseCmd.CombinedOutput()
outputRevParseCmd, err := revParseCmd.CombinedOutput()
if err != nil {
log.Warn("will re-clone repo, could not determine if was at correct commit: %s: %s: %s", strings.Join(revParseCmd.Args, " "), err, string(output))
return w.forceClone(log, cloneDir, headRepo, p)
log.Warn("will re-clone repo, could not determine if was at correct commit: %s: %s: %s", strings.Join(revParseCmd.Args, " "), err, string(outputRevParseCmd))
return cloneDir, false, w.forceClone(log, cloneDir, headRepo, p)
}
currCommit := strings.Trim(string(output), "\n")
currCommit := strings.Trim(string(outputRevParseCmd), "\n")

// We're prefix matching here because BitBucket doesn't give us the full
// commit, only a 12 character prefix.
if strings.HasPrefix(currCommit, p.HeadCommit) {
log.Debug("repo is at correct commit %q so will not re-clone", p.HeadCommit)
return cloneDir, nil
return cloneDir, w.warnDiverged(log, cloneDir), nil
}

log.Debug("repo was already cloned but is not at correct commit, wanted %q got %q", p.HeadCommit, currCommit)
// We'll fall through to re-clone.
}

// Otherwise we clone the repo.
return w.forceClone(log, cloneDir, headRepo, p)
return cloneDir, false, w.forceClone(log, cloneDir, headRepo, p)
}

// warnDiverged returns true if we should warn the user that the branch we're
// merging into has diverged from what we currently have checked out.
// This matters in the case of the merge checkout strategy because after
// cloning the repo and doing the merge, it's possible master was updated.
// Then users won't be getting the merge functionality they expected.
// If there are any errors we return false since we prefer things to succeed
// vs. stopping the plan/apply.
func (w *FileWorkspace) warnDiverged(log *logging.SimpleLogger, cloneDir string) bool {
if !w.CheckoutMerge {
// It only makes sense to warn that master has diverged if we're using
// the checkout merge strategy. If we're just checking out the branch,
// then it doesn't matter what's going on with master because we've
// decided to always run off the branch.
return false
}

// Bring our remote refs up to date.
remoteUpdateCmd := exec.Command("git", "remote", "update")
remoteUpdateCmd.Dir = cloneDir
outputRemoteUpdate, err := remoteUpdateCmd.CombinedOutput()
if err != nil {
log.Warn("getting remote update failed: %s", string(outputRemoteUpdate))
return false
}

// Check if remote master branch has diverged.
statusUnoCmd := exec.Command("git", "status", "--untracked-files=no")
statusUnoCmd.Dir = cloneDir
outputStatusUno, err := statusUnoCmd.CombinedOutput()
if err != nil {
log.Warn("getting repo status has failed: %s", string(outputStatusUno))
return false
}
hasDiverged := strings.Contains(string(outputStatusUno), "have diverged")
if hasDiverged {
log.Info("remote master branch is ahead and thereby has new commits, it is recommended to pull new commits")
} else {
log.Debug("remote master branch has no new commits")
}
return hasDiverged
}

func (w *FileWorkspace) forceClone(log *logging.SimpleLogger,
cloneDir string,
headRepo models.Repo,
p models.PullRequest) (string, error) {
p models.PullRequest) error {

err := os.RemoveAll(cloneDir)
if err != nil {
return "", errors.Wrapf(err, "deleting dir %q before cloning", cloneDir)
return errors.Wrapf(err, "deleting dir %q before cloning", cloneDir)
}

// Create the directory and parents if necessary.
log.Info("creating dir %q", cloneDir)
if err := os.MkdirAll(cloneDir, 0700); err != nil {
return "", errors.Wrap(err, "creating new workspace")
return errors.Wrap(err, "creating new workspace")
}

// During testing, we mock some of this out.
Expand Down Expand Up @@ -184,11 +230,11 @@ func (w *FileWorkspace) forceClone(log *logging.SimpleLogger,
sanitizedOutput := w.sanitizeGitCredentials(string(output), p.BaseRepo, headRepo)
if err != nil {
sanitizedErrMsg := w.sanitizeGitCredentials(err.Error(), p.BaseRepo, headRepo)
return "", fmt.Errorf("running %s: %s: %s", cmdStr, sanitizedOutput, sanitizedErrMsg)
return fmt.Errorf("running %s: %s: %s", cmdStr, sanitizedOutput, sanitizedErrMsg)
}
log.Debug("ran: %s. Output: %s", cmdStr, strings.TrimSuffix(sanitizedOutput, "\n"))
}
return cloneDir, nil
return nil
}

// GetWorkingDir returns the path to the workspace for this repo and pull.
Expand Down
Loading