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