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

Commit 2a340db

Browse files
committed
Add merge-base command
1 parent fee7a62 commit 2a340db

File tree

4 files changed

+237
-0
lines changed

4 files changed

+237
-0
lines changed

_examples/merge_base/help-long.msg.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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_% <commitRev> <commitRev>
9+
or: %_COMMAND_NAME_% --independent <commitRev>...
10+
or: %_COMMAND_NAME_% --is-ancestor <commitRev> <commitRev>
11+
12+
DESCRIPTION:
13+
%_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.
14+
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.
15+
Commits that does not share a common history has no common ancestors.
16+
17+
OPTIONS:
18+
As the most common special case, specifying only two commits on the command line means computing the merge base between the given two commits.
19+
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.
20+
21+
--independent
22+
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.
23+
24+
--is-ancestor
25+
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.
26+
27+
DISCUSSION:
28+
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.
29+
30+
For example, with this topology:
31+
32+
o---o---o---o---B
33+
/ /
34+
---3---2---o---1---o---A
35+
36+
the merge base between A and B is 1.
37+
38+
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.
39+
40+
When the history involves cross-cross merges, there can be more than one best common ancestor for two commits. For example, with this topology:
41+
42+
---1---o---A
43+
\ /
44+
X
45+
/ \
46+
---2---o---o---B
47+
48+
When the history involves feature branches depending on other feature branches there can be also more than one common ancestor. For example:
49+
50+
51+
o---o---o
52+
/ \
53+
1---o---A \
54+
/ / \
55+
---o---o---2---o---o---B
56+
57+
In both examples, both 1 and 2 are merge-bases of A and B for each situation.
58+
Neither one is better than the other (both are best merge bases) because 1 cannot be reached from 2, nor the opposite.
59+
`
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package main
2+
3+
const helpShortMsg = `
4+
usage: %_COMMAND_NAME_% <commitRev> <commitRev>
5+
or: %_COMMAND_NAME_% --independent <commitRev>...
6+
or: %_COMMAND_NAME_% --is-ancestor <commitRev> <commitRev>
7+
or: %_COMMAND_NAME_% --help
8+
9+
(no option) lists the best common ancestors of the two passed commits
10+
--independent list commits not reachable from the others
11+
--is-ancestor is the first one ancestor of the other?
12+
--help show the full help message of %_COMMAND_NAME_%
13+
`

_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: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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+
22+
// Command that mimics `git merge-base --all <baseRev> <headRev>`
23+
// Command that mimics `git merge-base --is-ancestor <baseRev> <headRev>`
24+
// Command that mimics `git merge-base --independent <commitRev>...`
25+
func main() {
26+
if len(os.Args) < 2 {
27+
helpAndExit("Returns the merge-base between two commits:", helpShortMsg, exitCodeSuccess)
28+
}
29+
30+
if os.Args[1] == "--help" || os.Args[1] == "-h" {
31+
helpAndExit("Returns the merge-base between two commits:", helpLongMsg, exitCodeSuccess)
32+
}
33+
34+
if len(os.Args) < 3 {
35+
helpAndExit("Wrong syntax", helpShortMsg, exitCodeWrongSyntax)
36+
}
37+
38+
var modeIndependent, modeAncestor bool
39+
var commitRevs []string
40+
var res []*object.Commit
41+
42+
switch os.Args[1] {
43+
case "--independent":
44+
modeIndependent = true
45+
commitRevs = os.Args[2:]
46+
case "--is-ancestor":
47+
modeAncestor = true
48+
commitRevs = os.Args[2:]
49+
if len(commitRevs) != 2 {
50+
helpAndExit("Wrong syntax", helpShortMsg, exitCodeWrongSyntax)
51+
}
52+
default:
53+
commitRevs = os.Args[1:]
54+
if len(commitRevs) != 2 {
55+
helpAndExit("Wrong syntax", helpShortMsg, exitCodeWrongSyntax)
56+
}
57+
}
58+
59+
// Open a git repository from current directory
60+
repo, err := git.PlainOpen(".")
61+
checkIfError(err, exitCodeCouldNotOpenRepository, "not in a git repository")
62+
63+
// Get the hashes of the passed revisions
64+
var hashes []*plumbing.Hash
65+
for _, rev := range commitRevs {
66+
hash, err := repo.ResolveRevision(plumbing.Revision(rev))
67+
checkIfError(err, exitCodeCouldNotParseRevision, "could not parse revision '%s'", rev)
68+
hashes = append(hashes, hash)
69+
}
70+
71+
// Get the commits identified by the passed hashes
72+
var commits []*object.Commit
73+
for _, hash := range hashes {
74+
commit, err := repo.CommitObject(*hash)
75+
checkIfError(err, exitCodeUnexpected, "could not find commit '%s'", hash.String())
76+
commits = append(commits, commit)
77+
}
78+
79+
if modeAncestor {
80+
isAncestor, err := git.IsAncestor(commits[0], commits[1])
81+
checkIfError(err, exitCodeUnexpected, "could not traverse the repository history")
82+
83+
if !isAncestor {
84+
os.Exit(int(exitCodeNotFound))
85+
}
86+
87+
os.Exit(int(exitCodeSuccess))
88+
}
89+
90+
if modeIndependent {
91+
res, err = git.Independents(commits)
92+
checkIfError(err, exitCodeUnexpected, "could not traverse the repository history")
93+
} else {
94+
res, err = git.MergeBase(commits[0], commits[1])
95+
checkIfError(err, exitCodeUnexpected, "could not traverse the repository history")
96+
97+
if len(res) == 0 {
98+
os.Exit(int(exitCodeNotFound))
99+
}
100+
}
101+
102+
printCommits(res)
103+
}
104+

0 commit comments

Comments
 (0)