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(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}