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 // "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 go func() {
79 defer close(out)
80
81 remoteRefSpec := fmt.Sprintf("refs/remotes/%s/%s/", remote, def.namespace)
82 remoteRefs, err := repo.ListRefs(remoteRefSpec)
83 if err != nil {
84 out <- entity.MergeResult{Err: err}
85 return
86 }
87
88 for _, remoteRef := range remoteRefs {
89 out <- merge(def, repo, remoteRef)
90 }
91 }()
92
93 return out
94}
95
96// merge perform a merge to make sure a local Entity is up to date.
97// See MergeAll for more details.
98func merge(def Definition, repo repository.ClockedRepo, remoteRef string) entity.MergeResult {
99 id := entity.RefToId(remoteRef)
100
101 if err := id.Validate(); err != nil {
102 return entity.NewMergeInvalidStatus(id, errors.Wrap(err, "invalid ref").Error())
103 }
104
105 remoteEntity, err := read(def, repo, remoteRef)
106 if err != nil {
107 return entity.NewMergeInvalidStatus(id,
108 errors.Wrapf(err, "remote %s is not readable", def.typename).Error())
109 }
110
111 // Check for error in remote data
112 if err := remoteEntity.Validate(); err != nil {
113 return entity.NewMergeInvalidStatus(id,
114 errors.Wrapf(err, "remote %s data is invalid", def.typename).Error())
115 }
116
117 localRef := fmt.Sprintf("refs/%s/%s", def.namespace, id.String())
118
119 // SCENARIO 1
120 // if the remote Entity doesn't exist locally, it's created
121
122 localExist, err := repo.RefExist(localRef)
123 if err != nil {
124 return entity.NewMergeError(err, id)
125 }
126
127 if !localExist {
128 // the bug is not local yet, simply create the reference
129 err := repo.CopyRef(remoteRef, localRef)
130 if err != nil {
131 return entity.NewMergeError(err, id)
132 }
133
134 return entity.NewMergeNewStatus(id, remoteEntity)
135 }
136
137 localCommit, err := repo.ResolveRef(localRef)
138 if err != nil {
139 return entity.NewMergeError(err, id)
140 }
141
142 remoteCommit, err := repo.ResolveRef(remoteRef)
143 if err != nil {
144 return entity.NewMergeError(err, id)
145 }
146
147 // SCENARIO 2
148 // if the remote and local Entity have the same state, nothing is changed
149
150 if localCommit == remoteCommit {
151 // nothing to merge
152 return entity.NewMergeNothingStatus(id)
153 }
154
155 // SCENARIO 3
156 // if the local Entity has new commits but the remote don't, nothing is changed
157
158 localCommits, err := repo.ListCommits(localRef)
159 if err != nil {
160 return entity.NewMergeError(err, id)
161 }
162
163 for _, hash := range localCommits {
164 if hash == localCommit {
165 return entity.NewMergeNothingStatus(id)
166 }
167 }
168
169 // SCENARIO 4
170 // if the remote has new commit, the local bug is updated to match the same history
171 // (fast-forward update)
172
173 remoteCommits, err := repo.ListCommits(remoteRef)
174 if err != nil {
175 return entity.NewMergeError(err, id)
176 }
177
178 // fast-forward is possible if otherRef include ref
179 fastForwardPossible := false
180 for _, hash := range remoteCommits {
181 if hash == localCommit {
182 fastForwardPossible = true
183 break
184 }
185 }
186
187 if fastForwardPossible {
188 err = repo.UpdateRef(localRef, remoteCommit)
189 if err != nil {
190 return entity.NewMergeError(err, id)
191 }
192 return entity.NewMergeUpdatedStatus(id, remoteEntity)
193 }
194
195 // SCENARIO 5
196 // if both local and remote Entity have new commits (that is, we have a concurrent edition),
197 // a merge commit with an empty operationPack is created to join both branch and form a DAG.
198
199 // fast-forward is not possible, we need to create a merge commit
200 // For simplicity when reading and to have clocks that record this change, we store
201 // an empty operationPack.
202 // First step is to collect those clocks.
203
204 localEntity, err := read(def, repo, localRef)
205 if err != nil {
206 return entity.NewMergeError(err, id)
207 }
208
209 // TODO: pack clock
210 // err = localEntity.packClock.Witness(remoteEntity.packClock.Time())
211 // if err != nil {
212 // return entity.NewMergeError(err, id)
213 // }
214 //
215 // packTime, err := localEntity.packClock.Increment()
216 // if err != nil {
217 // return entity.NewMergeError(err, id)
218 // }
219
220 editTime, err := repo.Increment(fmt.Sprintf(editClockPattern, def.namespace))
221 if err != nil {
222 return entity.NewMergeError(err, id)
223 }
224
225 opp := &operationPack{
226 Operations: nil,
227 CreateTime: 0,
228 EditTime: editTime,
229 // TODO: pack clock
230 // PackTime: packTime,
231 }
232
233 commitHash, err := opp.Write(def, repo, localCommit, remoteCommit)
234 if err != nil {
235 return entity.NewMergeError(err, id)
236 }
237
238 // finally update the ref
239 err = repo.UpdateRef(localRef, commitHash)
240 if err != nil {
241 return entity.NewMergeError(err, id)
242 }
243
244 // Note: we don't need to update localEntity state (lastCommit, operations...) as we
245 // discard it entirely anyway.
246
247 return entity.NewMergeUpdatedStatus(id, localEntity)
248}
249
250func Remove() error {
251 panic("")
252}