entity_actions.go

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