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