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