operation_pack.go

  1package bug
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6
  7	"github.com/pkg/errors"
  8
  9	"github.com/MichaelMure/git-bug/entity"
 10	"github.com/MichaelMure/git-bug/repository"
 11)
 12
 13// 1: original format
 14// 2: no more legacy identities
 15// 3: Ids are generated from the create operation serialized data instead of from the first git commit
 16const formatVersion = 3
 17
 18// OperationPack represent an ordered set of operation to apply
 19// to a Bug. These operations are stored in a single Git commit.
 20//
 21// These commits will be linked together in a linear chain of commits
 22// inside Git to form the complete ordered chain of operation to
 23// apply to get the final state of the Bug
 24type OperationPack struct {
 25	Operations []Operation
 26
 27	// Private field so not serialized
 28	commitHash repository.Hash
 29}
 30
 31func (opp *OperationPack) MarshalJSON() ([]byte, error) {
 32	return json.Marshal(struct {
 33		Version    uint        `json:"version"`
 34		Operations []Operation `json:"ops"`
 35	}{
 36		Version:    formatVersion,
 37		Operations: opp.Operations,
 38	})
 39}
 40
 41func (opp *OperationPack) UnmarshalJSON(data []byte) error {
 42	aux := struct {
 43		Version    uint              `json:"version"`
 44		Operations []json.RawMessage `json:"ops"`
 45	}{}
 46
 47	if err := json.Unmarshal(data, &aux); err != nil {
 48		return err
 49	}
 50
 51	if aux.Version < formatVersion {
 52		return entity.NewErrOldFormatVersion(aux.Version)
 53	}
 54	if aux.Version > formatVersion {
 55		return entity.NewErrNewFormatVersion(aux.Version)
 56	}
 57
 58	for _, raw := range aux.Operations {
 59		var t struct {
 60			OperationType OperationType `json:"type"`
 61		}
 62
 63		if err := json.Unmarshal(raw, &t); err != nil {
 64			return err
 65		}
 66
 67		// delegate to specialized unmarshal function
 68		op, err := opp.unmarshalOp(raw, t.OperationType)
 69		if err != nil {
 70			return err
 71		}
 72
 73		opp.Operations = append(opp.Operations, op)
 74	}
 75
 76	return nil
 77}
 78
 79func (opp *OperationPack) unmarshalOp(raw []byte, _type OperationType) (Operation, error) {
 80	switch _type {
 81	case AddCommentOp:
 82		op := &AddCommentOperation{}
 83		err := json.Unmarshal(raw, &op)
 84		return op, err
 85	case CreateOp:
 86		op := &CreateOperation{}
 87		err := json.Unmarshal(raw, &op)
 88		return op, err
 89	case EditCommentOp:
 90		op := &EditCommentOperation{}
 91		err := json.Unmarshal(raw, &op)
 92		return op, err
 93	case LabelChangeOp:
 94		op := &LabelChangeOperation{}
 95		err := json.Unmarshal(raw, &op)
 96		return op, err
 97	case NoOpOp:
 98		op := &NoOpOperation{}
 99		err := json.Unmarshal(raw, &op)
100		return op, err
101	case SetMetadataOp:
102		op := &SetMetadataOperation{}
103		err := json.Unmarshal(raw, &op)
104		return op, err
105	case SetStatusOp:
106		op := &SetStatusOperation{}
107		err := json.Unmarshal(raw, &op)
108		return op, err
109	case SetTitleOp:
110		op := &SetTitleOperation{}
111		err := json.Unmarshal(raw, &op)
112		return op, err
113	default:
114		return nil, fmt.Errorf("unknown operation type %v", _type)
115	}
116}
117
118// Append a new operation to the pack
119func (opp *OperationPack) Append(op Operation) {
120	opp.Operations = append(opp.Operations, op)
121}
122
123// IsEmpty tell if the OperationPack is empty
124func (opp *OperationPack) IsEmpty() bool {
125	return len(opp.Operations) == 0
126}
127
128// IsValid tell if the OperationPack is considered valid
129func (opp *OperationPack) Validate() error {
130	if opp.IsEmpty() {
131		return fmt.Errorf("empty")
132	}
133
134	for _, op := range opp.Operations {
135		if err := op.Validate(); err != nil {
136			return errors.Wrap(err, "op")
137		}
138	}
139
140	return nil
141}
142
143// Write will serialize and store the OperationPack as a git blob and return
144// its hash
145func (opp *OperationPack) Write(repo repository.ClockedRepo) (repository.Hash, error) {
146	// make sure we don't write invalid data
147	err := opp.Validate()
148	if err != nil {
149		return "", errors.Wrap(err, "validation error")
150	}
151
152	// First, make sure that all the identities are properly Commit as well
153	// TODO: this might be downgraded to "make sure it exist in git" but then, what make
154	// sure no data is lost on identities ?
155	for _, op := range opp.Operations {
156		if op.base().Author.NeedCommit() {
157			return "", fmt.Errorf("identity need commmit")
158		}
159	}
160
161	data, err := json.Marshal(opp)
162	if err != nil {
163		return "", err
164	}
165
166	hash, err := repo.StoreData(data)
167	if err != nil {
168		return "", err
169	}
170
171	return hash, nil
172}
173
174// Make a deep copy
175func (opp *OperationPack) Clone() OperationPack {
176
177	clone := OperationPack{
178		Operations: make([]Operation, len(opp.Operations)),
179		commitHash: opp.commitHash,
180	}
181
182	for i, op := range opp.Operations {
183		clone.Operations[i] = op
184	}
185
186	return clone
187}