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