Skip to content
This repository was archived by the owner on Sep 11, 2020. It is now read-only.

Commit 26b54e8

Browse files
authored
Merge pull request #1096 from dpordomingo/merge-base-command
Add merge base command
2 parents 6142bc3 + 56af959 commit 26b54e8

File tree

5 files changed

+256
-7
lines changed

5 files changed

+256
-7
lines changed

COMPATIBILITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ is supported by go-git.
8686
| for-each-ref ||
8787
| hash-object ||
8888
| ls-files ||
89-
| merge-base | |
89+
| merge-base | | Calculates the merge-base only between two commits, and supports `--independent` and `--is-ancestor` modifiers; Does not support `--fork-point` nor `--octopus` modifiers. |
9090
| read-tree | |
9191
| rev-list ||
9292
| rev-parse | |

_examples/common_test.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ var args = map[string][]string{
2929
"tag": {cloneRepository(defaultURL, tempFolder())},
3030
"pull": {createRepositoryWithRemote(tempFolder(), defaultURL)},
3131
"ls": {cloneRepository(defaultURL, tempFolder()), "HEAD", "vendor"},
32+
"merge_base": {cloneRepository(defaultURL, tempFolder()), "--is-ancestor", "HEAD~3", "HEAD^"},
3233
}
3334

3435
var ignored = map[string]bool{}
@@ -50,14 +51,15 @@ func TestExamples(t *testing.T) {
5051
}
5152

5253
for _, example := range examples {
53-
_, name := filepath.Split(filepath.Dir(example))
54+
dir := filepath.Dir(example)
55+
_, name := filepath.Split(dir)
5456

5557
if ignored[name] {
5658
continue
5759
}
5860

5961
t.Run(name, func(t *testing.T) {
60-
testExample(t, name, example)
62+
testExample(t, name, dir)
6163
})
6264
}
6365
}
@@ -135,10 +137,9 @@ func addRemote(local, remote string) {
135137
CheckIfError(err)
136138
}
137139

138-
func testExample(t *testing.T, name, example string) {
139-
cmd := exec.Command("go", append([]string{
140-
"run", filepath.Join(example),
141-
}, args[name]...)...)
140+
func testExample(t *testing.T, name, dir string) {
141+
arguments := append([]string{"run", dir}, args[name]...)
142+
cmd := exec.Command("go", arguments...)
142143

143144
cmd.Stdout = os.Stdout
144145
cmd.Stderr = os.Stderr

_examples/merge_base/help-long.msg.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package main
2+
3+
const helpLongMsg = `
4+
NAME:
5+
%_COMMAND_NAME_% - Lists the best common ancestors of the two passed commit revisions
6+
7+
SYNOPSIS:
8+
usage: %_COMMAND_NAME_% <path> <commitRev> <commitRev>
9+
or: %_COMMAND_NAME_% <path> --independent <commitRev>...
10+
or: %_COMMAND_NAME_% <path> --is-ancestor <commitRev> <commitRev>
11+
12+
params:
13+
<path> Path to the git repository
14+
<commitRev> Git revision as supported by go-git
15+
16+
DESCRIPTION:
17+
%_COMMAND_NAME_% finds the best common ancestor(s) between two commits. One common ancestor is better than another common ancestor if the latter is an ancestor of the former.
18+
A common ancestor that does not have any better common ancestor is a best common ancestor, i.e. a merge base. Note that there can be more than one merge base for a pair of commits.
19+
Commits that does not share a common history has no common ancestors.
20+
21+
OPTIONS:
22+
As the most common special case, specifying only two commits on the command line means computing the merge base between the given two commits.
23+
If there is no shared history between the passed commits, there won't be a merge-base, and the command will exit with status 1.
24+
25+
--independent
26+
List the subgroup from the passed commits, that cannot be reached from any other of the passed ones. In other words, it prints a minimal subset of the supplied commits with the same ancestors.
27+
28+
--is-ancestor
29+
Check if the first commit is an ancestor of the second one, and exit with status 0 if true, or with status 1 if not. Errors are signaled by a non-zero status that is not 1.
30+
31+
DISCUSSION:
32+
Given two commits A and B, %_COMMAND_NAME_% A B will output a commit which is the best common ancestor of both, what means that is reachable from both A and B through the parent relationship.
33+
34+
For example, with this topology:
35+
36+
o---o---o---o---B
37+
/ /
38+
---3---2---o---1---o---A
39+
40+
the merge base between A and B is 1.
41+
42+
With the given topology 2 and 3 are also common ancestors of A and B, but they are not the best ones because they can be also reached from 1.
43+
44+
When the history involves cross-cross merges, there can be more than one best common ancestor for two commits. For example, with this topology:
45+
46+
---1---o---A
47+
\ /
48+
X
49+
/ \
50+
---2---o---o---B
51+
52+
When the history involves feature branches depending on other feature branches there can be also more than one common ancestor. For example:
53+
54+
55+
o---o---o
56+
/ \
57+
1---o---A \
58+
/ / \
59+
---o---o---2---o---o---B
60+
61+
In both examples, both 1 and 2 are merge-bases of A and B for each situation.
62+
Neither one is better than the other (both are best merge bases) because 1 cannot be reached from 2, nor the opposite.
63+
`

_examples/merge_base/helpers.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
8+
"gopkg.in/src-d/go-git.v4/plumbing/object"
9+
)
10+
11+
func checkIfError(err error, code exitCode, mainReason string, v ...interface{}) {
12+
if err == nil {
13+
return
14+
}
15+
16+
printErr(wrappErr(err, mainReason, v...))
17+
os.Exit(int(code))
18+
}
19+
20+
func helpAndExit(s string, helpMsg string, code exitCode) {
21+
if code == exitCodeSuccess {
22+
printMsg("%s", s)
23+
} else {
24+
printErr(fmt.Errorf(s))
25+
}
26+
27+
fmt.Println(strings.Replace(helpMsg, "%_COMMAND_NAME_%", os.Args[0], -1))
28+
29+
os.Exit(int(code))
30+
}
31+
32+
func printErr(err error) {
33+
fmt.Printf("\x1b[31;1m%s\x1b[0m\n", fmt.Sprintf("error: %s", err))
34+
}
35+
36+
func printMsg(format string, args ...interface{}) {
37+
fmt.Printf("%s\n", fmt.Sprintf(format, args...))
38+
}
39+
40+
func printCommits(commits []*object.Commit) {
41+
for _, commit := range commits {
42+
if os.Getenv("LOG_LEVEL") == "verbose" {
43+
fmt.Printf(
44+
"\x1b[36;1m%s \x1b[90;21m%s\x1b[0m %s\n",
45+
commit.Hash.String()[:7],
46+
commit.Hash.String(),
47+
strings.Split(commit.Message, "\n")[0],
48+
)
49+
} else {
50+
fmt.Println(commit.Hash.String())
51+
}
52+
}
53+
}
54+
55+
func wrappErr(err error, s string, v ...interface{}) error {
56+
if err != nil {
57+
return fmt.Errorf("%s\n %s", fmt.Sprintf(s, v...), err)
58+
}
59+
60+
return nil
61+
}

_examples/merge_base/main.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package main
2+
3+
import (
4+
"os"
5+
6+
"gopkg.in/src-d/go-git.v4"
7+
"gopkg.in/src-d/go-git.v4/plumbing"
8+
"gopkg.in/src-d/go-git.v4/plumbing/object"
9+
)
10+
11+
type exitCode int
12+
13+
const (
14+
exitCodeSuccess exitCode = iota
15+
exitCodeNotFound
16+
exitCodeWrongSyntax
17+
exitCodeCouldNotOpenRepository
18+
exitCodeCouldNotParseRevision
19+
exitCodeUnexpected
20+
21+
cmdDesc = "Returns the merge-base between two commits:"
22+
23+
helpShortMsg = `
24+
usage: %_COMMAND_NAME_% <path> <commitRev> <commitRev>
25+
or: %_COMMAND_NAME_% <path> --independent <commitRev>...
26+
or: %_COMMAND_NAME_% <path> --is-ancestor <commitRev> <commitRev>
27+
or: %_COMMAND_NAME_% --help
28+
29+
params:
30+
<path> path to the git repository
31+
<commitRev> git revision as supported by go-git
32+
33+
options:
34+
(no options) lists the best common ancestors of the two passed commits
35+
--independent list commits not reachable from the others
36+
--is-ancestor is the first one ancestor of the other?
37+
--help show the full help message of %_COMMAND_NAME_%
38+
`
39+
)
40+
41+
// Command that mimics `git merge-base --all <baseRev> <headRev>`
42+
// Command that mimics `git merge-base --is-ancestor <baseRev> <headRev>`
43+
// Command that mimics `git merge-base --independent <commitRev>...`
44+
func main() {
45+
if len(os.Args) == 1 {
46+
helpAndExit("Returns the merge-base between two commits:", helpShortMsg, exitCodeSuccess)
47+
}
48+
49+
if os.Args[1] == "--help" || os.Args[1] == "-h" {
50+
helpAndExit("Returns the merge-base between two commits:", helpLongMsg, exitCodeSuccess)
51+
}
52+
53+
if len(os.Args) < 4 {
54+
helpAndExit("Wrong syntax", helpShortMsg, exitCodeWrongSyntax)
55+
}
56+
57+
path := os.Args[1]
58+
59+
var modeIndependent, modeAncestor bool
60+
var commitRevs []string
61+
var res []*object.Commit
62+
63+
switch os.Args[2] {
64+
case "--independent":
65+
modeIndependent = true
66+
commitRevs = os.Args[3:]
67+
case "--is-ancestor":
68+
modeAncestor = true
69+
commitRevs = os.Args[3:]
70+
if len(commitRevs) != 2 {
71+
helpAndExit("Wrong syntax", helpShortMsg, exitCodeWrongSyntax)
72+
}
73+
default:
74+
commitRevs = os.Args[2:]
75+
if len(commitRevs) != 2 {
76+
helpAndExit("Wrong syntax", helpShortMsg, exitCodeWrongSyntax)
77+
}
78+
}
79+
80+
// Open a git repository from current directory
81+
repo, err := git.PlainOpen(path)
82+
checkIfError(err, exitCodeCouldNotOpenRepository, "not in a git repository")
83+
84+
// Get the hashes of the passed revisions
85+
var hashes []*plumbing.Hash
86+
for _, rev := range commitRevs {
87+
hash, err := repo.ResolveRevision(plumbing.Revision(rev))
88+
checkIfError(err, exitCodeCouldNotParseRevision, "could not parse revision '%s'", rev)
89+
hashes = append(hashes, hash)
90+
}
91+
92+
// Get the commits identified by the passed hashes
93+
var commits []*object.Commit
94+
for _, hash := range hashes {
95+
commit, err := repo.CommitObject(*hash)
96+
checkIfError(err, exitCodeUnexpected, "could not find commit '%s'", hash.String())
97+
commits = append(commits, commit)
98+
}
99+
100+
if modeAncestor {
101+
isAncestor, err := commits[0].IsAncestor(commits[1])
102+
checkIfError(err, exitCodeUnexpected, "could not traverse the repository history")
103+
104+
if !isAncestor {
105+
os.Exit(int(exitCodeNotFound))
106+
}
107+
108+
os.Exit(int(exitCodeSuccess))
109+
}
110+
111+
if modeIndependent {
112+
res, err = object.Independents(commits)
113+
checkIfError(err, exitCodeUnexpected, "could not traverse the repository history")
114+
} else {
115+
res, err = commits[0].MergeBase(commits[1])
116+
checkIfError(err, exitCodeUnexpected, "could not traverse the repository history")
117+
118+
if len(res) == 0 {
119+
os.Exit(int(exitCodeNotFound))
120+
}
121+
}
122+
123+
printCommits(res)
124+
}

0 commit comments

Comments
 (0)