aboutsummaryrefslogtreecommitdiffstats
path: root/bridge/github
diff options
context:
space:
mode:
Diffstat (limited to 'bridge/github')
-rw-r--r--bridge/github/config.go221
-rw-r--r--bridge/github/config_test.go5
-rw-r--r--bridge/github/export.go29
-rw-r--r--bridge/github/export_test.go15
-rw-r--r--bridge/github/github.go21
-rw-r--r--bridge/github/import.go30
-rw-r--r--bridge/github/import_test.go8
7 files changed, 123 insertions, 206 deletions
diff --git a/bridge/github/config.go b/bridge/github/config.go
index bc26a2fc..9477801d 100644
--- a/bridge/github/config.go
+++ b/bridge/github/config.go
@@ -14,29 +14,17 @@ import (
"sort"
"strconv"
"strings"
- "syscall"
"time"
text "github.com/MichaelMure/go-term-text"
"github.com/pkg/errors"
- "golang.org/x/crypto/ssh/terminal"
"github.com/MichaelMure/git-bug/bridge/core"
"github.com/MichaelMure/git-bug/bridge/core/auth"
"github.com/MichaelMure/git-bug/cache"
- "github.com/MichaelMure/git-bug/entity"
+ "github.com/MichaelMure/git-bug/input"
"github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util/colors"
- "github.com/MichaelMure/git-bug/util/interrupt"
-)
-
-const (
- target = "github"
- githubV3Url = "https://api.github.com"
- keyOwner = "owner"
- keyProject = "project"
-
- defaultTimeout = 60 * time.Second
)
var (
@@ -50,12 +38,6 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
conf := make(core.Configuration)
var err error
-
- if (params.CredPrefix != "" || params.TokenRaw != "") &&
- (params.URL == "" && (params.Project == "" || params.Owner == "")) {
- return nil, fmt.Errorf("you must provide a project URL or Owner/Name to configure this bridge with a token")
- }
-
var owner string
var project string
@@ -88,9 +70,23 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
return nil, fmt.Errorf("invalid parameter owner: %v", owner)
}
- user, err := repo.GetUserIdentity()
- if err != nil {
- return nil, err
+ login := params.Login
+ if login == "" {
+ validator := func(name string, value string) (string, error) {
+ ok, err := validateUsername(value)
+ if err != nil {
+ return "", err
+ }
+ if !ok {
+ return "invalid login", nil
+ }
+ return "", nil
+ }
+
+ login, err = input.Prompt("Github login", "login", input.Required, validator)
+ if err != nil {
+ return nil, err
+ }
}
var cred auth.Credential
@@ -101,13 +97,11 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
if err != nil {
return nil, err
}
- if cred.UserId() != user.Id() {
- return nil, fmt.Errorf("selected credential don't match the user")
- }
case params.TokenRaw != "":
- cred = auth.NewToken(user.Id(), params.TokenRaw, target)
+ cred = auth.NewToken(params.TokenRaw, target)
+ cred.SetMetadata(auth.MetaKeyLogin, login)
default:
- cred, err = promptTokenOptions(repo, user.Id(), owner, project)
+ cred, err = promptTokenOptions(repo, login, owner, project)
if err != nil {
return nil, err
}
@@ -144,7 +138,7 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
}
}
- return conf, nil
+ return conf, core.FinishConfig(repo, metaKeyGithubLogin, login)
}
func (*Github) ValidateConfig(conf core.Configuration) error {
@@ -165,11 +159,11 @@ func (*Github) ValidateConfig(conf core.Configuration) error {
return nil
}
-func requestToken(note, username, password string, scope string) (*http.Response, error) {
- return requestTokenWith2FA(note, username, password, "", scope)
+func requestToken(note, login, password string, scope string) (*http.Response, error) {
+ return requestTokenWith2FA(note, login, password, "", scope)
}
-func requestTokenWith2FA(note, username, password, otpCode string, scope string) (*http.Response, error) {
+func requestTokenWith2FA(note, login, password, otpCode string, scope string) (*http.Response, error) {
url := fmt.Sprintf("%s/authorizations", githubV3Url)
params := struct {
Scopes []string `json:"scopes"`
@@ -191,7 +185,7 @@ func requestTokenWith2FA(note, username, password, otpCode string, scope string)
return nil, err
}
- req.SetBasicAuth(username, password)
+ req.SetBasicAuth(login, password)
req.Header.Set("Content-Type", "application/json")
if otpCode != "" {
@@ -235,9 +229,9 @@ func randomFingerprint() string {
return string(b)
}
-func promptTokenOptions(repo repository.RepoConfig, userId entity.Id, owner, project string) (auth.Credential, error) {
+func promptTokenOptions(repo repository.RepoConfig, login, owner, project string) (auth.Credential, error) {
for {
- creds, err := auth.List(repo, auth.WithUserId(userId), auth.WithTarget(target))
+ creds, err := auth.List(repo, auth.WithTarget(target), auth.WithMeta(auth.MetaKeyLogin, login))
if err != nil {
return nil, err
}
@@ -253,10 +247,11 @@ func promptTokenOptions(repo repository.RepoConfig, userId entity.Id, owner, pro
fmt.Println("Existing tokens for Github:")
for i, cred := range creds {
token := cred.(*auth.Token)
- fmt.Printf("[%d]: %s => %s (%s)\n",
+ fmt.Printf("[%d]: %s => %s (login: %s, %s)\n",
i+3,
colors.Cyan(token.ID().Human()),
colors.Red(text.TruncateMax(token.Value, 10)),
+ token.Metadata()[auth.MetaKeyLogin],
token.CreateTime().Format(time.RFC822),
)
}
@@ -284,13 +279,17 @@ func promptTokenOptions(repo repository.RepoConfig, userId entity.Id, owner, pro
if err != nil {
return nil, err
}
- return auth.NewToken(userId, value, target), nil
+ token := auth.NewToken(value, target)
+ token.SetMetadata(auth.MetaKeyLogin, login)
+ return token, nil
case 2:
- value, err := loginAndRequestToken(owner, project)
+ value, err := loginAndRequestToken(login, owner, project)
if err != nil {
return nil, err
}
- return auth.NewToken(userId, value, target), nil
+ token := auth.NewToken(value, target)
+ token.SetMetadata(auth.MetaKeyLogin, login)
+ return token, nil
default:
return creds[index-3], nil
}
@@ -308,29 +307,22 @@ func promptToken() (string, error) {
fmt.Println(" - 'repo' : to be able to read private repositories")
fmt.Println()
- re, err := regexp.Compile(`^[a-zA-Z0-9]{40}`)
+ 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
+ validator := func(name string, value string) (complaint string, err error) {
+ if re.MatchString(value) {
+ return "", nil
}
-
- token := strings.TrimSpace(line)
- if re.MatchString(token) {
- return token, nil
- }
-
- fmt.Println("token is invalid")
+ return "token has incorrect format", nil
}
+
+ return input.Prompt("Enter token", "token", input.Required, validator)
}
-func loginAndRequestToken(owner, project string) (string, error) {
+func loginAndRequestToken(login, 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 global git config.")
fmt.Println()
fmt.Println("The access scope depend on the type of repository.")
@@ -341,17 +333,13 @@ func loginAndRequestToken(owner, project string) (string, error) {
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()
+ i, err := input.PromptChoice("repository visibility", []string{"public", "private"})
if err != nil {
return "", err
}
+ isPublic := i == 0
- password, err := promptPassword()
+ password, err := input.PromptPassword("Password", "password", input.Required)
if err != nil {
return "", err
}
@@ -370,7 +358,7 @@ func loginAndRequestToken(owner, project string) (string, error) {
note := fmt.Sprintf("git-bug - %s/%s", owner, project)
- resp, err := requestToken(note, username, password, scope)
+ resp, err := requestToken(note, login, password, scope)
if err != nil {
return "", err
}
@@ -380,12 +368,12 @@ func loginAndRequestToken(owner, project string) (string, error) {
// Handle 2FA is needed
OTPHeader := resp.Header.Get("X-GitHub-OTP")
if resp.StatusCode == http.StatusUnauthorized && OTPHeader != "" {
- otpCode, err := prompt2FA()
+ otpCode, err := input.PromptPassword("Two-factor authentication code", "code", input.Required)
if err != nil {
return "", err
}
- resp, err = requestTokenWith2FA(note, username, password, otpCode, scope)
+ resp, err = requestTokenWith2FA(note, login, password, otpCode, scope)
if err != nil {
return "", err
}
@@ -401,29 +389,6 @@ func loginAndRequestToken(owner, project string) (string, error) {
return "", fmt.Errorf("error creating token %v: %v", resp.StatusCode, string(b))
}
-func promptUsername() (string, error) {
- for {
- fmt.Print("username: ")
-
- line, err := bufio.NewReader(os.Stdin).ReadString('\n')
- if err != nil {
- return "", err
- }
-
- line = strings.TrimSpace(line)
-
- ok, err := validateUsername(line)
- if err != nil {
- return "", err
- }
- if ok {
- return line, nil
- }
-
- fmt.Println("invalid username")
- }
-}
-
func promptURL(repo repository.RepoCommon) (string, string, error) {
// remote suggestions
remotes, err := repo.GetRemotes()
@@ -578,87 +543,3 @@ func validateProject(owner, project string, token *auth.Token) (bool, error) {
return resp.StatusCode == http.StatusOK, nil
}
-
-func promptPassword() (string, error) {
- termState, err := terminal.GetState(int(syscall.Stdin))
- if err != nil {
- return "", err
- }
-
- cancel := interrupt.RegisterCleaner(func() error {
- return terminal.Restore(int(syscall.Stdin), termState)
- })
- defer cancel()
-
- for {
- 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
- }
-
- if len(bytePassword) > 0 {
- return string(bytePassword), nil
- }
-
- fmt.Println("password is empty")
- }
-}
-
-func prompt2FA() (string, error) {
- termState, err := terminal.GetState(int(syscall.Stdin))
- if err != nil {
- return "", err
- }
-
- cancel := interrupt.RegisterCleaner(func() error {
- return terminal.Restore(int(syscall.Stdin), termState)
- })
- defer cancel()
-
- for {
- fmt.Print("two-factor authentication code: ")
-
- byte2fa, err := terminal.ReadPassword(int(syscall.Stdin))
- fmt.Println()
- if err != nil {
- return "", err
- }
-
- if len(byte2fa) > 0 {
- return string(byte2fa), nil
- }
-
- 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.TrimSpace(line)
-
- index, err := strconv.Atoi(line)
- if err != nil || (index != 1 && index != 2) {
- fmt.Println("invalid input")
- continue
- }
-
- // return true for public repositories, false for private
- return index == 1, nil
- }
-}
diff --git a/bridge/github/config_test.go b/bridge/github/config_test.go
index 9798d26b..d7b1b38d 100644
--- a/bridge/github/config_test.go
+++ b/bridge/github/config_test.go
@@ -7,7 +7,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/MichaelMure/git-bug/bridge/core/auth"
- "github.com/MichaelMure/git-bug/entity"
)
func TestSplitURL(t *testing.T) {
@@ -155,8 +154,8 @@ func TestValidateProject(t *testing.T) {
t.Skip("Env var GITHUB_TOKEN_PUBLIC missing")
}
- tokenPrivate := auth.NewToken(entity.UnsetId, envPrivate, target)
- tokenPublic := auth.NewToken(entity.UnsetId, envPublic, target)
+ tokenPrivate := auth.NewToken(envPrivate, target)
+ tokenPublic := auth.NewToken(envPublic, target)
type args struct {
owner string
diff --git a/bridge/github/export.go b/bridge/github/export.go
index 6c089a47..c363e188 100644
--- a/bridge/github/export.go
+++ b/bridge/github/export.go
@@ -7,6 +7,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
+ "os"
"strings"
"time"
@@ -19,7 +20,7 @@ import (
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity"
- "github.com/MichaelMure/git-bug/repository"
+ "github.com/MichaelMure/git-bug/identity"
)
var (
@@ -74,7 +75,8 @@ func (ge *githubExporter) Init(repo *cache.RepoCache, conf core.Configuration) e
return err
}
- creds, err := auth.List(repo, auth.WithUserId(user.Id()), auth.WithTarget(target), auth.WithKind(auth.KindToken))
+ login := user.ImmutableMetadata()[metaKeyGithubLogin]
+ creds, err := auth.List(repo, auth.WithMeta(auth.MetaKeyLogin, login), auth.WithTarget(target), auth.WithKind(auth.KindToken))
if err != nil {
return err
}
@@ -88,16 +90,30 @@ func (ge *githubExporter) Init(repo *cache.RepoCache, conf core.Configuration) e
return nil
}
-func (ge *githubExporter) cacheAllClient(repo repository.RepoConfig) error {
+func (ge *githubExporter) cacheAllClient(repo *cache.RepoCache) error {
creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken))
if err != nil {
return err
}
for _, cred := range creds {
- if _, ok := ge.identityClient[cred.UserId()]; !ok {
+ login, ok := cred.GetMetadata(auth.MetaKeyLogin)
+ if !ok {
+ _, _ = fmt.Fprintf(os.Stderr, "credential %s is not tagged with a Github login\n", cred.ID().Human())
+ continue
+ }
+
+ user, err := repo.ResolveIdentityImmutableMetadata(metaKeyGithubLogin, login)
+ if err == identity.ErrIdentityNotExist {
+ continue
+ }
+ if err != nil {
+ return nil
+ }
+
+ if _, ok := ge.identityClient[user.Id()]; !ok {
client := buildClient(creds[0].(*auth.Token))
- ge.identityClient[cred.UserId()] = client
+ ge.identityClient[user.Id()] = client
}
}
@@ -477,11 +493,12 @@ func (ge *githubExporter) cacheGithubLabels(ctx context.Context, gc *githubv4.Cl
for hasNextPage {
// create a new timeout context at each iteration
ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
- defer cancel()
if err := gc.Query(ctx, &q, variables); err != nil {
+ cancel()
return err
}
+ cancel()
for _, label := range q.Repository.Labels.Nodes {
ge.cachedLabels[label.Name] = label.ID
diff --git a/bridge/github/export_test.go b/bridge/github/export_test.go
index 5a0bc653..7d6e6fb1 100644
--- a/bridge/github/export_test.go
+++ b/bridge/github/export_test.go
@@ -144,8 +144,12 @@ func TestPushPull(t *testing.T) {
require.NoError(t, err)
// set author identity
+ login := "identity-test"
author, err := backend.NewIdentity("test identity", "test@test.org")
require.NoError(t, err)
+ author.SetMetadata(metaKeyGithubLogin, login)
+ err = author.Commit()
+ require.NoError(t, err)
err = backend.SetUserIdentity(author)
require.NoError(t, err)
@@ -153,6 +157,11 @@ func TestPushPull(t *testing.T) {
defer backend.Close()
interrupt.RegisterCleaner(backend.Close)
+ token := auth.NewToken(envToken, target)
+ token.SetMetadata(auth.MetaKeyLogin, login)
+ err = auth.Store(repo, token)
+ require.NoError(t, err)
+
tests := testCases(t, backend)
// generate project name
@@ -176,10 +185,6 @@ func TestPushPull(t *testing.T) {
return deleteRepository(projectName, envUser, envToken)
})
- token := auth.NewToken(author.Id(), envToken, target)
- err = auth.Store(repo, token)
- require.NoError(t, err)
-
// initialize exporter
exporter := &githubExporter{}
err = exporter.Init(backend, core.Configuration{
@@ -255,7 +260,7 @@ func TestPushPull(t *testing.T) {
// verify bug have same number of original operations
require.Len(t, importedBug.Snapshot().Operations, tt.numOrOp)
- // verify bugs are taged with origin=github
+ // verify bugs are tagged with origin=github
issueOrigin, ok := importedBug.Snapshot().GetCreateMetadata(core.MetaKeyOrigin)
require.True(t, ok)
require.Equal(t, issueOrigin, target)
diff --git a/bridge/github/github.go b/bridge/github/github.go
index 874c2d11..19dc8a08 100644
--- a/bridge/github/github.go
+++ b/bridge/github/github.go
@@ -3,6 +3,7 @@ package github
import (
"context"
+ "time"
"github.com/shurcooL/githubv4"
"golang.org/x/oauth2"
@@ -11,12 +12,32 @@ import (
"github.com/MichaelMure/git-bug/bridge/core/auth"
)
+const (
+ target = "github"
+
+ metaKeyGithubId = "github-id"
+ metaKeyGithubUrl = "github-url"
+ metaKeyGithubLogin = "github-login"
+
+ keyOwner = "owner"
+ keyProject = "project"
+
+ githubV3Url = "https://api.github.com"
+ defaultTimeout = 60 * time.Second
+)
+
+var _ core.BridgeImpl = &Github{}
+
type Github struct{}
func (*Github) Target() string {
return target
}
+func (g *Github) LoginMetaKey() string {
+ return metaKeyGithubLogin
+}
+
func (*Github) NewImporter() core.Importer {
return &githubImporter{}
}
diff --git a/bridge/github/import.go b/bridge/github/import.go
index 092e3e71..ea0ccba3 100644
--- a/bridge/github/import.go
+++ b/bridge/github/import.go
@@ -15,12 +15,6 @@ import (
"github.com/MichaelMure/git-bug/util/text"
)
-const (
- metaKeyGithubId = "github-id"
- metaKeyGithubUrl = "github-url"
- metaKeyGithubLogin = "github-login"
-)
-
// githubImporter implement the Importer interface
type githubImporter struct {
conf core.Configuration
@@ -38,17 +32,7 @@ type githubImporter struct {
func (gi *githubImporter) Init(repo *cache.RepoCache, conf core.Configuration) error {
gi.conf = conf
- opts := []auth.Option{
- auth.WithTarget(target),
- auth.WithKind(auth.KindToken),
- }
-
- user, err := repo.GetUserIdentity()
- if err == nil {
- opts = append(opts, auth.WithUserId(user.Id()))
- }
-
- creds, err := auth.List(repo, opts...)
+ creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken))
if err != nil {
return err
}
@@ -197,6 +181,11 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline
// other edits will be added as CommentEdit operations
target, err := b.ResolveOperationWithMetadata(metaKeyGithubId, parseId(issue.Id))
+ if err == cache.ErrNoMatchingOp {
+ // original comment is missing somehow, issuing a warning
+ gi.out <- core.NewImportWarning(fmt.Errorf("comment ID %s to edit is missing", parseId(issue.Id)), b.Id())
+ continue
+ }
if err != nil {
return nil, err
}
@@ -545,10 +534,14 @@ func (gi *githubImporter) ensurePerson(repo *cache.RepoCache, actor *actor) (*ca
case "Bot":
}
+ // Name is not necessarily set, fallback to login as a name is required in the identity
+ if name == "" {
+ name = string(actor.Login)
+ }
+
i, err = repo.NewIdentityRaw(
name,
email,
- string(actor.Login),
string(actor.AvatarUrl),
map[string]string{
metaKeyGithubLogin: string(actor.Login),
@@ -595,7 +588,6 @@ func (gi *githubImporter) getGhost(repo *cache.RepoCache) (*cache.IdentityCache,
return repo.NewIdentityRaw(
name,
"",
- string(q.User.Login),
string(q.User.AvatarUrl),
map[string]string{
metaKeyGithubLogin: string(q.User.Login),
diff --git a/bridge/github/import_test.go b/bridge/github/import_test.go
index 304229a0..a8f8e346 100644
--- a/bridge/github/import_test.go
+++ b/bridge/github/import_test.go
@@ -21,6 +21,7 @@ import (
func Test_Importer(t *testing.T) {
author := identity.NewIdentity("Michael Muré", "batolettre@gmail.com")
+
tests := []struct {
name string
url string
@@ -140,10 +141,11 @@ func Test_Importer(t *testing.T) {
t.Skip("Env var GITHUB_TOKEN_PRIVATE missing")
}
- err = author.Commit(repo)
- require.NoError(t, err)
+ login := "test-identity"
+ author.SetMetadata(metaKeyGithubLogin, login)
- token := auth.NewToken(author.Id(), envToken, target)
+ token := auth.NewToken(envToken, target)
+ token.SetMetadata(auth.MetaKeyLogin, login)
err = auth.Store(repo, token)
require.NoError(t, err)