diff options
author | Michael Muré <batolettre@gmail.com> | 2018-07-17 01:52:56 +0200 |
---|---|---|
committer | Michael Muré <batolettre@gmail.com> | 2018-07-17 01:52:56 +0200 |
commit | 0180b68cb0bb3aecf9b4a6186094a084762b4a25 (patch) | |
tree | 95597554cfdda76b9f6cc5aab7c3fd5b24a3db75 | |
parent | 1d678dfdfa026968dbb19795c9bed16385603b21 (diff) | |
download | git-bug-0180b68cb0bb3aecf9b4a6186094a084762b4a25.tar.gz |
implement pull/merge
-rw-r--r-- | bug/bug.go | 107 | ||||
-rw-r--r-- | bug/operation_pack.go | 18 | ||||
-rw-r--r-- | commands/ls.go | 6 | ||||
-rw-r--r-- | commands/new.go | 3 | ||||
-rw-r--r-- | commands/pull.go | 61 | ||||
-rw-r--r-- | notes | 2 | ||||
-rw-r--r-- | repository/git.go | 50 | ||||
-rw-r--r-- | repository/mock_repo.go | 26 | ||||
-rw-r--r-- | repository/repo.go | 16 | ||||
-rw-r--r-- | tests/bug_test.go | 2 |
10 files changed, 269 insertions, 22 deletions
@@ -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 } @@ -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) } |