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