github.com/jenkins-x/jx/v2@v2.1.155/pkg/gits/operations/pull_request_op.go (about)

     1  package operations
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"time"
     7  
     8  	"github.com/jenkins-x/jx/v2/pkg/helm"
     9  	"github.com/jenkins-x/jx/v2/pkg/secreturl"
    10  
    11  	"github.com/jenkins-x/jx/v2/pkg/cmd/opts"
    12  
    13  	"io"
    14  	"io/ioutil"
    15  	"net/http"
    16  	"os"
    17  	"path/filepath"
    18  	"regexp"
    19  	"sort"
    20  	"strings"
    21  
    22  	"github.com/jenkins-x/jx-logging/pkg/log"
    23  	"github.com/jenkins-x/jx/v2/pkg/dependencymatrix"
    24  	"github.com/jenkins-x/jx/v2/pkg/gits"
    25  	"github.com/jenkins-x/jx/v2/pkg/gits/releases"
    26  
    27  	"github.com/jenkins-x/jx/v2/pkg/versionstream"
    28  
    29  	"github.com/blang/semver"
    30  	"github.com/ghodss/yaml"
    31  	v1 "github.com/jenkins-x/jx-api/pkg/apis/jenkins.io/v1"
    32  
    33  	"github.com/jenkins-x/jx/v2/pkg/util"
    34  	"github.com/pkg/errors"
    35  	"k8s.io/apimachinery/pkg/util/uuid"
    36  )
    37  
    38  const (
    39  	// assetRetrievalTimeout controls the amount of time to wait when attempting to retrieve dependency information
    40  	assetRetrievalTimeout = 3 * time.Minute
    41  )
    42  
    43  // PullRequestOperation provides a way to execute a PullRequest operation using Git
    44  type PullRequestOperation struct {
    45  	*opts.CommonOptions
    46  	GitURLs       []string
    47  	SrcGitURL     string
    48  	Base          string
    49  	Component     string
    50  	BranchName    string
    51  	Version       string
    52  	DryRun        bool
    53  	SkipCommit    bool
    54  	AuthorName    string
    55  	AuthorEmail   string
    56  	SkipAutoMerge bool
    57  	Labels        []string
    58  }
    59  
    60  // ChangeFilesFn is the function called to create the pull request
    61  type ChangeFilesFn func(dir string, gitInfo *gits.GitRepository) ([]string, error)
    62  
    63  // CreatePullRequest will fork (if needed) and pull a git repo, then perform the update, and finally create or update a
    64  // PR for the change. Any open PR on the repo with the `updatebot` label will be updated.
    65  func (o *PullRequestOperation) CreatePullRequest(kind string, update ChangeFilesFn) (*gits.PullRequestInfo, error) {
    66  	var result *gits.PullRequestInfo
    67  	for _, gitURL := range o.GitURLs {
    68  		dir, err := ioutil.TempDir("", "create-pr")
    69  		if err != nil {
    70  			return nil, err
    71  		}
    72  		provider, _, err := o.CreateGitProviderForURLWithoutKind(gitURL)
    73  		if err != nil {
    74  			return nil, errors.Wrapf(err, "creating git provider for directory %s", dir)
    75  		}
    76  
    77  		dir, _, upstreamInfo, forkInfo, err := gits.ForkAndPullRepo(gitURL, dir, o.Base, o.BranchName, provider, o.Git(), "")
    78  		if err != nil {
    79  			return nil, errors.Wrapf(err, "failed to fork and pull %s", o.GitURLs)
    80  		}
    81  
    82  		gitInfo := upstreamInfo
    83  		if forkInfo != nil {
    84  			gitInfo = forkInfo
    85  		}
    86  		commitMessage, details, err := o.updateAndGenerateMessagesAndDependencyMatrix(dir, kind, upstreamInfo.Host, gitInfo, update)
    87  		if err != nil {
    88  			return nil, errors.WithStack(err)
    89  		}
    90  
    91  		labels := []string{}
    92  		if !o.SkipAutoMerge {
    93  			labels = append(labels, "updatebot")
    94  		}
    95  		if len(o.Labels) > 0 {
    96  			labels = append(labels, o.Labels...)
    97  		}
    98  		filter := &gits.PullRequestFilter{
    99  			Labels: labels,
   100  		}
   101  
   102  		details.Labels = labels
   103  		result, err = gits.PushRepoAndCreatePullRequest(dir, upstreamInfo, forkInfo, o.Base, details, filter, !o.SkipCommit, commitMessage, true, o.DryRun, o.Git(), provider)
   104  		if err != nil {
   105  			return nil, errors.Wrapf(err, "failed to create PR for base %s and head branch %s from temp dir %s", o.Base, details.BranchName, dir)
   106  		}
   107  	}
   108  	return result, nil
   109  }
   110  
   111  // WrapChangeFilesWithCommitFn wraps the passed ChangeFilesFn in a commit. This is useful for creating multiple commits
   112  // to batch e.g. in a single PR push/creation
   113  func (o *PullRequestOperation) WrapChangeFilesWithCommitFn(kind string, fn ChangeFilesFn) ChangeFilesFn {
   114  	return func(dir string, gitInfo *gits.GitRepository) ([]string, error) {
   115  		commitMessage, prDetails, err := o.updateAndGenerateMessagesAndDependencyMatrix(dir, kind, gitInfo.Host, gitInfo, fn)
   116  		if err != nil {
   117  			return nil, errors.WithStack(err)
   118  		}
   119  		err = o.Git().Add(dir, "-A")
   120  		if err != nil {
   121  			return nil, errors.WithStack(err)
   122  		}
   123  		changed, err := o.Git().HasChanges(dir)
   124  		if err != nil {
   125  			return nil, errors.WithStack(err)
   126  		}
   127  		if !changed {
   128  			log.Logger().Warnf("No changes made to the source code in %s. Code must be up to date!", dir)
   129  			return nil, nil
   130  		}
   131  		if commitMessage == "" {
   132  			commitMessage = prDetails.Message
   133  		}
   134  		err = o.Git().CommitDir(dir, commitMessage)
   135  		if err != nil {
   136  			return nil, errors.WithStack(err)
   137  		}
   138  		return nil, nil
   139  	}
   140  }
   141  
   142  func (o *PullRequestOperation) updateAndGenerateMessagesAndDependencyMatrix(dir string, kind string, destHost string, gitInfo *gits.GitRepository, update ChangeFilesFn) (string, *gits.PullRequestDetails, error) {
   143  
   144  	oldVersions, err := update(dir, gitInfo)
   145  	if err != nil {
   146  		return "", nil, errors.WithStack(err)
   147  	}
   148  	nonSemantic := make([]string, 0)
   149  	semantic := make([]semver.Version, 0)
   150  	for _, v := range oldVersions {
   151  		sv, err := semver.Parse(v)
   152  		if err != nil {
   153  			nonSemantic = append(nonSemantic, v)
   154  		} else {
   155  			semantic = append(semantic, sv)
   156  		}
   157  	}
   158  	semver.Sort(semantic)
   159  	sort.Strings(nonSemantic)
   160  	dedupedSemantic := make([]string, 0)
   161  	dedupedNonSemantic := make([]string, 0)
   162  	previous := ""
   163  	for _, v := range nonSemantic {
   164  		if v != previous {
   165  			dedupedNonSemantic = append(dedupedNonSemantic, v)
   166  		}
   167  		previous = v
   168  	}
   169  	previous = ""
   170  	for _, v := range semantic {
   171  		vStr := v.String()
   172  		if vStr != previous {
   173  			dedupedSemantic = append(dedupedSemantic, vStr)
   174  		}
   175  		previous = vStr
   176  	}
   177  
   178  	oldVersionsStr := strings.Join(dedupedSemantic, ", ")
   179  	if len(dedupedNonSemantic) > 0 {
   180  		if oldVersionsStr != "" {
   181  			oldVersionsStr += " and "
   182  		}
   183  		oldVersionsStr = oldVersionsStr + strings.Join(dedupedNonSemantic, ", ")
   184  	}
   185  
   186  	// remove the v prefix if we are using a v tag
   187  	version := strings.TrimPrefix(o.Version, "v")
   188  	commitMessage, details, updateDependency, assets, err := o.CreateDependencyUpdatePRDetails(kind, o.SrcGitURL, destHost, oldVersionsStr, version, o.Component)
   189  	if err != nil {
   190  		return "", nil, errors.WithStack(err)
   191  	}
   192  
   193  	var upstreamDependencyAsset *gits.GitReleaseAsset
   194  
   195  	for _, a := range assets {
   196  		asset := a
   197  		if asset.Name == dependencymatrix.DependencyUpdatesAssetName {
   198  			upstreamDependencyAsset = &asset
   199  			break
   200  		}
   201  	}
   202  
   203  	if updateDependency != nil {
   204  		err = dependencymatrix.UpdateDependencyMatrix(dir, updateDependency)
   205  		if err != nil {
   206  			return "", nil, errors.WithStack(err)
   207  		}
   208  	}
   209  
   210  	if upstreamDependencyAsset != nil {
   211  		updatedPaths, err := AddDependencyMatrixUpdatePaths(upstreamDependencyAsset, updateDependency)
   212  		if err != nil {
   213  			log.Logger().Warnf("adding dependency updates: %q", err)
   214  		} else {
   215  			for _, d := range updatedPaths {
   216  				err = dependencymatrix.UpdateDependencyMatrix(dir, d)
   217  				if err != nil {
   218  					return "", nil, errors.Wrapf(err, "updating dependency matrix with upstream dependency %+v", d)
   219  				}
   220  			}
   221  		}
   222  	}
   223  	return commitMessage, details, nil
   224  }
   225  
   226  // AddDependencyMatrixUpdatePaths retrieves the upstreamDependencyAsset and converts it to a slice of DependencyUpdates, prepending the updateDependency to the path
   227  func AddDependencyMatrixUpdatePaths(upstreamDependencyAsset *gits.GitReleaseAsset, updateDependency *v1.DependencyUpdate) ([]*v1.DependencyUpdate, error) {
   228  	var upstreamUpdates dependencymatrix.DependencyUpdates
   229  	var updates []*v1.DependencyUpdate
   230  
   231  	log.Logger().Infof("Attempting to retrieve dependency asset information from %q", upstreamDependencyAsset.BrowserDownloadURL)
   232  
   233  	err := util.Retry(assetRetrievalTimeout, func() error {
   234  		resp, err := http.Get(upstreamDependencyAsset.BrowserDownloadURL)
   235  		if err != nil {
   236  			return errors.Wrapf(err, "retrieving dependency updates from %s", upstreamDependencyAsset.BrowserDownloadURL)
   237  		}
   238  		defer resp.Body.Close()
   239  		// Write the body
   240  		var b bytes.Buffer
   241  		_, err = io.Copy(&b, resp.Body)
   242  		if err != nil {
   243  			return errors.Wrapf(err, "copying response body after retrieving dependency updates from %s", upstreamDependencyAsset.BrowserDownloadURL)
   244  		}
   245  		bytes := b.Bytes()
   246  		err = yaml.Unmarshal(bytes, &upstreamUpdates)
   247  		if err != nil {
   248  			log.Logger().Errorf("unable to unmarshall from %s", string(bytes))
   249  			return errors.Wrapf(err, "unmarshaling dependency updates from %s", upstreamDependencyAsset.BrowserDownloadURL)
   250  		}
   251  		return nil
   252  	})
   253  
   254  	if err != nil {
   255  		return nil, errors.Wrapf(err, "getting dependency updates from %s", upstreamDependencyAsset.BrowserDownloadURL)
   256  	}
   257  
   258  	for _, d := range upstreamUpdates.Updates {
   259  		// Need to prepend a path element
   260  		if d.Paths == nil {
   261  			d.Paths = []v1.DependencyUpdatePath{
   262  				{
   263  					{
   264  						Host:               updateDependency.Host,
   265  						Owner:              updateDependency.Owner,
   266  						Repo:               updateDependency.Repo,
   267  						URL:                updateDependency.URL,
   268  						Component:          updateDependency.Component,
   269  						ToReleaseHTMLURL:   updateDependency.ToReleaseHTMLURL,
   270  						ToVersion:          updateDependency.ToVersion,
   271  						FromVersion:        updateDependency.FromVersion,
   272  						FromReleaseName:    updateDependency.FromReleaseName,
   273  						FromReleaseHTMLURL: updateDependency.FromReleaseHTMLURL,
   274  						ToReleaseName:      updateDependency.ToReleaseName,
   275  					},
   276  				},
   277  			}
   278  		} else {
   279  			for j, e := range d.Paths {
   280  				if e == nil {
   281  					d.Paths[j] = v1.DependencyUpdatePath{
   282  						{
   283  							Host:               updateDependency.Host,
   284  							Owner:              updateDependency.Owner,
   285  							Repo:               updateDependency.Repo,
   286  							URL:                updateDependency.URL,
   287  							Component:          updateDependency.Component,
   288  							ToReleaseHTMLURL:   updateDependency.ToReleaseHTMLURL,
   289  							ToVersion:          updateDependency.ToVersion,
   290  							FromVersion:        updateDependency.FromVersion,
   291  							FromReleaseName:    updateDependency.FromReleaseName,
   292  							FromReleaseHTMLURL: updateDependency.FromReleaseHTMLURL,
   293  							ToReleaseName:      updateDependency.ToReleaseName,
   294  						},
   295  					}
   296  				}
   297  				d.Paths[j] = append([]v1.DependencyUpdateDetails{
   298  					{
   299  						Host:               updateDependency.Host,
   300  						Owner:              updateDependency.Owner,
   301  						Repo:               updateDependency.Repo,
   302  						URL:                updateDependency.URL,
   303  						Component:          updateDependency.Component,
   304  						ToReleaseHTMLURL:   updateDependency.ToReleaseHTMLURL,
   305  						ToVersion:          updateDependency.ToVersion,
   306  						FromVersion:        updateDependency.FromVersion,
   307  						FromReleaseName:    updateDependency.FromReleaseName,
   308  						FromReleaseHTMLURL: updateDependency.FromReleaseHTMLURL,
   309  						ToReleaseName:      updateDependency.ToReleaseName,
   310  					},
   311  				}, e...)
   312  			}
   313  		}
   314  		update := d
   315  		updates = append(updates, &update)
   316  	}
   317  	return updates, nil
   318  }
   319  
   320  // CreateDependencyUpdatePRDetails creates the PullRequestDetails for a pull request, taking the kind of change it is (an id)
   321  // the srcRepoUrl for the repo that caused the change, the destRepo for the repo receiving the change, the fromVersion and the toVersion
   322  func (o PullRequestOperation) CreateDependencyUpdatePRDetails(kind string, srcRepoURL string, destHost string, fromVersion string, toVersion string, component string) (string, *gits.PullRequestDetails, *v1.DependencyUpdate, []gits.GitReleaseAsset, error) {
   323  	var commitMessage, title, message strings.Builder
   324  	commitMessage.WriteString("chore(deps): bump ")
   325  	title.WriteString("chore(deps): bump ")
   326  	message.WriteString("Update ")
   327  	var update *v1.DependencyUpdate
   328  	var assets []gits.GitReleaseAsset
   329  
   330  	if srcRepoURL != "" {
   331  		provider, srcRepo, err := o.CreateGitProviderForURLWithoutKind(srcRepoURL)
   332  		if err != nil {
   333  			return "", nil, nil, nil, errors.Wrapf(err, "creating git provider for %s", srcRepoURL)
   334  		}
   335  		update = &v1.DependencyUpdate{
   336  			DependencyUpdateDetails: v1.DependencyUpdateDetails{
   337  				Owner: srcRepo.Organisation,
   338  				Repo:  srcRepo.Name,
   339  				URL:   srcRepoURL,
   340  			},
   341  		}
   342  		if srcRepo.Host != destHost {
   343  			commitMessage.WriteString(srcRepoURL)
   344  			title.WriteString(srcRepoURL)
   345  			update.Host = srcRepo.Host
   346  		} else {
   347  			titleStr := fmt.Sprintf("%s/%s", srcRepo.Organisation, srcRepo.Name)
   348  			commitMessage.WriteString(titleStr)
   349  			title.WriteString(titleStr)
   350  			update.Host = destHost
   351  		}
   352  		repoStr := fmt.Sprintf("[%s/%s](%s)", srcRepo.Organisation, srcRepo.Name, srcRepoURL)
   353  		message.WriteString(repoStr)
   354  
   355  		if component != "" {
   356  			componentStr := fmt.Sprintf(":%s", component)
   357  			commitMessage.WriteString(componentStr)
   358  			title.WriteString(componentStr)
   359  			message.WriteString(componentStr)
   360  			update.Component = component
   361  		}
   362  		commitMessage.WriteString(" ")
   363  		title.WriteString(" ")
   364  		message.WriteString(" ")
   365  
   366  		if fromVersion != "" {
   367  			fromText := fmt.Sprintf("from %s ", fromVersion)
   368  			commitMessage.WriteString(fromText)
   369  			title.WriteString(fromText)
   370  			update.FromVersion = fromVersion
   371  			release, err := releases.GetRelease(fromVersion, srcRepo.Organisation, srcRepo.Name, provider)
   372  			if err != nil {
   373  				return "", nil, nil, nil, errors.Wrapf(err, "getting release from %s/%s", srcRepo.Organisation, srcRepo.Name)
   374  			}
   375  			if release != nil {
   376  				message.WriteString(fmt.Sprintf("from [%s](%s) ", fromVersion, release.HTMLURL))
   377  				update.FromReleaseName = release.Name
   378  				update.FromReleaseHTMLURL = release.HTMLURL
   379  			} else {
   380  				message.WriteString(fmt.Sprintf("from %s ", fromVersion))
   381  			}
   382  		}
   383  		if toVersion != "" {
   384  			toText := fmt.Sprintf("to %s", toVersion)
   385  			commitMessage.WriteString(toText)
   386  			title.WriteString(toText)
   387  			update.ToVersion = toVersion
   388  			release, err := releases.GetRelease(toVersion, srcRepo.Organisation, srcRepo.Name, provider)
   389  			if err != nil {
   390  				return "", nil, nil, nil, errors.Wrapf(err, "getting release from %s/%s", srcRepo.Organisation, srcRepo.Name)
   391  			}
   392  			if release != nil {
   393  				message.WriteString(fmt.Sprintf("to [%s](%s)", toVersion, release.HTMLURL))
   394  				update.ToReleaseHTMLURL = release.HTMLURL
   395  				update.ToReleaseName = release.Name
   396  				if release.Assets != nil {
   397  					assets = *release.Assets
   398  				}
   399  			} else {
   400  				message.WriteString(fmt.Sprintf("to %s", toVersion))
   401  			}
   402  		}
   403  	} else {
   404  		commitMessage.WriteString(" versions")
   405  		title.WriteString(" versions")
   406  		message.WriteString(" versions")
   407  	}
   408  	message.WriteString(fmt.Sprintf("\n\nCommand run was `%s`", strings.Join(os.Args, " ")))
   409  	commitMessage.WriteString(fmt.Sprintf("\n\nCommand run was `%s`", strings.Join(os.Args, " ")))
   410  	if o.AuthorEmail != "" && o.AuthorName != "" {
   411  		commitMessage.WriteString(fmt.Sprintf("\n\nSigned-off-by: %s <%s>", o.AuthorName, o.AuthorEmail))
   412  	}
   413  	return commitMessage.String(), &gits.PullRequestDetails{
   414  		BranchName: fmt.Sprintf("bump-%s-version-%s", kind, string(uuid.NewUUID())),
   415  		Title:      title.String(),
   416  		Message:    message.String(),
   417  	}, update, assets, nil
   418  }
   419  
   420  // CreatePullRequestRegexFn creates the ChangeFilesFn that will apply the regex, updating the matches to version over the files
   421  func CreatePullRequestRegexFn(version string, regex string, files ...string) (ChangeFilesFn, error) {
   422  	r, err := regexp.Compile(regex)
   423  	if err != nil {
   424  		return nil, errors.Wrapf(err, "%s does not compile", regex)
   425  	}
   426  	namedCaptures := make([]bool, 0)
   427  	namedCapture := false
   428  	for i, n := range r.SubexpNames() {
   429  		if i == 0 {
   430  			continue
   431  		} else if n == "version" {
   432  			namedCaptures = append(namedCaptures, true)
   433  			namedCapture = true
   434  		} else {
   435  			namedCaptures = append(namedCaptures, false)
   436  		}
   437  	}
   438  	return func(dir string, gitInfo *gits.GitRepository) ([]string, error) {
   439  		oldVersions := make([]string, 0)
   440  		for _, glob := range files {
   441  
   442  			matches, err := filepath.Glob(filepath.Join(dir, glob))
   443  			if err != nil {
   444  				return nil, errors.Wrapf(err, "applying glob %s", glob)
   445  			}
   446  
   447  			// iterate over the glob matches
   448  			for _, path := range matches {
   449  
   450  				data, err := ioutil.ReadFile(path)
   451  				if err != nil {
   452  					return nil, errors.Wrapf(err, "reading %s", path)
   453  				}
   454  				info, err := os.Stat(path)
   455  				if err != nil {
   456  					return nil, errors.WithStack(err)
   457  				}
   458  				s := string(data)
   459  				for _, b := range namedCaptures {
   460  					if b {
   461  						namedCapture = true
   462  					}
   463  				}
   464  				answer := util.ReplaceAllStringSubmatchFunc(r, s, func(groups []util.Group) []string {
   465  					answer := make([]string, 0)
   466  					for i, group := range groups {
   467  						if namedCapture {
   468  							// If we are using named capture, then replace only the named captures that have the right name
   469  							if namedCaptures[i] {
   470  								oldVersions = append(oldVersions, group.Value)
   471  								answer = append(answer, version)
   472  							} else {
   473  								answer = append(answer, group.Value)
   474  							}
   475  						} else {
   476  							oldVersions = append(oldVersions, group.Value)
   477  							answer = append(answer, version)
   478  						}
   479  
   480  					}
   481  					return answer
   482  				})
   483  				err = ioutil.WriteFile(path, []byte(answer), info.Mode())
   484  				if err != nil {
   485  					return nil, errors.Wrapf(err, "writing %s", path)
   486  				}
   487  			}
   488  			if err != nil {
   489  				return nil, errors.WithStack(err)
   490  			}
   491  		}
   492  		return oldVersions, nil
   493  	}, nil
   494  }
   495  
   496  // CreatePullRequestBuildersFn creates the ChangeFilesFn that will update the gcr.io/jenkinsxio/builder-*.yml images
   497  func CreatePullRequestBuildersFn(version string) ChangeFilesFn {
   498  	return func(dir string, gitInfo *gits.GitRepository) ([]string, error) {
   499  		answer, err := versionstream.UpdateStableVersionFiles(filepath.Join(dir, string(versionstream.KindDocker), "gcr.io", "jenkinsxio", "builder-*.yml"), version, "builder-base.yml", "builder-machine-learning.yml", "builder-machine-learning-gpu.yml")
   500  		if err != nil {
   501  			return nil, errors.Wrap(err, "modifying the builder-*.yml image versions")
   502  		}
   503  		return answer, nil
   504  	}
   505  }
   506  
   507  // CreatePullRequestMLBuildersFn creates the ChangeFilesFn that will update the gcr.io/jenkinsxio/builder-machine-learning*.yml images
   508  func CreatePullRequestMLBuildersFn(version string) ChangeFilesFn {
   509  	return func(dir string, gitInfo *gits.GitRepository) ([]string, error) {
   510  		answer, err := versionstream.UpdateStableVersionFiles(filepath.Join(dir, string(versionstream.KindDocker), "gcr.io", "jenkinsxio", "builder-machine-learning*.yml"), version)
   511  		if err != nil {
   512  			return nil, errors.Wrap(err, "modifying the builder-machine-learning*.yml image versions")
   513  		}
   514  		return answer, nil
   515  	}
   516  }
   517  
   518  // CreatePullRequestGitReleasesFn creates the ChangeFilesFn that will update the git/ directory in the versions repo, using the git provider release api
   519  func (o *PullRequestOperation) CreatePullRequestGitReleasesFn(name string) ChangeFilesFn {
   520  	return func(dir string, gitInfo *gits.GitRepository) ([]string, error) {
   521  		u := fmt.Sprintf("https://%s.git", name)
   522  		provider, gitInfo, err := o.CreateGitProviderForURLWithoutKind(u)
   523  		if err != nil {
   524  			return nil, errors.Wrapf(err, "creating git provider for %s", u)
   525  		}
   526  		release, err := provider.GetLatestRelease(gitInfo.Organisation, gitInfo.Name)
   527  		if err != nil {
   528  			return nil, errors.Wrapf(err, "failed to find latest version for %s", u)
   529  		}
   530  
   531  		version := strings.TrimPrefix(release.Name, "v")
   532  		log.Logger().Infof("found latest version %s for git repo %s", util.ColorInfo(version), util.ColorInfo(u))
   533  		o.Version = version
   534  		if o.SrcGitURL == "" {
   535  			sv, err := versionstream.LoadStableVersion(dir, versionstream.KindGit, name)
   536  			if err != nil {
   537  				return nil, errors.Wrapf(err, "loading stable version")
   538  			}
   539  			o.SrcGitURL = sv.GitURL
   540  			if sv.Component != "" {
   541  				o.Component = sv.Component
   542  			}
   543  		}
   544  		oldVersions, err := versionstream.UpdateStableVersion(dir, string(versionstream.KindGit), name, version)
   545  		if err != nil {
   546  			return nil, errors.Wrapf(err, "updating version %s to %s", u, version)
   547  		}
   548  		return oldVersions, nil
   549  
   550  	}
   551  }
   552  
   553  // CreateChartChangeFilesFn creates the ChangeFilesFn for updating the chart with name to version. If the version is
   554  // empty it will fetch the latest version using helmer, using the vaultClient to get the repo creds or prompting using
   555  // in, out and outErr
   556  func CreateChartChangeFilesFn(name string, version string, kind string, pro *PullRequestOperation, helmer helm.Helmer,
   557  	vaultClient secreturl.Client, handles util.IOFileHandles) ChangeFilesFn {
   558  	return func(dir string, gitInfo *gits.GitRepository) ([]string, error) {
   559  		if version == "" && kind == string(versionstream.KindChart) {
   560  			parts := strings.Split(name, "/")
   561  			searchName := name
   562  			if len(parts) == 2 {
   563  				prefixes, err := versionstream.GetRepositoryPrefixes(dir)
   564  				if err != nil {
   565  					return nil, errors.Wrapf(err, "getting repository prefixes")
   566  				}
   567  				prefix := parts[0]
   568  				urls := prefixes.URLsForPrefix(prefix)
   569  				if len(urls) > 0 {
   570  					if len(urls) > 1 {
   571  						log.Logger().Warnf("helm repo %s has more than one url %+v, using first declared (%s)", prefix, urls, urls[0])
   572  					} else if len(urls) == 0 {
   573  						log.Logger().Warnf("helm repo %s has more than no urls, not adding", prefix)
   574  					}
   575  					prefix, err = helm.AddHelmRepoIfMissing(urls[0], prefix, "", "", helmer, vaultClient, handles)
   576  					if err != nil {
   577  						return nil, errors.Wrapf(err, "adding repository %s with url %s", prefix, urls[0])
   578  					}
   579  				}
   580  				searchName = fmt.Sprintf("%s/%s", prefix, parts[1])
   581  			}
   582  			c, err := helm.FindLatestChart(searchName, helmer)
   583  			if err != nil {
   584  				return nil, errors.Wrapf(err, "failed to find latest chart version for %s", name)
   585  			}
   586  			version = c.ChartVersion
   587  			log.Logger().Infof("found latest version %s for chart %s\n", util.ColorInfo(version), util.ColorInfo(name))
   588  		}
   589  		pro.Version = version
   590  		if pro.SrcGitURL == "" {
   591  			sv, err := versionstream.LoadStableVersion(dir, versionstream.VersionKind(kind), name)
   592  			if err != nil {
   593  				return nil, errors.Wrapf(err, "loading stable version")
   594  			}
   595  			pro.SrcGitURL = sv.GitURL
   596  			if sv.Component != "" {
   597  				pro.Component = sv.Component
   598  			}
   599  		}
   600  		if pro.SrcGitURL == "" {
   601  			err := helm.InspectChart(name, version, "", "", "", helmer, func(dir string) error {
   602  				fileName, err := helm.FindChartFileName(dir)
   603  				if err != nil {
   604  					return errors.Wrapf(err, "find chart file")
   605  				}
   606  				chart, err := helm.LoadChartFile(fileName)
   607  				if err != nil {
   608  					return errors.Wrapf(err, "loading chart file")
   609  				}
   610  				if len(chart.Sources) > 0 {
   611  					pro.SrcGitURL = chart.Sources[0]
   612  				} else {
   613  					return errors.Errorf("chart %s %s has no sources in Chart.yaml", name, version)
   614  				}
   615  				return nil
   616  			})
   617  			if err != nil {
   618  				return nil, errors.Wrapf(err, "failed to find source repo for %s", name)
   619  			}
   620  		}
   621  		if pro.SrcGitURL == "" {
   622  			return nil, errors.Errorf("Unable to determine git url for dependency %s", name)
   623  		}
   624  		answer, err := versionstream.UpdateStableVersion(dir, kind, name, version)
   625  		if err != nil {
   626  			return nil, errors.WithStack(err)
   627  		}
   628  		return answer, nil
   629  	}
   630  }