bridge.go

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