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