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