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 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 == 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 MergeResult {
 53	out := make(chan MergeResult)
 54
 55	go func() {
 56		defer close(out)
 57
 58		remoteRefSpec := fmt.Sprintf(bugsRemoteRefPattern, remote)
 59		remoteRefs, err := repo.ListRefs(remoteRefSpec)
 60
 61		if err != nil {
 62			out <- MergeResult{Err: err}
 63			return
 64		}
 65
 66		for _, remoteRef := range remoteRefs {
 67			refSplitted := strings.Split(remoteRef, "/")
 68			id := refSplitted[len(refSplitted)-1]
 69
 70			remoteBug, err := readBug(repo, remoteRef)
 71
 72			if err != nil {
 73				out <- newMergeInvalidStatus(id, errors.Wrap(err, "remote bug is not readable").Error())
 74				continue
 75			}
 76
 77			// Check for error in remote data
 78			if err := remoteBug.Validate(); err != nil {
 79				out <- newMergeInvalidStatus(id, errors.Wrap(err, "remote bug is invalid").Error())
 80				continue
 81			}
 82
 83			localRef := bugsRefPattern + remoteBug.Id()
 84			localExist, err := repo.RefExist(localRef)
 85
 86			if err != nil {
 87				out <- newMergeError(err, id)
 88				continue
 89			}
 90
 91			// the bug is not local yet, simply create the reference
 92			if !localExist {
 93				err := repo.CopyRef(remoteRef, localRef)
 94
 95				if err != nil {
 96					out <- newMergeError(err, id)
 97					return
 98				}
 99
100				out <- newMergeStatus(MergeStatusNew, id, remoteBug)
101				continue
102			}
103
104			localBug, err := readBug(repo, localRef)
105
106			if err != nil {
107				out <- newMergeError(errors.Wrap(err, "local bug is not readable"), id)
108				return
109			}
110
111			updated, err := localBug.Merge(repo, remoteBug)
112
113			if err != nil {
114				out <- newMergeInvalidStatus(id, errors.Wrap(err, "merge failed").Error())
115				return
116			}
117
118			if updated {
119				out <- newMergeStatus(MergeStatusUpdated, id, localBug)
120			} else {
121				out <- newMergeStatus(MergeStatusNothing, id, localBug)
122			}
123		}
124	}()
125
126	return out
127}
128
129// MergeStatus represent the result of a merge operation of a bug
130type MergeStatus int
131
132const (
133	_ MergeStatus = iota
134	MergeStatusNew
135	MergeStatusInvalid
136	MergeStatusUpdated
137	MergeStatusNothing
138)
139
140type MergeResult struct {
141	// Err is set when a terminal error occur in the process
142	Err error
143
144	Id     string
145	Status MergeStatus
146
147	// Only set for invalid status
148	Reason string
149
150	// Not set for invalid status
151	Bug *Bug
152}
153
154func (mr MergeResult) String() string {
155	switch mr.Status {
156	case MergeStatusNew:
157		return "new"
158	case MergeStatusInvalid:
159		return fmt.Sprintf("invalid data: %s", mr.Reason)
160	case MergeStatusUpdated:
161		return "updated"
162	case MergeStatusNothing:
163		return "nothing to do"
164	default:
165		panic("unknown merge status")
166	}
167}
168
169func newMergeError(err error, id string) MergeResult {
170	return MergeResult{
171		Err: err,
172		Id:  id,
173	}
174}
175
176func newMergeStatus(status MergeStatus, id string, bug *Bug) MergeResult {
177	return MergeResult{
178		Id:     id,
179		Status: status,
180
181		// Bug is not set for an invalid merge result
182		Bug: bug,
183	}
184}
185
186func newMergeInvalidStatus(id string, reason string) MergeResult {
187	return MergeResult{
188		Id:     id,
189		Status: MergeStatusInvalid,
190		Reason: reason,
191	}
192}