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