1package dag
2
3import (
4 "fmt"
5
6 "github.com/pkg/errors"
7
8 "github.com/MichaelMure/git-bug/entity"
9 "github.com/MichaelMure/git-bug/repository"
10)
11
12// ListLocalIds list all the available local Entity's Id
13func ListLocalIds(typename string, repo repository.RepoData) ([]entity.Id, error) {
14 refs, err := repo.ListRefs(fmt.Sprintf("refs/%s/", typename))
15 if err != nil {
16 return nil, err
17 }
18 return entity.RefsToIds(refs), nil
19}
20
21// Fetch retrieve updates from a remote
22// This does not change the local entity state
23func Fetch(def Definition, repo repository.Repo, remote string) (string, error) {
24 // "refs/<entity>/*:refs/remotes/<remote>/<entity>/*"
25 fetchRefSpec := fmt.Sprintf("refs/%s/*:refs/remotes/%s/%s/*",
26 def.namespace, remote, def.namespace)
27
28 return repo.FetchRefs(remote, fetchRefSpec)
29}
30
31// Push update a remote with the local changes
32func Push(def Definition, repo repository.Repo, remote string) (string, error) {
33 // "refs/<entity>/*:refs/<entity>/*"
34 refspec := fmt.Sprintf("refs/%s/*:refs/%s/*",
35 def.namespace, def.namespace)
36
37 return repo.PushRefs(remote, refspec)
38}
39
40// Pull will do a Fetch + MergeAll
41// Contrary to MergeAll, this function will return an error if a merge fail.
42func Pull(def Definition, repo repository.ClockedRepo, remote string) error {
43 _, err := Fetch(def, repo, remote)
44 if err != nil {
45 return err
46 }
47
48 for merge := range MergeAll(def, repo, remote) {
49 if merge.Err != nil {
50 return merge.Err
51 }
52 if merge.Status == entity.MergeStatusInvalid {
53 return errors.Errorf("merge failure: %s", merge.Reason)
54 }
55 }
56
57 return nil
58}
59
60// MergeAll will merge all the available remote Entity:
61//
62// Multiple scenario exist:
63// 1. if the remote Entity doesn't exist locally, it's created
64// --> emit entity.MergeStatusNew
65// 2. if the remote and local Entity have the same state, nothing is changed
66// --> emit entity.MergeStatusNothing
67// 3. if the local Entity has new commits but the remote don't, nothing is changed
68// --> emit entity.MergeStatusNothing
69// 4. if the remote has new commit, the local bug is updated to match the same history
70// (fast-forward update)
71// --> emit entity.MergeStatusUpdated
72// 5. if both local and remote Entity have new commits (that is, we have a concurrent edition),
73// a merge commit with an empty operationPack is created to join both branch and form a DAG.
74// --> emit entity.MergeStatusUpdated
75func MergeAll(def Definition, repo repository.ClockedRepo, remote string) <-chan entity.MergeResult {
76 out := make(chan entity.MergeResult)
77
78 // no caching for the merge, we load everything from git even if that means multiple
79 // copy of the same entity in memory. The cache layer will intercept the results to
80 // invalidate entities if necessary.
81
82 go func() {
83 defer close(out)
84
85 remoteRefSpec := fmt.Sprintf("refs/remotes/%s/%s/", remote, def.namespace)
86 remoteRefs, err := repo.ListRefs(remoteRefSpec)
87 if err != nil {
88 out <- entity.MergeResult{Err: err}
89 return
90 }
91
92 for _, remoteRef := range remoteRefs {
93 out <- merge(def, repo, remoteRef)
94 }
95 }()
96
97 return out
98}
99
100// merge perform a merge to make sure a local Entity is up to date.
101// See MergeAll for more details.
102func merge(def Definition, repo repository.ClockedRepo, remoteRef string) entity.MergeResult {
103 id := entity.RefToId(remoteRef)
104
105 if err := id.Validate(); err != nil {
106 return entity.NewMergeInvalidStatus(id, errors.Wrap(err, "invalid ref").Error())
107 }
108
109 remoteEntity, err := read(def, repo, remoteRef)
110 if err != nil {
111 return entity.NewMergeInvalidStatus(id,
112 errors.Wrapf(err, "remote %s is not readable", def.typename).Error())
113 }
114
115 // Check for error in remote data
116 if err := remoteEntity.Validate(); err != nil {
117 return entity.NewMergeInvalidStatus(id,
118 errors.Wrapf(err, "remote %s data is invalid", def.typename).Error())
119 }
120
121 localRef := fmt.Sprintf("refs/%s/%s", def.namespace, id.String())
122
123 // SCENARIO 1
124 // if the remote Entity doesn't exist locally, it's created
125
126 localExist, err := repo.RefExist(localRef)
127 if err != nil {
128 return entity.NewMergeError(err, id)
129 }
130
131 if !localExist {
132 // the bug is not local yet, simply create the reference
133 err := repo.CopyRef(remoteRef, localRef)
134 if err != nil {
135 return entity.NewMergeError(err, id)
136 }
137
138 return entity.NewMergeNewStatus(id, remoteEntity)
139 }
140
141 localCommit, err := repo.ResolveRef(localRef)
142 if err != nil {
143 return entity.NewMergeError(err, id)
144 }
145
146 remoteCommit, err := repo.ResolveRef(remoteRef)
147 if err != nil {
148 return entity.NewMergeError(err, id)
149 }
150
151 // SCENARIO 2
152 // if the remote and local Entity have the same state, nothing is changed
153
154 if localCommit == remoteCommit {
155 // nothing to merge
156 return entity.NewMergeNothingStatus(id)
157 }
158
159 // SCENARIO 3
160 // if the local Entity has new commits but the remote don't, nothing is changed
161
162 localCommits, err := repo.ListCommits(localRef)
163 if err != nil {
164 return entity.NewMergeError(err, id)
165 }
166
167 for _, hash := range localCommits {
168 if hash == localCommit {
169 return entity.NewMergeNothingStatus(id)
170 }
171 }
172
173 // SCENARIO 4
174 // if the remote has new commit, the local bug is updated to match the same history
175 // (fast-forward update)
176
177 remoteCommits, err := repo.ListCommits(remoteRef)
178 if err != nil {
179 return entity.NewMergeError(err, id)
180 }
181
182 // fast-forward is possible if otherRef include ref
183 fastForwardPossible := false
184 for _, hash := range remoteCommits {
185 if hash == localCommit {
186 fastForwardPossible = true
187 break
188 }
189 }
190
191 if fastForwardPossible {
192 err = repo.UpdateRef(localRef, remoteCommit)
193 if err != nil {
194 return entity.NewMergeError(err, id)
195 }
196 return entity.NewMergeUpdatedStatus(id, remoteEntity)
197 }
198
199 // SCENARIO 5
200 // if both local and remote Entity have new commits (that is, we have a concurrent edition),
201 // a merge commit with an empty operationPack is created to join both branch and form a DAG.
202
203 // fast-forward is not possible, we need to create a merge commit
204 // For simplicity when reading and to have clocks that record this change, we store
205 // an empty operationPack.
206 // First step is to collect those clocks.
207
208 localEntity, err := read(def, repo, localRef)
209 if err != nil {
210 return entity.NewMergeError(err, id)
211 }
212
213 // TODO: pack clock
214 // err = localEntity.packClock.Witness(remoteEntity.packClock.Time())
215 // if err != nil {
216 // return entity.NewMergeError(err, id)
217 // }
218 //
219 // packTime, err := localEntity.packClock.Increment()
220 // if err != nil {
221 // return entity.NewMergeError(err, id)
222 // }
223
224 editTime, err := repo.Increment(fmt.Sprintf(editClockPattern, def.namespace))
225 if err != nil {
226 return entity.NewMergeError(err, id)
227 }
228
229 opp := &operationPack{
230 Operations: nil,
231 CreateTime: 0,
232 EditTime: editTime,
233 // TODO: pack clock
234 // PackTime: packTime,
235 }
236
237 commitHash, err := opp.Write(def, repo, localCommit, remoteCommit)
238 if err != nil {
239 return entity.NewMergeError(err, id)
240 }
241
242 // finally update the ref
243 err = repo.UpdateRef(localRef, commitHash)
244 if err != nil {
245 return entity.NewMergeError(err, id)
246 }
247
248 // Note: we don't need to update localEntity state (lastCommit, operations...) as we
249 // discard it entirely anyway.
250
251 return entity.NewMergeUpdatedStatus(id, localEntity)
252}
253
254func Remove() error {
255 panic("")
256}