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