bug_actions.go

  1package bug
  2
  3import (
  4	"fmt"
  5	"strings"
  6
  7	"github.com/pkg/errors"
  8
  9	"github.com/MichaelMure/git-bug/entity"
 10	"github.com/MichaelMure/git-bug/identity"
 11	"github.com/MichaelMure/git-bug/repository"
 12)
 13
 14// Fetch retrieve updates from a remote
 15// This does not change the local bugs state
 16func Fetch(repo repository.Repo, remote string) (string, error) {
 17	return repo.FetchRefs(remote, "bugs")
 18}
 19
 20// Push update a remote with the local changes
 21func Push(repo repository.Repo, remote string) (string, error) {
 22	return repo.PushRefs(remote, "bugs")
 23}
 24
 25// Pull will do a Fetch + MergeAll
 26// This function will return an error if a merge fail
 27func Pull(repo repository.ClockedRepo, remote string) error {
 28	_, err := Fetch(repo, remote)
 29	if err != nil {
 30		return err
 31	}
 32
 33	for merge := range MergeAll(repo, remote) {
 34		if merge.Err != nil {
 35			return merge.Err
 36		}
 37		if merge.Status == entity.MergeStatusInvalid {
 38			return errors.Errorf("merge failure: %s", merge.Reason)
 39		}
 40	}
 41
 42	return nil
 43}
 44
 45// MergeAll will merge all the available remote bug:
 46//
 47// - If the remote has new commit, the local bug is updated to match the same history
 48//   (fast-forward update)
 49// - if the local bug has new commits but the remote don't, nothing is changed
 50// - if both local and remote bug have new commits (that is, we have a concurrent edition),
 51//   new local commits are rewritten at the head of the remote history (that is, a rebase)
 52func MergeAll(repo repository.ClockedRepo, remote string) <-chan entity.MergeResult {
 53	out := make(chan entity.MergeResult)
 54
 55	// no caching for the merge, we load everything from git even if that means multiple
 56	// copy of the same entity in memory. The cache layer will intercept the results to
 57	// invalidate entities if necessary.
 58	identityResolver := identity.NewSimpleResolver(repo)
 59
 60	go func() {
 61		defer close(out)
 62
 63		remoteRefSpec := fmt.Sprintf(bugsRemoteRefPattern, remote)
 64		remoteRefs, err := repo.ListRefs(remoteRefSpec)
 65		if err != nil {
 66			out <- entity.MergeResult{Err: err}
 67			return
 68		}
 69
 70		for _, remoteRef := range remoteRefs {
 71			refSplit := strings.Split(remoteRef, "/")
 72			id := entity.Id(refSplit[len(refSplit)-1])
 73
 74			if err := id.Validate(); err != nil {
 75				out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "invalid ref").Error())
 76				continue
 77			}
 78
 79			remoteBug, err := read(repo, identityResolver, remoteRef)
 80
 81			if err != nil {
 82				out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "remote bug is not readable").Error())
 83				continue
 84			}
 85
 86			// Check for error in remote data
 87			if err := remoteBug.Validate(); err != nil {
 88				out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "remote bug is invalid").Error())
 89				continue
 90			}
 91
 92			localRef := bugsRefPattern + remoteBug.Id().String()
 93			localExist, err := repo.RefExist(localRef)
 94
 95			if err != nil {
 96				out <- entity.NewMergeError(err, id)
 97				continue
 98			}
 99
100			// the bug is not local yet, simply create the reference
101			if !localExist {
102				err := repo.CopyRef(remoteRef, localRef)
103
104				if err != nil {
105					out <- entity.NewMergeError(err, id)
106					return
107				}
108
109				out <- entity.NewMergeNewStatus(id, remoteBug)
110				continue
111			}
112
113			localBug, err := read(repo, identityResolver, localRef)
114
115			if err != nil {
116				out <- entity.NewMergeError(errors.Wrap(err, "local bug is not readable"), id)
117				return
118			}
119
120			updated, err := localBug.Merge(repo, remoteBug)
121
122			if err != nil {
123				out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "merge failed").Error())
124				return
125			}
126
127			if updated {
128				out <- entity.NewMergeUpdatedStatus(id, localBug)
129			} else {
130				out <- entity.NewMergeNothingStatus(id)
131			}
132		}
133	}()
134
135	return out
136}