bridge.go

  1// Package core contains the target-agnostic code to define and run a bridge
  2package core
  3
  4import (
  5	"context"
  6	"fmt"
  7	"os"
  8	"reflect"
  9	"regexp"
 10	"sort"
 11	"strings"
 12	"time"
 13
 14	"github.com/pkg/errors"
 15
 16	"github.com/MichaelMure/git-bug/cache"
 17	"github.com/MichaelMure/git-bug/repository"
 18)
 19
 20var ErrImportNotSupported = errors.New("import is not supported")
 21var ErrExportNotSupported = errors.New("export is not supported")
 22
 23const (
 24	ConfigKeyTarget = "target"
 25
 26	MetaKeyOrigin = "origin"
 27
 28	bridgeConfigKeyPrefix = "git-bug.bridge"
 29)
 30
 31var bridgeImpl map[string]reflect.Type
 32var bridgeValidParams map[string][]string
 33var bridgeLoginMetaKey map[string]string
 34
 35// Bridge is a wrapper around a BridgeImpl that will bind low-level
 36// implementation with utility code to provide high-level functions.
 37type Bridge struct {
 38	Name           string
 39	repo           *cache.RepoCache
 40	impl           BridgeImpl
 41	importer       Importer
 42	exporter       Exporter
 43	conf           Configuration
 44	initImportDone bool
 45	initExportDone bool
 46}
 47
 48// Register will register a new BridgeImpl
 49func Register(impl BridgeImpl) {
 50	if bridgeImpl == nil {
 51		bridgeImpl = make(map[string]reflect.Type)
 52	}
 53	if bridgeLoginMetaKey == nil {
 54		bridgeLoginMetaKey = make(map[string]string)
 55	}
 56	bridgeImpl[impl.Target()] = reflect.TypeOf(impl).Elem()
 57	bridgeLoginMetaKey[impl.Target()] = impl.LoginMetaKey()
 58
 59	paramMap := bridgeImpl[impl.Target()].(BridgeImpl).ValidParams()
 60	params := make([]string, len(paramMap))
 61
 62	i := 0
 63	for k := range paramMap {
 64		params[i] = k
 65		i++
 66	}
 67	bridgeValidParams[impl.Target()] = params
 68}
 69
 70// Targets return all known bridge implementation target
 71func Targets() []string {
 72	var result []string
 73
 74	for key := range bridgeImpl {
 75		result = append(result, key)
 76	}
 77
 78	sort.Strings(result)
 79
 80	return result
 81}
 82
 83// TargetTypes returns all types of bridge implementation target
 84func TargetTypes() map[string]reflect.Type {
 85	return bridgeImpl
 86}
 87
 88func ValidParams(target string) ([]string, error) {
 89	validParams, ok := bridgeValidParams[target]
 90	if !ok {
 91		return nil, fmt.Errorf("unknown bridge target %v", target)
 92	}
 93
 94	return validParams, nil
 95}
 96
 97// TargetExist return true if the given target has a bridge implementation
 98func TargetExist(target string) bool {
 99	_, ok := bridgeImpl[target]
100	return ok
101}
102
103// LoginMetaKey return the metadata key used to store the remote bug-tracker login
104// on the user identity. The corresponding value is used to match identities and
105// credentials.
106func LoginMetaKey(target string) (string, error) {
107	metaKey, ok := bridgeLoginMetaKey[target]
108	if !ok {
109		return "", fmt.Errorf("unknown bridge target %v", target)
110	}
111
112	return metaKey, nil
113}
114
115// Instantiate a new Bridge for a repo, from the given target and name
116func NewBridge(repo *cache.RepoCache, target string, name string) (*Bridge, error) {
117	implType, ok := bridgeImpl[target]
118	if !ok {
119		return nil, fmt.Errorf("unknown bridge target %v", target)
120	}
121
122	impl := reflect.New(implType).Interface().(BridgeImpl)
123
124	bridge := &Bridge{
125		Name: name,
126		repo: repo,
127		impl: impl,
128	}
129
130	return bridge, nil
131}
132
133// LoadBridge instantiate a new bridge from a repo configuration
134func LoadBridge(repo *cache.RepoCache, name string) (*Bridge, error) {
135	conf, err := loadConfig(repo, name)
136	if err != nil {
137		return nil, err
138	}
139
140	target := conf[ConfigKeyTarget]
141	bridge, err := NewBridge(repo, target, name)
142	if err != nil {
143		return nil, err
144	}
145
146	err = bridge.impl.ValidateConfig(conf)
147	if err != nil {
148		return nil, errors.Wrap(err, "invalid configuration")
149	}
150
151	// will avoid reloading configuration before an export or import call
152	bridge.conf = conf
153	return bridge, nil
154}
155
156// Attempt to retrieve a default bridge for the given repo. If zero or multiple
157// bridge exist, it fails.
158func DefaultBridge(repo *cache.RepoCache) (*Bridge, error) {
159	bridges, err := ConfiguredBridges(repo)
160	if err != nil {
161		return nil, err
162	}
163
164	if len(bridges) == 0 {
165		return nil, fmt.Errorf("no configured bridge")
166	}
167
168	if len(bridges) > 1 {
169		return nil, fmt.Errorf("multiple bridge are configured, you need to select one explicitely")
170	}
171
172	return LoadBridge(repo, bridges[0])
173}
174
175// ConfiguredBridges return the list of bridge that are configured for the given
176// repo
177func ConfiguredBridges(repo repository.RepoConfig) ([]string, error) {
178	configs, err := repo.LocalConfig().ReadAll(bridgeConfigKeyPrefix + ".")
179	if err != nil {
180		return nil, errors.Wrap(err, "can't read configured bridges")
181	}
182
183	re := regexp.MustCompile(bridgeConfigKeyPrefix + `.([^.]+)`)
184
185	set := make(map[string]interface{})
186
187	for key := range configs {
188		res := re.FindStringSubmatch(key)
189
190		if res == nil {
191			continue
192		}
193
194		set[res[1]] = nil
195	}
196
197	result := make([]string, len(set))
198
199	i := 0
200	for key := range set {
201		result[i] = key
202		i++
203	}
204
205	return result, nil
206}
207
208// Check if a bridge exist
209func BridgeExist(repo repository.RepoConfig, name string) bool {
210	keyPrefix := fmt.Sprintf("git-bug.bridge.%s.", name)
211
212	conf, err := repo.LocalConfig().ReadAll(keyPrefix)
213
214	return err == nil && len(conf) > 0
215}
216
217// Remove a configured bridge
218func RemoveBridge(repo repository.RepoConfig, name string) error {
219	re := regexp.MustCompile(`^[a-zA-Z0-9]+`)
220
221	if !re.MatchString(name) {
222		return fmt.Errorf("bad bridge fullname: %s", name)
223	}
224
225	keyPrefix := fmt.Sprintf("git-bug.bridge.%s", name)
226	return repo.LocalConfig().RemoveAll(keyPrefix)
227}
228
229// Configure run the target specific configuration process
230func (b *Bridge) Configure(params BridgeParams) error {
231	validateParams(params, b.impl)
232
233	conf, err := b.impl.Configure(b.repo, params)
234	if err != nil {
235		return err
236	}
237
238	err = b.impl.ValidateConfig(conf)
239	if err != nil {
240		return fmt.Errorf("invalid configuration: %v", err)
241	}
242
243	b.conf = conf
244	return b.storeConfig(conf)
245}
246
247func validateParams(params BridgeParams, impl BridgeImpl) {
248	validParams := impl.ValidParams()
249
250	paramsValue := reflect.ValueOf(params)
251	paramsType := paramsValue.Type()
252
253	for i := 0; i < paramsValue.NumField(); i++ {
254		name := paramsType.Field(i).Name
255		val := paramsValue.Field(i).Interface().(string)
256		_, valid := validParams[name]
257		if val != "" && !valid {
258			_, _ = fmt.Fprintln(os.Stderr, params.fieldWarning(name, impl.Target()))
259		}
260	}
261}
262
263func (b *Bridge) storeConfig(conf Configuration) error {
264	for key, val := range conf {
265		storeKey := fmt.Sprintf("git-bug.bridge.%s.%s", b.Name, key)
266
267		err := b.repo.LocalConfig().StoreString(storeKey, val)
268		if err != nil {
269			return errors.Wrap(err, "error while storing bridge configuration")
270		}
271	}
272
273	return nil
274}
275
276func (b *Bridge) ensureConfig() error {
277	if b.conf == nil {
278		conf, err := loadConfig(b.repo, b.Name)
279		if err != nil {
280			return err
281		}
282		b.conf = conf
283	}
284
285	return nil
286}
287
288func loadConfig(repo repository.RepoConfig, name string) (Configuration, error) {
289	keyPrefix := fmt.Sprintf("git-bug.bridge.%s.", name)
290
291	pairs, err := repo.LocalConfig().ReadAll(keyPrefix)
292	if err != nil {
293		return nil, errors.Wrap(err, "error while reading bridge configuration")
294	}
295
296	result := make(Configuration, len(pairs))
297	for key, value := range pairs {
298		key := strings.TrimPrefix(key, keyPrefix)
299		result[key] = value
300	}
301
302	return result, nil
303}
304
305func (b *Bridge) getImporter() Importer {
306	if b.importer == nil {
307		b.importer = b.impl.NewImporter()
308	}
309
310	return b.importer
311}
312
313func (b *Bridge) getExporter() Exporter {
314	if b.exporter == nil {
315		b.exporter = b.impl.NewExporter()
316	}
317
318	return b.exporter
319}
320
321func (b *Bridge) ensureImportInit(ctx context.Context) error {
322	if b.initImportDone {
323		return nil
324	}
325
326	importer := b.getImporter()
327	if importer != nil {
328		err := importer.Init(ctx, b.repo, b.conf)
329		if err != nil {
330			return err
331		}
332	}
333
334	b.initImportDone = true
335	return nil
336}
337
338func (b *Bridge) ensureExportInit(ctx context.Context) error {
339	if b.initExportDone {
340		return nil
341	}
342
343	exporter := b.getExporter()
344	if exporter != nil {
345		err := exporter.Init(ctx, b.repo, b.conf)
346		if err != nil {
347			return err
348		}
349	}
350
351	b.initExportDone = true
352	return nil
353}
354
355func (b *Bridge) ImportAllSince(ctx context.Context, since time.Time) (<-chan ImportResult, error) {
356	// 5 seconds before the actual start just to be sure.
357	importStartTime := time.Now().Add(-5 * time.Second)
358
359	importer := b.getImporter()
360	if importer == nil {
361		return nil, ErrImportNotSupported
362	}
363
364	err := b.ensureConfig()
365	if err != nil {
366		return nil, err
367	}
368
369	err = b.ensureImportInit(ctx)
370	if err != nil {
371		return nil, err
372	}
373
374	events, err := importer.ImportAll(ctx, b.repo, since)
375	if err != nil {
376		return nil, err
377	}
378
379	out := make(chan ImportResult)
380	go func() {
381		defer close(out)
382		noError := true
383
384		// relay all events while checking that everything went well
385		for event := range events {
386			if event.Event == ImportEventError {
387				noError = false
388			}
389			out <- event
390		}
391
392		// store the last import time ONLY if no error happened
393		if noError {
394			key := fmt.Sprintf("git-bug.bridge.%s.lastImportTime", b.Name)
395			err = b.repo.LocalConfig().StoreTimestamp(key, importStartTime)
396		}
397	}()
398
399	return out, nil
400}
401
402func (b *Bridge) ImportAll(ctx context.Context) (<-chan ImportResult, error) {
403	// If possible, restart from the last import time
404	lastImport, err := b.repo.LocalConfig().ReadTimestamp(fmt.Sprintf("git-bug.bridge.%s.lastImportTime", b.Name))
405	if err == nil {
406		return b.ImportAllSince(ctx, lastImport)
407	}
408
409	return b.ImportAllSince(ctx, time.Time{})
410}
411
412func (b *Bridge) ExportAll(ctx context.Context, since time.Time) (<-chan ExportResult, error) {
413	exporter := b.getExporter()
414	if exporter == nil {
415		return nil, ErrExportNotSupported
416	}
417
418	err := b.ensureConfig()
419	if err != nil {
420		return nil, err
421	}
422
423	err = b.ensureExportInit(ctx)
424	if err != nil {
425		return nil, err
426	}
427
428	return exporter.ExportAll(ctx, b.repo, since)
429}