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