aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--COMPATIBILITY.md16
-rw-r--r--options.go20
-rw-r--r--remote.go2
-rw-r--r--repository.go54
-rw-r--r--repository_test.go126
5 files changed, 151 insertions, 67 deletions
diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md
index c1f280d..ff0c22c 100644
--- a/COMPATIBILITY.md
+++ b/COMPATIBILITY.md
@@ -27,14 +27,14 @@ compatibility status with go-git.
## Branching and merging
-| Feature | Sub-feature | Status | Notes | Examples |
-| ----------- | ----------- | ------ | --------------------------------------- | ----------------------------------------------------------------------------------------------- |
-| `branch` | | ✅ | | - [branch](_examples/branch/main.go) |
-| `checkout` | | ✅ | Basic usages of checkout are supported. | - [checkout](_examples/checkout/main.go) |
-| `merge` | | ❌ | | |
-| `mergetool` | | ❌ | | |
-| `stash` | | ❌ | | |
-| `tag` | | ✅ | | - [tag](_examples/tag/main.go) <br/> - [tag create and push](_examples/tag-create-push/main.go) |
+| Feature | Sub-feature | Status | Notes | Examples |
+| ----------- | ----------- | ------------ | --------------------------------------- | ----------------------------------------------------------------------------------------------- |
+| `branch` | | ✅ | | - [branch](_examples/branch/main.go) |
+| `checkout` | | ✅ | Basic usages of checkout are supported. | - [checkout](_examples/checkout/main.go) |
+| `merge` | | ⚠️ (partial) | Fast-forward only | |
+| `mergetool` | | ❌ | | |
+| `stash` | | ❌ | | |
+| `tag` | | ✅ | | - [tag](_examples/tag/main.go) <br/> - [tag create and push](_examples/tag-create-push/main.go) |
## Sharing and updating projects
diff --git a/options.go b/options.go
index 6a7963f..02248ad 100644
--- a/options.go
+++ b/options.go
@@ -89,13 +89,25 @@ type CloneOptions struct {
Shared bool
}
-// MergeOptions describes how a merge should be erformed
+// MergeOptions describes how a merge should be performed.
type MergeOptions struct {
- // Requires a merge to be fast forward only. If this is true, then a merge will
- // throw an error if ff is not possible.
- FFOnly bool
+ // Strategy defines the merge strategy to be used.
+ Strategy MergeStrategy
}
+// MergeStrategy represents the different types of merge strategies.
+type MergeStrategy int8
+
+const (
+ // FastForwardMerge represents a Git merge strategy where the current
+ // branch can be simply updated to point to the HEAD of the branch being
+ // merged. This is only possible if the history of the branch being merged
+ // is a linear descendant of the current branch, with no conflicting commits.
+ //
+ // This is the default option.
+ FastForwardMerge MergeStrategy = iota
+)
+
// Validate validates the fields and sets the default values.
func (o *CloneOptions) Validate() error {
if o.URL == "" {
diff --git a/remote.go b/remote.go
index f07292b..7cc0db9 100644
--- a/remote.go
+++ b/remote.go
@@ -1128,7 +1128,7 @@ func isFastForward(s storer.EncodedObjectStorer, old, new plumbing.Hash, earlies
}
found := false
- // stop iterating at the earlist shallow commit, ignoring its parents
+ // stop iterating at the earliest shallow commit, ignoring its parents
// note: when pull depth is smaller than the number of new changes on the remote, this fails due to missing parents.
// as far as i can tell, without the commits in-between the shallow pull and the earliest shallow, there's no
// real way of telling whether it will be a fast-forward merge.
diff --git a/repository.go b/repository.go
index 6ab40c0..d5e4f2f 100644
--- a/repository.go
+++ b/repository.go
@@ -51,19 +51,21 @@ var (
// ErrFetching is returned when the packfile could not be downloaded
ErrFetching = errors.New("unable to fetch packfile")
- ErrInvalidReference = errors.New("invalid reference, should be a tag or a branch")
- ErrRepositoryNotExists = errors.New("repository does not exist")
- ErrRepositoryIncomplete = errors.New("repository's commondir path does not exist")
- ErrRepositoryAlreadyExists = errors.New("repository already exists")
- ErrRemoteNotFound = errors.New("remote not found")
- ErrRemoteExists = errors.New("remote already exists")
- ErrAnonymousRemoteName = errors.New("anonymous remote name must be 'anonymous'")
- ErrWorktreeNotProvided = errors.New("worktree should be provided")
- ErrIsBareRepository = errors.New("worktree not available in a bare repository")
- ErrUnableToResolveCommit = errors.New("unable to resolve commit")
- ErrPackedObjectsNotSupported = errors.New("packed objects not supported")
- ErrSHA256NotSupported = errors.New("go-git was not compiled with SHA256 support")
- ErrAlternatePathNotSupported = errors.New("alternate path must use the file scheme")
+ ErrInvalidReference = errors.New("invalid reference, should be a tag or a branch")
+ ErrRepositoryNotExists = errors.New("repository does not exist")
+ ErrRepositoryIncomplete = errors.New("repository's commondir path does not exist")
+ ErrRepositoryAlreadyExists = errors.New("repository already exists")
+ ErrRemoteNotFound = errors.New("remote not found")
+ ErrRemoteExists = errors.New("remote already exists")
+ ErrAnonymousRemoteName = errors.New("anonymous remote name must be 'anonymous'")
+ ErrWorktreeNotProvided = errors.New("worktree should be provided")
+ ErrIsBareRepository = errors.New("worktree not available in a bare repository")
+ ErrUnableToResolveCommit = errors.New("unable to resolve commit")
+ ErrPackedObjectsNotSupported = errors.New("packed objects not supported")
+ ErrSHA256NotSupported = errors.New("go-git was not compiled with SHA256 support")
+ ErrAlternatePathNotSupported = errors.New("alternate path must use the file scheme")
+ ErrUnsupportedMergeStrategy = errors.New("unsupported merge strategy")
+ ErrFastForwardMergeNotPossible = errors.New("not possible to fast-forward merge changes")
)
// Repository represents a git repository
@@ -1769,10 +1771,22 @@ func (r *Repository) RepackObjects(cfg *RepackConfig) (err error) {
return nil
}
-// Merge attempts to merge ref onto HEAD. Currently only supports fast-forward merges
+// Merge merges the reference branch into the current branch.
+//
+// If the merge is not possible (or supported) returns an error without changing
+// the HEAD for the current branch. Possible errors include:
+// - The merge strategy is not supported.
+// - The specific strategy cannot be used (e.g. using FastForwardMerge when one is not possible).
func (r *Repository) Merge(ref plumbing.Reference, opts MergeOptions) error {
- if !opts.FFOnly {
- return errors.New("non fast-forward merges are not supported yet")
+ if opts.Strategy != FastForwardMerge {
+ return ErrUnsupportedMergeStrategy
+ }
+
+ // Ignore error as not having a shallow list is optional here.
+ shallowList, _ := r.Storer.Shallow()
+ var earliestShallow *plumbing.Hash
+ if len(shallowList) > 0 {
+ earliestShallow = &shallowList[0]
}
head, err := r.Head()
@@ -1780,9 +1794,13 @@ func (r *Repository) Merge(ref plumbing.Reference, opts MergeOptions) error {
return err
}
- ff, err := IsFastForward(r.Storer, head.Hash(), ref.Hash())
+ ff, err := isFastForward(r.Storer, head.Hash(), ref.Hash(), earliestShallow)
+ if err != nil {
+ return err
+ }
+
if !ff {
- return errors.New("fast forward is not possible")
+ return ErrFastForwardMergeNotPossible
}
return r.Storer.SetReference(plumbing.NewHashReference(head.Name(), ref.Hash()))
diff --git a/repository_test.go b/repository_test.go
index bcfecc7..b211f8c 100644
--- a/repository_test.go
+++ b/repository_test.go
@@ -82,7 +82,7 @@ func (s *RepositorySuite) TestInitWithInvalidDefaultBranch(c *C) {
c.Assert(err, NotNil)
}
-func createCommit(c *C, r *Repository) {
+func createCommit(c *C, r *Repository) plumbing.Hash {
// Create a commit so there is a HEAD to check
wt, err := r.Worktree()
c.Assert(err, IsNil)
@@ -101,13 +101,14 @@ func createCommit(c *C, r *Repository) {
Email: "go-git@fake.local",
When: time.Now(),
}
- _, err = wt.Commit("test commit message", &CommitOptions{
+
+ h, err := wt.Commit("test commit message", &CommitOptions{
All: true,
Author: &author,
Committer: &author,
})
c.Assert(err, IsNil)
-
+ return h
}
func (s *RepositorySuite) TestInitNonStandardDotGit(c *C) {
@@ -440,56 +441,109 @@ func (s *RepositorySuite) TestCreateBranchAndBranch(c *C) {
}
func (s *RepositorySuite) TestMergeFF(c *C) {
- r, _ := Init(memory.NewStorage(), memfs.New())
- err := r.clone(context.Background(), &CloneOptions{
- URL: s.GetBasicLocalRepositoryURL(),
+ r, err := Init(memory.NewStorage(), memfs.New())
+ c.Assert(err, IsNil)
+ c.Assert(r, NotNil)
+
+ createCommit(c, r)
+ createCommit(c, r)
+ createCommit(c, r)
+ lastCommit := createCommit(c, r)
+
+ wt, err := r.Worktree()
+ c.Assert(err, IsNil)
+
+ targetBranch := plumbing.NewBranchReferenceName("foo")
+ err = wt.Checkout(&CheckoutOptions{
+ Hash: lastCommit,
+ Create: true,
+ Branch: targetBranch,
})
c.Assert(err, IsNil)
+
+ createCommit(c, r)
+ fooHash := createCommit(c, r)
+
+ // Checkout the master branch so that we can try to merge foo into it.
+ err = wt.Checkout(&CheckoutOptions{
+ Branch: plumbing.Master,
+ })
+ c.Assert(err, IsNil)
+
head, err := r.Head()
c.Assert(err, IsNil)
+ c.Assert(head.Hash(), Equals, lastCommit)
+
+ targetRef := plumbing.NewHashReference(targetBranch, fooHash)
+ c.Assert(targetRef, NotNil)
- mergeBranchRefname := plumbing.NewBranchReferenceName("foo")
- err = r.Storer.SetReference(plumbing.NewHashReference(mergeBranchRefname, head.Hash()))
+ err = r.Merge(*targetRef, MergeOptions{
+ Strategy: FastForwardMerge,
+ })
c.Assert(err, IsNil)
- commit, err := r.CommitObject(head.Hash())
+ head, err = r.Head()
c.Assert(err, IsNil)
- treeHash := commit.TreeHash
+ c.Assert(head.Hash(), Equals, fooHash)
+}
- hash := commit.Hash
+func (s *RepositorySuite) TestMergeFF_Invalid(c *C) {
+ r, err := Init(memory.NewStorage(), memfs.New())
+ c.Assert(err, IsNil)
+ c.Assert(r, NotNil)
- for i := 0; i < 10; i++ {
- commit = &object.Commit{
- Author: object.Signature{
- Name: "A U Thor",
- Email: "author@example.com",
- },
- Committer: object.Signature{
- Name: "A U Thor",
- Email: "author@example.com",
- },
- Message: fmt.Sprintf("commit #%d", i),
- TreeHash: treeHash,
- ParentHashes: []plumbing.Hash{
- hash,
- },
- }
+ // Keep track of the first commit, which will be the
+ // reference to create the target branch so that we
+ // can simulate a non-ff merge.
+ firstCommit := createCommit(c, r)
+ createCommit(c, r)
+ createCommit(c, r)
+ lastCommit := createCommit(c, r)
- o := r.Storer.NewEncodedObject()
- c.Assert(commit.Encode(o), IsNil)
- hash, err = r.Storer.SetEncodedObject(o)
- }
+ wt, err := r.Worktree()
+ c.Assert(err, IsNil)
+
+ targetBranch := plumbing.NewBranchReferenceName("foo")
+ err = wt.Checkout(&CheckoutOptions{
+ Hash: firstCommit,
+ Create: true,
+ Branch: targetBranch,
+ })
- mergeBranchRef := plumbing.NewHashReference(mergeBranchRefname, hash)
- c.Assert(r.Storer.SetReference(mergeBranchRef), IsNil)
+ c.Assert(err, IsNil)
- err = r.Merge(*mergeBranchRef, MergeOptions{
- FFOnly: true,
+ createCommit(c, r)
+ h := createCommit(c, r)
+
+ // Checkout the master branch so that we can try to merge foo into it.
+ err = wt.Checkout(&CheckoutOptions{
+ Branch: plumbing.Master,
+ })
+ c.Assert(err, IsNil)
+
+ head, err := r.Head()
+ c.Assert(err, IsNil)
+ c.Assert(head.Hash(), Equals, lastCommit)
+
+ targetRef := plumbing.NewHashReference(targetBranch, h)
+ c.Assert(targetRef, NotNil)
+
+ err = r.Merge(*targetRef, MergeOptions{
+ Strategy: MergeStrategy(10),
})
+ c.Assert(err, Equals, ErrUnsupportedMergeStrategy)
+
+ // Failed merge operations must not change HEAD.
+ head, err = r.Head()
c.Assert(err, IsNil)
+ c.Assert(head.Hash(), Equals, lastCommit)
+
+ err = r.Merge(*targetRef, MergeOptions{})
+ c.Assert(err, Equals, ErrFastForwardMergeNotPossible)
head, err = r.Head()
- c.Assert(head.Hash(), Equals, mergeBranchRef.Hash())
+ c.Assert(err, IsNil)
+ c.Assert(head.Hash(), Equals, lastCommit)
}
func (s *RepositorySuite) TestCreateBranchUnmarshal(c *C) {