bridge: big refactor and cleanup

Michael Muré created

Change summary

bridge/bridges.go         |  15 ++-
bridge/core/bridge.go     | 165 +++++++++++++++++++++++++++++-----------
bridge/core/interfaces.go |  26 +++--
bridge/github/config.go   |  21 ++--
bridge/github/github.go   |  18 +++
5 files changed, 167 insertions(+), 78 deletions(-)

Detailed changes

bridge/bridges.go 🔗

@@ -2,12 +2,15 @@ package bridge
 
 import (
 	"github.com/MichaelMure/git-bug/bridge/core"
-	"github.com/MichaelMure/git-bug/bridge/github"
+	_ "github.com/MichaelMure/git-bug/bridge/github"
+	"github.com/MichaelMure/git-bug/repository"
 )
 
-// Bridges return all known bridges
-func Bridges() []*core.Bridge {
-	return []*core.Bridge{
-		core.NewBridge(&github.Github{}),
-	}
+// Targets return all known bridge implementation target
+func Targets() []string {
+	return core.Targets()
+}
+
+func ConfiguredBridges(repo repository.RepoCommon) ([]string, error) {
+	return core.ConfiguredBridges(repo)
 }

bridge/core/bridge.go 🔗

@@ -2,7 +2,8 @@ package core
 
 import (
 	"fmt"
-	"os/exec"
+	"reflect"
+	"regexp"
 	"strings"
 
 	"github.com/MichaelMure/git-bug/cache"
@@ -13,48 +14,130 @@ import (
 var ErrImportNorSupported = errors.New("import is not supported")
 var ErrExportNorSupported = errors.New("export is not supported")
 
+var bridgeImpl map[string]reflect.Type
+
 // Bridge is a wrapper around a BridgeImpl that will bind low-level
 // implementation with utility code to provide high-level functions.
 type Bridge struct {
+	Name string
+	repo *cache.RepoCache
 	impl BridgeImpl
 	conf Configuration
 }
 
-func NewBridge(impl BridgeImpl) *Bridge {
-	return &Bridge{
+// Register will register a new BridgeImpl
+func Register(impl BridgeImpl) {
+	if bridgeImpl == nil {
+		bridgeImpl = make(map[string]reflect.Type)
+	}
+	bridgeImpl[impl.Target()] = reflect.TypeOf(impl)
+}
+
+// Targets return all known bridge implementation target
+func Targets() []string {
+	var result []string
+
+	for key := range bridgeImpl {
+		result = append(result, key)
+	}
+
+	return result
+}
+
+func NewBridge(repo *cache.RepoCache, target string, name string) (*Bridge, error) {
+	implType, ok := bridgeImpl[target]
+	if !ok {
+		return nil, fmt.Errorf("unknown bridge target %v", target)
+	}
+
+	impl := reflect.New(implType).Elem().Interface().(BridgeImpl)
+
+	bridge := &Bridge{
+		Name: name,
+		repo: repo,
 		impl: impl,
 	}
+
+	return bridge, nil
+}
+
+func ConfiguredBridges(repo repository.RepoCommon) ([]string, error) {
+	configs, err := repo.ReadConfigs("git-bug.")
+	if err != nil {
+		return nil, errors.Wrap(err, "can't read configured bridges")
+	}
+
+	re, err := regexp.Compile(`git-bug.([^\.]+\.[^\.]+)`)
+	if err != nil {
+		return nil, err
+	}
+
+	set := make(map[string]interface{})
+
+	for key := range configs {
+		res := re.FindStringSubmatch(key)
+
+		if res == nil {
+			continue
+		}
+
+		set[res[1]] = nil
+	}
+
+	result := make([]string, len(set))
+
+	i := 0
+	for key := range set {
+		result[i] = key
+		i++
+	}
+
+	return result, nil
+}
+
+func (b *Bridge) String() string {
+	var _type string
+	if b.impl.Importer() != nil && b.impl.Exporter() != nil {
+		_type = "import/export"
+	} else if b.impl.Importer() != nil {
+		_type = "import"
+	} else if b.impl.Exporter() != nil {
+		_type = "export"
+	} else {
+		panic("bad bridge impl, neither import nor export")
+	}
+
+	return fmt.Sprintf("%s.%s: %s", b.impl.Target(), b.Name, _type)
 }
 
-func (b *Bridge) Configure(repo repository.RepoCommon) error {
-	conf, err := b.impl.Configure(repo)
+func (b *Bridge) Configure() error {
+	conf, err := b.impl.Configure(b.repo)
 	if err != nil {
 		return err
 	}
 
-	return b.storeConfig(repo, conf)
+	b.conf = conf
+
+	return b.storeConfig(conf)
 }
 
-func (b *Bridge) storeConfig(repo repository.RepoCommon, conf Configuration) error {
+func (b *Bridge) storeConfig(conf Configuration) error {
 	for key, val := range conf {
-		storeKey := fmt.Sprintf("git-bug.%s.%s", b.impl.Name(), key)
-
-		cmd := exec.Command("git", "config", "--replace-all", storeKey, val)
-		cmd.Dir = repo.GetPath()
+		storeKey := fmt.Sprintf("git-bug.%s.%s.%s", b.impl.Target(), b.Name, key)
 
-		out, err := cmd.CombinedOutput()
+		err := b.repo.StoreConfig(storeKey, val)
 		if err != nil {
-			return fmt.Errorf("error while storing bridge configuration: %s", out)
+			return errors.Wrap(err, "error while storing bridge configuration")
 		}
 	}
 
 	return nil
 }
 
-func (b Bridge) getConfig(repo repository.RepoCommon) (Configuration, error) {
+func (b Bridge) getConfig() (Configuration, error) {
 	var err error
 	if b.conf == nil {
-		b.conf, err = b.loadConfig(repo)
+		b.conf, err = b.loadConfig()
 		if err != nil {
 			return nil, err
 		}
@@ -63,87 +146,75 @@ func (b Bridge) getConfig(repo repository.RepoCommon) (Configuration, error) {
 	return b.conf, nil
 }
 
-func (b Bridge) loadConfig(repo repository.RepoCommon) (Configuration, error) {
-	key := fmt.Sprintf("git-bug.%s", b.impl.Name())
-	cmd := exec.Command("git", "config", "--get-regexp", key)
-	cmd.Dir = repo.GetPath()
+func (b Bridge) loadConfig() (Configuration, error) {
+	keyPrefix := fmt.Sprintf("git-bug.%s.%s.", b.impl.Target(), b.Name)
 
-	out, err := cmd.CombinedOutput()
+	pairs, err := b.repo.ReadConfigs(keyPrefix)
 	if err != nil {
-		return nil, fmt.Errorf("error while reading bridge configuration: %s", out)
+		return nil, errors.Wrap(err, "error while reading bridge configuration")
 	}
 
-	lines := strings.Split(string(out), "\n")
-
-	result := make(Configuration, len(lines))
-	for _, line := range lines {
-		if strings.TrimSpace(line) == "" {
-			continue
-		}
-
-		parts := strings.Fields(line)
-		if len(parts) != 2 {
-			return nil, fmt.Errorf("bad bridge configuration: %s", line)
-		}
-
-		result[parts[0]] = parts[1]
+	result := make(Configuration, len(pairs))
+	for key, value := range pairs {
+		key := strings.TrimPrefix(key, keyPrefix)
+		result[key] = value
 	}
 
 	return result, nil
 }
 
-func (b Bridge) ImportAll(repo *cache.RepoCache) error {
+func (b Bridge) ImportAll() error {
 	importer := b.impl.Importer()
 	if importer == nil {
 		return ErrImportNorSupported
 	}
 
-	conf, err := b.getConfig(repo)
+	conf, err := b.getConfig()
 	if err != nil {
 		return err
 	}
 
-	return b.impl.Importer().ImportAll(repo, conf)
+	return b.impl.Importer().ImportAll(b.repo, conf)
 }
 
-func (b Bridge) Import(repo *cache.RepoCache, id string) error {
+func (b Bridge) Import(id string) error {
 	importer := b.impl.Importer()
 	if importer == nil {
 		return ErrImportNorSupported
 	}
 
-	conf, err := b.getConfig(repo)
+	conf, err := b.getConfig()
 	if err != nil {
 		return err
 	}
 
-	return b.impl.Importer().Import(repo, conf, id)
+	return b.impl.Importer().Import(b.repo, conf, id)
 }
 
-func (b Bridge) ExportAll(repo *cache.RepoCache) error {
+func (b Bridge) ExportAll() error {
 	exporter := b.impl.Exporter()
 	if exporter == nil {
 		return ErrExportNorSupported
 	}
 
-	conf, err := b.getConfig(repo)
+	conf, err := b.getConfig()
 	if err != nil {
 		return err
 	}
 
-	return b.impl.Exporter().ExportAll(repo, conf)
+	return b.impl.Exporter().ExportAll(b.repo, conf)
 }
 
-func (b Bridge) Export(repo *cache.RepoCache, id string) error {
+func (b Bridge) Export(id string) error {
 	exporter := b.impl.Exporter()
 	if exporter == nil {
 		return ErrExportNorSupported
 	}
 
-	conf, err := b.getConfig(repo)
+	conf, err := b.getConfig()
 	if err != nil {
 		return err
 	}
 
-	return b.impl.Exporter().Export(repo, conf, id)
+	return b.impl.Exporter().Export(b.repo, conf, id)
 }

bridge/core/interfaces.go 🔗

@@ -7,23 +7,27 @@ import (
 
 type Configuration map[string]string
 
-type Importer interface {
-	ImportAll(repo *cache.RepoCache, conf Configuration) error
-	Import(repo *cache.RepoCache, conf Configuration, id string) error
-}
-
-type Exporter interface {
-	ExportAll(repo *cache.RepoCache, conf Configuration) error
-	Export(repo *cache.RepoCache, conf Configuration, id string) error
-}
-
 type BridgeImpl interface {
-	Name() string
+	// Target return the target of the bridge (e.g.: "github")
+	Target() string
 
 	// Configure handle the user interaction and return a key/value configuration
 	// for future use
 	Configure(repo repository.RepoCommon) (Configuration, error)
 
+	// Importer return an Importer implementation if the import is supported
 	Importer() Importer
+
+	// Exporter return an Exporter implementation if the export is supported
 	Exporter() Exporter
 }
+
+type Importer interface {
+	ImportAll(repo *cache.RepoCache, conf Configuration) error
+	Import(repo *cache.RepoCache, conf Configuration, id string) error
+}
+
+type Exporter interface {
+	ExportAll(repo *cache.RepoCache, conf Configuration) error
+	Export(repo *cache.RepoCache, conf Configuration, id string) error
+}

bridge/github/config.go 🔗

@@ -20,7 +20,7 @@ import (
 	"golang.org/x/crypto/ssh/terminal"
 )
 
-const githubV3Url = "https://api.Github.com"
+const githubV3Url = "https://api.github.com"
 const keyUser = "user"
 const keyProject = "project"
 const keyToken = "token"
@@ -28,6 +28,7 @@ const keyToken = "token"
 func (*Github) Configure(repo repository.RepoCommon) (core.Configuration, error) {
 	conf := make(core.Configuration)
 
+	fmt.Println()
 	fmt.Println("git-bug will generate an access token in your Github profile.")
 	// fmt.Println("The token will have the \"repo\" permission, giving it read/write access to your repositories and issues. There is no narrower scope available, sorry :-|")
 	fmt.Println()
@@ -40,22 +41,16 @@ func (*Github) Configure(repo repository.RepoCommon) (core.Configuration, error)
 	conf[keyUser] = projectUser
 	conf[keyProject] = projectName
 
-	fmt.Println()
-
 	username, err := promptUsername()
 	if err != nil {
 		return nil, err
 	}
 
-	fmt.Println()
-
 	password, err := promptPassword()
 	if err != nil {
 		return nil, err
 	}
 
-	fmt.Println()
-
 	// Attempt to authenticate and create a token
 
 	note := fmt.Sprintf("git-bug - %s/%s", projectUser, projectName)
@@ -168,7 +163,7 @@ func randomFingerprint() string {
 
 func promptUsername() (string, error) {
 	for {
-		fmt.Println("username:")
+		fmt.Print("username: ")
 
 		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
 		if err != nil {
@@ -191,7 +186,7 @@ func promptUsername() (string, error) {
 
 func promptURL() (string, string, error) {
 	for {
-		fmt.Println("Github project URL:")
+		fmt.Print("Github project URL: ")
 
 		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
 		if err != nil {
@@ -249,9 +244,13 @@ func validateUsername(username string) (bool, error) {
 
 func promptPassword() (string, error) {
 	for {
-		fmt.Println("password:")
+		fmt.Print("password: ")
 
 		bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
+		// new line for coherent formatting, ReadPassword clip the normal new line
+		// entered by the user
+		fmt.Println()
+
 		if err != nil {
 			return "", err
 		}
@@ -266,7 +265,7 @@ func promptPassword() (string, error) {
 
 func prompt2FA() (string, error) {
 	for {
-		fmt.Println("two-factor authentication code:")
+		fmt.Print("two-factor authentication code: ")
 
 		byte2fa, err := terminal.ReadPassword(int(syscall.Stdin))
 		if err != nil {

bridge/github/github.go 🔗

@@ -1,13 +1,19 @@
 package github
 
 import (
+	"fmt"
+
 	"github.com/MichaelMure/git-bug/bridge/core"
 	"github.com/MichaelMure/git-bug/cache"
 )
 
+func init() {
+	core.Register(&Github{})
+}
+
 type Github struct{}
 
-func (*Github) Name() string {
+func (*Github) Target() string {
 	return "github"
 }
 
@@ -22,9 +28,15 @@ func (*Github) Exporter() core.Exporter {
 type githubImporter struct{}
 
 func (*githubImporter) ImportAll(repo *cache.RepoCache, conf core.Configuration) error {
-	panic("implement me")
+	fmt.Println(conf)
+	fmt.Println("IMPORT ALL")
+
+	return nil
 }
 
 func (*githubImporter) Import(repo *cache.RepoCache, conf core.Configuration, id string) error {
-	panic("implement me")
+	fmt.Println(conf)
+	fmt.Println("IMPORT")
+
+	return nil
 }