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