diff options
Diffstat (limited to 'entity/dag')
-rw-r--r-- | entity/dag/entity.go | 14 | ||||
-rw-r--r-- | entity/dag/entity_actions.go | 87 | ||||
-rw-r--r-- | entity/dag/operation.go | 8 | ||||
-rw-r--r-- | entity/dag/operation_pack.go | 50 |
4 files changed, 113 insertions, 46 deletions
diff --git a/entity/dag/entity.go b/entity/dag/entity.go index 78347fa0..63d7fc3b 100644 --- a/entity/dag/entity.go +++ b/entity/dag/entity.go @@ -318,7 +318,6 @@ func (e *Entity) CommitAdNeeded(repo repository.ClockedRepo) error { } // Commit write the appended operations in the repository -// TODO: support commit signature func (e *Entity) Commit(repo repository.ClockedRepo) error { if !e.NeedCommit() { return fmt.Errorf("can't commit an entity with no pending operation") @@ -361,18 +360,13 @@ func (e *Entity) Commit(repo repository.ClockedRepo) error { // PackTime: packTime, } - treeHash, err := opp.Write(e.Definition, repo) - if err != nil { - return err - } - - // Write a Git commit referencing the tree, with the previous commit as parent var commitHash repository.Hash - if e.lastCommit != "" { - commitHash, err = repo.StoreCommit(treeHash, e.lastCommit) + if e.lastCommit == "" { + commitHash, err = opp.Write(e.Definition, repo) } else { - commitHash, err = repo.StoreCommit(treeHash) + commitHash, err = opp.Write(e.Definition, repo, e.lastCommit) } + if err != nil { return err } diff --git a/entity/dag/entity_actions.go b/entity/dag/entity_actions.go index 8dcf91e6..83ff7ddc 100644 --- a/entity/dag/entity_actions.go +++ b/entity/dag/entity_actions.go @@ -9,6 +9,7 @@ import ( "github.com/MichaelMure/git-bug/repository" ) +// ListLocalIds list all the available local Entity's Id func ListLocalIds(typename string, repo repository.RepoData) ([]entity.Id, error) { refs, err := repo.ListRefs(fmt.Sprintf("refs/%s/", typename)) if err != nil { @@ -56,6 +57,21 @@ func Pull(def Definition, repo repository.ClockedRepo, remote string) error { 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 func MergeAll(def Definition, repo repository.ClockedRepo, remote string) <-chan entity.MergeResult { out := make(chan entity.MergeResult) @@ -81,6 +97,8 @@ func MergeAll(def Definition, repo repository.ClockedRepo, remote string) <-chan return out } +// merge perform a merge to make sure a local Entity is up to date. +// See MergeAll for more details. func merge(def Definition, repo repository.ClockedRepo, remoteRef string) entity.MergeResult { id := entity.RefToId(remoteRef) @@ -102,36 +120,24 @@ func merge(def Definition, repo repository.ClockedRepo, remoteRef string) entity 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) } - // the bug is not local yet, simply create the reference 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.NewMergeStatus(entity.MergeStatusNew, id, remoteEntity) + return entity.NewMergeNewStatus(id, remoteEntity) } - // var updated bool - // err = repo.MergeRef(localRef, remoteRef, func() repository.Hash { - // updated = true - // - // }) - // if err != nil { - // return entity.NewMergeError(err, id) - // } - // - // if updated { - // return entity.NewMergeStatus(entity.MergeStatusUpdated, id, ) - // } else { - // return entity.NewMergeStatus(entity.MergeStatusNothing, id, ) - // } - localCommit, err := repo.ResolveRef(localRef) if err != nil { return entity.NewMergeError(err, id) @@ -142,18 +148,38 @@ func merge(def Definition, repo repository.ClockedRepo, remoteRef string) entity 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.NewMergeStatus(entity.MergeStatusNothing, id, remoteEntity) + return entity.NewMergeNothingStatus(id) } - // fast-forward is possible if otherRef include ref + // 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 == localCommit { + 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 { @@ -167,9 +193,13 @@ func merge(def Definition, repo repository.ClockedRepo, remoteRef string) entity if err != nil { return entity.NewMergeError(err, id) } - return entity.NewMergeStatus(entity.MergeStatusUpdated, id, remoteEntity) + 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. @@ -180,6 +210,7 @@ func merge(def Definition, repo repository.ClockedRepo, remoteRef string) entity return entity.NewMergeError(err, id) } + // TODO: pack clock // err = localEntity.packClock.Witness(remoteEntity.packClock.Time()) // if err != nil { // return entity.NewMergeError(err, id) @@ -199,27 +230,25 @@ func merge(def Definition, repo repository.ClockedRepo, remoteRef string) entity Operations: nil, CreateTime: 0, EditTime: editTime, + // TODO: pack clock // PackTime: packTime, } - treeHash, err := opp.Write(def, repo) - if err != nil { - return entity.NewMergeError(err, id) - } - - // Create the merge commit with two parents - newHash, err := repo.StoreCommit(treeHash, localCommit, remoteCommit) + commitHash, err := opp.Write(def, repo, localCommit, remoteCommit) if err != nil { return entity.NewMergeError(err, id) } // finally update the ref - err = repo.UpdateRef(localRef, newHash) + err = repo.UpdateRef(localRef, commitHash) if err != nil { return entity.NewMergeError(err, id) } - return entity.NewMergeStatus(entity.MergeStatusUpdated, id, localEntity) + // Note: we don't need to update localEntity state (lastCommit, operations...) as we + // discard it entirely anyway. + + return entity.NewMergeUpdatedStatus(id, localEntity) } func Remove() error { diff --git a/entity/dag/operation.go b/entity/dag/operation.go index 9fcc055b..86e2f7d7 100644 --- a/entity/dag/operation.go +++ b/entity/dag/operation.go @@ -12,17 +12,19 @@ type Operation interface { // Id return the Operation identifier // Some care need to be taken to define a correct Id derivation and enough entropy in the data used to avoid // collisions. Notably: - // - the Id of the first Operation will be used as the Id of the Entity. Collision need to be avoided across Entities. + // - the Id of the first Operation will be used as the Id of the Entity. Collision need to be avoided across entities of the same type + // (example: no collision within the "bug" namespace). // - collisions can also happen within the set of Operations of an Entity. Simple Operation might not have enough - // entropy to yield unique Ids. + // entropy to yield unique Ids (example: two "close" operation within the same second, same author). // A common way to derive an Id will be to use the DeriveId function on the serialized operation data. Id() entity.Id // Validate check if the Operation data is valid Validate() error - + // Author returns the author of this operation Author() identity.Interface } +// TODO: remove? type operationBase struct { author identity.Interface diff --git a/entity/dag/operation_pack.go b/entity/dag/operation_pack.go index 7cf4ee58..ebacdbd9 100644 --- a/entity/dag/operation_pack.go +++ b/entity/dag/operation_pack.go @@ -86,7 +86,10 @@ func (opp *operationPack) Validate() error { return nil } -func (opp *operationPack) Write(def Definition, repo repository.RepoData, parentCommit ...repository.Hash) (repository.Hash, error) { +// Write write the OperationPack in git, with zero, one or more parent commits. +// If the repository has a keypair able to sign (that is, with a private key), the resulting commit is signed with that key. +// Return the hash of the created commit. +func (opp *operationPack) Write(def Definition, repo repository.Repo, parentCommit ...repository.Hash) (repository.Hash, error) { if err := opp.Validate(); err != nil { return "", err } @@ -148,8 +151,13 @@ func (opp *operationPack) Write(def Definition, repo repository.RepoData, parent var commitHash repository.Hash // Sign the commit if we have a key - if opp.Author.SigningKey() != nil { - commitHash, err = repo.StoreSignedCommit(treeHash, opp.Author.SigningKey().PGPEntity(), parentCommit...) + signingKey, err := opp.Author.SigningKey(repo) + if err != nil { + return "", err + } + + if signingKey != nil { + commitHash, err = repo.StoreSignedCommit(treeHash, signingKey.PGPEntity(), parentCommit...) } else { commitHash, err = repo.StoreCommit(treeHash, parentCommit...) } @@ -240,7 +248,7 @@ func readOperationPack(def Definition, repo repository.RepoData, commit reposito // Verify signature if we expect one keys := author.ValidKeysAtTime(fmt.Sprintf(editClockPattern, def.namespace), editTime) if len(keys) > 0 { - keyring := identity.PGPKeyring(keys) + keyring := PGPKeyring(keys) _, err = openpgp.CheckDetachedSignature(keyring, commit.SignedData, commit.Signature) if err != nil { return nil, fmt.Errorf("signature failure: %v", err) @@ -292,3 +300,37 @@ func unmarshallPack(def Definition, data []byte) ([]Operation, identity.Interfac return ops, author, nil } + +var _ openpgp.KeyRing = &PGPKeyring{} + +// PGPKeyring implement a openpgp.KeyRing from an slice of Key +type PGPKeyring []*identity.Key + +func (pk PGPKeyring) KeysById(id uint64) []openpgp.Key { + var result []openpgp.Key + for _, key := range pk { + if key.Public().KeyId == id { + result = append(result, openpgp.Key{ + PublicKey: key.Public(), + PrivateKey: key.Private(), + }) + } + } + return result +} + +func (pk PGPKeyring) KeysByIdUsage(id uint64, requiredUsage byte) []openpgp.Key { + // the only usage we care about is the ability to sign, which all keys should already be capable of + return pk.KeysById(id) +} + +func (pk PGPKeyring) DecryptionKeys() []openpgp.Key { + result := make([]openpgp.Key, len(pk)) + for i, key := range pk { + result[i] = openpgp.Key{ + PublicKey: key.Public(), + PrivateKey: key.Private(), + } + } + return result +} |