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