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