github.com/trevoraustin/hub@v2.2.0-preview1.0.20141105230840-96d8bfc654cc+incompatible/github/client.go (about)

     1  package github
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"net/url"
     7  	"os"
     8  	"strings"
     9  
    10  	"github.com/octokit/go-octokit/octokit"
    11  )
    12  
    13  const (
    14  	GitHubHost    string = "github.com"
    15  	GitHubApiHost string = "api.github.com"
    16  	UserAgent     string = "Hub"
    17  	OAuthAppName  string = "hub"
    18  	OAuthAppURL   string = "http://hub.github.com/"
    19  )
    20  
    21  func NewClient(h string) *Client {
    22  	return NewClientWithHost(&Host{Host: h})
    23  }
    24  
    25  func NewClientWithHost(host *Host) *Client {
    26  	return &Client{host}
    27  }
    28  
    29  type AuthError struct {
    30  	error
    31  }
    32  
    33  func (e *AuthError) Error() string {
    34  	return e.error.Error()
    35  }
    36  
    37  func (e *AuthError) Is2FAError() bool {
    38  	re, ok := e.error.(*octokit.ResponseError)
    39  	return ok && re.Type == octokit.ErrorOneTimePasswordRequired
    40  }
    41  
    42  type Client struct {
    43  	Host *Host
    44  }
    45  
    46  func (client *Client) PullRequest(project *Project, id string) (pr *octokit.PullRequest, err error) {
    47  	url, err := octokit.PullRequestsURL.Expand(octokit.M{"owner": project.Owner, "repo": project.Name, "number": id})
    48  	if err != nil {
    49  		return
    50  	}
    51  
    52  	api, err := client.api()
    53  	if err != nil {
    54  		err = FormatError("getting pull request", err)
    55  		return
    56  	}
    57  
    58  	pr, result := api.PullRequests(client.requestURL(url)).One()
    59  	if result.HasError() {
    60  		err = FormatError("getting pull request", result.Err)
    61  		return
    62  	}
    63  
    64  	return
    65  }
    66  
    67  func (client *Client) PullRequestPatch(project *Project, id string) (patch io.ReadCloser, err error) {
    68  	url, err := octokit.PullRequestsURL.Expand(octokit.M{"owner": project.Owner, "repo": project.Name, "number": id})
    69  	if err != nil {
    70  		return
    71  	}
    72  
    73  	api, err := client.api()
    74  	if err != nil {
    75  		err = FormatError("getting pull request", err)
    76  		return
    77  	}
    78  
    79  	patch, result := api.PullRequests(client.requestURL(url)).Patch()
    80  	if result.HasError() {
    81  		err = FormatError("getting pull request", result.Err)
    82  		return
    83  	}
    84  
    85  	return
    86  }
    87  
    88  func (client *Client) CreatePullRequest(project *Project, base, head, title, body string) (pr *octokit.PullRequest, err error) {
    89  	url, err := octokit.PullRequestsURL.Expand(octokit.M{"owner": project.Owner, "repo": project.Name})
    90  	if err != nil {
    91  		return
    92  	}
    93  
    94  	api, err := client.api()
    95  	if err != nil {
    96  		err = FormatError("creating pull request", err)
    97  		return
    98  	}
    99  
   100  	params := octokit.PullRequestParams{Base: base, Head: head, Title: title, Body: body}
   101  	pr, result := api.PullRequests(client.requestURL(url)).Create(params)
   102  	if result.HasError() {
   103  		err = FormatError("creating pull request", result.Err)
   104  		if e := warnExistenceOfRepo(project, result.Err); e != nil {
   105  			err = fmt.Errorf("%s\n%s", err, e)
   106  		}
   107  
   108  		return
   109  	}
   110  
   111  	return
   112  }
   113  
   114  func (client *Client) CreatePullRequestForIssue(project *Project, base, head, issue string) (pr *octokit.PullRequest, err error) {
   115  	url, err := octokit.PullRequestsURL.Expand(octokit.M{"owner": project.Owner, "repo": project.Name})
   116  	if err != nil {
   117  		return
   118  	}
   119  
   120  	api, err := client.api()
   121  	if err != nil {
   122  		err = FormatError("creating pull request", err)
   123  		return
   124  	}
   125  
   126  	params := octokit.PullRequestForIssueParams{Base: base, Head: head, Issue: issue}
   127  	pr, result := api.PullRequests(client.requestURL(url)).Create(params)
   128  	if result.HasError() {
   129  		err = FormatError("creating pull request", result.Err)
   130  		if e := warnExistenceOfRepo(project, result.Err); e != nil {
   131  			err = fmt.Errorf("%s\n%s", err, e)
   132  		}
   133  
   134  		return
   135  	}
   136  
   137  	return
   138  }
   139  
   140  func (client *Client) CommitPatch(project *Project, sha string) (patch io.ReadCloser, err error) {
   141  	url, err := octokit.CommitsURL.Expand(octokit.M{"owner": project.Owner, "repo": project.Name, "sha": sha})
   142  	if err != nil {
   143  		return
   144  	}
   145  
   146  	api, err := client.api()
   147  	if err != nil {
   148  		err = FormatError("getting pull request", err)
   149  		return
   150  	}
   151  
   152  	patch, result := api.Commits(client.requestURL(url)).Patch()
   153  	if result.HasError() {
   154  		err = FormatError("getting pull request", result.Err)
   155  		return
   156  	}
   157  
   158  	return
   159  }
   160  
   161  func (client *Client) GistPatch(id string) (patch io.ReadCloser, err error) {
   162  	url, err := octokit.GistsURL.Expand(octokit.M{"gist_id": id})
   163  	if err != nil {
   164  		return
   165  	}
   166  
   167  	api, err := client.api()
   168  	if err != nil {
   169  		err = FormatError("getting pull request", err)
   170  		return
   171  	}
   172  
   173  	patch, result := api.Gists(client.requestURL(url)).Raw()
   174  	if result.HasError() {
   175  		err = FormatError("getting pull request", result.Err)
   176  		return
   177  	}
   178  
   179  	return
   180  }
   181  
   182  func (client *Client) Repository(project *Project) (repo *octokit.Repository, err error) {
   183  	url, err := octokit.RepositoryURL.Expand(octokit.M{"owner": project.Owner, "repo": project.Name})
   184  	if err != nil {
   185  		return
   186  	}
   187  
   188  	api, err := client.api()
   189  	if err != nil {
   190  		err = FormatError("getting repository", err)
   191  		return
   192  	}
   193  
   194  	repo, result := api.Repositories(client.requestURL(url)).One()
   195  	if result.HasError() {
   196  		err = FormatError("getting repository", result.Err)
   197  		return
   198  	}
   199  
   200  	return
   201  }
   202  
   203  func (client *Client) IsRepositoryExist(project *Project) bool {
   204  	repo, err := client.Repository(project)
   205  
   206  	return err == nil && repo != nil
   207  }
   208  
   209  func (client *Client) CreateRepository(project *Project, description, homepage string, isPrivate bool) (repo *octokit.Repository, err error) {
   210  	var repoURL octokit.Hyperlink
   211  	if project.Owner != client.Host.User {
   212  		repoURL = octokit.OrgRepositoriesURL
   213  	} else {
   214  		repoURL = octokit.UserRepositoriesURL
   215  	}
   216  
   217  	url, err := repoURL.Expand(octokit.M{"org": project.Owner})
   218  	if err != nil {
   219  		return
   220  	}
   221  
   222  	api, err := client.api()
   223  	if err != nil {
   224  		err = FormatError("creating repository", err)
   225  		return
   226  	}
   227  
   228  	params := octokit.Repository{
   229  		Name:        project.Name,
   230  		Description: description,
   231  		Homepage:    homepage,
   232  		Private:     isPrivate,
   233  	}
   234  	repo, result := api.Repositories(client.requestURL(url)).Create(params)
   235  	if result.HasError() {
   236  		err = FormatError("creating repository", result.Err)
   237  		return
   238  	}
   239  
   240  	return
   241  }
   242  
   243  func (client *Client) Releases(project *Project) (releases []octokit.Release, err error) {
   244  	url, err := octokit.ReleasesURL.Expand(octokit.M{"owner": project.Owner, "repo": project.Name})
   245  	if err != nil {
   246  		return
   247  	}
   248  
   249  	api, err := client.api()
   250  	if err != nil {
   251  		err = FormatError("getting release", err)
   252  		return
   253  	}
   254  
   255  	releases, result := api.Releases(client.requestURL(url)).All()
   256  	if result.HasError() {
   257  		err = FormatError("getting release", result.Err)
   258  		return
   259  	}
   260  
   261  	return
   262  }
   263  
   264  func (client *Client) CreateRelease(project *Project, params octokit.ReleaseParams) (release *octokit.Release, err error) {
   265  	url, err := octokit.ReleasesURL.Expand(octokit.M{"owner": project.Owner, "repo": project.Name})
   266  	if err != nil {
   267  		return
   268  	}
   269  
   270  	api, err := client.api()
   271  	if err != nil {
   272  		err = FormatError("creating release", err)
   273  		return
   274  	}
   275  
   276  	release, result := api.Releases(client.requestURL(url)).Create(params)
   277  	if result.HasError() {
   278  		err = FormatError("creating release", result.Err)
   279  		return
   280  	}
   281  
   282  	return
   283  }
   284  
   285  func (client *Client) UploadReleaseAsset(uploadUrl *url.URL, asset *os.File, contentType string) (err error) {
   286  	fileInfo, err := asset.Stat()
   287  	if err != nil {
   288  		return
   289  	}
   290  
   291  	api, err := client.api()
   292  	if err != nil {
   293  		err = FormatError("uploading asset", err)
   294  		return
   295  	}
   296  
   297  	result := api.Uploads(uploadUrl).UploadAsset(asset, contentType, fileInfo.Size())
   298  	if result.HasError() {
   299  		err = FormatError("uploading asset", result.Err)
   300  		return
   301  	}
   302  
   303  	return
   304  }
   305  
   306  func (client *Client) CIStatus(project *Project, sha string) (status *octokit.Status, err error) {
   307  	url, err := octokit.StatusesURL.Expand(octokit.M{"owner": project.Owner, "repo": project.Name, "ref": sha})
   308  	if err != nil {
   309  		return
   310  	}
   311  
   312  	api, err := client.api()
   313  	if err != nil {
   314  		err = FormatError("getting CI status", err)
   315  		return
   316  	}
   317  
   318  	statuses, result := api.Statuses(client.requestURL(url)).All()
   319  	if result.HasError() {
   320  		err = FormatError("getting CI status", result.Err)
   321  		return
   322  	}
   323  
   324  	if len(statuses) > 0 {
   325  		status = &statuses[0]
   326  	}
   327  
   328  	return
   329  }
   330  
   331  func (client *Client) ForkRepository(project *Project) (repo *octokit.Repository, err error) {
   332  	url, err := octokit.ForksURL.Expand(octokit.M{"owner": project.Owner, "repo": project.Name})
   333  	if err != nil {
   334  		return
   335  	}
   336  
   337  	api, err := client.api()
   338  	if err != nil {
   339  		err = FormatError("creating fork", err)
   340  		return
   341  	}
   342  
   343  	repo, result := api.Repositories(client.requestURL(url)).Create(nil)
   344  	if result.HasError() {
   345  		err = FormatError("creating fork", result.Err)
   346  		return
   347  	}
   348  
   349  	return
   350  }
   351  
   352  func (client *Client) Issues(project *Project) (issues []octokit.Issue, err error) {
   353  	url, err := octokit.RepoIssuesURL.Expand(octokit.M{"owner": project.Owner, "repo": project.Name})
   354  	if err != nil {
   355  		return
   356  	}
   357  
   358  	api, err := client.api()
   359  	if err != nil {
   360  		err = FormatError("getting issues", err)
   361  		return
   362  	}
   363  
   364  	issues, result := api.Issues(client.requestURL(url)).All()
   365  	if result.HasError() {
   366  		err = FormatError("getting issues", result.Err)
   367  		return
   368  	}
   369  
   370  	return
   371  }
   372  
   373  func (client *Client) CreateIssue(project *Project, title, body string, labels []string) (issue *octokit.Issue, err error) {
   374  	url, err := octokit.RepoIssuesURL.Expand(octokit.M{"owner": project.Owner, "repo": project.Name})
   375  	if err != nil {
   376  		return
   377  	}
   378  
   379  	api, err := client.api()
   380  	if err != nil {
   381  		err = FormatError("creating issues", err)
   382  		return
   383  	}
   384  
   385  	params := octokit.IssueParams{
   386  		Title:  title,
   387  		Body:   body,
   388  		Labels: labels,
   389  	}
   390  	issue, result := api.Issues(client.requestURL(url)).Create(params)
   391  	if result.HasError() {
   392  		err = FormatError("creating issue", result.Err)
   393  		return
   394  	}
   395  
   396  	return
   397  }
   398  
   399  func (client *Client) GhLatestTagName() (tagName string, err error) {
   400  	url, err := octokit.ReleasesURL.Expand(octokit.M{"owner": "jingweno", "repo": "gh"})
   401  	if err != nil {
   402  		return
   403  	}
   404  
   405  	c := client.newOctokitClient(nil)
   406  	releases, result := c.Releases(client.requestURL(url)).All()
   407  	if result.HasError() {
   408  		err = fmt.Errorf("Error getting gh release: %s", result.Err)
   409  		return
   410  	}
   411  
   412  	if len(releases) == 0 {
   413  		err = fmt.Errorf("No gh release is available")
   414  		return
   415  	}
   416  
   417  	tagName = releases[0].TagName
   418  
   419  	return
   420  }
   421  
   422  func (client *Client) CurrentUser() (user *octokit.User, err error) {
   423  	url, err := octokit.CurrentUserURL.Expand(nil)
   424  	if err != nil {
   425  		return
   426  	}
   427  
   428  	api, err := client.api()
   429  	if err != nil {
   430  		err = FormatError("getting current user", err)
   431  		return
   432  	}
   433  
   434  	user, result := api.Users(client.requestURL(url)).One()
   435  	if result.HasError() {
   436  		err = FormatError("getting current user", result.Err)
   437  		return
   438  	}
   439  
   440  	return
   441  }
   442  
   443  func (client *Client) FindOrCreateToken(user, password, twoFactorCode string) (token string, err error) {
   444  	authUrl, e := octokit.AuthorizationsURL.Expand(nil)
   445  	if e != nil {
   446  		err = &AuthError{e}
   447  		return
   448  	}
   449  
   450  	basicAuth := octokit.BasicAuth{Login: user, Password: password, OneTimePassword: twoFactorCode}
   451  	c := client.newOctokitClient(basicAuth)
   452  	authsService := c.Authorizations(client.requestURL(authUrl))
   453  
   454  	if twoFactorCode != "" {
   455  		// dummy request to trigger a 2FA SMS since a HTTP GET won't do it
   456  		authsService.Create(nil)
   457  	}
   458  
   459  	auths, result := authsService.All()
   460  	if result.HasError() {
   461  		err = &AuthError{result.Err}
   462  		return
   463  	}
   464  
   465  	var moreAuths []octokit.Authorization
   466  	for result.NextPage != nil {
   467  		authUrl, e := result.NextPage.Expand(nil)
   468  		if e != nil {
   469  			return "", e
   470  		}
   471  		authUrl, _ = url.Parse(authUrl.RequestURI())
   472  
   473  		as := c.Authorizations(authUrl)
   474  		moreAuths, result = as.All()
   475  		if result.HasError() {
   476  			err = &AuthError{result.Err}
   477  			return
   478  		}
   479  
   480  		auths = append(auths, moreAuths...)
   481  	}
   482  
   483  	for _, auth := range auths {
   484  		if auth.Note == OAuthAppName || auth.NoteURL == OAuthAppURL {
   485  			token = auth.Token
   486  			break
   487  		}
   488  	}
   489  
   490  	if token == "" {
   491  		authParam := octokit.AuthorizationParams{}
   492  		authParam.Scopes = append(authParam.Scopes, "repo")
   493  		authParam.Note = OAuthAppName
   494  		authParam.NoteURL = OAuthAppURL
   495  
   496  		auth, result := authsService.Create(authParam)
   497  		if result.HasError() {
   498  			err = &AuthError{result.Err}
   499  			return
   500  		}
   501  
   502  		token = auth.Token
   503  	}
   504  
   505  	return
   506  }
   507  
   508  func (client *Client) api() (c *octokit.Client, err error) {
   509  	if client.Host.AccessToken == "" {
   510  		host, e := CurrentConfig().PromptForHost(client.Host.Host)
   511  		if e != nil {
   512  			err = e
   513  			return
   514  		}
   515  		client.Host = host
   516  	}
   517  
   518  	tokenAuth := octokit.TokenAuth{AccessToken: client.Host.AccessToken}
   519  	c = client.newOctokitClient(tokenAuth)
   520  
   521  	return
   522  }
   523  
   524  func (client *Client) newOctokitClient(auth octokit.AuthMethod) *octokit.Client {
   525  	var host string
   526  	if client.Host != nil {
   527  		host = client.Host.Host
   528  	}
   529  	host = normalizeHost(host)
   530  	apiHostURL := client.absolute(host)
   531  
   532  	httpClient := newHttpClient(os.Getenv("HUB_TEST_HOST"), os.Getenv("HUB_VERBOSE") != "")
   533  	c := octokit.NewClientWith(apiHostURL.String(), UserAgent, auth, httpClient)
   534  
   535  	return c
   536  }
   537  
   538  func (client *Client) absolute(endpoint string) *url.URL {
   539  	u, _ := url.Parse(endpoint)
   540  	if u.Scheme == "" && client.Host != nil {
   541  		u.Scheme = client.Host.Protocol
   542  	}
   543  	if u.Scheme == "" {
   544  		u.Scheme = "https"
   545  	}
   546  
   547  	return u
   548  }
   549  
   550  func (client *Client) requestURL(u *url.URL) (uu *url.URL) {
   551  	uu = u
   552  	if client.Host != nil && client.Host.Host != GitHubHost {
   553  		uu, _ = url.Parse(fmt.Sprintf("/api/v3/%s", u.Path))
   554  	}
   555  
   556  	return
   557  }
   558  
   559  func normalizeHost(host string) string {
   560  	host = strings.ToLower(host)
   561  	if host == "" {
   562  		host = GitHubHost
   563  	}
   564  
   565  	if host == GitHubHost {
   566  		host = GitHubApiHost
   567  	}
   568  
   569  	return host
   570  }
   571  
   572  func FormatError(action string, err error) (ee error) {
   573  	switch e := err.(type) {
   574  	default:
   575  		ee = err
   576  	case *octokit.ResponseError:
   577  		statusCode := e.Response.StatusCode
   578  		var reason string
   579  		if s := strings.SplitN(e.Response.Status, " ", 2); len(s) >= 2 {
   580  			reason = strings.TrimSpace(s[1])
   581  		}
   582  
   583  		errStr := fmt.Sprintf("Error %s: %s (HTTP %d)", action, reason, statusCode)
   584  
   585  		var messages []string
   586  		if statusCode == 422 {
   587  			if e.Message != "" {
   588  				messages = append(messages, e.Message)
   589  			}
   590  
   591  			if len(e.Errors) > 0 {
   592  				for _, e := range e.Errors {
   593  					messages = append(messages, e.Error())
   594  				}
   595  			}
   596  		}
   597  
   598  		if len(messages) > 0 {
   599  			errStr = fmt.Sprintf("%s\n%s", errStr, strings.Join(messages, "\n"))
   600  		}
   601  
   602  		ee = fmt.Errorf(errStr)
   603  	case *AuthError:
   604  		errStr := fmt.Sprintf("Error %s: Unauthorized (HTTP 401)", action)
   605  		ee = fmt.Errorf(errStr)
   606  	}
   607  
   608  	return
   609  }
   610  
   611  func warnExistenceOfRepo(project *Project, ee error) (err error) {
   612  	if e, ok := ee.(*octokit.ResponseError); ok && e.Response.StatusCode == 404 {
   613  		var url string
   614  		if s := strings.SplitN(project.WebURL("", "", ""), "://", 2); len(s) >= 2 {
   615  			url = s[1]
   616  		}
   617  		if url != "" {
   618  			err = fmt.Errorf("Are you sure that %s exists?", url)
   619  		}
   620  	}
   621  
   622  	return
   623  }