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