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 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 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 +++++++++--------------------------------- 1 file changed, 9 insertions(+), 33 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, } -- cgit 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 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (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 -- cgit 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! --- bug/bug.go | 594 ++++++++----------------------------------------------------- 1 file changed, 75 insertions(+), 519 deletions(-) (limited to 'bug/bug.go') 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 } -- cgit