implement pull/merge

Michael Muré created

Change summary

bug/bug.go              | 107 ++++++++++++++++++++++++++++++++++++++++--
bug/operation_pack.go   |  18 +++++++
commands/ls.go          |   6 +-
commands/new.go         |   3 +
commands/pull.go        |  61 ++++++++++++++++++++++++
notes                   |   2 
repository/git.go       |  50 +++++++++++++++++--
repository/mock_repo.go |  26 ++++++++++
repository/repo.go      |  16 +++++
tests/bug_test.go       |   2 
10 files changed, 269 insertions(+), 22 deletions(-)

Detailed changes

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 {

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
+}

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

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
 
 }

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
 }
 

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

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
+}

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")
+}

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 {

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)
 	}