bridge: better interfaces, working github configurator

Michael Muré created

Change summary

bridge/bridges.go         |  13 +++
bridge/core/bridge.go     | 155 ++++++++++++++++++++++++++++++++++++----
bridge/core/interfaces.go |  29 +++++++
bridge/github/config.go   |  92 +++++++++++++++++-------
bridge/github/github.go   |  28 +++++++
bridge/main.go            |  16 ----
6 files changed, 273 insertions(+), 60 deletions(-)

Detailed changes

bridge/bridges.go 🔗

@@ -0,0 +1,13 @@
+package bridge
+
+import (
+	"github.com/MichaelMure/git-bug/bridge/core"
+	"github.com/MichaelMure/git-bug/bridge/github"
+)
+
+// Bridges return all known bridges
+func Bridges() []*core.Bridge {
+	return []*core.Bridge{
+		core.NewBridge(&github.Github{}),
+	}
+}

bridge/core/bridge.go 🔗

@@ -1,26 +1,149 @@
 package core
 
-import "github.com/MichaelMure/git-bug/cache"
+import (
+	"fmt"
+	"os/exec"
+	"strings"
 
-type Common interface {
-	// Configure handle the user interaction and return a key/value configuration
-	// for future use
-	Configure() (map[string]string, error)
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/repository"
+	"github.com/pkg/errors"
+)
+
+var ErrImportNorSupported = errors.New("import is not supported")
+var ErrExportNorSupported = errors.New("export is not supported")
+
+// Bridge is a wrapper around a BridgeImpl that will bind low-level
+// implementation with utility code to provide high-level functions.
+type Bridge struct {
+	impl BridgeImpl
+	conf Configuration
 }
 
-type Importer interface {
-	Common
-	ImportAll(repo *cache.RepoCache) error
-	Import(repo *cache.RepoCache, id string) error
+func NewBridge(impl BridgeImpl) *Bridge {
+	return &Bridge{
+		impl: impl,
+	}
 }
 
-type Exporter interface {
-	Common
-	ExportAll(repo *cache.RepoCache) error
-	Export(repo *cache.RepoCache, id string) error
+func (b *Bridge) Configure(repo repository.RepoCommon) error {
+	conf, err := b.impl.Configure(repo)
+	if err != nil {
+		return err
+	}
+
+	return b.storeConfig(repo, conf)
 }
 
-type NotSupportedImporter struct{}
-type NotSupportedExporter struct{}
+func (b *Bridge) storeConfig(repo repository.RepoCommon, 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()
+
+		out, err := cmd.CombinedOutput()
+		if err != nil {
+			return fmt.Errorf("error while storing bridge configuration: %s", out)
+		}
+	}
 
-// persist
+	return nil
+}
+
+func (b Bridge) getConfig(repo repository.RepoCommon) (Configuration, error) {
+	var err error
+	if b.conf == nil {
+		b.conf, err = b.loadConfig(repo)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	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()
+
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		return nil, fmt.Errorf("error while reading bridge configuration: %s", out)
+	}
+
+	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]
+	}
+
+	return result, nil
+}
+
+func (b Bridge) ImportAll(repo *cache.RepoCache) error {
+	importer := b.impl.Importer()
+	if importer == nil {
+		return ErrImportNorSupported
+	}
+
+	conf, err := b.getConfig(repo)
+	if err != nil {
+		return err
+	}
+
+	return b.impl.Importer().ImportAll(repo, conf)
+}
+
+func (b Bridge) Import(repo *cache.RepoCache, id string) error {
+	importer := b.impl.Importer()
+	if importer == nil {
+		return ErrImportNorSupported
+	}
+
+	conf, err := b.getConfig(repo)
+	if err != nil {
+		return err
+	}
+
+	return b.impl.Importer().Import(repo, conf, id)
+}
+
+func (b Bridge) ExportAll(repo *cache.RepoCache) error {
+	exporter := b.impl.Exporter()
+	if exporter == nil {
+		return ErrExportNorSupported
+	}
+
+	conf, err := b.getConfig(repo)
+	if err != nil {
+		return err
+	}
+
+	return b.impl.Exporter().ExportAll(repo, conf)
+}
+
+func (b Bridge) Export(repo *cache.RepoCache, id string) error {
+	exporter := b.impl.Exporter()
+	if exporter == nil {
+		return ErrExportNorSupported
+	}
+
+	conf, err := b.getConfig(repo)
+	if err != nil {
+		return err
+	}
+
+	return b.impl.Exporter().Export(repo, conf, id)
+}

bridge/core/interfaces.go 🔗

@@ -0,0 +1,29 @@
+package core
+
+import (
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/repository"
+)
+
+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
+
+	// Configure handle the user interaction and return a key/value configuration
+	// for future use
+	Configure(repo repository.RepoCommon) (Configuration, error)
+
+	Importer() Importer
+	Exporter() Exporter
+}

bridge/github/auth.go → bridge/github/config.go 🔗

@@ -10,25 +10,36 @@ import (
 	"math/rand"
 	"net/http"
 	"os"
+	"regexp"
 	"strings"
 	"syscall"
 	"time"
 
+	"github.com/MichaelMure/git-bug/bridge/core"
+	"github.com/MichaelMure/git-bug/repository"
 	"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"
+
+func (*Github) Configure(repo repository.RepoCommon) (core.Configuration, error) {
+	conf := make(core.Configuration)
 
-func Configure() (map[string]string, error) {
 	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()
 
-	tokenName, err := promptTokenName()
+	projectUser, projectName, err := promptURL()
 	if err != nil {
 		return nil, err
 	}
 
+	conf[keyUser] = projectUser
+	conf[keyProject] = projectName
+
 	fmt.Println()
 
 	username, err := promptUsername()
@@ -47,12 +58,7 @@ func Configure() (map[string]string, error) {
 
 	// Attempt to authenticate and create a token
 
-	var note string
-	if tokenName == "" {
-		note = "git-bug"
-	} else {
-		note = fmt.Sprintf("git-bug - %s", tokenName)
-	}
+	note := fmt.Sprintf("git-bug - %s/%s", projectUser, projectName)
 
 	resp, err := requestToken(note, username, password)
 	if err != nil {
@@ -61,10 +67,6 @@ func Configure() (map[string]string, error) {
 
 	defer resp.Body.Close()
 
-	if resp.StatusCode == http.StatusCreated {
-		return decodeBody(resp.Body)
-	}
-
 	// Handle 2FA is needed
 	OTPHeader := resp.Header.Get("X-GitHub-OTP")
 	if resp.StatusCode == http.StatusUnauthorized && OTPHeader != "" {
@@ -79,10 +81,15 @@ func Configure() (map[string]string, error) {
 		}
 
 		defer resp.Body.Close()
+	}
 
-		if resp.StatusCode == http.StatusCreated {
-			return decodeBody(resp.Body)
+	if resp.StatusCode == http.StatusCreated {
+		token, err := decodeBody(resp.Body)
+		if err != nil {
+			return nil, err
 		}
+		conf[keyToken] = token
+		return conf, nil
 	}
 
 	b, _ := ioutil.ReadAll(resp.Body)
@@ -129,7 +136,7 @@ func requestTokenWith2FA(note, username, password, otpCode string) (*http.Respon
 	return client.Do(req)
 }
 
-func decodeBody(body io.ReadCloser) (map[string]string, error) {
+func decodeBody(body io.ReadCloser) (string, error) {
 	data, _ := ioutil.ReadAll(body)
 
 	aux := struct {
@@ -138,12 +145,14 @@ func decodeBody(body io.ReadCloser) (map[string]string, error) {
 
 	err := json.Unmarshal(data, &aux)
 	if err != nil {
-		return nil, err
+		return "", err
+	}
+
+	if aux.Token == "" {
+		return "", fmt.Errorf("no token found in response: %s", string(data))
 	}
 
-	return map[string]string{
-		"token": aux.Token,
-	}, nil
+	return aux.Token, nil
 }
 
 func randomFingerprint() string {
@@ -180,17 +189,46 @@ func promptUsername() (string, error) {
 	}
 }
 
-func promptTokenName() (string, error) {
-	fmt.Println("To help distinguish the token, you can optionally provide a description")
-	fmt.Println("The token will be named \"git-bug - <description>\"")
-	fmt.Println("description:")
+func promptURL() (string, string, error) {
+	for {
+		fmt.Println("Github project URL:")
+
+		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
+		if err != nil {
+			return "", "", err
+		}
+
+		line = strings.TrimRight(line, "\n")
 
-	line, err := bufio.NewReader(os.Stdin).ReadString('\n')
+		if line == "" {
+			fmt.Println("URL is empty")
+			continue
+		}
+
+		projectUser, projectName, err := splitURL(line)
+
+		if err != nil {
+			fmt.Println(err)
+			continue
+		}
+
+		return projectUser, projectName, nil
+	}
+}
+
+func splitURL(url string) (string, string, error) {
+	re, err := regexp.Compile(`github\.com\/([^\/]*)\/([^\/]*)`)
 	if err != nil {
-		return "", err
+		return "", "", err
+	}
+
+	res := re.FindStringSubmatch(url)
+
+	if res == nil {
+		return "", "", fmt.Errorf("bad github project url")
 	}
 
-	return strings.TrimRight(line, "\n"), nil
+	return res[1], res[2], nil
 }
 
 func validateUsername(username string) (bool, error) {

bridge/github/github.go 🔗

@@ -1,4 +1,30 @@
 package github
 
-type github struct {
+import (
+	"github.com/MichaelMure/git-bug/bridge/core"
+	"github.com/MichaelMure/git-bug/cache"
+)
+
+type Github struct{}
+
+func (*Github) Name() string {
+	return "github"
+}
+
+func (*Github) Importer() core.Importer {
+	return &githubImporter{}
+}
+
+func (*Github) Exporter() core.Exporter {
+	return nil
+}
+
+type githubImporter struct{}
+
+func (*githubImporter) ImportAll(repo *cache.RepoCache, conf core.Configuration) error {
+	panic("implement me")
+}
+
+func (*githubImporter) Import(repo *cache.RepoCache, conf core.Configuration, id string) error {
+	panic("implement me")
 }

bridge/main.go 🔗

@@ -1,16 +0,0 @@
-package main
-
-import (
-	"fmt"
-
-	"github.com/MichaelMure/git-bug/bridge/github"
-)
-
-func main() {
-	conf, err := github.Configure()
-	if err != nil {
-		fmt.Println(err)
-	}
-
-	fmt.Println(conf)
-}