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