package dag import ( "fmt" "github.com/pkg/errors" "github.com/MichaelMure/git-bug/entities/identity" "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/repository" ) // ListLocalIds list all the available local Entity's Id func ListLocalIds(def Definition, repo repository.RepoData) ([]entity.Id, error) { refs, err := repo.ListRefs(fmt.Sprintf("refs/%s/", def.Namespace)) if err != nil { return nil, err } return entity.RefsToIds(refs), nil } // Fetch retrieve updates from a remote // This does not change the local entity state func Fetch(def Definition, repo repository.Repo, remote string) (string, error) { return repo.FetchRefs(remote, def.Namespace) } // Push update a remote with the local changes func Push(def Definition, repo repository.Repo, remote string) (string, error) { return repo.PushRefs(remote, def.Namespace) } // Pull will do a Fetch + MergeAll // Contrary to MergeAll, this function will return an error if a merge fail. func Pull[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, author identity.Interface) error { _, err := Fetch(def, repo, remote) if err != nil { return err } for merge := range MergeAll(def, wrapper, repo, resolvers, remote, author) { if merge.Err != nil { return merge.Err } if merge.Status == entity.MergeStatusInvalid { return errors.Errorf("merge failure: %s", merge.Reason) } } return nil } // MergeAll will merge all the available remote Entity: // // Multiple scenario exist: // 1. if the remote Entity doesn't exist locally, it's created // --> emit entity.MergeStatusNew // 2. if the remote and local Entity have the same state, nothing is changed // --> emit entity.MergeStatusNothing // 3. if the local Entity has new commits but the remote don't, nothing is changed // --> emit entity.MergeStatusNothing // 4. if the remote has new commit, the local bug is updated to match the same history // (fast-forward update) // --> emit entity.MergeStatusUpdated // 5. if both local and remote Entity have new commits (that is, we have a concurrent edition), // a merge commit with an empty operationPack is created to join both branch and form a DAG. // --> emit entity.MergeStatusUpdated // // Note: an author is necessary for the case where a merge commit is created, as this commit will // have an author and may be signed if a signing key is available. func MergeAll[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, author identity.Interface) <-chan entity.MergeResult { out := make(chan entity.MergeResult) go func() { defer close(out) remoteRefSpec := fmt.Sprintf("refs/remotes/%s/%s/", remote, def.Namespace) remoteRefs, err := repo.ListRefs(remoteRefSpec) if err != nil { out <- entity.MergeResult{Err: err} return } for _, remoteRef := range remoteRefs { out <- merge[EntityT](def, wrapper, repo, resolvers, remoteRef, author) } }() return out } // merge perform a merge to make sure a local Entity is up-to-date. // See MergeAll for more details. func merge[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, remoteRef string, author identity.Interface) entity.MergeResult { id := entity.RefToId(remoteRef) if err := id.Validate(); err != nil { return entity.NewMergeInvalidStatus(id, errors.Wrap(err, "invalid ref").Error()) } remoteEntity, err := read[EntityT](def, wrapper, repo, resolvers, remoteRef) if err != nil { return entity.NewMergeInvalidStatus(id, errors.Wrapf(err, "remote %s is not readable", def.Typename).Error()) } // Check for error in remote data if err := remoteEntity.Validate(); err != nil { return entity.NewMergeInvalidStatus(id, errors.Wrapf(err, "remote %s data is invalid", def.Typename).Error()) } localRef := fmt.Sprintf("refs/%s/%s", def.Namespace, id.String()) // SCENARIO 1 // if the remote Entity doesn't exist locally, it's created localExist, err := repo.RefExist(localRef) if err != nil { return entity.NewMergeError(err, id) } if !localExist { // the bug is not local yet, simply create the reference err := repo.CopyRef(remoteRef, localRef) if err != nil { return entity.NewMergeError(err, id) } return entity.NewMergeNewStatus(id, remoteEntity) } localCommit, err := repo.ResolveRef(localRef) if err != nil { return entity.NewMergeError(err, id) } remoteCommit, err := repo.ResolveRef(remoteRef) if err != nil { return entity.NewMergeError(err, id) } // SCENARIO 2 // if the remote and local Entity have the same state, nothing is changed if localCommit == remoteCommit { // nothing to merge return entity.NewMergeNothingStatus(id) } // SCENARIO 3 // if the local Entity has new commits but the remote don't, nothing is changed localCommits, err := repo.ListCommits(localRef) if err != nil { return entity.NewMergeError(err, id) } for _, hash := range localCommits { if hash == remoteCommit { return entity.NewMergeNothingStatus(id) } } // SCENARIO 4 // if the remote has new commit, the local bug is updated to match the same history // (fast-forward update) remoteCommits, err := repo.ListCommits(remoteRef) if err != nil { return entity.NewMergeError(err, id) } // fast-forward is possible if otherRef include ref fastForwardPossible := false for _, hash := range remoteCommits { if hash == localCommit { fastForwardPossible = true break } } if fastForwardPossible { err = repo.UpdateRef(localRef, remoteCommit) if err != nil { return entity.NewMergeError(err, id) } return entity.NewMergeUpdatedStatus(id, remoteEntity) } // SCENARIO 5 // if both local and remote Entity have new commits (that is, we have a concurrent edition), // a merge commit with an empty operationPack is created to join both branch and form a DAG. // fast-forward is not possible, we need to create a merge commit // For simplicity when reading and to have clocks that record this change, we store // an empty operationPack. // First step is to collect those clocks. localEntity, err := read[EntityT](def, wrapper, repo, resolvers, localRef) if err != nil { return entity.NewMergeError(err, id) } editTime, err := repo.Increment(fmt.Sprintf(editClockPattern, def.Namespace)) if err != nil { return entity.NewMergeError(err, id) } opp := &operationPack{ Author: author, Operations: nil, CreateTime: 0, EditTime: editTime, } commitHash, err := opp.Write(def, repo, localCommit, remoteCommit) if err != nil { return entity.NewMergeError(err, id) } // finally update the ref err = repo.UpdateRef(localRef, commitHash) if err != nil { return entity.NewMergeError(err, id) } // Note: we don't need to update localEntity state (lastCommit, operations...) as we // discard it entirely anyway. return entity.NewMergeUpdatedStatus(id, localEntity) } // Remove delete an Entity. // Remove is idempotent. func Remove(def Definition, repo repository.ClockedRepo, id entity.Id) error { var matches []string ref := fmt.Sprintf("refs/%s/%s", def.Namespace, id.String()) matches = append(matches, ref) remotes, err := repo.GetRemotes() if err != nil { return err } for remote := range remotes { ref = fmt.Sprintf("refs/remotes/%s/%s/%s", remote, def.Namespace, id.String()) matches = append(matches, ref) } for _, ref = range matches { err = repo.RemoveRef(ref) if err != nil { return err } } return nil }