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