aboutsummaryrefslogtreecommitdiffstats
path: root/_examples
diff options
context:
space:
mode:
authorDavid Pordomingo <David.Pordomingo.F@gmail.com>2019-03-25 19:48:19 +0100
committerDavid Pordomingo <David.Pordomingo.F@gmail.com>2019-06-03 20:58:26 +0200
commit66c4a36212ced976c33712ca4fb6abc6697f2654 (patch)
treeb65d063fc01b32497c57d0b23f4082a5b2235bf2 /_examples
parent37b80726760d2e0b17dfa437f3162dd930590ecf (diff)
downloadgo-git-66c4a36212ced976c33712ca4fb6abc6697f2654.tar.gz
Add merge-base command
Signed-off-by: David Pordomingo <David.Pordomingo.F@gmail.com>
Diffstat (limited to '_examples')
-rw-r--r--_examples/common_test.go13
-rw-r--r--_examples/merge_base/help-long.msg.go63
-rw-r--r--_examples/merge_base/helpers.go61
-rw-r--r--_examples/merge_base/main.go124
4 files changed, 255 insertions, 6 deletions
diff --git a/_examples/common_test.go b/_examples/common_test.go
index 47463a1..89d49a3 100644
--- a/_examples/common_test.go
+++ b/_examples/common_test.go
@@ -29,6 +29,7 @@ var args = map[string][]string{
"tag": {cloneRepository(defaultURL, tempFolder())},
"pull": {createRepositoryWithRemote(tempFolder(), defaultURL)},
"ls": {cloneRepository(defaultURL, tempFolder()), "HEAD", "vendor"},
+ "merge_base": {cloneRepository(defaultURL, tempFolder()), "--is-ancestor", "HEAD~3", "HEAD^"},
}
var ignored = map[string]bool{}
@@ -50,14 +51,15 @@ func TestExamples(t *testing.T) {
}
for _, example := range examples {
- _, name := filepath.Split(filepath.Dir(example))
+ dir := filepath.Dir(example)
+ _, name := filepath.Split(dir)
if ignored[name] {
continue
}
t.Run(name, func(t *testing.T) {
- testExample(t, name, example)
+ testExample(t, name, dir)
})
}
}
@@ -135,10 +137,9 @@ func addRemote(local, remote string) {
CheckIfError(err)
}
-func testExample(t *testing.T, name, example string) {
- cmd := exec.Command("go", append([]string{
- "run", filepath.Join(example),
- }, args[name]...)...)
+func testExample(t *testing.T, name, dir string) {
+ arguments := append([]string{"run", dir}, args[name]...)
+ cmd := exec.Command("go", arguments...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
diff --git a/_examples/merge_base/help-long.msg.go b/_examples/merge_base/help-long.msg.go
new file mode 100644
index 0000000..7759cbd
--- /dev/null
+++ b/_examples/merge_base/help-long.msg.go
@@ -0,0 +1,63 @@
+package main
+
+const helpLongMsg = `
+NAME:
+ %_COMMAND_NAME_% - Lists the best common ancestors of the two passed commit revisions
+
+SYNOPSIS:
+ usage: %_COMMAND_NAME_% <path> <commitRev> <commitRev>
+ or: %_COMMAND_NAME_% <path> --independent <commitRev>...
+ or: %_COMMAND_NAME_% <path> --is-ancestor <commitRev> <commitRev>
+
+ params:
+ <path> Path to the git repository
+ <commitRev> Git revision as supported by go-git
+
+DESCRIPTION:
+ %_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.
+ 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.
+ Commits that does not share a common history has no common ancestors.
+
+OPTIONS:
+ As the most common special case, specifying only two commits on the command line means computing the merge base between the given two commits.
+ 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.
+
+--independent
+ 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.
+
+--is-ancestor
+ 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.
+
+DISCUSSION:
+ 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.
+
+ For example, with this topology:
+
+ o---o---o---o---B
+ / /
+ ---3---2---o---1---o---A
+
+ the merge base between A and B is 1.
+
+ 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.
+
+ When the history involves cross-cross merges, there can be more than one best common ancestor for two commits. For example, with this topology:
+
+ ---1---o---A
+ \ /
+ X
+ / \
+ ---2---o---o---B
+
+ When the history involves feature branches depending on other feature branches there can be also more than one common ancestor. For example:
+
+
+ o---o---o
+ / \
+ 1---o---A \
+ / / \
+ ---o---o---2---o---o---B
+
+ In both examples, both 1 and 2 are merge-bases of A and B for each situation.
+ Neither one is better than the other (both are best merge bases) because 1 cannot be reached from 2, nor the opposite.
+`
diff --git a/_examples/merge_base/helpers.go b/_examples/merge_base/helpers.go
new file mode 100644
index 0000000..b7b1ed6
--- /dev/null
+++ b/_examples/merge_base/helpers.go
@@ -0,0 +1,61 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "gopkg.in/src-d/go-git.v4/plumbing/object"
+)
+
+func checkIfError(err error, code exitCode, mainReason string, v ...interface{}) {
+ if err == nil {
+ return
+ }
+
+ printErr(wrappErr(err, mainReason, v...))
+ os.Exit(int(code))
+}
+
+func helpAndExit(s string, helpMsg string, code exitCode) {
+ if code == exitCodeSuccess {
+ printMsg("%s", s)
+ } else {
+ printErr(fmt.Errorf(s))
+ }
+
+ fmt.Println(strings.Replace(helpMsg, "%_COMMAND_NAME_%", os.Args[0], -1))
+
+ os.Exit(int(code))
+}
+
+func printErr(err error) {
+ fmt.Printf("\x1b[31;1m%s\x1b[0m\n", fmt.Sprintf("error: %s", err))
+}
+
+func printMsg(format string, args ...interface{}) {
+ fmt.Printf("%s\n", fmt.Sprintf(format, args...))
+}
+
+func printCommits(commits []*object.Commit) {
+ for _, commit := range commits {
+ if os.Getenv("LOG_LEVEL") == "verbose" {
+ fmt.Printf(
+ "\x1b[36;1m%s \x1b[90;21m%s\x1b[0m %s\n",
+ commit.Hash.String()[:7],
+ commit.Hash.String(),
+ strings.Split(commit.Message, "\n")[0],
+ )
+ } else {
+ fmt.Println(commit.Hash.String())
+ }
+ }
+}
+
+func wrappErr(err error, s string, v ...interface{}) error {
+ if err != nil {
+ return fmt.Errorf("%s\n %s", fmt.Sprintf(s, v...), err)
+ }
+
+ return nil
+}
diff --git a/_examples/merge_base/main.go b/_examples/merge_base/main.go
new file mode 100644
index 0000000..fe6abc6
--- /dev/null
+++ b/_examples/merge_base/main.go
@@ -0,0 +1,124 @@
+package main
+
+import (
+ "os"
+
+ "gopkg.in/src-d/go-git.v4"
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/object"
+)
+
+type exitCode int
+
+const (
+ exitCodeSuccess exitCode = iota
+ exitCodeNotFound
+ exitCodeWrongSyntax
+ exitCodeCouldNotOpenRepository
+ exitCodeCouldNotParseRevision
+ exitCodeUnexpected
+
+ cmdDesc = "Returns the merge-base between two commits:"
+
+ helpShortMsg = `
+ usage: %_COMMAND_NAME_% <path> <commitRev> <commitRev>
+ or: %_COMMAND_NAME_% <path> --independent <commitRev>...
+ or: %_COMMAND_NAME_% <path> --is-ancestor <commitRev> <commitRev>
+ or: %_COMMAND_NAME_% --help
+
+ params:
+ <path> path to the git repository
+ <commitRev> git revision as supported by go-git
+
+options:
+ (no options) lists the best common ancestors of the two passed commits
+ --independent list commits not reachable from the others
+ --is-ancestor is the first one ancestor of the other?
+ --help show the full help message of %_COMMAND_NAME_%
+`
+)
+
+// Command that mimics `git merge-base --all <baseRev> <headRev>`
+// Command that mimics `git merge-base --is-ancestor <baseRev> <headRev>`
+// Command that mimics `git merge-base --independent <commitRev>...`
+func main() {
+ if len(os.Args) == 1 {
+ helpAndExit("Returns the merge-base between two commits:", helpShortMsg, exitCodeSuccess)
+ }
+
+ if os.Args[1] == "--help" || os.Args[1] == "-h" {
+ helpAndExit("Returns the merge-base between two commits:", helpLongMsg, exitCodeSuccess)
+ }
+
+ if len(os.Args) < 4 {
+ helpAndExit("Wrong syntax", helpShortMsg, exitCodeWrongSyntax)
+ }
+
+ path := os.Args[1]
+
+ var modeIndependent, modeAncestor bool
+ var commitRevs []string
+ var res []*object.Commit
+
+ switch os.Args[2] {
+ case "--independent":
+ modeIndependent = true
+ commitRevs = os.Args[3:]
+ case "--is-ancestor":
+ modeAncestor = true
+ commitRevs = os.Args[3:]
+ if len(commitRevs) != 2 {
+ helpAndExit("Wrong syntax", helpShortMsg, exitCodeWrongSyntax)
+ }
+ default:
+ commitRevs = os.Args[2:]
+ if len(commitRevs) != 2 {
+ helpAndExit("Wrong syntax", helpShortMsg, exitCodeWrongSyntax)
+ }
+ }
+
+ // Open a git repository from current directory
+ repo, err := git.PlainOpen(path)
+ checkIfError(err, exitCodeCouldNotOpenRepository, "not in a git repository")
+
+ // Get the hashes of the passed revisions
+ var hashes []*plumbing.Hash
+ for _, rev := range commitRevs {
+ hash, err := repo.ResolveRevision(plumbing.Revision(rev))
+ checkIfError(err, exitCodeCouldNotParseRevision, "could not parse revision '%s'", rev)
+ hashes = append(hashes, hash)
+ }
+
+ // Get the commits identified by the passed hashes
+ var commits []*object.Commit
+ for _, hash := range hashes {
+ commit, err := repo.CommitObject(*hash)
+ checkIfError(err, exitCodeUnexpected, "could not find commit '%s'", hash.String())
+ commits = append(commits, commit)
+ }
+
+ if modeAncestor {
+ isAncestor, err := commits[0].IsAncestor(commits[1])
+ checkIfError(err, exitCodeUnexpected, "could not traverse the repository history")
+
+ if !isAncestor {
+ os.Exit(int(exitCodeNotFound))
+ }
+
+ os.Exit(int(exitCodeSuccess))
+ }
+
+ if modeIndependent {
+ res, err = object.Independents(commits)
+ checkIfError(err, exitCodeUnexpected, "could not traverse the repository history")
+ } else {
+ res, err = commits[0].MergeBase(commits[1])
+ checkIfError(err, exitCodeUnexpected, "could not traverse the repository history")
+
+ if len(res) == 0 {
+ os.Exit(int(exitCodeNotFound))
+ }
+ }
+
+ printCommits(res)
+}