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