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