bug_actions.go

  1package bug
  2
  3import (
  4	"fmt"
  5	"strings"
  6
  7	"github.com/MichaelMure/git-bug/identity"
  8	"github.com/MichaelMure/git-bug/repository"
  9	"github.com/pkg/errors"
 10)
 11
 12// Note:
 13//
 14// For the actions (fetch/push/pull/merge/commit), this package act as a master for
 15// the identity package and will also drive the needed identity actions. That is,
 16// if bug.Push() is called, identity.Push will also be called to make sure that
 17// the dependant identities are also present and up to date on the remote.
 18//
 19// I'm not entirely sure this is the correct way to do it, as it might introduce
 20// too much complexity and hard coupling, but it does make this package easier
 21// to use.
 22
 23// Fetch retrieve updates from a remote
 24// This does not change the local bugs state
 25func Fetch(repo repository.Repo, remote string) (string, error) {
 26	stdout, err := identity.Fetch(repo, remote)
 27	if err != nil {
 28		return stdout, err
 29	}
 30
 31	remoteRefSpec := fmt.Sprintf(bugsRemoteRefPattern, remote)
 32	fetchRefSpec := fmt.Sprintf("%s*:%s*", bugsRefPattern, remoteRefSpec)
 33
 34	stdout2, err := repo.FetchRefs(remote, fetchRefSpec)
 35
 36	return stdout + "\n" + stdout2, err
 37}
 38
 39// Push update a remote with the local changes
 40func Push(repo repository.Repo, remote string) (string, error) {
 41	stdout, err := identity.Push(repo, remote)
 42	if err != nil {
 43		return stdout, err
 44	}
 45
 46	stdout2, err := repo.PushRefs(remote, bugsRefPattern+"*")
 47
 48	return stdout + "\n" + stdout2, err
 49}
 50
 51// Pull will do a Fetch + MergeAll
 52// This function will return an error if a merge fail
 53func Pull(repo repository.ClockedRepo, remote string) error {
 54	_, err := identity.Fetch(repo, remote)
 55	if err != nil {
 56		return err
 57	}
 58
 59	for merge := range identity.MergeAll(repo, remote) {
 60		if merge.Err != nil {
 61			return merge.Err
 62		}
 63		if merge.Status == identity.MergeStatusInvalid {
 64			return errors.Errorf("merge failure: %s", merge.Reason)
 65		}
 66	}
 67
 68	_, err = Fetch(repo, remote)
 69	if err != nil {
 70		return err
 71	}
 72
 73	for merge := range MergeAll(repo, remote) {
 74		if merge.Err != nil {
 75			return merge.Err
 76		}
 77		if merge.Status == MergeStatusInvalid {
 78			return errors.Errorf("merge failure: %s", merge.Reason)
 79		}
 80	}
 81
 82	return nil
 83}
 84
 85// MergeAll will merge all the available remote bug:
 86//
 87// - If the remote has new commit, the local bug is updated to match the same history
 88//   (fast-forward update)
 89// - if the local bug has new commits but the remote don't, nothing is changed
 90// - if both local and remote bug have new commits (that is, we have a concurrent edition),
 91//   new local commits are rewritten at the head of the remote history (that is, a rebase)
 92func MergeAll(repo repository.ClockedRepo, remote string) <-chan MergeResult {
 93	out := make(chan MergeResult)
 94
 95	go func() {
 96		defer close(out)
 97
 98		remoteRefSpec := fmt.Sprintf(bugsRemoteRefPattern, remote)
 99		remoteRefs, err := repo.ListRefs(remoteRefSpec)
100
101		if err != nil {
102			out <- MergeResult{Err: err}
103			return
104		}
105
106		for _, remoteRef := range remoteRefs {
107			refSplitted := strings.Split(remoteRef, "/")
108			id := refSplitted[len(refSplitted)-1]
109
110			remoteBug, err := readBug(repo, remoteRef)
111
112			if err != nil {
113				out <- newMergeInvalidStatus(id, errors.Wrap(err, "remote bug is not readable").Error())
114				continue
115			}
116
117			// Check for error in remote data
118			if err := remoteBug.Validate(); err != nil {
119				out <- newMergeInvalidStatus(id, errors.Wrap(err, "remote bug is invalid").Error())
120				continue
121			}
122
123			localRef := bugsRefPattern + remoteBug.Id()
124			localExist, err := repo.RefExist(localRef)
125
126			if err != nil {
127				out <- newMergeError(err, id)
128				continue
129			}
130
131			// the bug is not local yet, simply create the reference
132			if !localExist {
133				err := repo.CopyRef(remoteRef, localRef)
134
135				if err != nil {
136					out <- newMergeError(err, id)
137					return
138				}
139
140				out <- newMergeStatus(MergeStatusNew, id, remoteBug)
141				continue
142			}
143
144			localBug, err := readBug(repo, localRef)
145
146			if err != nil {
147				out <- newMergeError(errors.Wrap(err, "local bug is not readable"), id)
148				return
149			}
150
151			updated, err := localBug.Merge(repo, remoteBug)
152
153			if err != nil {
154				out <- newMergeInvalidStatus(id, errors.Wrap(err, "merge failed").Error())
155				return
156			}
157
158			if updated {
159				out <- newMergeStatus(MergeStatusUpdated, id, localBug)
160			} else {
161				out <- newMergeStatus(MergeStatusNothing, id, localBug)
162			}
163		}
164	}()
165
166	return out
167}
168
169// MergeStatus represent the result of a merge operation of a bug
170type MergeStatus int
171
172const (
173	_ MergeStatus = iota
174	MergeStatusNew
175	MergeStatusInvalid
176	MergeStatusUpdated
177	MergeStatusNothing
178)
179
180type MergeResult struct {
181	// Err is set when a terminal error occur in the process
182	Err error
183
184	Id     string
185	Status MergeStatus
186
187	// Only set for invalid status
188	Reason string
189
190	// Not set for invalid status
191	Bug *Bug
192}
193
194func (mr MergeResult) String() string {
195	switch mr.Status {
196	case MergeStatusNew:
197		return "new"
198	case MergeStatusInvalid:
199		return fmt.Sprintf("invalid data: %s", mr.Reason)
200	case MergeStatusUpdated:
201		return "updated"
202	case MergeStatusNothing:
203		return "nothing to do"
204	default:
205		panic("unknown merge status")
206	}
207}
208
209func newMergeError(err error, id string) MergeResult {
210	return MergeResult{
211		Err: err,
212		Id:  id,
213	}
214}
215
216func newMergeStatus(status MergeStatus, id string, bug *Bug) MergeResult {
217	return MergeResult{
218		Id:     id,
219		Status: status,
220
221		// Bug is not set for an invalid merge result
222		Bug: bug,
223	}
224}
225
226func newMergeInvalidStatus(id string, reason string) MergeResult {
227	return MergeResult{
228		Id:     id,
229		Status: MergeStatusInvalid,
230		Reason: reason,
231	}
232}