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