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	ConfigKeyTarget = "target"
 24
 25	MetaKeyOrigin = "origin"
 26
 27	bridgeConfigKeyPrefix = "git-bug.bridge"
 28)
 29
 30var bridgeImpl map[string]reflect.Type
 31var bridgeLoginMetaKey map[string]string
 32
 33// BridgeParams holds parameters to simplify the bridge configuration without
 34// having to make terminal prompts.
 35type BridgeParams struct {
 36	Owner      string // owner of the repo                    (Github)
 37	Project    string // name of the repo                     (Github,         Launchpad)
 38	URL        string // complete URL of a repo               (Github, Gitlab, Launchpad)
 39	BaseURL    string // base URL for self-hosted instance    (        Gitlab)
 40	CredPrefix string // ID prefix of the credential to use   (Github, Gitlab)
 41	TokenRaw   string // pre-existing token to use            (Github, Gitlab)
 42	Login      string // username for the passed credential   (Github, Gitlab)
 43}
 44
 45// Bridge is a wrapper around a BridgeImpl that will bind low-level
 46// implementation with utility code to provide high-level functions.
 47type Bridge struct {
 48	Name           string
 49	repo           *cache.RepoCache
 50	impl           BridgeImpl
 51	importer       Importer
 52	exporter       Exporter
 53	conf           Configuration
 54	initImportDone bool
 55	initExportDone bool
 56}
 57
 58// Register will register a new BridgeImpl
 59func Register(impl BridgeImpl) {
 60	if bridgeImpl == nil {
 61		bridgeImpl = make(map[string]reflect.Type)
 62	}
 63	if bridgeLoginMetaKey == nil {
 64		bridgeLoginMetaKey = make(map[string]string)
 65	}
 66	bridgeImpl[impl.Target()] = reflect.TypeOf(impl)
 67	bridgeLoginMetaKey[impl.Target()] = impl.LoginMetaKey()
 68}
 69
 70// Targets return all known bridge implementation target
 71func Targets() []string {
 72	var result []string
 73
 74	for key := range bridgeImpl {
 75		result = append(result, key)
 76	}
 77
 78	sort.Strings(result)
 79
 80	return result
 81}
 82
 83// TargetExist return true if the given target has a bridge implementation
 84func TargetExist(target string) bool {
 85	_, ok := bridgeImpl[target]
 86	return ok
 87}
 88
 89// LoginMetaKey return the metadata key used to store the remote bug-tracker login
 90// on the user identity. The corresponding value is used to match identities and
 91// credentials.
 92func LoginMetaKey(target string) (string, error) {
 93	metaKey, ok := bridgeLoginMetaKey[target]
 94	if !ok {
 95		return "", fmt.Errorf("unknown bridge target %v", target)
 96	}
 97
 98	return metaKey, nil
 99}
100
101// Instantiate a new Bridge for a repo, from the given target and name
102func NewBridge(repo *cache.RepoCache, target string, name string) (*Bridge, error) {
103	implType, ok := bridgeImpl[target]
104	if !ok {
105		return nil, fmt.Errorf("unknown bridge target %v", target)
106	}
107
108	impl := reflect.New(implType).Elem().Interface().(BridgeImpl)
109
110	bridge := &Bridge{
111		Name: name,
112		repo: repo,
113		impl: impl,
114	}
115
116	return bridge, nil
117}
118
119// LoadBridge instantiate a new bridge from a repo configuration
120func LoadBridge(repo *cache.RepoCache, name string) (*Bridge, error) {
121	conf, err := loadConfig(repo, name)
122	if err != nil {
123		return nil, err
124	}
125
126	target := conf[ConfigKeyTarget]
127	bridge, err := NewBridge(repo, target, name)
128	if err != nil {
129		return nil, err
130	}
131
132	err = bridge.impl.ValidateConfig(conf)
133	if err != nil {
134		return nil, errors.Wrap(err, "invalid configuration")
135	}
136
137	// will avoid reloading configuration before an export or import call
138	bridge.conf = conf
139	return bridge, nil
140}
141
142// Attempt to retrieve a default bridge for the given repo. If zero or multiple
143// bridge exist, it fails.
144func DefaultBridge(repo *cache.RepoCache) (*Bridge, error) {
145	bridges, err := ConfiguredBridges(repo)
146	if err != nil {
147		return nil, err
148	}
149
150	if len(bridges) == 0 {
151		return nil, fmt.Errorf("no configured bridge")
152	}
153
154	if len(bridges) > 1 {
155		return nil, fmt.Errorf("multiple bridge are configured, you need to select one explicitely")
156	}
157
158	return LoadBridge(repo, bridges[0])
159}
160
161// ConfiguredBridges return the list of bridge that are configured for the given
162// repo
163func ConfiguredBridges(repo repository.RepoConfig) ([]string, error) {
164	configs, err := repo.LocalConfig().ReadAll(bridgeConfigKeyPrefix + ".")
165	if err != nil {
166		return nil, errors.Wrap(err, "can't read configured bridges")
167	}
168
169	re, err := regexp.Compile(bridgeConfigKeyPrefix + `.([^.]+)`)
170	if err != nil {
171		panic(err)
172	}
173
174	set := make(map[string]interface{})
175
176	for key := range configs {
177		res := re.FindStringSubmatch(key)
178
179		if res == nil {
180			continue
181		}
182
183		set[res[1]] = nil
184	}
185
186	result := make([]string, len(set))
187
188	i := 0
189	for key := range set {
190		result[i] = key
191		i++
192	}
193
194	return result, nil
195}
196
197// Check if a bridge exist
198func BridgeExist(repo repository.RepoConfig, name string) bool {
199	keyPrefix := fmt.Sprintf("git-bug.bridge.%s.", name)
200
201	conf, err := repo.LocalConfig().ReadAll(keyPrefix)
202
203	return err == nil && len(conf) > 0
204}
205
206// Remove a configured bridge
207func RemoveBridge(repo repository.RepoConfig, name string) error {
208	re, err := regexp.Compile(`^[a-zA-Z0-9]+`)
209	if err != nil {
210		panic(err)
211	}
212
213	if !re.MatchString(name) {
214		return fmt.Errorf("bad bridge fullname: %s", name)
215	}
216
217	keyPrefix := fmt.Sprintf("git-bug.bridge.%s", name)
218	return repo.LocalConfig().RemoveAll(keyPrefix)
219}
220
221// Configure run the target specific configuration process
222func (b *Bridge) Configure(params BridgeParams) error {
223	conf, err := b.impl.Configure(b.repo, params)
224	if err != nil {
225		return err
226	}
227
228	err = b.impl.ValidateConfig(conf)
229	if err != nil {
230		return fmt.Errorf("invalid configuration: %v", err)
231	}
232
233	b.conf = conf
234	return b.storeConfig(conf)
235}
236
237func (b *Bridge) storeConfig(conf Configuration) error {
238	for key, val := range conf {
239		storeKey := fmt.Sprintf("git-bug.bridge.%s.%s", b.Name, key)
240
241		err := b.repo.LocalConfig().StoreString(storeKey, val)
242		if err != nil {
243			return errors.Wrap(err, "error while storing bridge configuration")
244		}
245	}
246
247	return nil
248}
249
250func (b *Bridge) ensureConfig() error {
251	if b.conf == nil {
252		conf, err := loadConfig(b.repo, b.Name)
253		if err != nil {
254			return err
255		}
256		b.conf = conf
257	}
258
259	return nil
260}
261
262func loadConfig(repo repository.RepoConfig, name string) (Configuration, error) {
263	keyPrefix := fmt.Sprintf("git-bug.bridge.%s.", name)
264
265	pairs, err := repo.LocalConfig().ReadAll(keyPrefix)
266	if err != nil {
267		return nil, errors.Wrap(err, "error while reading bridge configuration")
268	}
269
270	result := make(Configuration, len(pairs))
271	for key, value := range pairs {
272		key := strings.TrimPrefix(key, keyPrefix)
273		result[key] = value
274	}
275
276	return result, nil
277}
278
279func (b *Bridge) getImporter() Importer {
280	if b.importer == nil {
281		b.importer = b.impl.NewImporter()
282	}
283
284	return b.importer
285}
286
287func (b *Bridge) getExporter() Exporter {
288	if b.exporter == nil {
289		b.exporter = b.impl.NewExporter()
290	}
291
292	return b.exporter
293}
294
295func (b *Bridge) ensureImportInit() error {
296	if b.initImportDone {
297		return nil
298	}
299
300	importer := b.getImporter()
301	if importer != nil {
302		err := importer.Init(b.repo, b.conf)
303		if err != nil {
304			return err
305		}
306	}
307
308	b.initImportDone = true
309	return nil
310}
311
312func (b *Bridge) ensureExportInit() error {
313	if b.initExportDone {
314		return nil
315	}
316
317	importer := b.getImporter()
318	if importer != nil {
319		err := importer.Init(b.repo, b.conf)
320		if err != nil {
321			return err
322		}
323	}
324
325	exporter := b.getExporter()
326	if exporter != nil {
327		err := exporter.Init(b.repo, b.conf)
328		if err != nil {
329			return err
330		}
331	}
332
333	b.initExportDone = true
334	return nil
335}
336
337func (b *Bridge) ImportAllSince(ctx context.Context, since time.Time) (<-chan ImportResult, error) {
338	// 5 seconds before the actual start just to be sure.
339	importStartTime := time.Now().Add(-5 * time.Second)
340
341	importer := b.getImporter()
342	if importer == nil {
343		return nil, ErrImportNotSupported
344	}
345
346	err := b.ensureConfig()
347	if err != nil {
348		return nil, err
349	}
350
351	err = b.ensureImportInit()
352	if err != nil {
353		return nil, err
354	}
355
356	events, err := importer.ImportAll(ctx, b.repo, since)
357	if err != nil {
358		return nil, err
359	}
360
361	out := make(chan ImportResult)
362	go func() {
363		defer close(out)
364		noError := true
365
366		// relay all events while checking that everything went well
367		for event := range events {
368			if event.Event == ImportEventError {
369				noError = false
370			}
371			out <- event
372		}
373
374		// store the last import time ONLY if no error happened
375		if noError {
376			key := fmt.Sprintf("git-bug.bridge.%s.lastImportTime", b.Name)
377			err = b.repo.LocalConfig().StoreTimestamp(key, importStartTime)
378		}
379	}()
380
381	return out, nil
382}
383
384func (b *Bridge) ImportAll(ctx context.Context) (<-chan ImportResult, error) {
385	// If possible, restart from the last import time
386	lastImport, err := b.repo.LocalConfig().ReadTimestamp(fmt.Sprintf("git-bug.bridge.%s.lastImportTime", b.Name))
387	if err == nil {
388		return b.ImportAllSince(ctx, lastImport)
389	}
390
391	return b.ImportAllSince(ctx, time.Time{})
392}
393
394func (b *Bridge) ExportAll(ctx context.Context, since time.Time) (<-chan ExportResult, error) {
395	exporter := b.getExporter()
396	if exporter == nil {
397		return nil, ErrExportNotSupported
398	}
399
400	err := b.ensureConfig()
401	if err != nil {
402		return nil, err
403	}
404
405	err = b.ensureExportInit()
406	if err != nil {
407		return nil, err
408	}
409
410	return exporter.ExportAll(ctx, b.repo, since)
411}