From 7163b2283b4542a4d4abfe9a71963f122322bde7 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 8 Nov 2020 19:15:06 +0100 Subject: bug: Id from first operation data, not git + remove root link --- bug/bug.go | 233 +++++++++++++++++++------------------------------------------ 1 file changed, 73 insertions(+), 160 deletions(-) (limited to 'bug/bug.go') diff --git a/bug/bug.go b/bug/bug.go index f6c35a2d..e67920f9 100644 --- a/bug/bug.go +++ b/bug/bug.go @@ -4,7 +4,6 @@ package bug import ( "encoding/json" "fmt" - "strings" "github.com/pkg/errors" @@ -18,7 +17,6 @@ const bugsRefPattern = "refs/bugs/" const bugsRemoteRefPattern = "refs/remotes/%s/bugs/" const opsEntryName = "ops" -const rootEntryName = "root" const mediaEntryName = "media" const createClockEntryPrefix = "create-clock-" @@ -57,7 +55,6 @@ type Bug struct { id entity.Id lastCommit repository.Hash - rootPack repository.Hash // all the committed operations packs []OperationPack @@ -71,7 +68,7 @@ type Bug struct { func NewBug() *Bug { // No id yet // No logical clock yet - return &Bug{} + return &Bug{id: entity.UnsetId} } // ReadLocal will read a local bug from its hash @@ -100,122 +97,77 @@ func ReadRemoteWithResolver(repo repository.ClockedRepo, identityResolver identi // read will read and parse a Bug from git func read(repo repository.ClockedRepo, identityResolver identity.Resolver, ref string) (*Bug, error) { - refSplit := strings.Split(ref, "/") - id := entity.Id(refSplit[len(refSplit)-1]) + id := entity.RefToId(ref) if err := id.Validate(); err != nil { return nil, errors.Wrap(err, "invalid ref ") } hashes, err := repo.ListCommits(ref) - - // TODO: this is not perfect, it might be a command invoke error if err != nil { return nil, ErrBugNotExist } + if len(hashes) == 0 { + return nil, fmt.Errorf("empty bug") + } bug := Bug{ - id: id, - editTime: 0, + id: id, } // Load each OperationPack for _, hash := range hashes { - entries, err := repo.ReadTree(hash) + tree, err := readTree(repo, hash) if err != nil { - return nil, errors.Wrap(err, "can't list git tree entries") - } - - bug.lastCommit = hash - - var opsEntry repository.TreeEntry - opsFound := false - var rootEntry repository.TreeEntry - rootFound := false - var createTime uint64 - var editTime uint64 - - for _, entry := range entries { - if entry.Name == opsEntryName { - opsEntry = entry - opsFound = true - continue - } - if entry.Name == rootEntryName { - rootEntry = entry - rootFound = true - } - if strings.HasPrefix(entry.Name, createClockEntryPrefix) { - n, err := fmt.Sscanf(entry.Name, createClockEntryPattern, &createTime) - if err != nil { - return nil, errors.Wrap(err, "can't read create lamport time") - } - if n != 1 { - return nil, fmt.Errorf("could not parse create time lamport value") - } - } - if strings.HasPrefix(entry.Name, editClockEntryPrefix) { - n, err := fmt.Sscanf(entry.Name, editClockEntryPattern, &editTime) - if err != nil { - return nil, errors.Wrap(err, "can't read edit lamport time") - } - if n != 1 { - return nil, fmt.Errorf("could not parse edit time lamport value") - } - } - } - - if !opsFound { - return nil, errors.New("invalid tree, missing the ops entry") - } - if !rootFound { - return nil, errors.New("invalid tree, missing the root entry") - } - - if bug.rootPack == "" { - bug.rootPack = rootEntry.Hash - bug.createTime = lamport.Time(createTime) + return nil, err } // Due to rebase, edit Lamport time are not necessarily ordered - if editTime > uint64(bug.editTime) { - bug.editTime = lamport.Time(editTime) + if tree.editTime > bug.editTime { + bug.editTime = tree.editTime } // Update the clocks - createClock, err := repo.GetOrCreateClock(creationClockName) + err = repo.Witness(creationClockName, bug.createTime) if err != nil { - return nil, err - } - if err := createClock.Witness(bug.createTime); err != nil { return nil, errors.Wrap(err, "failed to update create lamport clock") } - editClock, err := repo.GetOrCreateClock(editClockName) + err = repo.Witness(editClockName, bug.editTime) if err != nil { - return nil, err - } - if err := editClock.Witness(bug.editTime); err != nil { return nil, errors.Wrap(err, "failed to update edit lamport clock") } - data, err := repo.ReadData(opsEntry.Hash) + data, err := repo.ReadData(tree.opsEntry.Hash) if err != nil { return nil, errors.Wrap(err, "failed to read git blob data") } opp := &OperationPack{} err = json.Unmarshal(data, &opp) - if err != nil { return nil, errors.Wrap(err, "failed to decode OperationPack json") } // tag the pack with the commit hash opp.commitHash = hash + bug.lastCommit = hash + + // if it's the first OperationPack read + if len(bug.packs) == 0 { + bug.createTime = tree.createTime + } bug.packs = append(bug.packs, *opp) } + // Bug Id is the Id of the first operation + if len(bug.packs[0].Operations) == 0 { + return nil, fmt.Errorf("first OperationPack is empty") + } + if bug.id != bug.packs[0].Operations[0].Id() { + return nil, fmt.Errorf("bug ID doesn't match the first operation ID") + } + // Make sure that the identities are properly loaded err = bug.EnsureIdentities(identityResolver) if err != nil { @@ -367,8 +319,8 @@ func (bug *Bug) Validate() error { return fmt.Errorf("first operation should be a Create op") } - // The bug Id should be the hash of the first commit - if len(bug.packs) > 0 && string(bug.packs[0].commitHash) != bug.id.String() { + // The bug Id should be the id of the first operation + if bug.FirstOp().Id() != bug.id { return fmt.Errorf("bug id should be the first commit hash") } @@ -396,12 +348,17 @@ func (bug *Bug) Validate() error { // Append an operation into the staging area, to be committed later func (bug *Bug) Append(op Operation) { + if len(bug.packs) == 0 && len(bug.staging.Operations) == 0 { + if op.base().OperationType != CreateOp { + panic("first operation should be a Create") + } + bug.id = op.Id() + } bug.staging.Append(op) } // Commit write the staging area in Git and move the operations to the packs func (bug *Bug) Commit(repo repository.ClockedRepo) error { - if !bug.NeedCommit() { return fmt.Errorf("can't commit a bug with no pending operation") } @@ -410,38 +367,29 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error { return errors.Wrap(err, "can't commit a bug with invalid data") } - // Write the Ops as a Git blob containing the serialized array - hash, err := bug.staging.Write(repo) + // update clocks + var err error + bug.editTime, err = repo.Increment(editClockName) if err != nil { return err } + if bug.lastCommit == "" { + bug.createTime, err = repo.Increment(creationClockName) + if err != nil { + return err + } + } - if bug.rootPack == "" { - bug.rootPack = hash + // Write the Ops as a Git blob containing the serialized array + hash, err := bug.staging.Write(repo) + if err != nil { + return err } // Make a Git tree referencing this blob tree := []repository.TreeEntry{ // the last pack of ops {ObjectType: repository.Blob, Hash: hash, Name: opsEntryName}, - // always the first pack of ops (might be the same) - {ObjectType: repository.Blob, Hash: bug.rootPack, Name: rootEntryName}, - } - - // Reference, if any, all the files required by the ops - // Git will check that they actually exist in the storage and will make sure - // to push/pull them as needed. - mediaTree := makeMediaTree(bug.staging) - if len(mediaTree) > 0 { - mediaTreeHash, err := repo.StoreTree(mediaTree) - if err != nil { - return err - } - tree = append(tree, repository.TreeEntry{ - ObjectType: repository.Tree, - Hash: mediaTreeHash, - Name: mediaEntryName, - }) } // Store the logical clocks as well @@ -454,31 +402,12 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error { if err != nil { return err } - - editClock, err := repo.GetOrCreateClock(editClockName) - if err != nil { - return err - } - bug.editTime, err = editClock.Increment() - if err != nil { - return err - } - tree = append(tree, repository.TreeEntry{ ObjectType: repository.Blob, Hash: emptyBlobHash, Name: fmt.Sprintf(editClockEntryPattern, bug.editTime), }) if bug.lastCommit == "" { - createClock, err := repo.GetOrCreateClock(creationClockName) - if err != nil { - return err - } - bug.createTime, err = createClock.Increment() - if err != nil { - return err - } - tree = append(tree, repository.TreeEntry{ ObjectType: repository.Blob, Hash: emptyBlobHash, @@ -486,6 +415,22 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error { }) } + // Reference, if any, all the files required by the ops + // Git will check that they actually exist in the storage and will make sure + // to push/pull them as needed. + mediaTree := makeMediaTree(bug.staging) + if len(mediaTree) > 0 { + mediaTreeHash, err := repo.StoreTree(mediaTree) + if err != nil { + return err + } + tree = append(tree, repository.TreeEntry{ + ObjectType: repository.Tree, + Hash: mediaTreeHash, + Name: mediaEntryName, + }) + } + // Store the tree hash, err = repo.StoreTree(tree) if err != nil { @@ -498,33 +443,25 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error { } else { hash, err = repo.StoreCommit(hash) } - if err != nil { return err } bug.lastCommit = hash + bug.staging.commitHash = hash + bug.packs = append(bug.packs, bug.staging) + bug.staging = OperationPack{} - // if it was the first commit, use the commit hash as bug id - if bug.id == "" { - bug.id = entity.Id(hash) + // if it was the first commit, use the Id of the first op (create) + if bug.id == "" || bug.id == entity.UnsetId { + bug.id = bug.packs[0].Operations[0].Id() } // Create or update the Git reference for this bug // When pushing later, the remote will ensure that this ref update // is fast-forward, that is no data has been overwritten ref := fmt.Sprintf("%s%s", bugsRefPattern, bug.id) - err = repo.UpdateRef(ref, hash) - - if err != nil { - return err - } - - bug.staging.commitHash = hash - bug.packs = append(bug.packs, bug.staging) - bug.staging = OperationPack{} - - return nil + return repo.UpdateRef(ref, hash) } func (bug *Bug) CommitAsNeeded(repo repository.ClockedRepo) error { @@ -538,30 +475,6 @@ func (bug *Bug) NeedCommit() bool { return !bug.staging.IsEmpty() } -func makeMediaTree(pack OperationPack) []repository.TreeEntry { - var tree []repository.TreeEntry - counter := 0 - added := make(map[repository.Hash]interface{}) - - for _, ops := range pack.Operations { - for _, file := range ops.GetFiles() { - if _, has := added[file]; !has { - tree = append(tree, repository.TreeEntry{ - ObjectType: repository.Blob, - Hash: file, - // The name is not important here, we only need to - // reference the blob. - Name: fmt.Sprintf("file%d", counter), - }) - counter++ - added[file] = struct{}{} - } - } - } - - return tree -} - // Merge a different version of the same bug by rebasing operations of this bug // that are not present in the other on top of the chain of operations of the // other version. @@ -657,9 +570,9 @@ func (bug *Bug) Merge(repo repository.Repo, other Interface) (bool, error) { // Id return the Bug identifier func (bug *Bug) Id() entity.Id { - if bug.id == "" { + if bug.id == "" || bug.id == entity.UnsetId { // simply panic as it would be a coding error - // (using an id of a bug not stored yet) + // (using an id of a bug without operation yet) panic("no id yet") } return bug.id -- cgit v1.2.3 From 497ec1376ab510af740910ed9c99b159809cf0e8 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Mon, 9 Nov 2020 00:35:06 +0100 Subject: bug: debug --- bug/bug.go | 2 ++ 1 file changed, 2 insertions(+) (limited to 'bug/bug.go') diff --git a/bug/bug.go b/bug/bug.go index e67920f9..86227c6b 100644 --- a/bug/bug.go +++ b/bug/bug.go @@ -321,6 +321,8 @@ func (bug *Bug) Validate() error { // The bug Id should be the id of the first operation if bug.FirstOp().Id() != bug.id { + fmt.Println("bug", bug.id.String()) + fmt.Println("op", bug.FirstOp().Id().String()) return fmt.Errorf("bug id should be the first commit hash") } -- cgit v1.2.3 From 2788c5fc87507974d3237d4edc233fda3f784b35 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 29 Nov 2020 20:22:09 +0100 Subject: bug: don't store the id in Bug, match how it's done for Identity --- bug/bug.go | 42 +++++++++--------------------------------- bug/op_create.go | 17 ++++++++++++++++- bug/with_snapshot.go | 2 +- 3 files changed, 26 insertions(+), 35 deletions(-) (limited to 'bug/bug.go') diff --git a/bug/bug.go b/bug/bug.go index 86227c6b..fb36bfd8 100644 --- a/bug/bug.go +++ b/bug/bug.go @@ -44,16 +44,12 @@ var _ entity.Interface = &Bug{} // how it will be persisted inside Git. This is the data structure // used to merge two different version of the same Bug. type Bug struct { - // A Lamport clock is a logical clock that allow to order event // inside a distributed system. // It must be the first field in this struct due to https://github.com/golang/go/issues/599 createTime lamport.Time editTime lamport.Time - // Id used as unique identifier - id entity.Id - lastCommit repository.Hash // all the committed operations @@ -66,9 +62,8 @@ type Bug struct { // NewBug create a new Bug func NewBug() *Bug { - // No id yet // No logical clock yet - return &Bug{id: entity.UnsetId} + return &Bug{} } // ReadLocal will read a local bug from its hash @@ -111,9 +106,7 @@ func read(repo repository.ClockedRepo, identityResolver identity.Resolver, ref s return nil, fmt.Errorf("empty bug") } - bug := Bug{ - id: id, - } + bug := Bug{} // Load each OperationPack for _, hash := range hashes { @@ -164,7 +157,7 @@ func read(repo repository.ClockedRepo, identityResolver identity.Resolver, ref s if len(bug.packs[0].Operations) == 0 { return nil, fmt.Errorf("first OperationPack is empty") } - if bug.id != bug.packs[0].Operations[0].Id() { + if id != bug.packs[0].Operations[0].Id() { return nil, fmt.Errorf("bug ID doesn't match the first operation ID") } @@ -319,13 +312,6 @@ func (bug *Bug) Validate() error { return fmt.Errorf("first operation should be a Create op") } - // The bug Id should be the id of the first operation - if bug.FirstOp().Id() != bug.id { - fmt.Println("bug", bug.id.String()) - fmt.Println("op", bug.FirstOp().Id().String()) - return fmt.Errorf("bug id should be the first commit hash") - } - // Check that there is no more CreateOp op // Check that there is no colliding operation's ID it := NewOperationIterator(bug) @@ -354,7 +340,6 @@ func (bug *Bug) Append(op Operation) { if op.base().OperationType != CreateOp { panic("first operation should be a Create") } - bug.id = op.Id() } bug.staging.Append(op) } @@ -454,15 +439,10 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error { bug.packs = append(bug.packs, bug.staging) bug.staging = OperationPack{} - // if it was the first commit, use the Id of the first op (create) - if bug.id == "" || bug.id == entity.UnsetId { - bug.id = bug.packs[0].Operations[0].Id() - } - // Create or update the Git reference for this bug // When pushing later, the remote will ensure that this ref update // is fast-forward, that is no data has been overwritten - ref := fmt.Sprintf("%s%s", bugsRefPattern, bug.id) + ref := fmt.Sprintf("%s%s", bugsRefPattern, bug.Id().String()) return repo.UpdateRef(ref, hash) } @@ -488,7 +468,7 @@ func (bug *Bug) Merge(repo repository.Repo, other Interface) (bool, error) { // Reading the other side is still necessary to validate remote data, at least // for new operations - if bug.id != otherBug.id { + if bug.Id() != otherBug.Id() { return false, errors.New("merging unrelated bugs is not supported") } @@ -562,7 +542,7 @@ func (bug *Bug) Merge(repo repository.Repo, other Interface) (bool, error) { bug.packs = newPacks // Update the git ref - err = repo.UpdateRef(bugsRefPattern+bug.id.String(), bug.lastCommit) + err = repo.UpdateRef(bugsRefPattern+bug.Id().String(), bug.lastCommit) if err != nil { return false, err } @@ -572,12 +552,8 @@ func (bug *Bug) Merge(repo repository.Repo, other Interface) (bool, error) { // Id return the Bug identifier func (bug *Bug) Id() entity.Id { - if bug.id == "" || bug.id == entity.UnsetId { - // simply panic as it would be a coding error - // (using an id of a bug without operation yet) - panic("no id yet") - } - return bug.id + // id is the id of the first operation + return bug.FirstOp().Id() } // CreateLamportTime return the Lamport time of creation @@ -629,7 +605,7 @@ func (bug *Bug) LastOp() Operation { // Compile a bug in a easily usable snapshot func (bug *Bug) Compile() Snapshot { snap := Snapshot{ - id: bug.id, + id: bug.Id(), Status: OpenStatus, } diff --git a/bug/op_create.go b/bug/op_create.go index 3c8ce658..41e0fca1 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -38,6 +38,21 @@ func (op *CreateOperation) Id() entity.Id { return idOperation(op) } +// OVERRIDE +func (op *CreateOperation) SetMetadata(key string, value string) { + // sanity check: we make sure we are not in the following scenario: + // - the bug is created with a first operation + // - Id() is used + // - metadata are added, which will change the Id + // - Id() is used again + + if op.id != entity.UnsetId { + panic("usage of Id() after changing the first operation") + } + + op.OpBase.SetMetadata(key, value) +} + func (op *CreateOperation) Apply(snapshot *Snapshot) { snapshot.addActor(op.Author) snapshot.addParticipant(op.Author) @@ -95,7 +110,7 @@ func (op *CreateOperation) Validate() error { return nil } -// UnmarshalJSON is a two step JSON unmarshaling +// UnmarshalJSON is a two step JSON unmarshalling // This workaround is necessary to avoid the inner OpBase.MarshalJSON // overriding the outer op's MarshalJSON func (op *CreateOperation) UnmarshalJSON(data []byte) error { diff --git a/bug/with_snapshot.go b/bug/with_snapshot.go index 2b2439df..41192d39 100644 --- a/bug/with_snapshot.go +++ b/bug/with_snapshot.go @@ -47,7 +47,7 @@ func (b *WithSnapshot) Commit(repo repository.ClockedRepo) error { return nil } - b.snap.id = b.Bug.id + b.snap.id = b.Bug.Id() return nil } -- cgit v1.2.3 From 8d63c983c982f93cc48d3996d6bd097ddeeb327f Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 3 Jan 2021 23:59:25 +0100 Subject: WIP --- bug/bug.go | 4 +- entity/TODO | 8 + entity/dag/common_test.go | 137 +++++++++ entity/dag/entity.go | 389 ++++++++++++++++++++++++++ entity/dag/entity_actions.go | 227 +++++++++++++++ entity/dag/entity_test.go | 117 ++++++++ entity/dag/operation.go | 31 +++ entity/dag/operation_pack.go | 294 ++++++++++++++++++++ entity/dag/operation_pack_test.go | 44 +++ entity/doc.go | 8 - entity/entity.go | 348 ----------------------- entity/entity_actions.go | 31 --- entity/entity_test.go | 107 ------- entity/merge.go | 14 +- entity/operation_pack.go | 199 ------------- entity/refs.go | 2 + identity/identity.go | 11 +- identity/identity_actions_test.go | 2 +- identity/identity_stub.go | 4 + identity/identity_test.go | 50 ++-- identity/interface.go | 3 + identity/key.go | 187 ++++++++++++- identity/key_test.go | 21 ++ identity/version_test.go | 20 +- repository/common.go | 120 ++++++++ repository/git.go | 570 -------------------------------------- repository/git_cli.go | 56 ---- repository/git_config.go | 221 --------------- repository/git_test.go | 6 - repository/git_testing.go | 72 ----- repository/gogit.go | 92 +++--- repository/gogit_testing.go | 8 +- repository/keyring.go | 10 + repository/mock_repo.go | 177 ++++++------ repository/mock_repo_test.go | 4 +- repository/repo.go | 27 +- repository/repo_testing.go | 24 +- util/lamport/clock_testing.go | 4 +- util/lamport/mem_clock.go | 12 +- 39 files changed, 1846 insertions(+), 1815 deletions(-) create mode 100644 entity/TODO create mode 100644 entity/dag/common_test.go create mode 100644 entity/dag/entity.go create mode 100644 entity/dag/entity_actions.go create mode 100644 entity/dag/entity_test.go create mode 100644 entity/dag/operation.go create mode 100644 entity/dag/operation_pack.go create mode 100644 entity/dag/operation_pack_test.go delete mode 100644 entity/doc.go delete mode 100644 entity/entity.go delete mode 100644 entity/entity_actions.go delete mode 100644 entity/entity_test.go delete mode 100644 entity/operation_pack.go create mode 100644 identity/key_test.go create mode 100644 repository/common.go delete mode 100644 repository/git.go delete mode 100644 repository/git_cli.go delete mode 100644 repository/git_config.go delete mode 100644 repository/git_test.go delete mode 100644 repository/git_testing.go (limited to 'bug/bug.go') diff --git a/bug/bug.go b/bug/bug.go index fb36bfd8..0c66f8ac 100644 --- a/bug/bug.go +++ b/bug/bug.go @@ -426,7 +426,7 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error { // Write a Git commit referencing the tree, with the previous commit as parent if bug.lastCommit != "" { - hash, err = repo.StoreCommitWithParent(hash, bug.lastCommit) + hash, err = repo.StoreCommit(hash, bug.lastCommit) } else { hash, err = repo.StoreCommit(hash) } @@ -524,7 +524,7 @@ func (bug *Bug) Merge(repo repository.Repo, other Interface) (bool, error) { } // create a new commit with the correct ancestor - hash, err := repo.StoreCommitWithParent(treeHash, bug.lastCommit) + hash, err := repo.StoreCommit(treeHash, bug.lastCommit) if err != nil { return false, err diff --git a/entity/TODO b/entity/TODO new file mode 100644 index 00000000..fd3c9710 --- /dev/null +++ b/entity/TODO @@ -0,0 +1,8 @@ +- is the pack Lamport clock really useful vs only topological sort? + - topological order is enforced on the clocks, so what's the point? + - is EditTime equivalent to PackTime? no, avoid the gaps. Is it better? +- how to do commit signature? +- how to avoid id collision between Operations? +- write tests for actions +- migrate Bug to the new structure +- migrate Identity to the new structure? \ No newline at end of file diff --git a/entity/dag/common_test.go b/entity/dag/common_test.go new file mode 100644 index 00000000..29f1279e --- /dev/null +++ b/entity/dag/common_test.go @@ -0,0 +1,137 @@ +package dag + +import ( + "encoding/json" + "fmt" + + "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/repository" +) + +// This file contains an example dummy entity to be used in the tests + +/* + Operations +*/ + +type op1 struct { + author identity.Interface + + OperationType int `json:"type"` + Field1 string `json:"field_1"` +} + +func newOp1(author identity.Interface, field1 string) *op1 { + return &op1{author: author, OperationType: 1, Field1: field1} +} + +func (o op1) Id() entity.Id { + data, _ := json.Marshal(o) + return entity.DeriveId(data) +} + +func (o op1) Author() identity.Interface { + return o.author +} + +func (o op1) Validate() error { return nil } + +type op2 struct { + author identity.Interface + + OperationType int `json:"type"` + Field2 string `json:"field_2"` +} + +func newOp2(author identity.Interface, field2 string) *op2 { + return &op2{author: author, OperationType: 2, Field2: field2} +} + +func (o op2) Id() entity.Id { + data, _ := json.Marshal(o) + return entity.DeriveId(data) +} + +func (o op2) Author() identity.Interface { + return o.author +} + +func (o op2) Validate() error { return nil } + +func unmarshaler(author identity.Interface, raw json.RawMessage) (Operation, error) { + var t struct { + OperationType int `json:"type"` + } + + if err := json.Unmarshal(raw, &t); err != nil { + return nil, err + } + + switch t.OperationType { + case 1: + op := &op1{} + err := json.Unmarshal(raw, &op) + op.author = author + return op, err + case 2: + op := &op2{} + err := json.Unmarshal(raw, &op) + op.author = author + return op, err + default: + return nil, fmt.Errorf("unknown operation type %v", t.OperationType) + } +} + +/* + Identities + repo + definition +*/ + +func makeTestContext() (repository.ClockedRepo, identity.Interface, identity.Interface, Definition) { + repo := repository.NewMockRepo() + + id1, err := identity.NewIdentity(repo, "name1", "email1") + if err != nil { + panic(err) + } + err = id1.Commit(repo) + if err != nil { + panic(err) + } + id2, err := identity.NewIdentity(repo, "name2", "email2") + if err != nil { + panic(err) + } + err = id2.Commit(repo) + if err != nil { + panic(err) + } + + resolver := identityResolverFunc(func(id entity.Id) (identity.Interface, error) { + switch id { + case id1.Id(): + return id1, nil + case id2.Id(): + return id2, nil + default: + return nil, identity.ErrIdentityNotExist + } + }) + + def := Definition{ + typename: "foo", + namespace: "foos", + operationUnmarshaler: unmarshaler, + identityResolver: resolver, + formatVersion: 1, + } + + return repo, id1, id2, def +} + +type identityResolverFunc func(id entity.Id) (identity.Interface, error) + +func (fn identityResolverFunc) ResolveIdentity(id entity.Id) (identity.Interface, error) { + return fn(id) +} diff --git a/entity/dag/entity.go b/entity/dag/entity.go new file mode 100644 index 00000000..78347fa0 --- /dev/null +++ b/entity/dag/entity.go @@ -0,0 +1,389 @@ +// Package dag contains the base common code to define an entity stored +// in a chain of git objects, supporting actions like Push, Pull and Merge. +package dag + +import ( + "encoding/json" + "fmt" + "sort" + + "github.com/pkg/errors" + + "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/util/lamport" +) + +const refsPattern = "refs/%s/%s" +const creationClockPattern = "%s-create" +const editClockPattern = "%s-edit" + +// Definition hold the details defining one specialization of an Entity. +type Definition struct { + // the name of the entity (bug, pull-request, ...) + typename string + // the namespace in git (bugs, prs, ...) + namespace string + // a function decoding a JSON message into an Operation + operationUnmarshaler func(author identity.Interface, raw json.RawMessage) (Operation, error) + // a function loading an identity.Identity from its Id + identityResolver identity.Resolver + // the expected format version number, that can be used for data migration/upgrade + formatVersion uint +} + +// Entity is a data structure stored in a chain of git objects, supporting actions like Push, Pull and Merge. +type Entity struct { + Definition + + // operations that are already stored in the repository + ops []Operation + // operations not yet stored in the repository + staging []Operation + + // TODO: add here createTime and editTime + + // // TODO: doesn't seems to actually be useful over the topological sort ? Timestamp can be generated from graph depth + // // TODO: maybe EditTime is better because it could spread ops in consecutive groups on the logical timeline --> avoid interleaving + // packClock lamport.Clock + lastCommit repository.Hash +} + +// New create an empty Entity +func New(definition Definition) *Entity { + return &Entity{ + Definition: definition, + // packClock: lamport.NewMemClock(), + } +} + +// Read will read and decode a stored Entity from a repository +func Read(def Definition, repo repository.ClockedRepo, id entity.Id) (*Entity, error) { + if err := id.Validate(); err != nil { + return nil, errors.Wrap(err, "invalid id") + } + + ref := fmt.Sprintf("refs/%s/%s", def.namespace, id.String()) + + return read(def, repo, ref) +} + +// read fetch from git and decode an Entity at an arbitrary git reference. +func read(def Definition, repo repository.ClockedRepo, ref string) (*Entity, error) { + rootHash, err := repo.ResolveRef(ref) + if err != nil { + return nil, err + } + + // Perform a depth-first search to get a topological order of the DAG where we discover the + // parents commit and go back in time up to the chronological root + + stack := make([]repository.Hash, 0, 32) + visited := make(map[repository.Hash]struct{}) + DFSOrder := make([]repository.Commit, 0, 32) + + stack = append(stack, rootHash) + + for len(stack) > 0 { + // pop + hash := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + if _, ok := visited[hash]; ok { + continue + } + + // mark as visited + visited[hash] = struct{}{} + + commit, err := repo.ReadCommit(hash) + if err != nil { + return nil, err + } + + DFSOrder = append(DFSOrder, commit) + + for _, parent := range commit.Parents { + stack = append(stack, parent) + } + } + + // Now, we can reverse this topological order and read the commits in an order where + // we are sure to have read all the chronological ancestors when we read a commit. + + // Next step is to: + // 1) read the operationPacks + // 2) make sure that the clocks causality respect the DAG topology. + + oppMap := make(map[repository.Hash]*operationPack) + var opsCount int + // var packClock = lamport.NewMemClock() + + for i := len(DFSOrder) - 1; i >= 0; i-- { + commit := DFSOrder[i] + isFirstCommit := i == len(DFSOrder)-1 + isMerge := len(commit.Parents) > 1 + + // Verify DAG structure: single chronological root, so only the root + // can have no parents. Said otherwise, the DAG need to have exactly + // one leaf. + if !isFirstCommit && len(commit.Parents) == 0 { + return nil, fmt.Errorf("multiple leafs in the entity DAG") + } + + opp, err := readOperationPack(def, repo, commit) + if err != nil { + return nil, err + } + + err = opp.Validate() + if err != nil { + return nil, err + } + + // Check that the create lamport clock is set (not checked in Validate() as it's optional) + if isFirstCommit && opp.CreateTime <= 0 { + return nil, fmt.Errorf("creation lamport time not set") + } + + // make sure that the lamport clocks causality match the DAG topology + for _, parentHash := range commit.Parents { + parentPack, ok := oppMap[parentHash] + if !ok { + panic("DFS failed") + } + + if parentPack.EditTime >= opp.EditTime { + return nil, fmt.Errorf("lamport clock ordering doesn't match the DAG") + } + + // to avoid an attack where clocks are pushed toward the uint64 rollover, make sure + // that the clocks don't jump too far in the future + // we ignore merge commits here to allow merging after a loooong time without breaking anything, + // as long as there is one valid chain of small hops, it's fine. + if !isMerge && opp.EditTime-parentPack.EditTime > 1_000_000 { + return nil, fmt.Errorf("lamport clock jumping too far in the future, likely an attack") + } + + // TODO: PackTime is not checked + } + + oppMap[commit.Hash] = opp + opsCount += len(opp.Operations) + } + + // The clocks are fine, we witness them + for _, opp := range oppMap { + err = repo.Witness(fmt.Sprintf(creationClockPattern, def.namespace), opp.CreateTime) + if err != nil { + return nil, err + } + err = repo.Witness(fmt.Sprintf(editClockPattern, def.namespace), opp.EditTime) + if err != nil { + return nil, err + } + // err = packClock.Witness(opp.PackTime) + // if err != nil { + // return nil, err + // } + } + + // Now that we know that the topological order and clocks are fine, we order the operationPacks + // based on the logical clocks, entirely ignoring the DAG topology + + oppSlice := make([]*operationPack, 0, len(oppMap)) + for _, pack := range oppMap { + oppSlice = append(oppSlice, pack) + } + sort.Slice(oppSlice, func(i, j int) bool { + // Primary ordering with the dedicated "pack" Lamport time that encode causality + // within the entity + // if oppSlice[i].PackTime != oppSlice[j].PackTime { + // return oppSlice[i].PackTime < oppSlice[i].PackTime + // } + // We have equal PackTime, which means we had a concurrent edition. We can't tell which exactly + // came first. As a secondary arbitrary ordering, we can use the EditTime. It's unlikely to be + // enough but it can give us an edge to approach what really happened. + if oppSlice[i].EditTime != oppSlice[j].EditTime { + return oppSlice[i].EditTime < oppSlice[j].EditTime + } + // Well, what now? We still need a total ordering and the most stable possible. + // As a last resort, we can order based on a hash of the serialized Operations in the + // operationPack. It doesn't carry much meaning but it's unbiased and hard to abuse. + // This is a lexicographic ordering on the stringified ID. + return oppSlice[i].Id() < oppSlice[j].Id() + }) + + // Now that we ordered the operationPacks, we have the order of the Operations + + ops := make([]Operation, 0, opsCount) + for _, pack := range oppSlice { + for _, operation := range pack.Operations { + ops = append(ops, operation) + } + } + + return &Entity{ + Definition: def, + ops: ops, + // packClock: packClock, + lastCommit: rootHash, + }, nil +} + +// Id return the Entity identifier +func (e *Entity) Id() entity.Id { + // id is the id of the first operation + return e.FirstOp().Id() +} + +// Validate check if the Entity data is valid +func (e *Entity) Validate() error { + // non-empty + if len(e.ops) == 0 && len(e.staging) == 0 { + return fmt.Errorf("entity has no operations") + } + + // check if each operations are valid + for _, op := range e.ops { + if err := op.Validate(); err != nil { + return err + } + } + + // check if staging is valid if needed + for _, op := range e.staging { + if err := op.Validate(); err != nil { + return err + } + } + + // Check that there is no colliding operation's ID + ids := make(map[entity.Id]struct{}) + for _, op := range e.Operations() { + if _, ok := ids[op.Id()]; ok { + return fmt.Errorf("id collision: %s", op.Id()) + } + ids[op.Id()] = struct{}{} + } + + return nil +} + +// Operations return the ordered operations +func (e *Entity) Operations() []Operation { + return append(e.ops, e.staging...) +} + +// FirstOp lookup for the very first operation of the Entity +func (e *Entity) FirstOp() Operation { + for _, op := range e.ops { + return op + } + for _, op := range e.staging { + return op + } + return nil +} + +// LastOp lookup for the very last operation of the Entity +func (e *Entity) LastOp() Operation { + if len(e.staging) > 0 { + return e.staging[len(e.staging)-1] + } + if len(e.ops) > 0 { + return e.ops[len(e.ops)-1] + } + return nil +} + +// Append add a new Operation to the Entity +func (e *Entity) Append(op Operation) { + e.staging = append(e.staging, op) +} + +// NeedCommit indicate if the in-memory state changed and need to be commit in the repository +func (e *Entity) NeedCommit() bool { + return len(e.staging) > 0 +} + +// CommitAdNeeded execute a Commit only if necessary. This function is useful to avoid getting an error if the Entity +// is already in sync with the repository. +func (e *Entity) CommitAdNeeded(repo repository.ClockedRepo) error { + if e.NeedCommit() { + return e.Commit(repo) + } + return nil +} + +// 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") + } + + if err := e.Validate(); err != nil { + return errors.Wrapf(err, "can't commit a %s with invalid data", e.Definition.typename) + } + + var author identity.Interface + for _, op := range e.staging { + if author != nil && op.Author() != author { + return fmt.Errorf("operations with different author") + } + author = op.Author() + } + + // increment the various clocks for this new operationPack + // packTime, err := e.packClock.Increment() + // if err != nil { + // return err + // } + editTime, err := repo.Increment(fmt.Sprintf(editClockPattern, e.namespace)) + if err != nil { + return err + } + var creationTime lamport.Time + if e.lastCommit == "" { + creationTime, err = repo.Increment(fmt.Sprintf(creationClockPattern, e.namespace)) + if err != nil { + return err + } + } + + opp := &operationPack{ + Author: author, + Operations: e.staging, + CreateTime: creationTime, + EditTime: editTime, + // 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) + } else { + commitHash, err = repo.StoreCommit(treeHash) + } + if err != nil { + return err + } + + e.lastCommit = commitHash + e.ops = append(e.ops, e.staging...) + e.staging = nil + + // Create or update the Git reference for this entity + // When pushing later, the remote will ensure that this ref update + // is fast-forward, that is no data has been overwritten. + ref := fmt.Sprintf(refsPattern, e.namespace, e.Id().String()) + return repo.UpdateRef(ref, commitHash) +} diff --git a/entity/dag/entity_actions.go b/entity/dag/entity_actions.go new file mode 100644 index 00000000..8dcf91e6 --- /dev/null +++ b/entity/dag/entity_actions.go @@ -0,0 +1,227 @@ +package dag + +import ( + "fmt" + + "github.com/pkg/errors" + + "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/repository" +) + +func ListLocalIds(typename string, repo repository.RepoData) ([]entity.Id, error) { + refs, err := repo.ListRefs(fmt.Sprintf("refs/%s/", typename)) + 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) { + // "refs//*:refs/remotes///*" + fetchRefSpec := fmt.Sprintf("refs/%s/*:refs/remotes/%s/%s/*", + def.namespace, remote, def.namespace) + + return repo.FetchRefs(remote, fetchRefSpec) +} + +// Push update a remote with the local changes +func Push(def Definition, repo repository.Repo, remote string) (string, error) { + // "refs//*:refs//*" + refspec := fmt.Sprintf("refs/%s/*:refs/%s/*", + def.namespace, def.namespace) + + return repo.PushRefs(remote, refspec) +} + +// Pull will do a Fetch + MergeAll +// Contrary to MergeAll, this function will return an error if a merge fail. +func Pull(def Definition, repo repository.ClockedRepo, remote string) error { + _, err := Fetch(def, repo, remote) + if err != nil { + return err + } + + for merge := range MergeAll(def, repo, remote) { + if merge.Err != nil { + return merge.Err + } + if merge.Status == entity.MergeStatusInvalid { + return errors.Errorf("merge failure: %s", merge.Reason) + } + } + + return nil +} + +func MergeAll(def Definition, repo repository.ClockedRepo, remote string) <-chan entity.MergeResult { + out := make(chan entity.MergeResult) + + // no caching for the merge, we load everything from git even if that means multiple + // copy of the same entity in memory. The cache layer will intercept the results to + // invalidate entities if necessary. + + 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(def, repo, remoteRef) + } + }() + + return out +} + +func merge(def Definition, repo repository.ClockedRepo, remoteRef string) 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(def, repo, 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()) + + 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 { + err := repo.CopyRef(remoteRef, localRef) + if err != nil { + return entity.NewMergeError(err, id) + } + + return entity.NewMergeStatus(entity.MergeStatusNew, 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) + } + + remoteCommit, err := repo.ResolveRef(remoteRef) + if err != nil { + return entity.NewMergeError(err, id) + } + + if localCommit == remoteCommit { + // nothing to merge + return entity.NewMergeStatus(entity.MergeStatusNothing, id, remoteEntity) + } + + // fast-forward is possible if otherRef include ref + + remoteCommits, err := repo.ListCommits(remoteRef) + if err != nil { + return entity.NewMergeError(err, id) + } + + 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.NewMergeStatus(entity.MergeStatusUpdated, id, remoteEntity) + } + + // 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(def, repo, localRef) + if err != nil { + return entity.NewMergeError(err, id) + } + + // err = localEntity.packClock.Witness(remoteEntity.packClock.Time()) + // if err != nil { + // return entity.NewMergeError(err, id) + // } + // + // packTime, err := localEntity.packClock.Increment() + // 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{ + Operations: nil, + CreateTime: 0, + EditTime: editTime, + // 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) + if err != nil { + return entity.NewMergeError(err, id) + } + + // finally update the ref + err = repo.UpdateRef(localRef, newHash) + if err != nil { + return entity.NewMergeError(err, id) + } + + return entity.NewMergeStatus(entity.MergeStatusUpdated, id, localEntity) +} + +func Remove() error { + panic("") +} diff --git a/entity/dag/entity_test.go b/entity/dag/entity_test.go new file mode 100644 index 00000000..c5c83567 --- /dev/null +++ b/entity/dag/entity_test.go @@ -0,0 +1,117 @@ +package dag + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestWriteRead(t *testing.T) { + repo, id1, id2, def := makeTestContext() + + entity := New(def) + require.False(t, entity.NeedCommit()) + + entity.Append(newOp1(id1, "foo")) + entity.Append(newOp2(id1, "bar")) + + require.True(t, entity.NeedCommit()) + require.NoError(t, entity.CommitAdNeeded(repo)) + require.False(t, entity.NeedCommit()) + + entity.Append(newOp2(id2, "foobar")) + require.True(t, entity.NeedCommit()) + require.NoError(t, entity.CommitAdNeeded(repo)) + require.False(t, entity.NeedCommit()) + + read, err := Read(def, repo, entity.Id()) + require.NoError(t, err) + + assertEqualEntities(t, entity, read) +} + +func assertEqualEntities(t *testing.T, a, b *Entity) { + // testify doesn't support comparing functions and systematically fail if they are not nil + // so we have to set them to nil temporarily + + backOpUnA := a.Definition.operationUnmarshaler + backOpUnB := b.Definition.operationUnmarshaler + + a.Definition.operationUnmarshaler = nil + b.Definition.operationUnmarshaler = nil + + backIdResA := a.Definition.identityResolver + backIdResB := b.Definition.identityResolver + + a.Definition.identityResolver = nil + b.Definition.identityResolver = nil + + defer func() { + a.Definition.operationUnmarshaler = backOpUnA + b.Definition.operationUnmarshaler = backOpUnB + a.Definition.identityResolver = backIdResA + b.Definition.identityResolver = backIdResB + }() + + require.Equal(t, a, b) +} + +// // Merge +// +// merge1 := makeCommit(t, repo) +// merge1 = makeCommit(t, repo, merge1) +// err = repo.UpdateRef("merge1", merge1) +// require.NoError(t, err) +// +// err = repo.UpdateRef("merge2", merge1) +// require.NoError(t, err) +// +// // identical merge +// err = repo.MergeRef("merge1", "merge2") +// require.NoError(t, err) +// +// refMerge1, err := repo.ResolveRef("merge1") +// require.NoError(t, err) +// require.Equal(t, merge1, refMerge1) +// refMerge2, err := repo.ResolveRef("merge2") +// require.NoError(t, err) +// require.Equal(t, merge1, refMerge2) +// +// // fast-forward merge +// merge2 := makeCommit(t, repo, merge1) +// merge2 = makeCommit(t, repo, merge2) +// +// err = repo.UpdateRef("merge2", merge2) +// require.NoError(t, err) +// +// err = repo.MergeRef("merge1", "merge2") +// require.NoError(t, err) +// +// refMerge1, err = repo.ResolveRef("merge1") +// require.NoError(t, err) +// require.Equal(t, merge2, refMerge1) +// refMerge2, err = repo.ResolveRef("merge2") +// require.NoError(t, err) +// require.Equal(t, merge2, refMerge2) +// +// // merge commit +// merge1 = makeCommit(t, repo, merge1) +// err = repo.UpdateRef("merge1", merge1) +// require.NoError(t, err) +// +// merge2 = makeCommit(t, repo, merge2) +// err = repo.UpdateRef("merge2", merge2) +// require.NoError(t, err) +// +// err = repo.MergeRef("merge1", "merge2") +// require.NoError(t, err) +// +// refMerge1, err = repo.ResolveRef("merge1") +// require.NoError(t, err) +// require.NotEqual(t, merge1, refMerge1) +// commitRefMerge1, err := repo.ReadCommit(refMerge1) +// require.NoError(t, err) +// require.ElementsMatch(t, commitRefMerge1.Parents, []Hash{merge1, merge2}) +// refMerge2, err = repo.ResolveRef("merge2") +// require.NoError(t, err) +// require.Equal(t, merge2, refMerge2) diff --git a/entity/dag/operation.go b/entity/dag/operation.go new file mode 100644 index 00000000..9fcc055b --- /dev/null +++ b/entity/dag/operation.go @@ -0,0 +1,31 @@ +package dag + +import ( + "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/identity" +) + +// Operation is a piece of data defining a change to reflect on the state of an Entity. +// What this Operation or Entity's state looks like is not of the resort of this package as it only deals with the +// data structure and storage. +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. + // - collisions can also happen within the set of Operations of an Entity. Simple Operation might not have enough + // entropy to yield unique Ids. + // 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() identity.Interface +} + +type operationBase struct { + author identity.Interface + + // Not serialized. Store the op's id in memory. + id entity.Id +} diff --git a/entity/dag/operation_pack.go b/entity/dag/operation_pack.go new file mode 100644 index 00000000..7cf4ee58 --- /dev/null +++ b/entity/dag/operation_pack.go @@ -0,0 +1,294 @@ +package dag + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/pkg/errors" + "golang.org/x/crypto/openpgp" + + "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/util/lamport" +) + +// TODO: extra data tree +const extraEntryName = "extra" + +const opsEntryName = "ops" +const versionEntryPrefix = "version-" +const createClockEntryPrefix = "create-clock-" +const editClockEntryPrefix = "edit-clock-" +const packClockEntryPrefix = "pack-clock-" + +// operationPack is a wrapper structure to store multiple operations in a single git blob. +// Additionally, it holds and store the metadata for those operations. +type operationPack struct { + // An identifier, taken from a hash of the serialized Operations. + id entity.Id + + // The author of the Operations. Must be the same author for all the Operations. + Author identity.Interface + // The list of Operation stored in the operationPack + Operations []Operation + // Encode the entity's logical time of creation across all entities of the same type. + // Only exist on the root operationPack + CreateTime lamport.Time + // Encode the entity's logical time of last edition across all entities of the same type. + // Exist on all operationPack + EditTime lamport.Time + // // Encode the operationPack's logical time of creation withing this entity. + // // Exist on all operationPack + // PackTime lamport.Time +} + +func (opp *operationPack) Id() entity.Id { + if opp.id == "" || opp.id == entity.UnsetId { + // This means we are trying to get the opp's Id *before* it has been stored. + // As the Id is computed based on the actual bytes written on the disk, we are going to predict + // those and then get the Id. This is safe as it will be the exact same code writing on disk later. + + data, err := json.Marshal(opp) + if err != nil { + panic(err) + } + opp.id = entity.DeriveId(data) + } + + return opp.id +} + +func (opp *operationPack) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Author identity.Interface `json:"author"` + Operations []Operation `json:"ops"` + }{ + Author: opp.Author, + Operations: opp.Operations, + }) +} + +func (opp *operationPack) Validate() error { + if opp.Author == nil { + return fmt.Errorf("missing author") + } + for _, op := range opp.Operations { + if op.Author() != opp.Author { + return fmt.Errorf("operation has different author than the operationPack's") + } + } + if opp.EditTime == 0 { + return fmt.Errorf("lamport edit time is zero") + } + return nil +} + +func (opp *operationPack) Write(def Definition, repo repository.RepoData, parentCommit ...repository.Hash) (repository.Hash, error) { + if err := opp.Validate(); err != nil { + return "", err + } + + // For different reason, we store the clocks and format version directly in the git tree. + // Version has to be accessible before any attempt to decode to return early with a unique error. + // Clocks could possibly be stored in the git blob but it's nice to separate data and metadata, and + // we are storing something directly in the tree already so why not. + // + // To have a valid Tree, we point the "fake" entries to always the same value, the empty blob. + emptyBlobHash, err := repo.StoreData([]byte{}) + if err != nil { + return "", err + } + + // Write the Ops as a Git blob containing the serialized array of operations + data, err := json.Marshal(opp) + if err != nil { + return "", err + } + + // compute the Id while we have the serialized data + opp.id = entity.DeriveId(data) + + hash, err := repo.StoreData(data) + if err != nil { + return "", err + } + + // Make a Git tree referencing this blob and encoding the other values: + // - format version + // - clocks + tree := []repository.TreeEntry{ + {ObjectType: repository.Blob, Hash: emptyBlobHash, + Name: fmt.Sprintf(versionEntryPrefix+"%d", def.formatVersion)}, + {ObjectType: repository.Blob, Hash: hash, + Name: opsEntryName}, + {ObjectType: repository.Blob, Hash: emptyBlobHash, + Name: fmt.Sprintf(editClockEntryPrefix+"%d", opp.EditTime)}, + // {ObjectType: repository.Blob, Hash: emptyBlobHash, + // Name: fmt.Sprintf(packClockEntryPrefix+"%d", opp.PackTime)}, + } + if opp.CreateTime > 0 { + tree = append(tree, repository.TreeEntry{ + ObjectType: repository.Blob, + Hash: emptyBlobHash, + Name: fmt.Sprintf(createClockEntryPrefix+"%d", opp.CreateTime), + }) + } + + // Store the tree + treeHash, err := repo.StoreTree(tree) + if err != nil { + return "", err + } + + // Write a Git commit referencing the tree, with the previous commit as parent + // If we have keys, sign. + 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...) + } else { + commitHash, err = repo.StoreCommit(treeHash, parentCommit...) + } + + if err != nil { + return "", err + } + + return commitHash, nil +} + +// readOperationPack read the operationPack encoded in git at the given Tree hash. +// +// Validity of the Lamport clocks is left for the caller to decide. +func readOperationPack(def Definition, repo repository.RepoData, commit repository.Commit) (*operationPack, error) { + entries, err := repo.ReadTree(commit.TreeHash) + if err != nil { + return nil, err + } + + // check the format version first, fail early instead of trying to read something + var version uint + for _, entry := range entries { + if strings.HasPrefix(entry.Name, versionEntryPrefix) { + v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, versionEntryPrefix), 10, 64) + if err != nil { + return nil, errors.Wrap(err, "can't read format version") + } + if v > 1<<12 { + return nil, fmt.Errorf("format version too big") + } + version = uint(v) + break + } + } + if version == 0 { + return nil, entity.NewErrUnknowFormat(def.formatVersion) + } + if version != def.formatVersion { + return nil, entity.NewErrInvalidFormat(version, def.formatVersion) + } + + var id entity.Id + var author identity.Interface + var ops []Operation + var createTime lamport.Time + var editTime lamport.Time + // var packTime lamport.Time + + for _, entry := range entries { + switch { + case entry.Name == opsEntryName: + data, err := repo.ReadData(entry.Hash) + if err != nil { + return nil, errors.Wrap(err, "failed to read git blob data") + } + ops, author, err = unmarshallPack(def, data) + if err != nil { + return nil, err + } + id = entity.DeriveId(data) + + case strings.HasPrefix(entry.Name, createClockEntryPrefix): + v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, createClockEntryPrefix), 10, 64) + if err != nil { + return nil, errors.Wrap(err, "can't read creation lamport time") + } + createTime = lamport.Time(v) + + case strings.HasPrefix(entry.Name, editClockEntryPrefix): + v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, editClockEntryPrefix), 10, 64) + if err != nil { + return nil, errors.Wrap(err, "can't read edit lamport time") + } + editTime = lamport.Time(v) + + // case strings.HasPrefix(entry.Name, packClockEntryPrefix): + // found &= 1 << 3 + // + // v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, packClockEntryPrefix), 10, 64) + // if err != nil { + // return nil, errors.Wrap(err, "can't read pack lamport time") + // } + // packTime = lamport.Time(v) + } + } + + // Verify signature if we expect one + keys := author.ValidKeysAtTime(fmt.Sprintf(editClockPattern, def.namespace), editTime) + if len(keys) > 0 { + keyring := identity.PGPKeyring(keys) + _, err = openpgp.CheckDetachedSignature(keyring, commit.SignedData, commit.Signature) + if err != nil { + return nil, fmt.Errorf("signature failure: %v", err) + } + } + + return &operationPack{ + id: id, + Author: author, + Operations: ops, + CreateTime: createTime, + EditTime: editTime, + // PackTime: packTime, + }, nil +} + +// unmarshallPack delegate the unmarshalling of the Operation's JSON to the decoding +// function provided by the concrete entity. This gives access to the concrete type of each +// Operation. +func unmarshallPack(def Definition, data []byte) ([]Operation, identity.Interface, error) { + aux := struct { + Author identity.IdentityStub `json:"author"` + Operations []json.RawMessage `json:"ops"` + }{} + + if err := json.Unmarshal(data, &aux); err != nil { + return nil, nil, err + } + + if aux.Author.Id() == "" || aux.Author.Id() == entity.UnsetId { + return nil, nil, fmt.Errorf("missing author") + } + + author, err := def.identityResolver.ResolveIdentity(aux.Author.Id()) + if err != nil { + return nil, nil, err + } + + ops := make([]Operation, 0, len(aux.Operations)) + + for _, raw := range aux.Operations { + // delegate to specialized unmarshal function + op, err := def.operationUnmarshaler(author, raw) + if err != nil { + return nil, nil, err + } + ops = append(ops, op) + } + + return ops, author, nil +} diff --git a/entity/dag/operation_pack_test.go b/entity/dag/operation_pack_test.go new file mode 100644 index 00000000..ad2a9859 --- /dev/null +++ b/entity/dag/operation_pack_test.go @@ -0,0 +1,44 @@ +package dag + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOperationPackReadWrite(t *testing.T) { + repo, id1, _, def := makeTestContext() + + opp := &operationPack{ + Author: id1, + Operations: []Operation{ + newOp1(id1, "foo"), + newOp2(id1, "bar"), + }, + CreateTime: 123, + EditTime: 456, + } + + commitHash, err := opp.Write(def, repo) + require.NoError(t, err) + + commit, err := repo.ReadCommit(commitHash) + require.NoError(t, err) + + opp2, err := readOperationPack(def, repo, commit) + require.NoError(t, err) + + require.Equal(t, opp, opp2) + + // make sure we get the same Id with the same data + opp3 := &operationPack{ + Author: id1, + Operations: []Operation{ + newOp1(id1, "foo"), + newOp2(id1, "bar"), + }, + CreateTime: 123, + EditTime: 456, + } + require.Equal(t, opp.Id(), opp3.Id()) +} diff --git a/entity/doc.go b/entity/doc.go deleted file mode 100644 index 4682d545..00000000 --- a/entity/doc.go +++ /dev/null @@ -1,8 +0,0 @@ -// Package entity contains the base common code to define an entity stored -// in a chain of git objects, supporting actions like Push, Pull and Merge. -package entity - -// TODO: Bug and Identity are very similar, right ? I expect that this package -// will eventually hold the common code to define an entity and the related -// helpers, errors and so on. When this work is done, it will become easier -// to add new entities, for example to support pull requests. diff --git a/entity/entity.go b/entity/entity.go deleted file mode 100644 index a1e8e57e..00000000 --- a/entity/entity.go +++ /dev/null @@ -1,348 +0,0 @@ -package entity - -import ( - "encoding/json" - "fmt" - "sort" - - "github.com/pkg/errors" - - "github.com/MichaelMure/git-bug/repository" - "github.com/MichaelMure/git-bug/util/lamport" -) - -const refsPattern = "refs/%s/%s" -const creationClockPattern = "%s-create" -const editClockPattern = "%s-edit" - -type Operation interface { - Id() Id - // MarshalJSON() ([]byte, error) - Validate() error -} - -// Definition hold the details defining one specialization of an Entity. -type Definition struct { - // the name of the entity (bug, pull-request, ...) - typename string - // the namespace in git (bugs, prs, ...) - namespace string - // a function decoding a JSON message into an Operation - operationUnmarshaler func(raw json.RawMessage) (Operation, error) - // the expected format version number - formatVersion uint -} - -type Entity struct { - Definition - - ops []Operation - staging []Operation - - packClock lamport.Clock - lastCommit repository.Hash -} - -func New(definition Definition) *Entity { - return &Entity{ - Definition: definition, - packClock: lamport.NewMemClock(), - } -} - -func Read(def Definition, repo repository.ClockedRepo, id Id) (*Entity, error) { - if err := id.Validate(); err != nil { - return nil, errors.Wrap(err, "invalid id") - } - - ref := fmt.Sprintf("refs/%s/%s", def.namespace, id.String()) - - return read(def, repo, ref) -} - -// read fetch from git and decode an Entity at an arbitrary git reference. -func read(def Definition, repo repository.ClockedRepo, ref string) (*Entity, error) { - rootHash, err := repo.ResolveRef(ref) - if err != nil { - return nil, err - } - - // Perform a depth-first search to get a topological order of the DAG where we discover the - // parents commit and go back in time up to the chronological root - - stack := make([]repository.Hash, 0, 32) - visited := make(map[repository.Hash]struct{}) - DFSOrder := make([]repository.Commit, 0, 32) - - stack = append(stack, rootHash) - - for len(stack) > 0 { - // pop - hash := stack[len(stack)-1] - stack = stack[:len(stack)-1] - - if _, ok := visited[hash]; ok { - continue - } - - // mark as visited - visited[hash] = struct{}{} - - commit, err := repo.ReadCommit(hash) - if err != nil { - return nil, err - } - - DFSOrder = append(DFSOrder, commit) - - for _, parent := range commit.Parents { - stack = append(stack, parent) - } - } - - // Now, we can reverse this topological order and read the commits in an order where - // we are sure to have read all the chronological ancestors when we read a commit. - - // Next step is to: - // 1) read the operationPacks - // 2) make sure that the clocks causality respect the DAG topology. - - oppMap := make(map[repository.Hash]*operationPack) - var opsCount int - var packClock = lamport.NewMemClock() - - for i := len(DFSOrder) - 1; i >= 0; i-- { - commit := DFSOrder[i] - firstCommit := i == len(DFSOrder)-1 - - // Verify DAG structure: single chronological root, so only the root - // can have no parents - if !firstCommit && len(commit.Parents) == 0 { - return nil, fmt.Errorf("multiple root in the entity DAG") - } - - opp, err := readOperationPack(def, repo, commit.TreeHash) - if err != nil { - return nil, err - } - - // Check that the lamport clocks are set - if firstCommit && opp.CreateTime <= 0 { - return nil, fmt.Errorf("creation lamport time not set") - } - if opp.EditTime <= 0 { - return nil, fmt.Errorf("edition lamport time not set") - } - if opp.PackTime <= 0 { - return nil, fmt.Errorf("pack lamport time not set") - } - - // make sure that the lamport clocks causality match the DAG topology - for _, parentHash := range commit.Parents { - parentPack, ok := oppMap[parentHash] - if !ok { - panic("DFS failed") - } - - if parentPack.EditTime >= opp.EditTime { - return nil, fmt.Errorf("lamport clock ordering doesn't match the DAG") - } - - // to avoid an attack where clocks are pushed toward the uint64 rollover, make sure - // that the clocks don't jump too far in the future - if opp.EditTime-parentPack.EditTime > 10_000 { - return nil, fmt.Errorf("lamport clock jumping too far in the future, likely an attack") - } - } - - oppMap[commit.Hash] = opp - opsCount += len(opp.Operations) - } - - // The clocks are fine, we witness them - for _, opp := range oppMap { - err = repo.Witness(fmt.Sprintf(creationClockPattern, def.namespace), opp.CreateTime) - if err != nil { - return nil, err - } - err = repo.Witness(fmt.Sprintf(editClockPattern, def.namespace), opp.EditTime) - if err != nil { - return nil, err - } - err = packClock.Witness(opp.PackTime) - if err != nil { - return nil, err - } - } - - // Now that we know that the topological order and clocks are fine, we order the operationPacks - // based on the logical clocks, entirely ignoring the DAG topology - - oppSlice := make([]*operationPack, 0, len(oppMap)) - for _, pack := range oppMap { - oppSlice = append(oppSlice, pack) - } - sort.Slice(oppSlice, func(i, j int) bool { - // Primary ordering with the dedicated "pack" Lamport time that encode causality - // within the entity - if oppSlice[i].PackTime != oppSlice[j].PackTime { - return oppSlice[i].PackTime < oppSlice[i].PackTime - } - // We have equal PackTime, which means we had a concurrent edition. We can't tell which exactly - // came first. As a secondary arbitrary ordering, we can use the EditTime. It's unlikely to be - // enough but it can give us an edge to approach what really happened. - if oppSlice[i].EditTime != oppSlice[j].EditTime { - return oppSlice[i].EditTime < oppSlice[j].EditTime - } - // Well, what now? We still need a total ordering, the most stable possible. - // As a last resort, we can order based on a hash of the serialized Operations in the - // operationPack. It doesn't carry much meaning but it's unbiased and hard to abuse. - // This is a lexicographic ordering. - return oppSlice[i].Id < oppSlice[j].Id - }) - - // Now that we ordered the operationPacks, we have the order of the Operations - - ops := make([]Operation, 0, opsCount) - for _, pack := range oppSlice { - for _, operation := range pack.Operations { - ops = append(ops, operation) - } - } - - return &Entity{ - Definition: def, - ops: ops, - lastCommit: rootHash, - }, nil -} - -// Id return the Entity identifier -func (e *Entity) Id() Id { - // id is the id of the first operation - return e.FirstOp().Id() -} - -func (e *Entity) Validate() error { - // non-empty - if len(e.ops) == 0 && len(e.staging) == 0 { - return fmt.Errorf("entity has no operations") - } - - // check if each operations are valid - for _, op := range e.ops { - if err := op.Validate(); err != nil { - return err - } - } - - // check if staging is valid if needed - for _, op := range e.staging { - if err := op.Validate(); err != nil { - return err - } - } - - // Check that there is no colliding operation's ID - ids := make(map[Id]struct{}) - for _, op := range e.Operations() { - if _, ok := ids[op.Id()]; ok { - return fmt.Errorf("id collision: %s", op.Id()) - } - ids[op.Id()] = struct{}{} - } - - return nil -} - -// return the ordered operations -func (e *Entity) Operations() []Operation { - return append(e.ops, e.staging...) -} - -// Lookup for the very first operation of the Entity. -func (e *Entity) FirstOp() Operation { - for _, op := range e.ops { - return op - } - for _, op := range e.staging { - return op - } - return nil -} - -func (e *Entity) Append(op Operation) { - e.staging = append(e.staging, op) -} - -func (e *Entity) NeedCommit() bool { - return len(e.staging) > 0 -} - -func (e *Entity) CommitAdNeeded(repo repository.ClockedRepo) error { - if e.NeedCommit() { - return e.Commit(repo) - } - return nil -} - -// 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") - } - - if err := e.Validate(); err != nil { - return errors.Wrapf(err, "can't commit a %s with invalid data", e.Definition.typename) - } - - // increment the various clocks for this new operationPack - packTime, err := e.packClock.Increment() - if err != nil { - return err - } - editTime, err := repo.Increment(fmt.Sprintf(editClockPattern, e.namespace)) - if err != nil { - return err - } - var creationTime lamport.Time - if e.lastCommit == "" { - creationTime, err = repo.Increment(fmt.Sprintf(creationClockPattern, e.namespace)) - if err != nil { - return err - } - } - - opp := &operationPack{ - Operations: e.staging, - CreateTime: creationTime, - EditTime: editTime, - 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.StoreCommitWithParent(treeHash, e.lastCommit) - } else { - commitHash, err = repo.StoreCommit(treeHash) - } - if err != nil { - return err - } - - e.lastCommit = commitHash - e.ops = append(e.ops, e.staging...) - e.staging = nil - - // Create or update the Git reference for this entity - // When pushing later, the remote will ensure that this ref update - // is fast-forward, that is no data has been overwritten. - ref := fmt.Sprintf(refsPattern, e.namespace, e.Id().String()) - return repo.UpdateRef(ref, commitHash) -} diff --git a/entity/entity_actions.go b/entity/entity_actions.go deleted file mode 100644 index 34e76a62..00000000 --- a/entity/entity_actions.go +++ /dev/null @@ -1,31 +0,0 @@ -package entity - -import ( - "fmt" - - "github.com/MichaelMure/git-bug/repository" -) - -func ListLocalIds(typename string, repo repository.RepoData) ([]Id, error) { - refs, err := repo.ListRefs(fmt.Sprintf("refs/%s/", typename)) - if err != nil { - return nil, err - } - return RefsToIds(refs), nil -} - -func Fetch() { - -} - -func Pull() { - -} - -func Push() { - -} - -func Remove() error { - panic("") -} diff --git a/entity/entity_test.go b/entity/entity_test.go deleted file mode 100644 index 92a53179..00000000 --- a/entity/entity_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package entity - -import ( - "encoding/json" - "fmt" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/MichaelMure/git-bug/repository" -) - -// func TestFoo(t *testing.T) { -// repo, err := repository.OpenGoGitRepo("~/dev/git-bug", nil) -// require.NoError(t, err) -// -// b, err := ReadBug(repo, Id("8b22e548c93a6ed23c31fd4e337c6286c3d1e5c9cae5537bc8e5842e11bd1099")) -// require.NoError(t, err) -// -// fmt.Println(b) -// } - -type op1 struct { - OperationType int `json:"type"` - Field1 string `json:"field_1"` -} - -func newOp1(field1 string) *op1 { - return &op1{OperationType: 1, Field1: field1} -} - -func (o op1) Id() Id { - data, _ := json.Marshal(o) - return DeriveId(data) -} - -func (o op1) Validate() error { return nil } - -type op2 struct { - OperationType int `json:"type"` - Field2 string `json:"field_2"` -} - -func newOp2(field2 string) *op2 { - return &op2{OperationType: 2, Field2: field2} -} - -func (o op2) Id() Id { - data, _ := json.Marshal(o) - return DeriveId(data) -} - -func (o op2) Validate() error { return nil } - -var def = Definition{ - typename: "foo", - namespace: "foos", - operationUnmarshaler: unmarshaller, - formatVersion: 1, -} - -func unmarshaller(raw json.RawMessage) (Operation, error) { - var t struct { - OperationType int `json:"type"` - } - - if err := json.Unmarshal(raw, &t); err != nil { - return nil, err - } - - switch t.OperationType { - case 1: - op := &op1{} - err := json.Unmarshal(raw, &op) - return op, err - case 2: - op := &op2{} - err := json.Unmarshal(raw, &op) - return op, err - default: - return nil, fmt.Errorf("unknown operation type %v", t.OperationType) - } -} - -func TestWriteRead(t *testing.T) { - repo := repository.NewMockRepo() - - entity := New(def) - require.False(t, entity.NeedCommit()) - - entity.Append(newOp1("foo")) - entity.Append(newOp2("bar")) - - require.True(t, entity.NeedCommit()) - require.NoError(t, entity.CommitAdNeeded(repo)) - require.False(t, entity.NeedCommit()) - - entity.Append(newOp2("foobar")) - require.True(t, entity.NeedCommit()) - require.NoError(t, entity.CommitAdNeeded(repo)) - require.False(t, entity.NeedCommit()) - - read, err := Read(def, repo, entity.Id()) - require.NoError(t, err) - - fmt.Println(*read) -} diff --git a/entity/merge.go b/entity/merge.go index 3ce8edac..7d1f3f43 100644 --- a/entity/merge.go +++ b/entity/merge.go @@ -8,14 +8,15 @@ import ( type MergeStatus int const ( - _ MergeStatus = iota - MergeStatusNew - MergeStatusInvalid - MergeStatusUpdated - MergeStatusNothing - MergeStatusError + _ MergeStatus = iota + MergeStatusNew // a new Entity was created locally + MergeStatusInvalid // the remote data is invalid + MergeStatusUpdated // a local Entity has been updated + MergeStatusNothing // no changes were made to a local Entity (already up to date) + MergeStatusError // a terminal error happened ) +// MergeResult hold the result of a merge operation on an Entity. type MergeResult struct { // Err is set when a terminal error occur in the process Err error @@ -55,6 +56,7 @@ func NewMergeError(err error, id Id) MergeResult { } } +// TODO: Interface --> *Entity ? func NewMergeStatus(status MergeStatus, id Id, entity Interface) MergeResult { return MergeResult{ Id: id, diff --git a/entity/operation_pack.go b/entity/operation_pack.go deleted file mode 100644 index 0a16dd61..00000000 --- a/entity/operation_pack.go +++ /dev/null @@ -1,199 +0,0 @@ -package entity - -import ( - "encoding/json" - "fmt" - "strconv" - "strings" - - "github.com/pkg/errors" - - "github.com/MichaelMure/git-bug/repository" - "github.com/MichaelMure/git-bug/util/lamport" -) - -// TODO: extra data tree -const extraEntryName = "extra" - -const opsEntryName = "ops" -const versionEntryPrefix = "version-" -const createClockEntryPrefix = "create-clock-" -const editClockEntryPrefix = "edit-clock-" -const packClockEntryPrefix = "pack-clock-" - -type operationPack struct { - Operations []Operation - // Encode the entity's logical time of creation across all entities of the same type. - // Only exist on the root operationPack - CreateTime lamport.Time - // Encode the entity's logical time of last edition across all entities of the same type. - // Exist on all operationPack - EditTime lamport.Time - // Encode the operationPack's logical time of creation withing this entity. - // Exist on all operationPack - PackTime lamport.Time -} - -func (opp operationPack) write(def Definition, repo repository.RepoData) (repository.Hash, error) { - // For different reason, we store the clocks and format version directly in the git tree. - // Version has to be accessible before any attempt to decode to return early with a unique error. - // Clocks could possibly be stored in the git blob but it's nice to separate data and metadata, and - // we are storing something directly in the tree already so why not. - // - // To have a valid Tree, we point the "fake" entries to always the same value, the empty blob. - emptyBlobHash, err := repo.StoreData([]byte{}) - if err != nil { - return "", err - } - - // Write the Ops as a Git blob containing the serialized array - data, err := json.Marshal(struct { - Operations []Operation `json:"ops"` - }{ - Operations: opp.Operations, - }) - if err != nil { - return "", err - } - hash, err := repo.StoreData(data) - if err != nil { - return "", err - } - - // Make a Git tree referencing this blob and encoding the other values: - // - format version - // - clocks - tree := []repository.TreeEntry{ - {ObjectType: repository.Blob, Hash: emptyBlobHash, - Name: fmt.Sprintf(versionEntryPrefix+"%d", def.formatVersion)}, - {ObjectType: repository.Blob, Hash: hash, - Name: opsEntryName}, - {ObjectType: repository.Blob, Hash: emptyBlobHash, - Name: fmt.Sprintf(editClockEntryPrefix+"%d", opp.EditTime)}, - {ObjectType: repository.Blob, Hash: emptyBlobHash, - Name: fmt.Sprintf(packClockEntryPrefix+"%d", opp.PackTime)}, - } - if opp.CreateTime > 0 { - tree = append(tree, repository.TreeEntry{ - ObjectType: repository.Blob, - Hash: emptyBlobHash, - Name: fmt.Sprintf(createClockEntryPrefix+"%d", opp.CreateTime), - }) - } - - // Store the tree - return repo.StoreTree(tree) -} - -// readOperationPack read the operationPack encoded in git at the given Tree hash. -// -// Validity of the Lamport clocks is left for the caller to decide. -func readOperationPack(def Definition, repo repository.RepoData, treeHash repository.Hash) (*operationPack, error) { - entries, err := repo.ReadTree(treeHash) - if err != nil { - return nil, err - } - - // check the format version first, fail early instead of trying to read something - var version uint - for _, entry := range entries { - if strings.HasPrefix(entry.Name, versionEntryPrefix) { - v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, versionEntryPrefix), 10, 64) - if err != nil { - return nil, errors.Wrap(err, "can't read format version") - } - if v > 1<<12 { - return nil, fmt.Errorf("format version too big") - } - version = uint(v) - break - } - } - if version == 0 { - return nil, NewErrUnknowFormat(def.formatVersion) - } - if version != def.formatVersion { - return nil, NewErrInvalidFormat(version, def.formatVersion) - } - - var ops []Operation - var createTime lamport.Time - var editTime lamport.Time - var packTime lamport.Time - - for _, entry := range entries { - if entry.Name == opsEntryName { - data, err := repo.ReadData(entry.Hash) - if err != nil { - return nil, errors.Wrap(err, "failed to read git blob data") - } - - ops, err = unmarshallOperations(def, data) - if err != nil { - return nil, err - } - continue - } - - if strings.HasPrefix(entry.Name, createClockEntryPrefix) { - v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, createClockEntryPrefix), 10, 64) - if err != nil { - return nil, errors.Wrap(err, "can't read creation lamport time") - } - createTime = lamport.Time(v) - continue - } - - if strings.HasPrefix(entry.Name, editClockEntryPrefix) { - v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, editClockEntryPrefix), 10, 64) - if err != nil { - return nil, errors.Wrap(err, "can't read edit lamport time") - } - editTime = lamport.Time(v) - continue - } - - if strings.HasPrefix(entry.Name, packClockEntryPrefix) { - v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, packClockEntryPrefix), 10, 64) - if err != nil { - return nil, errors.Wrap(err, "can't read pack lamport time") - } - packTime = lamport.Time(v) - continue - } - } - - return &operationPack{ - Operations: ops, - CreateTime: createTime, - EditTime: editTime, - PackTime: packTime, - }, nil -} - -// unmarshallOperations delegate the unmarshalling of the Operation's JSON to the decoding -// function provided by the concrete entity. This gives access to the concrete type of each -// Operation. -func unmarshallOperations(def Definition, data []byte) ([]Operation, error) { - aux := struct { - Operations []json.RawMessage `json:"ops"` - }{} - - if err := json.Unmarshal(data, &aux); err != nil { - return nil, err - } - - ops := make([]Operation, 0, len(aux.Operations)) - - for _, raw := range aux.Operations { - // delegate to specialized unmarshal function - op, err := def.operationUnmarshaler(raw) - if err != nil { - return nil, err - } - - ops = append(ops, op) - } - - return ops, nil -} diff --git a/entity/refs.go b/entity/refs.go index f505dbf0..070d4dba 100644 --- a/entity/refs.go +++ b/entity/refs.go @@ -2,6 +2,7 @@ package entity import "strings" +// RefsToIds parse a slice of git references and return the corresponding Entity's Id. func RefsToIds(refs []string) []Id { ids := make([]Id, len(refs)) @@ -12,6 +13,7 @@ func RefsToIds(refs []string) []Id { return ids } +// RefsToIds parse a git reference and return the corresponding Entity's Id. func RefToId(ref string) Id { split := strings.Split(ref, "/") return Id(split[len(split)-1]) diff --git a/identity/identity.go b/identity/identity.go index ef488712..65019041 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -344,7 +344,7 @@ func (i *Identity) Commit(repo repository.ClockedRepo) error { var commitHash repository.Hash if lastCommit != "" { - commitHash, err = repo.StoreCommitWithParent(treeHash, lastCommit) + commitHash, err = repo.StoreCommit(treeHash, lastCommit) } else { commitHash, err = repo.StoreCommit(treeHash) } @@ -518,6 +518,15 @@ func (i *Identity) Keys() []*Key { return i.lastVersion().keys } +// SigningKey return the key that should be used to sign new messages. If no key is available, return nil. +func (i *Identity) SigningKey() *Key { + keys := i.Keys() + if len(keys) > 0 { + return keys[0] + } + return nil +} + // ValidKeysAtTime return the set of keys valid at a given lamport time func (i *Identity) ValidKeysAtTime(clockName string, time lamport.Time) []*Key { var result []*Key diff --git a/identity/identity_actions_test.go b/identity/identity_actions_test.go index 63f6aacd..54cb2a46 100644 --- a/identity/identity_actions_test.go +++ b/identity/identity_actions_test.go @@ -9,7 +9,7 @@ import ( ) func TestPushPull(t *testing.T) { - repoA, repoB, remote := repository.SetupReposAndRemote() + repoA, repoB, remote := repository.SetupGoGitReposAndRemote() defer repository.CleanupTestRepos(repoA, repoB, remote) identity1, err := NewIdentity(repoA, "name1", "email1") diff --git a/identity/identity_stub.go b/identity/identity_stub.go index fec92010..91945378 100644 --- a/identity/identity_stub.go +++ b/identity/identity_stub.go @@ -71,6 +71,10 @@ func (IdentityStub) Keys() []*Key { panic("identities needs to be properly loaded with identity.ReadLocal()") } +func (i *IdentityStub) SigningKey() *Key { + panic("identities needs to be properly loaded with identity.ReadLocal()") +} + func (IdentityStub) ValidKeysAtTime(_ string, _ lamport.Time) []*Key { panic("identities needs to be properly loaded with identity.ReadLocal()") } diff --git a/identity/identity_test.go b/identity/identity_test.go index ad8317ce..2cdb4b36 100644 --- a/identity/identity_test.go +++ b/identity/identity_test.go @@ -36,18 +36,18 @@ func TestIdentityCommitLoad(t *testing.T) { // multiple versions - identity, err = NewIdentityFull(repo, "René Descartes", "rene.descartes@example.com", "", "", []*Key{{PubKey: "pubkeyA"}}) + identity, err = NewIdentityFull(repo, "René Descartes", "rene.descartes@example.com", "", "", []*Key{generatePublicKey()}) require.NoError(t, err) idBeforeCommit = identity.Id() err = identity.Mutate(repo, func(orig *Mutator) { - orig.Keys = []*Key{{PubKey: "pubkeyB"}} + orig.Keys = []*Key{generatePublicKey()} }) require.NoError(t, err) err = identity.Mutate(repo, func(orig *Mutator) { - orig.Keys = []*Key{{PubKey: "pubkeyC"}} + orig.Keys = []*Key{generatePublicKey()} }) require.NoError(t, err) @@ -70,13 +70,13 @@ func TestIdentityCommitLoad(t *testing.T) { err = identity.Mutate(repo, func(orig *Mutator) { orig.Email = "rene@descartes.com" - orig.Keys = []*Key{{PubKey: "pubkeyD"}} + orig.Keys = []*Key{generatePublicKey()} }) require.NoError(t, err) err = identity.Mutate(repo, func(orig *Mutator) { orig.Email = "rene@descartes.com" - orig.Keys = []*Key{{PubKey: "pubkeyD"}, {PubKey: "pubkeyE"}} + orig.Keys = []*Key{generatePublicKey(), generatePublicKey()} }) require.NoError(t, err) @@ -123,49 +123,45 @@ func commitsAreSet(t *testing.T, identity *Identity) { // Test that the correct crypto keys are returned for a given lamport time func TestIdentity_ValidKeysAtTime(t *testing.T) { + pubKeyA := generatePublicKey() + pubKeyB := generatePublicKey() + pubKeyC := generatePublicKey() + pubKeyD := generatePublicKey() + pubKeyE := generatePublicKey() + identity := Identity{ versions: []*version{ { times: map[string]lamport.Time{"foo": 100}, - keys: []*Key{ - {PubKey: "pubkeyA"}, - }, + keys: []*Key{pubKeyA}, }, { times: map[string]lamport.Time{"foo": 200}, - keys: []*Key{ - {PubKey: "pubkeyB"}, - }, + keys: []*Key{pubKeyB}, }, { times: map[string]lamport.Time{"foo": 201}, - keys: []*Key{ - {PubKey: "pubkeyC"}, - }, + keys: []*Key{pubKeyC}, }, { times: map[string]lamport.Time{"foo": 201}, - keys: []*Key{ - {PubKey: "pubkeyD"}, - }, + keys: []*Key{pubKeyD}, }, { times: map[string]lamport.Time{"foo": 300}, - keys: []*Key{ - {PubKey: "pubkeyE"}, - }, + keys: []*Key{pubKeyE}, }, }, } require.Nil(t, identity.ValidKeysAtTime("foo", 10)) - require.Equal(t, identity.ValidKeysAtTime("foo", 100), []*Key{{PubKey: "pubkeyA"}}) - require.Equal(t, identity.ValidKeysAtTime("foo", 140), []*Key{{PubKey: "pubkeyA"}}) - require.Equal(t, identity.ValidKeysAtTime("foo", 200), []*Key{{PubKey: "pubkeyB"}}) - require.Equal(t, identity.ValidKeysAtTime("foo", 201), []*Key{{PubKey: "pubkeyD"}}) - require.Equal(t, identity.ValidKeysAtTime("foo", 202), []*Key{{PubKey: "pubkeyD"}}) - require.Equal(t, identity.ValidKeysAtTime("foo", 300), []*Key{{PubKey: "pubkeyE"}}) - require.Equal(t, identity.ValidKeysAtTime("foo", 3000), []*Key{{PubKey: "pubkeyE"}}) + require.Equal(t, identity.ValidKeysAtTime("foo", 100), []*Key{pubKeyA}) + require.Equal(t, identity.ValidKeysAtTime("foo", 140), []*Key{pubKeyA}) + require.Equal(t, identity.ValidKeysAtTime("foo", 200), []*Key{pubKeyB}) + require.Equal(t, identity.ValidKeysAtTime("foo", 201), []*Key{pubKeyD}) + require.Equal(t, identity.ValidKeysAtTime("foo", 202), []*Key{pubKeyD}) + require.Equal(t, identity.ValidKeysAtTime("foo", 300), []*Key{pubKeyE}) + require.Equal(t, identity.ValidKeysAtTime("foo", 3000), []*Key{pubKeyE}) } // Test the immutable or mutable metadata search diff --git a/identity/interface.go b/identity/interface.go index 92a03c51..528cb067 100644 --- a/identity/interface.go +++ b/identity/interface.go @@ -36,6 +36,9 @@ type Interface interface { // Can be empty. Keys() []*Key + // SigningKey return the key that should be used to sign new messages. If no key is available, return nil. + SigningKey() *Key + // ValidKeysAtTime return the set of keys valid at a given lamport time for a given clock of another entity // Can be empty. ValidKeysAtTime(clockName string, time lamport.Time) []*Key diff --git a/identity/key.go b/identity/key.go index cc948394..8dd5e8c1 100644 --- a/identity/key.go +++ b/identity/key.go @@ -1,18 +1,193 @@ package identity +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "strings" + "time" + + "github.com/pkg/errors" + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" + "golang.org/x/crypto/openpgp/packet" + + "github.com/MichaelMure/git-bug/repository" +) + type Key struct { - // The GPG fingerprint of the key - Fingerprint string `json:"fingerprint"` - PubKey string `json:"pub_key"` + public *packet.PublicKey + private *packet.PrivateKey +} + +// GenerateKey generate a keypair (public+private) +func GenerateKey() *Key { + entity, err := openpgp.NewEntity("", "", "", &packet.Config{ + // The armored format doesn't include the creation time, which makes the round-trip data not being fully equal. + // We don't care about the creation time so we can set it to the zero value. + Time: func() time.Time { + return time.Time{} + }, + }) + if err != nil { + panic(err) + } + return &Key{ + public: entity.PrimaryKey, + private: entity.PrivateKey, + } +} + +// generatePublicKey generate only a public key (only useful for testing) +// See GenerateKey for the details. +func generatePublicKey() *Key { + k := GenerateKey() + k.private = nil + return k +} + +func (k *Key) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + w, err := armor.Encode(&buf, openpgp.PublicKeyType, nil) + if err != nil { + return nil, err + } + err = k.public.Serialize(w) + if err != nil { + return nil, err + } + err = w.Close() + if err != nil { + return nil, err + } + return json.Marshal(buf.String()) +} + +func (k *Key) UnmarshalJSON(data []byte) error { + var armored string + err := json.Unmarshal(data, &armored) + if err != nil { + return err + } + + block, err := armor.Decode(strings.NewReader(armored)) + if err == io.EOF { + return fmt.Errorf("no armored data found") + } + if err != nil { + return err + } + + if block.Type != openpgp.PublicKeyType { + return fmt.Errorf("invalid key type") + } + + reader := packet.NewReader(block.Body) + p, err := reader.Next() + if err != nil { + return errors.Wrap(err, "failed to read public key packet") + } + + public, ok := p.(*packet.PublicKey) + if !ok { + return errors.New("got no packet.publicKey") + } + + // The armored format doesn't include the creation time, which makes the round-trip data not being fully equal. + // We don't care about the creation time so we can set it to the zero value. + public.CreationTime = time.Time{} + + k.public = public + return nil } func (k *Key) Validate() error { - // Todo + if k.public == nil { + return fmt.Errorf("nil public key") + } + if !k.public.CanSign() { + return fmt.Errorf("public key can't sign") + } + + if k.private != nil { + if !k.private.CanSign() { + return fmt.Errorf("private key can't sign") + } + } return nil } func (k *Key) Clone() *Key { - clone := *k - return &clone + clone := &Key{} + + pub := *k.public + clone.public = &pub + + if k.private != nil { + priv := *k.private + clone.private = &priv + } + + return clone +} + +func (k *Key) EnsurePrivateKey(repo repository.RepoKeyring) error { + if k.private != nil { + return nil + } + + // item, err := repo.Keyring().Get(k.Fingerprint()) + // if err != nil { + // return fmt.Errorf("no private key found for %s", k.Fingerprint()) + // } + // + + panic("TODO") +} + +func (k *Key) Fingerprint() string { + return string(k.public.Fingerprint[:]) +} + +func (k *Key) PGPEntity() *openpgp.Entity { + return &openpgp.Entity{ + PrimaryKey: k.public, + PrivateKey: k.private, + } +} + +var _ openpgp.KeyRing = &PGPKeyring{} + +// PGPKeyring implement a openpgp.KeyRing from an slice of Key +type PGPKeyring []*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 } diff --git a/identity/key_test.go b/identity/key_test.go new file mode 100644 index 00000000..3206c34e --- /dev/null +++ b/identity/key_test.go @@ -0,0 +1,21 @@ +package identity + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestKeyJSON(t *testing.T) { + k := generatePublicKey() + + data, err := json.Marshal(k) + require.NoError(t, err) + + var read Key + err = json.Unmarshal(data, &read) + require.NoError(t, err) + + require.Equal(t, k, &read) +} diff --git a/identity/version_test.go b/identity/version_test.go index 1efa0d03..385ad4d7 100644 --- a/identity/version_test.go +++ b/identity/version_test.go @@ -18,29 +18,23 @@ func makeIdentityTestRepo(t *testing.T) repository.ClockedRepo { clock1, err := repo.GetOrCreateClock("foo") require.NoError(t, err) - err = clock1.Witness(42) // clock goes to 43 + err = clock1.Witness(42) require.NoError(t, err) clock2, err := repo.GetOrCreateClock("bar") require.NoError(t, err) - err = clock2.Witness(34) // clock goes to 35 + err = clock2.Witness(34) require.NoError(t, err) return repo } -func TestVersionSerialize(t *testing.T) { +func TestVersionJSON(t *testing.T) { repo := makeIdentityTestRepo(t) keys := []*Key{ - { - Fingerprint: "fingerprint1", - PubKey: "pubkey1", - }, - { - Fingerprint: "fingerprint2", - PubKey: "pubkey2", - }, + generatePublicKey(), + generatePublicKey(), } before, err := newVersion(repo, "name", "email", "login", "avatarUrl", keys) @@ -57,8 +51,8 @@ func TestVersionSerialize(t *testing.T) { avatarURL: "avatarUrl", unixTime: time.Now().Unix(), times: map[string]lamport.Time{ - "foo": 43, - "bar": 35, + "foo": 42, + "bar": 34, }, keys: keys, nonce: before.nonce, diff --git a/repository/common.go b/repository/common.go new file mode 100644 index 00000000..7fd7ae19 --- /dev/null +++ b/repository/common.go @@ -0,0 +1,120 @@ +package repository + +import ( + "io" + + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" + "golang.org/x/crypto/openpgp/errors" +) + +// nonNativeMerge is an implementation of a branch merge, for the case where +// the underlying git implementation doesn't support it natively. +func nonNativeMerge(repo RepoData, ref string, otherRef string, treeHashFn func() Hash) error { + commit, err := repo.ResolveRef(ref) + if err != nil { + return err + } + + otherCommit, err := repo.ResolveRef(otherRef) + if err != nil { + return err + } + + if commit == otherCommit { + // nothing to merge + return nil + } + + // fast-forward is possible if otherRef include ref + + otherCommits, err := repo.ListCommits(otherRef) + if err != nil { + return err + } + + fastForwardPossible := false + for _, hash := range otherCommits { + if hash == commit { + fastForwardPossible = true + break + } + } + + if fastForwardPossible { + return repo.UpdateRef(ref, otherCommit) + } + + // fast-forward is not possible, we need to create a merge commit + + // we need a Tree to make the commit, an empty Tree will do + emptyTreeHash, err := repo.StoreTree(nil) + if err != nil { + return err + } + + newHash, err := repo.StoreCommit(emptyTreeHash, commit, otherCommit) + if err != nil { + return err + } + + return repo.UpdateRef(ref, newHash) +} + +// nonNativeListCommits is an implementation for ListCommits, for the case where +// the underlying git implementation doesn't support if natively. +func nonNativeListCommits(repo RepoData, ref string) ([]Hash, error) { + var result []Hash + + stack := make([]Hash, 0, 32) + visited := make(map[Hash]struct{}) + + hash, err := repo.ResolveRef(ref) + if err != nil { + return nil, err + } + + stack = append(stack, hash) + + for len(stack) > 0 { + // pop + hash := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + if _, ok := visited[hash]; ok { + continue + } + + // mark as visited + visited[hash] = struct{}{} + result = append(result, hash) + + commit, err := repo.ReadCommit(hash) + if err != nil { + return nil, err + } + + for _, parent := range commit.Parents { + stack = append(stack, parent) + } + } + + // reverse + for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 { + result[i], result[j] = result[j], result[i] + } + + return result, nil +} + +// deArmorSignature convert an armored (text serialized) signature into raw binary +func deArmorSignature(armoredSig io.Reader) (io.Reader, error) { + block, err := armor.Decode(armoredSig) + if err != nil { + return nil, err + } + if block.Type != openpgp.SignatureType { + return nil, errors.InvalidArgumentError("expected '" + openpgp.SignatureType + "', got: " + block.Type) + } + return block.Body, nil +} diff --git a/repository/git.go b/repository/git.go deleted file mode 100644 index e89bae87..00000000 --- a/repository/git.go +++ /dev/null @@ -1,570 +0,0 @@ -// Package repository contains helper methods for working with the Git repo. -package repository - -import ( - "bytes" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" - "sync" - - "github.com/blevesearch/bleve" - "github.com/go-git/go-billy/v5" - "github.com/go-git/go-billy/v5/osfs" - - "github.com/MichaelMure/git-bug/util/lamport" -) - -var _ ClockedRepo = &GitRepo{} -var _ TestedRepo = &GitRepo{} - -// GitRepo represents an instance of a (local) git repository. -type GitRepo struct { - gitCli - path string - - clocksMutex sync.Mutex - clocks map[string]lamport.Clock - - indexesMutex sync.Mutex - indexes map[string]bleve.Index - - keyring Keyring - localStorage billy.Filesystem -} - -func (repo *GitRepo) ReadCommit(hash Hash) (Commit, error) { - panic("implement me") -} - -func (repo *GitRepo) ResolveRef(ref string) (Hash, error) { - panic("implement me") -} - -// OpenGitRepo determines if the given working directory is inside of a git repository, -// and returns the corresponding GitRepo instance if it is. -func OpenGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) { - k, err := defaultKeyring() - if err != nil { - return nil, err - } - - repo := &GitRepo{ - gitCli: gitCli{path: path}, - path: path, - clocks: make(map[string]lamport.Clock), - indexes: make(map[string]bleve.Index), - keyring: k, - } - - // Check the repo and retrieve the root path - stdout, err := repo.runGitCommand("rev-parse", "--absolute-git-dir") - - // Now dir is fetched with "git rev-parse --git-dir". May be it can - // still return nothing in some cases. Then empty stdout check is - // kept. - if err != nil || stdout == "" { - return nil, ErrNotARepo - } - - // Fix the path to be sure we are at the root - repo.path = stdout - repo.gitCli.path = stdout - repo.localStorage = osfs.New(filepath.Join(path, "git-bug")) - - for _, loader := range clockLoaders { - allExist := true - for _, name := range loader.Clocks { - if _, err := repo.getClock(name); err != nil { - allExist = false - } - } - - if !allExist { - err = loader.Witnesser(repo) - if err != nil { - return nil, err - } - } - } - - return repo, nil -} - -// InitGitRepo create a new empty git repo at the given path -func InitGitRepo(path string) (*GitRepo, error) { - k, err := defaultKeyring() - if err != nil { - return nil, err - } - - repo := &GitRepo{ - gitCli: gitCli{path: path}, - path: filepath.Join(path, ".git"), - clocks: make(map[string]lamport.Clock), - indexes: make(map[string]bleve.Index), - keyring: k, - localStorage: osfs.New(filepath.Join(path, ".git", "git-bug")), - } - - _, err = repo.runGitCommand("init", path) - if err != nil { - return nil, err - } - - return repo, nil -} - -// InitBareGitRepo create a new --bare empty git repo at the given path -func InitBareGitRepo(path string) (*GitRepo, error) { - k, err := defaultKeyring() - if err != nil { - return nil, err - } - - repo := &GitRepo{ - gitCli: gitCli{path: path}, - path: path, - clocks: make(map[string]lamport.Clock), - indexes: make(map[string]bleve.Index), - keyring: k, - localStorage: osfs.New(filepath.Join(path, "git-bug")), - } - - _, err = repo.runGitCommand("init", "--bare", path) - if err != nil { - return nil, err - } - - return repo, nil -} - -func (repo *GitRepo) Close() error { - var firstErr error - for _, index := range repo.indexes { - err := index.Close() - if err != nil && firstErr == nil { - firstErr = err - } - } - return firstErr -} - -// LocalConfig give access to the repository scoped configuration -func (repo *GitRepo) LocalConfig() Config { - return newGitConfig(repo.gitCli, false) -} - -// GlobalConfig give access to the global scoped configuration -func (repo *GitRepo) GlobalConfig() Config { - return newGitConfig(repo.gitCli, true) -} - -// AnyConfig give access to a merged local/global configuration -func (repo *GitRepo) AnyConfig() ConfigRead { - return mergeConfig(repo.LocalConfig(), repo.GlobalConfig()) -} - -// Keyring give access to a user-wide storage for secrets -func (repo *GitRepo) Keyring() Keyring { - return repo.keyring -} - -// GetPath returns the path to the repo. -func (repo *GitRepo) GetPath() string { - return repo.path -} - -// GetUserName returns the name the the user has used to configure git -func (repo *GitRepo) GetUserName() (string, error) { - return repo.runGitCommand("config", "user.name") -} - -// GetUserEmail returns the email address that the user has used to configure git. -func (repo *GitRepo) GetUserEmail() (string, error) { - return repo.runGitCommand("config", "user.email") -} - -// GetCoreEditor returns the name of the editor that the user has used to configure git. -func (repo *GitRepo) GetCoreEditor() (string, error) { - return repo.runGitCommand("var", "GIT_EDITOR") -} - -// GetRemotes returns the configured remotes repositories. -func (repo *GitRepo) GetRemotes() (map[string]string, error) { - stdout, err := repo.runGitCommand("remote", "--verbose") - if err != nil { - return nil, err - } - - lines := strings.Split(stdout, "\n") - remotes := make(map[string]string, len(lines)) - - for _, line := range lines { - if strings.TrimSpace(line) == "" { - continue - } - elements := strings.Fields(line) - if len(elements) != 3 { - return nil, fmt.Errorf("git remote: unexpected output format: %s", line) - } - - remotes[elements[0]] = elements[1] - } - - return remotes, nil -} - -// LocalStorage return a billy.Filesystem giving access to $RepoPath/.git/git-bug -func (repo *GitRepo) LocalStorage() billy.Filesystem { - return repo.localStorage -} - -// GetBleveIndex return a bleve.Index that can be used to index documents -func (repo *GitRepo) GetBleveIndex(name string) (bleve.Index, error) { - repo.indexesMutex.Lock() - defer repo.indexesMutex.Unlock() - - if index, ok := repo.indexes[name]; ok { - return index, nil - } - - path := filepath.Join(repo.path, "indexes", name) - - index, err := bleve.Open(path) - if err == nil { - repo.indexes[name] = index - return index, nil - } - - err = os.MkdirAll(path, os.ModeDir) - if err != nil { - return nil, err - } - - mapping := bleve.NewIndexMapping() - mapping.DefaultAnalyzer = "en" - - index, err = bleve.New(path, mapping) - if err != nil { - return nil, err - } - - repo.indexes[name] = index - - return index, nil -} - -// ClearBleveIndex will wipe the given index -func (repo *GitRepo) ClearBleveIndex(name string) error { - repo.indexesMutex.Lock() - defer repo.indexesMutex.Unlock() - - path := filepath.Join(repo.path, "indexes", name) - - err := os.RemoveAll(path) - if err != nil { - return err - } - - delete(repo.indexes, name) - - return nil -} - -// FetchRefs fetch git refs from a remote -func (repo *GitRepo) FetchRefs(remote, refSpec string) (string, error) { - stdout, err := repo.runGitCommand("fetch", remote, refSpec) - - if err != nil { - return stdout, fmt.Errorf("failed to fetch from the remote '%s': %v", remote, err) - } - - return stdout, err -} - -// PushRefs push git refs to a remote -func (repo *GitRepo) PushRefs(remote string, refSpec string) (string, error) { - stdout, stderr, err := repo.runGitCommandRaw(nil, "push", remote, refSpec) - - if err != nil { - return stdout + stderr, fmt.Errorf("failed to push to the remote '%s': %v", remote, stderr) - } - return stdout + stderr, nil -} - -// StoreData will store arbitrary data and return the corresponding hash -func (repo *GitRepo) StoreData(data []byte) (Hash, error) { - var stdin = bytes.NewReader(data) - - stdout, err := repo.runGitCommandWithStdin(stdin, "hash-object", "--stdin", "-w") - - return Hash(stdout), err -} - -// ReadData will attempt to read arbitrary data from the given hash -func (repo *GitRepo) ReadData(hash Hash) ([]byte, error) { - var stdout bytes.Buffer - var stderr bytes.Buffer - - err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "cat-file", "-p", string(hash)) - - if err != nil { - return []byte{}, err - } - - return stdout.Bytes(), nil -} - -// StoreTree will store a mapping key-->Hash as a Git tree -func (repo *GitRepo) StoreTree(entries []TreeEntry) (Hash, error) { - buffer := prepareTreeEntries(entries) - - stdout, err := repo.runGitCommandWithStdin(&buffer, "mktree") - - if err != nil { - return "", err - } - - return Hash(stdout), nil -} - -// StoreCommit will store a Git commit with the given Git tree -func (repo *GitRepo) StoreCommit(treeHash Hash) (Hash, error) { - stdout, err := repo.runGitCommand("commit-tree", string(treeHash)) - - if err != nil { - return "", err - } - - return Hash(stdout), nil -} - -// StoreCommitWithParent will store a Git commit with the given Git tree -func (repo *GitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) { - stdout, err := repo.runGitCommand("commit-tree", string(treeHash), - "-p", string(parent)) - - if err != nil { - return "", err - } - - return Hash(stdout), nil -} - -// UpdateRef will create or update a Git reference -func (repo *GitRepo) UpdateRef(ref string, hash Hash) error { - _, err := repo.runGitCommand("update-ref", ref, string(hash)) - - return err -} - -// RemoveRef will remove a Git reference -func (repo *GitRepo) RemoveRef(ref string) error { - _, err := repo.runGitCommand("update-ref", "-d", ref) - - return err -} - -// ListRefs will return a list of Git ref matching the given refspec -func (repo *GitRepo) ListRefs(refPrefix string) ([]string, error) { - stdout, err := repo.runGitCommand("for-each-ref", "--format=%(refname)", refPrefix) - - if err != nil { - return nil, err - } - - split := strings.Split(stdout, "\n") - - if len(split) == 1 && split[0] == "" { - return []string{}, nil - } - - return split, nil -} - -// RefExist will check if a reference exist in Git -func (repo *GitRepo) RefExist(ref string) (bool, error) { - stdout, err := repo.runGitCommand("for-each-ref", ref) - - if err != nil { - return false, err - } - - return stdout != "", nil -} - -// CopyRef will create a new reference with the same value as another one -func (repo *GitRepo) CopyRef(source string, dest string) error { - _, err := repo.runGitCommand("update-ref", dest, source) - - return err -} - -// ListCommits will return the list of commit hashes of a ref, in chronological order -func (repo *GitRepo) ListCommits(ref string) ([]Hash, error) { - stdout, err := repo.runGitCommand("rev-list", "--first-parent", "--reverse", ref) - - if err != nil { - return nil, err - } - - split := strings.Split(stdout, "\n") - - casted := make([]Hash, len(split)) - for i, line := range split { - casted[i] = Hash(line) - } - - return casted, nil - -} - -// ReadTree will return the list of entries in a Git tree -func (repo *GitRepo) ReadTree(hash Hash) ([]TreeEntry, error) { - stdout, err := repo.runGitCommand("ls-tree", string(hash)) - - if err != nil { - return nil, err - } - - return readTreeEntries(stdout) -} - -// FindCommonAncestor will return the last common ancestor of two chain of commit -func (repo *GitRepo) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error) { - stdout, err := repo.runGitCommand("merge-base", string(hash1), string(hash2)) - - if err != nil { - return "", err - } - - return Hash(stdout), nil -} - -// GetTreeHash return the git tree hash referenced in a commit -func (repo *GitRepo) GetTreeHash(commit Hash) (Hash, error) { - stdout, err := repo.runGitCommand("rev-parse", string(commit)+"^{tree}") - - if err != nil { - return "", err - } - - return Hash(stdout), nil -} - -func (repo *GitRepo) AllClocks() (map[string]lamport.Clock, error) { - repo.clocksMutex.Lock() - defer repo.clocksMutex.Unlock() - - result := make(map[string]lamport.Clock) - - files, err := ioutil.ReadDir(filepath.Join(repo.path, "git-bug", clockPath)) - if os.IsNotExist(err) { - return nil, nil - } - if err != nil { - return nil, err - } - - for _, file := range files { - name := file.Name() - if c, ok := repo.clocks[name]; ok { - result[name] = c - } else { - c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name)) - if err != nil { - return nil, err - } - repo.clocks[name] = c - result[name] = c - } - } - - return result, nil -} - -// GetOrCreateClock return a Lamport clock stored in the Repo. -// If the clock doesn't exist, it's created. -func (repo *GitRepo) GetOrCreateClock(name string) (lamport.Clock, error) { - repo.clocksMutex.Lock() - defer repo.clocksMutex.Unlock() - - c, err := repo.getClock(name) - if err == nil { - return c, nil - } - if err != ErrClockNotExist { - return nil, err - } - - c, err = lamport.NewPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name)) - if err != nil { - return nil, err - } - - repo.clocks[name] = c - return c, nil -} - -func (repo *GitRepo) getClock(name string) (lamport.Clock, error) { - if c, ok := repo.clocks[name]; ok { - return c, nil - } - - c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name)) - if err == nil { - repo.clocks[name] = c - return c, nil - } - if err == lamport.ErrClockNotExist { - return nil, ErrClockNotExist - } - return nil, err -} - -// Increment is equivalent to c = GetOrCreateClock(name) + c.Increment() -func (repo *GitRepo) Increment(name string) (lamport.Time, error) { - c, err := repo.GetOrCreateClock(name) - if err != nil { - return lamport.Time(0), err - } - return c.Increment() -} - -// Witness is equivalent to c = GetOrCreateClock(name) + c.Witness(time) -func (repo *GitRepo) Witness(name string, time lamport.Time) error { - c, err := repo.GetOrCreateClock(name) - if err != nil { - return err - } - return c.Witness(time) -} - -// AddRemote add a new remote to the repository -// Not in the interface because it's only used for testing -func (repo *GitRepo) AddRemote(name string, url string) error { - _, err := repo.runGitCommand("remote", "add", name, url) - - return err -} - -// GetLocalRemote return the URL to use to add this repo as a local remote -func (repo *GitRepo) GetLocalRemote() string { - return repo.path -} - -// EraseFromDisk delete this repository entirely from the disk -func (repo *GitRepo) EraseFromDisk() error { - err := repo.Close() - if err != nil { - return err - } - - path := filepath.Clean(strings.TrimSuffix(repo.path, string(filepath.Separator)+".git")) - - // fmt.Println("Cleaning repo:", path) - return os.RemoveAll(path) -} diff --git a/repository/git_cli.go b/repository/git_cli.go deleted file mode 100644 index 085b1cda..00000000 --- a/repository/git_cli.go +++ /dev/null @@ -1,56 +0,0 @@ -package repository - -import ( - "bytes" - "fmt" - "io" - "os/exec" - "strings" -) - -// gitCli is a helper to launch CLI git commands -type gitCli struct { - path string -} - -// Run the given git command with the given I/O reader/writers, returning an error if it fails. -func (cli gitCli) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error { - // make sure that the working directory for the command - // always exist, in particular when running "git init". - path := strings.TrimSuffix(cli.path, ".git") - - // fmt.Printf("[%s] Running git %s\n", path, strings.Join(args, " ")) - - cmd := exec.Command("git", args...) - cmd.Dir = path - cmd.Stdin = stdin - cmd.Stdout = stdout - cmd.Stderr = stderr - - return cmd.Run() -} - -// Run the given git command and return its stdout, or an error if the command fails. -func (cli gitCli) runGitCommandRaw(stdin io.Reader, args ...string) (string, string, error) { - var stdout bytes.Buffer - var stderr bytes.Buffer - err := cli.runGitCommandWithIO(stdin, &stdout, &stderr, args...) - return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err -} - -// Run the given git command and return its stdout, or an error if the command fails. -func (cli gitCli) runGitCommandWithStdin(stdin io.Reader, args ...string) (string, error) { - stdout, stderr, err := cli.runGitCommandRaw(stdin, args...) - if err != nil { - if stderr == "" { - stderr = "Error running git command: " + strings.Join(args, " ") - } - err = fmt.Errorf(stderr) - } - return stdout, err -} - -// Run the given git command and return its stdout, or an error if the command fails. -func (cli gitCli) runGitCommand(args ...string) (string, error) { - return cli.runGitCommandWithStdin(nil, args...) -} diff --git a/repository/git_config.go b/repository/git_config.go deleted file mode 100644 index b46cc69b..00000000 --- a/repository/git_config.go +++ /dev/null @@ -1,221 +0,0 @@ -package repository - -import ( - "fmt" - "regexp" - "strconv" - "strings" - "time" - - "github.com/blang/semver" - "github.com/pkg/errors" -) - -var _ Config = &gitConfig{} - -type gitConfig struct { - cli gitCli - localityFlag string -} - -func newGitConfig(cli gitCli, global bool) *gitConfig { - localityFlag := "--local" - if global { - localityFlag = "--global" - } - return &gitConfig{ - cli: cli, - localityFlag: localityFlag, - } -} - -// StoreString store a single key/value pair in the config of the repo -func (gc *gitConfig) StoreString(key string, value string) error { - _, err := gc.cli.runGitCommand("config", gc.localityFlag, "--replace-all", key, value) - return err -} - -func (gc *gitConfig) StoreBool(key string, value bool) error { - return gc.StoreString(key, strconv.FormatBool(value)) -} - -func (gc *gitConfig) StoreTimestamp(key string, value time.Time) error { - return gc.StoreString(key, strconv.Itoa(int(value.Unix()))) -} - -// ReadAll read all key/value pair matching the key prefix -func (gc *gitConfig) ReadAll(keyPrefix string) (map[string]string, error) { - stdout, err := gc.cli.runGitCommand("config", gc.localityFlag, "--includes", "--get-regexp", keyPrefix) - - // / \ - // / ! \ - // ------- - // - // There can be a legitimate error here, but I see no portable way to - // distinguish them from the git error that say "no matching value exist" - if err != nil { - return nil, nil - } - - lines := strings.Split(stdout, "\n") - - result := make(map[string]string, len(lines)) - - for _, line := range lines { - if strings.TrimSpace(line) == "" { - continue - } - - parts := strings.SplitN(line, " ", 2) - result[parts[0]] = parts[1] - } - - return result, nil -} - -func (gc *gitConfig) ReadString(key string) (string, error) { - stdout, err := gc.cli.runGitCommand("config", gc.localityFlag, "--includes", "--get-all", key) - - // / \ - // / ! \ - // ------- - // - // There can be a legitimate error here, but I see no portable way to - // distinguish them from the git error that say "no matching value exist" - if err != nil { - return "", ErrNoConfigEntry - } - - lines := strings.Split(stdout, "\n") - - if len(lines) == 0 { - return "", ErrNoConfigEntry - } - if len(lines) > 1 { - return "", ErrMultipleConfigEntry - } - - return lines[0], nil -} - -func (gc *gitConfig) ReadBool(key string) (bool, error) { - val, err := gc.ReadString(key) - if err != nil { - return false, err - } - - return strconv.ParseBool(val) -} - -func (gc *gitConfig) ReadTimestamp(key string) (time.Time, error) { - value, err := gc.ReadString(key) - if err != nil { - return time.Time{}, err - } - return ParseTimestamp(value) -} - -func (gc *gitConfig) rmSection(keyPrefix string) error { - _, err := gc.cli.runGitCommand("config", gc.localityFlag, "--remove-section", keyPrefix) - return err -} - -func (gc *gitConfig) unsetAll(keyPrefix string) error { - _, err := gc.cli.runGitCommand("config", gc.localityFlag, "--unset-all", keyPrefix) - return err -} - -// return keyPrefix section -// example: sectionFromKey(a.b.c.d) return a.b.c -func sectionFromKey(keyPrefix string) string { - s := strings.Split(keyPrefix, ".") - if len(s) == 1 { - return keyPrefix - } - - return strings.Join(s[:len(s)-1], ".") -} - -// rmConfigs with git version lesser than 2.18 -func (gc *gitConfig) rmConfigsGitVersionLT218(keyPrefix string) error { - // try to remove key/value pair by key - err := gc.unsetAll(keyPrefix) - if err != nil { - return gc.rmSection(keyPrefix) - } - - m, err := gc.ReadAll(sectionFromKey(keyPrefix)) - if err != nil { - return err - } - - // if section doesn't have any left key/value remove the section - if len(m) == 0 { - return gc.rmSection(sectionFromKey(keyPrefix)) - } - - return nil -} - -// RmConfigs remove all key/value pair matching the key prefix -func (gc *gitConfig) RemoveAll(keyPrefix string) error { - // starting from git 2.18.0 sections are automatically deleted when the last existing - // key/value is removed. Before 2.18.0 we should remove the section - // see https://github.com/git/git/blob/master/Documentation/RelNotes/2.18.0.txt#L379 - lt218, err := gc.gitVersionLT218() - if err != nil { - return errors.Wrap(err, "getting git version") - } - - if lt218 { - return gc.rmConfigsGitVersionLT218(keyPrefix) - } - - err = gc.unsetAll(keyPrefix) - if err != nil { - return gc.rmSection(keyPrefix) - } - - return nil -} - -func (gc *gitConfig) gitVersion() (*semver.Version, error) { - versionOut, err := gc.cli.runGitCommand("version") - if err != nil { - return nil, err - } - return parseGitVersion(versionOut) -} - -func parseGitVersion(versionOut string) (*semver.Version, error) { - // extract the version and truncate potential bad parts - // ex: 2.23.0.rc1 instead of 2.23.0-rc1 - r := regexp.MustCompile(`(\d+\.){1,2}\d+`) - - extracted := r.FindString(versionOut) - if extracted == "" { - return nil, fmt.Errorf("unreadable git version %s", versionOut) - } - - version, err := semver.Make(extracted) - if err != nil { - return nil, err - } - - return &version, nil -} - -func (gc *gitConfig) gitVersionLT218() (bool, error) { - version, err := gc.gitVersion() - if err != nil { - return false, err - } - - version218string := "2.18.0" - gitVersion218, err := semver.Make(version218string) - if err != nil { - return false, err - } - - return version.LT(gitVersion218), nil -} diff --git a/repository/git_test.go b/repository/git_test.go deleted file mode 100644 index 6603e339..00000000 --- a/repository/git_test.go +++ /dev/null @@ -1,6 +0,0 @@ -// Package repository contains helper methods for working with the Git repo. -package repository - -// func TestGitRepo(t *testing.T) { -// RepoTest(t, CreateTestRepo, CleanupTestRepos) -// } diff --git a/repository/git_testing.go b/repository/git_testing.go deleted file mode 100644 index 2168d53e..00000000 --- a/repository/git_testing.go +++ /dev/null @@ -1,72 +0,0 @@ -package repository - -import ( - "io/ioutil" - "log" - - "github.com/99designs/keyring" -) - -// This is intended for testing only - -func CreateTestRepo(bare bool) TestedRepo { - dir, err := ioutil.TempDir("", "") - if err != nil { - log.Fatal(err) - } - - var creator func(string) (*GitRepo, error) - - if bare { - creator = InitBareGitRepo - } else { - creator = InitGitRepo - } - - repo, err := creator(dir) - if err != nil { - log.Fatal(err) - } - - config := repo.LocalConfig() - if err := config.StoreString("user.name", "testuser"); err != nil { - log.Fatal("failed to set user.name for test repository: ", err) - } - if err := config.StoreString("user.email", "testuser@example.com"); err != nil { - log.Fatal("failed to set user.email for test repository: ", err) - } - - // make sure we use a mock keyring for testing to not interact with the global system - return &replaceKeyring{ - TestedRepo: repo, - keyring: keyring.NewArrayKeyring(nil), - } -} - -func SetupReposAndRemote() (repoA, repoB, remote TestedRepo) { - repoA = CreateGoGitTestRepo(false) - repoB = CreateGoGitTestRepo(false) - remote = CreateGoGitTestRepo(true) - - err := repoA.AddRemote("origin", remote.GetLocalRemote()) - if err != nil { - log.Fatal(err) - } - - err = repoB.AddRemote("origin", remote.GetLocalRemote()) - if err != nil { - log.Fatal(err) - } - - return repoA, repoB, remote -} - -// replaceKeyring allow to replace the Keyring of the underlying repo -type replaceKeyring struct { - TestedRepo - keyring Keyring -} - -func (rk replaceKeyring) Keyring() Keyring { - return rk.keyring -} diff --git a/repository/gogit.go b/repository/gogit.go index 64ccb773..d6eb8621 100644 --- a/repository/gogit.go +++ b/repository/gogit.go @@ -20,6 +20,7 @@ import ( "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/filemode" "github.com/go-git/go-git/v5/plumbing/object" + "golang.org/x/crypto/openpgp" "github.com/MichaelMure/git-bug/util/lamport" ) @@ -521,12 +522,13 @@ func (repo *GoGitRepo) ReadTree(hash Hash) ([]TreeEntry, error) { } // StoreCommit will store a Git commit with the given Git tree -func (repo *GoGitRepo) StoreCommit(treeHash Hash) (Hash, error) { - return repo.StoreCommitWithParent(treeHash, "") +func (repo *GoGitRepo) StoreCommit(treeHash Hash, parents ...Hash) (Hash, error) { + return repo.StoreSignedCommit(treeHash, nil, parents...) } -// StoreCommit will store a Git commit with the given Git tree -func (repo *GoGitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) { +// StoreCommit will store a Git commit with the given Git tree. If signKey is not nil, the commit +// will be signed accordingly. +func (repo *GoGitRepo) StoreSignedCommit(treeHash Hash, signKey *openpgp.Entity, parents ...Hash) (Hash, error) { cfg, err := repo.r.Config() if err != nil { return "", err @@ -547,8 +549,28 @@ func (repo *GoGitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, TreeHash: plumbing.NewHash(treeHash.String()), } - if parent != "" { - commit.ParentHashes = []plumbing.Hash{plumbing.NewHash(parent.String())} + for _, parent := range parents { + commit.ParentHashes = append(commit.ParentHashes, plumbing.NewHash(parent.String())) + } + + // Compute the signature if needed + if signKey != nil { + // first get the serialized commit + encoded := &plumbing.MemoryObject{} + if err := commit.Encode(encoded); err != nil { + return "", err + } + r, err := encoded.Reader() + if err != nil { + return "", err + } + + // sign the data + var sig bytes.Buffer + if err := openpgp.ArmoredDetachSign(&sig, signKey, r, nil); err != nil { + return "", err + } + commit.PGPSignature = sig.String() } obj := repo.r.Storer.NewEncodedObject() @@ -608,6 +630,13 @@ func (repo *GoGitRepo) UpdateRef(ref string, hash Hash) error { return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(ref), plumbing.NewHash(hash.String()))) } +// MergeRef merge other into ref and update the reference +// If the update is not fast-forward, the callback treeHashFn will be called for the caller to generate +// the Tree to store in the merge commit. +func (repo *GoGitRepo) MergeRef(ref string, otherRef string, treeHashFn func() Hash) error { + return nonNativeMerge(repo, ref, otherRef, treeHashFn) +} + // RemoveRef will remove a Git reference func (repo *GoGitRepo) RemoveRef(ref string) error { return repo.r.Storer.RemoveReference(plumbing.ReferenceName(ref)) @@ -657,38 +686,16 @@ func (repo *GoGitRepo) CopyRef(source string, dest string) error { // ListCommits will return the list of tree hashes of a ref, in chronological order func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) { - r, err := repo.r.Reference(plumbing.ReferenceName(ref), false) - if err != nil { - return nil, err - } + return nonNativeListCommits(repo, ref) +} - commit, err := repo.r.CommitObject(r.Hash()) +func (repo *GoGitRepo) ReadCommit(hash Hash) (Commit, error) { + encoded, err := repo.r.Storer.EncodedObject(plumbing.CommitObject, plumbing.NewHash(hash.String())) if err != nil { - return nil, err - } - hashes := []Hash{Hash(commit.Hash.String())} - - for { - commit, err = commit.Parent(0) - if err == object.ErrParentNotFound { - break - } - if err != nil { - return nil, err - } - - if commit.NumParents() > 1 { - return nil, fmt.Errorf("multiple parents") - } - - hashes = append([]Hash{Hash(commit.Hash.String())}, hashes...) + return Commit{}, err } - return hashes, nil -} - -func (repo *GoGitRepo) ReadCommit(hash Hash) (Commit, error) { - commit, err := repo.r.CommitObject(plumbing.NewHash(hash.String())) + commit, err := object.DecodeCommit(repo.r.Storer, encoded) if err != nil { return Commit{}, err } @@ -698,12 +705,25 @@ func (repo *GoGitRepo) ReadCommit(hash Hash) (Commit, error) { parents[i] = Hash(parentHash.String()) } - return Commit{ + result := Commit{ Hash: hash, Parents: parents, TreeHash: Hash(commit.TreeHash.String()), - }, nil + } + + if commit.PGPSignature != "" { + result.SignedData, err = encoded.Reader() + if err != nil { + return Commit{}, err + } + result.Signature, err = deArmorSignature(strings.NewReader(commit.PGPSignature)) + if err != nil { + return Commit{}, err + } + } + + return result, nil } func (repo *GoGitRepo) AllClocks() (map[string]lamport.Clock, error) { diff --git a/repository/gogit_testing.go b/repository/gogit_testing.go index a8bff41e..cad776b3 100644 --- a/repository/gogit_testing.go +++ b/repository/gogit_testing.go @@ -3,6 +3,8 @@ package repository import ( "io/ioutil" "log" + + "github.com/99designs/keyring" ) // This is intended for testing only @@ -34,7 +36,11 @@ func CreateGoGitTestRepo(bare bool) TestedRepo { log.Fatal("failed to set user.email for test repository: ", err) } - return repo + // make sure we use a mock keyring for testing to not interact with the global system + return &replaceKeyring{ + TestedRepo: repo, + keyring: keyring.NewArrayKeyring(nil), + } } func SetupGoGitReposAndRemote() (repoA, repoB, remote TestedRepo) { diff --git a/repository/keyring.go b/repository/keyring.go index 4cb3c9ff..64365c39 100644 --- a/repository/keyring.go +++ b/repository/keyring.go @@ -48,3 +48,13 @@ func defaultKeyring() (Keyring, error) { }, }) } + +// replaceKeyring allow to replace the Keyring of the underlying repo +type replaceKeyring struct { + TestedRepo + keyring Keyring +} + +func (rk replaceKeyring) Keyring() Keyring { + return rk.keyring +} diff --git a/repository/mock_repo.go b/repository/mock_repo.go index 227e0f2c..095ad61c 100644 --- a/repository/mock_repo.go +++ b/repository/mock_repo.go @@ -1,6 +1,7 @@ package repository import ( + "bytes" "crypto/sha1" "fmt" "strings" @@ -10,6 +11,7 @@ import ( "github.com/blevesearch/bleve" "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/memfs" + "golang.org/x/crypto/openpgp" "github.com/MichaelMure/git-bug/util/lamport" ) @@ -180,6 +182,7 @@ var _ RepoData = &mockRepoData{} type commit struct { treeHash Hash parents []Hash + sig string } type mockRepoData struct { @@ -198,12 +201,12 @@ func NewMockRepoData() *mockRepoData { } } -// PushRefs push git refs to a remote -func (r *mockRepoData) PushRefs(remote string, refSpec string) (string, error) { +func (r *mockRepoData) FetchRefs(remote string, refSpec string) (string, error) { return "", nil } -func (r *mockRepoData) FetchRefs(remote string, refSpec string) (string, error) { +// PushRefs push git refs to a remote +func (r *mockRepoData) PushRefs(remote string, refSpec string) (string, error) { return "", nil } @@ -216,7 +219,6 @@ func (r *mockRepoData) StoreData(data []byte) (Hash, error) { func (r *mockRepoData) ReadData(hash Hash) ([]byte, error) { data, ok := r.blobs[hash] - if !ok { return nil, fmt.Errorf("unknown hash") } @@ -233,25 +235,86 @@ func (r *mockRepoData) StoreTree(entries []TreeEntry) (Hash, error) { return hash, nil } -func (r *mockRepoData) StoreCommit(treeHash Hash) (Hash, error) { - rawHash := sha1.Sum([]byte(treeHash)) - hash := Hash(fmt.Sprintf("%x", rawHash)) - r.commits[hash] = commit{ - treeHash: treeHash, +func (r *mockRepoData) ReadTree(hash Hash) ([]TreeEntry, error) { + var data string + + data, ok := r.trees[hash] + + if !ok { + // Git will understand a commit hash to reach a tree + commit, ok := r.commits[hash] + + if !ok { + return nil, fmt.Errorf("unknown hash") + } + + data, ok = r.trees[commit.treeHash] + + if !ok { + return nil, fmt.Errorf("unknown hash") + } } - return hash, nil + + return readTreeEntries(data) +} + +func (r *mockRepoData) StoreCommit(treeHash Hash, parents ...Hash) (Hash, error) { + return r.StoreSignedCommit(treeHash, nil, parents...) } -func (r *mockRepoData) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) { - rawHash := sha1.Sum([]byte(treeHash + parent)) +func (r *mockRepoData) StoreSignedCommit(treeHash Hash, signKey *openpgp.Entity, parents ...Hash) (Hash, error) { + hasher := sha1.New() + hasher.Write([]byte(treeHash)) + for _, parent := range parents { + hasher.Write([]byte(parent)) + } + rawHash := hasher.Sum(nil) hash := Hash(fmt.Sprintf("%x", rawHash)) - r.commits[hash] = commit{ + c := commit{ treeHash: treeHash, - parents: []Hash{parent}, + parents: parents, + } + if signKey != nil { + // unlike go-git, we only sign the tree hash for simplicity instead of all the fields (parents ...) + var sig bytes.Buffer + if err := openpgp.DetachSign(&sig, signKey, strings.NewReader(string(treeHash)), nil); err != nil { + return "", err + } + c.sig = sig.String() } + r.commits[hash] = c return hash, nil } +func (r *mockRepoData) ReadCommit(hash Hash) (Commit, error) { + c, ok := r.commits[hash] + if !ok { + return Commit{}, fmt.Errorf("unknown commit") + } + + result := Commit{ + Hash: hash, + Parents: c.parents, + TreeHash: c.treeHash, + } + + if c.sig != "" { + result.SignedData = strings.NewReader(string(c.treeHash)) + result.Signature = strings.NewReader(c.sig) + } + + return result, nil +} + +func (r *mockRepoData) GetTreeHash(commit Hash) (Hash, error) { + c, ok := r.commits[commit] + if !ok { + return "", fmt.Errorf("unknown commit") + } + + return c.treeHash, nil +} + func (r *mockRepoData) ResolveRef(ref string) (Hash, error) { h, ok := r.refs[ref] if !ok { @@ -270,22 +333,6 @@ func (r *mockRepoData) RemoveRef(ref string) error { return nil } -func (r *mockRepoData) RefExist(ref string) (bool, error) { - _, exist := r.refs[ref] - return exist, nil -} - -func (r *mockRepoData) CopyRef(source string, dest string) error { - hash, exist := r.refs[source] - - if !exist { - return fmt.Errorf("Unknown ref") - } - - r.refs[dest] = hash - return nil -} - func (r *mockRepoData) ListRefs(refPrefix string) ([]string, error) { var keys []string @@ -298,63 +345,20 @@ func (r *mockRepoData) ListRefs(refPrefix string) ([]string, error) { return keys, nil } -func (r *mockRepoData) ListCommits(ref string) ([]Hash, error) { - var hashes []Hash - - hash := r.refs[ref] - - for { - commit, ok := r.commits[hash] - - if !ok { - break - } - - hashes = append([]Hash{hash}, hashes...) - - if len(commit.parents) == 0 { - break - } - hash = commit.parents[0] - } - - return hashes, nil -} - -func (r *mockRepoData) ReadCommit(hash Hash) (Commit, error) { - c, ok := r.commits[hash] - if !ok { - return Commit{}, fmt.Errorf("unknown commit") - } - - return Commit{ - Hash: hash, - Parents: c.parents, - TreeHash: c.treeHash, - }, nil +func (r *mockRepoData) RefExist(ref string) (bool, error) { + _, exist := r.refs[ref] + return exist, nil } -func (r *mockRepoData) ReadTree(hash Hash) ([]TreeEntry, error) { - var data string - - data, ok := r.trees[hash] - - if !ok { - // Git will understand a commit hash to reach a tree - commit, ok := r.commits[hash] - - if !ok { - return nil, fmt.Errorf("unknown hash") - } - - data, ok = r.trees[commit.treeHash] +func (r *mockRepoData) CopyRef(source string, dest string) error { + hash, exist := r.refs[source] - if !ok { - return nil, fmt.Errorf("unknown hash") - } + if !exist { + return fmt.Errorf("Unknown ref") } - return readTreeEntries(data) + r.refs[dest] = hash + return nil } func (r *mockRepoData) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error) { @@ -392,13 +396,8 @@ func (r *mockRepoData) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error) } } -func (r *mockRepoData) GetTreeHash(commit Hash) (Hash, error) { - c, ok := r.commits[commit] - if !ok { - return "", fmt.Errorf("unknown commit") - } - - return c.treeHash, nil +func (r *mockRepoData) ListCommits(ref string) ([]Hash, error) { + return nonNativeListCommits(r, ref) } var _ RepoClock = &mockRepoClock{} diff --git a/repository/mock_repo_test.go b/repository/mock_repo_test.go index dec09380..12851a80 100644 --- a/repository/mock_repo_test.go +++ b/repository/mock_repo_test.go @@ -1,6 +1,8 @@ package repository -import "testing" +import ( + "testing" +) func TestMockRepo(t *testing.T) { creator := func(bare bool) TestedRepo { return NewMockRepo() } diff --git a/repository/repo.go b/repository/repo.go index afd8ff77..d7afa983 100644 --- a/repository/repo.go +++ b/repository/repo.go @@ -3,15 +3,17 @@ package repository import ( "errors" + "io" "github.com/blevesearch/bleve" "github.com/go-git/go-billy/v5" + "golang.org/x/crypto/openpgp" "github.com/MichaelMure/git-bug/util/lamport" ) var ( - // ErrNotARepo is the error returned when the git repo root wan't be found + // ErrNotARepo is the error returned when the git repo root can't be found ErrNotARepo = errors.New("not a git repository") // ErrClockNotExist is the error returned when a clock can't be found ErrClockNotExist = errors.New("clock doesn't exist") @@ -89,9 +91,11 @@ type RepoBleve interface { } type Commit struct { - Hash Hash - Parents []Hash - TreeHash Hash + Hash Hash + Parents []Hash // hashes of the parents, if any + TreeHash Hash // hash of the git Tree + SignedData io.Reader // if signed, reader for the signed data (likely, the serialized commit) + Signature io.Reader // if signed, reader for the (non-armored) signature } // RepoData give access to the git data storage @@ -116,21 +120,29 @@ type RepoData interface { ReadTree(hash Hash) ([]TreeEntry, error) // StoreCommit will store a Git commit with the given Git tree - StoreCommit(treeHash Hash) (Hash, error) + StoreCommit(treeHash Hash, parents ...Hash) (Hash, error) - // StoreCommit will store a Git commit with the given Git tree - StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) + // StoreCommit will store a Git commit with the given Git tree. If signKey is not nil, the commit + // will be signed accordingly. + StoreSignedCommit(treeHash Hash, signKey *openpgp.Entity, parents ...Hash) (Hash, error) + // ReadCommit read a Git commit and returns some of its characteristic ReadCommit(hash Hash) (Commit, error) // GetTreeHash return the git tree hash referenced in a commit GetTreeHash(commit Hash) (Hash, error) + // ResolveRef returns the hash of the target commit of the given ref ResolveRef(ref string) (Hash, error) // UpdateRef will create or update a Git reference UpdateRef(ref string, hash Hash) error + // // MergeRef merge other into ref and update the reference + // // If the update is not fast-forward, the callback treeHashFn will be called for the caller to generate + // // the Tree to store in the merge commit. + // MergeRef(ref string, otherRef string, treeHashFn func() Hash) error + // RemoveRef will remove a Git reference RemoveRef(ref string) error @@ -148,7 +160,6 @@ type RepoData interface { FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, error) // ListCommits will return the list of tree hashes of a ref, in chronological order - // Deprecated ListCommits(ref string) ([]Hash, error) } diff --git a/repository/repo_testing.go b/repository/repo_testing.go index 1d3a3155..4a5c48bb 100644 --- a/repository/repo_testing.go +++ b/repository/repo_testing.go @@ -135,7 +135,8 @@ func RepoDataTest(t *testing.T, repo RepoData) { require.NoError(t, err) require.Equal(t, treeHash1, treeHash1Read) - commit2, err := repo.StoreCommitWithParent(treeHash2, commit1) + // commit with a parent + commit2, err := repo.StoreCommit(treeHash2, commit1) require.NoError(t, err) require.True(t, commit2.IsValid()) @@ -187,7 +188,7 @@ func RepoDataTest(t *testing.T, repo RepoData) { // Graph - commit3, err := repo.StoreCommitWithParent(treeHash1, commit1) + commit3, err := repo.StoreCommit(treeHash1, commit1) require.NoError(t, err) ancestorHash, err := repo.FindCommonAncestor(commit2, commit3) @@ -237,3 +238,22 @@ func randomData() []byte { } return b } + +func makeCommit(t *testing.T, repo RepoData, parents ...Hash) Hash { + blobHash, err := repo.StoreData(randomData()) + require.NoError(t, err) + + treeHash, err := repo.StoreTree([]TreeEntry{ + { + ObjectType: Blob, + Hash: blobHash, + Name: "foo", + }, + }) + require.NoError(t, err) + + commitHash, err := repo.StoreCommit(treeHash, parents...) + require.NoError(t, err) + + return commitHash +} diff --git a/util/lamport/clock_testing.go b/util/lamport/clock_testing.go index 4bf6d2bf..de66c5c9 100644 --- a/util/lamport/clock_testing.go +++ b/util/lamport/clock_testing.go @@ -14,11 +14,11 @@ func testClock(t *testing.T, c Clock) { assert.Equal(t, Time(2), val) assert.Equal(t, Time(2), c.Time()) - err = c.Witness(41) + err = c.Witness(42) assert.NoError(t, err) assert.Equal(t, Time(42), c.Time()) - err = c.Witness(41) + err = c.Witness(42) assert.NoError(t, err) assert.Equal(t, Time(42), c.Time()) diff --git a/util/lamport/mem_clock.go b/util/lamport/mem_clock.go index f113b501..d824d834 100644 --- a/util/lamport/mem_clock.go +++ b/util/lamport/mem_clock.go @@ -25,6 +25,14 @@ */ +// Note: this code originally originate from Hashicorp's Serf but has been changed since to fit git-bug's need. + +// Note: this Lamport clock implementation is different than the algorithms you can find, notably Wikipedia or the +// original Serf implementation. The reason is lie to what constitute an event in this distributed system. +// Commonly, events happen when messages are sent or received, whereas in git-bug events happen when some data is +// written, but *not* when read. This is why Witness set the time to the max seen value instead of max seen value +1. +// See https://cs.stackexchange.com/a/133730/129795 + package lamport import ( @@ -72,12 +80,12 @@ WITNESS: // If the other value is old, we do not need to do anything cur := atomic.LoadUint64(&mc.counter) other := uint64(v) - if other < cur { + if other <= cur { return nil } // Ensure that our local clock is at least one ahead. - if !atomic.CompareAndSwapUint64(&mc.counter, cur, other+1) { + if !atomic.CompareAndSwapUint64(&mc.counter, cur, other) { // CAS: CompareAndSwap // The CAS failed, so we just retry. Eventually our CAS should // succeed or a future witness will pass us by and our witness -- cgit v1.2.3 From 3f6ef50883492f77995a7e27872d0b5ae17b9d6a Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 14 Feb 2021 11:36:32 +0100 Subject: bug: migrate to the DAG entity structure! --- api/graphql/resolvers/operations.go | 12 +- api/graphql/resolvers/query.go | 13 - bridge/github/export.go | 2 +- bridge/github/import_test.go | 27 +- bridge/gitlab/export.go | 2 +- bridge/gitlab/import_test.go | 27 +- bridge/jira/export.go | 2 +- bug/bug.go | 594 +++++---------------------------- bug/bug_actions.go | 106 +----- bug/bug_actions_test.go | 394 ---------------------- bug/bug_test.go | 190 ----------- bug/clocks.go | 40 --- bug/err.go | 17 + bug/git_tree.go | 84 ----- bug/identity.go | 27 -- bug/interface.go | 8 +- bug/op_add_comment.go | 14 +- bug/op_add_comment_test.go | 4 +- bug/op_create.go | 16 +- bug/op_create_test.go | 4 +- bug/op_edit_comment.go | 10 +- bug/op_edit_comment_test.go | 4 +- bug/op_label_change.go | 12 +- bug/op_label_change_test.go | 4 +- bug/op_noop.go | 8 +- bug/op_noop_test.go | 4 +- bug/op_set_metadata.go | 21 +- bug/op_set_metadata_test.go | 4 +- bug/op_set_status.go | 12 +- bug/op_set_status_test.go | 4 +- bug/op_set_title.go | 24 +- bug/op_set_title_test.go | 4 +- bug/operation.go | 152 ++++++--- bug/operation_iterator.go | 72 ---- bug/operation_iterator_test.go | 79 ----- bug/operation_pack.go | 187 ----------- bug/operation_pack_test.go | 78 ----- bug/operation_test.go | 4 +- bug/sorting.go | 8 +- bug/with_snapshot.go | 6 - cache/bug_cache.go | 4 +- cache/bug_excerpt.go | 2 +- cache/repo_cache.go | 2 +- cache/repo_cache_bug.go | 2 +- cache/repo_cache_common.go | 15 +- cache/repo_cache_test.go | 4 + entity/TODO | 9 - entity/merge.go | 6 +- go.sum | 1 + misc/random_bugs/create_random_bugs.go | 46 --- tests/read_bugs_test.go | 4 +- 51 files changed, 335 insertions(+), 2040 deletions(-) delete mode 100644 bug/bug_actions_test.go delete mode 100644 bug/bug_test.go delete mode 100644 bug/clocks.go create mode 100644 bug/err.go delete mode 100644 bug/git_tree.go delete mode 100644 bug/identity.go delete mode 100644 bug/operation_iterator.go delete mode 100644 bug/operation_iterator_test.go delete mode 100644 bug/operation_pack.go delete mode 100644 bug/operation_pack_test.go delete mode 100644 entity/TODO (limited to 'bug/bug.go') diff --git a/api/graphql/resolvers/operations.go b/api/graphql/resolvers/operations.go index 8d3e5bba..0ede9f13 100644 --- a/api/graphql/resolvers/operations.go +++ b/api/graphql/resolvers/operations.go @@ -19,7 +19,7 @@ func (createOperationResolver) ID(_ context.Context, obj *bug.CreateOperation) ( } func (createOperationResolver) Author(_ context.Context, obj *bug.CreateOperation) (models.IdentityWrapper, error) { - return models.NewLoadedIdentity(obj.Author), nil + return models.NewLoadedIdentity(obj.Author()), nil } func (createOperationResolver) Date(_ context.Context, obj *bug.CreateOperation) (*time.Time, error) { @@ -36,7 +36,7 @@ func (addCommentOperationResolver) ID(_ context.Context, obj *bug.AddCommentOper } func (addCommentOperationResolver) Author(_ context.Context, obj *bug.AddCommentOperation) (models.IdentityWrapper, error) { - return models.NewLoadedIdentity(obj.Author), nil + return models.NewLoadedIdentity(obj.Author()), nil } func (addCommentOperationResolver) Date(_ context.Context, obj *bug.AddCommentOperation) (*time.Time, error) { @@ -57,7 +57,7 @@ func (editCommentOperationResolver) Target(_ context.Context, obj *bug.EditComme } func (editCommentOperationResolver) Author(_ context.Context, obj *bug.EditCommentOperation) (models.IdentityWrapper, error) { - return models.NewLoadedIdentity(obj.Author), nil + return models.NewLoadedIdentity(obj.Author()), nil } func (editCommentOperationResolver) Date(_ context.Context, obj *bug.EditCommentOperation) (*time.Time, error) { @@ -74,7 +74,7 @@ func (labelChangeOperationResolver) ID(_ context.Context, obj *bug.LabelChangeOp } func (labelChangeOperationResolver) Author(_ context.Context, obj *bug.LabelChangeOperation) (models.IdentityWrapper, error) { - return models.NewLoadedIdentity(obj.Author), nil + return models.NewLoadedIdentity(obj.Author()), nil } func (labelChangeOperationResolver) Date(_ context.Context, obj *bug.LabelChangeOperation) (*time.Time, error) { @@ -91,7 +91,7 @@ func (setStatusOperationResolver) ID(_ context.Context, obj *bug.SetStatusOperat } func (setStatusOperationResolver) Author(_ context.Context, obj *bug.SetStatusOperation) (models.IdentityWrapper, error) { - return models.NewLoadedIdentity(obj.Author), nil + return models.NewLoadedIdentity(obj.Author()), nil } func (setStatusOperationResolver) Date(_ context.Context, obj *bug.SetStatusOperation) (*time.Time, error) { @@ -112,7 +112,7 @@ func (setTitleOperationResolver) ID(_ context.Context, obj *bug.SetTitleOperatio } func (setTitleOperationResolver) Author(_ context.Context, obj *bug.SetTitleOperation) (models.IdentityWrapper, error) { - return models.NewLoadedIdentity(obj.Author), nil + return models.NewLoadedIdentity(obj.Author()), nil } func (setTitleOperationResolver) Date(_ context.Context, obj *bug.SetTitleOperation) (*time.Time, error) { diff --git a/api/graphql/resolvers/query.go b/api/graphql/resolvers/query.go index 4ad7ae0c..b2003555 100644 --- a/api/graphql/resolvers/query.go +++ b/api/graphql/resolvers/query.go @@ -14,19 +14,6 @@ type rootQueryResolver struct { cache *cache.MultiRepoCache } -func (r rootQueryResolver) DefaultRepository(_ context.Context) (*models.Repository, error) { - repo, err := r.cache.DefaultRepo() - - if err != nil { - return nil, err - } - - return &models.Repository{ - Cache: r.cache, - Repo: repo, - }, nil -} - func (r rootQueryResolver) Repository(_ context.Context, ref *string) (*models.Repository, error) { var repo *cache.RepoCache var err error diff --git a/bridge/github/export.go b/bridge/github/export.go index 57b52ee0..1a59fbb3 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -294,7 +294,7 @@ func (ge *githubExporter) exportBug(ctx context.Context, b *cache.BugCache, out continue } - opAuthor := op.GetAuthor() + opAuthor := op.Author() client, err := ge.getClientForIdentity(opAuthor.Id()) if err != nil { continue diff --git a/bridge/github/import_test.go b/bridge/github/import_test.go index 84bf774e..324d5421 100644 --- a/bridge/github/import_test.go +++ b/bridge/github/import_test.go @@ -182,29 +182,24 @@ func TestGithubImporter(t *testing.T) { for i, op := range tt.bug.Operations { require.IsType(t, ops[i], op) + require.Equal(t, op.Author().Name(), ops[i].Author().Name()) - switch op.(type) { + switch op := op.(type) { case *bug.CreateOperation: - require.Equal(t, op.(*bug.CreateOperation).Title, ops[i].(*bug.CreateOperation).Title) - require.Equal(t, op.(*bug.CreateOperation).Message, ops[i].(*bug.CreateOperation).Message) - require.Equal(t, op.(*bug.CreateOperation).Author.Name(), ops[i].(*bug.CreateOperation).Author.Name()) + require.Equal(t, op.Title, ops[i].(*bug.CreateOperation).Title) + require.Equal(t, op.Message, ops[i].(*bug.CreateOperation).Message) case *bug.SetStatusOperation: - require.Equal(t, op.(*bug.SetStatusOperation).Status, ops[i].(*bug.SetStatusOperation).Status) - require.Equal(t, op.(*bug.SetStatusOperation).Author.Name(), ops[i].(*bug.SetStatusOperation).Author.Name()) + require.Equal(t, op.Status, ops[i].(*bug.SetStatusOperation).Status) case *bug.SetTitleOperation: - require.Equal(t, op.(*bug.SetTitleOperation).Was, ops[i].(*bug.SetTitleOperation).Was) - require.Equal(t, op.(*bug.SetTitleOperation).Title, ops[i].(*bug.SetTitleOperation).Title) - require.Equal(t, op.(*bug.SetTitleOperation).Author.Name(), ops[i].(*bug.SetTitleOperation).Author.Name()) + require.Equal(t, op.Was, ops[i].(*bug.SetTitleOperation).Was) + require.Equal(t, op.Title, ops[i].(*bug.SetTitleOperation).Title) case *bug.LabelChangeOperation: - require.ElementsMatch(t, op.(*bug.LabelChangeOperation).Added, ops[i].(*bug.LabelChangeOperation).Added) - require.ElementsMatch(t, op.(*bug.LabelChangeOperation).Removed, ops[i].(*bug.LabelChangeOperation).Removed) - require.Equal(t, op.(*bug.LabelChangeOperation).Author.Name(), ops[i].(*bug.LabelChangeOperation).Author.Name()) + require.ElementsMatch(t, op.Added, ops[i].(*bug.LabelChangeOperation).Added) + require.ElementsMatch(t, op.Removed, ops[i].(*bug.LabelChangeOperation).Removed) case *bug.AddCommentOperation: - require.Equal(t, op.(*bug.AddCommentOperation).Message, ops[i].(*bug.AddCommentOperation).Message) - require.Equal(t, op.(*bug.AddCommentOperation).Author.Name(), ops[i].(*bug.AddCommentOperation).Author.Name()) + require.Equal(t, op.Message, ops[i].(*bug.AddCommentOperation).Message) case *bug.EditCommentOperation: - require.Equal(t, op.(*bug.EditCommentOperation).Message, ops[i].(*bug.EditCommentOperation).Message) - require.Equal(t, op.(*bug.EditCommentOperation).Author.Name(), ops[i].(*bug.EditCommentOperation).Author.Name()) + require.Equal(t, op.Message, ops[i].(*bug.EditCommentOperation).Message) default: panic("unknown operation type") diff --git a/bridge/gitlab/export.go b/bridge/gitlab/export.go index c3aa6202..9c3864ec 100644 --- a/bridge/gitlab/export.go +++ b/bridge/gitlab/export.go @@ -267,7 +267,7 @@ func (ge *gitlabExporter) exportBug(ctx context.Context, b *cache.BugCache, out continue } - opAuthor := op.GetAuthor() + opAuthor := op.Author() client, err := ge.getIdentityClient(opAuthor.Id()) if err != nil { continue diff --git a/bridge/gitlab/import_test.go b/bridge/gitlab/import_test.go index 2956ad8b..1405e43b 100644 --- a/bridge/gitlab/import_test.go +++ b/bridge/gitlab/import_test.go @@ -138,29 +138,24 @@ func TestGitlabImport(t *testing.T) { for i, op := range tt.bug.Operations { require.IsType(t, ops[i], op) + require.Equal(t, op.Author().Name(), ops[i].Author().Name()) - switch op.(type) { + switch op := op.(type) { case *bug.CreateOperation: - require.Equal(t, op.(*bug.CreateOperation).Title, ops[i].(*bug.CreateOperation).Title) - require.Equal(t, op.(*bug.CreateOperation).Message, ops[i].(*bug.CreateOperation).Message) - require.Equal(t, op.(*bug.CreateOperation).Author.Name(), ops[i].(*bug.CreateOperation).Author.Name()) + require.Equal(t, op.Title, ops[i].(*bug.CreateOperation).Title) + require.Equal(t, op.Message, ops[i].(*bug.CreateOperation).Message) case *bug.SetStatusOperation: - require.Equal(t, op.(*bug.SetStatusOperation).Status, ops[i].(*bug.SetStatusOperation).Status) - require.Equal(t, op.(*bug.SetStatusOperation).Author.Name(), ops[i].(*bug.SetStatusOperation).Author.Name()) + require.Equal(t, op.Status, ops[i].(*bug.SetStatusOperation).Status) case *bug.SetTitleOperation: - require.Equal(t, op.(*bug.SetTitleOperation).Was, ops[i].(*bug.SetTitleOperation).Was) - require.Equal(t, op.(*bug.SetTitleOperation).Title, ops[i].(*bug.SetTitleOperation).Title) - require.Equal(t, op.(*bug.SetTitleOperation).Author.Name(), ops[i].(*bug.SetTitleOperation).Author.Name()) + require.Equal(t, op.Was, ops[i].(*bug.SetTitleOperation).Was) + require.Equal(t, op.Title, ops[i].(*bug.SetTitleOperation).Title) case *bug.LabelChangeOperation: - require.ElementsMatch(t, op.(*bug.LabelChangeOperation).Added, ops[i].(*bug.LabelChangeOperation).Added) - require.ElementsMatch(t, op.(*bug.LabelChangeOperation).Removed, ops[i].(*bug.LabelChangeOperation).Removed) - require.Equal(t, op.(*bug.LabelChangeOperation).Author.Name(), ops[i].(*bug.LabelChangeOperation).Author.Name()) + require.ElementsMatch(t, op.Added, ops[i].(*bug.LabelChangeOperation).Added) + require.ElementsMatch(t, op.Removed, ops[i].(*bug.LabelChangeOperation).Removed) case *bug.AddCommentOperation: - require.Equal(t, op.(*bug.AddCommentOperation).Message, ops[i].(*bug.AddCommentOperation).Message) - require.Equal(t, op.(*bug.AddCommentOperation).Author.Name(), ops[i].(*bug.AddCommentOperation).Author.Name()) + require.Equal(t, op.Message, ops[i].(*bug.AddCommentOperation).Message) case *bug.EditCommentOperation: - require.Equal(t, op.(*bug.EditCommentOperation).Message, ops[i].(*bug.EditCommentOperation).Message) - require.Equal(t, op.(*bug.EditCommentOperation).Author.Name(), ops[i].(*bug.EditCommentOperation).Author.Name()) + require.Equal(t, op.Message, ops[i].(*bug.EditCommentOperation).Message) default: panic("unknown operation type") diff --git a/bridge/jira/export.go b/bridge/jira/export.go index e6167966..34f41d09 100644 --- a/bridge/jira/export.go +++ b/bridge/jira/export.go @@ -309,7 +309,7 @@ func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, out ch continue } - opAuthor := op.GetAuthor() + opAuthor := op.Author() client, err := je.getClientForIdentity(opAuthor.Id()) if err != nil { out <- core.NewExportError( diff --git a/bug/bug.go b/bug/bug.go index 0c66f8ac..9d19a42c 100644 --- a/bug/bug.go +++ b/bug/bug.go @@ -2,222 +2,62 @@ package bug import ( - "encoding/json" "fmt" - "github.com/pkg/errors" - "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/entity/dag" "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" - "github.com/MichaelMure/git-bug/util/lamport" ) -const bugsRefPattern = "refs/bugs/" -const bugsRemoteRefPattern = "refs/remotes/%s/bugs/" - -const opsEntryName = "ops" -const mediaEntryName = "media" - -const createClockEntryPrefix = "create-clock-" -const createClockEntryPattern = "create-clock-%d" -const editClockEntryPrefix = "edit-clock-" -const editClockEntryPattern = "edit-clock-%d" - -const creationClockName = "bug-create" -const editClockName = "bug-edit" +var _ Interface = &Bug{} +var _ entity.Interface = &Bug{} -var ErrBugNotExist = errors.New("bug doesn't exist") +// 1: original format +// 2: no more legacy identities +// 3: Ids are generated from the create operation serialized data instead of from the first git commit +// 4: with DAG entity framework +const formatVersion = 4 -func NewErrMultipleMatchBug(matching []entity.Id) *entity.ErrMultipleMatch { - return entity.NewErrMultipleMatch("bug", matching) +var def = dag.Definition{ + Typename: "bug", + Namespace: "bugs", + OperationUnmarshaler: operationUnmarshaller, + FormatVersion: formatVersion, } -func NewErrMultipleMatchOp(matching []entity.Id) *entity.ErrMultipleMatch { - return entity.NewErrMultipleMatch("operation", matching) -} - -var _ Interface = &Bug{} -var _ entity.Interface = &Bug{} +var ClockLoader = dag.ClockLoader(def) // Bug hold the data of a bug thread, organized in a way close to // how it will be persisted inside Git. This is the data structure // used to merge two different version of the same Bug. type Bug struct { - // A Lamport clock is a logical clock that allow to order event - // inside a distributed system. - // It must be the first field in this struct due to https://github.com/golang/go/issues/599 - createTime lamport.Time - editTime lamport.Time - - lastCommit repository.Hash - - // all the committed operations - packs []OperationPack - - // a temporary pack of operations used for convenience to pile up new operations - // before a commit - staging OperationPack + *dag.Entity } // NewBug create a new Bug func NewBug() *Bug { - // No logical clock yet - return &Bug{} -} - -// ReadLocal will read a local bug from its hash -func ReadLocal(repo repository.ClockedRepo, id entity.Id) (*Bug, error) { - ref := bugsRefPattern + id.String() - return read(repo, identity.NewSimpleResolver(repo), ref) -} - -// ReadLocalWithResolver will read a local bug from its hash -func ReadLocalWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver, id entity.Id) (*Bug, error) { - ref := bugsRefPattern + id.String() - return read(repo, identityResolver, ref) -} - -// ReadRemote will read a remote bug from its hash -func ReadRemote(repo repository.ClockedRepo, remote string, id entity.Id) (*Bug, error) { - ref := fmt.Sprintf(bugsRemoteRefPattern, remote) + id.String() - return read(repo, identity.NewSimpleResolver(repo), ref) -} - -// ReadRemoteWithResolver will read a remote bug from its hash -func ReadRemoteWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver, remote string, id entity.Id) (*Bug, error) { - ref := fmt.Sprintf(bugsRemoteRefPattern, remote) + id.String() - return read(repo, identityResolver, ref) -} - -// read will read and parse a Bug from git -func read(repo repository.ClockedRepo, identityResolver identity.Resolver, ref string) (*Bug, error) { - id := entity.RefToId(ref) - - if err := id.Validate(); err != nil { - return nil, errors.Wrap(err, "invalid ref ") - } - - hashes, err := repo.ListCommits(ref) - if err != nil { - return nil, ErrBugNotExist - } - if len(hashes) == 0 { - return nil, fmt.Errorf("empty bug") - } - - bug := Bug{} - - // Load each OperationPack - for _, hash := range hashes { - tree, err := readTree(repo, hash) - if err != nil { - return nil, err - } - - // Due to rebase, edit Lamport time are not necessarily ordered - if tree.editTime > bug.editTime { - bug.editTime = tree.editTime - } - - // Update the clocks - err = repo.Witness(creationClockName, bug.createTime) - if err != nil { - return nil, errors.Wrap(err, "failed to update create lamport clock") - } - err = repo.Witness(editClockName, bug.editTime) - if err != nil { - return nil, errors.Wrap(err, "failed to update edit lamport clock") - } - - data, err := repo.ReadData(tree.opsEntry.Hash) - if err != nil { - return nil, errors.Wrap(err, "failed to read git blob data") - } - - opp := &OperationPack{} - err = json.Unmarshal(data, &opp) - if err != nil { - return nil, errors.Wrap(err, "failed to decode OperationPack json") - } - - // tag the pack with the commit hash - opp.commitHash = hash - bug.lastCommit = hash - - // if it's the first OperationPack read - if len(bug.packs) == 0 { - bug.createTime = tree.createTime - } - - bug.packs = append(bug.packs, *opp) - } - - // Bug Id is the Id of the first operation - if len(bug.packs[0].Operations) == 0 { - return nil, fmt.Errorf("first OperationPack is empty") - } - if id != bug.packs[0].Operations[0].Id() { - return nil, fmt.Errorf("bug ID doesn't match the first operation ID") + return &Bug{ + Entity: dag.New(def), } +} - // Make sure that the identities are properly loaded - err = bug.EnsureIdentities(identityResolver) +// Read will read a bug from a repository +func Read(repo repository.ClockedRepo, id entity.Id) (*Bug, error) { + e, err := dag.Read(def, repo, identity.NewSimpleResolver(repo), id) if err != nil { return nil, err } - - return &bug, nil + return &Bug{Entity: e}, nil } -// RemoveBug will remove a local bug from its entity.Id -func RemoveBug(repo repository.ClockedRepo, id entity.Id) error { - var fullMatches []string - - refs, err := repo.ListRefs(bugsRefPattern + id.String()) +// ReadWithResolver will read a bug from its Id, with a custom identity.Resolver +func ReadWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver, id entity.Id) (*Bug, error) { + e, err := dag.Read(def, repo, identityResolver, id) if err != nil { - return err - } - if len(refs) > 1 { - return NewErrMultipleMatchBug(entity.RefsToIds(refs)) - } - if len(refs) == 1 { - // we have the bug locally - fullMatches = append(fullMatches, refs[0]) - } - - remotes, err := repo.GetRemotes() - if err != nil { - return err - } - - for remote := range remotes { - remotePrefix := fmt.Sprintf(bugsRemoteRefPattern+id.String(), remote) - remoteRefs, err := repo.ListRefs(remotePrefix) - if err != nil { - return err - } - if len(remoteRefs) > 1 { - return NewErrMultipleMatchBug(entity.RefsToIds(refs)) - } - if len(remoteRefs) == 1 { - // found the bug in a remote - fullMatches = append(fullMatches, remoteRefs[0]) - } - } - - if len(fullMatches) == 0 { - return ErrBugNotExist - } - - for _, ref := range fullMatches { - err = repo.RemoveRef(ref) - if err != nil { - return err - } + return nil, err } - - return nil + return &Bug{Entity: e}, nil } type StreamedBug struct { @@ -225,50 +65,33 @@ type StreamedBug struct { Err error } -// ReadAllLocal read and parse all local bugs -func ReadAllLocal(repo repository.ClockedRepo) <-chan StreamedBug { - return readAll(repo, identity.NewSimpleResolver(repo), bugsRefPattern) +// ReadAll read and parse all local bugs +func ReadAll(repo repository.ClockedRepo) <-chan StreamedBug { + return readAll(repo, identity.NewSimpleResolver(repo)) } -// ReadAllLocalWithResolver read and parse all local bugs -func ReadAllLocalWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver) <-chan StreamedBug { - return readAll(repo, identityResolver, bugsRefPattern) -} - -// ReadAllRemote read and parse all remote bugs for a given remote -func ReadAllRemote(repo repository.ClockedRepo, remote string) <-chan StreamedBug { - refPrefix := fmt.Sprintf(bugsRemoteRefPattern, remote) - return readAll(repo, identity.NewSimpleResolver(repo), refPrefix) -} - -// ReadAllRemoteWithResolver read and parse all remote bugs for a given remote -func ReadAllRemoteWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver, remote string) <-chan StreamedBug { - refPrefix := fmt.Sprintf(bugsRemoteRefPattern, remote) - return readAll(repo, identityResolver, refPrefix) +// ReadAllWithResolver read and parse all local bugs +func ReadAllWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver) <-chan StreamedBug { + return readAll(repo, identityResolver) } // Read and parse all available bug with a given ref prefix -func readAll(repo repository.ClockedRepo, identityResolver identity.Resolver, refPrefix string) <-chan StreamedBug { +func readAll(repo repository.ClockedRepo, identityResolver identity.Resolver) <-chan StreamedBug { out := make(chan StreamedBug) go func() { defer close(out) - refs, err := repo.ListRefs(refPrefix) - if err != nil { - out <- StreamedBug{Err: err} - return - } - - for _, ref := range refs { - b, err := read(repo, identityResolver, ref) - - if err != nil { - out <- StreamedBug{Err: err} - return + for streamedEntity := range dag.ReadAll(def, repo, identityResolver) { + if streamedEntity.Err != nil { + out <- StreamedBug{ + Err: streamedEntity.Err, + } + } else { + out <- StreamedBug{ + Bug: &Bug{Entity: streamedEntity.Entity}, + } } - - out <- StreamedBug{Bug: b} } }() @@ -277,345 +100,78 @@ func readAll(repo repository.ClockedRepo, identityResolver identity.Resolver, re // ListLocalIds list all the available local bug ids func ListLocalIds(repo repository.Repo) ([]entity.Id, error) { - refs, err := repo.ListRefs(bugsRefPattern) - if err != nil { - return nil, err - } - - return entity.RefsToIds(refs), nil + return dag.ListLocalIds(def, repo) } // Validate check if the Bug data is valid func (bug *Bug) Validate() error { - // non-empty - if len(bug.packs) == 0 && bug.staging.IsEmpty() { - return fmt.Errorf("bug has no operations") - } - - // check if each pack and operations are valid - for _, pack := range bug.packs { - if err := pack.Validate(); err != nil { - return err - } - } - - // check if staging is valid if needed - if !bug.staging.IsEmpty() { - if err := bug.staging.Validate(); err != nil { - return errors.Wrap(err, "staging") - } + if err := bug.Entity.Validate(); err != nil { + return err } // The very first Op should be a CreateOp firstOp := bug.FirstOp() - if firstOp == nil || firstOp.base().OperationType != CreateOp { + if firstOp == nil || firstOp.Type() != CreateOp { return fmt.Errorf("first operation should be a Create op") } // Check that there is no more CreateOp op - // Check that there is no colliding operation's ID - it := NewOperationIterator(bug) - createCount := 0 - ids := make(map[entity.Id]struct{}) - for it.Next() { - if it.Value().base().OperationType == CreateOp { - createCount++ + for i, op := range bug.Operations() { + if i == 0 { + continue } - if _, ok := ids[it.Value().Id()]; ok { - return fmt.Errorf("id collision: %s", it.Value().Id()) + if op.Type() == CreateOp { + return fmt.Errorf("only one Create op allowed") } - ids[it.Value().Id()] = struct{}{} - } - - if createCount != 1 { - return fmt.Errorf("only one Create op allowed") } return nil } -// Append an operation into the staging area, to be committed later +// Append add a new Operation to the Bug func (bug *Bug) Append(op Operation) { - if len(bug.packs) == 0 && len(bug.staging.Operations) == 0 { - if op.base().OperationType != CreateOp { - panic("first operation should be a Create") - } - } - bug.staging.Append(op) + bug.Entity.Append(op) } -// Commit write the staging area in Git and move the operations to the packs -func (bug *Bug) Commit(repo repository.ClockedRepo) error { - if !bug.NeedCommit() { - return fmt.Errorf("can't commit a bug with no pending operation") - } - - if err := bug.Validate(); err != nil { - return errors.Wrap(err, "can't commit a bug with invalid data") - } - - // update clocks - var err error - bug.editTime, err = repo.Increment(editClockName) - if err != nil { - return err - } - if bug.lastCommit == "" { - bug.createTime, err = repo.Increment(creationClockName) - if err != nil { - return err - } +// Operations return the ordered operations +func (bug *Bug) Operations() []Operation { + source := bug.Entity.Operations() + result := make([]Operation, len(source)) + for i, op := range source { + result[i] = op.(Operation) } - - // Write the Ops as a Git blob containing the serialized array - hash, err := bug.staging.Write(repo) - if err != nil { - return err - } - - // Make a Git tree referencing this blob - tree := []repository.TreeEntry{ - // the last pack of ops - {ObjectType: repository.Blob, Hash: hash, Name: opsEntryName}, - } - - // Store the logical clocks as well - // --> edit clock for each OperationPack/commits - // --> create clock only for the first OperationPack/commits - // - // To avoid having one blob for each clock value, clocks are serialized - // directly into the entry name - emptyBlobHash, err := repo.StoreData([]byte{}) - if err != nil { - return err - } - tree = append(tree, repository.TreeEntry{ - ObjectType: repository.Blob, - Hash: emptyBlobHash, - Name: fmt.Sprintf(editClockEntryPattern, bug.editTime), - }) - if bug.lastCommit == "" { - tree = append(tree, repository.TreeEntry{ - ObjectType: repository.Blob, - Hash: emptyBlobHash, - Name: fmt.Sprintf(createClockEntryPattern, bug.createTime), - }) - } - - // Reference, if any, all the files required by the ops - // Git will check that they actually exist in the storage and will make sure - // to push/pull them as needed. - mediaTree := makeMediaTree(bug.staging) - if len(mediaTree) > 0 { - mediaTreeHash, err := repo.StoreTree(mediaTree) - if err != nil { - return err - } - tree = append(tree, repository.TreeEntry{ - ObjectType: repository.Tree, - Hash: mediaTreeHash, - Name: mediaEntryName, - }) - } - - // Store the tree - hash, err = repo.StoreTree(tree) - if err != nil { - return err - } - - // Write a Git commit referencing the tree, with the previous commit as parent - if bug.lastCommit != "" { - hash, err = repo.StoreCommit(hash, bug.lastCommit) - } else { - hash, err = repo.StoreCommit(hash) - } - if err != nil { - return err - } - - bug.lastCommit = hash - bug.staging.commitHash = hash - bug.packs = append(bug.packs, bug.staging) - bug.staging = OperationPack{} - - // Create or update the Git reference for this bug - // When pushing later, the remote will ensure that this ref update - // is fast-forward, that is no data has been overwritten - ref := fmt.Sprintf("%s%s", bugsRefPattern, bug.Id().String()) - return repo.UpdateRef(ref, hash) + return result } -func (bug *Bug) CommitAsNeeded(repo repository.ClockedRepo) error { - if !bug.NeedCommit() { - return nil - } - return bug.Commit(repo) -} - -func (bug *Bug) NeedCommit() bool { - return !bug.staging.IsEmpty() -} - -// Merge a different version of the same bug by rebasing operations of this bug -// that are not present in the other on top of the chain of operations of the -// other version. -func (bug *Bug) Merge(repo repository.Repo, other Interface) (bool, error) { - var otherBug = bugFromInterface(other) - - // Note: a faster merge should be possible without actually reading and parsing - // all operations pack of our side. - // Reading the other side is still necessary to validate remote data, at least - // for new operations - - if bug.Id() != otherBug.Id() { - return false, errors.New("merging unrelated bugs is not supported") - } - - if len(otherBug.staging.Operations) > 0 { - return false, errors.New("merging a bug with a non-empty staging is not supported") - } - - if bug.lastCommit == "" || otherBug.lastCommit == "" { - return false, errors.New("can't merge a bug that has never been stored") - } - - ancestor, err := repo.FindCommonAncestor(bug.lastCommit, otherBug.lastCommit) - if err != nil { - return false, errors.Wrap(err, "can't find common ancestor") - } - - ancestorIndex := 0 - newPacks := make([]OperationPack, 0, len(bug.packs)) - - // Find the root of the rebase - for i, pack := range bug.packs { - newPacks = append(newPacks, pack) - - if pack.commitHash == ancestor { - ancestorIndex = i - break - } - } - - if len(otherBug.packs) == ancestorIndex+1 { - // Nothing to rebase, return early - return false, nil - } - - // get other bug's extra packs - for i := ancestorIndex + 1; i < len(otherBug.packs); i++ { - // clone is probably not necessary - newPack := otherBug.packs[i].Clone() - - newPacks = append(newPacks, newPack) - bug.lastCommit = newPack.commitHash - } - - // rebase our extra packs - for i := ancestorIndex + 1; i < len(bug.packs); i++ { - pack := bug.packs[i] - - // get the referenced git tree - treeHash, err := repo.GetTreeHash(pack.commitHash) - - if err != nil { - return false, err - } - - // create a new commit with the correct ancestor - hash, err := repo.StoreCommit(treeHash, bug.lastCommit) - - if err != nil { - return false, err - } - - // replace the pack - newPack := pack.Clone() - newPack.commitHash = hash - newPacks = append(newPacks, newPack) - - // update the bug - bug.lastCommit = hash +// Compile a bug in a easily usable snapshot +func (bug *Bug) Compile() Snapshot { + snap := Snapshot{ + id: bug.Id(), + Status: OpenStatus, } - bug.packs = newPacks - - // Update the git ref - err = repo.UpdateRef(bugsRefPattern+bug.Id().String(), bug.lastCommit) - if err != nil { - return false, err + for _, op := range bug.Operations() { + op.Apply(&snap) + snap.Operations = append(snap.Operations, op) } - return true, nil -} - -// Id return the Bug identifier -func (bug *Bug) Id() entity.Id { - // id is the id of the first operation - return bug.FirstOp().Id() -} - -// CreateLamportTime return the Lamport time of creation -func (bug *Bug) CreateLamportTime() lamport.Time { - return bug.createTime -} - -// EditLamportTime return the Lamport time of the last edit -func (bug *Bug) EditLamportTime() lamport.Time { - return bug.editTime + return snap } // Lookup for the very first operation of the bug. // For a valid Bug, this operation should be a CreateOp func (bug *Bug) FirstOp() Operation { - for _, pack := range bug.packs { - for _, op := range pack.Operations { - return op - } + if fo := bug.Entity.FirstOp(); fo != nil { + return fo.(Operation) } - - if !bug.staging.IsEmpty() { - return bug.staging.Operations[0] - } - return nil } // Lookup for the very last operation of the bug. // For a valid Bug, should never be nil func (bug *Bug) LastOp() Operation { - if !bug.staging.IsEmpty() { - return bug.staging.Operations[len(bug.staging.Operations)-1] + if lo := bug.Entity.LastOp(); lo != nil { + return lo.(Operation) } - - if len(bug.packs) == 0 { - return nil - } - - lastPack := bug.packs[len(bug.packs)-1] - - if len(lastPack.Operations) == 0 { - return nil - } - - return lastPack.Operations[len(lastPack.Operations)-1] -} - -// Compile a bug in a easily usable snapshot -func (bug *Bug) Compile() Snapshot { - snap := Snapshot{ - id: bug.Id(), - Status: OpenStatus, - } - - it := NewOperationIterator(bug) - - for it.Next() { - op := it.Value() - op.Apply(&snap) - snap.Operations = append(snap.Operations, op) - } - - return snap + return nil } diff --git a/bug/bug_actions.go b/bug/bug_actions.go index bf894ef8..6ca5ffd7 100644 --- a/bug/bug_actions.go +++ b/bug/bug_actions.go @@ -1,12 +1,10 @@ package bug import ( - "fmt" - "strings" - "github.com/pkg/errors" "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/entity/dag" "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" ) @@ -14,23 +12,23 @@ import ( // Fetch retrieve updates from a remote // This does not change the local bugs state func Fetch(repo repository.Repo, remote string) (string, error) { - return repo.FetchRefs(remote, "bugs") + return dag.Fetch(def, repo, remote) } // Push update a remote with the local changes func Push(repo repository.Repo, remote string) (string, error) { - return repo.PushRefs(remote, "bugs") + return dag.Push(def, repo, remote) } // Pull will do a Fetch + MergeAll // This function will return an error if a merge fail -func Pull(repo repository.ClockedRepo, remote string) error { +func Pull(repo repository.ClockedRepo, remote string, author identity.Interface) error { _, err := Fetch(repo, remote) if err != nil { return err } - for merge := range MergeAll(repo, remote) { + for merge := range MergeAll(repo, remote, author) { if merge.Err != nil { return merge.Err } @@ -42,95 +40,19 @@ func Pull(repo repository.ClockedRepo, remote string) error { return nil } -// MergeAll will merge all the available remote bug: -// -// - If the remote has new commit, the local bug is updated to match the same history -// (fast-forward update) -// - if the local bug has new commits but the remote don't, nothing is changed -// - if both local and remote bug have new commits (that is, we have a concurrent edition), -// new local commits are rewritten at the head of the remote history (that is, a rebase) -func MergeAll(repo repository.ClockedRepo, remote string) <-chan entity.MergeResult { - out := make(chan entity.MergeResult) - +// MergeAll will merge all the available remote bug +// 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(repo repository.ClockedRepo, remote string, author identity.Interface) <-chan entity.MergeResult { // no caching for the merge, we load everything from git even if that means multiple // copy of the same entity in memory. The cache layer will intercept the results to // invalidate entities if necessary. identityResolver := identity.NewSimpleResolver(repo) - go func() { - defer close(out) - - remoteRefSpec := fmt.Sprintf(bugsRemoteRefPattern, remote) - remoteRefs, err := repo.ListRefs(remoteRefSpec) - if err != nil { - out <- entity.MergeResult{Err: err} - return - } - - for _, remoteRef := range remoteRefs { - refSplit := strings.Split(remoteRef, "/") - id := entity.Id(refSplit[len(refSplit)-1]) - - if err := id.Validate(); err != nil { - out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "invalid ref").Error()) - continue - } - - remoteBug, err := read(repo, identityResolver, remoteRef) - - if err != nil { - out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "remote bug is not readable").Error()) - continue - } - - // Check for error in remote data - if err := remoteBug.Validate(); err != nil { - out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "remote bug is invalid").Error()) - continue - } - - localRef := bugsRefPattern + remoteBug.Id().String() - localExist, err := repo.RefExist(localRef) - - if err != nil { - out <- entity.NewMergeError(err, id) - continue - } - - // the bug is not local yet, simply create the reference - if !localExist { - err := repo.CopyRef(remoteRef, localRef) - - if err != nil { - out <- entity.NewMergeError(err, id) - return - } - - out <- entity.NewMergeNewStatus(id, remoteBug) - continue - } - - localBug, err := read(repo, identityResolver, localRef) - - if err != nil { - out <- entity.NewMergeError(errors.Wrap(err, "local bug is not readable"), id) - return - } - - updated, err := localBug.Merge(repo, remoteBug) - - if err != nil { - out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "merge failed").Error()) - return - } - - if updated { - out <- entity.NewMergeUpdatedStatus(id, localBug) - } else { - out <- entity.NewMergeNothingStatus(id) - } - } - }() + return dag.MergeAll(def, repo, identityResolver, remote, author) +} - return out +// RemoveBug will remove a local bug from its entity.Id +func RemoveBug(repo repository.ClockedRepo, id entity.Id) error { + return dag.Remove(def, repo, id) } diff --git a/bug/bug_actions_test.go b/bug/bug_actions_test.go deleted file mode 100644 index fc671063..00000000 --- a/bug/bug_actions_test.go +++ /dev/null @@ -1,394 +0,0 @@ -package bug - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/MichaelMure/git-bug/identity" - "github.com/MichaelMure/git-bug/repository" -) - -func TestPushPull(t *testing.T) { - repoA, repoB, remote := repository.SetupGoGitReposAndRemote() - defer repository.CleanupTestRepos(repoA, repoB, remote) - - reneA, err := identity.NewIdentity(repoA, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = reneA.Commit(repoA) - require.NoError(t, err) - - bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message") - require.NoError(t, err) - assert.True(t, bug1.NeedCommit()) - err = bug1.Commit(repoA) - require.NoError(t, err) - assert.False(t, bug1.NeedCommit()) - - // distribute the identity - _, err = identity.Push(repoA, "origin") - require.NoError(t, err) - err = identity.Pull(repoB, "origin") - require.NoError(t, err) - - // A --> remote --> B - _, err = Push(repoA, "origin") - require.NoError(t, err) - - err = Pull(repoB, "origin") - require.NoError(t, err) - - bugs := allBugs(t, ReadAllLocal(repoB)) - - if len(bugs) != 1 { - t.Fatal("Unexpected number of bugs") - } - - // B --> remote --> A - reneB, err := identity.ReadLocal(repoA, reneA.Id()) - require.NoError(t, err) - - bug2, _, err := Create(reneB, time.Now().Unix(), "bug2", "message") - require.NoError(t, err) - err = bug2.Commit(repoB) - require.NoError(t, err) - - _, err = Push(repoB, "origin") - require.NoError(t, err) - - err = Pull(repoA, "origin") - require.NoError(t, err) - - bugs = allBugs(t, ReadAllLocal(repoA)) - - if len(bugs) != 2 { - t.Fatal("Unexpected number of bugs") - } -} - -func allBugs(t testing.TB, bugs <-chan StreamedBug) []*Bug { - var result []*Bug - for streamed := range bugs { - if streamed.Err != nil { - t.Fatal(streamed.Err) - } - result = append(result, streamed.Bug) - } - return result -} - -func TestRebaseTheirs(t *testing.T) { - _RebaseTheirs(t) -} - -func BenchmarkRebaseTheirs(b *testing.B) { - for n := 0; n < b.N; n++ { - _RebaseTheirs(b) - } -} - -func _RebaseTheirs(t testing.TB) { - repoA, repoB, remote := repository.SetupGoGitReposAndRemote() - defer repository.CleanupTestRepos(repoA, repoB, remote) - - reneA, err := identity.NewIdentity(repoA, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = reneA.Commit(repoA) - require.NoError(t, err) - - bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message") - require.NoError(t, err) - assert.True(t, bug1.NeedCommit()) - err = bug1.Commit(repoA) - require.NoError(t, err) - assert.False(t, bug1.NeedCommit()) - - // distribute the identity - _, err = identity.Push(repoA, "origin") - require.NoError(t, err) - err = identity.Pull(repoB, "origin") - require.NoError(t, err) - - // A --> remote - - _, err = Push(repoA, "origin") - require.NoError(t, err) - - // remote --> B - err = Pull(repoB, "origin") - require.NoError(t, err) - - bug2, err := ReadLocal(repoB, bug1.Id()) - require.NoError(t, err) - assert.False(t, bug2.NeedCommit()) - - reneB, err := identity.ReadLocal(repoA, reneA.Id()) - require.NoError(t, err) - - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message2") - require.NoError(t, err) - assert.True(t, bug2.NeedCommit()) - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message3") - require.NoError(t, err) - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message4") - require.NoError(t, err) - err = bug2.Commit(repoB) - require.NoError(t, err) - assert.False(t, bug2.NeedCommit()) - - // B --> remote - _, err = Push(repoB, "origin") - require.NoError(t, err) - - // remote --> A - err = Pull(repoA, "origin") - require.NoError(t, err) - - bugs := allBugs(t, ReadAllLocal(repoB)) - - if len(bugs) != 1 { - t.Fatal("Unexpected number of bugs") - } - - bug3, err := ReadLocal(repoA, bug1.Id()) - require.NoError(t, err) - - if nbOps(bug3) != 4 { - t.Fatal("Unexpected number of operations") - } -} - -func TestRebaseOurs(t *testing.T) { - _RebaseOurs(t) -} - -func BenchmarkRebaseOurs(b *testing.B) { - for n := 0; n < b.N; n++ { - _RebaseOurs(b) - } -} - -func _RebaseOurs(t testing.TB) { - repoA, repoB, remote := repository.SetupGoGitReposAndRemote() - defer repository.CleanupTestRepos(repoA, repoB, remote) - - reneA, err := identity.NewIdentity(repoA, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = reneA.Commit(repoA) - require.NoError(t, err) - - bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message") - require.NoError(t, err) - err = bug1.Commit(repoA) - require.NoError(t, err) - - // distribute the identity - _, err = identity.Push(repoA, "origin") - require.NoError(t, err) - err = identity.Pull(repoB, "origin") - require.NoError(t, err) - - // A --> remote - _, err = Push(repoA, "origin") - require.NoError(t, err) - - // remote --> B - err = Pull(repoB, "origin") - require.NoError(t, err) - - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message2") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message3") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message4") - require.NoError(t, err) - err = bug1.Commit(repoA) - require.NoError(t, err) - - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message5") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message6") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message7") - require.NoError(t, err) - err = bug1.Commit(repoA) - require.NoError(t, err) - - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message8") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message9") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message10") - require.NoError(t, err) - err = bug1.Commit(repoA) - require.NoError(t, err) - - // remote --> A - err = Pull(repoA, "origin") - require.NoError(t, err) - - bugs := allBugs(t, ReadAllLocal(repoA)) - - if len(bugs) != 1 { - t.Fatal("Unexpected number of bugs") - } - - bug2, err := ReadLocal(repoA, bug1.Id()) - require.NoError(t, err) - - if nbOps(bug2) != 10 { - t.Fatal("Unexpected number of operations") - } -} - -func nbOps(b *Bug) int { - it := NewOperationIterator(b) - counter := 0 - for it.Next() { - counter++ - } - return counter -} - -func TestRebaseConflict(t *testing.T) { - _RebaseConflict(t) -} - -func BenchmarkRebaseConflict(b *testing.B) { - for n := 0; n < b.N; n++ { - _RebaseConflict(b) - } -} - -func _RebaseConflict(t testing.TB) { - repoA, repoB, remote := repository.SetupGoGitReposAndRemote() - defer repository.CleanupTestRepos(repoA, repoB, remote) - - reneA, err := identity.NewIdentity(repoA, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = reneA.Commit(repoA) - require.NoError(t, err) - - bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message") - require.NoError(t, err) - err = bug1.Commit(repoA) - require.NoError(t, err) - - // distribute the identity - _, err = identity.Push(repoA, "origin") - require.NoError(t, err) - err = identity.Pull(repoB, "origin") - require.NoError(t, err) - - // A --> remote - _, err = Push(repoA, "origin") - require.NoError(t, err) - - // remote --> B - err = Pull(repoB, "origin") - require.NoError(t, err) - - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message2") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message3") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message4") - require.NoError(t, err) - err = bug1.Commit(repoA) - require.NoError(t, err) - - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message5") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message6") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message7") - require.NoError(t, err) - err = bug1.Commit(repoA) - require.NoError(t, err) - - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message8") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message9") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message10") - require.NoError(t, err) - err = bug1.Commit(repoA) - require.NoError(t, err) - - bug2, err := ReadLocal(repoB, bug1.Id()) - require.NoError(t, err) - - reneB, err := identity.ReadLocal(repoA, reneA.Id()) - require.NoError(t, err) - - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message11") - require.NoError(t, err) - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message12") - require.NoError(t, err) - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message13") - require.NoError(t, err) - err = bug2.Commit(repoB) - require.NoError(t, err) - - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message14") - require.NoError(t, err) - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message15") - require.NoError(t, err) - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message16") - require.NoError(t, err) - err = bug2.Commit(repoB) - require.NoError(t, err) - - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message17") - require.NoError(t, err) - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message18") - require.NoError(t, err) - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message19") - require.NoError(t, err) - err = bug2.Commit(repoB) - require.NoError(t, err) - - // A --> remote - _, err = Push(repoA, "origin") - require.NoError(t, err) - - // remote --> B - err = Pull(repoB, "origin") - require.NoError(t, err) - - bugs := allBugs(t, ReadAllLocal(repoB)) - - if len(bugs) != 1 { - t.Fatal("Unexpected number of bugs") - } - - bug3, err := ReadLocal(repoB, bug1.Id()) - require.NoError(t, err) - - if nbOps(bug3) != 19 { - t.Fatal("Unexpected number of operations") - } - - // B --> remote - _, err = Push(repoB, "origin") - require.NoError(t, err) - - // remote --> A - err = Pull(repoA, "origin") - require.NoError(t, err) - - bugs = allBugs(t, ReadAllLocal(repoA)) - - if len(bugs) != 1 { - t.Fatal("Unexpected number of bugs") - } - - bug4, err := ReadLocal(repoA, bug1.Id()) - require.NoError(t, err) - - if nbOps(bug4) != 19 { - t.Fatal("Unexpected number of operations") - } -} diff --git a/bug/bug_test.go b/bug/bug_test.go deleted file mode 100644 index a8987ac1..00000000 --- a/bug/bug_test.go +++ /dev/null @@ -1,190 +0,0 @@ -package bug - -import ( - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/MichaelMure/git-bug/identity" - "github.com/MichaelMure/git-bug/repository" -) - -func TestBugId(t *testing.T) { - repo := repository.NewMockRepo() - - bug1 := NewBug() - - rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = rene.Commit(repo) - require.NoError(t, err) - - createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil) - - bug1.Append(createOp) - - err = bug1.Commit(repo) - - if err != nil { - t.Fatal(err) - } - - bug1.Id() -} - -func TestBugValidity(t *testing.T) { - repo := repository.NewMockRepo() - - bug1 := NewBug() - - rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = rene.Commit(repo) - require.NoError(t, err) - - createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil) - - if bug1.Validate() == nil { - t.Fatal("Empty bug should be invalid") - } - - bug1.Append(createOp) - - if bug1.Validate() != nil { - t.Fatal("Bug with just a CreateOp should be valid") - } - - err = bug1.Commit(repo) - if err != nil { - t.Fatal(err) - } - - bug1.Append(createOp) - - if bug1.Validate() == nil { - t.Fatal("Bug with multiple CreateOp should be invalid") - } - - err = bug1.Commit(repo) - if err == nil { - t.Fatal("Invalid bug should not commit") - } -} - -func TestBugCommitLoad(t *testing.T) { - repo := repository.NewMockRepo() - - bug1 := NewBug() - - rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = rene.Commit(repo) - require.NoError(t, err) - - createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil) - setTitleOp := NewSetTitleOp(rene, time.Now().Unix(), "title2", "title1") - addCommentOp := NewAddCommentOp(rene, time.Now().Unix(), "message2", nil) - - bug1.Append(createOp) - bug1.Append(setTitleOp) - - require.True(t, bug1.NeedCommit()) - - err = bug1.Commit(repo) - require.Nil(t, err) - require.False(t, bug1.NeedCommit()) - - bug2, err := ReadLocal(repo, bug1.Id()) - require.NoError(t, err) - equivalentBug(t, bug1, bug2) - - // add more op - - bug1.Append(addCommentOp) - - require.True(t, bug1.NeedCommit()) - - err = bug1.Commit(repo) - require.Nil(t, err) - require.False(t, bug1.NeedCommit()) - - bug3, err := ReadLocal(repo, bug1.Id()) - require.NoError(t, err) - equivalentBug(t, bug1, bug3) -} - -func equivalentBug(t *testing.T, expected, actual *Bug) { - require.Equal(t, len(expected.packs), len(actual.packs)) - - for i := range expected.packs { - for j := range expected.packs[i].Operations { - actual.packs[i].Operations[j].base().id = expected.packs[i].Operations[j].base().id - } - } - - require.Equal(t, expected, actual) -} - -func TestBugRemove(t *testing.T) { - repo := repository.CreateGoGitTestRepo(false) - remoteA := repository.CreateGoGitTestRepo(true) - remoteB := repository.CreateGoGitTestRepo(true) - defer repository.CleanupTestRepos(repo, remoteA, remoteB) - - err := repo.AddRemote("remoteA", remoteA.GetLocalRemote()) - require.NoError(t, err) - - err = repo.AddRemote("remoteB", remoteB.GetLocalRemote()) - require.NoError(t, err) - - // generate a bunch of bugs - rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = rene.Commit(repo) - require.NoError(t, err) - - for i := 0; i < 100; i++ { - b := NewBug() - createOp := NewCreateOp(rene, time.Now().Unix(), "title", fmt.Sprintf("message%v", i), nil) - b.Append(createOp) - err = b.Commit(repo) - require.NoError(t, err) - } - - // and one more for testing - b := NewBug() - createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil) - b.Append(createOp) - err = b.Commit(repo) - require.NoError(t, err) - - _, err = Push(repo, "remoteA") - require.NoError(t, err) - - _, err = Push(repo, "remoteB") - require.NoError(t, err) - - _, err = Fetch(repo, "remoteA") - require.NoError(t, err) - - _, err = Fetch(repo, "remoteB") - require.NoError(t, err) - - err = RemoveBug(repo, b.Id()) - require.NoError(t, err) - - _, err = ReadLocal(repo, b.Id()) - require.Error(t, ErrBugNotExist, err) - - _, err = ReadRemote(repo, "remoteA", b.Id()) - require.Error(t, ErrBugNotExist, err) - - _, err = ReadRemote(repo, "remoteB", b.Id()) - require.Error(t, ErrBugNotExist, err) - - ids, err := ListLocalIds(repo) - require.NoError(t, err) - require.Len(t, ids, 100) -} diff --git a/bug/clocks.go b/bug/clocks.go deleted file mode 100644 index 58fce923..00000000 --- a/bug/clocks.go +++ /dev/null @@ -1,40 +0,0 @@ -package bug - -import ( - "github.com/MichaelMure/git-bug/identity" - "github.com/MichaelMure/git-bug/repository" -) - -// ClockLoader is the repository.ClockLoader for the Bug entity -var ClockLoader = repository.ClockLoader{ - Clocks: []string{creationClockName, editClockName}, - Witnesser: func(repo repository.ClockedRepo) error { - // We don't care about the actual identity so an IdentityStub will do - resolver := identity.NewStubResolver() - for b := range ReadAllLocalWithResolver(repo, resolver) { - if b.Err != nil { - return b.Err - } - - createClock, err := repo.GetOrCreateClock(creationClockName) - if err != nil { - return err - } - err = createClock.Witness(b.Bug.createTime) - if err != nil { - return err - } - - editClock, err := repo.GetOrCreateClock(editClockName) - if err != nil { - return err - } - err = editClock.Witness(b.Bug.editTime) - if err != nil { - return err - } - } - - return nil - }, -} diff --git a/bug/err.go b/bug/err.go new file mode 100644 index 00000000..1bd174bb --- /dev/null +++ b/bug/err.go @@ -0,0 +1,17 @@ +package bug + +import ( + "errors" + + "github.com/MichaelMure/git-bug/entity" +) + +var ErrBugNotExist = errors.New("bug doesn't exist") + +func NewErrMultipleMatchBug(matching []entity.Id) *entity.ErrMultipleMatch { + return entity.NewErrMultipleMatch("bug", matching) +} + +func NewErrMultipleMatchOp(matching []entity.Id) *entity.ErrMultipleMatch { + return entity.NewErrMultipleMatch("operation", matching) +} diff --git a/bug/git_tree.go b/bug/git_tree.go deleted file mode 100644 index a5583bda..00000000 --- a/bug/git_tree.go +++ /dev/null @@ -1,84 +0,0 @@ -package bug - -import ( - "fmt" - "strings" - - "github.com/pkg/errors" - - "github.com/MichaelMure/git-bug/repository" - "github.com/MichaelMure/git-bug/util/lamport" -) - -type gitTree struct { - opsEntry repository.TreeEntry - createTime lamport.Time - editTime lamport.Time -} - -func readTree(repo repository.RepoData, hash repository.Hash) (*gitTree, error) { - tree := &gitTree{} - - entries, err := repo.ReadTree(hash) - if err != nil { - return nil, errors.Wrap(err, "can't list git tree entries") - } - - opsFound := false - - for _, entry := range entries { - if entry.Name == opsEntryName { - tree.opsEntry = entry - opsFound = true - continue - } - if strings.HasPrefix(entry.Name, createClockEntryPrefix) { - n, err := fmt.Sscanf(entry.Name, createClockEntryPattern, &tree.createTime) - if err != nil { - return nil, errors.Wrap(err, "can't read create lamport time") - } - if n != 1 { - return nil, fmt.Errorf("could not parse create time lamport value") - } - } - if strings.HasPrefix(entry.Name, editClockEntryPrefix) { - n, err := fmt.Sscanf(entry.Name, editClockEntryPattern, &tree.editTime) - if err != nil { - return nil, errors.Wrap(err, "can't read edit lamport time") - } - if n != 1 { - return nil, fmt.Errorf("could not parse edit time lamport value") - } - } - } - - if !opsFound { - return nil, errors.New("invalid tree, missing the ops entry") - } - - return tree, nil -} - -func makeMediaTree(pack OperationPack) []repository.TreeEntry { - var tree []repository.TreeEntry - counter := 0 - added := make(map[repository.Hash]interface{}) - - for _, ops := range pack.Operations { - for _, file := range ops.GetFiles() { - if _, has := added[file]; !has { - tree = append(tree, repository.TreeEntry{ - ObjectType: repository.Blob, - Hash: file, - // The name is not important here, we only need to - // reference the blob. - Name: fmt.Sprintf("file%d", counter), - }) - counter++ - added[file] = struct{}{} - } - } - } - - return tree -} diff --git a/bug/identity.go b/bug/identity.go deleted file mode 100644 index 2eb2bcaf..00000000 --- a/bug/identity.go +++ /dev/null @@ -1,27 +0,0 @@ -package bug - -import ( - "github.com/MichaelMure/git-bug/identity" -) - -// EnsureIdentities walk the graph of operations and make sure that all Identity -// are properly loaded. That is, it replace all the IdentityStub with the full -// Identity, loaded through a Resolver. -func (bug *Bug) EnsureIdentities(resolver identity.Resolver) error { - it := NewOperationIterator(bug) - - for it.Next() { - op := it.Value() - base := op.base() - - if stub, ok := base.Author.(*identity.IdentityStub); ok { - i, err := resolver.ResolveIdentity(stub.Id()) - if err != nil { - return err - } - - base.Author = i - } - } - return nil -} diff --git a/bug/interface.go b/bug/interface.go index 5c8f2729..e71496a9 100644 --- a/bug/interface.go +++ b/bug/interface.go @@ -16,17 +16,15 @@ type Interface interface { // Append an operation into the staging area, to be committed later Append(op Operation) + // Operations return the ordered operations + Operations() []Operation + // Indicate that the in-memory state changed and need to be commit in the repository NeedCommit() bool // Commit write the staging area in Git and move the operations to the packs Commit(repo repository.ClockedRepo) error - // Merge a different version of the same bug by rebasing operations of this bug - // that are not present in the other on top of the chain of operations of the - // other version. - Merge(repo repository.Repo, other Interface) (bool, error) - // Lookup for the very first operation of the bug. // For a valid Bug, this operation should be a CreateOp FirstOp() Operation diff --git a/bug/op_add_comment.go b/bug/op_add_comment.go index e52c46fd..fd00860b 100644 --- a/bug/op_add_comment.go +++ b/bug/op_add_comment.go @@ -24,23 +24,19 @@ type AddCommentOperation struct { // Sign-post method for gqlgen func (op *AddCommentOperation) IsOperation() {} -func (op *AddCommentOperation) base() *OpBase { - return &op.OpBase -} - func (op *AddCommentOperation) Id() entity.Id { - return idOperation(op) + return idOperation(op, &op.OpBase) } func (op *AddCommentOperation) Apply(snapshot *Snapshot) { - snapshot.addActor(op.Author) - snapshot.addParticipant(op.Author) + snapshot.addActor(op.Author_) + snapshot.addParticipant(op.Author_) commentId := entity.CombineIds(snapshot.Id(), op.Id()) comment := Comment{ id: commentId, Message: op.Message, - Author: op.Author, + Author: op.Author_, Files: op.Files, UnixTime: timestamp.Timestamp(op.UnixTime), } @@ -59,7 +55,7 @@ func (op *AddCommentOperation) GetFiles() []repository.Hash { } func (op *AddCommentOperation) Validate() error { - if err := opBaseValidate(op, AddCommentOp); err != nil { + if err := op.OpBase.Validate(op, AddCommentOp); err != nil { return err } diff --git a/bug/op_add_comment_test.go b/bug/op_add_comment_test.go index 3b41d62d..fb6fa8ed 100644 --- a/bug/op_add_comment_test.go +++ b/bug/op_add_comment_test.go @@ -32,8 +32,8 @@ func TestAddCommentSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - assert.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + assert.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene assert.Equal(t, before, &after) } diff --git a/bug/op_create.go b/bug/op_create.go index 1e944d13..2423e571 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -30,12 +30,8 @@ type CreateOperation struct { // Sign-post method for gqlgen func (op *CreateOperation) IsOperation() {} -func (op *CreateOperation) base() *OpBase { - return &op.OpBase -} - func (op *CreateOperation) Id() entity.Id { - return idOperation(op) + return idOperation(op, &op.OpBase) } // OVERRIDE @@ -61,8 +57,8 @@ func (op *CreateOperation) Apply(snapshot *Snapshot) { snapshot.id = op.Id() - snapshot.addActor(op.Author) - snapshot.addParticipant(op.Author) + snapshot.addActor(op.Author_) + snapshot.addParticipant(op.Author_) snapshot.Title = op.Title @@ -70,12 +66,12 @@ func (op *CreateOperation) Apply(snapshot *Snapshot) { comment := Comment{ id: commentId, Message: op.Message, - Author: op.Author, + Author: op.Author_, UnixTime: timestamp.Timestamp(op.UnixTime), } snapshot.Comments = []Comment{comment} - snapshot.Author = op.Author + snapshot.Author = op.Author_ snapshot.CreateTime = op.Time() snapshot.Timeline = []TimelineItem{ @@ -90,7 +86,7 @@ func (op *CreateOperation) GetFiles() []repository.Hash { } func (op *CreateOperation) Validate() error { - if err := opBaseValidate(op, CreateOp); err != nil { + if err := op.OpBase.Validate(op, CreateOp); err != nil { return err } diff --git a/bug/op_create_test.go b/bug/op_create_test.go index 456357c4..1b359dee 100644 --- a/bug/op_create_test.go +++ b/bug/op_create_test.go @@ -79,8 +79,8 @@ func TestCreateSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - require.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + require.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene require.Equal(t, before, &after) } diff --git a/bug/op_edit_comment.go b/bug/op_edit_comment.go index 5bfc36bf..f9e30e62 100644 --- a/bug/op_edit_comment.go +++ b/bug/op_edit_comment.go @@ -27,19 +27,15 @@ type EditCommentOperation struct { // Sign-post method for gqlgen func (op *EditCommentOperation) IsOperation() {} -func (op *EditCommentOperation) base() *OpBase { - return &op.OpBase -} - func (op *EditCommentOperation) Id() entity.Id { - return idOperation(op) + return idOperation(op, &op.OpBase) } func (op *EditCommentOperation) Apply(snapshot *Snapshot) { // Todo: currently any message can be edited, even by a different author // crypto signature are needed. - snapshot.addActor(op.Author) + snapshot.addActor(op.Author_) var target TimelineItem @@ -85,7 +81,7 @@ func (op *EditCommentOperation) GetFiles() []repository.Hash { } func (op *EditCommentOperation) Validate() error { - if err := opBaseValidate(op, EditCommentOp); err != nil { + if err := op.OpBase.Validate(op, EditCommentOp); err != nil { return err } diff --git a/bug/op_edit_comment_test.go b/bug/op_edit_comment_test.go index a7330932..777f5f87 100644 --- a/bug/op_edit_comment_test.go +++ b/bug/op_edit_comment_test.go @@ -97,8 +97,8 @@ func TestEditCommentSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - require.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + require.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene require.Equal(t, before, &after) } diff --git a/bug/op_label_change.go b/bug/op_label_change.go index fefe2402..5e51534d 100644 --- a/bug/op_label_change.go +++ b/bug/op_label_change.go @@ -24,17 +24,13 @@ type LabelChangeOperation struct { // Sign-post method for gqlgen func (op *LabelChangeOperation) IsOperation() {} -func (op *LabelChangeOperation) base() *OpBase { - return &op.OpBase -} - func (op *LabelChangeOperation) Id() entity.Id { - return idOperation(op) + return idOperation(op, &op.OpBase) } // Apply apply the operation func (op *LabelChangeOperation) Apply(snapshot *Snapshot) { - snapshot.addActor(op.Author) + snapshot.addActor(op.Author_) // Add in the set AddLoop: @@ -66,7 +62,7 @@ AddLoop: item := &LabelChangeTimelineItem{ id: op.Id(), - Author: op.Author, + Author: op.Author_, UnixTime: timestamp.Timestamp(op.UnixTime), Added: op.Added, Removed: op.Removed, @@ -76,7 +72,7 @@ AddLoop: } func (op *LabelChangeOperation) Validate() error { - if err := opBaseValidate(op, LabelChangeOp); err != nil { + if err := op.OpBase.Validate(op, LabelChangeOp); err != nil { return err } diff --git a/bug/op_label_change_test.go b/bug/op_label_change_test.go index 96716ffe..40dc4f0d 100644 --- a/bug/op_label_change_test.go +++ b/bug/op_label_change_test.go @@ -31,8 +31,8 @@ func TestLabelChangeSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - require.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + require.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene require.Equal(t, before, &after) } diff --git a/bug/op_noop.go b/bug/op_noop.go index 6364f918..d59666ae 100644 --- a/bug/op_noop.go +++ b/bug/op_noop.go @@ -19,12 +19,8 @@ type NoOpOperation struct { // Sign-post method for gqlgen func (op *NoOpOperation) IsOperation() {} -func (op *NoOpOperation) base() *OpBase { - return &op.OpBase -} - func (op *NoOpOperation) Id() entity.Id { - return idOperation(op) + return idOperation(op, &op.OpBase) } func (op *NoOpOperation) Apply(snapshot *Snapshot) { @@ -32,7 +28,7 @@ func (op *NoOpOperation) Apply(snapshot *Snapshot) { } func (op *NoOpOperation) Validate() error { - return opBaseValidate(op, NoOpOp) + return op.OpBase.Validate(op, NoOpOp) } // UnmarshalJSON is a two step JSON unmarshaling diff --git a/bug/op_noop_test.go b/bug/op_noop_test.go index ce2f98af..0e3727c2 100644 --- a/bug/op_noop_test.go +++ b/bug/op_noop_test.go @@ -33,8 +33,8 @@ func TestNoopSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - assert.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + assert.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene assert.Equal(t, before, &after) } diff --git a/bug/op_set_metadata.go b/bug/op_set_metadata.go index 23d11461..c4988b84 100644 --- a/bug/op_set_metadata.go +++ b/bug/op_set_metadata.go @@ -20,38 +20,25 @@ type SetMetadataOperation struct { // Sign-post method for gqlgen func (op *SetMetadataOperation) IsOperation() {} -func (op *SetMetadataOperation) base() *OpBase { - return &op.OpBase -} - func (op *SetMetadataOperation) Id() entity.Id { - return idOperation(op) + return idOperation(op, &op.OpBase) } func (op *SetMetadataOperation) Apply(snapshot *Snapshot) { for _, target := range snapshot.Operations { if target.Id() == op.Target { - base := target.base() - - if base.extraMetadata == nil { - base.extraMetadata = make(map[string]string) - } - // Apply the metadata in an immutable way: if a metadata already // exist, it's not possible to override it. - for key, val := range op.NewMetadata { - if _, exist := base.extraMetadata[key]; !exist { - base.extraMetadata[key] = val - } + for key, value := range op.NewMetadata { + target.setExtraMetadataImmutable(key, value) } - return } } } func (op *SetMetadataOperation) Validate() error { - if err := opBaseValidate(op, SetMetadataOp); err != nil { + if err := op.OpBase.Validate(op, SetMetadataOp); err != nil { return err } diff --git a/bug/op_set_metadata_test.go b/bug/op_set_metadata_test.go index c0c91617..78f7d883 100644 --- a/bug/op_set_metadata_test.go +++ b/bug/op_set_metadata_test.go @@ -120,8 +120,8 @@ func TestSetMetadataSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - require.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + require.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene require.Equal(t, before, &after) } diff --git a/bug/op_set_status.go b/bug/op_set_status.go index eb2c0ba4..fcf33f8c 100644 --- a/bug/op_set_status.go +++ b/bug/op_set_status.go @@ -21,21 +21,17 @@ type SetStatusOperation struct { // Sign-post method for gqlgen func (op *SetStatusOperation) IsOperation() {} -func (op *SetStatusOperation) base() *OpBase { - return &op.OpBase -} - func (op *SetStatusOperation) Id() entity.Id { - return idOperation(op) + return idOperation(op, &op.OpBase) } func (op *SetStatusOperation) Apply(snapshot *Snapshot) { snapshot.Status = op.Status - snapshot.addActor(op.Author) + snapshot.addActor(op.Author_) item := &SetStatusTimelineItem{ id: op.Id(), - Author: op.Author, + Author: op.Author_, UnixTime: timestamp.Timestamp(op.UnixTime), Status: op.Status, } @@ -44,7 +40,7 @@ func (op *SetStatusOperation) Apply(snapshot *Snapshot) { } func (op *SetStatusOperation) Validate() error { - if err := opBaseValidate(op, SetStatusOp); err != nil { + if err := op.OpBase.Validate(op, SetStatusOp); err != nil { return err } diff --git a/bug/op_set_status_test.go b/bug/op_set_status_test.go index 3b26282f..83ff22ae 100644 --- a/bug/op_set_status_test.go +++ b/bug/op_set_status_test.go @@ -31,8 +31,8 @@ func TestSetStatusSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - require.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + require.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene require.Equal(t, before, &after) } diff --git a/bug/op_set_title.go b/bug/op_set_title.go index ddd98f0e..7fd7c20d 100644 --- a/bug/op_set_title.go +++ b/bug/op_set_title.go @@ -24,21 +24,17 @@ type SetTitleOperation struct { // Sign-post method for gqlgen func (op *SetTitleOperation) IsOperation() {} -func (op *SetTitleOperation) base() *OpBase { - return &op.OpBase -} - func (op *SetTitleOperation) Id() entity.Id { - return idOperation(op) + return idOperation(op, &op.OpBase) } func (op *SetTitleOperation) Apply(snapshot *Snapshot) { snapshot.Title = op.Title - snapshot.addActor(op.Author) + snapshot.addActor(op.Author_) item := &SetTitleTimelineItem{ id: op.Id(), - Author: op.Author, + Author: op.Author_, UnixTime: timestamp.Timestamp(op.UnixTime), Title: op.Title, Was: op.Was, @@ -48,7 +44,7 @@ func (op *SetTitleOperation) Apply(snapshot *Snapshot) { } func (op *SetTitleOperation) Validate() error { - if err := opBaseValidate(op, SetTitleOp); err != nil { + if err := op.OpBase.Validate(op, SetTitleOp); err != nil { return err } @@ -132,19 +128,17 @@ func (s *SetTitleTimelineItem) IsAuthored() {} // Convenience function to apply the operation func SetTitle(b Interface, author identity.Interface, unixTime int64, title string) (*SetTitleOperation, error) { - it := NewOperationIterator(b) - - var lastTitleOp Operation - for it.Next() { - op := it.Value() - if op.base().OperationType == SetTitleOp { + var lastTitleOp *SetTitleOperation + for _, op := range b.Operations() { + switch op := op.(type) { + case *SetTitleOperation: lastTitleOp = op } } var was string if lastTitleOp != nil { - was = lastTitleOp.(*SetTitleOperation).Title + was = lastTitleOp.Title } else { was = b.FirstOp().(*CreateOperation).Title } diff --git a/bug/op_set_title_test.go b/bug/op_set_title_test.go index 6ae325be..7059c4c7 100644 --- a/bug/op_set_title_test.go +++ b/bug/op_set_title_test.go @@ -31,8 +31,8 @@ func TestSetTitleSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - require.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + require.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene require.Equal(t, before, &after) } diff --git a/bug/operation.go b/bug/operation.go index bdaa2016..71a5c15d 100644 --- a/bug/operation.go +++ b/bug/operation.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/entity/dag" "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" ) @@ -29,34 +30,31 @@ const ( // Operation define the interface to fulfill for an edit operation of a Bug type Operation interface { - // base return the OpBase of the Operation, for package internal use - base() *OpBase - // Id return the identifier of the operation, to be used for back references - Id() entity.Id + dag.Operation + + // Type return the type of the operation + Type() OperationType + // Time return the time when the operation was added Time() time.Time // GetFiles return the files needed by this operation GetFiles() []repository.Hash // Apply the operation to a Snapshot to create the final state Apply(snapshot *Snapshot) - // Validate check if the operation is valid (ex: a title is a single line) - Validate() error // SetMetadata store arbitrary metadata about the operation SetMetadata(key string, value string) // GetMetadata retrieve arbitrary metadata about the operation GetMetadata(key string) (string, bool) // AllMetadata return all metadata for this operation AllMetadata() map[string]string - // GetAuthor return the author identity - GetAuthor() identity.Interface + + setExtraMetadataImmutable(key string, value string) // sign-post method for gqlgen IsOperation() } -func idOperation(op Operation) entity.Id { - base := op.base() - +func idOperation(op Operation, base *OpBase) entity.Id { if base.id == "" { // something went really wrong panic("op's id not set") @@ -77,10 +75,69 @@ func idOperation(op Operation) entity.Id { return base.id } +func operationUnmarshaller(author identity.Interface, raw json.RawMessage) (dag.Operation, error) { + var t struct { + OperationType OperationType `json:"type"` + } + + if err := json.Unmarshal(raw, &t); err != nil { + return nil, err + } + + var op Operation + + switch t.OperationType { + case AddCommentOp: + op = &AddCommentOperation{} + case CreateOp: + op = &CreateOperation{} + case EditCommentOp: + op = &EditCommentOperation{} + case LabelChangeOp: + op = &LabelChangeOperation{} + case NoOpOp: + op = &NoOpOperation{} + case SetMetadataOp: + op = &SetMetadataOperation{} + case SetStatusOp: + op = &SetStatusOperation{} + case SetTitleOp: + op = &SetTitleOperation{} + default: + panic(fmt.Sprintf("unknown operation type %v", t.OperationType)) + } + + err := json.Unmarshal(raw, &op) + if err != nil { + return nil, err + } + + switch op := op.(type) { + case *AddCommentOperation: + op.Author_ = author + case *CreateOperation: + op.Author_ = author + case *LabelChangeOperation: + op.Author_ = author + case *NoOpOperation: + op.Author_ = author + case *SetMetadataOperation: + op.Author_ = author + case *SetStatusOperation: + op.Author_ = author + case *SetTitleOperation: + op.Author_ = author + default: + panic(fmt.Sprintf("unknown operation type %T", op)) + } + + return op, nil +} + // OpBase implement the common code for all operations type OpBase struct { OperationType OperationType `json:"type"` - Author identity.Interface `json:"author"` + Author_ identity.Interface `json:"author"` // TODO: part of the data model upgrade, this should eventually be a timestamp + lamport UnixTime int64 `json:"timestamp"` Metadata map[string]string `json:"metadata,omitempty"` @@ -95,15 +152,15 @@ type OpBase struct { func newOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase { return OpBase{ OperationType: opType, - Author: author, + Author_: author, UnixTime: unixTime, id: entity.UnsetId, } } -func (op *OpBase) UnmarshalJSON(data []byte) error { +func (base *OpBase) UnmarshalJSON(data []byte) error { // Compute the Id when loading the op from disk. - op.id = entity.DeriveId(data) + base.id = entity.DeriveId(data) aux := struct { OperationType OperationType `json:"type"` @@ -122,39 +179,43 @@ func (op *OpBase) UnmarshalJSON(data []byte) error { return err } - op.OperationType = aux.OperationType - op.Author = author - op.UnixTime = aux.UnixTime - op.Metadata = aux.Metadata + base.OperationType = aux.OperationType + base.Author_ = author + base.UnixTime = aux.UnixTime + base.Metadata = aux.Metadata return nil } +func (base *OpBase) Type() OperationType { + return base.OperationType +} + // Time return the time when the operation was added -func (op *OpBase) Time() time.Time { - return time.Unix(op.UnixTime, 0) +func (base *OpBase) Time() time.Time { + return time.Unix(base.UnixTime, 0) } // GetFiles return the files needed by this operation -func (op *OpBase) GetFiles() []repository.Hash { +func (base *OpBase) GetFiles() []repository.Hash { return nil } // Validate check the OpBase for errors -func opBaseValidate(op Operation, opType OperationType) error { - if op.base().OperationType != opType { - return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, op.base().OperationType) +func (base *OpBase) Validate(op Operation, opType OperationType) error { + if base.OperationType != opType { + return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, base.OperationType) } if op.Time().Unix() == 0 { return fmt.Errorf("time not set") } - if op.base().Author == nil { + if base.Author_ == nil { return fmt.Errorf("author not set") } - if err := op.base().Author.Validate(); err != nil { + if err := op.Author().Validate(); err != nil { return errors.Wrap(err, "author") } @@ -168,46 +229,55 @@ func opBaseValidate(op Operation, opType OperationType) error { } // SetMetadata store arbitrary metadata about the operation -func (op *OpBase) SetMetadata(key string, value string) { - if op.Metadata == nil { - op.Metadata = make(map[string]string) +func (base *OpBase) SetMetadata(key string, value string) { + if base.Metadata == nil { + base.Metadata = make(map[string]string) } - op.Metadata[key] = value - op.id = entity.UnsetId + base.Metadata[key] = value + base.id = entity.UnsetId } // GetMetadata retrieve arbitrary metadata about the operation -func (op *OpBase) GetMetadata(key string) (string, bool) { - val, ok := op.Metadata[key] +func (base *OpBase) GetMetadata(key string) (string, bool) { + val, ok := base.Metadata[key] if ok { return val, true } // extraMetadata can't replace the original operations value if any - val, ok = op.extraMetadata[key] + val, ok = base.extraMetadata[key] return val, ok } // AllMetadata return all metadata for this operation -func (op *OpBase) AllMetadata() map[string]string { +func (base *OpBase) AllMetadata() map[string]string { result := make(map[string]string) - for key, val := range op.extraMetadata { + for key, val := range base.extraMetadata { result[key] = val } // Original metadata take precedence - for key, val := range op.Metadata { + for key, val := range base.Metadata { result[key] = val } return result } -// GetAuthor return author identity -func (op *OpBase) GetAuthor() identity.Interface { - return op.Author +func (base *OpBase) setExtraMetadataImmutable(key string, value string) { + if base.extraMetadata == nil { + base.extraMetadata = make(map[string]string) + } + if _, exist := base.extraMetadata[key]; !exist { + base.extraMetadata[key] = value + } +} + +// Author return author identity +func (base *OpBase) Author() identity.Interface { + return base.Author_ } diff --git a/bug/operation_iterator.go b/bug/operation_iterator.go deleted file mode 100644 index f42b1776..00000000 --- a/bug/operation_iterator.go +++ /dev/null @@ -1,72 +0,0 @@ -package bug - -type OperationIterator struct { - bug *Bug - packIndex int - opIndex int -} - -func NewOperationIterator(bug Interface) *OperationIterator { - return &OperationIterator{ - bug: bugFromInterface(bug), - packIndex: 0, - opIndex: -1, - } -} - -func (it *OperationIterator) Next() bool { - // Special case of the staging area - if it.packIndex == len(it.bug.packs) { - pack := it.bug.staging - it.opIndex++ - return it.opIndex < len(pack.Operations) - } - - if it.packIndex >= len(it.bug.packs) { - return false - } - - pack := it.bug.packs[it.packIndex] - - it.opIndex++ - - if it.opIndex < len(pack.Operations) { - return true - } - - // Note: this iterator doesn't handle the empty pack case - it.opIndex = 0 - it.packIndex++ - - // Special case of the non-empty staging area - if it.packIndex == len(it.bug.packs) && len(it.bug.staging.Operations) > 0 { - return true - } - - return it.packIndex < len(it.bug.packs) -} - -func (it *OperationIterator) Value() Operation { - // Special case of the staging area - if it.packIndex == len(it.bug.packs) { - pack := it.bug.staging - - if it.opIndex >= len(pack.Operations) { - panic("Iterator is not valid anymore") - } - - return pack.Operations[it.opIndex] - } - - if it.packIndex >= len(it.bug.packs) { - panic("Iterator is not valid anymore") - } - - pack := it.bug.packs[it.packIndex] - - if it.opIndex >= len(pack.Operations) { - panic("Iterator is not valid anymore") - } - - return pack.Operations[it.opIndex] -} diff --git a/bug/operation_iterator_test.go b/bug/operation_iterator_test.go deleted file mode 100644 index 81d87a5f..00000000 --- a/bug/operation_iterator_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package bug - -import ( - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/MichaelMure/git-bug/identity" - "github.com/MichaelMure/git-bug/repository" -) - -func ExampleOperationIterator() { - b := NewBug() - - // add operations - - it := NewOperationIterator(b) - - for it.Next() { - // do something with each operations - _ = it.Value() - } -} - -func TestOpIterator(t *testing.T) { - repo := repository.NewMockRepo() - - rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = rene.Commit(repo) - require.NoError(t, err) - - unix := time.Now().Unix() - - createOp := NewCreateOp(rene, unix, "title", "message", nil) - addCommentOp := NewAddCommentOp(rene, unix, "message2", nil) - setStatusOp := NewSetStatusOp(rene, unix, ClosedStatus) - labelChangeOp := NewLabelChangeOperation(rene, unix, []Label{"added"}, []Label{"removed"}) - - var i int - genTitleOp := func() Operation { - i++ - return NewSetTitleOp(rene, unix, fmt.Sprintf("title%d", i), "") - } - - bug1 := NewBug() - - // first pack - bug1.Append(createOp) - bug1.Append(addCommentOp) - bug1.Append(setStatusOp) - bug1.Append(labelChangeOp) - err = bug1.Commit(repo) - require.NoError(t, err) - - // second pack - bug1.Append(genTitleOp()) - bug1.Append(genTitleOp()) - bug1.Append(genTitleOp()) - err = bug1.Commit(repo) - require.NoError(t, err) - - // staging - bug1.Append(genTitleOp()) - bug1.Append(genTitleOp()) - bug1.Append(genTitleOp()) - - it := NewOperationIterator(bug1) - - counter := 0 - for it.Next() { - _ = it.Value() - counter++ - } - - require.Equal(t, 10, counter) -} diff --git a/bug/operation_pack.go b/bug/operation_pack.go deleted file mode 100644 index 74d15f50..00000000 --- a/bug/operation_pack.go +++ /dev/null @@ -1,187 +0,0 @@ -package bug - -import ( - "encoding/json" - "fmt" - - "github.com/pkg/errors" - - "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/repository" -) - -// 1: original format -// 2: no more legacy identities -// 3: Ids are generated from the create operation serialized data instead of from the first git commit -const formatVersion = 3 - -// OperationPack represent an ordered set of operation to apply -// to a Bug. These operations are stored in a single Git commit. -// -// These commits will be linked together in a linear chain of commits -// inside Git to form the complete ordered chain of operation to -// apply to get the final state of the Bug -type OperationPack struct { - Operations []Operation - - // Private field so not serialized - commitHash repository.Hash -} - -func (opp *OperationPack) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { - Version uint `json:"version"` - Operations []Operation `json:"ops"` - }{ - Version: formatVersion, - Operations: opp.Operations, - }) -} - -func (opp *OperationPack) UnmarshalJSON(data []byte) error { - aux := struct { - Version uint `json:"version"` - Operations []json.RawMessage `json:"ops"` - }{} - - if err := json.Unmarshal(data, &aux); err != nil { - return err - } - - if aux.Version < formatVersion { - return entity.NewErrOldFormatVersion(aux.Version) - } - if aux.Version > formatVersion { - return entity.NewErrNewFormatVersion(aux.Version) - } - - for _, raw := range aux.Operations { - var t struct { - OperationType OperationType `json:"type"` - } - - if err := json.Unmarshal(raw, &t); err != nil { - return err - } - - // delegate to specialized unmarshal function - op, err := opp.unmarshalOp(raw, t.OperationType) - if err != nil { - return err - } - - opp.Operations = append(opp.Operations, op) - } - - return nil -} - -func (opp *OperationPack) unmarshalOp(raw []byte, _type OperationType) (Operation, error) { - switch _type { - case AddCommentOp: - op := &AddCommentOperation{} - err := json.Unmarshal(raw, &op) - return op, err - case CreateOp: - op := &CreateOperation{} - err := json.Unmarshal(raw, &op) - return op, err - case EditCommentOp: - op := &EditCommentOperation{} - err := json.Unmarshal(raw, &op) - return op, err - case LabelChangeOp: - op := &LabelChangeOperation{} - err := json.Unmarshal(raw, &op) - return op, err - case NoOpOp: - op := &NoOpOperation{} - err := json.Unmarshal(raw, &op) - return op, err - case SetMetadataOp: - op := &SetMetadataOperation{} - err := json.Unmarshal(raw, &op) - return op, err - case SetStatusOp: - op := &SetStatusOperation{} - err := json.Unmarshal(raw, &op) - return op, err - case SetTitleOp: - op := &SetTitleOperation{} - err := json.Unmarshal(raw, &op) - return op, err - default: - return nil, fmt.Errorf("unknown operation type %v", _type) - } -} - -// Append a new operation to the pack -func (opp *OperationPack) Append(op Operation) { - opp.Operations = append(opp.Operations, op) -} - -// IsEmpty tell if the OperationPack is empty -func (opp *OperationPack) IsEmpty() bool { - return len(opp.Operations) == 0 -} - -// IsValid tell if the OperationPack is considered valid -func (opp *OperationPack) Validate() error { - if opp.IsEmpty() { - return fmt.Errorf("empty") - } - - for _, op := range opp.Operations { - if err := op.Validate(); err != nil { - return errors.Wrap(err, "op") - } - } - - return nil -} - -// Write will serialize and store the OperationPack as a git blob and return -// its hash -func (opp *OperationPack) Write(repo repository.ClockedRepo) (repository.Hash, error) { - // make sure we don't write invalid data - err := opp.Validate() - if err != nil { - return "", errors.Wrap(err, "validation error") - } - - // First, make sure that all the identities are properly Commit as well - // TODO: this might be downgraded to "make sure it exist in git" but then, what make - // sure no data is lost on identities ? - for _, op := range opp.Operations { - if op.base().Author.NeedCommit() { - return "", fmt.Errorf("identity need commmit") - } - } - - data, err := json.Marshal(opp) - if err != nil { - return "", err - } - - hash, err := repo.StoreData(data) - if err != nil { - return "", err - } - - return hash, nil -} - -// Make a deep copy -func (opp *OperationPack) Clone() OperationPack { - - clone := OperationPack{ - Operations: make([]Operation, len(opp.Operations)), - commitHash: opp.commitHash, - } - - for i, op := range opp.Operations { - clone.Operations[i] = op - } - - return clone -} diff --git a/bug/operation_pack_test.go b/bug/operation_pack_test.go deleted file mode 100644 index 02d72f0f..00000000 --- a/bug/operation_pack_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package bug - -import ( - "encoding/json" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/MichaelMure/git-bug/identity" - "github.com/MichaelMure/git-bug/repository" -) - -func TestOperationPackSerialize(t *testing.T) { - opp := &OperationPack{} - - repo := repository.NewMockRepo() - - rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - - createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil) - setTitleOp := NewSetTitleOp(rene, time.Now().Unix(), "title2", "title1") - addCommentOp := NewAddCommentOp(rene, time.Now().Unix(), "message2", nil) - setStatusOp := NewSetStatusOp(rene, time.Now().Unix(), ClosedStatus) - labelChangeOp := NewLabelChangeOperation(rene, time.Now().Unix(), []Label{"added"}, []Label{"removed"}) - - opp.Append(createOp) - opp.Append(setTitleOp) - opp.Append(addCommentOp) - opp.Append(setStatusOp) - opp.Append(labelChangeOp) - - opMeta := NewSetTitleOp(rene, time.Now().Unix(), "title3", "title2") - opMeta.SetMetadata("key", "value") - opp.Append(opMeta) - - require.Equal(t, 1, len(opMeta.Metadata)) - - opFile := NewAddCommentOp(rene, time.Now().Unix(), "message", []repository.Hash{ - "abcdef", - "ghijkl", - }) - opp.Append(opFile) - - require.Equal(t, 2, len(opFile.Files)) - - data, err := json.Marshal(opp) - require.NoError(t, err) - - var opp2 *OperationPack - err = json.Unmarshal(data, &opp2) - require.NoError(t, err) - - ensureIds(opp) - ensureAuthors(t, opp, opp2) - - require.Equal(t, opp, opp2) -} - -func ensureIds(opp *OperationPack) { - for _, op := range opp.Operations { - op.Id() - } -} - -func ensureAuthors(t *testing.T, opp1 *OperationPack, opp2 *OperationPack) { - require.Equal(t, len(opp1.Operations), len(opp2.Operations)) - for i := 0; i < len(opp1.Operations); i++ { - op1 := opp1.Operations[i] - op2 := opp2.Operations[i] - - // ensure we have equivalent authors (IdentityStub vs Identity) then - // enforce equality - require.Equal(t, op1.base().Author.Id(), op2.base().Author.Id()) - op1.base().Author = op2.base().Author - } -} diff --git a/bug/operation_test.go b/bug/operation_test.go index f66938ad..619f2b43 100644 --- a/bug/operation_test.go +++ b/bug/operation_test.go @@ -45,7 +45,7 @@ func TestValidate(t *testing.T) { NewSetStatusOp(makeIdentity(t, "René \nDescartes", "rene@descartes.fr"), unix, ClosedStatus), NewSetStatusOp(makeIdentity(t, "René Descartes", "rene@\ndescartes.fr"), unix, ClosedStatus), &CreateOperation{OpBase: OpBase{ - Author: rene, + Author_: rene, UnixTime: 0, OperationType: CreateOp, }, @@ -121,7 +121,7 @@ func TestID(t *testing.T) { require.NoError(t, id2.Validate()) require.Equal(t, id1, id2) - b2, err := ReadLocal(repo, b.Id()) + b2, err := Read(repo, b.Id()) require.NoError(t, err) op3 := b2.FirstOp() diff --git a/bug/sorting.go b/bug/sorting.go index d1c370d3..2e64b92d 100644 --- a/bug/sorting.go +++ b/bug/sorting.go @@ -7,11 +7,11 @@ func (b BugsByCreationTime) Len() int { } func (b BugsByCreationTime) Less(i, j int) bool { - if b[i].createTime < b[j].createTime { + if b[i].CreateLamportTime() < b[j].CreateLamportTime() { return true } - if b[i].createTime > b[j].createTime { + if b[i].CreateLamportTime() > b[j].CreateLamportTime() { return false } @@ -35,11 +35,11 @@ func (b BugsByEditTime) Len() int { } func (b BugsByEditTime) Less(i, j int) bool { - if b[i].editTime < b[j].editTime { + if b[i].EditLamportTime() < b[j].EditLamportTime() { return true } - if b[i].editTime > b[j].editTime { + if b[i].EditLamportTime() > b[j].EditLamportTime() { return false } diff --git a/bug/with_snapshot.go b/bug/with_snapshot.go index 41192d39..9b706d61 100644 --- a/bug/with_snapshot.go +++ b/bug/with_snapshot.go @@ -50,9 +50,3 @@ func (b *WithSnapshot) Commit(repo repository.ClockedRepo) error { b.snap.id = b.Bug.Id() return nil } - -// Merge intercept Bug.Merge() and clear the snapshot -func (b *WithSnapshot) Merge(repo repository.Repo, other Interface) (bool, error) { - b.snap = nil - return b.Bug.Merge(repo, other) -} diff --git a/cache/bug_cache.go b/cache/bug_cache.go index ca526f7b..bbe9830f 100644 --- a/cache/bug_cache.go +++ b/cache/bug_cache.go @@ -51,9 +51,7 @@ func (c *BugCache) ResolveOperationWithMetadata(key string, value string) (entit // preallocate but empty matching := make([]entity.Id, 0, 5) - it := bug.NewOperationIterator(c.bug) - for it.Next() { - op := it.Value() + for _, op := range c.bug.Operations() { opValue, ok := op.GetMetadata(key) if ok && value == opValue { matching = append(matching, op.Id()) diff --git a/cache/bug_excerpt.go b/cache/bug_excerpt.go index 6a9e7f75..152bdacf 100644 --- a/cache/bug_excerpt.go +++ b/cache/bug_excerpt.go @@ -87,7 +87,7 @@ func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt { } switch snap.Author.(type) { - case *identity.Identity, *IdentityCache: + case *identity.Identity, *identity.IdentityStub, *IdentityCache: e.AuthorId = snap.Author.Id() default: panic("unhandled identity type") diff --git a/cache/repo_cache.go b/cache/repo_cache.go index ab3e1bcb..58022bda 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -195,7 +195,7 @@ func (c *RepoCache) buildCache() error { c.bugExcerpts = make(map[entity.Id]*BugExcerpt) - allBugs := bug.ReadAllLocal(c.repo) + allBugs := bug.ReadAll(c.repo) // wipe the index just to be sure err := c.repo.ClearBleveIndex("bug") diff --git a/cache/repo_cache_bug.go b/cache/repo_cache_bug.go index 9f011c04..c05f30cf 100644 --- a/cache/repo_cache_bug.go +++ b/cache/repo_cache_bug.go @@ -151,7 +151,7 @@ func (c *RepoCache) ResolveBug(id entity.Id) (*BugCache, error) { } c.muBug.RUnlock() - b, err := bug.ReadLocalWithResolver(c.repo, newIdentityCacheResolver(c), id) + b, err := bug.ReadWithResolver(c.repo, newIdentityCacheResolver(c), id) if err != nil { return nil, err } diff --git a/cache/repo_cache_common.go b/cache/repo_cache_common.go index 5dc19d22..e23315f9 100644 --- a/cache/repo_cache_common.go +++ b/cache/repo_cache_common.go @@ -95,6 +95,12 @@ func (c *RepoCache) MergeAll(remote string) <-chan entity.MergeResult { go func() { defer close(out) + author, err := c.GetUserIdentity() + if err != nil { + out <- entity.NewMergeError(err, "") + return + } + results := identity.MergeAll(c.repo, remote) for result := range results { out <- result @@ -112,7 +118,7 @@ func (c *RepoCache) MergeAll(remote string) <-chan entity.MergeResult { } } - results = bug.MergeAll(c.repo, remote) + results = bug.MergeAll(c.repo, remote, author) for result := range results { out <- result @@ -130,11 +136,10 @@ func (c *RepoCache) MergeAll(remote string) <-chan entity.MergeResult { } } - err := c.write() - - // No easy way out here .. + err = c.write() if err != nil { - panic(err) + out <- entity.NewMergeError(err, "") + return } }() diff --git a/cache/repo_cache_test.go b/cache/repo_cache_test.go index 9cdd584d..a85fde66 100644 --- a/cache/repo_cache_test.go +++ b/cache/repo_cache_test.go @@ -123,6 +123,10 @@ func TestPushPull(t *testing.T) { require.NoError(t, err) err = cacheA.SetUserIdentity(reneA) require.NoError(t, err) + isaacB, err := cacheB.NewIdentity("Isaac Newton", "isaac@newton.uk") + require.NoError(t, err) + err = cacheB.SetUserIdentity(isaacB) + require.NoError(t, err) // distribute the identity _, err = cacheA.Push("origin") diff --git a/entity/TODO b/entity/TODO deleted file mode 100644 index 9f33dd09..00000000 --- a/entity/TODO +++ /dev/null @@ -1,9 +0,0 @@ -- is the pack Lamport clock really useful vs only topological sort? - - topological order is enforced on the clocks, so what's the point? - - is EditTime equivalent to PackTime? no, avoid the gaps. Is it better? - --> PackTime is contained within a bug and might avoid extreme reordering? -- how to do commit signature? -- how to avoid id collision between Operations? -- write tests for actions -- migrate Bug to the new structure -- migrate Identity to the new structure? \ No newline at end of file diff --git a/entity/merge.go b/entity/merge.go index 1b68b4de..0661b7fc 100644 --- a/entity/merge.go +++ b/entity/merge.go @@ -42,13 +42,15 @@ func (mr MergeResult) String() string { case MergeStatusNothing: return "nothing to do" case MergeStatusError: - return fmt.Sprintf("merge error on %s: %s", mr.Id, mr.Err.Error()) + if mr.Id != "" { + return fmt.Sprintf("merge error on %s: %s", mr.Id, mr.Err.Error()) + } + return fmt.Sprintf("merge error: %s", mr.Err.Error()) default: panic("unknown merge status") } } -// TODO: Interface --> *Entity ? func NewMergeNewStatus(id Id, entity Interface) MergeResult { return MergeResult{ Id: id, diff --git a/go.sum b/go.sum index 9d0a8c82..e316fb66 100644 --- a/go.sum +++ b/go.sum @@ -575,6 +575,7 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/misc/random_bugs/create_random_bugs.go b/misc/random_bugs/create_random_bugs.go index 3d93135e..a69918f4 100644 --- a/misc/random_bugs/create_random_bugs.go +++ b/misc/random_bugs/create_random_bugs.go @@ -111,52 +111,6 @@ func generateRandomBugsWithSeed(opts Options, seed int64) []*bug.Bug { return result } -func GenerateRandomOperationPacks(packNumber int, opNumber int) []*bug.OperationPack { - return GenerateRandomOperationPacksWithSeed(packNumber, opNumber, time.Now().UnixNano()) -} - -func GenerateRandomOperationPacksWithSeed(packNumber int, opNumber int, seed int64) []*bug.OperationPack { - // Note: this is a bit crude, only generate a Create + Comments - - panic("this piece of code needs to be updated to make sure that the identities " + - "are properly commit before usage. That is, generateRandomPersons() need to be called.") - - rand.Seed(seed) - fake.Seed(seed) - - result := make([]*bug.OperationPack, packNumber) - - for i := 0; i < packNumber; i++ { - opp := &bug.OperationPack{} - - var op bug.Operation - - op = bug.NewCreateOp( - randomPerson(), - time.Now().Unix(), - fake.Sentence(), - paragraphs(), - nil, - ) - - opp.Append(op) - - for j := 0; j < opNumber-1; j++ { - op = bug.NewAddCommentOp( - randomPerson(), - time.Now().Unix(), - paragraphs(), - nil, - ) - opp.Append(op) - } - - result[i] = opp - } - - return result -} - func person(repo repository.RepoClock) (*identity.Identity, error) { return identity.NewIdentity(repo, fake.FullName(), fake.EmailAddress()) } diff --git a/tests/read_bugs_test.go b/tests/read_bugs_test.go index 53b84fd5..b1983689 100644 --- a/tests/read_bugs_test.go +++ b/tests/read_bugs_test.go @@ -14,7 +14,7 @@ func TestReadBugs(t *testing.T) { random_bugs.FillRepoWithSeed(repo, 15, 42) - bugs := bug.ReadAllLocal(repo) + bugs := bug.ReadAll(repo) for b := range bugs { if b.Err != nil { t.Fatal(b.Err) @@ -30,7 +30,7 @@ func benchmarkReadBugs(bugNumber int, t *testing.B) { t.ResetTimer() for n := 0; n < t.N; n++ { - bugs := bug.ReadAllLocal(repo) + bugs := bug.ReadAll(repo) for b := range bugs { if b.Err != nil { t.Fatal(b.Err) -- cgit v1.2.3