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 // owner of the repo (Github)
36 Project string // name of the repo (Github, Launchpad)
37 URL string // complete URL of a repo (Github, Gitlab, Launchpad)
38 BaseURL string // base URL for self-hosted instance ( Gitlab)
39 CredPrefix string // ID prefix of the credential to use (Github, Gitlab)
40 TokenRaw string // pre-existing token to use (Github, Gitlab)
41 Login string // username for the passed credential (Github, Gitlab)
42}
43
44// Bridge is a wrapper around a BridgeImpl that will bind low-level
45// implementation with utility code to provide high-level functions.
46type Bridge struct {
47 Name string
48 repo *cache.RepoCache
49 impl BridgeImpl
50 importer Importer
51 exporter Exporter
52 conf Configuration
53 initImportDone bool
54 initExportDone 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.RepoConfig) ([]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.RepoConfig, 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.RepoConfig, 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.RepoConfig, 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) ensureImportInit() error {
279 if b.initImportDone {
280 return nil
281 }
282
283 importer := b.getImporter()
284 if importer != nil {
285 err := importer.Init(b.repo, b.conf)
286 if err != nil {
287 return err
288 }
289 }
290
291 b.initImportDone = true
292 return nil
293}
294
295func (b *Bridge) ensureExportInit() error {
296 if b.initExportDone {
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 exporter := b.getExporter()
309 if exporter != nil {
310 err := exporter.Init(b.repo, b.conf)
311 if err != nil {
312 return err
313 }
314 }
315
316 b.initExportDone = true
317 return nil
318}
319
320func (b *Bridge) ImportAllSince(ctx context.Context, since time.Time) (<-chan ImportResult, error) {
321 // 5 seconds before the actual start just to be sure.
322 importStartTime := time.Now().Add(-5 * time.Second)
323
324 importer := b.getImporter()
325 if importer == nil {
326 return nil, ErrImportNotSupported
327 }
328
329 err := b.ensureConfig()
330 if err != nil {
331 return nil, err
332 }
333
334 err = b.ensureImportInit()
335 if err != nil {
336 return nil, err
337 }
338
339 events, err := importer.ImportAll(ctx, b.repo, since)
340 if err != nil {
341 return nil, err
342 }
343
344 out := make(chan ImportResult)
345 go func() {
346 defer close(out)
347 noError := true
348
349 // relay all events while checking that everything went well
350 for event := range events {
351 if event.Event == ImportEventError {
352 noError = false
353 }
354 out <- event
355 }
356
357 // store the last import time ONLY if no error happened
358 if noError {
359 key := fmt.Sprintf("git-bug.bridge.%s.lastImportTime", b.Name)
360 err = b.repo.LocalConfig().StoreTimestamp(key, importStartTime)
361 }
362 }()
363
364 return out, nil
365}
366
367func (b *Bridge) ImportAll(ctx context.Context) (<-chan ImportResult, error) {
368 // If possible, restart from the last import time
369 lastImport, err := b.repo.LocalConfig().ReadTimestamp(fmt.Sprintf("git-bug.bridge.%s.lastImportTime", b.Name))
370 if err == nil {
371 return b.ImportAllSince(ctx, lastImport)
372 }
373
374 return b.ImportAllSince(ctx, time.Time{})
375}
376
377func (b *Bridge) ExportAll(ctx context.Context, since time.Time) (<-chan ExportResult, error) {
378 exporter := b.getExporter()
379 if exporter == nil {
380 return nil, ErrExportNotSupported
381 }
382
383 err := b.ensureConfig()
384 if err != nil {
385 return nil, err
386 }
387
388 err = b.ensureExportInit()
389 if err != nil {
390 return nil, err
391 }
392
393 return exporter.ExportAll(ctx, b.repo, since)
394}