bug: Id from first operation data, not git + remove root link

This commit is contained in:
Michael Muré 2020-11-08 19:15:06 +01:00
parent 5ae8a13277
commit 7163b2283b
No known key found for this signature in database
GPG Key ID: A4457C029293126F
19 changed files with 348 additions and 314 deletions

View File

@ -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,22 +367,52 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error {
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
}
}
// Write the Ops as a Git blob containing the serialized array
hash, err := bug.staging.Write(repo)
if err != nil {
return err
}
if bug.rootPack == "" {
bug.rootPack = hash
}
// 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},
}
// 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
@ -444,48 +431,6 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error {
})
}
// 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
}
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,
Name: fmt.Sprintf(createClockEntryPattern, bug.createTime),
})
}
// 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

View File

@ -15,8 +15,9 @@ func TestPushPull(t *testing.T) {
repoA, repoB, remote := repository.SetupReposAndRemote()
defer repository.CleanupTestRepos(repoA, repoB, remote)
reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr")
err := reneA.Commit(repoA)
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")
@ -92,8 +93,9 @@ func _RebaseTheirs(t testing.TB) {
repoA, repoB, remote := repository.SetupReposAndRemote()
defer repository.CleanupTestRepos(repoA, repoB, remote)
reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr")
err := reneA.Commit(repoA)
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")
@ -172,8 +174,9 @@ func _RebaseOurs(t testing.TB) {
repoA, repoB, remote := repository.SetupReposAndRemote()
defer repository.CleanupTestRepos(repoA, repoB, remote)
reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr")
err := reneA.Commit(repoA)
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")
@ -263,8 +266,9 @@ func _RebaseConflict(t testing.TB) {
repoA, repoB, remote := repository.SetupReposAndRemote()
defer repository.CleanupTestRepos(repoA, repoB, remote)
reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr")
err := reneA.Commit(repoA)
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")

View File

@ -12,19 +12,20 @@ import (
)
func TestBugId(t *testing.T) {
mockRepo := repository.NewMockRepo()
repo := repository.NewMockRepo()
bug1 := NewBug()
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
err := rene.Commit(mockRepo)
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(mockRepo)
err = bug1.Commit(repo)
if err != nil {
t.Fatal(err)
@ -34,12 +35,13 @@ func TestBugId(t *testing.T) {
}
func TestBugValidity(t *testing.T) {
mockRepo := repository.NewMockRepo()
repo := repository.NewMockRepo()
bug1 := NewBug()
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
err := rene.Commit(mockRepo)
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)
@ -54,7 +56,7 @@ func TestBugValidity(t *testing.T) {
t.Fatal("Bug with just a CreateOp should be valid")
}
err = bug1.Commit(mockRepo)
err = bug1.Commit(repo)
if err != nil {
t.Fatal(err)
}
@ -65,7 +67,7 @@ func TestBugValidity(t *testing.T) {
t.Fatal("Bug with multiple CreateOp should be invalid")
}
err = bug1.Commit(mockRepo)
err = bug1.Commit(repo)
if err == nil {
t.Fatal("Invalid bug should not commit")
}
@ -76,8 +78,9 @@ func TestBugCommitLoad(t *testing.T) {
bug1 := NewBug()
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
err := rene.Commit(repo)
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)
@ -137,7 +140,8 @@ func TestBugRemove(t *testing.T) {
require.NoError(t, err)
// generate a bunch of bugs
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
err = rene.Commit(repo)
require.NoError(t, err)

84
bug/git_tree.go Normal file
View File

@ -0,0 +1,84 @@
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
}

View File

@ -14,8 +14,8 @@ import (
func TestAddCommentSerialize(t *testing.T) {
repo := repository.NewMockRepo()
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
err := rene.Commit(repo)
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
unix := time.Now().Unix()

View File

@ -1,6 +1,7 @@
package bug
import (
"crypto/rand"
"encoding/json"
"fmt"
"strings"
@ -17,6 +18,10 @@ var _ Operation = &CreateOperation{}
// CreateOperation define the initial creation of a bug
type CreateOperation struct {
OpBase
// mandatory random bytes to ensure a better randomness of the data of the first
// operation of a bug, used to later generate the ID
// len(Nonce) should be > 20 and < 64 bytes
Nonce []byte `json:"nonce"`
Title string `json:"title"`
Message string `json:"message"`
Files []repository.Hash `json:"files"`
@ -66,14 +71,19 @@ func (op *CreateOperation) Validate() error {
return err
}
if len(op.Nonce) > 64 {
return fmt.Errorf("create nonce is too big")
}
if len(op.Nonce) < 20 {
return fmt.Errorf("create nonce is too small")
}
if text.Empty(op.Title) {
return fmt.Errorf("title is empty")
}
if strings.Contains(op.Title, "\n") {
return fmt.Errorf("title should be a single line")
}
if !text.Safe(op.Title) {
return fmt.Errorf("title is not fully printable")
}
@ -98,6 +108,7 @@ func (op *CreateOperation) UnmarshalJSON(data []byte) error {
}
aux := struct {
Nonce []byte `json:"nonce"`
Title string `json:"title"`
Message string `json:"message"`
Files []repository.Hash `json:"files"`
@ -109,6 +120,7 @@ func (op *CreateOperation) UnmarshalJSON(data []byte) error {
}
op.OpBase = base
op.Nonce = aux.Nonce
op.Title = aux.Title
op.Message = aux.Message
op.Files = aux.Files
@ -119,9 +131,19 @@ func (op *CreateOperation) UnmarshalJSON(data []byte) error {
// Sign post method for gqlgen
func (op *CreateOperation) IsAuthored() {}
func makeNonce(len int) []byte {
result := make([]byte, len)
_, err := rand.Read(result)
if err != nil {
panic(err)
}
return result
}
func NewCreateOp(author identity.Interface, unixTime int64, title, message string, files []repository.Hash) *CreateOperation {
return &CreateOperation{
OpBase: newOpBase(CreateOp, author, unixTime),
Nonce: makeNonce(20),
Title: title,
Message: message,
Files: files,

View File

@ -5,17 +5,21 @@ import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util/timestamp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCreate(t *testing.T) {
snapshot := Snapshot{}
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
repo := repository.NewMockRepoClock()
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
unix := time.Now().Unix()
create := NewCreateOp(rene, unix, "title", "message", nil)
@ -23,7 +27,7 @@ func TestCreate(t *testing.T) {
create.Apply(&snapshot)
id := create.Id()
assert.NoError(t, id.Validate())
require.NoError(t, id.Validate())
comment := Comment{
id: id,
@ -48,31 +52,31 @@ func TestCreate(t *testing.T) {
},
}
assert.Equal(t, expected, snapshot)
require.Equal(t, expected, snapshot)
}
func TestCreateSerialize(t *testing.T) {
repo := repository.NewMockRepo()
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
err := rene.Commit(repo)
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
unix := time.Now().Unix()
before := NewCreateOp(rene, unix, "title", "message", nil)
data, err := json.Marshal(before)
assert.NoError(t, err)
require.NoError(t, err)
var after CreateOperation
err = json.Unmarshal(data, &after)
assert.NoError(t, err)
require.NoError(t, err)
// enforce creating the ID
before.Id()
// Replace the identity stub with the real thing
assert.Equal(t, rene.Id(), after.base().Author.Id())
require.Equal(t, rene.Id(), after.base().Author.Id())
after.Author = rene
assert.Equal(t, before, &after)
require.Equal(t, before, &after)
}

View File

@ -5,7 +5,6 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/identity"
@ -16,8 +15,8 @@ func TestEdit(t *testing.T) {
snapshot := Snapshot{}
repo := repository.NewMockRepo()
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
err := rene.Commit(repo)
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
unix := time.Now().Unix()
@ -47,59 +46,59 @@ func TestEdit(t *testing.T) {
edit := NewEditCommentOp(rene, unix, id1, "create edited", nil)
edit.Apply(&snapshot)
assert.Equal(t, len(snapshot.Timeline), 4)
assert.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2)
assert.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 1)
assert.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 1)
assert.Equal(t, snapshot.Comments[0].Message, "create edited")
assert.Equal(t, snapshot.Comments[1].Message, "comment 1")
assert.Equal(t, snapshot.Comments[2].Message, "comment 2")
require.Equal(t, len(snapshot.Timeline), 4)
require.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2)
require.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 1)
require.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 1)
require.Equal(t, snapshot.Comments[0].Message, "create edited")
require.Equal(t, snapshot.Comments[1].Message, "comment 1")
require.Equal(t, snapshot.Comments[2].Message, "comment 2")
edit2 := NewEditCommentOp(rene, unix, id2, "comment 1 edited", nil)
edit2.Apply(&snapshot)
assert.Equal(t, len(snapshot.Timeline), 4)
assert.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2)
assert.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 2)
assert.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 1)
assert.Equal(t, snapshot.Comments[0].Message, "create edited")
assert.Equal(t, snapshot.Comments[1].Message, "comment 1 edited")
assert.Equal(t, snapshot.Comments[2].Message, "comment 2")
require.Equal(t, len(snapshot.Timeline), 4)
require.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2)
require.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 2)
require.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 1)
require.Equal(t, snapshot.Comments[0].Message, "create edited")
require.Equal(t, snapshot.Comments[1].Message, "comment 1 edited")
require.Equal(t, snapshot.Comments[2].Message, "comment 2")
edit3 := NewEditCommentOp(rene, unix, id3, "comment 2 edited", nil)
edit3.Apply(&snapshot)
assert.Equal(t, len(snapshot.Timeline), 4)
assert.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2)
assert.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 2)
assert.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 2)
assert.Equal(t, snapshot.Comments[0].Message, "create edited")
assert.Equal(t, snapshot.Comments[1].Message, "comment 1 edited")
assert.Equal(t, snapshot.Comments[2].Message, "comment 2 edited")
require.Equal(t, len(snapshot.Timeline), 4)
require.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2)
require.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 2)
require.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 2)
require.Equal(t, snapshot.Comments[0].Message, "create edited")
require.Equal(t, snapshot.Comments[1].Message, "comment 1 edited")
require.Equal(t, snapshot.Comments[2].Message, "comment 2 edited")
}
func TestEditCommentSerialize(t *testing.T) {
repo := repository.NewMockRepo()
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
err := rene.Commit(repo)
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
unix := time.Now().Unix()
before := NewEditCommentOp(rene, unix, "target", "message", nil)
data, err := json.Marshal(before)
assert.NoError(t, err)
require.NoError(t, err)
var after EditCommentOperation
err = json.Unmarshal(data, &after)
assert.NoError(t, err)
require.NoError(t, err)
// enforce creating the ID
before.Id()
// Replace the identity stub with the real thing
assert.Equal(t, rene.Id(), after.base().Author.Id())
require.Equal(t, rene.Id(), after.base().Author.Id())
after.Author = rene
assert.Equal(t, before, &after)
require.Equal(t, before, &after)
}

View File

@ -9,32 +9,30 @@ import (
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
"github.com/stretchr/testify/assert"
)
func TestLabelChangeSerialize(t *testing.T) {
repo := repository.NewMockRepo()
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
err := rene.Commit(repo)
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
unix := time.Now().Unix()
before := NewLabelChangeOperation(rene, unix, []Label{"added"}, []Label{"removed"})
data, err := json.Marshal(before)
assert.NoError(t, err)
require.NoError(t, err)
var after LabelChangeOperation
err = json.Unmarshal(data, &after)
assert.NoError(t, err)
require.NoError(t, err)
// enforce creating the ID
before.Id()
// Replace the identity stub with the real thing
assert.Equal(t, rene.Id(), after.base().Author.Id())
require.Equal(t, rene.Id(), after.base().Author.Id())
after.Author = rene
assert.Equal(t, before, &after)
require.Equal(t, before, &after)
}

View File

@ -15,8 +15,8 @@ import (
func TestNoopSerialize(t *testing.T) {
repo := repository.NewMockRepo()
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
err := rene.Commit(repo)
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
unix := time.Now().Unix()

View File

@ -8,7 +8,6 @@ import (
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -16,8 +15,8 @@ func TestSetMetadata(t *testing.T) {
snapshot := Snapshot{}
repo := repository.NewMockRepo()
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
err := rene.Commit(repo)
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
unix := time.Now().Unix()
@ -47,15 +46,15 @@ func TestSetMetadata(t *testing.T) {
snapshot.Operations = append(snapshot.Operations, op1)
createMetadata := snapshot.Operations[0].AllMetadata()
assert.Equal(t, len(createMetadata), 2)
require.Equal(t, len(createMetadata), 2)
// original key is not overrided
assert.Equal(t, createMetadata["key"], "value")
require.Equal(t, createMetadata["key"], "value")
// new key is set
assert.Equal(t, createMetadata["key2"], "value")
require.Equal(t, createMetadata["key2"], "value")
commentMetadata := snapshot.Operations[1].AllMetadata()
assert.Equal(t, len(commentMetadata), 1)
assert.Equal(t, commentMetadata["key2"], "value2")
require.Equal(t, len(commentMetadata), 1)
require.Equal(t, commentMetadata["key2"], "value2")
op2 := NewSetMetadataOp(rene, unix, id2, map[string]string{
"key2": "value",
@ -66,16 +65,16 @@ func TestSetMetadata(t *testing.T) {
snapshot.Operations = append(snapshot.Operations, op2)
createMetadata = snapshot.Operations[0].AllMetadata()
assert.Equal(t, len(createMetadata), 2)
assert.Equal(t, createMetadata["key"], "value")
assert.Equal(t, createMetadata["key2"], "value")
require.Equal(t, len(createMetadata), 2)
require.Equal(t, createMetadata["key"], "value")
require.Equal(t, createMetadata["key2"], "value")
commentMetadata = snapshot.Operations[1].AllMetadata()
assert.Equal(t, len(commentMetadata), 2)
require.Equal(t, len(commentMetadata), 2)
// original key is not overrided
assert.Equal(t, commentMetadata["key2"], "value2")
require.Equal(t, commentMetadata["key2"], "value2")
// new key is set
assert.Equal(t, commentMetadata["key3"], "value3")
require.Equal(t, commentMetadata["key3"], "value3")
op3 := NewSetMetadataOp(rene, unix, id1, map[string]string{
"key": "override",
@ -86,22 +85,22 @@ func TestSetMetadata(t *testing.T) {
snapshot.Operations = append(snapshot.Operations, op3)
createMetadata = snapshot.Operations[0].AllMetadata()
assert.Equal(t, len(createMetadata), 2)
require.Equal(t, len(createMetadata), 2)
// original key is not overrided
assert.Equal(t, createMetadata["key"], "value")
require.Equal(t, createMetadata["key"], "value")
// previously set key is not overrided
assert.Equal(t, createMetadata["key2"], "value")
require.Equal(t, createMetadata["key2"], "value")
commentMetadata = snapshot.Operations[1].AllMetadata()
assert.Equal(t, len(commentMetadata), 2)
assert.Equal(t, commentMetadata["key2"], "value2")
assert.Equal(t, commentMetadata["key3"], "value3")
require.Equal(t, len(commentMetadata), 2)
require.Equal(t, commentMetadata["key2"], "value2")
require.Equal(t, commentMetadata["key3"], "value3")
}
func TestSetMetadataSerialize(t *testing.T) {
repo := repository.NewMockRepo()
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
err := rene.Commit(repo)
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
unix := time.Now().Unix()
@ -111,18 +110,18 @@ func TestSetMetadataSerialize(t *testing.T) {
})
data, err := json.Marshal(before)
assert.NoError(t, err)
require.NoError(t, err)
var after SetMetadataOperation
err = json.Unmarshal(data, &after)
assert.NoError(t, err)
require.NoError(t, err)
// enforce creating the ID
before.Id()
// Replace the identity stub with the real thing
assert.Equal(t, rene.Id(), after.base().Author.Id())
require.Equal(t, rene.Id(), after.base().Author.Id())
after.Author = rene
assert.Equal(t, before, &after)
require.Equal(t, before, &after)
}

View File

@ -9,32 +9,30 @@ import (
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
"github.com/stretchr/testify/assert"
)
func TestSetStatusSerialize(t *testing.T) {
repo := repository.NewMockRepo()
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
err := rene.Commit(repo)
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
unix := time.Now().Unix()
before := NewSetStatusOp(rene, unix, ClosedStatus)
data, err := json.Marshal(before)
assert.NoError(t, err)
require.NoError(t, err)
var after SetStatusOperation
err = json.Unmarshal(data, &after)
assert.NoError(t, err)
require.NoError(t, err)
// enforce creating the ID
before.Id()
// Replace the identity stub with the real thing
assert.Equal(t, rene.Id(), after.base().Author.Id())
require.Equal(t, rene.Id(), after.base().Author.Id())
after.Author = rene
assert.Equal(t, before, &after)
require.Equal(t, before, &after)
}

View File

@ -9,32 +9,30 @@ import (
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
"github.com/stretchr/testify/assert"
)
func TestSetTitleSerialize(t *testing.T) {
repo := repository.NewMockRepo()
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
err := rene.Commit(repo)
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
unix := time.Now().Unix()
before := NewSetTitleOp(rene, unix, "title", "was")
data, err := json.Marshal(before)
assert.NoError(t, err)
require.NoError(t, err)
var after SetTitleOperation
err = json.Unmarshal(data, &after)
assert.NoError(t, err)
require.NoError(t, err)
// enforce creating the ID
before.Id()
// Replace the identity stub with the real thing
assert.Equal(t, rene.Id(), after.base().Author.Id())
require.Equal(t, rene.Id(), after.base().Author.Id())
after.Author = rene
assert.Equal(t, before, &after)
require.Equal(t, before, &after)
}

View File

@ -25,10 +25,11 @@ func ExampleOperationIterator() {
}
func TestOpIterator(t *testing.T) {
mockRepo := repository.NewMockRepo()
repo := repository.NewMockRepo()
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
err := rene.Commit(mockRepo)
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()
@ -51,14 +52,14 @@ func TestOpIterator(t *testing.T) {
bug1.Append(addCommentOp)
bug1.Append(setStatusOp)
bug1.Append(labelChangeOp)
err = bug1.Commit(mockRepo)
err = bug1.Commit(repo)
require.NoError(t, err)
// second pack
bug1.Append(genTitleOp())
bug1.Append(genTitleOp())
bug1.Append(genTitleOp())
err = bug1.Commit(mockRepo)
err = bug1.Commit(repo)
require.NoError(t, err)
// staging

View File

@ -12,7 +12,8 @@ import (
// 1: original format
// 2: no more legacy identities
const formatVersion = 2
// 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.
@ -158,13 +159,11 @@ func (opp *OperationPack) Write(repo repository.ClockedRepo) (repository.Hash, e
}
data, err := json.Marshal(opp)
if err != nil {
return "", err
}
hash, err := repo.StoreData(data)
if err != nil {
return "", err
}

View File

@ -5,7 +5,6 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/identity"
@ -16,8 +15,8 @@ func TestOperationPackSerialize(t *testing.T) {
opp := &OperationPack{}
repo := repository.NewMockRepo()
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
err := rene.Commit(repo)
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
@ -36,7 +35,7 @@ func TestOperationPackSerialize(t *testing.T) {
opMeta.SetMetadata("key", "value")
opp.Append(opMeta)
assert.Equal(t, 1, len(opMeta.Metadata))
require.Equal(t, 1, len(opMeta.Metadata))
opFile := NewAddCommentOp(rene, time.Now().Unix(), "message", []repository.Hash{
"abcdef",
@ -44,19 +43,19 @@ func TestOperationPackSerialize(t *testing.T) {
})
opp.Append(opFile)
assert.Equal(t, 2, len(opFile.Files))
require.Equal(t, 2, len(opFile.Files))
data, err := json.Marshal(opp)
assert.NoError(t, err)
require.NoError(t, err)
var opp2 *OperationPack
err = json.Unmarshal(data, &opp2)
assert.NoError(t, err)
require.NoError(t, err)
ensureIds(opp)
ensureAuthors(t, opp, opp2)
assert.Equal(t, opp, opp2)
require.Equal(t, opp, opp2)
}
func ensureIds(opp *OperationPack) {

View File

@ -11,7 +11,16 @@ import (
)
func TestValidate(t *testing.T) {
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
repo := repository.NewMockRepoClock()
makeIdentity := func(t *testing.T, name, email string) *identity.Identity {
i, err := identity.NewIdentity(repo, name, email)
require.NoError(t, err)
return i
}
rene := makeIdentity(t, "René Descartes", "rene@descartes.fr")
unix := time.Now().Unix()
good := []Operation{
@ -30,11 +39,11 @@ func TestValidate(t *testing.T) {
bad := []Operation{
// opbase
NewSetStatusOp(identity.NewIdentity("", "rene@descartes.fr"), unix, ClosedStatus),
NewSetStatusOp(identity.NewIdentity("René Descartes\u001b", "rene@descartes.fr"), unix, ClosedStatus),
NewSetStatusOp(identity.NewIdentity("René Descartes", "rene@descartes.fr\u001b"), unix, ClosedStatus),
NewSetStatusOp(identity.NewIdentity("René \nDescartes", "rene@descartes.fr"), unix, ClosedStatus),
NewSetStatusOp(identity.NewIdentity("René Descartes", "rene@\ndescartes.fr"), unix, ClosedStatus),
NewSetStatusOp(makeIdentity(t, "", "rene@descartes.fr"), unix, ClosedStatus),
NewSetStatusOp(makeIdentity(t, "René Descartes\u001b", "rene@descartes.fr"), unix, ClosedStatus),
NewSetStatusOp(makeIdentity(t, "René Descartes", "rene@descartes.fr\u001b"), unix, ClosedStatus),
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,
UnixTime: 0,
@ -68,7 +77,11 @@ func TestValidate(t *testing.T) {
}
func TestMetadata(t *testing.T) {
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
repo := repository.NewMockRepoClock()
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
op := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
op.SetMetadata("key", "value")
@ -88,8 +101,9 @@ func TestID(t *testing.T) {
}
for _, repo := range repos {
rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
err := rene.Commit(repo)
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
err = rene.Commit(repo)
require.NoError(t, err)
b, op, err := Create(rene, time.Now().Unix(), "title", "message")

View File

@ -6,13 +6,13 @@ func RefsToIds(refs []string) []Id {
ids := make([]Id, len(refs))
for i, ref := range refs {
ids[i] = refToId(ref)
ids[i] = RefToId(ref)
}
return ids
}
func refToId(ref string) Id {
func RefToId(ref string) Id {
split := strings.Split(ref, "/")
return Id(split[len(split)-1])
}

View File

@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"reflect"
"strings"
"github.com/pkg/errors"
@ -102,8 +101,7 @@ func ReadRemote(repo repository.Repo, remote string, id string) (*Identity, erro
// read will load and parse an identity from git
func read(repo repository.Repo, ref string) (*Identity, 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")