Skip to content

Commit

Permalink
git: Add support for FastForwardMerge
Browse files Browse the repository at this point in the history
Introduces a Merge function which only supports fast forward merges.

Signed-off-by: Paulo Gomes <paulo.gomes@suse.com>
  • Loading branch information
pjbgf committed Mar 9, 2024
1 parent 3102fdb commit ec81c25
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 66 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
20 changes: 16 additions & 4 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
// Mode defines the merge mode to be applied for a Merge.
Mode MergeMode
}

// MergeMode represents the different types of merge operations.
type MergeMode int8

const (
// FastForwardMerge represents a Git merge operation 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 MergeMode = 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
47 changes: 30 additions & 17 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")
ErrUnsupportedMergeMode = errors.New("unsupported merge mode")
ErrFastForwardMergeNotPossible = errors.New("not possible to fast-forward merge changes")
)

// Repository represents a git repository
Expand Down Expand Up @@ -1771,18 +1773,29 @@ func (r *Repository) RepackObjects(cfg *RepackConfig) (err error) {

// 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")
if opts.Mode != FastForwardMerge {
return ErrUnsupportedMergeMode
}

// 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())
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()))
Expand Down
126 changes: 90 additions & 36 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 @@ -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{
Mode: 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{
Mode: MergeMode(10),
})
c.Assert(err, Equals, ErrUnsupportedMergeMode)

// 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) {
Expand Down

0 comments on commit ec81c25

Please sign in to comment.