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