Skip to content

Commit

Permalink
Merge pull request #1044 from pjbgf/ff-merge
Browse files Browse the repository at this point in the history
git: Implement Merge function with initial `FastForwardMerge` support
  • Loading branch information
pjbgf committed Mar 12, 2024
2 parents f4f1a87 + 3ee5bc9 commit e6c3e58
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 25 deletions.
16 changes: 8 additions & 8 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
19 changes: 19 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,25 @@ type CloneOptions struct {
Shared bool
}

// MergeOptions describes how a merge should be performed.
type MergeOptions struct {
// 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 == "" {
Expand Down
2 changes: 1 addition & 1 deletion remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
63 changes: 50 additions & 13 deletions repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1769,6 +1771,41 @@ func (r *Repository) RepackObjects(cfg *RepackConfig) (err error) {
return nil
}

// 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.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()
if err != nil {
return err
}

ff, err := isFastForward(r.Storer, head.Hash(), ref.Hash(), earliestShallow)
if err != nil {
return err
}

if !ff {
return ErrFastForwardMergeNotPossible
}

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.
Expand Down
113 changes: 110 additions & 3 deletions repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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) {
Expand Down Expand Up @@ -439,6 +440,112 @@ func (s *RepositorySuite) TestCreateBranchAndBranch(c *C) {
c.Assert(branch.Merge, Equals, testBranch.Merge)
}

func (s *RepositorySuite) TestMergeFF(c *C) {
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)

err = r.Merge(*targetRef, MergeOptions{
Strategy: FastForwardMerge,
})
c.Assert(err, IsNil)

head, err = r.Head()
c.Assert(err, IsNil)
c.Assert(head.Hash(), Equals, fooHash)
}

func (s *RepositorySuite) TestMergeFF_Invalid(c *C) {
r, err := Init(memory.NewStorage(), memfs.New())
c.Assert(err, IsNil)
c.Assert(r, NotNil)

// 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)

wt, err := r.Worktree()
c.Assert(err, IsNil)

targetBranch := plumbing.NewBranchReferenceName("foo")
err = wt.Checkout(&CheckoutOptions{
Hash: firstCommit,
Create: true,
Branch: targetBranch,
})

c.Assert(err, IsNil)

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(err, IsNil)
c.Assert(head.Hash(), Equals, lastCommit)
}

func (s *RepositorySuite) TestCreateBranchUnmarshal(c *C) {
r, _ := Init(memory.NewStorage(), nil)

Expand Down

0 comments on commit e6c3e58

Please sign in to comment.