Skip to content

Commit 3ee5bc9

Browse files
committed
git: Implement Merge function with initial FastForwardMerge support
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>
1 parent 4bed230 commit 3ee5bc9

File tree

5 files changed

+151
-67
lines changed

5 files changed

+151
-67
lines changed

COMPATIBILITY.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ compatibility status with go-git.
2727

2828
## Branching and merging
2929

30-
| Feature | Sub-feature | Status | Notes | Examples |
31-
| ----------- | ----------- | ------ | --------------------------------------- | ----------------------------------------------------------------------------------------------- |
32-
| `branch` | || | - [branch](_examples/branch/main.go) |
33-
| `checkout` | || Basic usages of checkout are supported. | - [checkout](_examples/checkout/main.go) |
34-
| `merge` | | | | |
35-
| `mergetool` | || | |
36-
| `stash` | || | |
37-
| `tag` | || | - [tag](_examples/tag/main.go) <br/> - [tag create and push](_examples/tag-create-push/main.go) |
30+
| Feature | Sub-feature | Status | Notes | Examples |
31+
| ----------- | ----------- | ------------ | --------------------------------------- | ----------------------------------------------------------------------------------------------- |
32+
| `branch` | | | | - [branch](_examples/branch/main.go) |
33+
| `checkout` | | | Basic usages of checkout are supported. | - [checkout](_examples/checkout/main.go) |
34+
| `merge` | | ⚠️ (partial) | Fast-forward only | |
35+
| `mergetool` | | | | |
36+
| `stash` | | | | |
37+
| `tag` | | | | - [tag](_examples/tag/main.go) <br/> - [tag create and push](_examples/tag-create-push/main.go) |
3838

3939
## Sharing and updating projects
4040

options.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,25 @@ type CloneOptions struct {
8989
Shared bool
9090
}
9191

92-
// MergeOptions describes how a merge should be erformed
92+
// MergeOptions describes how a merge should be performed.
9393
type MergeOptions struct {
94-
// Requires a merge to be fast forward only. If this is true, then a merge will
95-
// throw an error if ff is not possible.
96-
FFOnly bool
94+
// Strategy defines the merge strategy to be used.
95+
Strategy MergeStrategy
9796
}
9897

98+
// MergeStrategy represents the different types of merge strategies.
99+
type MergeStrategy int8
100+
101+
const (
102+
// FastForwardMerge represents a Git merge strategy where the current
103+
// branch can be simply updated to point to the HEAD of the branch being
104+
// merged. This is only possible if the history of the branch being merged
105+
// is a linear descendant of the current branch, with no conflicting commits.
106+
//
107+
// This is the default option.
108+
FastForwardMerge MergeStrategy = iota
109+
)
110+
99111
// Validate validates the fields and sets the default values.
100112
func (o *CloneOptions) Validate() error {
101113
if o.URL == "" {

remote.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1128,7 +1128,7 @@ func isFastForward(s storer.EncodedObjectStorer, old, new plumbing.Hash, earlies
11281128
}
11291129

11301130
found := false
1131-
// stop iterating at the earlist shallow commit, ignoring its parents
1131+
// stop iterating at the earliest shallow commit, ignoring its parents
11321132
// note: when pull depth is smaller than the number of new changes on the remote, this fails due to missing parents.
11331133
// as far as i can tell, without the commits in-between the shallow pull and the earliest shallow, there's no
11341134
// real way of telling whether it will be a fast-forward merge.

repository.go

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -51,19 +51,21 @@ var (
5151
// ErrFetching is returned when the packfile could not be downloaded
5252
ErrFetching = errors.New("unable to fetch packfile")
5353

54-
ErrInvalidReference = errors.New("invalid reference, should be a tag or a branch")
55-
ErrRepositoryNotExists = errors.New("repository does not exist")
56-
ErrRepositoryIncomplete = errors.New("repository's commondir path does not exist")
57-
ErrRepositoryAlreadyExists = errors.New("repository already exists")
58-
ErrRemoteNotFound = errors.New("remote not found")
59-
ErrRemoteExists = errors.New("remote already exists")
60-
ErrAnonymousRemoteName = errors.New("anonymous remote name must be 'anonymous'")
61-
ErrWorktreeNotProvided = errors.New("worktree should be provided")
62-
ErrIsBareRepository = errors.New("worktree not available in a bare repository")
63-
ErrUnableToResolveCommit = errors.New("unable to resolve commit")
64-
ErrPackedObjectsNotSupported = errors.New("packed objects not supported")
65-
ErrSHA256NotSupported = errors.New("go-git was not compiled with SHA256 support")
66-
ErrAlternatePathNotSupported = errors.New("alternate path must use the file scheme")
54+
ErrInvalidReference = errors.New("invalid reference, should be a tag or a branch")
55+
ErrRepositoryNotExists = errors.New("repository does not exist")
56+
ErrRepositoryIncomplete = errors.New("repository's commondir path does not exist")
57+
ErrRepositoryAlreadyExists = errors.New("repository already exists")
58+
ErrRemoteNotFound = errors.New("remote not found")
59+
ErrRemoteExists = errors.New("remote already exists")
60+
ErrAnonymousRemoteName = errors.New("anonymous remote name must be 'anonymous'")
61+
ErrWorktreeNotProvided = errors.New("worktree should be provided")
62+
ErrIsBareRepository = errors.New("worktree not available in a bare repository")
63+
ErrUnableToResolveCommit = errors.New("unable to resolve commit")
64+
ErrPackedObjectsNotSupported = errors.New("packed objects not supported")
65+
ErrSHA256NotSupported = errors.New("go-git was not compiled with SHA256 support")
66+
ErrAlternatePathNotSupported = errors.New("alternate path must use the file scheme")
67+
ErrUnsupportedMergeStrategy = errors.New("unsupported merge strategy")
68+
ErrFastForwardMergeNotPossible = errors.New("not possible to fast-forward merge changes")
6769
)
6870

6971
// Repository represents a git repository
@@ -1769,20 +1771,36 @@ func (r *Repository) RepackObjects(cfg *RepackConfig) (err error) {
17691771
return nil
17701772
}
17711773

1772-
// Merge attempts to merge ref onto HEAD. Currently only supports fast-forward merges
1774+
// Merge merges the reference branch into the current branch.
1775+
//
1776+
// If the merge is not possible (or supported) returns an error without changing
1777+
// the HEAD for the current branch. Possible errors include:
1778+
// - The merge strategy is not supported.
1779+
// - The specific strategy cannot be used (e.g. using FastForwardMerge when one is not possible).
17731780
func (r *Repository) Merge(ref plumbing.Reference, opts MergeOptions) error {
1774-
if !opts.FFOnly {
1775-
return errors.New("non fast-forward merges are not supported yet")
1781+
if opts.Strategy != FastForwardMerge {
1782+
return ErrUnsupportedMergeStrategy
1783+
}
1784+
1785+
// Ignore error as not having a shallow list is optional here.
1786+
shallowList, _ := r.Storer.Shallow()
1787+
var earliestShallow *plumbing.Hash
1788+
if len(shallowList) > 0 {
1789+
earliestShallow = &shallowList[0]
17761790
}
17771791

17781792
head, err := r.Head()
17791793
if err != nil {
17801794
return err
17811795
}
17821796

1783-
ff, err := IsFastForward(r.Storer, head.Hash(), ref.Hash())
1797+
ff, err := isFastForward(r.Storer, head.Hash(), ref.Hash(), earliestShallow)
1798+
if err != nil {
1799+
return err
1800+
}
1801+
17841802
if !ff {
1785-
return errors.New("fast forward is not possible")
1803+
return ErrFastForwardMergeNotPossible
17861804
}
17871805

17881806
return r.Storer.SetReference(plumbing.NewHashReference(head.Name(), ref.Hash()))

repository_test.go

Lines changed: 90 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func (s *RepositorySuite) TestInitWithInvalidDefaultBranch(c *C) {
8282
c.Assert(err, NotNil)
8383
}
8484

85-
func createCommit(c *C, r *Repository) {
85+
func createCommit(c *C, r *Repository) plumbing.Hash {
8686
// Create a commit so there is a HEAD to check
8787
wt, err := r.Worktree()
8888
c.Assert(err, IsNil)
@@ -101,13 +101,14 @@ func createCommit(c *C, r *Repository) {
101101
Email: "go-git@fake.local",
102102
When: time.Now(),
103103
}
104-
_, err = wt.Commit("test commit message", &CommitOptions{
104+
105+
h, err := wt.Commit("test commit message", &CommitOptions{
105106
All: true,
106107
Author: &author,
107108
Committer: &author,
108109
})
109110
c.Assert(err, IsNil)
110-
111+
return h
111112
}
112113

113114
func (s *RepositorySuite) TestInitNonStandardDotGit(c *C) {
@@ -440,56 +441,109 @@ func (s *RepositorySuite) TestCreateBranchAndBranch(c *C) {
440441
}
441442

442443
func (s *RepositorySuite) TestMergeFF(c *C) {
443-
r, _ := Init(memory.NewStorage(), memfs.New())
444-
err := r.clone(context.Background(), &CloneOptions{
445-
URL: s.GetBasicLocalRepositoryURL(),
444+
r, err := Init(memory.NewStorage(), memfs.New())
445+
c.Assert(err, IsNil)
446+
c.Assert(r, NotNil)
447+
448+
createCommit(c, r)
449+
createCommit(c, r)
450+
createCommit(c, r)
451+
lastCommit := createCommit(c, r)
452+
453+
wt, err := r.Worktree()
454+
c.Assert(err, IsNil)
455+
456+
targetBranch := plumbing.NewBranchReferenceName("foo")
457+
err = wt.Checkout(&CheckoutOptions{
458+
Hash: lastCommit,
459+
Create: true,
460+
Branch: targetBranch,
446461
})
447462
c.Assert(err, IsNil)
463+
464+
createCommit(c, r)
465+
fooHash := createCommit(c, r)
466+
467+
// Checkout the master branch so that we can try to merge foo into it.
468+
err = wt.Checkout(&CheckoutOptions{
469+
Branch: plumbing.Master,
470+
})
471+
c.Assert(err, IsNil)
472+
448473
head, err := r.Head()
449474
c.Assert(err, IsNil)
475+
c.Assert(head.Hash(), Equals, lastCommit)
476+
477+
targetRef := plumbing.NewHashReference(targetBranch, fooHash)
478+
c.Assert(targetRef, NotNil)
450479

451-
mergeBranchRefname := plumbing.NewBranchReferenceName("foo")
452-
err = r.Storer.SetReference(plumbing.NewHashReference(mergeBranchRefname, head.Hash()))
480+
err = r.Merge(*targetRef, MergeOptions{
481+
Strategy: FastForwardMerge,
482+
})
453483
c.Assert(err, IsNil)
454484

455-
commit, err := r.CommitObject(head.Hash())
485+
head, err = r.Head()
456486
c.Assert(err, IsNil)
457-
treeHash := commit.TreeHash
487+
c.Assert(head.Hash(), Equals, fooHash)
488+
}
458489

459-
hash := commit.Hash
490+
func (s *RepositorySuite) TestMergeFF_Invalid(c *C) {
491+
r, err := Init(memory.NewStorage(), memfs.New())
492+
c.Assert(err, IsNil)
493+
c.Assert(r, NotNil)
460494

461-
for i := 0; i < 10; i++ {
462-
commit = &object.Commit{
463-
Author: object.Signature{
464-
Name: "A U Thor",
465-
Email: "author@example.com",
466-
},
467-
Committer: object.Signature{
468-
Name: "A U Thor",
469-
Email: "author@example.com",
470-
},
471-
Message: fmt.Sprintf("commit #%d", i),
472-
TreeHash: treeHash,
473-
ParentHashes: []plumbing.Hash{
474-
hash,
475-
},
476-
}
495+
// Keep track of the first commit, which will be the
496+
// reference to create the target branch so that we
497+
// can simulate a non-ff merge.
498+
firstCommit := createCommit(c, r)
499+
createCommit(c, r)
500+
createCommit(c, r)
501+
lastCommit := createCommit(c, r)
477502

478-
o := r.Storer.NewEncodedObject()
479-
c.Assert(commit.Encode(o), IsNil)
480-
hash, err = r.Storer.SetEncodedObject(o)
481-
}
503+
wt, err := r.Worktree()
504+
c.Assert(err, IsNil)
505+
506+
targetBranch := plumbing.NewBranchReferenceName("foo")
507+
err = wt.Checkout(&CheckoutOptions{
508+
Hash: firstCommit,
509+
Create: true,
510+
Branch: targetBranch,
511+
})
482512

483-
mergeBranchRef := plumbing.NewHashReference(mergeBranchRefname, hash)
484-
c.Assert(r.Storer.SetReference(mergeBranchRef), IsNil)
513+
c.Assert(err, IsNil)
485514

486-
err = r.Merge(*mergeBranchRef, MergeOptions{
487-
FFOnly: true,
515+
createCommit(c, r)
516+
h := createCommit(c, r)
517+
518+
// Checkout the master branch so that we can try to merge foo into it.
519+
err = wt.Checkout(&CheckoutOptions{
520+
Branch: plumbing.Master,
521+
})
522+
c.Assert(err, IsNil)
523+
524+
head, err := r.Head()
525+
c.Assert(err, IsNil)
526+
c.Assert(head.Hash(), Equals, lastCommit)
527+
528+
targetRef := plumbing.NewHashReference(targetBranch, h)
529+
c.Assert(targetRef, NotNil)
530+
531+
err = r.Merge(*targetRef, MergeOptions{
532+
Strategy: MergeStrategy(10),
488533
})
534+
c.Assert(err, Equals, ErrUnsupportedMergeStrategy)
535+
536+
// Failed merge operations must not change HEAD.
537+
head, err = r.Head()
489538
c.Assert(err, IsNil)
539+
c.Assert(head.Hash(), Equals, lastCommit)
540+
541+
err = r.Merge(*targetRef, MergeOptions{})
542+
c.Assert(err, Equals, ErrFastForwardMergeNotPossible)
490543

491544
head, err = r.Head()
492-
c.Assert(head.Hash(), Equals, mergeBranchRef.Hash())
545+
c.Assert(err, IsNil)
546+
c.Assert(head.Hash(), Equals, lastCommit)
493547
}
494548

495549
func (s *RepositorySuite) TestCreateBranchUnmarshal(c *C) {

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy