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