Detects if master branch has diverged and warn to pull new commits in comment

This commit is contained in:
Marco Rinalducci
2019-10-26 17:13:06 +02:00
committed by Luke Kysow
parent 54d677aed0
commit a824ad4d23
10 changed files with 195 additions and 36 deletions

View File

@@ -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: Master branch 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: Master branch is ahead, it is recommended to pull new commits first.{{end}}"))
// planNextSteps are instructions appended after successful plans as to what
// to do next.

View File

@@ -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: Master branch is ahead, it is recommended to pull new commits first.
---
* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
* $atlantis apply$

View File

@@ -4,11 +4,12 @@
package events
import (
"reflect"
"time"
pegomock "github.com/petergtz/pegomock"
models "github.com/runatlantis/atlantis/server/events/models"
logging "github.com/runatlantis/atlantis/server/logging"
"reflect"
"time"
)
type MockWorkingDir struct {
@@ -26,7 +27,7 @@ func NewMockWorkingDir(options ...pegomock.Option) *MockWorkingDir {
func (mock *MockWorkingDir) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }
func (mock *MockWorkingDir) FailHandler() pegomock.FailHandler { return mock.fail }
func (mock *MockWorkingDir) Clone(log *logging.SimpleLogger, baseRepo models.Repo, headRepo models.Repo, p models.PullRequest, workspace string) (string, error) {
func (mock *MockWorkingDir) Clone(log *logging.SimpleLogger, baseRepo models.Repo, headRepo models.Repo, p models.PullRequest, workspace string) (string, bool, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockWorkingDir().")
}
@@ -42,7 +43,7 @@ func (mock *MockWorkingDir) Clone(log *logging.SimpleLogger, baseRepo models.Rep
ret1 = result[1].(error)
}
}
return ret0, ret1
return ret0, false, ret1
}
func (mock *MockWorkingDir) GetWorkingDir(r models.Repo, p models.PullRequest, workspace string) (string, error) {

View File

@@ -4,11 +4,12 @@
package mocks
import (
"reflect"
"time"
pegomock "github.com/petergtz/pegomock"
models "github.com/runatlantis/atlantis/server/events/models"
logging "github.com/runatlantis/atlantis/server/logging"
"reflect"
"time"
)
type MockWorkingDir struct {
@@ -26,7 +27,7 @@ func NewMockWorkingDir(options ...pegomock.Option) *MockWorkingDir {
func (mock *MockWorkingDir) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }
func (mock *MockWorkingDir) FailHandler() pegomock.FailHandler { return mock.fail }
func (mock *MockWorkingDir) Clone(log *logging.SimpleLogger, baseRepo models.Repo, headRepo models.Repo, p models.PullRequest, workspace string) (string, error) {
func (mock *MockWorkingDir) Clone(log *logging.SimpleLogger, baseRepo models.Repo, headRepo models.Repo, p models.PullRequest, workspace string) (string, bool, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockWorkingDir().")
}
@@ -42,7 +43,7 @@ func (mock *MockWorkingDir) Clone(log *logging.SimpleLogger, baseRepo models.Rep
ret1 = result[1].(error)
}
}
return ret0, ret1
return ret0, false, ret1
}
func (mock *MockWorkingDir) GetWorkingDir(r models.Repo, p models.PullRequest, workspace string) (string, error) {

View File

@@ -431,6 +431,8 @@ type PlanSuccess struct {
RePlanCmd string
// ApplyCmd is the command that users should run to apply this plan.
ApplyCmd string
// Indicates if remote master has diverged
HasDiverged bool
}
// PullStatus is the current status of a pull request that is in progress.

View File

@@ -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
}
@@ -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
}

View File

@@ -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)
@@ -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
}

View File

@@ -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"

View File

@@ -34,8 +34,8 @@ 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 as well as if master branch has diverged.
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)
@@ -70,7 +70,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.
@@ -89,17 +89,41 @@ 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))
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 w.forceClone(log, cloneDir, headRepo, p)
}
currCommit := strings.Trim(string(output), "\n")
currCommit := strings.Trim(string(outputRevParseCmd), "\n")
// 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))
}
// 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))
}
status := strings.Trim(string(outputStatusUno), "\n")
hasDiverged := strings.Contains(status, "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")
}
// 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, hasDiverged, 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.
@@ -112,17 +136,17 @@ func (w *FileWorkspace) Clone(
func (w *FileWorkspace) forceClone(log *logging.SimpleLogger,
cloneDir string,
headRepo models.Repo,
p models.PullRequest) (string, error) {
p models.PullRequest) (string, bool, error) {
err := os.RemoveAll(cloneDir)
if err != nil {
return "", errors.Wrapf(err, "deleting dir %q before cloning", cloneDir)
return "", false, 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 "", false, errors.Wrap(err, "creating new workspace")
}
// During testing, we mock some of this out.
@@ -184,11 +208,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 "", false, fmt.Errorf("running %s: %s: %s", cmdStr, sanitizedOutput, sanitizedErrMsg)
}
log.Debug("ran: %s. Output: %s", cmdStr, strings.TrimSuffix(sanitizedOutput, "\n"))
}
return cloneDir, nil
return cloneDir, false, nil
}
// GetWorkingDir returns the path to the workspace for this repo and pull.

View File

@@ -27,11 +27,14 @@ func TestClone_NoneExisting(t *testing.T) {
TestingOverrideHeadCloneURL: fmt.Sprintf("file://%s", repoDir),
}
cloneDir, err := wd.Clone(nil, models.Repo{}, models.Repo{}, models.PullRequest{
cloneDir, hasDiverged, err := wd.Clone(nil, models.Repo{}, models.Repo{}, models.PullRequest{
HeadBranch: "branch",
}, "default")
Ok(t, err)
// Check if master repo has not diverged
Equals(t, hasDiverged, false)
// Use rev-parse to verify at correct commit.
actCommit := runCmd(t, cloneDir, "git", "rev-parse", "HEAD")
Equals(t, expCommit, actCommit)
@@ -76,12 +79,15 @@ func TestClone_CheckoutMergeNoneExisting(t *testing.T) {
TestingOverrideBaseCloneURL: overrideURL,
}
cloneDir, err := wd.Clone(nil, models.Repo{}, models.Repo{}, models.PullRequest{
cloneDir, hasDiverged, err := wd.Clone(nil, models.Repo{}, models.Repo{}, models.PullRequest{
HeadBranch: "branch",
BaseBranch: "master",
}, "default")
Ok(t, err)
// Check if master repo has not diverged
Equals(t, hasDiverged, false)
// Check the commits.
actBaseCommit := runCmd(t, cloneDir, "git", "rev-parse", "HEAD~1")
actHeadCommit := runCmd(t, cloneDir, "git", "rev-parse", "HEAD^2")
@@ -123,22 +129,28 @@ func TestClone_CheckoutMergeNoReclone(t *testing.T) {
TestingOverrideBaseCloneURL: overrideURL,
}
_, err := wd.Clone(nil, models.Repo{}, models.Repo{}, models.PullRequest{
_, hasDiverged, err := wd.Clone(nil, models.Repo{}, models.Repo{}, models.PullRequest{
HeadBranch: "branch",
BaseBranch: "master",
}, "default")
Ok(t, err)
// Check if master repo has not diverged
Equals(t, hasDiverged, false)
// Create a file that we can use to check if the repo was recloned.
runCmd(t, dataDir, "touch", "repos/0/default/proof")
// Now run the clone again.
cloneDir, err := wd.Clone(nil, models.Repo{}, models.Repo{}, models.PullRequest{
cloneDir, hasDiverged, err := wd.Clone(nil, models.Repo{}, models.Repo{}, models.PullRequest{
HeadBranch: "branch",
BaseBranch: "master",
}, "default")
Ok(t, err)
// Check if master repo has not diverged
Equals(t, hasDiverged, false)
// Check that our proof file is still there, proving that we didn't reclone.
_, err = os.Stat(filepath.Join(cloneDir, "proof"))
Ok(t, err)
@@ -169,22 +181,28 @@ func TestClone_CheckoutMergeNoRecloneFastForward(t *testing.T) {
TestingOverrideBaseCloneURL: overrideURL,
}
_, err := wd.Clone(nil, models.Repo{}, models.Repo{}, models.PullRequest{
_, hasDiverged, err := wd.Clone(nil, models.Repo{}, models.Repo{}, models.PullRequest{
HeadBranch: "branch",
BaseBranch: "master",
}, "default")
Ok(t, err)
// Check if master repo has not diverged
Equals(t, hasDiverged, false)
// Create a file that we can use to check if the repo was recloned.
runCmd(t, dataDir, "touch", "repos/0/default/proof")
// Now run the clone again.
cloneDir, err := wd.Clone(nil, models.Repo{}, models.Repo{}, models.PullRequest{
cloneDir, hasDiverged, err := wd.Clone(nil, models.Repo{}, models.Repo{}, models.PullRequest{
HeadBranch: "branch",
BaseBranch: "master",
}, "default")
Ok(t, err)
// Check if master repo has not diverged
Equals(t, hasDiverged, false)
// Check that our proof file is still there, proving that we didn't reclone.
_, err = os.Stat(filepath.Join(cloneDir, "proof"))
Ok(t, err)
@@ -220,11 +238,14 @@ func TestClone_CheckoutMergeConflict(t *testing.T) {
TestingOverrideBaseCloneURL: overrideURL,
}
_, err := wd.Clone(nil, models.Repo{}, models.Repo{}, models.PullRequest{
_, hasDiverged, err := wd.Clone(nil, models.Repo{}, models.Repo{}, models.PullRequest{
HeadBranch: "branch",
BaseBranch: "master",
}, "default")
// Check if master repo has not diverged
Equals(t, hasDiverged, false)
ErrContains(t, "running git merge -q --no-ff -m atlantis-merge FETCH_HEAD", err)
ErrContains(t, "Auto-merging file", err)
ErrContains(t, "CONFLICT (add/add)", err)
@@ -250,11 +271,14 @@ func TestClone_NoReclone(t *testing.T) {
CheckoutMerge: false,
TestingOverrideHeadCloneURL: fmt.Sprintf("file://%s", repoDir),
}
cloneDir, err := wd.Clone(nil, models.Repo{}, models.Repo{}, models.PullRequest{
cloneDir, hasDiverged, err := wd.Clone(nil, models.Repo{}, models.Repo{}, models.PullRequest{
HeadBranch: "branch",
}, "default")
Ok(t, err)
// Check if master repo has not diverged
Equals(t, hasDiverged, false)
// Check that our proof file is still there.
_, err = os.Stat(filepath.Join(cloneDir, "proof"))
Ok(t, err)
@@ -284,17 +308,83 @@ func TestClone_RecloneWrongCommit(t *testing.T) {
CheckoutMerge: false,
TestingOverrideHeadCloneURL: fmt.Sprintf("file://%s", repoDir),
}
cloneDir, err := wd.Clone(nil, models.Repo{}, models.Repo{}, models.PullRequest{
cloneDir, hasDiverged, err := wd.Clone(nil, models.Repo{}, models.Repo{}, models.PullRequest{
HeadBranch: "branch",
HeadCommit: expCommit,
}, "default")
Ok(t, err)
// Check if master repo has not diverged
Equals(t, hasDiverged, false)
// Use rev-parse to verify at correct commit.
actCommit := runCmd(t, cloneDir, "git", "rev-parse", "HEAD")
Equals(t, expCommit, actCommit)
}
// Test that if master (remote) repo has diverged, see issue 804
func TestClone_MasterHasDiverged(t *testing.T) {
// Initialize the git repo.
repoDir, cleanup := initRepo(t)
defer cleanup()
// Simulate first PR.
runCmd(t, repoDir, "git", "checkout", "-b", "first-pr")
runCmd(t, repoDir, "touch", "file1")
runCmd(t, repoDir, "git", "add", "file1")
runCmd(t, repoDir, "git", "commit", "-m", "file1")
// Atlantis checkout first PR.
firstPRDir := repoDir + "/first-pr"
runCmd(t, repoDir, "mkdir", "-p", "first-pr")
runCmd(t, firstPRDir, "git", "clone", "--branch", "master", "--single-branch", repoDir, ".")
runCmd(t, firstPRDir, "git", "remote", "add", "head", repoDir)
runCmd(t, firstPRDir, "git", "fetch", "head", "+refs/heads/first-pr")
runCmd(t, firstPRDir, "git", "config", "--local", "user.email", "atlantisbot@runatlantis.io")
runCmd(t, firstPRDir, "git", "config", "--local", "user.name", "atlantisbot")
runCmd(t, firstPRDir, "git", "merge", "-q", "--no-ff", "-m", "atlantis-merge", "FETCH_HEAD")
// Simulate second PR.
runCmd(t, repoDir, "git", "checkout", "master")
runCmd(t, repoDir, "git", "checkout", "-b", "second-pr")
runCmd(t, repoDir, "touch", "file2")
runCmd(t, repoDir, "git", "add", "file2")
runCmd(t, repoDir, "git", "commit", "-m", "file2")
// Atlantis checkout second PR.
secondPRDir := repoDir + "/second-pr"
runCmd(t, repoDir, "mkdir", "-p", "second-pr")
runCmd(t, secondPRDir, "git", "clone", "--branch", "master", "--single-branch", repoDir, ".")
runCmd(t, secondPRDir, "git", "remote", "add", "head", repoDir)
runCmd(t, secondPRDir, "git", "fetch", "head", "+refs/heads/second-pr")
runCmd(t, secondPRDir, "git", "config", "--local", "user.email", "atlantisbot@runatlantis.io")
runCmd(t, secondPRDir, "git", "config", "--local", "user.name", "atlantisbot")
runCmd(t, secondPRDir, "git", "merge", "-q", "--no-ff", "-m", "atlantis-merge", "FETCH_HEAD")
// Merge first PR
runCmd(t, repoDir, "git", "checkout", "master")
runCmd(t, repoDir, "git", "merge", "first-pr")
// Copy the second-pr repo to our data dir which has diverged remote master
runCmd(t, repoDir, "mkdir", "-p", "repos/0/")
runCmd(t, repoDir, "cp", "-R", secondPRDir, "repos/0/default")
// Run the clone.
wd := &events.FileWorkspace{
DataDir: repoDir,
CheckoutMerge: true,
}
_, hasDiverged, err := wd.Clone(nil, models.Repo{}, models.Repo{}, models.PullRequest{
HeadBranch: "second-pr",
BaseBranch: "master",
}, "default")
Ok(t, err)
// Check if master repo is ahead and has diverged
Equals(t, hasDiverged, true)
}
func initRepo(t *testing.T) (string, func()) {
repoDir, cleanup := TempDir(t)
runCmd(t, repoDir, "git", "init")