Skip to content

Commit

Permalink
git: Implement Merge function with initial FastForwardMerge support
Browse files Browse the repository at this point in the history
Introduces the Merge function for merging branches in the codebase.
Currently, the function only supports FastForwardMerge strategy, meaning it
can efficiently update the target branch pointer if the source branch history
is a linear descendant.

Support for additional merge strategies (e.g., three-way merge) will be added
in future commits.

Signed-off-by: Paulo Gomes <paulo.gomes@suse.com>
  • Loading branch information
pjbgf committed Mar 9, 2024
1 parent 4bed230 commit a45b480
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 67 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
// 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
54 changes: 36 additions & 18 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 @@ -1769,20 +1771,36 @@ 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 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,

Check failure on line 481 in repository_test.go

View workflow job for this annotation

GitHub Actions / test (master, ubuntu-latest)

unknown field Mode in struct literal of type MergeOptions

Check failure on line 481 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.19.x, ubuntu-latest)

unknown field 'Mode' in struct literal of type MergeOptions

Check failure on line 481 in repository_test.go

View workflow job for this annotation

GitHub Actions / test (v2.11.0, ubuntu-latest)

unknown field Mode in struct literal of type MergeOptions

Check failure on line 481 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.19.x, macos-latest)

unknown field 'Mode' in struct literal of type MergeOptions

Check failure on line 481 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.19.x, windows-latest)

unknown field 'Mode' in struct literal of type MergeOptions

Check failure on line 481 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.20.x, ubuntu-latest)

unknown field Mode in struct literal of type MergeOptions

Check failure on line 481 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.20.x, macos-latest)

unknown field Mode in struct literal of type MergeOptions

Check failure on line 481 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.20.x, windows-latest)

unknown field Mode in struct literal of type MergeOptions

Check failure on line 481 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.21.x, ubuntu-latest)

unknown field Mode in struct literal of type MergeOptions

Check failure on line 481 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.21.x, macos-latest)

unknown field Mode in struct literal of type MergeOptions

Check failure on line 481 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.21.x, windows-latest)

unknown field Mode in struct literal of type MergeOptions
})
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),

Check failure on line 532 in repository_test.go

View workflow job for this annotation

GitHub Actions / test (master, ubuntu-latest)

unknown field Mode in struct literal of type MergeOptions

Check failure on line 532 in repository_test.go

View workflow job for this annotation

GitHub Actions / test (master, ubuntu-latest)

undefined: MergeMode

Check failure on line 532 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.19.x, ubuntu-latest)

unknown field 'Mode' in struct literal of type MergeOptions

Check failure on line 532 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.19.x, ubuntu-latest)

undefined: MergeMode

Check failure on line 532 in repository_test.go

View workflow job for this annotation

GitHub Actions / test (v2.11.0, ubuntu-latest)

unknown field Mode in struct literal of type MergeOptions

Check failure on line 532 in repository_test.go

View workflow job for this annotation

GitHub Actions / test (v2.11.0, ubuntu-latest)

undefined: MergeMode

Check failure on line 532 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.19.x, macos-latest)

unknown field 'Mode' in struct literal of type MergeOptions

Check failure on line 532 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.19.x, macos-latest)

undefined: MergeMode

Check failure on line 532 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.19.x, windows-latest)

unknown field 'Mode' in struct literal of type MergeOptions

Check failure on line 532 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.19.x, windows-latest)

undefined: MergeMode

Check failure on line 532 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.20.x, ubuntu-latest)

unknown field Mode in struct literal of type MergeOptions

Check failure on line 532 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.20.x, ubuntu-latest)

undefined: MergeMode

Check failure on line 532 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.20.x, macos-latest)

unknown field Mode in struct literal of type MergeOptions

Check failure on line 532 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.20.x, macos-latest)

undefined: MergeMode

Check failure on line 532 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.20.x, windows-latest)

unknown field Mode in struct literal of type MergeOptions

Check failure on line 532 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.20.x, windows-latest)

undefined: MergeMode

Check failure on line 532 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.21.x, ubuntu-latest)

unknown field Mode in struct literal of type MergeOptions

Check failure on line 532 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.21.x, ubuntu-latest)

undefined: MergeMode

Check failure on line 532 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.21.x, macos-latest)

unknown field Mode in struct literal of type MergeOptions

Check failure on line 532 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.21.x, macos-latest)

undefined: MergeMode

Check failure on line 532 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.21.x, windows-latest)

unknown field Mode in struct literal of type MergeOptions

Check failure on line 532 in repository_test.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.21.x, windows-latest)

undefined: MergeMode
})
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 a45b480

Please sign in to comment.