Skip to content

Commit

Permalink
Allow multiple projects to be synchronized (adapted from PR 18: coreo…
Browse files Browse the repository at this point in the history
  • Loading branch information
andrivet committed Dec 2, 2017
1 parent de3c446 commit 84c3d08
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 55 deletions.
132 changes: 98 additions & 34 deletions cfg/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ type fields struct {
lastUpdate string
}

// Project represents the project configuration as it exists in the configuration file.
type Project struct {
Repo string `json:"repo" mapstructure:"repo"`
Key string `json:"key" mapstructure:"key"`
}

// Config is the root configuration object the application creates.
type Config struct {
// cmdFile is the file Viper is using for its configuration (default $HOME/.issue-sync.json).
Expand All @@ -64,8 +70,8 @@ type Config struct {
// fieldIDs is the list of custom fields we pulled from the `fields` JIRA endpoint.
fieldIDs fields

// project represents the JIRA project the user has requested.
project jira.Project
// projects represents the mapping from the GitHub repos to JIRA projects the user configured.
projects map[string]jira.Project

// since is the parsed value of the `since` configuration parameter, which is the earliest that
// a GitHub issue can have been updated to be retrieved.
Expand All @@ -90,6 +96,7 @@ func NewConfig(cmd *cobra.Command) (Config, error) {
config.cmdFile = config.cmdConfig.ConfigFileUsed()

config.log = *newLogger("issue-sync", config.cmdConfig.GetString("log-level"))
config.projects = make(map[string]jira.Project)

if err := config.validateConfig(); err != nil {
return Config{}, err
Expand All @@ -101,21 +108,27 @@ func NewConfig(cmd *cobra.Command) (Config, error) {
// LoadJIRAConfig loads the JIRA configuration (project key,
// custom field IDs) from a remote JIRA server.
func (c *Config) LoadJIRAConfig(client jira.Client) error {
proj, res, err := client.Project.Get(c.cmdConfig.GetString("jira-project"))
if err != nil {
c.log.Errorf("Error retrieving JIRA project; check key and credentials. Error: %v", err)
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
var projects []Project

c.cmdConfig.UnmarshalKey("projects", &projects)

for _, project := range projects {
proj, res, err := client.Project.Get(project.Key)
if err != nil {
c.log.Errorf("Error occured trying to read error body: %v", err)
return err
c.log.Errorf("Error retrieving JIRA project; check key and credentials. Error: %v", err)
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
c.log.Errorf("Error occured trying to read error body: %v", err)
return err
}
c.log.Debugf("Error body: %s", body)
return errors.New(string(body))
}

c.log.Debugf("Error body: %s", body)
return errors.New(string(body))
c.projects[project.Repo] = *proj
}
c.project = *proj

var err error
c.fieldIDs, err = c.getFieldIDs(client)
if err != nil {
return err
Expand Down Expand Up @@ -195,20 +208,38 @@ func (c Config) GetFieldKey(key fieldKey) string {
return fmt.Sprintf("customfield_%s", c.GetFieldID(key))
}

// GetProject returns the JIRA project the user has configured.
func (c Config) GetProject() jira.Project {
return c.project
// GetProjects returns the map of GitHub repos and JIRA projects, which is useful
// for iterating with.
func (c Config) GetProjects() map[string]jira.Project {
return c.projects
}

// GetProjectKey returns the JIRA key of the configured project.
func (c Config) GetProjectKey() string {
return c.project.Key
// GetProject returns the JIRA project for a GitHub repo.
func (c Config) GetProject(repo string) jira.Project {
return c.projects[repo]
}

// GetRepo returns the user/org name and the repo name of the configured GitHub repository.
func (c Config) GetRepo() (string, string) {
fullName := c.cmdConfig.GetString("repo-name")
parts := strings.Split(fullName, "/")
// GetProjectKey returns the JIRA key of the project for a GitHub repo.
func (c Config) GetProjectKey(repo string) string {
return c.projects[repo].Key
}

// GetRepoList returns the list of GitHub repo names provided.
func (c Config) GetRepoList() []string {
keys := make([]string, len(c.projects))

i := 0
for k := range c.projects {
keys[i] = k
i++
}

return keys
}

// GetRepo returns the user/org name and the repo name of the given GitHub repository.
func (c Config) GetRepo(repo string) (string, string) {
parts := strings.Split(repo, "/")
// We check that repo-name is two parts separated by a slash in NewConfig, so this is safe
return parts[0], parts[1]
}
Expand All @@ -232,6 +263,7 @@ type configFile struct {
RepoName string `json:"repo-name" mapstructure:"repo-name"`
JIRAURI string `json:"jira-uri" mapstructure:"jira-uri"`
JIRAProject string `json:"jira-project" mapstructure:"jira-project"`
Projects []jira.Project `json:"projects" mapstructure:"projects"`
Since string `json:"since" mapstructure:"since"`
Timeout time.Duration `json:"timeout" mapstructure:"timeout"`
}
Expand Down Expand Up @@ -390,14 +422,6 @@ func (c *Config) validateConfig() error {
}
}

repo := c.cmdConfig.GetString("repo-name")
if repo == "" {
return errors.New("GitHub repository required")
}
if !strings.Contains(repo, "/") || len(strings.Split(repo, "/")) != 2 {
return errors.New("GitHub repository must be of form user/repo")
}

uri := c.cmdConfig.GetString("jira-uri")
if uri == "" {
return errors.New("JIRA URI required")
Expand All @@ -406,9 +430,49 @@ func (c *Config) validateConfig() error {
return errors.New("JIRA URI must be valid URI")
}

project := c.cmdConfig.GetString("jira-project")
if project == "" {
return errors.New("JIRA project required")
if c.cmdConfig.GetString("jira-project") != "" || c.cmdConfig.GetString("repo-name") != "" {
c.log.Debug("Using provided project and repo")

repo := c.cmdConfig.GetString("repo-name")
if repo == "" {
return errors.New("GitHub repository required")
}
if !strings.Contains(repo, "/") || len(strings.Split(repo, "/")) != 2 {
return errors.New("GitHub repository must be of form user/repo")
}

project := c.cmdConfig.GetString("jira-project")
if project == "" {
return errors.New("JIRA project required")
}

projects := make([]Project, 1)
projects[0] = Project{
Repo: repo,
Key: project,
}

c.cmdConfig.Set("projects", projects)
c.cmdConfig.Set("repo-name", "")
c.cmdConfig.Set("jira-project", "")
} else {
c.log.Debug("Using project list from configuration file")

var projects []Project

c.cmdConfig.UnmarshalKey("projects", &projects)

for i, project := range projects {
if project.Repo == "" {
return fmt.Errorf("project number %d is missing a repo", i)
}
if !strings.Contains(project.Repo, "/") || len(strings.Split(project.Repo, "/")) != 2 {
return fmt.Errorf("project number %d has bad repo; must be user/repo or org/repo", i)
}
if project.Key == "" {
return fmt.Errorf("project number %d is missing JIRA project key", i)
}
}
}

sinceStr := c.cmdConfig.GetString("since")
Expand Down
26 changes: 19 additions & 7 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"time"

"github.com/Sirupsen/logrus"
"github.com/andygrunwald/go-jira"
"github.com/coreos/issue-sync/cfg"
"github.com/coreos/issue-sync/lib"
"github.com/coreos/issue-sync/lib/clients"
Expand Down Expand Up @@ -32,18 +33,29 @@ var RootCmd = &cobra.Command{

log := config.GetLogger()

jiraClient, err := clients.NewJIRAClient(&config)
if err != nil {
return err
}
ghClient, err := clients.NewGitHubClient(config)
// Create a temporary JIRA client which we can use to populate the
// configuration object with all of the JIRA settings (projects,
// field IDs, etc.)
rootJCli, err := clients.NewJIRAClient(config, jira.Project{})
if err != nil {
return err
}
config.LoadJIRAConfig(rootJCli.GetClient())

for {
if err := lib.CompareIssues(config, ghClient, jiraClient); err != nil {
log.Error(err)
for _, repo := range config.GetRepoList() {
ghClient, err := clients.NewGitHubClient(config, repo)
if err != nil {
return err
}
jiraClient, err := clients.NewJIRAClient(config, config.GetProject(repo))
if err != nil {
return err
}

if err := lib.CompareIssues(config, ghClient, jiraClient); err != nil {
return err
}
}
if !config.IsDryRun() {
if err := config.SaveConfig(); err != nil {
Expand Down
24 changes: 21 additions & 3 deletions lib/clients/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ type GitHubClient interface {
ListComments(issue github.Issue) ([]*github.IssueComment, error)
GetUser(login string) (github.User, error)
GetRateLimits() (github.RateLimits, error)
GetRepo() string
GetRepoSplit() (string, string)
}

// realGHClient is a standard GitHub clients, that actually makes all of the
Expand All @@ -28,6 +30,7 @@ type GitHubClient interface {
type realGHClient struct {
config cfg.Config
client github.Client
repo string
}

// ListIssues returns the list of GitHub issues since the last run of the tool.
Expand All @@ -36,7 +39,7 @@ func (g realGHClient) ListIssues() ([]github.Issue, error) {

ctx := context.Background()

user, repo := g.config.GetRepo()
user, repo := g.GetRepoSplit()

// Set it so that it will run the loop once, and it'll be updated in the loop.
pages := 1
Expand Down Expand Up @@ -87,7 +90,7 @@ func (g realGHClient) ListComments(issue github.Issue) ([]*github.IssueComment,
log := g.config.GetLogger()

ctx := context.Background()
user, repo := g.config.GetRepo()
user, repo := g.GetRepoSplit()
c, _, err := g.request(func() (interface{}, *github.Response, error) {
return g.client.Issues.ListComments(ctx, user, repo, issue.GetNumber(), &github.IssueListCommentsOptions{
Sort: "created",
Expand Down Expand Up @@ -153,6 +156,20 @@ func (g realGHClient) GetRateLimits() (github.RateLimits, error) {

const retryBackoffRoundRatio = time.Millisecond / time.Nanosecond

// GetRepo returns the user/repo form name of the GitHub repository the client
// has been configured with.

func (g realGHClient) GetRepo() string {
return g.repo
}

// GetRepoSplit returns the username and repo name of the GitHub repository
// the client has been configured with.

func (g realGHClient) GetRepoSplit() (string, string) {
return g.config.GetRepo(g.repo)
}

// request takes an API function from the GitHub library
// and calls it with exponential backoff. If the function succeeds, it
// returns the expected value and the GitHub API response, as well as a nil
Expand Down Expand Up @@ -189,7 +206,7 @@ func (g realGHClient) request(f func() (interface{}, *github.Response, error)) (
// run. For example, a dry-run clients may be created which does
// not make any requests that would change anything on the server,
// but instead simply prints out the actions that it's asked to take.
func NewGitHubClient(config cfg.Config) (GitHubClient, error) {
func NewGitHubClient(config cfg.Config, repo string) (GitHubClient, error) {
var ret GitHubClient

log := config.GetLogger()
Expand All @@ -205,6 +222,7 @@ func NewGitHubClient(config cfg.Config) (GitHubClient, error) {
ret = realGHClient{
config: config,
client: *client,
repo: repo,
}

// Make a request so we can check that we can connect fine.
Expand Down
Loading

0 comments on commit 84c3d08

Please sign in to comment.