Skip to content

Commit 54a4a96

Browse files
committed
wip
Signed-off-by: David Pordomingo <[email protected]>
1 parent 6c7fbbe commit 54a4a96

File tree

4 files changed

+357
-1
lines changed

4 files changed

+357
-1
lines changed

COMPATIBILITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ is supported by go-git.
2121
| **branching and merging** |
2222
| branch ||
2323
| checkout || Basic usages of checkout are supported. |
24-
| merge | |
24+
| merge | | Only supports merges where the merge can be resolved as a fast-forward. Does not support `--no-commit` nor `--allow-unrelated-histories` flags |
2525
| mergetool ||
2626
| stash ||
2727
| tag ||

_examples/merge/help-long.msg.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package main
2+
3+
const helpLongMsg = `
4+
NAME:
5+
%_COMMAND_NAME_% - Join two commits
6+
7+
SYNOPSIS:
8+
usage: %_COMMAND_NAME_% <path> <baseCommitRev> <commitRev> [-m <msg>] [--ff-only]
9+
[--no-ff] [--no-commit] [--allow-unrelated-histories]
10+
or: %_COMMAND_NAME_% --help
11+
12+
params:
13+
<path> Path to the git repository
14+
<baseCommitRev> Git revision of the commit that will be the base of the merge
15+
<commitRev> Git revision of the commit that will be merged over the base
16+
17+
DESCRIPTION:
18+
%_COMMAND_NAME_% Incorporates changes from the passed <commitRev> over <baseCommitRev> and moves the HEAD of the repo to the merge commit.
19+
20+
OPTIONS:
21+
If no options passed, the merge commit will be avoided if it could be considered as a fast-forward, and if needed, it will be used a default merge commit message.
22+
23+
-m <msg>
24+
[NOT IMPLEMENTED]
25+
Set the commit message to be used for the merge commit (in case one is created).
26+
If not provided, an automated message will be generated.
27+
28+
--ff-only
29+
Refuse to merge and exit with 1 unless the current HEAD is already up to date or the merge can be resolved as a fast-forward.
30+
31+
--no-ff
32+
[NOT IMPLEMENTED]
33+
Create a merge commit even when the merge resolves as a fast-forward.
34+
35+
--no-commit
36+
[NOT IMPLEMENTED]
37+
With --no-commit perform the merge but pretend the merge failed and do not autocommit, to give the user a chance to
38+
inspect and further tweak the merge result before committing.
39+
40+
--allow-unrelated-histories
41+
[NOT IMPLEMENTED]
42+
By default, git merge command refuses to merge histories that do not share a common ancestor. This option can be
43+
used to override this safety when merging histories of two projects that started their lives independently.
44+
45+
DISCUSSION:
46+
Assume the following history exists and the current branch is "master":
47+
48+
o---o---o---o---C <topic
49+
/ /
50+
---C---B---o---A---o---G <master <HEAD
51+
52+
Then "%_COMMAND_NAME_% <path> master topic" will replay the changes made on the topic branch since it diverged from master (i.e., A) until its current commit (i.e., C) on top of master, and record the result in a new commit (i.e., M) along with the names of the two parent commits and a log message from the user describing the changes.
53+
54+
o---o---o---o---C <topic
55+
/ / \
56+
---C---B---o---A---o---G---M <HEAD
57+
58+
Once the merge has been performed successfully HEAD will be updated to the new merge commit.
59+
`
60+

_examples/merge/main.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"gopkg.in/src-d/go-git.v4"
8+
utils "gopkg.in/src-d/go-git.v4/_examples"
9+
"gopkg.in/src-d/go-git.v4/plumbing"
10+
"gopkg.in/src-d/go-git.v4/plumbing/object"
11+
)
12+
13+
type exitCode int
14+
15+
const (
16+
exitCodeNoFastForward utils.ExitCode = 1
17+
exitCodeFeatureNotImplemented utils.ExitCode = 9
18+
)
19+
20+
const (
21+
cmdDesc = "Performs the merge between two commits, and moves HEAD into the merge commit:"
22+
23+
helpShortMsg = `
24+
usage: %_COMMAND_NAME_% <path> <baseCommitRev> <commitRev> [-m <msg>] [--ff-only]
25+
[--no-ff] [--no-commit] [--allow-unrelated-histories]
26+
or: %_COMMAND_NAME_% --help
27+
28+
params:
29+
<path> Path to the git repository
30+
<baseCommitRev> Git revision of the commit that will be the base of the merge
31+
<commitRev> Git revision of the commit that will be merged over the base
32+
33+
options:
34+
(no options) Performs the regular merge, as fast-forward if possible
35+
--ff-only If the merge is not a fast-forward the process exits with 1
36+
--help Show the full help message of %_COMMAND_NAME_%
37+
38+
[NOT IMPLEMENTED]
39+
-m <msg> Uses the passed <msg> for the merge commit message
40+
--no-ff Create a merge commit even when it could be a fast-forward
41+
--no-commit Performs the merge in the worktree and do not create a commit
42+
--allow-unrelated-histories
43+
Lets the merge to operate with commits not sharing its history
44+
`
45+
)
46+
47+
func main() {
48+
if len(os.Args) == 1 {
49+
utils.HelpAndExit(cmdDesc, helpShortMsg)
50+
}
51+
52+
if os.Args[1] == "--help" || os.Args[1] == "-h" {
53+
utils.HelpAndExit(cmdDesc, helpLongMsg)
54+
}
55+
56+
if len(os.Args) < 4 {
57+
utils.WrongSyntaxAndExit(helpShortMsg)
58+
}
59+
60+
path := os.Args[1]
61+
commitRevs := os.Args[2:4]
62+
63+
mergeOptions := &git.MergeOptions{}
64+
if len(os.Args) > 4 {
65+
args := os.Args[4:]
66+
skipNext := false
67+
for i, v := range args {
68+
if skipNext {
69+
skipNext = false
70+
continue
71+
}
72+
switch v {
73+
case "--no-ff":
74+
mergeOptions.NoFF = true
75+
case "--ff-only":
76+
mergeOptions.FFOnly = true
77+
case "--no-commit":
78+
mergeOptions.NoCommit = true
79+
case "--allow-unrelated-histories":
80+
mergeOptions.AllowUnrelated = true
81+
case "-m":
82+
if len(args) < i+2 {
83+
utils.WrongSyntaxAndExit("message not defined, should be -m <msg>")
84+
}
85+
86+
mergeOptions.Message = args[i+1]
87+
skipNext = true
88+
default:
89+
utils.WrongSyntaxAndExit(helpShortMsg)
90+
}
91+
}
92+
}
93+
94+
// Open a git repository from current directory
95+
repo, err := git.PlainOpen(path)
96+
utils.ExitIfError(err, utils.ExitCodeCouldNotOpenRepository)
97+
98+
// Get the hashes of the passed revisions
99+
var hashes []*plumbing.Hash
100+
for _, rev := range commitRevs {
101+
hash, err := repo.ResolveRevision(plumbing.Revision(rev))
102+
utils.ExitIfError(err, utils.ExitCodeCouldNotParseRevision, rev)
103+
hashes = append(hashes, hash)
104+
}
105+
106+
// Get the commits identified by the passed hashes
107+
var commits []*object.Commit
108+
for _, hash := range hashes {
109+
commit, err := repo.CommitObject(*hash)
110+
utils.ExitIfError(err, utils.ExitCodeWrongCommitHash, hash.String())
111+
commits = append(commits, commit)
112+
}
113+
114+
commit, err := git.Merge(repo, commits[0], commits[1], mergeOptions)
115+
switch err {
116+
case git.ErrSameCommit:
117+
fmt.Println("Already up to date. Both are the same commit.")
118+
return
119+
case git.ErrAlreadyUpToDate:
120+
fmt.Println("Already up to date.")
121+
return
122+
case git.ErrNotImplementedUnrelated,
123+
git.ErrNotImplementedNoCommit,
124+
git.ErrNotImplementedNoFF,
125+
git.ErrNotImplementedMessage:
126+
utils.ExitIfError(err, exitCodeFeatureNotImplemented, "unimplemented feature")
127+
}
128+
129+
utils.ExitIfError(err, utils.ExitCodeExpected, "merge failed")
130+
131+
utils.PrintCommits(commit)
132+
}

merge.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package git
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"gopkg.in/src-d/go-git.v4/plumbing/object"
8+
)
9+
10+
type strategy int
11+
12+
const (
13+
// Fail will cause the merge fail if a conflict is found
14+
Fail strategy = iota
15+
// Ours will use the changes from candidate if a conflict is found
16+
Ours
17+
// Theirs will use the changes from the base if a conflict is found
18+
Theirs
19+
)
20+
21+
var (
22+
// ErrSameCommit returned when passed commits are the same
23+
ErrSameCommit = errors.New("passed commits are the same")
24+
// ErrAlreadyUpToDate returned when the second is behind first
25+
ErrAlreadyUpToDate = errors.New("second is behind first")
26+
// ErrHasConflicts returned when conflicts found
27+
ErrHasConflicts = errors.New("conflicts found")
28+
// ErrNoCommonHistory returned when no shared history
29+
ErrNoCommonHistory = errors.New("no shared history")
30+
// ErrNonFastForwardUpdate returned when no fast forward was possible
31+
// defined at worktree.go
32+
// ErrWorktreeNotClean returned when no clean state in worktree
33+
// defined at worktree.go
34+
35+
// ExitCodeUnexpected returned when commit merge is required
36+
ErrNotImplementedNoFF = errors.New("no fast-forward merge is not implemented")
37+
// ErrNotImplementedNoCommit returned when no-commit is required
38+
ErrNotImplementedNoCommit = errors.New("no commit merge is not implemented")
39+
// ErrNotImplementedUnrelated returned
40+
ErrNotImplementedUnrelated = errors.New("unrelated merge is not implemented")
41+
// ErrNotImplementedMessage returned
42+
ErrNotImplementedMessage = errors.New("custom message is not implemented")
43+
)
44+
45+
// MergeOptions describes how a merge should be performed.
46+
type MergeOptions struct {
47+
NoFF bool // NoFF when set to true, Merge will always create a merge commit
48+
FFOnly bool // FFOnly causes the Merge fail if it is not a fast forward
49+
NoCommit bool // NoCommit leaves the changes in the worktree without commit them
50+
AllowUnrelated bool // AllowUnrelated performs the merge even with unrelated histories
51+
Message string // Message text to be used for the message
52+
}
53+
54+
// Merge merges the second commit over the first one, and moves `HEAD` to the merge.
55+
// If `NoCommit` option was passed, the changes required for the merge will be
56+
// left in the worktree, and the merge commit won't be created.
57+
// It returns the merge commit, and an error if the HEAD was not moved or
58+
// when the merge operation could not be done.
59+
func Merge(
60+
repo *Repository,
61+
first *object.Commit,
62+
second *object.Commit,
63+
options *MergeOptions,
64+
) (*object.Commit, error) {
65+
if options == nil {
66+
options = &MergeOptions{}
67+
}
68+
69+
worktree, err := repo.Worktree()
70+
if err != nil {
71+
return nil, err
72+
}
73+
74+
status, err := worktree.Status()
75+
if err != nil {
76+
return nil, err
77+
}
78+
79+
for range status {
80+
return nil, ErrWorktreeNotClean
81+
}
82+
83+
if first.Hash == second.Hash {
84+
return nil, ErrSameCommit
85+
}
86+
87+
ancestors, err := MergeBase(first, second)
88+
if err != nil {
89+
return nil, err
90+
}
91+
92+
if len(ancestors) == 0 {
93+
if options.AllowUnrelated {
94+
return merge(first, second, nil, options.NoCommit, options.Message)
95+
}
96+
97+
return nil, ErrNoCommonHistory
98+
}
99+
100+
for _, ancestor := range ancestors {
101+
if ancestor.Hash == first.Hash {
102+
if options.NoFF {
103+
// TODO(dpordomingo): there is a special case;
104+
// if asked with `--no-ff` it should be created an empty merge-commit.
105+
return nil, ErrNotImplementedNoFF
106+
}
107+
108+
return second, nil
109+
}
110+
111+
if ancestor.Hash == second.Hash {
112+
return nil, ErrAlreadyUpToDate
113+
}
114+
}
115+
116+
mergeBase := ancestors[0]
117+
118+
if options.FFOnly {
119+
return nil, ErrNonFastForwardUpdate
120+
}
121+
122+
return merge(first, second, mergeBase, options.NoCommit, options.Message)
123+
}
124+
125+
func merge(
126+
first, second, mergeBase *object.Commit,
127+
noCommit bool,
128+
msg string,
129+
) (*object.Commit, error) {
130+
131+
if mergeBase == nil {
132+
// TODO(dpordomingo): handle --no-commit flag
133+
return nil, ErrNotImplementedUnrelated
134+
}
135+
136+
var trees []*object.Tree
137+
for _, commit := range []*object.Commit{first, second} {
138+
tree, err := commit.Tree()
139+
if err != nil {
140+
return nil, err
141+
}
142+
143+
trees = append(trees, tree)
144+
}
145+
146+
changes, err := object.DiffTree(trees[0], trees[1])
147+
if err != nil {
148+
return nil, err
149+
}
150+
fmt.Println(changes)
151+
152+
if noCommit {
153+
// TODO(dpordomingo): handle --no-commit flag
154+
return nil, ErrNotImplementedNoCommit
155+
}
156+
157+
if msg != "" {
158+
// TODO(dpordomingo): handle -m option
159+
return nil, ErrNotImplementedMessage
160+
}
161+
162+
// TODO(dpordomingo)
163+
return nil, ErrNotImplementedNoFF
164+
}

0 commit comments

Comments
 (0)