aboutsummaryrefslogtreecommitdiffstats
path: root/bridge
diff options
context:
space:
mode:
authorMichael Muré <batolettre@gmail.com>2019-06-07 02:53:21 +0200
committerGitHub <noreply@github.com>2019-06-07 02:53:21 +0200
commitd69dcce806d280ddbd6a4fb17700153bc03da90b (patch)
treeda07a7beadc77b3fe5e99f09bd38d919566a0191 /bridge
parenta9629dbad15f0e80ea244eb81abda4ddc08f7a0e (diff)
parent1c2ad95960c09d029e6306ac5a5ea76c58e8b5c9 (diff)
downloadgit-bug-d69dcce806d280ddbd6a4fb17700153bc03da90b.tar.gz
Merge pull request #153 from A-Hilaly/bridge-configuration
[Breaking] Bridge configuration enhancements
Diffstat (limited to 'bridge')
-rw-r--r--bridge/core/bridge.go13
-rw-r--r--bridge/core/interfaces.go2
-rw-r--r--bridge/github/config.go406
-rw-r--r--bridge/github/config_test.go209
-rw-r--r--bridge/github/github.go3
-rw-r--r--bridge/github/import.go5
-rw-r--r--bridge/github/import_test.go10
-rw-r--r--bridge/github/iterator.go8
-rw-r--r--bridge/launchpad/config.go79
-rw-r--r--bridge/launchpad/config_test.go93
10 files changed, 734 insertions, 94 deletions
diff --git a/bridge/core/bridge.go b/bridge/core/bridge.go
index 42730bf6..3513b790 100644
--- a/bridge/core/bridge.go
+++ b/bridge/core/bridge.go
@@ -21,6 +21,15 @@ const bridgeConfigKeyPrefix = "git-bug.bridge"
var bridgeImpl map[string]reflect.Type
+// BridgeParams holds parameters to simplify the bridge configuration without
+// having to make terminal prompts.
+type BridgeParams struct {
+ Owner string
+ Project string
+ URL string
+ Token string
+}
+
// Bridge is a wrapper around a BridgeImpl that will bind low-level
// implementation with utility code to provide high-level functions.
type Bridge struct {
@@ -169,8 +178,8 @@ func RemoveBridge(repo repository.RepoCommon, fullName string) error {
}
// Configure run the target specific configuration process
-func (b *Bridge) Configure() error {
- conf, err := b.impl.Configure(b.repo)
+func (b *Bridge) Configure(params BridgeParams) error {
+ conf, err := b.impl.Configure(b.repo, params)
if err != nil {
return err
}
diff --git a/bridge/core/interfaces.go b/bridge/core/interfaces.go
index be5afa62..37fdb3d7 100644
--- a/bridge/core/interfaces.go
+++ b/bridge/core/interfaces.go
@@ -15,7 +15,7 @@ type BridgeImpl interface {
// Configure handle the user interaction and return a key/value configuration
// for future use
- Configure(repo repository.RepoCommon) (Configuration, error)
+ Configure(repo repository.RepoCommon, params BridgeParams) (Configuration, error)
// ValidateConfig check the configuration for error
ValidateConfig(conf Configuration) error
diff --git a/bridge/github/config.go b/bridge/github/config.go
index 2a3119a6..707b3e2f 100644
--- a/bridge/github/config.go
+++ b/bridge/github/config.go
@@ -11,91 +11,101 @@ import (
"net/http"
"os"
"regexp"
+ "strconv"
"strings"
"syscall"
"time"
+ "github.com/pkg/errors"
+
+ "golang.org/x/crypto/ssh/terminal"
+
"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"
- keyUser = "user"
+ keyOwner = "owner"
keyProject = "project"
keyToken = "token"
-)
-func (*Github) Configure(repo repository.RepoCommon) (core.Configuration, error) {
- conf := make(core.Configuration)
+ defaultTimeout = 60 * time.Second
+)
- fmt.Println()
- fmt.Println("git-bug will now generate an access token in your Github profile. Your credential are not stored and are only used to generate the token. The token is stored in the repository git config.")
- fmt.Println()
- fmt.Println("The token will have the following scopes:")
- fmt.Println(" - user:email: to be able to read public-only users email")
- // 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()
+var (
+ ErrBadProjectURL = errors.New("bad project url")
+)
- projectUser, projectName, err := promptURL()
- if err != nil {
- return nil, err
- }
+func (*Github) Configure(repo repository.RepoCommon, params core.BridgeParams) (core.Configuration, error) {
+ conf := make(core.Configuration)
+ var err error
+ var token string
+ var owner string
+ var project string
+
+ // getting owner and project name
+ if params.Owner != "" && params.Project != "" {
+ // first try to use params if both or project and owner are provided
+ owner = params.Owner
+ project = params.Project
+
+ } else if params.URL != "" {
+ // try to parse params URL and extract owner and project
+ owner, project, err = splitURL(params.URL)
+ if err != nil {
+ return nil, err
+ }
- conf[keyUser] = projectUser
- conf[keyProject] = projectName
+ } else {
+ // remote suggestions
+ remotes, err := repo.GetRemotes()
+ if err != nil {
+ return nil, err
+ }
- username, err := promptUsername()
- if err != nil {
- return nil, err
+ // terminal prompt
+ owner, project, err = promptURL(remotes)
+ if err != nil {
+ return nil, err
+ }
}
- password, err := promptPassword()
+ // validate project owner
+ ok, err := validateUsername(owner)
if err != nil {
return nil, err
}
-
- // Attempt to authenticate and create a token
-
- note := fmt.Sprintf("git-bug - %s/%s", projectUser, projectName)
-
- resp, err := requestToken(note, username, password)
- if err != nil {
- return nil, err
+ if !ok {
+ return nil, fmt.Errorf("invalid parameter owner: %v", owner)
}
- defer resp.Body.Close()
-
- // Handle 2FA is needed
- OTPHeader := resp.Header.Get("X-GitHub-OTP")
- if resp.StatusCode == http.StatusUnauthorized && OTPHeader != "" {
- otpCode, err := prompt2FA()
- if err != nil {
- return nil, err
- }
+ // try to get token from params if provided, else use terminal prompt to either
+ // enter a token or login and generate a new one
+ if params.Token != "" {
+ token = params.Token
- resp, err = requestTokenWith2FA(note, username, password, otpCode)
+ } else {
+ token, err = promptTokenOptions(owner, project)
if err != nil {
return nil, err
}
-
- defer resp.Body.Close()
}
- if resp.StatusCode == http.StatusCreated {
- token, err := decodeBody(resp.Body)
- if err != nil {
- return nil, err
- }
- conf[keyToken] = token
- return conf, nil
+ // verify access to the repository with token
+ ok, err = validateProject(owner, project, token)
+ if err != nil {
+ return nil, err
+ }
+ if !ok {
+ return nil, fmt.Errorf("project doesn't exist or authentication token has an incorrect scope")
}
- b, _ := ioutil.ReadAll(resp.Body)
- fmt.Printf("Error %v: %v\n", resp.StatusCode, string(b))
+ conf[keyToken] = token
+ conf[keyOwner] = owner
+ conf[keyProject] = project
- return nil, nil
+ return conf, nil
}
func (*Github) ValidateConfig(conf core.Configuration) error {
@@ -103,8 +113,8 @@ func (*Github) ValidateConfig(conf core.Configuration) error {
return fmt.Errorf("missing %s key", keyToken)
}
- if _, ok := conf[keyUser]; !ok {
- return fmt.Errorf("missing %s key", keyUser)
+ if _, ok := conf[keyOwner]; !ok {
+ return fmt.Errorf("missing %s key", keyOwner)
}
if _, ok := conf[keyProject]; !ok {
@@ -114,20 +124,18 @@ func (*Github) ValidateConfig(conf core.Configuration) error {
return nil
}
-func requestToken(note, username, password string) (*http.Response, error) {
- return requestTokenWith2FA(note, username, password, "")
+func requestToken(note, username, password string, scope string) (*http.Response, error) {
+ return requestTokenWith2FA(note, username, password, "", scope)
}
-func requestTokenWith2FA(note, username, password, otpCode string) (*http.Response, error) {
+func requestTokenWith2FA(note, username, password, otpCode string, scope string) (*http.Response, error) {
url := fmt.Sprintf("%s/authorizations", githubV3Url)
params := struct {
Scopes []string `json:"scopes"`
Note string `json:"note"`
Fingerprint string `json:"fingerprint"`
}{
- // user:email is requested to be able to read public emails
- // - a private email will stay private, even with this token
- Scopes: []string{"user:email"},
+ Scopes: []string{scope},
Note: note,
Fingerprint: randomFingerprint(),
}
@@ -149,7 +157,9 @@ func requestTokenWith2FA(note, username, password, otpCode string) (*http.Respon
req.Header.Set("X-GitHub-OTP", otpCode)
}
- client := http.Client{}
+ client := &http.Client{
+ Timeout: defaultTimeout,
+ }
return client.Do(req)
}
@@ -184,6 +194,139 @@ func randomFingerprint() string {
return string(b)
}
+func promptTokenOptions(owner, project string) (string, error) {
+ for {
+ fmt.Println()
+ fmt.Println("[1]: user provided token")
+ fmt.Println("[2]: interactive token creation")
+ fmt.Print("Select option: ")
+
+ line, err := bufio.NewReader(os.Stdin).ReadString('\n')
+ fmt.Println()
+ if err != nil {
+ return "", err
+ }
+
+ line = strings.TrimRight(line, "\n")
+
+ index, err := strconv.Atoi(line)
+ if err != nil || (index != 1 && index != 2) {
+ fmt.Println("invalid input")
+ continue
+ }
+
+ if index == 1 {
+ return promptToken()
+ }
+
+ return loginAndRequestToken(owner, project)
+ }
+}
+
+func promptToken() (string, error) {
+ fmt.Println("You can generate a new token by visiting https://github.com/settings/tokens.")
+ fmt.Println("Choose 'Generate new token' and set the necessary access scope for your repository.")
+ fmt.Println()
+ fmt.Println("The access scope depend on the type of repository.")
+ fmt.Println("Public:")
+ fmt.Println(" - 'public_repo': to be able to read public repositories")
+ fmt.Println("Private:")
+ fmt.Println(" - 'repo' : to be able to read private repositories")
+ fmt.Println()
+
+ re, err := regexp.Compile(`^[a-zA-Z0-9]{40}`)
+ if err != nil {
+ panic("regexp compile:" + err.Error())
+ }
+
+ for {
+ fmt.Print("Enter token: ")
+
+ line, err := bufio.NewReader(os.Stdin).ReadString('\n')
+ if err != nil {
+ return "", err
+ }
+
+ token := strings.TrimRight(line, "\n")
+ if re.MatchString(token) {
+ return token, nil
+ }
+
+ fmt.Println("token is invalid")
+ }
+}
+
+func loginAndRequestToken(owner, project string) (string, error) {
+ fmt.Println("git-bug will now generate an access token in your Github profile. Your credential are not stored and are only used to generate the token. The token is stored in the repository git config.")
+ fmt.Println()
+ fmt.Println("The access scope depend on the type of repository.")
+ fmt.Println("Public:")
+ fmt.Println(" - 'public_repo': to be able to read public repositories")
+ fmt.Println("Private:")
+ fmt.Println(" - 'repo' : to be able to read private repositories")
+ fmt.Println()
+
+ // prompt project visibility to know the token scope needed for the repository
+ isPublic, err := promptProjectVisibility()
+ if err != nil {
+ return "", err
+ }
+
+ username, err := promptUsername()
+ if err != nil {
+ return "", err
+ }
+
+ password, err := promptPassword()
+ if err != nil {
+ return "", err
+ }
+
+ var scope string
+ if isPublic {
+ // public_repo is requested to be able to read public repositories
+ scope = "public_repo"
+ } else {
+ // 'repo' is request to be able to read private repositories
+ // /!\ token will have read/write rights on every private repository you have access to
+ scope = "repo"
+ }
+
+ // Attempt to authenticate and create a token
+
+ note := fmt.Sprintf("git-bug - %s/%s", owner, project)
+
+ resp, err := requestToken(note, username, password, scope)
+ if err != nil {
+ return "", err
+ }
+
+ defer resp.Body.Close()
+
+ // Handle 2FA is needed
+ OTPHeader := resp.Header.Get("X-GitHub-OTP")
+ if resp.StatusCode == http.StatusUnauthorized && OTPHeader != "" {
+ otpCode, err := prompt2FA()
+ if err != nil {
+ return "", err
+ }
+
+ resp, err = requestTokenWith2FA(note, username, password, otpCode, scope)
+ if err != nil {
+ return "", err
+ }
+
+ defer resp.Body.Close()
+ }
+
+ if resp.StatusCode == http.StatusCreated {
+ return decodeBody(resp.Body)
+ }
+
+ b, _ := ioutil.ReadAll(resp.Body)
+ return "", fmt.Errorf("error creating token %v: %v", resp.StatusCode, string(b))
+}
+
func promptUsername() (string, error) {
for {
fmt.Print("username: ")
@@ -207,7 +350,45 @@ func promptUsername() (string, error) {
}
}
-func promptURL() (string, string, error) {
+func promptURL(remotes map[string]string) (string, string, error) {
+ validRemotes := getValidGithubRemoteURLs(remotes)
+ if len(validRemotes) > 0 {
+ for {
+ fmt.Println("\nDetected projects:")
+
+ // print valid remote github urls
+ for i, remote := range validRemotes {
+ fmt.Printf("[%d]: %v\n", i+1, remote)
+ }
+
+ fmt.Printf("\n[0]: Another project\n\n")
+ fmt.Printf("Select option: ")
+
+ line, err := bufio.NewReader(os.Stdin).ReadString('\n')
+ if err != nil {
+ return "", "", err
+ }
+
+ line = strings.TrimRight(line, "\n")
+
+ index, err := strconv.Atoi(line)
+ if err != nil || (index < 0 && index >= len(validRemotes)) {
+ fmt.Println("invalid input")
+ continue
+ }
+
+ // if user want to enter another project url break this loop
+ if index == 0 {
+ break
+ }
+
+ // get owner and project with index
+ owner, project, _ := splitURL(validRemotes[index-1])
+ return owner, project, nil
+ }
+ }
+
+ // manually enter github url
for {
fmt.Print("Github project URL: ")
@@ -217,42 +398,93 @@ func promptURL() (string, string, error) {
}
line = strings.TrimRight(line, "\n")
-
if line == "" {
fmt.Println("URL is empty")
continue
}
- projectUser, projectName, err := splitURL(line)
-
+ // get owner and project from url
+ owner, project, err := splitURL(line)
if err != nil {
fmt.Println(err)
continue
}
- return projectUser, projectName, nil
+ return owner, project, nil
}
}
-func splitURL(url string) (string, string, error) {
- re, err := regexp.Compile(`github\.com\/([^\/]*)\/([^\/]*)`)
+// splitURL extract the owner and project from a github repository URL. It will remove the
+// '.git' extension from the URL before parsing it.
+// Note that Github removes the '.git' extension from projects names at their creation
+func splitURL(url string) (owner string, project string, err error) {
+ cleanURL := strings.TrimSuffix(url, ".git")
+
+ re, err := regexp.Compile(`github\.com[/:]([a-zA-Z0-9\-_]+)/([a-zA-Z0-9\-_.]+)`)
if err != nil {
- panic(err)
+ panic("regexp compile:" + err.Error())
}
- res := re.FindStringSubmatch(url)
-
+ res := re.FindStringSubmatch(cleanURL)
if res == nil {
- return "", "", fmt.Errorf("bad github project url")
+ return "", "", ErrBadProjectURL
+ }
+
+ owner = res[1]
+ project = res[2]
+ return
+}
+
+func getValidGithubRemoteURLs(remotes map[string]string) []string {
+ urls := make([]string, 0, len(remotes))
+ for _, url := range remotes {
+ // split url can work again with shortURL
+ owner, project, err := splitURL(url)
+ if err == nil {
+ shortURL := fmt.Sprintf("%s/%s/%s", "github.com", owner, project)
+ urls = append(urls, shortURL)
+ }
}
- return res[1], res[2], nil
+ return urls
}
func validateUsername(username string) (bool, error) {
url := fmt.Sprintf("%s/users/%s", githubV3Url, username)
- resp, err := http.Get(url)
+ client := &http.Client{
+ Timeout: defaultTimeout,
+ }
+
+ resp, err := client.Get(url)
+ if err != nil {
+ return false, err
+ }
+
+ err = resp.Body.Close()
+ if err != nil {
+ return false, err
+ }
+
+ return resp.StatusCode == http.StatusOK, nil
+}
+
+func validateProject(owner, project, token string) (bool, error) {
+ url := fmt.Sprintf("%s/repos/%s/%s", githubV3Url, owner, project)
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return false, err
+ }
+
+ // need the token for private repositories
+ req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
+
+ client := &http.Client{
+ Timeout: defaultTimeout,
+ }
+
+ resp, err := client.Do(req)
if err != nil {
return false, err
}
@@ -291,6 +523,7 @@ func prompt2FA() (string, error) {
fmt.Print("two-factor authentication code: ")
byte2fa, err := terminal.ReadPassword(int(syscall.Stdin))
+ fmt.Println()
if err != nil {
return "", err
}
@@ -302,3 +535,28 @@ func prompt2FA() (string, error) {
fmt.Println("code is empty")
}
}
+
+func promptProjectVisibility() (bool, error) {
+ for {
+ fmt.Println("[1]: public")
+ fmt.Println("[2]: private")
+ fmt.Print("repository visibility: ")
+
+ line, err := bufio.NewReader(os.Stdin).ReadString('\n')
+ fmt.Println()
+ if err != nil {
+ return false, err
+ }
+
+ line = strings.TrimRight(line, "\n")
+
+ index, err := strconv.Atoi(line)
+ if err != nil || (index != 0 && index != 1) {
+ fmt.Println("invalid input")
+ continue
+ }
+
+ // return true for public repositories, false for private
+ return index == 0, nil
+ }
+}
diff --git a/bridge/github/config_test.go b/bridge/github/config_test.go
new file mode 100644
index 00000000..4feeaa74
--- /dev/null
+++ b/bridge/github/config_test.go
@@ -0,0 +1,209 @@
+package github
+
+import (
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSplitURL(t *testing.T) {
+ type args struct {
+ url string
+ }
+ type want struct {
+ owner string
+ project string
+ err error
+ }
+ tests := []struct {
+ name string
+ args args
+ want want
+ }{
+ {
+ name: "default url",
+ args: args{
+ url: "https://github.com/MichaelMure/git-bug",
+ },
+ want: want{
+ owner: "MichaelMure",
+ project: "git-bug",
+ err: nil,
+ },
+ },
+ {
+ name: "default issues url",
+ args: args{
+ url: "https://github.com/MichaelMure/git-bug/issues",
+ },
+ want: want{
+ owner: "MichaelMure",
+ project: "git-bug",
+ err: nil,
+ },
+ },
+ {
+ name: "default url with git extension",
+ args: args{
+ url: "https://github.com/MichaelMure/git-bug.git",
+ },
+ want: want{
+ owner: "MichaelMure",
+ project: "git-bug",
+ err: nil,
+ },
+ },
+ {
+ name: "url with git protocol",
+ args: args{
+ url: "git://github.com/MichaelMure/git-bug.git",
+ },
+ want: want{
+ owner: "MichaelMure",
+ project: "git-bug",
+ err: nil,
+ },
+ },
+ {
+ name: "ssh url",
+ args: args{
+ url: "git@github.com:MichaelMure/git-bug.git",
+ },
+ want: want{
+ owner: "MichaelMure",
+ project: "git-bug",
+ err: nil,
+ },
+ },
+ {
+ name: "bad url",
+ args: args{
+ url: "https://githb.com/MichaelMure/git-bug.git",
+ },
+ want: want{
+ err: ErrBadProjectURL,
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ owner, project, err := splitURL(tt.args.url)
+ assert.Equal(t, tt.want.err, err)
+ assert.Equal(t, tt.want.owner, owner)
+ assert.Equal(t, tt.want.project, project)
+ })
+ }
+}
+
+func TestValidateUsername(t *testing.T) {
+ if env := os.Getenv("TRAVIS"); env == "true" {
+ t.Skip("Travis environment: avoiding non authenticated requests")
+ }
+
+ type args struct {
+ username string
+ }
+ tests := []struct {
+ name string
+ args args
+ want bool
+ }{
+ {
+ name: "existing username",
+ args: args{
+ username: "MichaelMure",
+ },
+ want: true,
+ },
+ {
+ name: "existing organisation name",
+ args: args{
+ username: "ipfs",
+ },
+ want: true,
+ },
+ {
+ name: "non existing username",
+ args: args{
+ username: "cant-find-this",
+ },
+ want: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ok, _ := validateUsername(tt.args.username)
+ assert.Equal(t, tt.want, ok)
+ })
+ }
+}
+
+func TestValidateProject(t *testing.T) {
+ tokenPrivateScope := os.Getenv("GITHUB_TOKEN_PRIVATE")
+ if tokenPrivateScope == "" {
+ t.Skip("Env var GITHUB_TOKEN_PRIVATE missing")
+ }
+
+ tokenPublicScope := os.Getenv("GITHUB_TOKEN_PUBLIC")
+ if tokenPublicScope == "" {
+ t.Skip("Env var GITHUB_TOKEN_PUBLIC missing")
+ }
+
+ type args struct {
+ owner string
+ project string
+ token string
+ }
+ tests := []struct {
+ name string
+ args args
+ want bool
+ }{
+ {
+ name: "public repository and token with scope 'public_repo'",
+ args: args{
+ project: "git-bug",
+ owner: "MichaelMure",
+ token: tokenPublicScope,
+ },
+ want: true,
+ },
+ {
+ name: "private repository and token with scope 'repo'",
+ args: args{
+ project: "git-bug-test-github-bridge",
+ owner: "MichaelMure",
+ token: tokenPrivateScope,
+ },
+ want: true,
+ },
+ {
+ name: "private repository and token with scope 'public_repo'",
+ args: args{
+ project: "git-bug-test-github-bridge",
+ owner: "MichaelMure",
+ token: tokenPublicScope,
+ },
+ want: false,
+ },
+ {
+ name: "project not existing",
+ args: args{
+ project: "cant-find-this",
+ owner: "organisation-not-found",
+ token: tokenPublicScope,
+ },
+ want: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ok, _ := validateProject(tt.args.owner, tt.args.project, tt.args.token)
+ assert.Equal(t, tt.want, ok)
+ })
+ }
+}
diff --git a/bridge/github/github.go b/bridge/github/github.go
index 5fee7487..3e717ee9 100644
--- a/bridge/github/github.go
+++ b/bridge/github/github.go
@@ -4,9 +4,10 @@ package github
import (
"context"
- "github.com/MichaelMure/git-bug/bridge/core"
"github.com/shurcooL/githubv4"
"golang.org/x/oauth2"
+
+ "github.com/MichaelMure/git-bug/bridge/core"
)
func init() {
diff --git a/bridge/github/import.go b/bridge/github/import.go
index 2b9e5561..edb97c4f 100644
--- a/bridge/github/import.go
+++ b/bridge/github/import.go
@@ -5,13 +5,14 @@ import (
"fmt"
"time"
+ "github.com/shurcooL/githubv4"
+
"github.com/MichaelMure/git-bug/bridge/core"
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/util/git"
"github.com/MichaelMure/git-bug/util/text"
- "github.com/shurcooL/githubv4"
)
const (
@@ -42,7 +43,7 @@ func (gi *githubImporter) Init(conf core.Configuration) error {
// ImportAll iterate over all the configured repository issues and ensure the creation of the
// missing issues / timeline items / edits / label events ...
func (gi *githubImporter) ImportAll(repo *cache.RepoCache, since time.Time) error {
- gi.iterator = NewIterator(gi.conf[keyUser], gi.conf[keyProject], gi.conf[keyToken], since)
+ gi.iterator = NewIterator(gi.conf[keyOwner], gi.conf[keyProject], gi.conf[keyToken], since)
// Loop over all matching issues
for gi.iterator.NextIssue() {
diff --git a/bridge/github/import_test.go b/bridge/github/import_test.go
index 1e31501b..24356f34 100644
--- a/bridge/github/import_test.go
+++ b/bridge/github/import_test.go
@@ -133,16 +133,16 @@ func Test_Importer(t *testing.T) {
defer backend.Close()
interrupt.RegisterCleaner(backend.Close)
- token := os.Getenv("GITHUB_TOKEN")
+ token := os.Getenv("GITHUB_TOKEN_PRIVATE")
if token == "" {
- t.Skip("Env var GITHUB_TOKEN missing")
+ t.Skip("Env var GITHUB_TOKEN_PRIVATE missing")
}
importer := &githubImporter{}
err = importer.Init(core.Configuration{
- "user": "MichaelMure",
- "project": "git-bug-test-github-bridge",
- "token": token,
+ keyOwner: "MichaelMure",
+ keyProject: "git-bug-test-github-bridge",
+ keyToken: token,
})
require.NoError(t, err)
diff --git a/bridge/github/iterator.go b/bridge/github/iterator.go
index 5935276a..fcf72b8f 100644
--- a/bridge/github/iterator.go
+++ b/bridge/github/iterator.go
@@ -60,7 +60,7 @@ type iterator struct {
}
// NewIterator create and initalize a new iterator
-func NewIterator(user, project, token string, since time.Time) *iterator {
+func NewIterator(owner, project, token string, since time.Time) *iterator {
i := &iterator{
gc: buildClient(token),
since: since,
@@ -70,21 +70,21 @@ func NewIterator(user, project, token string, since time.Time) *iterator {
issueEdit: indexer{-1},
commentEdit: indexer{-1},
variables: map[string]interface{}{
- "owner": githubv4.String(user),
+ "owner": githubv4.String(owner),
"name": githubv4.String(project),
},
},
commentEdit: commentEditIterator{
index: -1,
variables: map[string]interface{}{
- "owner": githubv4.String(user),
+ "owner": githubv4.String(owner),
"name": githubv4.String(project),
},
},
issueEdit: issueEditIterator{
index: -1,
variables: map[string]interface{}{
- "owner": githubv4.String(user),
+ "owner": githubv4.String(owner),
"name": githubv4.String(project),
},
},
diff --git a/bridge/launchpad/config.go b/bridge/launchpad/config.go
index 11a465be..d8efea46 100644
--- a/bridge/launchpad/config.go
+++ b/bridge/launchpad/config.go
@@ -2,26 +2,65 @@ package launchpad
import (
"bufio"
+ "errors"
"fmt"
+ "net/http"
"os"
+ "regexp"
"strings"
+ "time"
"github.com/MichaelMure/git-bug/bridge/core"
"github.com/MichaelMure/git-bug/repository"
)
-const keyProject = "project"
+var ErrBadProjectURL = errors.New("bad Launchpad project URL")
+
+const (
+ keyProject = "project"
+ defaultTimeout = 60 * time.Second
+)
+
+func (*Launchpad) Configure(repo repository.RepoCommon, params core.BridgeParams) (core.Configuration, error) {
+ if params.Token != "" {
+ fmt.Println("warning: --token is ineffective for a Launchpad bridge")
+ }
+ if params.Owner != "" {
+ fmt.Println("warning: --owner is ineffective for a Launchpad bridge")
+ }
-func (*Launchpad) Configure(repo repository.RepoCommon) (core.Configuration, error) {
conf := make(core.Configuration)
+ var err error
+ var project string
+
+ if params.Project != "" {
+ project = params.Project
- projectName, err := promptProjectName()
+ } else if params.URL != "" {
+ // get project name from url
+ project, err = splitURL(params.URL)
+ if err != nil {
+ return nil, err
+ }
+
+ } else {
+ // get project name from terminal prompt
+ project, err = promptProjectName()
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // verify project
+ ok, err := validateProject(project)
if err != nil {
return nil, err
}
+ if !ok {
+ return nil, fmt.Errorf("project doesn't exist")
+ }
- conf[keyProject] = projectName
-
+ conf[keyProject] = project
return conf, nil
}
@@ -52,3 +91,33 @@ func promptProjectName() (string, error) {
return line, nil
}
}
+
+func validateProject(project string) (bool, error) {
+ url := fmt.Sprintf("%s/%s", apiRoot, project)
+
+ client := &http.Client{
+ Timeout: defaultTimeout,
+ }
+
+ resp, err := client.Get(url)
+ if err != nil {
+ return false, err
+ }
+
+ return resp.StatusCode == http.StatusOK, nil
+}
+
+// extract project name from url
+func splitURL(url string) (string, error) {
+ re, err := regexp.Compile(`launchpad\.net[\/:]([^\/]*[a-z]+)`)
+ if err != nil {
+ panic("regexp compile:" + err.Error())
+ }
+
+ res := re.FindStringSubmatch(url)
+ if res == nil {
+ return "", ErrBadProjectURL
+ }
+
+ return res[1], nil
+}
diff --git a/bridge/launchpad/config_test.go b/bridge/launchpad/config_test.go
new file mode 100644
index 00000000..275c0d24
--- /dev/null
+++ b/bridge/launchpad/config_test.go
@@ -0,0 +1,93 @@
+package launchpad
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSplitURL(t *testing.T) {
+ type args struct {
+ url string
+ }
+ type want struct {
+ project string
+ err error
+ }
+ tests := []struct {
+ name string
+ args args
+ want want
+ }{
+ {
+ name: "default project url",
+ args: args{
+ url: "https://launchpad.net/ubuntu",
+ },
+ want: want{
+ project: "ubuntu",
+ err: nil,
+ },
+ },
+ {
+ name: "project bugs url",
+ args: args{
+ url: "https://bugs.launchpad.net/ubuntu",
+ },
+ want: want{
+ project: "ubuntu",
+ err: nil,
+ },
+ },
+ {
+ name: "bad url",
+ args: args{
+ url: "https://launchpa.net/ubuntu",
+ },
+ want: want{
+ err: ErrBadProjectURL,
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ project, err := splitURL(tt.args.url)
+ assert.Equal(t, tt.want.err, err)
+ assert.Equal(t, tt.want.project, project)
+ })
+ }
+}
+
+func TestValidateProject(t *testing.T) {
+ type args struct {
+ project string
+ }
+ tests := []struct {
+ name string
+ args args
+ want bool
+ }{
+ {
+ name: "public project",
+ args: args{
+ project: "ubuntu",
+ },
+ want: true,
+ },
+ {
+ name: "non existing project",
+ args: args{
+ project: "cant-find-this",
+ },
+ want: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ok, _ := validateProject(tt.args.project)
+ assert.Equal(t, tt.want, ok)
+ })
+ }
+}