github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/cmd/opts/pullrequest.go (about)

     1  package opts
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/jenkins-x/jx-logging/pkg/log"
     8  	"github.com/olli-ai/jx/v2/pkg/gits"
     9  	"github.com/olli-ai/jx/v2/pkg/util"
    10  	"github.com/pkg/errors"
    11  	"k8s.io/apimachinery/pkg/util/uuid"
    12  )
    13  
    14  // PullRequestDetails details to pass in to create a PullRequest if the repository is modified
    15  type PullRequestDetails struct {
    16  	Dir               string
    17  	RepositoryGitURL  string
    18  	RepositoryBranch  string
    19  	RepositoryMessage string
    20  	BranchNameText    string
    21  	Title             string
    22  	Message           string
    23  }
    24  
    25  // CreatePullRequest creates a Pull Request on the given repository
    26  func (options *CommonOptions) CreatePullRequest(o *PullRequestDetails, modifyFn func() error) error {
    27  	if o.RepositoryBranch == "" {
    28  		o.RepositoryBranch = "master"
    29  	}
    30  	dir := o.Dir
    31  	originalGitURL := o.RepositoryGitURL
    32  	message := o.RepositoryMessage
    33  	gitter := options.Git()
    34  	gitInfo, err := gits.ParseGitURL(originalGitURL)
    35  	if err != nil {
    36  		return err
    37  	}
    38  	provider, err := options.GitProviderForURL(originalGitURL, message)
    39  	if err != nil {
    40  		return err
    41  	}
    42  
    43  	if o.Message == "" {
    44  		log.Logger().Warn("missing option: 'message' when creating a PR")
    45  	}
    46  	username := provider.CurrentUsername()
    47  	if username == "" {
    48  		return fmt.Errorf("no git user name found")
    49  	}
    50  
    51  	originalOrg := gitInfo.Organisation
    52  	originalRepo := gitInfo.Name
    53  
    54  	repo, err := provider.GetRepository(username, originalRepo)
    55  	if err != nil {
    56  		if originalOrg == username {
    57  			return err
    58  		}
    59  
    60  		// lets try create a fork - using a blank organisation to force a user specific fork
    61  		repo, err = provider.ForkRepository(originalOrg, originalRepo, "")
    62  		if err != nil {
    63  			return errors.Wrapf(err, "failed to fork GitHub repo %s/%s to user %s", originalOrg, originalRepo, username)
    64  		}
    65  		log.Logger().Infof("Forked %s to %s\n", message, util.ColorInfo(repo.HTMLURL))
    66  
    67  		repo, err = provider.GetRepository(username, originalRepo)
    68  		if err != nil {
    69  			return errors.Wrapf(err, "failed to load GitHub repo %s/%s", username, originalRepo)
    70  		}
    71  	}
    72  	fork := repo.Fork
    73  
    74  	err = gitter.Clone(repo.CloneURL, dir)
    75  	if err != nil {
    76  		return errors.Wrapf(err, "cloning the %s %q", message, repo.CloneURL)
    77  	}
    78  	log.Logger().Infof("cloned fork of %s %s to %s", message, util.ColorInfo(repo.HTMLURL), util.ColorInfo(dir))
    79  
    80  	err = gitter.SetRemoteURL(dir, "upstream", originalGitURL)
    81  	if err != nil {
    82  		return errors.Wrapf(err, "setting remote upstream %q in forked %s", originalGitURL, message)
    83  	}
    84  	err = gitter.PullUpstream(dir)
    85  	if err != nil {
    86  		return errors.Wrapf(err, "pulling upstream of forked %s", message)
    87  	}
    88  
    89  	if fork {
    90  		base := "master"
    91  		err = gitter.ResetToUpstream(dir, base)
    92  		if err != nil {
    93  			return errors.Wrapf(err, "resetting forked branch %s to upstream version", base)
    94  		}
    95  	}
    96  
    97  	branchName := gitter.ConvertToValidBranchName(o.BranchNameText)
    98  	branchNames, err := gitter.RemoteBranchNames(dir, "remotes/origin/")
    99  	if err != nil {
   100  		return errors.Wrapf(err, "failed to load remote branch names")
   101  	}
   102  	if util.StringArrayIndex(branchNames, branchName) >= 0 {
   103  		// lets append a UUID as the branch name already exists
   104  		branchName += "-" + string(uuid.NewUUID())
   105  	}
   106  
   107  	err = gitter.CreateBranch(dir, branchName)
   108  	if err != nil {
   109  		return err
   110  	}
   111  	err = gitter.Checkout(dir, branchName)
   112  	if err != nil {
   113  		return err
   114  	}
   115  
   116  	err = modifyFn()
   117  	if err != nil {
   118  		return err
   119  	}
   120  
   121  	err = gitter.Add(dir, "*", "*/*")
   122  	if err != nil {
   123  		return err
   124  	}
   125  	changes, err := gitter.HasChanges(dir)
   126  	if err != nil {
   127  		return err
   128  	}
   129  	if !changes {
   130  		log.Logger().Infof("No source changes so not generating a Pull Request")
   131  		return nil
   132  	}
   133  
   134  	err = gitter.CommitDir(dir, o.Title)
   135  	if err != nil {
   136  		return err
   137  	}
   138  
   139  	// lets find a previous PR so we can force push to its branch
   140  	prs, err := provider.ListOpenPullRequests(gitInfo.Organisation, gitInfo.Name)
   141  	if err != nil {
   142  		return errors.Wrapf(err, "failed to list open pull requests on %s", gitInfo.HTMLURL)
   143  	}
   144  	for _, pr := range prs {
   145  		author := pr.Author
   146  		if pr.Title == o.Title && author != nil && author.Login == username {
   147  			log.Logger().Infof("found existing PullRequest: %s", util.ColorInfo(pr.URL))
   148  
   149  			head := pr.HeadRef
   150  			if head == nil {
   151  				log.Logger().Warnf("No head value!")
   152  			} else {
   153  				headText := *head
   154  				remoteBranch := headText
   155  				paths := strings.SplitN(headText, ":", 2)
   156  				if len(paths) > 1 {
   157  					remoteBranch = paths[1]
   158  				}
   159  				log.Logger().Infof("force pushing to remote branch %s", util.ColorInfo(remoteBranch))
   160  				err := gitter.ForcePushBranch(dir, branchName, remoteBranch)
   161  				if err != nil {
   162  					return errors.Wrapf(err, "failed to force push to remote branch %s", remoteBranch)
   163  				}
   164  
   165  				pr.Body = o.Message
   166  
   167  				log.Logger().Infof("force pushed new pull request change to: %s", util.ColorInfo(pr.URL))
   168  
   169  				err = provider.AddPRComment(pr, o.Message)
   170  				if err != nil {
   171  					return errors.Wrapf(err, "failed to add message to PR %s", pr.URL)
   172  				}
   173  				return nil
   174  			}
   175  		}
   176  	}
   177  
   178  	err = gitter.Push(dir, "origin", false, "HEAD")
   179  	if err != nil {
   180  		return errors.Wrapf(err, "pushing to %s in dir %q", message, dir)
   181  	}
   182  
   183  	base := o.RepositoryBranch
   184  
   185  	head := branchName
   186  	if fork {
   187  		head = username + ":" + branchName
   188  	}
   189  	gha := &gits.GitPullRequestArguments{
   190  		GitRepository: gitInfo,
   191  		Title:         o.Title,
   192  		Body:          o.Message,
   193  		Base:          base,
   194  		Head:          head,
   195  	}
   196  
   197  	pr, err := provider.CreatePullRequest(gha)
   198  	if err != nil {
   199  		return errors.Wrapf(err, "failed to create pull quest from base %s with header %s", base, head)
   200  	}
   201  	log.Logger().Infof("Created Pull Request: %s\n", util.ColorInfo(pr.URL))
   202  	return nil
   203  }