aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMichael Muré <batolettre@gmail.com>2018-07-17 01:52:56 +0200
committerMichael Muré <batolettre@gmail.com>2018-07-17 01:52:56 +0200
commit0180b68cb0bb3aecf9b4a6186094a084762b4a25 (patch)
tree95597554cfdda76b9f6cc5aab7c3fd5b24a3db75
parent1d678dfdfa026968dbb19795c9bed16385603b21 (diff)
downloadgit-bug-0180b68cb0bb3aecf9b4a6186094a084762b4a25.tar.gz
implement pull/merge
-rw-r--r--bug/bug.go107
-rw-r--r--bug/operation_pack.go18
-rw-r--r--commands/ls.go6
-rw-r--r--commands/new.go3
-rw-r--r--commands/pull.go61
-rw-r--r--notes2
-rw-r--r--repository/git.go50
-rw-r--r--repository/mock_repo.go26
-rw-r--r--repository/repo.go16
-rw-r--r--tests/bug_test.go2
10 files changed, 269 insertions, 22 deletions
diff --git a/bug/bug.go b/bug/bug.go
index a82ee371..c5268cc2 100644
--- a/bug/bug.go
+++ b/bug/bug.go
@@ -56,7 +56,7 @@ func NewBug() (*Bug, error) {
// Find an existing Bug matching a prefix
func FindBug(repo repository.Repo, prefix string) (*Bug, error) {
- refs, err := repo.ListRefs(BugsRefPattern)
+ ids, err := repo.ListRefs(BugsRefPattern)
if err != nil {
return nil, err
@@ -65,9 +65,9 @@ func FindBug(repo repository.Repo, prefix string) (*Bug, error) {
// preallocate but empty
matching := make([]string, 0, 5)
- for _, ref := range refs {
- if strings.HasPrefix(ref, prefix) {
- matching = append(matching, ref)
+ for _, id := range ids {
+ if strings.HasPrefix(id, prefix) {
+ matching = append(matching, id)
}
}
@@ -79,21 +79,25 @@ func FindBug(repo repository.Repo, prefix string) (*Bug, error) {
return nil, fmt.Errorf("Multiple matching bug found:\n%s", strings.Join(matching, "\n"))
}
- return ReadBug(repo, matching[0])
+ return ReadBug(repo, BugsRefPattern+matching[0])
}
// Read and parse a Bug from git
-func ReadBug(repo repository.Repo, id string) (*Bug, error) {
- hashes, err := repo.ListCommits(BugsRefPattern + id)
+func ReadBug(repo repository.Repo, ref string) (*Bug, error) {
+ hashes, err := repo.ListCommits(ref)
if err != nil {
return nil, err
}
+ refSplitted := strings.Split(ref, "/")
+ id := refSplitted[len(refSplitted)-1]
+
bug := Bug{
id: id,
}
+ // Load each OperationPack
for _, hash := range hashes {
entries, err := repo.ListEntries(hash)
@@ -144,6 +148,13 @@ func ReadBug(repo repository.Repo, id string) (*Bug, error) {
return nil, err
}
+ // tag the pack with the commit hash
+ op.commitHash = hash
+
+ if err != nil {
+ return nil, err
+ }
+
bug.packs = append(bug.packs, *op)
}
@@ -251,14 +262,96 @@ func (bug *Bug) Commit(repo repository.Repo) error {
return nil
}
+// Merge a different version of the same bug by rebasing operations of this bug
+// that are not present in the other on top of the chain of operations of the
+// other version.
+func (bug *Bug) Merge(repo repository.Repo, other *Bug) (bool, error) {
+
+ if bug.id != other.id {
+ return false, errors.New("merging unrelated bugs is not supported")
+ }
+
+ if len(other.staging.Operations) > 0 {
+ return false, errors.New("merging a bug with a non-empty staging is not supported")
+ }
+
+ if bug.lastCommit == "" || other.lastCommit == "" {
+ return false, errors.New("can't merge a bug that has never been stored")
+ }
+
+ ancestor, err := repo.FindCommonAncestor(bug.lastCommit, other.lastCommit)
+
+ if err != nil {
+ return false, err
+ }
+
+ rebaseStarted := false
+ updated := false
+
+ for i, pack := range bug.packs {
+ if pack.commitHash == ancestor {
+ rebaseStarted = true
+
+ // get other bug's extra pack
+ for j := i + 1; j < len(other.packs); j++ {
+ // clone is probably not necessary
+ newPack := other.packs[j].Clone()
+
+ bug.packs = append(bug.packs, newPack)
+ bug.lastCommit = newPack.commitHash
+ updated = true
+ }
+
+ continue
+ }
+
+ if !rebaseStarted {
+ continue
+ }
+
+ updated = true
+
+ // get the referenced git tree
+ treeHash, err := repo.GetTreeHash(pack.commitHash)
+
+ if err != nil {
+ return false, err
+ }
+
+ // create a new commit with the correct ancestor
+ hash, err := repo.StoreCommitWithParent(treeHash, bug.lastCommit)
+
+ // replace the pack
+ bug.packs[i] = pack.Clone()
+ bug.packs[i].commitHash = hash
+
+ // update the bug
+ bug.lastCommit = hash
+ }
+
+ // Update the git ref
+ if updated {
+ err := repo.UpdateRef(BugsRefPattern+bug.id, bug.lastCommit)
+ if err != nil {
+ return false, err
+ }
+ }
+
+ return updated, nil
+}
+
+// Return the Bug identifier
func (bug *Bug) Id() string {
return bug.id
}
+// Return the Bug identifier truncated for human consumption
func (bug *Bug) HumanId() string {
return fmt.Sprintf("%.8s", bug.id)
}
+// Lookup for the very first operation of the bug.
+// For a valid Bug, this operation should be a CREATE
func (bug *Bug) firstOp() Operation {
for _, pack := range bug.packs {
for _, op := range pack.Operations {
diff --git a/bug/operation_pack.go b/bug/operation_pack.go
index 60016474..cce1845c 100644
--- a/bug/operation_pack.go
+++ b/bug/operation_pack.go
@@ -15,6 +15,9 @@ import (
// apply to get the final state of the Bug
type OperationPack struct {
Operations []Operation
+
+ // Private field so not serialized by gob
+ commitHash util.Hash
}
func ParseOperationPack(data []byte) (*OperationPack, error) {
@@ -73,3 +76,18 @@ func (opp *OperationPack) Write(repo repository.Repo) (util.Hash, error) {
return hash, nil
}
+
+// Make a deep copy
+func (opp *OperationPack) Clone() OperationPack {
+
+ clone := OperationPack{
+ Operations: make([]Operation, len(opp.Operations)),
+ commitHash: opp.commitHash,
+ }
+
+ for i, op := range opp.Operations {
+ clone.Operations[i] = op
+ }
+
+ return clone
+}
diff --git a/commands/ls.go b/commands/ls.go
index dea2bba2..94755b2d 100644
--- a/commands/ls.go
+++ b/commands/ls.go
@@ -7,14 +7,14 @@ import (
)
func runLsBug(repo repository.Repo, args []string) error {
- refs, err := repo.ListRefs(b.BugsRefPattern)
+ ids, err := repo.ListRefs(b.BugsRefPattern)
if err != nil {
return err
}
- for _, ref := range refs {
- bug, err := b.ReadBug(repo, ref)
+ for _, ref := range ids {
+ bug, err := b.ReadBug(repo, b.BugsRefPattern+ref)
if err != nil {
return err
diff --git a/commands/new.go b/commands/new.go
index a2373515..4f6008cf 100644
--- a/commands/new.go
+++ b/commands/new.go
@@ -3,6 +3,7 @@ package commands
import (
"errors"
"flag"
+ "fmt"
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/bug/operations"
"github.com/MichaelMure/git-bug/commands/input"
@@ -60,6 +61,8 @@ func runNewBug(repo repository.Repo, args []string) error {
err = newbug.Commit(repo)
+ fmt.Println(newbug.HumanId())
+
return err
}
diff --git a/commands/pull.go b/commands/pull.go
index eefcc641..15d832fe 100644
--- a/commands/pull.go
+++ b/commands/pull.go
@@ -2,6 +2,7 @@ package commands
import (
"errors"
+ "fmt"
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/repository"
)
@@ -16,9 +17,67 @@ func runPull(repo repository.Repo, args []string) error {
remote = args[0]
}
- if err := repo.PullRefs(remote, bug.BugsRefPattern+"*", bug.BugsRemoteRefPattern+"*"); err != nil {
+ fmt.Printf("Fetching remote ...\n\n")
+
+ if err := repo.FetchRefs(remote, bug.BugsRefPattern+"*", bug.BugsRemoteRefPattern+"*"); err != nil {
+ return err
+ }
+
+ fmt.Printf("\nMerging data ...\n\n")
+
+ remoteRefSpec := fmt.Sprintf(bug.BugsRemoteRefPattern, remote)
+ remoteRefs, err := repo.ListRefs(remoteRefSpec)
+
+ if err != nil {
return err
}
+
+ for _, ref := range remoteRefs {
+ remoteRef := fmt.Sprintf(bug.BugsRemoteRefPattern, remote) + ref
+ remoteBug, err := bug.ReadBug(repo, remoteRef)
+
+ if err != nil {
+ return err
+ }
+
+ // Check for error in remote data
+ if !remoteBug.IsValid() {
+ fmt.Printf("%s: %s\n", remoteBug.HumanId(), "invalid remote data")
+ continue
+ }
+
+ localRef := bug.BugsRefPattern + remoteBug.Id()
+ localExist, err := repo.RefExist(localRef)
+
+ // the bug is not local yet, simply create the reference
+ if !localExist {
+ err := repo.CopyRef(remoteRef, localRef)
+
+ if err != nil {
+ return err
+ }
+
+ fmt.Printf("%s: %s\n", remoteBug.HumanId(), "new")
+ continue
+ }
+
+ localBug, err := bug.ReadBug(repo, localRef)
+
+ if err != nil {
+ return err
+ }
+
+ updated, err := localBug.Merge(repo, remoteBug)
+
+ if err != nil {
+ return err
+ }
+
+ if updated {
+ fmt.Printf("%s: %s\n", remoteBug.HumanId(), "updated")
+ }
+ }
+
return nil
}
diff --git a/notes b/notes
index 0bdccee3..356a9a63 100644
--- a/notes
+++ b/notes
@@ -23,6 +23,8 @@ git show-ref --hash refs/bugs/4ef19f8a-2e6a-45f7-910e-52e3c639cd86
git for-each-ref --format="%(refname)" "refs/bugs/*"
+-- delete all remote bug refs
+git for-each-ref refs/remote/origin/bugs/* --format="%(refname:lstrip=-1)" | xargs -i git push origin :refs/bugs/{}
Bug operations:
- create bug
diff --git a/repository/git.go b/repository/git.go
index 50806778..8d265325 100644
--- a/repository/git.go
+++ b/repository/git.go
@@ -98,25 +98,21 @@ func (repo *GitRepo) GetCoreEditor() (string, error) {
return repo.runGitCommand("var", "GIT_EDITOR")
}
-// PullRefs pull git refs from a remote
-func (repo *GitRepo) PullRefs(remote, refPattern, remoteRefPattern string) error {
+// FetchRefs fetch git refs from a remote
+func (repo *GitRepo) FetchRefs(remote, refPattern, remoteRefPattern string) error {
remoteRefSpec := fmt.Sprintf(remoteRefPattern, remote)
fetchRefSpec := fmt.Sprintf("%s:%s", refPattern, remoteRefSpec)
err := repo.runGitCommandInline("fetch", remote, fetchRefSpec)
if err != nil {
- return fmt.Errorf("failed to pull from the remote '%s': %v", remote, err)
+ return fmt.Errorf("failed to fetch from the remote '%s': %v", remote, err)
}
- // TODO: merge new data
-
return err
}
// PushRefs push git refs to a remote
func (repo *GitRepo) PushRefs(remote string, refPattern string) error {
- // The push is liable to fail if the user forgot to do a pull first, so
- // we treat errors as user errors rather than fatal errors.
err := repo.runGitCommandInline("push", remote, refPattern)
if err != nil {
@@ -209,6 +205,24 @@ func (repo *GitRepo) ListRefs(refspec string) ([]string, error) {
return splitted, nil
}
+// RefExist will check if a reference exist in Git
+func (repo *GitRepo) RefExist(ref string) (bool, error) {
+ stdout, err := repo.runGitCommand("for-each-ref", ref)
+
+ if err != nil {
+ return false, err
+ }
+
+ return stdout != "", nil
+}
+
+// CopyRef will create a new reference with the same value as another one
+func (repo *GitRepo) CopyRef(source string, dest string) error {
+ _, err := repo.runGitCommand("update-ref", dest, source)
+
+ return err
+}
+
// ListCommits will return the list of commit hashes of a ref, in chronological order
func (repo *GitRepo) ListCommits(ref string) ([]util.Hash, error) {
stdout, err := repo.runGitCommand("rev-list", "--first-parent", "--reverse", ref)
@@ -238,3 +252,25 @@ func (repo *GitRepo) ListEntries(hash util.Hash) ([]TreeEntry, error) {
return readTreeEntries(stdout)
}
+
+// FindCommonAncestor will return the last common ancestor of two chain of commit
+func (repo *GitRepo) FindCommonAncestor(hash1 util.Hash, hash2 util.Hash) (util.Hash, error) {
+ stdout, err := repo.runGitCommand("merge-base", string(hash1), string(hash2))
+
+ if err != nil {
+ return "", nil
+ }
+
+ return util.Hash(stdout), nil
+}
+
+// Return the git tree hash referenced in a commit
+func (repo *GitRepo) GetTreeHash(commit util.Hash) (util.Hash, error) {
+ stdout, err := repo.runGitCommand("rev-parse", string(commit)+"^{tree}")
+
+ if err != nil {
+ return "", nil
+ }
+
+ return util.Hash(stdout), nil
+}
diff --git a/repository/mock_repo.go b/repository/mock_repo.go
index f526c3dc..1ac2c6e7 100644
--- a/repository/mock_repo.go
+++ b/repository/mock_repo.go
@@ -53,7 +53,7 @@ func (r *mockRepoForTest) PushRefs(remote string, refPattern string) error {
return nil
}
-func (r *mockRepoForTest) PullRefs(remote string, refPattern string, remoteRefPattern string) error {
+func (r *mockRepoForTest) FetchRefs(remote string, refPattern string, remoteRefPattern string) error {
return nil
}
@@ -107,6 +107,22 @@ func (r *mockRepoForTest) UpdateRef(ref string, hash util.Hash) error {
return nil
}
+func (r *mockRepoForTest) RefExist(ref string) (bool, error) {
+ _, exist := r.refs[ref]
+ return exist, nil
+}
+
+func (r *mockRepoForTest) CopyRef(source string, dest string) error {
+ hash, exist := r.refs[source]
+
+ if !exist {
+ return errors.New("Unknown ref")
+ }
+
+ r.refs[dest] = hash
+ return nil
+}
+
func (r *mockRepoForTest) ListRefs(refspec string) ([]string, error) {
keys := make([]string, len(r.refs))
@@ -160,3 +176,11 @@ func (r *mockRepoForTest) ListEntries(hash util.Hash) ([]TreeEntry, error) {
return readTreeEntries(data)
}
+
+func (r *mockRepoForTest) FindCommonAncestor(hash1 util.Hash, hash2 util.Hash) (util.Hash, error) {
+ panic("implement me")
+}
+
+func (r *mockRepoForTest) GetTreeHash(commit util.Hash) (util.Hash, error) {
+ panic("implement me")
+}
diff --git a/repository/repo.go b/repository/repo.go
index 26fe0fa6..7ba8a8b5 100644
--- a/repository/repo.go
+++ b/repository/repo.go
@@ -21,8 +21,8 @@ type Repo interface {
// GetCoreEditor returns the name of the editor that the user has used to configure git.
GetCoreEditor() (string, error)
- // PullRefs pull git refs from a remote
- PullRefs(remote string, refPattern string, remoteRefPattern string) error
+ // FetchRefs fetch git refs from a remote
+ FetchRefs(remote string, refPattern string, remoteRefPattern string) error
// PushRefs push git refs to a remote
PushRefs(remote string, refPattern string) error
@@ -48,11 +48,23 @@ type Repo interface {
// ListRefs will return a list of Git ref matching the given refspec
ListRefs(refspec string) ([]string, error)
+ // RefExist will check if a reference exist in Git
+ RefExist(ref string) (bool, error)
+
+ // CopyRef will create a new reference with the same value as another one
+ CopyRef(source string, dest string) error
+
// ListCommits will return the list of tree hashes of a ref, in chronological order
ListCommits(ref string) ([]util.Hash, error)
// ListEntries will return the list of entries in a Git tree
ListEntries(hash util.Hash) ([]TreeEntry, error)
+
+ // FindCommonAncestor will return the last common ancestor of two chain of commit
+ FindCommonAncestor(hash1 util.Hash, hash2 util.Hash) (util.Hash, error)
+
+ // Return the git tree hash referenced in a commit
+ GetTreeHash(commit util.Hash) (util.Hash, error)
}
func prepareTreeEntries(entries []TreeEntry) bytes.Buffer {
diff --git a/tests/bug_test.go b/tests/bug_test.go
index 62166723..366b56b6 100644
--- a/tests/bug_test.go
+++ b/tests/bug_test.go
@@ -62,7 +62,7 @@ func TestBugSerialisation(t *testing.T) {
bug1.Commit(repo)
- bug2, err := bug.ReadBug(repo, bug1.Id())
+ bug2, err := bug.ReadBug(repo, bug.BugsRefPattern+bug1.Id())
if err != nil {
t.Error(err)
}