Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

git: Implement Merge function with initial FastForwardMerge support #1044

Merged
merged 2 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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