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}