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