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