aboutsummaryrefslogtreecommitdiffstats
path: root/entity/dag
diff options
context:
space:
mode:
Diffstat (limited to 'entity/dag')
-rw-r--r--entity/dag/entity.go14
-rw-r--r--entity/dag/entity_actions.go87
-rw-r--r--entity/dag/operation.go8
-rw-r--r--entity/dag/operation_pack.go50
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
+}