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