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