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