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