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