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}