diff --git a/options.go b/options.go index d6f0e9f11..63a265fb5 100644 --- a/options.go +++ b/options.go @@ -66,6 +66,13 @@ type CloneOptions struct { CABundle []byte } +// MergeOptions describes how a merge should be erformed +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 +} + // 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 db78ae718..c60b14fae 100644 --- a/remote.go +++ b/remote.go @@ -1032,7 +1032,7 @@ func checkFastForwardUpdate(s storer.EncodedObjectStorer, remoteRefs storer.Refe return fmt.Errorf("non-fast-forward update: %s", cmd.Name.String()) } - ff, err := isFastForward(s, cmd.Old, cmd.New) + ff, err := IsFastForward(s, cmd.Old, cmd.New) if err != nil { return err } @@ -1044,7 +1044,7 @@ func checkFastForwardUpdate(s storer.EncodedObjectStorer, remoteRefs storer.Refe return nil } -func isFastForward(s storer.EncodedObjectStorer, old, new plumbing.Hash) (bool, error) { +func IsFastForward(s storer.EncodedObjectStorer, old, new plumbing.Hash) (bool, error) { c, err := object.GetCommit(s, new) if err != nil { return false, err @@ -1166,7 +1166,7 @@ func (r *Remote) updateLocalReferenceStorage( // If the ref exists locally as a branch and force is not specified, // only update if the new ref is an ancestor of the old if old != nil && old.Name().IsBranch() && !force && !spec.IsForceUpdate() { - ff, err := isFastForward(r.s, old.Hash(), new.Hash()) + ff, err := IsFastForward(r.s, old.Hash(), new.Hash()) if err != nil { return updated, err } diff --git a/repository.go b/repository.go index e8eb53f70..c3f987cbb 100644 --- a/repository.go +++ b/repository.go @@ -1642,6 +1642,25 @@ func (r *Repository) RepackObjects(cfg *RepackConfig) (err error) { return nil } +// Merge attempts to merge ref onto HEAD. Currently only supports fast-forward merges +func (r *Repository) Merge(ref plumbing.Reference, opts MergeOptions) error { + if !opts.FFOnly { + return errors.New("non fast-forward merges are not supported yet") + } + + head, err := r.Head() + if err != nil { + return err + } + + ff, err := IsFastForward(r.Storer, head.Hash(), ref.Hash()) + if !ff { + return errors.New("fast forward is not possible") + } + + return r.Storer.SetReference(plumbing.NewHashReference(head.Name(), ref.Hash())) +} + // createNewObjectPack is a helper for RepackObjects taking care // of creating a new pack. It is used so the the PackfileWriter // deferred close has the right scope. diff --git a/repository_test.go b/repository_test.go index e284df8d6..34d8911c4 100644 --- a/repository_test.go +++ b/repository_test.go @@ -337,6 +337,59 @@ func (s *RepositorySuite) TestCreateBranchAndBranch(c *C) { c.Assert(branch.Merge, Equals, testBranch.Merge) } +func (s *RepositorySuite) TestMergeFF(c *C) { + r, _ := Init(memory.NewStorage(), memfs.New()) + err := r.clone(context.Background(), &CloneOptions{ + URL: s.GetBasicLocalRepositoryURL(), + }) + c.Assert(err, IsNil) + head, err := r.Head() + c.Assert(err, IsNil) + + mergeBranchRefname := plumbing.NewBranchReferenceName("foo") + err = r.Storer.SetReference(plumbing.NewHashReference(mergeBranchRefname, head.Hash())) + c.Assert(err, IsNil) + + commit, err := r.CommitObject(head.Hash()) + c.Assert(err, IsNil) + treeHash := commit.TreeHash + + hash := commit.Hash + + 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, + }, + } + + o := r.Storer.NewEncodedObject() + c.Assert(commit.Encode(o), IsNil) + hash, err = r.Storer.SetEncodedObject(o) + } + + mergeBranchRef := plumbing.NewHashReference(mergeBranchRefname, hash) + c.Assert(r.Storer.SetReference(mergeBranchRef), IsNil) + + err = r.Merge(*mergeBranchRef, MergeOptions{ + FFOnly: true, + }) + c.Assert(err, IsNil) + + head, err = r.Head() + c.Assert(head.Hash(), Equals, mergeBranchRef.Hash()) +} + func (s *RepositorySuite) TestCreateBranchUnmarshal(c *C) { r, _ := Init(memory.NewStorage(), nil) diff --git a/worktree.go b/worktree.go index c974aed24..0925e73c3 100644 --- a/worktree.go +++ b/worktree.go @@ -95,7 +95,7 @@ func (w *Worktree) PullContext(ctx context.Context, o *PullOptions) error { head, err := w.r.Head() if err == nil { - headAheadOfRef, err := isFastForward(w.r.Storer, ref.Hash(), head.Hash()) + headAheadOfRef, err := IsFastForward(w.r.Storer, ref.Hash(), head.Hash()) if err != nil { return err } @@ -104,7 +104,7 @@ func (w *Worktree) PullContext(ctx context.Context, o *PullOptions) error { return NoErrAlreadyUpToDate } - ff, err := isFastForward(w.r.Storer, head.Hash(), ref.Hash()) + ff, err := IsFastForward(w.r.Storer, head.Hash(), ref.Hash()) if err != nil { return err }