Skip to content

Commit

Permalink
fix(boot): Try to update local boot clone to config ref from versions
Browse files Browse the repository at this point in the history
This will check if the boot clone isn't using the ref the desired tag
points to, after cloning the boot config if needed. If the clone is
using a different ref, it will try to stash any local changes, update
the clone to use the appropriate ref, and unstash those local
changes. If there are errors in the unstash due to conflicts, it'll
report the `git stash pop` error message and tell the user what they
need to do to fix it.

fixes #5929

Signed-off-by: Andrew Bayer <[email protected]>
  • Loading branch information
abayer authored and jenkins-x-bot committed Oct 29, 2019
1 parent d8c3e03 commit 006bdea
Show file tree
Hide file tree
Showing 3 changed files with 302 additions and 15 deletions.
71 changes: 56 additions & 15 deletions pkg/cmd/boot/boot.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,17 @@ func (o *BootOptions) Run() error {
// lets report errors parsing this file after the check we are outside of a git clone
o.defaultVersionStream(requirements)

resolver, err := o.CreateVersionResolver(requirements.VersionStream.URL, requirements.VersionStream.Ref)
if err != nil {
return errors.Wrapf(err, "there was a problem creating a version resolver from versions stream repository %s and ref %s", requirements.VersionStream.URL, requirements.VersionStream.Ref)
}
if o.GitRef == "" {
gitRef, err = o.determineGitRef(resolver, requirements, gitURL)
if err != nil {
return errors.Wrapf(err, "failed to determine git ref")
}
}

if !isBootClone {
log.Logger().Infof("No Jenkins X pipeline file %s or no jx boot requirements file %s found. You are not running this command from inside a "+
"Jenkins X Boot git clone", info(pipelineFile), info(config.RequirementsConfigFileName))
Expand All @@ -163,17 +174,6 @@ func (o *BootOptions) Run() error {
repo := gitInfo.Name
cloneDir := filepath.Join(o.Dir, repo)

if o.GitRef == "" {
resolver, err := o.CreateVersionResolver(requirements.VersionStream.URL, requirements.VersionStream.Ref)
if err != nil {
return errors.Wrapf(err, "failed to create version resolver")
}
gitRef, err = o.determineGitRef(resolver, requirements, gitURL)
if err != nil {
return errors.Wrapf(err, "failed to determine git ref")
}
}

if !o.BatchMode {
log.Logger().Infof("To continue we will clone %s @ %s to %s", info(gitURL), info(gitRef), info(cloneDir))

Expand Down Expand Up @@ -248,6 +248,11 @@ func (o *BootOptions) Run() error {
return fmt.Errorf("no requirements file %s are you sure you are running this command inside a GitOps clone?", requirementsFile)
}

err = o.updateBootCloneIfOutOfDate(gitRef)
if err != nil {
return err
}

err = o.verifyRequirements(requirements, requirementsFile)
if err != nil {
return err
Expand Down Expand Up @@ -299,10 +304,7 @@ func (o *BootOptions) Run() error {
if err != nil {
return errors.Wrapf(err, "setting namespace in jenkins-x.yml")
}
so.VersionResolver, err = o.CreateVersionResolver(requirements.VersionStream.URL, requirements.VersionStream.Ref)
if err != nil {
return errors.Wrapf(err, "there was a problem creating a version resolver from versions stream repository %s and ref %s", requirements.VersionStream.URL, requirements.VersionStream.Ref)
}
so.VersionResolver = resolver

if o.BatchMode {
so.AdditionalEnvVars["JX_BATCH_MODE"] = "true"
Expand All @@ -319,6 +321,45 @@ func (o *BootOptions) Run() error {
return no.Run()
}

func (o *BootOptions) updateBootCloneIfOutOfDate(gitRef string) error {
// Get the tag corrresponding to the git ref.
commitish, err := gits.FindTagForVersion(o.Dir, gitRef, o.Git())
if err != nil {
log.Logger().Debugf(errors.Wrapf(err, "finding tag for %s", gitRef).Error())
commitish = fmt.Sprintf("%s/%s", "origin", gitRef)
}

// Get the tag on the current boot clone HEAD, if any.
resolved, _, err := o.Git().Describe(o.Dir, true, "HEAD", "0", true)
if err != nil {
return errors.Wrap(err, "could not describe boot clone HEAD to find its tag with git describe HEAD --abbrev=0 --contains --always")
}

if resolved != commitish {
log.Logger().Infof("Local boot clone is out of date. It is based on %s, but the version stream is using %s. The clone will now be updated to %s.",
resolved, commitish, commitish)
log.Logger().Info("Stashing any changes made in local boot clone.")

err = o.Git().StashPush(o.Dir)
if err != nil {
return errors.WithStack(err)
}
err = o.Git().Reset(o.Dir, commitish, true)
if err != nil {
return errors.Wrapf(err, "Could not reset local boot clone to %s", commitish)
}

err = o.Git().StashPop(o.Dir)
if err != nil && !gits.IsNoStashEntriesError(err) { // Ignore no stashes as that's just because there was nothing to stash
return fmt.Errorf("Could not update local boot clone due to conflicts between local changes and %s.\n"+
"To fix this, resolve the conflicts listed below manually, run 'git reset HEAD', and run 'jx boot' again.\n%s",
commitish, gits.GetSimpleIndentedStashPopErrorMessage(err))
}
}

return nil
}

func (o *BootOptions) setupCloudBeesProfile(gitURL string) string {
if o.GitURL == "" {
gitURL = config.DefaultCloudBeesBootRepository
Expand Down
228 changes: 228 additions & 0 deletions pkg/cmd/boot/boot_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// +build integration

package boot

import (
"io/ioutil"
"os"
"path/filepath"
"testing"

"github.com/acarl005/stripansi"
"github.com/jenkins-x/jx/pkg/cmd/opts"
"github.com/jenkins-x/jx/pkg/gits"
"github.com/jenkins-x/jx/pkg/log"
"github.com/jenkins-x/jx/pkg/util"
"github.com/stretchr/testify/assert"
)

const (
FirstTag = "v1.0.0"
SecondTag = "v2.0.0"

testFileName = "some-file"
)

func TestUpdateBootCloneIfOutOfDate_Conflicts(t *testing.T) {
gitter := gits.NewGitCLI()

repoDir, err := initializeTempGitRepo(gitter)
assert.NoError(t, err)

defer func() {
err := os.RemoveAll(repoDir)
assert.NoError(t, err)
}()

testDir, err := ioutil.TempDir("", "update-local-boot-clone-test-clone-")
assert.NoError(t, err)
defer func() {
err := os.RemoveAll(testDir)
assert.NoError(t, err)
}()

err = gitter.Clone(repoDir, testDir)
assert.NoError(t, err)

err = gitter.FetchTags(testDir)
assert.NoError(t, err)

err = gitter.Reset(testDir, FirstTag, true)
assert.NoError(t, err)

conflictingContent := []byte("something else")
err = ioutil.WriteFile(filepath.Join(testDir, testFileName), conflictingContent, util.DefaultWritePermissions)
assert.NoError(t, err)

o := &BootOptions{
CommonOptions: &opts.CommonOptions{},
Dir: testDir,
}
r, fakeStdout, _ := os.Pipe()
log.SetOutput(fakeStdout)
o.CommonOptions.Out = fakeStdout

err = o.updateBootCloneIfOutOfDate(SecondTag)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Could not update local boot clone due to conflicts between local changes and v2.0.0.")

fakeStdout.Close()
outBytes, _ := ioutil.ReadAll(r)
r.Close()
output := stripansi.Strip(string(outBytes))
assert.Contains(t, output, "It is based on v1.0.0, but the version stream is using v2.0.0.")
}

func TestUpdateBootCloneIfOutOfDate_NoConflicts(t *testing.T) {
gitter := gits.NewGitCLI()

repoDir, err := initializeTempGitRepo(gitter)
assert.NoError(t, err)

defer func() {
err := os.RemoveAll(repoDir)
assert.NoError(t, err)
}()

testDir, err := ioutil.TempDir("", "update-local-boot-clone-test-clone-")
assert.NoError(t, err)
defer func() {
err := os.RemoveAll(testDir)
assert.NoError(t, err)
}()

err = gitter.Clone(repoDir, testDir)
assert.NoError(t, err)

err = gitter.FetchTags(testDir)
assert.NoError(t, err)

err = gitter.Reset(testDir, FirstTag, true)
assert.NoError(t, err)

conflictingContent := []byte("something else")
err = ioutil.WriteFile(filepath.Join(testDir, "some-other-file"), conflictingContent, util.DefaultWritePermissions)
assert.NoError(t, err)

o := &BootOptions{
CommonOptions: &opts.CommonOptions{},
Dir: testDir,
}
r, fakeStdout, _ := os.Pipe()
log.SetOutput(fakeStdout)
o.CommonOptions.Out = fakeStdout

err = o.updateBootCloneIfOutOfDate(SecondTag)
assert.NoError(t, err)

fakeStdout.Close()
outBytes, _ := ioutil.ReadAll(r)
r.Close()
output := stripansi.Strip(string(outBytes))
assert.Contains(t, output, "It is based on v1.0.0, but the version stream is using v2.0.0.")
}

func TestUpdateBootCloneIfOutOfDate_UpToDate(t *testing.T) {
gitter := gits.NewGitCLI()

repoDir, err := initializeTempGitRepo(gitter)
assert.NoError(t, err)

defer func() {
err := os.RemoveAll(repoDir)
assert.NoError(t, err)
}()

testDir, err := ioutil.TempDir("", "update-local-boot-clone-test-clone-")
assert.NoError(t, err)
defer func() {
err := os.RemoveAll(testDir)
assert.NoError(t, err)
}()

err = gitter.Clone(repoDir, testDir)
assert.NoError(t, err)

err = gitter.FetchTags(testDir)
assert.NoError(t, err)

err = gitter.Reset(testDir, SecondTag, true)
assert.NoError(t, err)

conflictingContent := []byte("something else")
err = ioutil.WriteFile(filepath.Join(testDir, "some-other-file"), conflictingContent, util.DefaultWritePermissions)
assert.NoError(t, err)

o := &BootOptions{
CommonOptions: &opts.CommonOptions{},
Dir: testDir,
}
r, fakeStdout, _ := os.Pipe()
log.SetOutput(fakeStdout)
o.CommonOptions.Out = fakeStdout

err = o.updateBootCloneIfOutOfDate(SecondTag)
assert.NoError(t, err)

fakeStdout.Close()
outBytes, _ := ioutil.ReadAll(r)
r.Close()
output := stripansi.Strip(string(outBytes))
assert.Equal(t, "", output)
}

func initializeTempGitRepo(gitter gits.Gitter) (string, error) {
dir, err := ioutil.TempDir("", "update-local-boot-clone-test-repo-")
if err != nil {
return "", err
}

err = gitter.Init(dir)
if err != nil {
return "", err
}

err = gitter.AddCommit(dir, "Initial Commit")
if err != nil {
return "", err
}

testFile := filepath.Join(dir, testFileName)
testFileContents := []byte("foo")
err = ioutil.WriteFile(testFile, testFileContents, util.DefaultWritePermissions)
if err != nil {
return "", err
}

err = gitter.Add(dir, ".")
if err != nil {
return "", err
}
err = gitter.AddCommit(dir, "Adding foo")
if err != nil {
return "", err
}

err = gitter.CreateTag(dir, FirstTag, "First Tag")
if err != nil {
return "", err
}

testFileContents = []byte("bar")
err = ioutil.WriteFile(testFile, testFileContents, util.DefaultWritePermissions)
if err != nil {
return "", err
}

err = gitter.AddCommit(dir, "Adding bar")
if err != nil {
return "", err
}

err = gitter.CreateTag(dir, SecondTag, "Second Tag")
if err != nil {
return "", err
}

return dir, nil
}
18 changes: 18 additions & 0 deletions pkg/gits/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,24 @@ func IsNoStashEntriesError(err error) bool {
return strings.Contains(err.Error(), "No stash entries found.")
}

// GetSimpleIndentedStashPopErrorMessage gets the output of a failed git stash pop without duplication or additional content,
// with each line indented four characters.
func GetSimpleIndentedStashPopErrorMessage(err error) string {
errStr := err.Error()
idx := strings.Index(errStr, ": failed to run 'git stash pop'")
if idx > -1 {
errStr = errStr[:idx]
}

var indentedLines []string

for _, line := range strings.Split(errStr, "\n") {
indentedLines = append(indentedLines, " "+line)
}

return strings.Join(indentedLines, "\n")
}

// FindTagForVersion will find a tag for a version number (first fetching the tags, then looking for a tag <version>
// then trying the common convention v<version>). It will return the tag or an error if the tag can't be found.
func FindTagForVersion(dir string, version string, gitter Gitter) (string, error) {
Expand Down

0 comments on commit 006bdea

Please sign in to comment.