diff options
-rw-r--r-- | COMPATIBILITY.md | 16 | ||||
-rw-r--r-- | options.go | 20 | ||||
-rw-r--r-- | remote.go | 2 | ||||
-rw-r--r-- | repository.go | 54 | ||||
-rw-r--r-- | repository_test.go | 126 |
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 @@ -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 == "" { @@ -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) { |