github.com/samcontesse/bitbucket-cascade-merge@v0.0.0-20230227091349-c5ec053235b5/git.go (about)

     1  package main
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"github.com/libgit2/git2go/v34"
     7  	"strings"
     8  	"time"
     9  )
    10  
    11  const (
    12  	DefaultMaster                = "master"
    13  	DefaultRemoteName            = "origin"
    14  	DefaultRemoteReferencePrefix = "refs/heads/"
    15  	DefaultCommitReferenceName   = "HEAD"
    16  )
    17  
    18  type Client struct {
    19  	Repository      *git.Repository
    20  	RemoteCallbacks git.RemoteCallbacks
    21  	Author          *Author
    22  }
    23  
    24  type Credentials struct {
    25  	Username string
    26  	Password string
    27  }
    28  
    29  type ClientOptions struct {
    30  	Path        string
    31  	URL         string
    32  	Author      *Author
    33  	Credentials *Credentials
    34  }
    35  
    36  func (c *Client) CascadeMerge(branchName string, options *CascadeOptions) *CascadeMergeState {
    37  
    38  	if options == nil {
    39  		options = &CascadeOptions{
    40  			DevelopmentName: "develop",
    41  			ReleasePrefix:   "release/",
    42  		}
    43  	}
    44  
    45  	err := c.RemoveLocalBranches()
    46  	if err != nil {
    47  		return &CascadeMergeState{error: err}
    48  	}
    49  
    50  	err = c.Fetch()
    51  	if err != nil {
    52  		return &CascadeMergeState{error: err}
    53  	}
    54  
    55  	cascade, err := c.BuildCascade(options, branchName)
    56  	if err != nil {
    57  		return &CascadeMergeState{error: err}
    58  	}
    59  
    60  	source := branchName
    61  
    62  	err = c.Checkout(source)
    63  	if err != nil {
    64  		return &CascadeMergeState{error: err}
    65  	}
    66  
    67  	err = c.Reset(source)
    68  	if err != nil {
    69  		return &CascadeMergeState{error: err}
    70  	}
    71  
    72  	for target := cascade.Next(); target != ""; target = cascade.Next() {
    73  		err = c.Checkout(target)
    74  		if err != nil {
    75  			return &CascadeMergeState{Source: source, Target: target, error: err}
    76  		}
    77  
    78  		err = c.Reset(target)
    79  		if err != nil {
    80  			return &CascadeMergeState{Source: source, Target: target, error: err}
    81  		}
    82  
    83  		err = c.MergeBranches(source, target)
    84  		if err != nil {
    85  			return &CascadeMergeState{Source: source, Target: target, error: err}
    86  		}
    87  
    88  		err := c.Push(target)
    89  		if err != nil {
    90  			return &CascadeMergeState{Source: source, Target: target, error: err}
    91  		}
    92  
    93  		source = target
    94  	}
    95  
    96  	return nil
    97  }
    98  
    99  func (c *Client) Commit(message string, path ...string) (*git.Oid, error) {
   100  	index, err := c.Repository.Index()
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  	defer index.Free()
   105  
   106  	var parent *git.Commit
   107  	head, _ := c.Repository.Head()
   108  	if head != nil {
   109  		parent, err = c.Repository.LookupCommit(head.Target())
   110  		if err != nil {
   111  			return nil, err
   112  		}
   113  		defer parent.Free()
   114  		defer head.Free()
   115  	}
   116  
   117  	for _, p := range path {
   118  		err = index.AddByPath(p)
   119  		if err != nil {
   120  			return nil, err
   121  		}
   122  	}
   123  
   124  	oid, err := index.WriteTree()
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  
   129  	err = index.Write()
   130  	if err != nil {
   131  		return nil, err
   132  	}
   133  
   134  	tree, err := c.Repository.LookupTree(oid)
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  	defer tree.Free()
   139  
   140  	signature := &git.Signature{
   141  		Name:  c.Author.Name,
   142  		Email: c.Author.Email,
   143  		When:  time.Now(),
   144  	}
   145  
   146  	if parent != nil {
   147  		return c.Repository.CreateCommit(DefaultCommitReferenceName, signature, signature, message, tree, parent)
   148  	} else {
   149  		return c.Repository.CreateCommit(DefaultCommitReferenceName, signature, signature, message, tree)
   150  	}
   151  }
   152  
   153  func (c *Client) Checkout(branchName string) error {
   154  	checkoutOpts := &git.CheckoutOpts{
   155  		Strategy: git.CheckoutSafe | git.CheckoutRecreateMissing | git.CheckoutAllowConflicts | git.CheckoutUseTheirs,
   156  	}
   157  
   158  	var commit *git.Commit
   159  	remoteBranch, err := c.Repository.LookupBranch(DefaultRemoteName+"/"+branchName, git.BranchRemote)
   160  	if remoteBranch != nil {
   161  		// read remote branch commit
   162  		commit, err = c.Repository.LookupCommit(remoteBranch.Target())
   163  		if err != nil {
   164  			return err
   165  		}
   166  		defer commit.Free()
   167  		defer remoteBranch.Free()
   168  	} else {
   169  		// read head commit
   170  		head, _ := c.Repository.Head()
   171  		if head != nil {
   172  			commit, err = c.Repository.LookupCommit(head.Target())
   173  			if err != nil {
   174  				return err
   175  			}
   176  			defer commit.Free()
   177  			defer head.Free()
   178  		}
   179  	}
   180  
   181  	localBranch, _ := c.Repository.LookupBranch(branchName, git.BranchLocal)
   182  	if localBranch == nil {
   183  		// creating local branch
   184  		localBranch, err = c.Repository.CreateBranch(branchName, commit, false)
   185  		if err != nil {
   186  			return err
   187  		}
   188  
   189  		// setting upstream to origin branch
   190  		if remoteBranch != nil {
   191  			err = localBranch.SetUpstream(DefaultRemoteName + "/" + branchName)
   192  			if err != nil {
   193  				return err
   194  			}
   195  		}
   196  	}
   197  	if localBranch == nil {
   198  		return errors.New("error while locating/creating local branch")
   199  	}
   200  	defer localBranch.Free()
   201  
   202  	// getting the tree for the branch
   203  	localCommit, err := c.Repository.LookupCommit(localBranch.Target())
   204  	if err != nil {
   205  		return err
   206  	}
   207  	defer localCommit.Free()
   208  
   209  	tree, err := c.Repository.LookupTree(localCommit.TreeId())
   210  	if err != nil {
   211  		return err
   212  	}
   213  	defer tree.Free()
   214  
   215  	// checkout the tree
   216  	err = c.Repository.CheckoutTree(tree, checkoutOpts)
   217  	if err != nil {
   218  		return err
   219  	}
   220  	// setting the Head to point to our branch
   221  	c.Repository.SetHead("refs/heads/" + branchName)
   222  	return nil
   223  }
   224  
   225  func (c *Client) Push(branchName string) error {
   226  	remote, err := c.Repository.Remotes.Lookup(DefaultRemoteName)
   227  	if err != nil {
   228  		return err
   229  	}
   230  	defer remote.Free()
   231  
   232  	err = remote.Push([]string{DefaultRemoteReferencePrefix + branchName}, &git.PushOptions{RemoteCallbacks: c.RemoteCallbacks})
   233  
   234  	if err != nil {
   235  		return err
   236  	}
   237  
   238  	return nil
   239  }
   240  
   241  func (c *Client) Fetch() error {
   242  	remote, err := c.Repository.Remotes.Lookup(DefaultRemoteName)
   243  	if err != nil {
   244  		return err
   245  	}
   246  	defer remote.Free()
   247  
   248  	var refs []string
   249  	err = remote.Fetch(refs, &git.FetchOptions{RemoteCallbacks: c.RemoteCallbacks, Prune: git.FetchPruneOn}, "")
   250  
   251  	if err != nil {
   252  		return err
   253  	}
   254  
   255  	return nil
   256  }
   257  
   258  // Reset current HEAD to the remote branch
   259  func (c *Client) Reset(branchName string) error {
   260  	branch, err := c.Repository.LookupBranch(fmt.Sprintf("%s/%s", DefaultRemoteName, branchName), git.BranchRemote)
   261  	if err != nil {
   262  		return err
   263  	}
   264  	defer branch.Free()
   265  
   266  	commit, err := c.Repository.LookupCommit(branch.Target())
   267  	if err != nil {
   268  		return err
   269  	}
   270  	defer commit.Free()
   271  
   272  	err = c.Repository.ResetToCommit(commit, git.ResetHard, &git.CheckoutOpts{})
   273  	if err != nil {
   274  		return err
   275  	}
   276  
   277  	return nil
   278  }
   279  
   280  func (c *Client) BuildCascade(options *CascadeOptions, startBranch string) (*Cascade, error) {
   281  	cascade := Cascade{
   282  		Branches: make([]string, 0),
   283  		Current:  0,
   284  	}
   285  
   286  	iterator, err := c.Repository.NewBranchIterator(git.BranchRemote)
   287  	if err != nil {
   288  		return nil, err
   289  	}
   290  
   291  	iterator.ForEach(func(branch *git.Branch, branchType git.BranchType) error {
   292  		shorthand := branch.Shorthand()
   293  		branchName := strings.TrimPrefix(shorthand, DefaultRemoteName+"/")
   294  		if branchName == options.DevelopmentName || strings.HasPrefix(branchName, options.ReleasePrefix) {
   295  			cascade.Append(branchName)
   296  		}
   297  		return nil
   298  	})
   299  
   300  	cascade.Slice(startBranch)
   301  
   302  	return &cascade, nil
   303  }
   304  
   305  func (c *Client) MergeBranches(sourceBranchName string, destinationBranchName string) error {
   306  	// assuming that these two branches are local already
   307  	sourceBranch, err := c.Repository.LookupBranch(sourceBranchName, git.BranchLocal)
   308  	if err != nil {
   309  		return err
   310  	}
   311  	defer sourceBranch.Free()
   312  
   313  	destinationBranch, err := c.Repository.LookupBranch(destinationBranchName, git.BranchLocal)
   314  	if err != nil {
   315  		return err
   316  	}
   317  	defer destinationBranch.Free()
   318  
   319  	// assuming we are already checkout as the destination branch
   320  	sourceAnnCommit, err := c.Repository.AnnotatedCommitFromRef(sourceBranch.Reference)
   321  	if err != nil {
   322  		return err
   323  	}
   324  	defer sourceAnnCommit.Free()
   325  
   326  	// getting repo head
   327  	head, err := c.Repository.Head()
   328  	if err != nil {
   329  		return err
   330  	}
   331  
   332  	// do merge analysis
   333  	mergeHeads := make([]*git.AnnotatedCommit, 1)
   334  	mergeHeads[0] = sourceAnnCommit
   335  	analysis, _, err := c.Repository.MergeAnalysis(mergeHeads)
   336  
   337  	// branches are already merged?
   338  	if analysis&git.MergeAnalysisNone != 0 || analysis&git.MergeAnalysisUpToDate != 0 {
   339  		return nil
   340  	}
   341  
   342  	// should merge
   343  	if analysis&git.MergeAnalysisNormal == 0 {
   344  		return errors.New("merge analysis returned as not normal merge")
   345  	}
   346  
   347  	// options for merge
   348  	mergeOpts, _ := git.DefaultMergeOptions()
   349  	mergeOpts.FileFavor = git.MergeFileFavorNormal
   350  	mergeOpts.TreeFlags = git.MergeTreeFailOnConflict
   351  
   352  	// options for checkout
   353  	checkoutOpts := &git.CheckoutOpts{
   354  		Strategy: git.CheckoutSafe | git.CheckoutRecreateMissing | git.CheckoutUseTheirs,
   355  	}
   356  
   357  	// merge action
   358  	if err = c.Repository.Merge(mergeHeads, &mergeOpts, checkoutOpts); err != nil {
   359  		return err
   360  	}
   361  
   362  	// getting repo index
   363  	index, err := c.Repository.Index()
   364  	if err != nil {
   365  		return err
   366  	}
   367  	defer index.Free()
   368  
   369  	// checking for conflicts
   370  	if index.HasConflicts() {
   371  		return errors.New("merge resulted in conflicts, please solve the conflicts before merging")
   372  	}
   373  
   374  	// getting last commit from source
   375  	commit, err := c.Repository.LookupCommit(sourceBranch.Target())
   376  	if err != nil {
   377  		return err
   378  	}
   379  	defer commit.Free()
   380  
   381  	// getting signature
   382  	signature := commit.Author()
   383  
   384  	// writing tree to index
   385  	treeId, err := index.WriteTree()
   386  	if err != nil {
   387  		return err
   388  	}
   389  
   390  	// getting the created tree
   391  	tree, err := c.Repository.LookupTree(treeId)
   392  	if err != nil {
   393  		return err
   394  	}
   395  	defer tree.Free()
   396  
   397  	// getting head's commit
   398  	currentDestinationCommit, err := c.Repository.LookupCommit(head.Target())
   399  	if err != nil {
   400  		return err
   401  	}
   402  
   403  	// commit
   404  	_, err = c.Repository.CreateCommit(DefaultCommitReferenceName, signature, signature, "Automatic merge "+sourceBranchName+" into "+destinationBranchName,
   405  		tree, currentDestinationCommit, commit)
   406  	if err != nil {
   407  		return err
   408  	}
   409  
   410  	err = c.Repository.StateCleanup()
   411  	if err != nil {
   412  		return err
   413  	}
   414  
   415  	return nil
   416  }
   417  
   418  func (c *Client) RemoveLocalBranches() error {
   419  	iterator, err := c.Repository.NewBranchIterator(git.BranchLocal)
   420  	if err != nil {
   421  		return err
   422  	}
   423  
   424  	iterator.ForEach(func(branch *git.Branch, branchType git.BranchType) error {
   425  		if DefaultMaster != branch.Shorthand() {
   426  			err = branch.Delete()
   427  			if err != nil {
   428  				return err
   429  			}
   430  		}
   431  		return nil
   432  	})
   433  
   434  	return nil
   435  }
   436  
   437  func (c *Client) Close() {
   438  	c.Repository.Free()
   439  }
   440  
   441  func NewClient(options *ClientOptions) (*Client, error) {
   442  
   443  	if options == nil || !options.Validate() {
   444  		return nil, errors.New("invalid client options")
   445  	}
   446  
   447  	var r *git.Repository
   448  	var cb git.RemoteCallbacks
   449  	var err error
   450  
   451  	// try to open an existing repository
   452  	r, err = git.OpenRepository(options.Path)
   453  
   454  	// create fetch options (credentials callback)
   455  	cb = options.CreateRemoteCallbacks()
   456  
   457  	if err != nil {
   458  		// try clone the given url with the given credentials
   459  		r, err = git.Clone(options.URL, options.Path, &git.CloneOptions{FetchOptions: git.FetchOptions{RemoteCallbacks: cb}})
   460  		if err != nil {
   461  			return nil, fmt.Errorf("cannot initialize repository at %s : %s", options.URL, err)
   462  		}
   463  	}
   464  
   465  	if r == nil {
   466  		return nil, errors.New("error while initializing repository")
   467  	}
   468  
   469  	return &Client{
   470  		Repository:      r,
   471  		RemoteCallbacks: cb,
   472  		Author:          options.Author,
   473  	}, nil
   474  
   475  }
   476  
   477  func (o *ClientOptions) Validate() bool {
   478  	if len(o.URL) > 0 && len(o.Path) > 0 {
   479  		return true
   480  	}
   481  	return false
   482  }
   483  
   484  func (o *ClientOptions) CreateRemoteCallbacks() git.RemoteCallbacks {
   485  	if c := o.Credentials; c != nil {
   486  		return git.RemoteCallbacks{
   487  			CredentialsCallback: makeCredentialsCallback(c.Username, c.Password),
   488  		}
   489  	}
   490  	return git.RemoteCallbacks{}
   491  }
   492  
   493  func makeCredentialsCallback(username, password string) git.CredentialsCallback {
   494  	return func(url, u string, ct git.CredType) (*git.Cred, error) {
   495  		cred, err := git.NewCredUserpassPlaintext(username, password)
   496  		return cred, err
   497  	}
   498  }