diff options
-rw-r--r-- | bridge/bridges.go | 13 | ||||
-rw-r--r-- | bridge/core/bridge.go | 155 | ||||
-rw-r--r-- | bridge/core/interfaces.go | 29 | ||||
-rw-r--r-- | bridge/github/config.go (renamed from bridge/github/auth.go) | 92 | ||||
-rw-r--r-- | bridge/github/github.go | 28 | ||||
-rw-r--r-- | bridge/main.go | 16 |
6 files changed, 273 insertions, 60 deletions
diff --git a/bridge/bridges.go b/bridge/bridges.go new file mode 100644 index 00000000..c542903c --- /dev/null +++ b/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{}), + } +} diff --git a/bridge/core/bridge.go b/bridge/core/bridge.go index 3ff8404b..3e3b935e 100644 --- a/bridge/core/bridge.go +++ b/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) +} diff --git a/bridge/core/interfaces.go b/bridge/core/interfaces.go new file mode 100644 index 00000000..5336e7a8 --- /dev/null +++ b/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 +} diff --git a/bridge/github/auth.go b/bridge/github/config.go index b721df7f..3b12d3f9 100644 --- a/bridge/github/auth.go +++ b/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) { diff --git a/bridge/github/github.go b/bridge/github/github.go index 0238b4bf..45954e23 100644 --- a/bridge/github/github.go +++ b/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") } diff --git a/bridge/main.go b/bridge/main.go deleted file mode 100644 index e2472691..00000000 --- a/bridge/main.go +++ /dev/null @@ -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) -} |