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