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