bug_actions.go

  1package bug
  2
  3import (
  4	"fmt"
  5	"strings"
  6
  7	"github.com/MichaelMure/git-bug/repository"
  8	"github.com/pkg/errors"
  9)
 10
 11// Fetch retrieve updates from a remote
 12// This does not change the local bugs state
 13func Fetch(repo repository.Repo, remote string) (string, error) {
 14	remoteRefSpec := fmt.Sprintf(bugsRemoteRefPattern, remote)
 15	fetchRefSpec := fmt.Sprintf("%s*:%s*", bugsRefPattern, remoteRefSpec)
 16
 17	return repo.FetchRefs(remote, fetchRefSpec)
 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, bugsRefPattern+"*")
 23}
 24
 25// Pull will do a Fetch + MergeAll
 26// This function won't give details on the underlying process. If you need more,
 27// use Fetch and MergeAll separately.
 28func Pull(repo repository.ClockedRepo, remote string) error {
 29	_, err := Fetch(repo, remote)
 30	if err != nil {
 31		return err
 32	}
 33
 34	for merge := range MergeAll(repo, remote) {
 35		if merge.Err != nil {
 36			return merge.Err
 37		}
 38		if merge.Status == MergeStatusInvalid {
 39			// Not awesome: simply output the merge failure here as this function
 40			// is only used in tests for now.
 41			fmt.Println(merge)
 42		}
 43	}
 44
 45	return nil
 46}
 47
 48// MergeAll will merge all the available remote bug:
 49//
 50// - If the remote has new commit, the local bug is updated to match the same history
 51//   (fast-forward update)
 52// - if the local bug has new commits but the remote don't, nothing is changed
 53// - if both local and remote bug have new commits (that is, we have a concurrent edition),
 54//   new local commits are rewritten at the head of the remote history (that is, a rebase)
 55func MergeAll(repo repository.ClockedRepo, remote string) <-chan MergeResult {
 56	out := make(chan MergeResult)
 57
 58	go func() {
 59		defer close(out)
 60
 61		remoteRefSpec := fmt.Sprintf(bugsRemoteRefPattern, remote)
 62		remoteRefs, err := repo.ListRefs(remoteRefSpec)
 63
 64		if err != nil {
 65			out <- MergeResult{Err: err}
 66			return
 67		}
 68
 69		for _, remoteRef := range remoteRefs {
 70			refSplitted := strings.Split(remoteRef, "/")
 71			id := refSplitted[len(refSplitted)-1]
 72
 73			remoteBug, err := readBug(repo, remoteRef)
 74
 75			if err != nil {
 76				out <- newMergeInvalidStatus(id, errors.Wrap(err, "remote bug is not readable").Error())
 77				continue
 78			}
 79
 80			// Check for error in remote data
 81			if err := remoteBug.Validate(); err != nil {
 82				out <- newMergeInvalidStatus(id, errors.Wrap(err, "remote bug is invalid").Error())
 83				continue
 84			}
 85
 86			localRef := bugsRefPattern + remoteBug.Id()
 87			localExist, err := repo.RefExist(localRef)
 88
 89			if err != nil {
 90				out <- newMergeError(err, id)
 91				continue
 92			}
 93
 94			// the bug is not local yet, simply create the reference
 95			if !localExist {
 96				err := repo.CopyRef(remoteRef, localRef)
 97
 98				if err != nil {
 99					out <- newMergeError(err, id)
100					return
101				}
102
103				out <- newMergeStatus(MergeStatusNew, id, remoteBug)
104				continue
105			}
106
107			localBug, err := readBug(repo, localRef)
108
109			if err != nil {
110				out <- newMergeError(errors.Wrap(err, "local bug is not readable"), id)
111				return
112			}
113
114			updated, err := localBug.Merge(repo, remoteBug)
115
116			if err != nil {
117				out <- newMergeInvalidStatus(id, errors.Wrap(err, "merge failed").Error())
118				return
119			}
120
121			if updated {
122				out <- newMergeStatus(MergeStatusUpdated, id, localBug)
123			} else {
124				out <- newMergeStatus(MergeStatusNothing, id, localBug)
125			}
126		}
127	}()
128
129	return out
130}
131
132// MergeStatus represent the result of a merge operation of a bug
133type MergeStatus int
134
135const (
136	_ MergeStatus = iota
137	MergeStatusNew
138	MergeStatusInvalid
139	MergeStatusUpdated
140	MergeStatusNothing
141)
142
143type MergeResult struct {
144	// Err is set when a terminal error occur in the process
145	Err error
146
147	Id     string
148	Status MergeStatus
149
150	// Only set for invalid status
151	Reason string
152
153	// Not set for invalid status
154	Bug *Bug
155}
156
157func (mr MergeResult) String() string {
158	switch mr.Status {
159	case MergeStatusNew:
160		return "new"
161	case MergeStatusInvalid:
162		return fmt.Sprintf("invalid data: %s", mr.Reason)
163	case MergeStatusUpdated:
164		return "updated"
165	case MergeStatusNothing:
166		return "nothing to do"
167	default:
168		panic("unknown merge status")
169	}
170}
171
172func newMergeError(err error, id string) MergeResult {
173	return MergeResult{
174		Err: err,
175		Id:  id,
176	}
177}
178
179func newMergeStatus(status MergeStatus, id string, bug *Bug) MergeResult {
180	return MergeResult{
181		Id:     id,
182		Status: status,
183
184		// Bug is not set for an invalid merge result
185		Bug: bug,
186	}
187}
188
189func newMergeInvalidStatus(id string, reason string) MergeResult {
190	return MergeResult{
191		Id:     id,
192		Status: MergeStatusInvalid,
193		Reason: reason,
194	}
195}