Skip to content

Commit e6c3e58

Browse files
authored
Merge pull request #1044 from pjbgf/ff-merge
git: Implement Merge function with initial `FastForwardMerge` support
2 parents f4f1a87 + 3ee5bc9 commit e6c3e58

5 files changed

Lines changed: 188 additions & 25 deletions

File tree

‎COMPATIBILITY.md‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ compatibility status with go-git.
2727

2828
## Branching and merging
2929

30-
| Feature | Sub-feature | Status | Notes | Examples |
31-
| ----------- | ----------- | ------ | --------------------------------------- | ----------------------------------------------------------------------------------------------- |
32-
| `branch` | | ✅ | | - [branch](_examples/branch/main.go) |
33-
| `checkout` | | ✅ | Basic usages of checkout are supported. | - [checkout](_examples/checkout/main.go) |
34-
| `merge` | | ❌ | | |
35-
| `mergetool` | | ❌ | | |
36-
| `stash` | | ❌ | | |
37-
| `tag` | | ✅ | | - [tag](_examples/tag/main.go) <br/> - [tag create and push](_examples/tag-create-push/main.go) |
30+
| Feature | Sub-feature | Status | Notes | Examples |
31+
| ----------- | ----------- | ------------ | --------------------------------------- | ----------------------------------------------------------------------------------------------- |
32+
| `branch` | | ✅ | | - [branch](_examples/branch/main.go) |
33+
| `checkout` | | ✅ | Basic usages of checkout are supported. | - [checkout](_examples/checkout/main.go) |
34+
| `merge` | | ⚠️ (partial) | Fast-forward only | |
35+
| `mergetool` | | ❌ | | |
36+
| `stash` | | ❌ | | |
37+
| `tag` | | ✅ | | - [tag](_examples/tag/main.go) <br/> - [tag create and push](_examples/tag-create-push/main.go) |
3838

3939
## Sharing and updating projects
4040

‎options.go‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,25 @@ type CloneOptions struct {
8989
Shared bool
9090
}
9191

92+
// MergeOptions describes how a merge should be performed.
93+
type MergeOptions struct {
94+
// Strategy defines the merge strategy to be used.
95+
Strategy MergeStrategy
96+
}
97+
98+
// MergeStrategy represents the different types of merge strategies.
99+
type MergeStrategy int8
100+
101+
const (
102+
// FastForwardMerge represents a Git merge strategy where the current
103+
// branch can be simply updated to point to the HEAD of the branch being
104+
// merged. This is only possible if the history of the branch being merged
105+
// is a linear descendant of the current branch, with no conflicting commits.
106+
//
107+
// This is the default option.
108+
FastForwardMerge MergeStrategy = iota
109+
)
110+
92111
// Validate validates the fields and sets the default values.
93112
func (o *CloneOptions) Validate() error {
94113
if o.URL == "" {

‎remote.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1128,7 +1128,7 @@ func isFastForward(s storer.EncodedObjectStorer, old, new plumbing.Hash, earlies
11281128
}
11291129

11301130
found := false
1131-
// stop iterating at the earlist shallow commit, ignoring its parents
1131+
// stop iterating at the earliest shallow commit, ignoring its parents
11321132
// note: when pull depth is smaller than the number of new changes on the remote, this fails due to missing parents.
11331133
// as far as i can tell, without the commits in-between the shallow pull and the earliest shallow, there's no
11341134
// real way of telling whether it will be a fast-forward merge.

‎repository.go‎

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,19 +51,21 @@ var (
5151
// ErrFetching is returned when the packfile could not be downloaded
5252
ErrFetching = errors.New("unable to fetch packfile")
5353

54-
ErrInvalidReference = errors.New("invalid reference, should be a tag or a branch")
55-
ErrRepositoryNotExists = errors.New("repository does not exist")
56-
ErrRepositoryIncomplete = errors.New("repository's commondir path does not exist")
57-
ErrRepositoryAlreadyExists = errors.New("repository already exists")
58-
ErrRemoteNotFound = errors.New("remote not found")
59-
ErrRemoteExists = errors.New("remote already exists")
60-
ErrAnonymousRemoteName = errors.New("anonymous remote name must be 'anonymous'")
61-
ErrWorktreeNotProvided = errors.New("worktree should be provided")
62-
ErrIsBareRepository = errors.New("worktree not available in a bare repository")
63-
ErrUnableToResolveCommit = errors.New("unable to resolve commit")
64-
ErrPackedObjectsNotSupported = errors.New("packed objects not supported")
65-
ErrSHA256NotSupported = errors.New("go-git was not compiled with SHA256 support")
66-
ErrAlternatePathNotSupported = errors.New("alternate path must use the file scheme")
54+
ErrInvalidReference = errors.New("invalid reference, should be a tag or a branch")
55+
ErrRepositoryNotExists = errors.New("repository does not exist")
56+
ErrRepositoryIncomplete = errors.New("repository's commondir path does not exist")
57+
ErrRepositoryAlreadyExists = errors.New("repository already exists")
58+
ErrRemoteNotFound = errors.New("remote not found")
59+
ErrRemoteExists = errors.New("remote already exists")
60+
ErrAnonymousRemoteName = errors.New("anonymous remote name must be 'anonymous'")
61+
ErrWorktreeNotProvided = errors.New("worktree should be provided")
62+
ErrIsBareRepository = errors.New("worktree not available in a bare repository")
63+
ErrUnableToResolveCommit = errors.New("unable to resolve commit")
64+
ErrPackedObjectsNotSupported = errors.New("packed objects not supported")
65+
ErrSHA256NotSupported = errors.New("go-git was not compiled with SHA256 support")
66+
ErrAlternatePathNotSupported = errors.New("alternate path must use the file scheme")
67+
ErrUnsupportedMergeStrategy = errors.New("unsupported merge strategy")
68+
ErrFastForwardMergeNotPossible = errors.New("not possible to fast-forward merge changes")
6769
)
6870

6971
// Repository represents a git repository
@@ -1769,6 +1771,41 @@ func (r *Repository) RepackObjects(cfg *RepackConfig) (err error) {
17691771
return nil
17701772
}
17711773

1774+
// Merge merges the reference branch into the current branch.
1775+
//
1776+
// If the merge is not possible (or supported) returns an error without changing
1777+
// the HEAD for the current branch. Possible errors include:
1778+
// - The merge strategy is not supported.
1779+
// - The specific strategy cannot be used (e.g. using FastForwardMerge when one is not possible).
1780+
func (r *Repository) Merge(ref plumbing.Reference, opts MergeOptions) error {
1781+
if opts.Strategy != FastForwardMerge {
1782+
return ErrUnsupportedMergeStrategy
1783+
}
1784+
1785+
// Ignore error as not having a shallow list is optional here.
1786+
shallowList, _ := r.Storer.Shallow()
1787+
var earliestShallow *plumbing.Hash
1788+
if len(shallowList) > 0 {
1789+
earliestShallow = &shallowList[0]
1790+
}
1791+
1792+
head, err := r.Head()
1793+
if err != nil {
1794+
return err
1795+
}
1796+
1797+
ff, err := isFastForward(r.Storer, head.Hash(), ref.Hash(), earliestShallow)
1798+
if err != nil {
1799+
return err
1800+
}
1801+
1802+
if !ff {
1803+
return ErrFastForwardMergeNotPossible
1804+
}
1805+
1806+
return r.Storer.SetReference(plumbing.NewHashReference(head.Name(), ref.Hash()))
1807+
}
1808+
17721809
// createNewObjectPack is a helper for RepackObjects taking care
17731810
// of creating a new pack. It is used so the the PackfileWriter
17741811
// deferred close has the right scope.

‎repository_test.go‎

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func (s *RepositorySuite) TestInitWithInvalidDefaultBranch(c *C) {
8282
c.Assert(err, NotNil)
8383
}
8484

85-
func createCommit(c *C, r *Repository) {
85+
func createCommit(c *C, r *Repository) plumbing.Hash {
8686
// Create a commit so there is a HEAD to check
8787
wt, err := r.Worktree()
8888
c.Assert(err, IsNil)
@@ -101,13 +101,14 @@ func createCommit(c *C, r *Repository) {
101101
Email: "go-git@fake.local",
102102
When: time.Now(),
103103
}
104-
_, err = wt.Commit("test commit message", &CommitOptions{
104+
105+
h, err := wt.Commit("test commit message", &CommitOptions{
105106
All: true,
106107
Author: &author,
107108
Committer: &author,
108109
})
109110
c.Assert(err, IsNil)
110-
111+
return h
111112
}
112113

113114
func (s *RepositorySuite) TestInitNonStandardDotGit(c *C) {
@@ -439,6 +440,112 @@ func (s *RepositorySuite) TestCreateBranchAndBranch(c *C) {
439440
c.Assert(branch.Merge, Equals, testBranch.Merge)
440441
}
441442

443+
func (s *RepositorySuite) TestMergeFF(c *C) {
444+
r, err := Init(memory.NewStorage(), memfs.New())
445+
c.Assert(err, IsNil)
446+
c.Assert(r, NotNil)
447+
448+
createCommit(c, r)
449+
createCommit(c, r)
450+
createCommit(c, r)
451+
lastCommit := createCommit(c, r)
452+
453+
wt, err := r.Worktree()
454+
c.Assert(err, IsNil)
455+
456+
targetBranch := plumbing.NewBranchReferenceName("foo")
457+
err = wt.Checkout(&CheckoutOptions{
458+
Hash: lastCommit,
459+
Create: true,
460+
Branch: targetBranch,
461+
})
462+
c.Assert(err, IsNil)
463+
464+
createCommit(c, r)
465+
fooHash := createCommit(c, r)
466+
467+
// Checkout the master branch so that we can try to merge foo into it.
468+
err = wt.Checkout(&CheckoutOptions{
469+
Branch: plumbing.Master,
470+
})
471+
c.Assert(err, IsNil)
472+
473+
head, err := r.Head()
474+
c.Assert(err, IsNil)
475+
c.Assert(head.Hash(), Equals, lastCommit)
476+
477+
targetRef := plumbing.NewHashReference(targetBranch, fooHash)
478+
c.Assert(targetRef, NotNil)
479+
480+
err = r.Merge(*targetRef, MergeOptions{
481+
Strategy: FastForwardMerge,
482+
})
483+
c.Assert(err, IsNil)
484+
485+
head, err = r.Head()
486+
c.Assert(err, IsNil)
487+
c.Assert(head.Hash(), Equals, fooHash)
488+
}
489+
490+
func (s *RepositorySuite) TestMergeFF_Invalid(c *C) {
491+
r, err := Init(memory.NewStorage(), memfs.New())
492+
c.Assert(err, IsNil)
493+
c.Assert(r, NotNil)
494+
495+
// Keep track of the first commit, which will be the
496+
// reference to create the target branch so that we
497+
// can simulate a non-ff merge.
498+
firstCommit := createCommit(c, r)
499+
createCommit(c, r)
500+
createCommit(c, r)
501+
lastCommit := createCommit(c, r)
502+
503+
wt, err := r.Worktree()
504+
c.Assert(err, IsNil)
505+
506+
targetBranch := plumbing.NewBranchReferenceName("foo")
507+
err = wt.Checkout(&CheckoutOptions{
508+
Hash: firstCommit,
509+
Create: true,
510+
Branch: targetBranch,
511+
})
512+
513+
c.Assert(err, IsNil)
514+
515+
createCommit(c, r)
516+
h := createCommit(c, r)
517+
518+
// Checkout the master branch so that we can try to merge foo into it.
519+
err = wt.Checkout(&CheckoutOptions{
520+
Branch: plumbing.Master,
521+
})
522+
c.Assert(err, IsNil)
523+
524+
head, err := r.Head()
525+
c.Assert(err, IsNil)
526+
c.Assert(head.Hash(), Equals, lastCommit)
527+
528+
targetRef := plumbing.NewHashReference(targetBranch, h)
529+
c.Assert(targetRef, NotNil)
530+
531+
err = r.Merge(*targetRef, MergeOptions{
532+
Strategy: MergeStrategy(10),
533+
})
534+
c.Assert(err, Equals, ErrUnsupportedMergeStrategy)
535+
536+
// Failed merge operations must not change HEAD.
537+
head, err = r.Head()
538+
c.Assert(err, IsNil)
539+
c.Assert(head.Hash(), Equals, lastCommit)
540+
541+
err = r.Merge(*targetRef, MergeOptions{})
542+
c.Assert(err, Equals, ErrFastForwardMergeNotPossible)
543+
544+
head, err = r.Head()
545+
c.Assert(err, IsNil)
546+
c.Assert(head.Hash(), Equals, lastCommit)
547+
}
548+
442549
func (s *RepositorySuite) TestCreateBranchUnmarshal(c *C) {
443550
r, _ := Init(memory.NewStorage(), nil)
444551

0 commit comments

Comments
 (0)