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