From 66c4a36212ced976c33712ca4fb6abc6697f2654 Mon Sep 17 00:00:00 2001 From: David Pordomingo Date: Mon, 25 Mar 2019 19:48:19 +0100 Subject: Add merge-base command Signed-off-by: David Pordomingo --- _examples/common_test.go | 13 ++-- _examples/merge_base/help-long.msg.go | 63 +++++++++++++++++ _examples/merge_base/helpers.go | 61 +++++++++++++++++ _examples/merge_base/main.go | 124 ++++++++++++++++++++++++++++++++++ 4 files changed, 255 insertions(+), 6 deletions(-) create mode 100644 _examples/merge_base/help-long.msg.go create mode 100644 _examples/merge_base/helpers.go create mode 100644 _examples/merge_base/main.go 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_% + or: %_COMMAND_NAME_% --independent ... + or: %_COMMAND_NAME_% --is-ancestor + + params: + Path to the git repository + 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_% + or: %_COMMAND_NAME_% --independent ... + or: %_COMMAND_NAME_% --is-ancestor + or: %_COMMAND_NAME_% --help + + params: + path to the git repository + 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 ` +// Command that mimics `git merge-base --is-ancestor ` +// Command that mimics `git merge-base --independent ...` +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) +} -- cgit