aboutsummaryrefslogtreecommitdiffstats
path: root/bridge
diff options
context:
space:
mode:
authorMichael Muré <batolettre@gmail.com>2020-02-15 16:01:15 +0100
committerMichael Muré <batolettre@gmail.com>2020-02-15 16:01:15 +0100
commit5c230cb81e399f12cc7a1c1688b73f549b12a5f0 (patch)
treed8795111b390d98274a644cc88dd64c7d24e598b /bridge
parent0bb9ed9b0e6a53fd668be0c60127af78ddd061b5 (diff)
downloadgit-bug-5c230cb81e399f12cc7a1c1688b73f549b12a5f0.tar.gz
jira: rework to use the credential system + adapt to refactors
Diffstat (limited to 'bridge')
-rw-r--r--bridge/jira/client.go114
-rw-r--r--bridge/jira/config.go166
-rw-r--r--bridge/jira/export.go152
-rw-r--r--bridge/jira/import.go133
-rw-r--r--bridge/jira/jira.go59
5 files changed, 346 insertions, 278 deletions
diff --git a/bridge/jira/client.go b/bridge/jira/client.go
index 15098a3c..5e1db26f 100644
--- a/bridge/jira/client.go
+++ b/bridge/jira/client.go
@@ -14,10 +14,9 @@ import (
"strings"
"time"
- "github.com/MichaelMure/git-bug/bridge/core"
- "github.com/MichaelMure/git-bug/bug"
- "github.com/MichaelMure/git-bug/input"
"github.com/pkg/errors"
+
+ "github.com/MichaelMure/git-bug/bug"
)
var errDone = errors.New("Iteration Done")
@@ -39,14 +38,14 @@ func ParseTime(timeStr string) (time.Time, error) {
return out, err
}
-// MyTime is just a time.Time with a JSON serialization
-type MyTime struct {
+// Time is just a time.Time with a JSON serialization
+type Time struct {
time.Time
}
// UnmarshalJSON parses an RFC3339 date string into a time object
// borrowed from: https://stackoverflow.com/a/39180230/141023
-func (self *MyTime) UnmarshalJSON(data []byte) (err error) {
+func (t *Time) UnmarshalJSON(data []byte) (err error) {
str := string(data)
// Get rid of the quotes "" around the value.
@@ -56,7 +55,7 @@ func (self *MyTime) UnmarshalJSON(data []byte) (err error) {
str = str[1 : len(str)-1]
timeObj, err := ParseTime(str)
- self.Time = timeObj
+ t.Time = timeObj
return
}
@@ -100,8 +99,8 @@ type Comment struct {
Body string `json:"body"`
Author User `json:"author"`
UpdateAuthor User `json:"updateAuthor"`
- Created MyTime `json:"created"`
- Updated MyTime `json:"updated"`
+ Created Time `json:"created"`
+ Updated Time `json:"updated"`
}
// CommentPage the JSON object holding a single page of comments returned
@@ -115,13 +114,13 @@ type CommentPage struct {
}
// NextStartAt return the index of the first item on the next page
-func (self *CommentPage) NextStartAt() int {
- return self.StartAt + len(self.Comments)
+func (cp *CommentPage) NextStartAt() int {
+ return cp.StartAt + len(cp.Comments)
}
// IsLastPage return true if there are no more items beyond this page
-func (self *CommentPage) IsLastPage() bool {
- return self.NextStartAt() >= self.Total
+func (cp *CommentPage) IsLastPage() bool {
+ return cp.NextStartAt() >= cp.Total
}
// IssueFields the JSON object returned as the "fields" member of an issue.
@@ -130,7 +129,7 @@ func (self *CommentPage) IsLastPage() bool {
// https://docs.atlassian.com/software/jira/docs/api/REST/8.2.6/#api/2/issue-getIssue
type IssueFields struct {
Creator User `json:"creator"`
- Created MyTime `json:"created"`
+ Created Time `json:"created"`
Description string `json:"description"`
Summary string `json:"summary"`
Comments CommentPage `json:"comment"`
@@ -155,7 +154,7 @@ type ChangeLogItem struct {
type ChangeLogEntry struct {
ID string `json:"id"`
Author User `json:"author"`
- Created MyTime `json:"created"`
+ Created Time `json:"created"`
Items []ChangeLogItem `json:"items"`
}
@@ -171,16 +170,16 @@ type ChangeLogPage struct {
}
// NextStartAt return the index of the first item on the next page
-func (self *ChangeLogPage) NextStartAt() int {
- return self.StartAt + len(self.Entries)
+func (clp *ChangeLogPage) NextStartAt() int {
+ return clp.StartAt + len(clp.Entries)
}
// IsLastPage return true if there are no more items beyond this page
-func (self *ChangeLogPage) IsLastPage() bool {
+func (clp *ChangeLogPage) IsLastPage() bool {
// NOTE(josh): The "isLast" field is returned on JIRA cloud, but not on
// JIRA server. If we can distinguish which one we are working with, we can
// possibly rely on that instead.
- return self.NextStartAt() >= self.Total
+ return clp.NextStartAt() >= clp.Total
}
// Issue Top-level object for an issue
@@ -202,13 +201,13 @@ type SearchResult struct {
}
// NextStartAt return the index of the first item on the next page
-func (self *SearchResult) NextStartAt() int {
- return self.StartAt + len(self.Issues)
+func (sr *SearchResult) NextStartAt() int {
+ return sr.StartAt + len(sr.Issues)
}
// IsLastPage return true if there are no more items beyond this page
-func (self *SearchResult) IsLastPage() bool {
- return self.NextStartAt() >= self.Total
+func (sr *SearchResult) IsLastPage() bool {
+ return sr.NextStartAt() >= sr.Total
}
// SearchRequest the JSON object POSTed to the /search endpoint
@@ -296,8 +295,8 @@ type ServerInfo struct {
Version string `json:"version"`
VersionNumbers []int `json:"versionNumbers"`
BuildNumber int `json:"buildNumber"`
- BuildDate MyTime `json:"buildDate"`
- ServerTime MyTime `json:"serverTime"`
+ BuildDate Time `json:"buildDate"`
+ ServerTime Time `json:"serverTime"`
ScmInfo string `json:"scmInfo"`
BuildPartnerName string `json:"buildPartnerName"`
ServerTitle string `json:"serverTitle"`
@@ -331,7 +330,7 @@ func (ct *ClientTransport) SetCredentials(username string, token string) {
}
// Client Thin wrapper around the http.Client providing jira-specific methods
-// for APIendpoints
+// for API endpoints
type Client struct {
*http.Client
serverURL string
@@ -340,7 +339,7 @@ type Client struct {
// NewClient Construct a new client connected to the provided server and
// utilizing the given context for asynchronous events
-func NewClient(serverURL string, ctx context.Context) *Client {
+func NewClient(ctx context.Context, serverURL string) *Client {
cookiJar, _ := cookiejar.New(nil)
client := &http.Client{
Transport: &ClientTransport{underlyingTransport: http.DefaultTransport},
@@ -350,57 +349,20 @@ func NewClient(serverURL string, ctx context.Context) *Client {
return &Client{client, serverURL, ctx}
}
-// Login POST credentials to the /session endpoing and get a session cookie
-func (client *Client) Login(conf core.Configuration) error {
- credType := conf[keyCredentialsType]
-
- if conf[keyCredentialsFile] != "" {
- content, err := ioutil.ReadFile(conf[keyCredentialsFile])
- if err != nil {
- return err
- }
-
- switch credType {
- case "SESSION":
- return client.RefreshSessionTokenRaw(content)
- case "TOKEN":
- var params SessionQuery
- err := json.Unmarshal(content, &params)
- if err != nil {
- return err
- }
- return client.SetTokenCredentials(params.Username, params.Password)
- }
- return fmt.Errorf("Unexpected credType: %s", credType)
- }
-
- username := conf[keyUsername]
- if username == "" {
- return fmt.Errorf(
- "Invalid configuration lacks both a username and credentials sidecar " +
- "path. At least one is required.")
- }
-
- password := conf[keyPassword]
- if password == "" {
- var err error
- password, err = input.PromptPassword("Password", "password", input.Required)
- if err != nil {
- return err
- }
- }
-
+// Login POST credentials to the /session endpoint and get a session cookie
+func (client *Client) Login(credType, login, password string) error {
switch credType {
case "SESSION":
- return client.RefreshSessionToken(username, password)
+ return client.RefreshSessionToken(login, password)
case "TOKEN":
- return client.SetTokenCredentials(username, password)
+ return client.SetTokenCredentials(login, password)
+ default:
+ panic("unknown Jira cred type")
}
- return fmt.Errorf("Unexpected credType: %s", credType)
}
// RefreshSessionToken formulate the JSON request object from the user
-// credentials and POST it to the /session endpoing and get a session cookie
+// credentials and POST it to the /session endpoint and get a session cookie
func (client *Client) RefreshSessionToken(username, password string) error {
params := SessionQuery{
Username: username,
@@ -415,7 +377,7 @@ func (client *Client) RefreshSessionToken(username, password string) error {
return client.RefreshSessionTokenRaw(data)
}
-// SetTokenCredentials POST credentials to the /session endpoing and get a
+// SetTokenCredentials POST credentials to the /session endpoint and get a
// session cookie
func (client *Client) SetTokenCredentials(username, password string) error {
switch transport := client.Transport.(type) {
@@ -427,7 +389,7 @@ func (client *Client) SetTokenCredentials(username, password string) error {
return nil
}
-// RefreshSessionTokenRaw POST credentials to the /session endpoing and get a
+// RefreshSessionTokenRaw POST credentials to the /session endpoint and get a
// session cookie
func (client *Client) RefreshSessionTokenRaw(credentialsJSON []byte) error {
postURL := fmt.Sprintf("%s/rest/auth/1/session", client.serverURL)
@@ -796,7 +758,7 @@ func (client *Client) IterComments(idOrKey string, pageSize int) *CommentIterato
return iter
}
-// GetChangeLog fetchs one page of the changelog for an issue via the
+// GetChangeLog fetch one page of the changelog for an issue via the
// /issue/{IssueIdOrKey}/changelog endpoint (for JIRA cloud) or
// /issue/{IssueIdOrKey} with (fields=*none&expand=changelog)
// (for JIRA server)
@@ -1489,8 +1451,8 @@ func (client *Client) GetServerInfo() (*ServerInfo, error) {
}
// GetServerTime returns the current time on the server
-func (client *Client) GetServerTime() (MyTime, error) {
- var result MyTime
+func (client *Client) GetServerTime() (Time, error) {
+ var result Time
info, err := client.GetServerInfo()
if err != nil {
return result, err
diff --git a/bridge/jira/config.go b/bridge/jira/config.go
index 077f258a..c4743448 100644
--- a/bridge/jira/config.go
+++ b/bridge/jira/config.go
@@ -1,15 +1,13 @@
package jira
import (
- "encoding/json"
"fmt"
- "io/ioutil"
-
- "github.com/pkg/errors"
"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/input"
+ "github.com/MichaelMure/git-bug/repository"
)
const moreConfigText = `
@@ -28,32 +26,27 @@ after October 1st 2019 must use "TOKEN" authentication. You must create a user
API token and the client will provide this along with your username with each
request.`
-// Configure sets up the bridge configuration
-func (g *Jira) Configure(repo *cache.RepoCache, params core.BridgeParams) (core.Configuration, error) {
- conf := make(core.Configuration)
- conf[core.ConfigKeyTarget] = target
+func (*Jira) ValidParams() map[string]interface{} {
+ return map[string]interface{}{
+ "BaseURL": nil,
+ "Login": nil,
+ "CredPrefix": nil,
+ "Project": nil,
+ }
+}
+// Configure sets up the bridge configuration
+func (j *Jira) Configure(repo *cache.RepoCache, params core.BridgeParams) (core.Configuration, error) {
var err error
- // if params.Token != "" || params.TokenStdin {
- // return nil, fmt.Errorf(
- // "JIRA session tokens are extremely short lived. We don't store them " +
- // "in the configuration, so they are not valid for this bridge.")
- // }
-
- if params.Owner != "" {
- fmt.Println("warning: --owner is ineffective for a Jira bridge")
- }
-
- serverURL := params.URL
- if serverURL == "" {
+ baseURL := params.BaseURL
+ if baseURL == "" {
// terminal prompt
- serverURL, err = input.Prompt("JIRA server URL", "URL", input.Required)
+ baseURL, err = input.Prompt("JIRA server URL", "URL", input.Required, input.IsURL)
if err != nil {
return nil, err
}
}
- conf[keyServer] = serverURL
project := params.Project
if project == "" {
@@ -62,77 +55,56 @@ func (g *Jira) Configure(repo *cache.RepoCache, params core.BridgeParams) (core.
return nil, err
}
}
- conf[keyProject] = project
fmt.Println(credTypeText)
- credType, err := input.PromptChoice("Authentication mechanism", []string{"SESSION", "TOKEN"})
- if err != nil {
- return nil, err
- }
-
- switch credType {
- case 1:
- conf[keyCredentialsType] = "SESSION"
- case 2:
- conf[keyCredentialsType] = "TOKEN"
- }
-
- fmt.Println("How would you like to store your JIRA login credentials?")
- credTargetChoice, err := input.PromptChoice("Credential storage", []string{
- "sidecar JSON file: Your credentials will be stored in a JSON sidecar next" +
- "to your git config. Note that it will contain your JIRA password in clear" +
- "text.",
- "git-config: Your credentials will be stored in the git config. Note that" +
- "it will contain your JIRA password in clear text.",
- "username in config, askpass: Your username will be stored in the git" +
- "config. We will ask you for your password each time you execute the bridge.",
- })
- if err != nil {
- return nil, err
- }
-
- username, err := input.Prompt("JIRA username", "username", input.Required)
+ credTypeInput, err := input.PromptChoice("Authentication mechanism", []string{"SESSION", "TOKEN"})
if err != nil {
return nil, err
}
+ credType := []string{"SESSION", "TOKEN"}[credTypeInput]
- password, err := input.PromptPassword("Password", "password", input.Required)
- if err != nil {
- return nil, err
- }
+ var login string
+ var cred auth.Credential
- switch credTargetChoice {
- case 1:
- // TODO: a validator to see if the path is writable ?
- credentialsFile, err := input.Prompt("Credentials file path", "path", input.Required)
+ switch {
+ case params.CredPrefix != "":
+ cred, err = auth.LoadWithPrefix(repo, params.CredPrefix)
if err != nil {
return nil, err
}
- conf[keyCredentialsFile] = credentialsFile
- jsonData, err := json.Marshal(&SessionQuery{Username: username, Password: password})
- if err != nil {
- return nil, err
+ l, ok := cred.GetMetadata(auth.MetaKeyLogin)
+ if !ok {
+ return nil, fmt.Errorf("credential doesn't have a login")
+ }
+ login = l
+ default:
+ login := params.Login
+ if login == "" {
+ // TODO: validate username
+ login, err = input.Prompt("JIRA login", "login", input.Required)
+ if err != nil {
+ return nil, err
+ }
}
- err = ioutil.WriteFile(credentialsFile, jsonData, 0644)
+ cred, err = promptCredOptions(repo, login, baseURL)
if err != nil {
- return nil, errors.Wrap(
- err, fmt.Sprintf("Unable to write credentials to %s", credentialsFile))
+ return nil, err
}
- case 2:
- conf[keyUsername] = username
- conf[keyPassword] = password
- case 3:
- conf[keyUsername] = username
}
- err = g.ValidateConfig(conf)
+ conf := make(core.Configuration)
+ conf[core.ConfigKeyTarget] = target
+ conf[confKeyBaseUrl] = baseURL
+ conf[confKeyProject] = project
+ conf[confKeyCredentialType] = credType
+
+ err = j.ValidateConfig(conf)
if err != nil {
return nil, err
}
fmt.Printf("Attempting to login with credentials...\n")
- client := NewClient(serverURL, nil)
- err = client.Login(conf)
+ client, err := buildClient(nil, baseURL, credType, cred)
if err != nil {
return nil, err
}
@@ -144,7 +116,12 @@ func (g *Jira) Configure(repo *cache.RepoCache, params core.BridgeParams) (core.
return nil, fmt.Errorf(
"Project %s doesn't exist on %s, or authentication credentials for (%s)"+
" are invalid",
- project, serverURL, username)
+ project, baseURL, login)
+ }
+
+ err = core.FinishConfig(repo, metaKeyJiraLogin, login)
+ if err != nil {
+ return nil, err
}
fmt.Print(moreConfigText)
@@ -159,9 +136,46 @@ func (*Jira) ValidateConfig(conf core.Configuration) error {
return fmt.Errorf("unexpected target name: %v", v)
}
- if _, ok := conf[keyProject]; !ok {
- return fmt.Errorf("missing %s key", keyProject)
+ if _, ok := conf[confKeyProject]; !ok {
+ return fmt.Errorf("missing %s key", confKeyProject)
}
return nil
}
+
+func promptCredOptions(repo repository.RepoConfig, login, baseUrl string) (auth.Credential, error) {
+ creds, err := auth.List(repo,
+ auth.WithTarget(target),
+ auth.WithKind(auth.KindToken),
+ auth.WithMeta(auth.MetaKeyLogin, login),
+ auth.WithMeta(auth.MetaKeyBaseURL, baseUrl),
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ cred, index, err := input.PromptCredential(target, "password", creds, []string{
+ "enter my password",
+ "ask my password each time",
+ })
+ switch {
+ case err != nil:
+ return nil, err
+ case cred != nil:
+ return cred, nil
+ case index == 0:
+ password, err := input.PromptPassword("Password", "password", input.Required)
+ if err != nil {
+ return nil, err
+ }
+ lp := auth.NewLoginPassword(target, login, password)
+ lp.SetMetadata(auth.MetaKeyLogin, login)
+ return lp, nil
+ case index == 1:
+ l := auth.NewLogin(target, login)
+ l.SetMetadata(auth.MetaKeyLogin, login)
+ return l, nil
+ default:
+ panic("missed case")
+ }
+}
diff --git a/bridge/jira/export.go b/bridge/jira/export.go
index f329e490..37066263 100644
--- a/bridge/jira/export.go
+++ b/bridge/jira/export.go
@@ -5,14 +5,17 @@ import (
"encoding/json"
"fmt"
"net/http"
+ "os"
"time"
"github.com/pkg/errors"
"github.com/MichaelMure/git-bug/bridge/core"
+ "github.com/MichaelMure/git-bug/bridge/core/auth"
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity"
+ "github.com/MichaelMure/git-bug/identity"
)
var (
@@ -23,14 +26,12 @@ var (
type jiraExporter struct {
conf core.Configuration
- // the current user identity
- // NOTE: this is only needed to mock the credentials database in
- // getIdentityClient
- userIdentity entity.Id
-
// cache identities clients
identityClient map[entity.Id]*Client
+ // the mapping from git-bug "status" to JIRA "status" id
+ statusMap map[string]string
+
// cache identifiers used to speed up exporting operations
// cleared for each bug
cachedOperationIDs map[entity.Id]string
@@ -43,62 +44,99 @@ type jiraExporter struct {
}
// Init .
-func (je *jiraExporter) Init(repo *cache.RepoCache, conf core.Configuration) error {
+func (je *jiraExporter) Init(ctx context.Context, repo *cache.RepoCache, conf core.Configuration) error {
je.conf = conf
je.identityClient = make(map[entity.Id]*Client)
je.cachedOperationIDs = make(map[entity.Id]string)
je.cachedLabels = make(map[string]string)
- return nil
-}
-// getIdentityClient return an API client configured with the credentials
-// of the given identity. If no client were found it will initialize it from
-// the known credentials map and cache it for next use
-func (je *jiraExporter) getIdentityClient(ctx context.Context, id entity.Id) (*Client, error) {
- client, ok := je.identityClient[id]
- if ok {
- return client, nil
+ statusMap, err := getStatusMap(je.conf)
+ if err != nil {
+ return err
+ }
+ je.statusMap = statusMap
+
+ // preload all clients
+ err = je.cacheAllClient(ctx, repo)
+ if err != nil {
+ return err
}
- client = NewClient(je.conf[keyServer], ctx)
+ if len(je.identityClient) == 0 {
+ return fmt.Errorf("no credentials for this bridge")
+ }
+
+ var client *Client
+ for _, c := range je.identityClient {
+ client = c
+ break
+ }
- // NOTE: as a future enhancement, the bridge would ideally be able to generate
- // a separate session token for each user that we have stored credentials
- // for. However we currently only support a single user.
- if id != je.userIdentity {
- return nil, ErrMissingCredentials
+ if client == nil {
+ panic("nil client")
}
- err := client.Login(je.conf)
+
+ je.project, err = client.GetProject(je.conf[confKeyProject])
if err != nil {
- return nil, err
+ return err
}
- je.identityClient[id] = client
- return client, nil
+ return nil
}
-// ExportAll export all event made by the current user to Jira
-func (je *jiraExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ExportResult, error) {
- out := make(chan core.ExportResult)
-
- user, err := repo.GetUserIdentity()
+func (je *jiraExporter) cacheAllClient(ctx context.Context, repo *cache.RepoCache) error {
+ creds, err := auth.List(repo,
+ auth.WithTarget(target),
+ auth.WithKind(auth.KindLoginPassword), auth.WithKind(auth.KindLogin),
+ auth.WithMeta(auth.MetaKeyBaseURL, je.conf[confKeyBaseUrl]),
+ )
if err != nil {
- return nil, err
+ return err
}
- // NOTE: this is currently only need to mock the credentials database in
- // getIdentityClient.
- je.userIdentity = user.Id()
- client, err := je.getIdentityClient(ctx, user.Id())
- if err != nil {
- return nil, err
+ for _, cred := range creds {
+ login, ok := cred.GetMetadata(auth.MetaKeyLogin)
+ if !ok {
+ _, _ = fmt.Fprintf(os.Stderr, "credential %s is not tagged with a Jira login\n", cred.ID().Human())
+ continue
+ }
+
+ user, err := repo.ResolveIdentityImmutableMetadata(metaKeyJiraLogin, login)
+ if err == identity.ErrIdentityNotExist {
+ continue
+ }
+ if err != nil {
+ return nil
+ }
+
+ if _, ok := je.identityClient[user.Id()]; !ok {
+ client, err := buildClient(ctx, je.conf[confKeyBaseUrl], je.conf[confKeyCredentialType], creds[0])
+ if err != nil {
+ return err
+ }
+ je.identityClient[user.Id()] = client
+ }
}
- je.project, err = client.GetProject(je.conf[keyProject])
- if err != nil {
- return nil, err
+ return nil
+}
+
+// getClientForIdentity return an API client configured with the credentials
+// of the given identity. If no client were found it will initialize it from
+// the known credentials and cache it for next use.
+func (je *jiraExporter) getClientForIdentity(userId entity.Id) (*Client, error) {
+ client, ok := je.identityClient[userId]
+ if ok {
+ return client, nil
}
+ return nil, ErrMissingCredentials
+}
+
+// ExportAll export all event made by the current user to Jira
+func (je *jiraExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ExportResult, error) {
+ out := make(chan core.ExportResult)
+
go func() {
defer close(out)
@@ -134,7 +172,7 @@ func (je *jiraExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, si
if snapshot.HasAnyActor(allIdentitiesIds...) {
// try to export the bug and it associated events
- err := je.exportBug(ctx, b, since, out)
+ err := je.exportBug(ctx, b, out)
if err != nil {
out <- core.NewExportError(errors.Wrap(err, "can't export bug"), id)
return
@@ -150,7 +188,7 @@ func (je *jiraExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, si
}
// exportBug publish bugs and related events
-func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, since time.Time, out chan<- core.ExportResult) error {
+func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, out chan<- core.ExportResult) error {
snapshot := b.Snapshot()
var bugJiraID string
@@ -174,7 +212,7 @@ func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, since
// skip bug if it is a jira bug but is associated with another project
// (one bridge per JIRA project)
- project, ok := snapshot.GetCreateMetadata(keyJiraProject)
+ project, ok := snapshot.GetCreateMetadata(metaKeyJiraProject)
if ok && !stringInSlice(project, []string{je.project.ID, je.project.Key}) {
out <- core.NewExportNothing(
b.Id(), fmt.Sprintf("issue tagged with project: %s", project))
@@ -182,18 +220,18 @@ func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, since
}
// get jira bug ID
- jiraID, ok := snapshot.GetCreateMetadata(keyJiraID)
+ jiraID, ok := snapshot.GetCreateMetadata(metaKeyJiraId)
if ok {
// will be used to mark operation related to a bug as exported
bugJiraID = jiraID
} else {
// check that we have credentials for operation author
- client, err := je.getIdentityClient(ctx, author.Id())
+ client, err := je.getClientForIdentity(author.Id())
if err != nil {
// if bug is not yet exported and we do not have the author's credentials
// then there is nothing we can do, so just skip this bug
out <- core.NewExportNothing(
- b.Id(), fmt.Sprintf("missing author token for user %.8s",
+ b.Id(), fmt.Sprintf("missing author credentials for user %.8s",
author.Id().String()))
return err
}
@@ -201,7 +239,7 @@ func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, since
// Load any custom fields required to create an issue from the git
// config file.
fields := make(map[string]interface{})
- defaultFields, hasConf := je.conf[keyCreateDefaults]
+ defaultFields, hasConf := je.conf[confKeyCreateDefaults]
if hasConf {
err = json.Unmarshal([]byte(defaultFields), &fields)
if err != nil {
@@ -215,7 +253,7 @@ func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, since
"id": "10001",
}
}
- bugIDField, hasConf := je.conf[keyCreateGitBug]
+ bugIDField, hasConf := je.conf[confKeyCreateGitBug]
if hasConf {
// If the git configuration also indicates it, we can assign the git-bug
// id to a custom field to assist in integrations
@@ -257,12 +295,6 @@ func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, since
// cache operation jira id
je.cachedOperationIDs[createOp.Id()] = bugJiraID
- // lookup the mapping from git-bug "status" to JIRA "status" id
- statusMap, err := getStatusMap(je.conf)
- if err != nil {
- return err
- }
-
for _, op := range snapshot.Operations[1:] {
// ignore SetMetadata operations
if _, ok := op.(*bug.SetMetadataOperation); ok {
@@ -272,13 +304,13 @@ func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, since
// ignore operations already existing in jira (due to import or export)
// cache the ID of already exported or imported issues and events from
// Jira
- if id, ok := op.GetMetadata(keyJiraID); ok {
+ if id, ok := op.GetMetadata(metaKeyJiraId); ok {
je.cachedOperationIDs[op.Id()] = id
continue
}
opAuthor := op.GetAuthor()
- client, err := je.getIdentityClient(ctx, opAuthor.Id())
+ client, err := je.getClientForIdentity(opAuthor.Id())
if err != nil {
out <- core.NewExportError(
fmt.Errorf("missing operation author credentials for user %.8s",
@@ -340,7 +372,7 @@ func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, since
}
case *bug.SetStatusOperation:
- jiraStatus, hasStatus := statusMap[opr.Status.String()]
+ jiraStatus, hasStatus := je.statusMap[opr.Status.String()]
if hasStatus {
exportTime, err = UpdateIssueStatus(client, bugJiraID, jiraStatus)
if err != nil {
@@ -407,11 +439,11 @@ func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, since
func markOperationAsExported(b *cache.BugCache, target entity.Id, jiraID, jiraProject string, exportTime time.Time) error {
newMetadata := map[string]string{
- keyJiraID: jiraID,
- keyJiraProject: jiraProject,
+ metaKeyJiraId: jiraID,
+ metaKeyJiraProject: jiraProject,
}
if !exportTime.IsZero() {
- newMetadata[keyJiraExportTime] = exportTime.Format(http.TimeFormat)
+ newMetadata[metaKeyJiraExportTime] = exportTime.Format(http.TimeFormat)
}
_, err := b.SetMetadata(target, newMetadata)
diff --git a/bridge/jira/import.go b/bridge/jira/import.go
index 6a755a36..bfe83f4d 100644
--- a/bridge/jira/import.go
+++ b/bridge/jira/import.go
@@ -10,6 +10,7 @@ import (
"time"
"github.com/MichaelMure/git-bug/bridge/core"
+ "github.com/MichaelMure/git-bug/bridge/core/auth"
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity"
@@ -17,51 +18,75 @@ import (
)
const (
- keyJiraID = "jira-id"
- keyJiraOperationID = "jira-derived-id"
- keyJiraKey = "jira-key"
- keyJiraUser = "jira-user"
- keyJiraProject = "jira-project"
- keyJiraExportTime = "jira-export-time"
- defaultPageSize = 10
+ defaultPageSize = 10
)
// jiraImporter implement the Importer interface
type jiraImporter struct {
conf core.Configuration
+ client *Client
+
// send only channel
out chan<- core.ImportResult
}
// Init .
-func (ji *jiraImporter) Init(repo *cache.RepoCache, conf core.Configuration) error {
+func (ji *jiraImporter) Init(ctx context.Context, repo *cache.RepoCache, conf core.Configuration) error {
ji.conf = conf
- return nil
+
+ var cred auth.Credential
+
+ // Prioritize LoginPassword credentials to avoid a prompt
+ creds, err := auth.List(repo,
+ auth.WithTarget(target),
+ auth.WithMeta(auth.MetaKeyBaseURL, conf[confKeyBaseUrl]),
+ auth.WithKind(auth.KindLoginPassword),
+ )
+ if err != nil {
+ return err
+ }
+ if len(creds) > 0 {
+ cred = creds[0]
+ goto end
+ }
+
+ creds, err = auth.List(repo,
+ auth.WithTarget(target),
+ auth.WithMeta(auth.MetaKeyBaseURL, conf[confKeyBaseUrl]),
+ auth.WithKind(auth.KindLogin),
+ )
+ if err != nil {
+ return err
+ }
+ if len(creds) > 0 {
+ cred = creds[0]
+ }
+
+end:
+ if cred == nil {
+ return fmt.Errorf("no credential for this bridge")
+ }
+
+ // TODO(josh)[da52062]: Validate token and if it is expired then prompt for
+ // credentials and generate a new one
+ ji.client, err = buildClient(ctx, conf[confKeyBaseUrl], conf[confKeyCredentialType], cred)
+ return err
}
// ImportAll iterate over all the configured repository issues and ensure the
// creation of the missing issues / timeline items / edits / label events ...
func (ji *jiraImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ImportResult, error) {
sinceStr := since.Format("2006-01-02 15:04")
- serverURL := ji.conf[keyServer]
- project := ji.conf[keyProject]
- // TODO(josh)[da52062]: Validate token and if it is expired then prompt for
- // credentials and generate a new one
+ project := ji.conf[confKeyProject]
+
out := make(chan core.ImportResult)
ji.out = out
go func() {
defer close(ji.out)
- client := NewClient(serverURL, ctx)
- err := client.Login(ji.conf)
- if err != nil {
- out <- core.NewImportError(err, "")
- return
- }
-
- message, err := client.Search(
+ message, err := ji.client.Search(
fmt.Sprintf("project=%s AND updatedDate>\"%s\"", project, sinceStr), 0, 0)
if err != nil {
out <- core.NewImportError(err, "")
@@ -73,7 +98,7 @@ func (ji *jiraImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, si
jql := fmt.Sprintf("project=%s AND updatedDate>\"%s\"", project, sinceStr)
var searchIter *SearchIterator
for searchIter =
- client.IterSearch(jql, defaultPageSize); searchIter.HasNext(); {
+ ji.client.IterSearch(jql, defaultPageSize); searchIter.HasNext(); {
issue := searchIter.Next()
b, err := ji.ensureIssue(repo, *issue)
if err != nil {
@@ -84,7 +109,7 @@ func (ji *jiraImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, si
var commentIter *CommentIterator
for commentIter =
- client.IterComments(issue.ID, defaultPageSize); commentIter.HasNext(); {
+ ji.client.IterComments(issue.ID, defaultPageSize); commentIter.HasNext(); {
comment := commentIter.Next()
err := ji.ensureComment(repo, b, *comment)
if err != nil {
@@ -100,7 +125,7 @@ func (ji *jiraImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, si
var changelogIter *ChangeLogIterator
for changelogIter =
- client.IterChangeLog(issue.ID, defaultPageSize); changelogIter.HasNext(); {
+ ji.client.IterChangeLog(issue.ID, defaultPageSize); changelogIter.HasNext(); {
changelogEntry := changelogIter.Next()
// Advance the operation iterator up to the first operation which has
@@ -110,7 +135,7 @@ func (ji *jiraImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, si
var exportTime time.Time
for ; opIdx < len(snapshot.Operations); opIdx++ {
exportTimeStr, hasTime := snapshot.Operations[opIdx].GetMetadata(
- keyJiraExportTime)
+ metaKeyJiraExportTime)
if !hasTime {
continue
}
@@ -156,7 +181,7 @@ func (ji *jiraImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, si
func (ji *jiraImporter) ensurePerson(repo *cache.RepoCache, user User) (*cache.IdentityCache, error) {
// Look first in the cache
i, err := repo.ResolveIdentityImmutableMetadata(
- keyJiraUser, string(user.Key))
+ metaKeyJiraUser, string(user.Key))
if err == nil {
return i, nil
}
@@ -169,7 +194,7 @@ func (ji *jiraImporter) ensurePerson(repo *cache.RepoCache, user User) (*cache.I
user.EmailAddress,
user.Key,
map[string]string{
- keyJiraUser: string(user.Key),
+ metaKeyJiraUser: string(user.Key),
},
)
@@ -188,7 +213,7 @@ func (ji *jiraImporter) ensureIssue(repo *cache.RepoCache, issue Issue) (*cache.
return nil, err
}
- b, err := repo.ResolveBugCreateMetadata(keyJiraID, issue.ID)
+ b, err := repo.ResolveBugCreateMetadata(metaKeyJiraId, issue.ID)
if err != nil && err != bug.ErrBugNotExist {
return nil, err
}
@@ -210,9 +235,9 @@ func (ji *jiraImporter) ensureIssue(repo *cache.RepoCache, issue Issue) (*cache.
nil,
map[string]string{
core.MetaKeyOrigin: target,
- keyJiraID: issue.ID,
- keyJiraKey: issue.Key,
- keyJiraProject: ji.conf[keyProject],
+ metaKeyJiraId: issue.ID,
+ metaKeyJiraKey: issue.Key,
+ metaKeyJiraProject: ji.conf[confKeyProject],
})
if err != nil {
return nil, err
@@ -225,7 +250,7 @@ func (ji *jiraImporter) ensureIssue(repo *cache.RepoCache, issue Issue) (*cache.
}
// Return a unique string derived from a unique jira id and a timestamp
-func getTimeDerivedID(jiraID string, timestamp MyTime) string {
+func getTimeDerivedID(jiraID string, timestamp Time) string {
return fmt.Sprintf("%s-%d", jiraID, timestamp.Unix())
}
@@ -238,7 +263,7 @@ func (ji *jiraImporter) ensureComment(repo *cache.RepoCache, b *cache.BugCache,
}
targetOpID, err := b.ResolveOperationWithMetadata(
- keyJiraID, item.ID)
+ metaKeyJiraId, item.ID)
if err != nil && err != cache.ErrNoMatchingOp {
return err
}
@@ -263,7 +288,7 @@ func (ji *jiraImporter) ensureComment(repo *cache.RepoCache, b *cache.BugCache,
cleanText,
nil,
map[string]string{
- keyJiraID: item.ID,
+ metaKeyJiraId: item.ID,
},
)
if err != nil {
@@ -284,7 +309,7 @@ func (ji *jiraImporter) ensureComment(repo *cache.RepoCache, b *cache.BugCache,
// timestamp. Note that this must be consistent with the exporter during
// export of an EditCommentOperation
derivedID := getTimeDerivedID(item.ID, item.Updated)
- _, err = b.ResolveOperationWithMetadata(keyJiraID, derivedID)
+ _, err = b.ResolveOperationWithMetadata(metaKeyJiraId, derivedID)
if err == nil {
// Already imported this edition
return nil
@@ -311,7 +336,7 @@ func (ji *jiraImporter) ensureComment(repo *cache.RepoCache, b *cache.BugCache,
targetOpID,
cleanText,
map[string]string{
- keyJiraID: derivedID,
+ metaKeyJiraId: derivedID,
},
)
@@ -358,7 +383,7 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
// If we have an operation which is already mapped to the entire changelog
// entry then that means this changelog entry was induced by an export
// operation and we've already done the match, so we skip this one
- _, err := b.ResolveOperationWithMetadata(keyJiraOperationID, entry.ID)
+ _, err := b.ResolveOperationWithMetadata(metaKeyJiraOperationId, entry.ID)
if err == nil {
return nil
} else if err != cache.ErrNoMatchingOp {
@@ -400,7 +425,7 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
opr, isRightType := potentialOp.(*bug.LabelChangeOperation)
if isRightType && labelSetsMatch(addedLabels, opr.Added) && labelSetsMatch(removedLabels, opr.Removed) {
_, err := b.SetMetadata(opr.Id(), map[string]string{
- keyJiraOperationID: entry.ID,
+ metaKeyJiraOperationId: entry.ID,
})
if err != nil {
return err
@@ -412,7 +437,7 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
opr, isRightType := potentialOp.(*bug.SetStatusOperation)
if isRightType && statusMap[opr.Status.String()] == item.To {
_, err := b.SetMetadata(opr.Id(), map[string]string{
- keyJiraOperationID: entry.ID,
+ metaKeyJiraOperationId: entry.ID,
})
if err != nil {
return err
@@ -426,7 +451,7 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
opr, isRightType := potentialOp.(*bug.SetTitleOperation)
if isRightType && opr.Title == item.To {
_, err := b.SetMetadata(opr.Id(), map[string]string{
- keyJiraOperationID: entry.ID,
+ metaKeyJiraOperationId: entry.ID,
})
if err != nil {
return err
@@ -442,7 +467,7 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
opr.Target == b.Snapshot().Operations[0].Id() &&
opr.Message == item.ToString {
_, err := b.SetMetadata(opr.Id(), map[string]string{
- keyJiraOperationID: entry.ID,
+ metaKeyJiraOperationId: entry.ID,
})
if err != nil {
return err
@@ -457,7 +482,7 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
// changelog entry item as a separate git-bug operation.
for idx, item := range entry.Items {
derivedID := getIndexDerivedID(entry.ID, idx)
- _, err := b.ResolveOperationWithMetadata(keyJiraOperationID, derivedID)
+ _, err := b.ResolveOperationWithMetadata(metaKeyJiraOperationId, derivedID)
if err == nil {
continue
}
@@ -477,8 +502,8 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
addedLabels,
removedLabels,
map[string]string{
- keyJiraID: entry.ID,
- keyJiraOperationID: derivedID,
+ metaKeyJiraId: entry.ID,
+ metaKeyJiraOperationId: derivedID,
},
)
if err != nil {
@@ -496,8 +521,8 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
author,
entry.Created.Unix(),
map[string]string{
- keyJiraID: entry.ID,
- keyJiraOperationID: derivedID,
+ metaKeyJiraId: entry.ID,
+ metaKeyJiraOperationId: derivedID,
},
)
if err != nil {
@@ -510,8 +535,8 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
author,
entry.Created.Unix(),
map[string]string{
- keyJiraID: entry.ID,
- keyJiraOperationID: derivedID,
+ metaKeyJiraId: entry.ID,
+ metaKeyJiraOperationId: derivedID,
},
)
if err != nil {
@@ -534,8 +559,8 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
entry.Created.Unix(),
string(item.ToString),
map[string]string{
- keyJiraID: entry.ID,
- keyJiraOperationID: derivedID,
+ metaKeyJiraId: entry.ID,
+ metaKeyJiraOperationId: derivedID,
},
)
if err != nil {
@@ -552,8 +577,8 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
entry.Created.Unix(),
string(item.ToString),
map[string]string{
- keyJiraID: entry.ID,
- keyJiraOperationID: derivedID,
+ metaKeyJiraId: entry.ID,
+ metaKeyJiraOperationId: derivedID,
},
)
if err != nil {
@@ -580,7 +605,7 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
}
func getStatusMap(conf core.Configuration) (map[string]string, error) {
- mapStr, hasConf := conf[keyIDMap]
+ mapStr, hasConf := conf[confKeyIDMap]
if !hasConf {
return map[string]string{
bug.OpenStatus.String(): "1",
@@ -604,7 +629,7 @@ func getStatusMapReverse(conf core.Configuration) (map[string]string, error) {
outMap[val] = key
}
- mapStr, hasConf := conf[keyIDRevMap]
+ mapStr, hasConf := conf[confKeyIDRevMap]
if !hasConf {
return outMap, nil
}
diff --git a/bridge/jira/jira.go b/bridge/jira/jira.go
index 43a11c05..0ba27df3 100644
--- a/bridge/jira/jira.go
+++ b/bridge/jira/jira.go
@@ -2,27 +2,36 @@
package jira
import (
+ "context"
+ "fmt"
"sort"
"time"
"github.com/MichaelMure/git-bug/bridge/core"
+ "github.com/MichaelMure/git-bug/bridge/core/auth"
+ "github.com/MichaelMure/git-bug/input"
)
const (
target = "jira"
- metaKeyJiraLogin = "jira-login"
-
- keyServer = "server"
- keyProject = "project"
- keyCredentialsType = "credentials-type"
- keyCredentialsFile = "credentials-file"
- keyUsername = "username"
- keyPassword = "password"
- keyIDMap = "bug-id-map"
- keyIDRevMap = "bug-id-revmap"
- keyCreateDefaults = "create-issue-defaults"
- keyCreateGitBug = "create-issue-gitbug-id"
+ metaKeyJiraId = "jira-id"
+ metaKeyJiraOperationId = "jira-derived-id"
+ metaKeyJiraKey = "jira-key"
+ metaKeyJiraUser = "jira-user"
+ metaKeyJiraProject = "jira-project"
+ metaKeyJiraExportTime = "jira-export-time"
+ metaKeyJiraLogin = "jira-login"
+
+ confKeyBaseUrl = "base-url"
+ confKeyProject = "project"
+ confKeyCredentialType = "credentials-type" // "SESSION" or "TOKEN"
+ confKeyIDMap = "bug-id-map"
+ confKeyIDRevMap = "bug-id-revmap"
+ // the issue type when exporting a new bug. Default is Story (10001)
+ confKeyCreateDefaults = "create-issue-defaults"
+ // if set, the bridge fill this JIRA field with the `git-bug` id when exporting
+ confKeyCreateGitBug = "create-issue-gitbug-id"
defaultTimeout = 60 * time.Second
)
@@ -51,6 +60,32 @@ func (*Jira) NewExporter() core.Exporter {
return &jiraExporter{}
}
+func buildClient(ctx context.Context, baseURL string, credType string, cred auth.Credential) (*Client, error) {
+ client := NewClient(ctx, baseURL)
+
+ var login, password string
+
+ switch cred := cred.(type) {
+ case *auth.LoginPassword:
+ login = cred.Login
+ password = cred.Password
+ case *auth.Login:
+ login = cred.Login
+ p, err := input.PromptPassword(fmt.Sprintf("Password for %s", login), "password", input.Required)
+ if err != nil {
+ return nil, err
+ }
+ password = p
+ }
+
+ err := client.Login(credType, login, password)
+ if err != nil {
+ return nil, err
+ }
+
+ return client, nil
+}
+
// stringInSlice returns true if needle is found in haystack
func stringInSlice(needle string, haystack []string) bool {
for _, match := range haystack {