bridge.go

  1// Package core contains the target-agnostic code to define and run a bridge
  2package core
  3
  4import (
  5	"fmt"
  6	"reflect"
  7	"regexp"
  8	"strings"
  9	"time"
 10
 11	"github.com/MichaelMure/git-bug/cache"
 12	"github.com/MichaelMure/git-bug/repository"
 13	"github.com/pkg/errors"
 14)
 15
 16var ErrImportNotSupported = errors.New("import is not supported")
 17var ErrExportNotSupported = errors.New("export is not supported")
 18
 19const bridgeConfigKeyPrefix = "git-bug.bridge"
 20
 21var bridgeImpl map[string]reflect.Type
 22
 23// Bridge is a wrapper around a BridgeImpl that will bind low-level
 24// implementation with utility code to provide high-level functions.
 25type Bridge struct {
 26	Name     string
 27	repo     *cache.RepoCache
 28	impl     BridgeImpl
 29	importer Importer
 30	exporter Exporter
 31	conf     Configuration
 32	initDone bool
 33}
 34
 35// Register will register a new BridgeImpl
 36func Register(impl BridgeImpl) {
 37	if bridgeImpl == nil {
 38		bridgeImpl = make(map[string]reflect.Type)
 39	}
 40	bridgeImpl[impl.Target()] = reflect.TypeOf(impl)
 41}
 42
 43// Targets return all known bridge implementation target
 44func Targets() []string {
 45	var result []string
 46
 47	for key := range bridgeImpl {
 48		result = append(result, key)
 49	}
 50
 51	return result
 52}
 53
 54// Instantiate a new Bridge for a repo, from the given target and name
 55func NewBridge(repo *cache.RepoCache, target string, name string) (*Bridge, error) {
 56	implType, ok := bridgeImpl[target]
 57	if !ok {
 58		return nil, fmt.Errorf("unknown bridge target %v", target)
 59	}
 60
 61	impl := reflect.New(implType).Elem().Interface().(BridgeImpl)
 62
 63	bridge := &Bridge{
 64		Name: name,
 65		repo: repo,
 66		impl: impl,
 67	}
 68
 69	return bridge, nil
 70}
 71
 72// Instantiate a new bridge for a repo, from the combined target and name contained
 73// in the full name
 74func NewBridgeFromFullName(repo *cache.RepoCache, fullName string) (*Bridge, error) {
 75	target, name, err := splitFullName(fullName)
 76	if err != nil {
 77		return nil, err
 78	}
 79
 80	return NewBridge(repo, target, name)
 81}
 82
 83// Attempt to retrieve a default bridge for the given repo. If zero or multiple
 84// bridge exist, it fails.
 85func DefaultBridge(repo *cache.RepoCache) (*Bridge, error) {
 86	bridges, err := ConfiguredBridges(repo)
 87	if err != nil {
 88		return nil, err
 89	}
 90
 91	if len(bridges) == 0 {
 92		return nil, fmt.Errorf("no configured bridge")
 93	}
 94
 95	if len(bridges) > 1 {
 96		return nil, fmt.Errorf("multiple bridge are configured, you need to select one explicitely")
 97	}
 98
 99	target, name, err := splitFullName(bridges[0])
100	if err != nil {
101		return nil, err
102	}
103
104	return NewBridge(repo, target, name)
105}
106
107func splitFullName(fullName string) (string, string, error) {
108	split := strings.Split(fullName, ".")
109
110	if len(split) != 2 {
111		return "", "", fmt.Errorf("bad bridge fullname: %s", fullName)
112	}
113
114	return split[0], split[1], nil
115}
116
117// ConfiguredBridges return the list of bridge that are configured for the given
118// repo
119func ConfiguredBridges(repo repository.RepoCommon) ([]string, error) {
120	configs, err := repo.ReadConfigs(bridgeConfigKeyPrefix + ".")
121	if err != nil {
122		return nil, errors.Wrap(err, "can't read configured bridges")
123	}
124
125	re, err := regexp.Compile(bridgeConfigKeyPrefix + `.([^.]+\.[^.]+)`)
126	if err != nil {
127		panic(err)
128	}
129
130	set := make(map[string]interface{})
131
132	for key := range configs {
133		res := re.FindStringSubmatch(key)
134
135		if res == nil {
136			continue
137		}
138
139		set[res[1]] = nil
140	}
141
142	result := make([]string, len(set))
143
144	i := 0
145	for key := range set {
146		result[i] = key
147		i++
148	}
149
150	return result, nil
151}
152
153// Remove a configured bridge
154func RemoveBridge(repo repository.RepoCommon, fullName string) error {
155	re, err := regexp.Compile(`^[^.]+\.[^.]+$`)
156	if err != nil {
157		panic(err)
158	}
159
160	if !re.MatchString(fullName) {
161		return fmt.Errorf("bad bridge fullname: %s", fullName)
162	}
163
164	keyPrefix := fmt.Sprintf("git-bug.bridge.%s", fullName)
165	return repo.RmConfigs(keyPrefix)
166}
167
168// Configure run the target specific configuration process
169func (b *Bridge) Configure() error {
170	conf, err := b.impl.Configure(b.repo)
171	if err != nil {
172		return err
173	}
174
175	b.conf = conf
176
177	return b.storeConfig(conf)
178}
179
180func (b *Bridge) storeConfig(conf Configuration) error {
181	for key, val := range conf {
182		storeKey := fmt.Sprintf("git-bug.bridge.%s.%s.%s", b.impl.Target(), b.Name, key)
183
184		err := b.repo.StoreConfig(storeKey, val)
185		if err != nil {
186			return errors.Wrap(err, "error while storing bridge configuration")
187		}
188	}
189
190	return nil
191}
192
193func (b *Bridge) ensureConfig() error {
194	if b.conf == nil {
195		conf, err := b.loadConfig()
196		if err != nil {
197			return err
198		}
199		b.conf = conf
200	}
201
202	return nil
203}
204
205func (b *Bridge) loadConfig() (Configuration, error) {
206	keyPrefix := fmt.Sprintf("git-bug.bridge.%s.%s.", b.impl.Target(), b.Name)
207
208	pairs, err := b.repo.ReadConfigs(keyPrefix)
209	if err != nil {
210		return nil, errors.Wrap(err, "error while reading bridge configuration")
211	}
212
213	result := make(Configuration, len(pairs))
214	for key, value := range pairs {
215		key := strings.TrimPrefix(key, keyPrefix)
216		result[key] = value
217	}
218
219	err = b.impl.ValidateConfig(result)
220	if err != nil {
221		return nil, errors.Wrap(err, "invalid configuration")
222	}
223
224	return result, nil
225}
226
227func (b *Bridge) getImporter() Importer {
228	if b.importer == nil {
229		b.importer = b.impl.NewImporter()
230	}
231
232	return b.importer
233}
234
235func (b *Bridge) getExporter() Exporter {
236	if b.exporter == nil {
237		b.exporter = b.impl.NewExporter()
238	}
239
240	return b.exporter
241}
242
243func (b *Bridge) ensureInit() error {
244	if b.initDone {
245		return nil
246	}
247
248	importer := b.getImporter()
249	if importer != nil {
250		err := importer.Init(b.conf)
251		if err != nil {
252			return err
253		}
254	}
255
256	exporter := b.getExporter()
257	if exporter != nil {
258		err := exporter.Init(b.conf)
259		if err != nil {
260			return err
261		}
262	}
263
264	b.initDone = true
265
266	return nil
267}
268
269func (b *Bridge) ImportAll(since time.Time) error {
270	importer := b.getImporter()
271	if importer == nil {
272		return ErrImportNotSupported
273	}
274
275	err := b.ensureConfig()
276	if err != nil {
277		return err
278	}
279
280	err = b.ensureInit()
281	if err != nil {
282		return err
283	}
284
285	return importer.ImportAll(b.repo, since)
286}
287
288func (b *Bridge) ExportAll(since time.Time) error {
289	exporter := b.getExporter()
290	if exporter == nil {
291		return ErrExportNotSupported
292	}
293
294	err := b.ensureConfig()
295	if err != nil {
296		return err
297	}
298
299	err = b.ensureInit()
300	if err != nil {
301		return err
302	}
303
304	return exporter.ExportAll(b.repo, since)
305}