implement pull/merge

This commit is contained in:
Michael Muré 2018-07-17 01:52:56 +02:00
parent 1d678dfdfa
commit 0180b68cb0
No known key found for this signature in database
GPG Key ID: A4457C029293126F
10 changed files with 269 additions and 22 deletions

View File

@ -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 {

View File

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

View File

@ -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

View File

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

View File

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

2
notes
View File

@ -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

View File

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

View File

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

View File

@ -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 {

View File

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