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}