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