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	KeyTarget = "target"
 24	KeyOrigin = "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// Instantiate a new Bridge for a repo, from the given target and name
 75func NewBridge(repo *cache.RepoCache, target string, name string) (*Bridge, error) {
 76	implType, ok := bridgeImpl[target]
 77	if !ok {
 78		return nil, fmt.Errorf("unknown bridge target %v", target)
 79	}
 80
 81	impl := reflect.New(implType).Elem().Interface().(BridgeImpl)
 82
 83	bridge := &Bridge{
 84		Name: name,
 85		repo: repo,
 86		impl: impl,
 87	}
 88
 89	return bridge, nil
 90}
 91
 92// LoadBridge instantiate a new bridge from a repo configuration
 93func LoadBridge(repo *cache.RepoCache, name string) (*Bridge, error) {
 94	conf, err := loadConfig(repo, name)
 95	if err != nil {
 96		return nil, err
 97	}
 98
 99	target := conf[KeyTarget]
100	bridge, err := NewBridge(repo, target, name)
101	if err != nil {
102		return nil, err
103	}
104
105	err = bridge.impl.ValidateConfig(conf)
106	if err != nil {
107		return nil, errors.Wrap(err, "invalid configuration")
108	}
109
110	// will avoid reloading configuration before an export or import call
111	bridge.conf = conf
112	return bridge, nil
113}
114
115// Attempt to retrieve a default bridge for the given repo. If zero or multiple
116// bridge exist, it fails.
117func DefaultBridge(repo *cache.RepoCache) (*Bridge, error) {
118	bridges, err := ConfiguredBridges(repo)
119	if err != nil {
120		return nil, err
121	}
122
123	if len(bridges) == 0 {
124		return nil, fmt.Errorf("no configured bridge")
125	}
126
127	if len(bridges) > 1 {
128		return nil, fmt.Errorf("multiple bridge are configured, you need to select one explicitely")
129	}
130
131	return LoadBridge(repo, bridges[0])
132}
133
134// ConfiguredBridges return the list of bridge that are configured for the given
135// repo
136func ConfiguredBridges(repo repository.RepoCommon) ([]string, error) {
137	configs, err := repo.LocalConfig().ReadAll(bridgeConfigKeyPrefix + ".")
138	if err != nil {
139		return nil, errors.Wrap(err, "can't read configured bridges")
140	}
141
142	re, err := regexp.Compile(bridgeConfigKeyPrefix + `.([^.]+)`)
143	if err != nil {
144		panic(err)
145	}
146
147	set := make(map[string]interface{})
148
149	for key := range configs {
150		res := re.FindStringSubmatch(key)
151
152		if res == nil {
153			continue
154		}
155
156		set[res[1]] = nil
157	}
158
159	result := make([]string, len(set))
160
161	i := 0
162	for key := range set {
163		result[i] = key
164		i++
165	}
166
167	return result, nil
168}
169
170// Check if a bridge exist
171func BridgeExist(repo repository.RepoCommon, name string) bool {
172	keyPrefix := fmt.Sprintf("git-bug.bridge.%s.", name)
173
174	conf, err := repo.LocalConfig().ReadAll(keyPrefix)
175
176	return err == nil && len(conf) > 0
177}
178
179// Remove a configured bridge
180func RemoveBridge(repo repository.RepoCommon, name string) error {
181	re, err := regexp.Compile(`^[a-zA-Z0-9]+`)
182	if err != nil {
183		panic(err)
184	}
185
186	if !re.MatchString(name) {
187		return fmt.Errorf("bad bridge fullname: %s", name)
188	}
189
190	keyPrefix := fmt.Sprintf("git-bug.bridge.%s", name)
191	return repo.LocalConfig().RemoveAll(keyPrefix)
192}
193
194// Configure run the target specific configuration process
195func (b *Bridge) Configure(params BridgeParams) error {
196	conf, err := b.impl.Configure(b.repo, params)
197	if err != nil {
198		return err
199	}
200
201	err = b.impl.ValidateConfig(conf)
202	if err != nil {
203		return fmt.Errorf("invalid configuration: %v", err)
204	}
205
206	b.conf = conf
207	return b.storeConfig(conf)
208}
209
210func (b *Bridge) storeConfig(conf Configuration) error {
211	for key, val := range conf {
212		storeKey := fmt.Sprintf("git-bug.bridge.%s.%s", b.Name, key)
213
214		err := b.repo.LocalConfig().StoreString(storeKey, val)
215		if err != nil {
216			return errors.Wrap(err, "error while storing bridge configuration")
217		}
218	}
219
220	return nil
221}
222
223func (b *Bridge) ensureConfig() error {
224	if b.conf == nil {
225		conf, err := loadConfig(b.repo, b.Name)
226		if err != nil {
227			return err
228		}
229		b.conf = conf
230	}
231
232	return nil
233}
234
235func loadConfig(repo repository.RepoCommon, name string) (Configuration, error) {
236	keyPrefix := fmt.Sprintf("git-bug.bridge.%s.", name)
237
238	pairs, err := repo.LocalConfig().ReadAll(keyPrefix)
239	if err != nil {
240		return nil, errors.Wrap(err, "error while reading bridge configuration")
241	}
242
243	result := make(Configuration, len(pairs))
244	for key, value := range pairs {
245		key := strings.TrimPrefix(key, keyPrefix)
246		result[key] = value
247	}
248
249	return result, nil
250}
251
252func (b *Bridge) getImporter() Importer {
253	if b.importer == nil {
254		b.importer = b.impl.NewImporter()
255	}
256
257	return b.importer
258}
259
260func (b *Bridge) getExporter() Exporter {
261	if b.exporter == nil {
262		b.exporter = b.impl.NewExporter()
263	}
264
265	return b.exporter
266}
267
268func (b *Bridge) ensureInit() error {
269	if b.initDone {
270		return nil
271	}
272
273	importer := b.getImporter()
274	if importer != nil {
275		err := importer.Init(b.conf)
276		if err != nil {
277			return err
278		}
279	}
280
281	exporter := b.getExporter()
282	if exporter != nil {
283		err := exporter.Init(b.conf)
284		if err != nil {
285			return err
286		}
287	}
288
289	b.initDone = true
290
291	return nil
292}
293
294func (b *Bridge) ImportAll(ctx context.Context, since time.Time) (<-chan ImportResult, error) {
295	importer := b.getImporter()
296	if importer == nil {
297		return nil, ErrImportNotSupported
298	}
299
300	err := b.ensureConfig()
301	if err != nil {
302		return nil, err
303	}
304
305	err = b.ensureInit()
306	if err != nil {
307		return nil, err
308	}
309
310	return importer.ImportAll(ctx, b.repo, since)
311}
312
313func (b *Bridge) ExportAll(ctx context.Context, since time.Time) (<-chan ExportResult, error) {
314	exporter := b.getExporter()
315	if exporter == nil {
316		return nil, ErrExportNotSupported
317	}
318
319	err := b.ensureConfig()
320	if err != nil {
321		return nil, err
322	}
323
324	err = b.ensureInit()
325	if err != nil {
326		return nil, err
327	}
328
329	return exporter.ExportAll(ctx, b.repo, since)
330}