aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--COMPATIBILITY.md2
-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
-rw-r--r--plumbing/format/packfile/diff_delta.go13
-rw-r--r--plumbing/format/packfile/packfile.go8
-rw-r--r--worktree.go2
8 files changed, 267 insertions, 19 deletions
diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md
index e07e799..4a3da62 100644
--- a/COMPATIBILITY.md
+++ b/COMPATIBILITY.md
@@ -86,7 +86,7 @@ is supported by go-git.
| for-each-ref | ✔ |
| hash-object | ✔ |
| ls-files | ✔ |
-| merge-base | |
+| 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. |
| read-tree | |
| rev-list | ✔ |
| rev-parse | |
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)
+}
diff --git a/plumbing/format/packfile/diff_delta.go b/plumbing/format/packfile/diff_delta.go
index d35e78a..43f87a0 100644
--- a/plumbing/format/packfile/diff_delta.go
+++ b/plumbing/format/packfile/diff_delta.go
@@ -40,8 +40,8 @@ func getDelta(index *deltaIndex, base, target plumbing.EncodedObject) (plumbing.
defer tr.Close()
bb := bufPool.Get().(*bytes.Buffer)
- bb.Reset()
defer bufPool.Put(bb)
+ bb.Reset()
_, err = bb.ReadFrom(br)
if err != nil {
@@ -49,8 +49,8 @@ func getDelta(index *deltaIndex, base, target plumbing.EncodedObject) (plumbing.
}
tb := bufPool.Get().(*bytes.Buffer)
- tb.Reset()
defer bufPool.Put(tb)
+ tb.Reset()
_, err = tb.ReadFrom(tr)
if err != nil {
@@ -77,6 +77,7 @@ func DiffDelta(src, tgt []byte) []byte {
func diffDelta(index *deltaIndex, src []byte, tgt []byte) []byte {
buf := bufPool.Get().(*bytes.Buffer)
+ defer bufPool.Put(buf)
buf.Reset()
buf.Write(deltaEncodeSize(len(src)))
buf.Write(deltaEncodeSize(len(tgt)))
@@ -86,6 +87,7 @@ func diffDelta(index *deltaIndex, src []byte, tgt []byte) []byte {
}
ibuf := bufPool.Get().(*bytes.Buffer)
+ defer bufPool.Put(ibuf)
ibuf.Reset()
for i := 0; i < len(tgt); i++ {
offset, l := index.findMatch(src, tgt, i)
@@ -127,12 +129,9 @@ func diffDelta(index *deltaIndex, src []byte, tgt []byte) []byte {
}
encodeInsertOperation(ibuf, buf)
- bytes := buf.Bytes()
-
- bufPool.Put(buf)
- bufPool.Put(ibuf)
- return bytes
+ // buf.Bytes() is only valid until the next modifying operation on the buffer. Copy it.
+ return append([]byte{}, buf.Bytes()...)
}
func encodeInsertOperation(ibuf, buf *bytes.Buffer) {
diff --git a/plumbing/format/packfile/packfile.go b/plumbing/format/packfile/packfile.go
index f528073..21a15de 100644
--- a/plumbing/format/packfile/packfile.go
+++ b/plumbing/format/packfile/packfile.go
@@ -133,8 +133,8 @@ func (p *Packfile) getObjectSize(h *ObjectHeader) (int64, error) {
return h.Length, nil
case plumbing.REFDeltaObject, plumbing.OFSDeltaObject:
buf := bufPool.Get().(*bytes.Buffer)
- buf.Reset()
defer bufPool.Put(buf)
+ buf.Reset()
if _, _, err := p.s.NextObject(buf); err != nil {
return 0, err
@@ -222,11 +222,11 @@ func (p *Packfile) getNextObject(h *ObjectHeader, hash plumbing.Hash) (plumbing.
// optimization only if the expanded version of the object still meets
// the small object threshold condition.
buf := bufPool.Get().(*bytes.Buffer)
+ defer bufPool.Put(buf)
buf.Reset()
if _, _, err := p.s.NextObject(buf); err != nil {
return nil, err
}
- defer bufPool.Put(buf)
size = p.getDeltaObjectSize(buf)
if size <= smallObjectThreshold {
@@ -321,12 +321,12 @@ func (p *Packfile) fillRegularObjectContent(obj plumbing.EncodedObject) error {
func (p *Packfile) fillREFDeltaObjectContent(obj plumbing.EncodedObject, ref plumbing.Hash) error {
buf := bufPool.Get().(*bytes.Buffer)
+ defer bufPool.Put(buf)
buf.Reset()
_, _, err := p.s.NextObject(buf)
if err != nil {
return err
}
- defer bufPool.Put(buf)
return p.fillREFDeltaObjectContentWithBuffer(obj, ref, buf)
}
@@ -351,12 +351,12 @@ func (p *Packfile) fillREFDeltaObjectContentWithBuffer(obj plumbing.EncodedObjec
func (p *Packfile) fillOFSDeltaObjectContent(obj plumbing.EncodedObject, offset int64) error {
buf := bufPool.Get().(*bytes.Buffer)
+ defer bufPool.Put(buf)
buf.Reset()
_, _, err := p.s.NextObject(buf)
if err != nil {
return err
}
- defer bufPool.Put(buf)
return p.fillOFSDeltaObjectContentWithBuffer(obj, offset, buf)
}
diff --git a/worktree.go b/worktree.go
index 1b10449..576ae0a 100644
--- a/worktree.go
+++ b/worktree.go
@@ -722,7 +722,7 @@ func (w *Worktree) Clean(opts *CleanOptions) error {
func (w *Worktree) doClean(status Status, opts *CleanOptions, dir string, files []os.FileInfo) error {
for _, fi := range files {
- if fi.Name() == ".git" {
+ if fi.Name() == GitDirName {
continue
}