mirror of
https://github.com/MichaelMure/git-bug.git
synced 2024-12-15 02:01:43 +03:00
implement pull/merge
This commit is contained in:
parent
1d678dfdfa
commit
0180b68cb0
107
bug/bug.go
107
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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -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
2
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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user