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}