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