Repository: kubesphere/ks-devops
Pull request #476 (2022-02-24 14:14:52)

Add API of listing organizations and repositories
<!-- Thanks for sending a pull request! Here are some tips for you:

1. If you want **faster** PR reviews, read how: https://github.com/kubesphere/community/blob/master/developer-guide/development/the-pr-author-guide-to-getting-through-code-review.md
2. In case you want to know how your PR got reviewed, read: https://github.com/kubesphere/community/blob/master/developer-guide/development/code-review-guide.md
3. Here are some coding conventions followed by the KubeSphere community: https://github.com/kubesphere/community/blob/master/developer-guide/development/coding-conventions.md
4. Additional open-source best practice: https://github.com/LinuxSuRen/open-source-best-practice
-->

### What type of PR is this?
<!-- 
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind design
/kind chore

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->
/kind feature

### What this PR does / why we need it:
Listing the organizations and repositories should not rely on the Jenkins.

### Which issue(s) this PR fixes:
<!--
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
Please leave it or change # to be None if there is no corresponding issue that exists
-->
Fixes #

### Special notes for reviewers:
<!--
You can use the following command to let the DevOps SIG members help you to review your PR.
/cc @kubesphere/sig-devops 
And please avoid cc any individual.
-->
Please check the following list before waiting reviewers:

- [ ] Already committed the CRD files to [the Helm Chart](https://github.com/kubesphere-sigs/ks-devops-helm-chart/) if you created some new CRDs
- [ ] Already [added the permission](https://github.com/kubesphere/ks-installer/blob/9e063b085a0e43fdb3d0d9e3e7f4149146f14b9c/roles/ks-core/prepare/files/ks-init/role-templates.yaml) for the new API
  see #475
- [ ] Already added the RBAC markers for the new controllers

### Does this PR introduce a user-facing change??
<!--
If no, just write "None" in the release-note block below.
If yes, a release note is required:
Enter your extended-release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".

Please keep the note be same as your PR title if you believe it should be in the release notes.
-->
```release-note
None
```


ks-ci-bot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubesphere/ks-devops/pull/476#" title="Author self-approved">LinuxSuRen</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubesphere%2Fks-devops).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubesphere/ks-devops/blob/master/OWNERS)~~ [LinuxSuRen]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
codecov[bot]: # [Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/476?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) Report
> Merging [#476](https://codecov.io/gh/kubesphere/ks-devops/pull/476?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (23b2c80) into [master](https://codecov.io/gh/kubesphere/ks-devops/commit/7090ba5f4af621bd33eff993017092d1e80b9bc6?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (7090ba5) will **increase** coverage by `0.10%`.
> The diff coverage is `88.23%`.

> :exclamation: Current head 23b2c80 differs from pull request most recent head 2d06097. Consider uploading reports for the commit 2d06097 to get more accurate results

[![Impacted file tree graph](https://codecov.io/gh/kubesphere/ks-devops/pull/476/graphs/tree.svg?width=650&height=150&src=pr&token=qSK4RBbyNg&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere)](https://codecov.io/gh/kubesphere/ks-devops/pull/476?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere)

```diff
@@            Coverage Diff             @@
##           master     #476      +/-   ##
==========================================
+ Coverage   76.94%   77.05%   +0.10%     
==========================================
  Files         132      133       +1     
  Lines       22676    22745      +69     
==========================================
+ Hits        17448    17526      +78     
+ Misses       5038     5025      -13     
- Partials      190      194       +4     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `77.05% <88.23%> (+0.10%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/kubesphere/ks-devops/pull/476?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) | Coverage Δ | |
|---|---|---|
| [pkg/client/git/message.go](https://codecov.io/gh/kubesphere/ks-devops/pull/476/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2NsaWVudC9naXQvbWVzc2FnZS5nbw==) | `100.00% <ø> (ø)` | |
| [pkg/kapis/devops/v1alpha3/scm/scmhandler.go](https://codecov.io/gh/kubesphere/ks-devops/pull/476/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2thcGlzL2Rldm9wcy92MWFscGhhMy9zY20vc2NtaGFuZGxlci5nbw==) | `82.60% <83.05%> (+76.72%)` | :arrow_up: |
| [pkg/kapis/devops/v1alpha2/register.go](https://codecov.io/gh/kubesphere/ks-devops/pull/476/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2thcGlzL2Rldm9wcy92MWFscGhhMi9yZWdpc3Rlci5nbw==) | `91.80% <100.00%> (+0.02%)` | :arrow_up: |
| [pkg/kapis/devops/v1alpha3/register.go](https://codecov.io/gh/kubesphere/ks-devops/pull/476/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2thcGlzL2Rldm9wcy92MWFscGhhMy9yZWdpc3Rlci5nbw==) | `100.00% <100.00%> (ø)` | |
| [pkg/kapis/devops/v1alpha3/scm/route.go](https://codecov.io/gh/kubesphere/ks-devops/pull/476/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2thcGlzL2Rldm9wcy92MWFscGhhMy9zY20vcm91dGUuZ28=) | `100.00% <100.00%> (ø)` | |
| [controllers/s2ibinary/s2ibinary\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/476/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvczJpYmluYXJ5L3MyaWJpbmFyeV9jb250cm9sbGVyLmdv) | `38.00% <0.00%> (+5.00%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/476?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/476?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere). Last update [7090ba5...2d06097](https://codecov.io/gh/kubesphere/ks-devops/pull/476?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere).

ks-ci-bot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubesphere/ks-devops/pull/476#pullrequestreview-893208565" title="LGTM">JohnNiang</a>*, *<a href="https://github.com/kubesphere/ks-devops/pull/476#" title="Author self-approved">LinuxSuRen</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubesphere%2Fks-devops).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubesphere/ks-devops/blob/master/OWNERS)~~ [LinuxSuRen]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go
index 045dca1..65b810c 100644
--- a/pkg/apiserver/apiserver.go
+++ b/pkg/apiserver/apiserver.go
@@ -183,6 +183,7 @@ func (s *APIServer) Run(stopCh <-chan struct{}) (err error) {
 	}()
 
 	klog.V(0).Infof("Start listening on %s", s.Server.Addr)
+	klog.V(0).Infof("Open the swagger-ui from http://localhost%s/apidocs/?url=http://localhost:9090/apidocs.json", s.Server.Addr)
 	if s.Server.TLSConfig != nil {
 		err = s.Server.ListenAndServeTLS("", "")
 	} else {
diff --git a/pkg/client/git/message.go b/pkg/client/git/message.go
index 49a993b..e882449 100644
--- a/pkg/client/git/message.go
+++ b/pkg/client/git/message.go
@@ -21,7 +21,8 @@ type VerifyResponse struct {
 	// Message is the detail of the result
 	Message string `json:"message"`
 	// Code represents a group of cases
-	Code int `json:"code"`
+	Code         int    `json:"code"`
+	CredentialID string `json:"credentialId,omitempty" description:"the id of credential"`
 }
 
 func VerifyPass() *VerifyResponse {
diff --git a/pkg/kapis/devops/v1alpha2/register.go b/pkg/kapis/devops/v1alpha2/register.go
index 0d9da9a..4ece5ef 100644
--- a/pkg/kapis/devops/v1alpha2/register.go
+++ b/pkg/kapis/devops/v1alpha2/register.go
@@ -533,6 +533,7 @@ func AddPipelineToWebService(webservice *restful.WebService, devopsClient devops
 		// match "/blue/rest/organizations/jenkins/scm/{scm}/organizations/?credentialId=github"
 		webservice.Route(webservice.GET("/scms/{scm}/organizations").
 			To(projectPipelineHandler.GetSCMOrg).
+			Deprecate().
 			Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsScmTag}).
 			Doc("List all organizations of the specified source configuration management (SCM) such as Github.").
 			Param(webservice.PathParameter("scm", "the ID of the source configuration management (SCM).")).
@@ -543,6 +544,7 @@ func AddPipelineToWebService(webservice *restful.WebService, devopsClient devops
 		// match "/blue/rest/organizations/jenkins/scm/{scm}/organizations/{organization}/repositories/?credentialId=&pageNumber&pageSize="
 		webservice.Route(webservice.GET("/scms/{scm}/organizations/{organization}/repositories").
 			To(projectPipelineHandler.GetOrgRepo).
+			Deprecate().
 			Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsScmTag}).
 			Doc("List all repositories in the specified organization.").
 			Param(webservice.PathParameter("scm", "The ID of the source configuration management (SCM).")).
diff --git a/pkg/kapis/devops/v1alpha3/register.go b/pkg/kapis/devops/v1alpha3/register.go
index cbbfe99..478b798 100644
--- a/pkg/kapis/devops/v1alpha3/register.go
+++ b/pkg/kapis/devops/v1alpha3/register.go
@@ -19,7 +19,6 @@
 package v1alpha3
 
 import (
-	"kubesphere.io/devops/pkg/client/git"
 	"kubesphere.io/devops/pkg/kapis/devops/v1alpha3/scm"
 	"net/http"
 
@@ -68,7 +67,7 @@ func registerRoutes(devopsClient devopsClient.Interface, k8sClient k8s.Client, c
 	registerRoutersForCredentials(handler, ws)
 	registerRoutersForPipelines(handler, ws)
 	registerRoutersForWorkspace(handler, ws)
-	registerRoutersForSCM(client, ws)
+	scm.RegisterRoutersForSCM(client, ws)
 }
 
 func registerRoutersForCredentials(handler *devopsHandler, ws *restful.WebService) {
@@ -204,15 +203,3 @@ func registerRoutersForWorkspace(handler *devopsHandler, ws *restful.WebService)
 		Returns(http.StatusOK, api.StatusOK, []v1alpha3.DevOpsProject{}).
 		Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsProjectTag}))
 }
-
-func registerRoutersForSCM(k8sClient client.Client, ws *restful.WebService) {
-	handler := scm.NewHandler(k8sClient)
-
-	ws.Route(ws.POST("/scms/{scm}/verify").
-		To(handler.Verify).
-		Param(ws.PathParameter("scm", "the SCM type")).
-		Param(ws.QueryParameter("secret", "the secret name")).
-		Param(ws.QueryParameter("secretNamespace", "the namespace of target secret")).
-		Doc("Verify the token of different git providers").
-		Returns(http.StatusOK, api.StatusOK, git.VerifyResponse{}))
-}
diff --git a/pkg/kapis/devops/v1alpha3/scm/route.go b/pkg/kapis/devops/v1alpha3/scm/route.go
new file mode 100644
index 0000000..0303a5d
--- /dev/null
+++ b/pkg/kapis/devops/v1alpha3/scm/route.go
@@ -0,0 +1,63 @@
+/*
+Copyright 2022 The KubeSphere Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package scm
+
+import (
+	"github.com/emicklei/go-restful"
+	"kubesphere.io/devops/pkg/api"
+	"kubesphere.io/devops/pkg/client/git"
+	"net/http"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+var (
+	pathParameterSCM          = restful.PathParameter("scm", "the SCM type")
+	pathParameterOrganization = restful.PathParameter("organization",
+		"The git provider organization. For a GitHub repository address: https://github.com/kubesphere/ks-devops. kubesphere is the organization name")
+	queryParameterSecret          = restful.QueryParameter("secret", "the secret name")
+	queryParameterSecretNamespace = restful.QueryParameter("secretNamespace", "the namespace of target secret")
+)
+
+// RegisterRoutersForSCM registers the APIs which related to scm
+func RegisterRoutersForSCM(k8sClient client.Client, ws *restful.WebService) {
+	handler := newHandler(k8sClient)
+
+	ws.Route(ws.POST("/scms/{scm}/verify").
+		To(handler.verify).
+		Param(pathParameterSCM).
+		Param(queryParameterSecret).
+		Param(queryParameterSecretNamespace).
+		Doc("verify the token of different git providers").
+		Returns(http.StatusOK, api.StatusOK, git.VerifyResponse{}))
+
+	ws.Route(ws.GET("/scms/{scm}/organizations").
+		To(handler.listOrganizations).
+		Param(pathParameterSCM).
+		Param(queryParameterSecret).
+		Param(queryParameterSecretNamespace).
+		Doc("List all the readable organizations").
+		Returns(http.StatusOK, api.StatusOK, []organization{}))
+
+	ws.Route(ws.GET("/scms/{scm}/organizations/{organization}/repositories").
+		To(handler.listRepositories).
+		Param(pathParameterSCM).
+		Param(pathParameterOrganization).
+		Param(queryParameterSecret).
+		Param(queryParameterSecretNamespace).
+		Doc("List all the readable repositories").
+		Returns(http.StatusOK, api.StatusOK, []repository{}))
+}
diff --git a/pkg/kapis/devops/v1alpha3/scm/scmhandler.go b/pkg/kapis/devops/v1alpha3/scm/scmhandler.go
index 18c6385..74eee5c 100644
--- a/pkg/kapis/devops/v1alpha3/scm/scmhandler.go
+++ b/pkg/kapis/devops/v1alpha3/scm/scmhandler.go
@@ -22,43 +22,134 @@ import (
 	"github.com/emicklei/go-restful"
 	v1 "k8s.io/api/core/v1"
 	"kubesphere.io/devops/pkg/client/git"
+	"kubesphere.io/devops/pkg/kapis"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
-// Handler holds all the API handlers of SCM
-type Handler struct {
+// handler holds all the API handlers of SCM
+type handler struct {
 	k8sClient client.Client
 }
 
 // NewHandler creates the instance of the SCM handler
-func NewHandler(k8sClient client.Client) *Handler {
-	return &Handler{k8sClient: k8sClient}
+func newHandler(k8sClient client.Client) *handler {
+	return &handler{k8sClient: k8sClient}
 }
 
-// Verify verifies a SCM auth
-func (h *Handler) Verify(request *restful.Request, response *restful.Response) {
+// verify checks a SCM auth
+func (h *handler) verify(request *restful.Request, response *restful.Response) {
 	scm := request.PathParameter("scm")
 	secretName := request.QueryParameter("secret")
 	secretNamespace := request.QueryParameter("secretNamespace")
 
+	_, code, err := h.getOrganizations(scm, secretName, secretNamespace, 1)
+
+	response.Header().Set(restful.HEADER_ContentType, restful.MIME_JSON)
+	verifyResult := git.VerifyResult(err, code)
+	verifyResult.CredentialID = secretName
+	_ = response.WriteAsJson(verifyResult)
+}
+
+func (h *handler) getOrganizations(scm, secret, namespace string, size int) (orgs []*goscm.Organization, code int, err error) {
 	factory := git.NewClientFactory(scm, &v1.SecretReference{
-		Namespace: secretNamespace, Name: secretName,
+		Namespace: namespace, Name: secret,
 	}, h.k8sClient)
 
 	var c *goscm.Client
-	var err error
-	var code int
+	if c, err = factory.GetClient(); err == nil {
+		var resp *goscm.Response
+
+		if orgs, resp, err = c.Organizations.List(context.TODO(), goscm.ListOptions{Size: size, Page: 1}); err == nil {
+			code = resp.Status
+		} else {
+			code = 101
+		}
+	} else {
+		code = 100
+	}
+	return
+}
 
+func (h *handler) getRepositories(scm, org, secret, namespace string, size int) (repos []*goscm.Repository, code int, err error) {
+	factory := git.NewClientFactory(scm, &v1.SecretReference{
+		Namespace: namespace, Name: secret,
+	}, h.k8sClient)
+
+	var c *goscm.Client
 	if c, err = factory.GetClient(); err == nil {
 		var resp *goscm.Response
 
-		if _, resp, err = c.Organizations.List(context.TODO(), goscm.ListOptions{Size: 1, Page: 1}); err == nil {
+		var allRepos []*goscm.Repository
+		if allRepos, resp, err = c.Repositories.List(context.TODO(), goscm.ListOptions{
+			Page: 1,
+			Size: size,
+		}); err == nil {
 			code = resp.Status
+
+			for i := range allRepos {
+				repo := allRepos[i]
+				if repo.Namespace == org {
+					repos = append(repos, repo)
+				}
+			}
+		} else {
+			code = 101
 		}
 	} else {
 		code = 100
 	}
+	return
+}
 
-	response.Header().Set(restful.HEADER_ContentType, restful.MIME_JSON)
-	_ = response.WriteAsJson(git.VerifyResult(err, code))
+func (h *handler) listOrganizations(req *restful.Request, rsp *restful.Response) {
+	scm := req.PathParameter("scm")
+	secretName := req.QueryParameter("secret")
+	secretNamespace := req.QueryParameter("secretNamespace")
+
+	orgs, _, err := h.getOrganizations(scm, secretName, secretNamespace, 1000)
+	if err != nil {
+		kapis.HandleError(req, rsp, err)
+	} else {
+		_ = rsp.WriteEntity(transformOrganizations(orgs))
+	}
+}
+
+func (h *handler) listRepositories(req *restful.Request, rsp *restful.Response) {
+	scm := req.PathParameter("scm")
+	organization := req.PathParameter("organization")
+	secretName := req.QueryParameter("secret")
+	secretNamespace := req.QueryParameter("secretNamespace")
+
+	repos, _, err := h.getRepositories(scm, organization, secretName, secretNamespace, 10000)
+	if err != nil {
+		kapis.HandleError(req, rsp, err)
+	} else {
+		_ = rsp.WriteEntity(transformRepositories(repos))
+	}
+}
+
+func transformOrganizations(orgs []*goscm.Organization) (result []organization) {
+	if orgs != nil {
+		result = make([]organization, len(orgs))
+		for i := range orgs {
+			result[i] = organization{
+				Name:   orgs[i].Name,
+				Avatar: orgs[i].Avatar,
+			}
+		}
+	}
+	return
+}
+
+func transformRepositories(repos []*goscm.Repository) (result []repository) {
+	if repos != nil {
+		result = make([]repository, len(repos))
+		for i := range repos {
+			result[i] = repository{
+				Name:          repos[i].Name,
+				DefaultBranch: repos[i].Branch,
+			}
+		}
+	}
+	return
 }
diff --git a/pkg/kapis/devops/v1alpha3/scm/types.go b/pkg/kapis/devops/v1alpha3/scm/types.go
new file mode 100644
index 0000000..fb8649c
--- /dev/null
+++ b/pkg/kapis/devops/v1alpha3/scm/types.go
@@ -0,0 +1,30 @@
+/*
+Copyright 2022 The KubeSphere Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package scm
+
+type organization struct {
+	Name string `json:"name"`
+	// Avatar is the image of an organization which comes from a git provider
+	// try to find a better way to have it. Now, keeping it just because we need to keep compatible with the Jenkins response
+	// example: https://avatars.githubusercontent.com/jenkinsci
+	Avatar string `json:"avatar"`
+}
+
+type repository struct {
+	DefaultBranch string `json:"defaultBranch"`
+	Name          string `json:"name"`
+}


Test patch:
diff --git a/pkg/kapis/devops/v1alpha3/register_test.go b/pkg/kapis/devops/v1alpha3/register_test.go
index 53a0c88..9deae44 100644
--- a/pkg/kapis/devops/v1alpha3/register_test.go
+++ b/pkg/kapis/devops/v1alpha3/register_test.go
@@ -18,9 +18,7 @@ package v1alpha3
 
 import (
 	"context"
-	"encoding/json"
 	"github.com/emicklei/go-restful"
-	"github.com/h2non/gock"
 	"github.com/stretchr/testify/assert"
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -29,7 +27,6 @@ import (
 	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
 	fakeclientset "kubesphere.io/devops/pkg/client/clientset/versioned/fake"
 	fakedevops "kubesphere.io/devops/pkg/client/devops/fake"
-	"kubesphere.io/devops/pkg/client/git"
 	"kubesphere.io/devops/pkg/client/k8s"
 	"kubesphere.io/devops/pkg/constants"
 	"net/http"
@@ -175,107 +172,6 @@ func TestAPIsExist(t *testing.T) {
 	}
 }
 
-func TestSCMAPI(t *testing.T) {
-	schema, err := v1alpha1.SchemeBuilder.Register().Build()
-	assert.Nil(t, err)
-
-	err = v1.SchemeBuilder.AddToScheme(schema)
-	assert.Nil(t, err)
-
-	container := restful.NewContainer()
-	AddToContainer(container, fakedevops.NewFakeDevops(nil),
-		k8s.NewFakeClientSets(k8sfake.NewSimpleClientset(), nil, nil, "", nil,
-			fakeclientset.NewSimpleClientset()),
-		fake.NewFakeClientWithScheme(schema, &v1.Secret{
-			ObjectMeta: metav1.ObjectMeta{
-				Name: "token", Namespace: "default",
-			},
-			Type: v1.SecretTypeOpaque,
-			Data: map[string][]byte{
-				v1.ServiceAccountTokenKey: []byte("token"),
-			},
-		}))
-
-	type args struct {
-		method string
-		uri    string
-	}
-	tests := []struct {
-		name    string
-		args    args
-		prepare func()
-		verify  func(code int, response []byte, t *testing.T)
-	}{{
-		name: "verify",
-		args: args{
-			method: http.MethodPost,
-			uri:    "/scms/github/verify?secret=token&secretNamespace=default",
-		},
-		prepare: func() {
-			var mockHeaders = map[string]string{
-				"X-GitHub-Request-Id":   "DD0E:6011:12F21A8:1926790:5A2064E2",
-				"X-RateLimit-Limit":     "60",
-				"X-RateLimit-Remaining": "59",
-				"X-RateLimit-Reset":     "1512076018",
-			}
-
-			var mockPageHeaders = map[string]string{
-				"Link": `<https://api.github.com/resource?page=1>; rel="next",` +
-					`<https://api.github.com/resource?page=1>; rel="prev",` +
-					`<https://api.github.com/resource?page=1>; rel="first",` +
-					`<https://api.github.com/resource?page=1>; rel="last"`,
-			}
-
-			gock.New("https://api.github.com").
-				Get("/user/orgs").
-				MatchParam("per_page", "1").
-				MatchParam("page", "1").
-				Reply(200).
-				Type("application/json").
-				SetHeaders(mockHeaders).
-				SetHeaders(mockPageHeaders).
-				File("testdata/orgs.json")
-		},
-		verify: func(code int, response []byte, t *testing.T) {
-			assert.Equal(t, 200, code)
-
-			resp := &git.VerifyResponse{}
-			err := json.Unmarshal(response, resp)
-			assert.Nil(t, err)
-			assert.Equal(t, "ok", resp.Message)
-		},
-	}, {
-		name: "verify against a fake scm type",
-		args: args{
-			method: http.MethodPost,
-			uri:    "/scms/fake/verify?secret=token&secretNamespace=default",
-		},
-		prepare: func() {},
-		verify: func(code int, response []byte, t *testing.T) {
-			assert.Equal(t, http.StatusOK, code)
-
-			resp := &git.VerifyResponse{}
-			err := json.Unmarshal(response, resp)
-			assert.Nil(t, err, string(response))
-			assert.Equal(t, 100, resp.Code)
-		},
-	}}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			defer gock.Off()
-			tt.prepare()
-
-			httpRequest, _ := http.NewRequest(tt.args.method,
-				"http://fake.com/kapis/devops.kubesphere.io/v1alpha3"+tt.args.uri, nil)
-			httpRequest = httpRequest.WithContext(context.WithValue(context.TODO(), constants.K8SToken, constants.ContextKeyK8SToken("")))
-
-			httpWriter := httptest.NewRecorder()
-			container.Dispatch(httpWriter, httpRequest)
-			tt.verify(httpWriter.Code, httpWriter.Body.Bytes(), t)
-		})
-	}
-}
-
 func TestGetDevOpsProject(t *testing.T) {
 	schema, err := v1alpha1.SchemeBuilder.Register().Build()
 	assert.Nil(t, err)
diff --git a/pkg/kapis/devops/v1alpha3/scm/scmhandler_test.go b/pkg/kapis/devops/v1alpha3/scm/scmhandler_test.go
index 45ed622..38d3af8 100644
--- a/pkg/kapis/devops/v1alpha3/scm/scmhandler_test.go
+++ b/pkg/kapis/devops/v1alpha3/scm/scmhandler_test.go
@@ -17,11 +17,209 @@ limitations under the License.
 package scm
 
 import (
+	"context"
+	"encoding/json"
+	"github.com/emicklei/go-restful"
+	"github.com/h2non/gock"
 	"github.com/stretchr/testify/assert"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtimeSchema "k8s.io/apimachinery/pkg/runtime/schema"
+	"kubesphere.io/devops/pkg/api"
+	"kubesphere.io/devops/pkg/api/devops/v1alpha1"
+	"kubesphere.io/devops/pkg/apiserver/runtime"
+	"kubesphere.io/devops/pkg/client/git"
+	"kubesphere.io/devops/pkg/constants"
+	"net/http"
+	"net/http/httptest"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 	"testing"
 )
 
 func TestNewHandler(t *testing.T) {
-	handler := NewHandler(nil)
+	handler := newHandler(nil)
 	assert.NotNil(t, handler)
 }
+
+func TestSCMAPI(t *testing.T) {
+	schema, err := v1alpha1.SchemeBuilder.Register().Build()
+	assert.Nil(t, err)
+
+	err = v1.SchemeBuilder.AddToScheme(schema)
+	assert.Nil(t, err)
+
+	type args struct {
+		method string
+		uri    string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		prepare func()
+		verify  func(code int, response []byte, t *testing.T)
+	}{{
+		name: "verify",
+		args: args{
+			method: http.MethodPost,
+			uri:    "/scms/github/verify?secret=token&secretNamespace=default",
+		},
+		prepare: func() {
+			var mockHeaders = map[string]string{
+				"X-GitHub-Request-Id":   "DD0E:6011:12F21A8:1926790:5A2064E2",
+				"X-RateLimit-Limit":     "60",
+				"X-RateLimit-Remaining": "59",
+				"X-RateLimit-Reset":     "1512076018",
+			}
+
+			var mockPageHeaders = map[string]string{
+				"Link": `<https://api.github.com/resource?page=1>; rel="next",` +
+					`<https://api.github.com/resource?page=1>; rel="prev",` +
+					`<https://api.github.com/resource?page=1>; rel="first",` +
+					`<https://api.github.com/resource?page=1>; rel="last"`,
+			}
+
+			gock.New("https://api.github.com").
+				Get("/user/orgs").
+				MatchParam("per_page", "1").
+				MatchParam("page", "1").
+				Reply(200).
+				Type("application/json").
+				SetHeaders(mockHeaders).
+				SetHeaders(mockPageHeaders).
+				File("testdata/orgs.json")
+		},
+		verify: func(code int, response []byte, t *testing.T) {
+			assert.Equal(t, 200, code)
+
+			resp := &git.VerifyResponse{}
+			err := json.Unmarshal(response, resp)
+			assert.Nil(t, err)
+			assert.Equal(t, "ok", resp.Message)
+			assert.Equal(t, "token", resp.CredentialID)
+		},
+	}, {
+		name: "verify against a fake scm type",
+		args: args{
+			method: http.MethodPost,
+			uri:    "/scms/fake/verify?secret=token&secretNamespace=default",
+		},
+		prepare: func() {},
+		verify: func(code int, response []byte, t *testing.T) {
+			assert.Equal(t, http.StatusOK, code)
+
+			resp := &git.VerifyResponse{}
+			err := json.Unmarshal(response, resp)
+			assert.Nil(t, err, string(response))
+			assert.Equal(t, 100, resp.Code)
+		},
+	}, {
+		name: "get the organization list",
+		args: args{
+			method: http.MethodGet,
+			uri:    "/scms/github/organizations?secret=token&secretNamespace=default",
+		},
+		prepare: func() {
+			var mockHeaders = map[string]string{
+				"X-GitHub-Request-Id":   "DD0E:6011:12F21A8:1926790:5A2064E2",
+				"X-RateLimit-Limit":     "60",
+				"X-RateLimit-Remaining": "59",
+				"X-RateLimit-Reset":     "1512076018",
+			}
+
+			var mockPageHeaders = map[string]string{
+				"Link": `<https://api.github.com/resource?page=1>; rel="next",` +
+					`<https://api.github.com/resource?page=1>; rel="prev",` +
+					`<https://api.github.com/resource?page=1>; rel="first",` +
+					`<https://api.github.com/resource?page=1>; rel="last"`,
+			}
+
+			gock.New("https://api.github.com").
+				Get("/user/orgs").
+				MatchParam("per_page", "1").
+				MatchParam("page", "1").
+				Reply(200).
+				Type("application/json").
+				SetHeaders(mockHeaders).
+				SetHeaders(mockPageHeaders).
+				File("testdata/orgs.json")
+		},
+		verify: func(code int, response []byte, t *testing.T) {
+			assert.Equal(t, 200, code)
+
+			var orgs []organization
+			err := json.Unmarshal(response, &orgs)
+			assert.Nil(t, err)
+			assert.Equal(t, 1, len(orgs))
+			assert.Equal(t, "https://github.com/images/error/octocat_happy.gif", orgs[0].Avatar)
+			assert.Equal(t, "github", orgs[0].Name)
+		},
+	}, {
+		name: "get the repository list",
+		args: args{
+			method: http.MethodGet,
+			uri:    "/scms/github/organizations/octocat/repositories?secret=token&secretNamespace=default",
+		},
+		prepare: func() {
+			var mockHeaders = map[string]string{
+				"X-GitHub-Request-Id":   "DD0E:6011:12F21A8:1926790:5A2064E2",
+				"X-RateLimit-Limit":     "60",
+				"X-RateLimit-Remaining": "59",
+				"X-RateLimit-Reset":     "1512076018",
+			}
+
+			var mockPageHeaders = map[string]string{
+				"Link": `<https://api.github.com/resource?page=1>; rel="next",` +
+					`<https://api.github.com/resource?page=1>; rel="prev",` +
+					`<https://api.github.com/resource?page=1>; rel="first",` +
+					`<https://api.github.com/resource?page=1>; rel="last"`,
+			}
+
+			gock.New("https://api.github.com").
+				Get("user/repos").
+				MatchParam("per_page", "1").
+				MatchParam("page", "1").
+				Reply(200).
+				Type("application/json").
+				SetHeaders(mockHeaders).
+				SetHeaders(mockPageHeaders).
+				File("testdata/repos.json")
+		},
+		verify: func(code int, response []byte, t *testing.T) {
+			assert.Equal(t, 200, code)
+
+			var repos []repository
+			err := json.Unmarshal(response, &repos)
+			assert.Nil(t, err)
+			assert.Equal(t, 1, len(repos))
+			assert.Equal(t, "Hello-World", repos[0].Name)
+			assert.Equal(t, "master", repos[0].DefaultBranch)
+		},
+	}}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			defer gock.Off()
+			tt.prepare()
+
+			httpRequest, _ := http.NewRequest(tt.args.method,
+				"http://fake.com/kapis/devops.kubesphere.io/v1alpha3"+tt.args.uri, nil)
+			httpRequest = httpRequest.WithContext(context.WithValue(context.TODO(), constants.K8SToken, constants.ContextKeyK8SToken("")))
+
+			ws := runtime.NewWebService(runtimeSchema.GroupVersion{Group: api.GroupName, Version: "v1alpha3"})
+			RegisterRoutersForSCM(fake.NewFakeClientWithScheme(schema, &v1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "token", Namespace: "default",
+				},
+				Type: v1.SecretTypeOpaque,
+				Data: map[string][]byte{
+					v1.ServiceAccountTokenKey: []byte("token"),
+				},
+			}), ws)
+			container := restful.NewContainer()
+			container.Add(ws)
+
+			httpWriter := httptest.NewRecorder()
+			container.Dispatch(httpWriter, httpRequest)
+			tt.verify(httpWriter.Code, httpWriter.Body.Bytes(), t)
+		})
+	}
+}
diff --git a/pkg/kapis/devops/v1alpha3/testdata/orgs.json b/pkg/kapis/devops/v1alpha3/scm/testdata/orgs.json
similarity index 100%
rename from pkg/kapis/devops/v1alpha3/testdata/orgs.json
rename to pkg/kapis/devops/v1alpha3/scm/testdata/orgs.json
diff --git a/pkg/kapis/devops/v1alpha3/scm/testdata/repos.json b/pkg/kapis/devops/v1alpha3/scm/testdata/repos.json
new file mode 100644
index 0000000..9de2220
--- /dev/null
+++ b/pkg/kapis/devops/v1alpha3/scm/testdata/repos.json
@@ -0,0 +1,222 @@
+[
+    {
+        "id": 1296269,
+        "owner": {
+            "login": "octocat",
+            "id": 1,
+            "avatar_url": "https://github.com/images/error/octocat_happy.gif",
+            "gravatar_id": "",
+            "url": "https://api.github.com/users/octocat",
+            "html_url": "https://github.com/octocat",
+            "followers_url": "https://api.github.com/users/octocat/followers",
+            "following_url": "https://api.github.com/users/octocat/following{/other_user}",
+            "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
+            "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
+            "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
+            "organizations_url": "https://api.github.com/users/octocat/orgs",
+            "repos_url": "https://api.github.com/users/octocat/repos",
+            "events_url": "https://api.github.com/users/octocat/events{/privacy}",
+            "received_events_url": "https://api.github.com/users/octocat/received_events",
+            "type": "User",
+            "site_admin": false
+        },
+        "name": "Hello-World",
+        "full_name": "octocat/Hello-World",
+        "description": "This your first repo!",
+        "private": true,
+        "fork": true,
+        "visibility": "public",
+        "url": "https://api.github.com/repos/octocat/Hello-World",
+        "html_url": "https://github.com/octocat/Hello-World",
+        "archive_url": "http://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}",
+        "assignees_url": "http://api.github.com/repos/octocat/Hello-World/assignees{/user}",
+        "blobs_url": "http://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}",
+        "branches_url": "http://api.github.com/repos/octocat/Hello-World/branches{/branch}",
+        "clone_url": "https://github.com/octocat/Hello-World.git",
+        "collaborators_url": "http://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}",
+        "comments_url": "http://api.github.com/repos/octocat/Hello-World/comments{/number}",
+        "commits_url": "http://api.github.com/repos/octocat/Hello-World/commits{/sha}",
+        "compare_url": "http://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}",
+        "contents_url": "http://api.github.com/repos/octocat/Hello-World/contents/{+path}",
+        "contributors_url": "http://api.github.com/repos/octocat/Hello-World/contributors",
+        "deployments_url": "http://api.github.com/repos/octocat/Hello-World/deployments",
+        "downloads_url": "http://api.github.com/repos/octocat/Hello-World/downloads",
+        "events_url": "http://api.github.com/repos/octocat/Hello-World/events",
+        "forks_url": "http://api.github.com/repos/octocat/Hello-World/forks",
+        "git_commits_url": "http://api.github.com/repos/octocat/Hello-World/git/commits{/sha}",
+        "git_refs_url": "http://api.github.com/repos/octocat/Hello-World/git/refs{/sha}",
+        "git_tags_url": "http://api.github.com/repos/octocat/Hello-World/git/tags{/sha}",
+        "git_url": "git:github.com/octocat/Hello-World.git",
+        "hooks_url": "http://api.github.com/repos/octocat/Hello-World/hooks",
+        "issue_comment_url": "http://api.github.com/repos/octocat/Hello-World/issues/comments{/number}",
+        "issue_events_url": "http://api.github.com/repos/octocat/Hello-World/issues/events{/number}",
+        "issues_url": "http://api.github.com/repos/octocat/Hello-World/issues{/number}",
+        "keys_url": "http://api.github.com/repos/octocat/Hello-World/keys{/key_id}",
+        "labels_url": "http://api.github.com/repos/octocat/Hello-World/labels{/name}",
+        "languages_url": "http://api.github.com/repos/octocat/Hello-World/languages",
+        "merges_url": "http://api.github.com/repos/octocat/Hello-World/merges",
+        "milestones_url": "http://api.github.com/repos/octocat/Hello-World/milestones{/number}",
+        "mirror_url": "git:git.example.com/octocat/Hello-World",
+        "notifications_url": "http://api.github.com/repos/octocat/Hello-World/notifications{?since, all, participating}",
+        "pulls_url": "http://api.github.com/repos/octocat/Hello-World/pulls{/number}",
+        "releases_url": "http://api.github.com/repos/octocat/Hello-World/releases{/id}",
+        "ssh_url": "git@github.com:octocat/Hello-World.git",
+        "stargazers_url": "http://api.github.com/repos/octocat/Hello-World/stargazers",
+        "statuses_url": "http://api.github.com/repos/octocat/Hello-World/statuses/{sha}",
+        "subscribers_url": "http://api.github.com/repos/octocat/Hello-World/subscribers",
+        "subscription_url": "http://api.github.com/repos/octocat/Hello-World/subscription",
+        "svn_url": "https://svn.github.com/octocat/Hello-World",
+        "tags_url": "http://api.github.com/repos/octocat/Hello-World/tags",
+        "teams_url": "http://api.github.com/repos/octocat/Hello-World/teams",
+        "trees_url": "http://api.github.com/repos/octocat/Hello-World/git/trees{/sha}",
+        "homepage": "https://github.com",
+        "language": null,
+        "forks_count": 9,
+        "stargazers_count": 80,
+        "watchers_count": 80,
+        "size": 108,
+        "default_branch": "master",
+        "open_issues_count": 0,
+        "topics": [
+            "octocat",
+            "atom",
+            "electron",
+            "API"
+        ],
+        "has_issues": true,
+        "has_wiki": true,
+        "has_pages": false,
+        "has_downloads": true,
+        "archived": false,
+        "pushed_at": "2011-01-26T19:06:43Z",
+        "created_at": "2011-01-26T19:01:12Z",
+        "updated_at": "2011-01-26T19:14:43Z",
+        "permissions": {
+            "admin": true,
+            "push": true,
+            "pull": true
+        },
+        "allow_rebase_merge": true,
+        "allow_squash_merge": true,
+        "allow_merge_commit": true,
+        "subscribers_count": 42,
+        "network_count": 0,
+        "license": {
+            "key": "mit",
+            "name": "MIT License",
+            "spdx_id": "MIT",
+            "url": "https://api.github.com/licenses/mit",
+            "html_url": "http://choosealicense.com/licenses/mit/"
+        }
+    },
+    {
+        "id": 1296269,
+        "owner": {
+            "login": "kubesphere",
+            "id": 1,
+            "avatar_url": "https://github.com/images/error/kubesphere_happy.gif",
+            "gravatar_id": "",
+            "url": "https://api.github.com/users/kubesphere",
+            "html_url": "https://github.com/kubesphere",
+            "followers_url": "https://api.github.com/users/kubesphere/followers",
+            "following_url": "https://api.github.com/users/kubesphere/following{/other_user}",
+            "gists_url": "https://api.github.com/users/kubesphere/gists{/gist_id}",
+            "starred_url": "https://api.github.com/users/kubesphere/starred{/owner}{/repo}",
+            "subscriptions_url": "https://api.github.com/users/kubesphere/subscriptions",
+            "organizations_url": "https://api.github.com/users/kubesphere/orgs",
+            "repos_url": "https://api.github.com/users/kubesphere/repos",
+            "events_url": "https://api.github.com/users/kubesphere/events{/privacy}",
+            "received_events_url": "https://api.github.com/users/kubesphere/received_events",
+            "type": "User",
+            "site_admin": false
+        },
+        "name": "Hello-World",
+        "full_name": "kubesphere/Hello-World",
+        "description": "This your first repo!",
+        "private": true,
+        "fork": true,
+        "visibility": "public",
+        "url": "https://api.github.com/repos/kubesphere/Hello-World",
+        "html_url": "https://github.com/kubesphere/Hello-World",
+        "archive_url": "http://api.github.com/repos/kubesphere/Hello-World/{archive_format}{/ref}",
+        "assignees_url": "http://api.github.com/repos/kubesphere/Hello-World/assignees{/user}",
+        "blobs_url": "http://api.github.com/repos/kubesphere/Hello-World/git/blobs{/sha}",
+        "branches_url": "http://api.github.com/repos/kubesphere/Hello-World/branches{/branch}",
+        "clone_url": "https://github.com/kubesphere/Hello-World.git",
+        "collaborators_url": "http://api.github.com/repos/kubesphere/Hello-World/collaborators{/collaborator}",
+        "comments_url": "http://api.github.com/repos/kubesphere/Hello-World/comments{/number}",
+        "commits_url": "http://api.github.com/repos/kubesphere/Hello-World/commits{/sha}",
+        "compare_url": "http://api.github.com/repos/kubesphere/Hello-World/compare/{base}...{head}",
+        "contents_url": "http://api.github.com/repos/kubesphere/Hello-World/contents/{+path}",
+        "contributors_url": "http://api.github.com/repos/kubesphere/Hello-World/contributors",
+        "deployments_url": "http://api.github.com/repos/kubesphere/Hello-World/deployments",
+        "downloads_url": "http://api.github.com/repos/kubesphere/Hello-World/downloads",
+        "events_url": "http://api.github.com/repos/kubesphere/Hello-World/events",
+        "forks_url": "http://api.github.com/repos/kubesphere/Hello-World/forks",
+        "git_commits_url": "http://api.github.com/repos/kubesphere/Hello-World/git/commits{/sha}",
+        "git_refs_url": "http://api.github.com/repos/kubesphere/Hello-World/git/refs{/sha}",
+        "git_tags_url": "http://api.github.com/repos/kubesphere/Hello-World/git/tags{/sha}",
+        "git_url": "git:github.com/kubesphere/Hello-World.git",
+        "hooks_url": "http://api.github.com/repos/kubesphere/Hello-World/hooks",
+        "issue_comment_url": "http://api.github.com/repos/kubesphere/Hello-World/issues/comments{/number}",
+        "issue_events_url": "http://api.github.com/repos/kubesphere/Hello-World/issues/events{/number}",
+        "issues_url": "http://api.github.com/repos/kubesphere/Hello-World/issues{/number}",
+        "keys_url": "http://api.github.com/repos/kubesphere/Hello-World/keys{/key_id}",
+        "labels_url": "http://api.github.com/repos/kubesphere/Hello-World/labels{/name}",
+        "languages_url": "http://api.github.com/repos/kubesphere/Hello-World/languages",
+        "merges_url": "http://api.github.com/repos/kubesphere/Hello-World/merges",
+        "milestones_url": "http://api.github.com/repos/kubesphere/Hello-World/milestones{/number}",
+        "mirror_url": "git:git.example.com/kubesphere/Hello-World",
+        "notifications_url": "http://api.github.com/repos/kubesphere/Hello-World/notifications{?since, all, participating}",
+        "pulls_url": "http://api.github.com/repos/kubesphere/Hello-World/pulls{/number}",
+        "releases_url": "http://api.github.com/repos/kubesphere/Hello-World/releases{/id}",
+        "ssh_url": "git@github.com:kubesphere/Hello-World.git",
+        "stargazers_url": "http://api.github.com/repos/kubesphere/Hello-World/stargazers",
+        "statuses_url": "http://api.github.com/repos/kubesphere/Hello-World/statuses/{sha}",
+        "subscribers_url": "http://api.github.com/repos/kubesphere/Hello-World/subscribers",
+        "subscription_url": "http://api.github.com/repos/kubesphere/Hello-World/subscription",
+        "svn_url": "https://svn.github.com/kubesphere/Hello-World",
+        "tags_url": "http://api.github.com/repos/kubesphere/Hello-World/tags",
+        "teams_url": "http://api.github.com/repos/kubesphere/Hello-World/teams",
+        "trees_url": "http://api.github.com/repos/kubesphere/Hello-World/git/trees{/sha}",
+        "homepage": "https://github.com",
+        "language": null,
+        "forks_count": 9,
+        "stargazers_count": 80,
+        "watchers_count": 80,
+        "size": 108,
+        "default_branch": "master",
+        "open_issues_count": 0,
+        "topics": [
+            "kubesphere",
+            "atom",
+            "electron",
+            "API"
+        ],
+        "has_issues": true,
+        "has_wiki": true,
+        "has_pages": false,
+        "has_downloads": true,
+        "archived": false,
+        "pushed_at": "2011-01-26T19:06:43Z",
+        "created_at": "2011-01-26T19:01:12Z",
+        "updated_at": "2011-01-26T19:14:43Z",
+        "permissions": {
+            "admin": true,
+            "push": true,
+            "pull": true
+        },
+        "allow_rebase_merge": true,
+        "allow_squash_merge": true,
+        "allow_merge_commit": true,
+        "subscribers_count": 42,
+        "network_count": 0,
+        "license": {
+            "key": "mit",
+            "name": "MIT License",
+            "spdx_id": "MIT",
+            "url": "https://api.github.com/licenses/mit",
+            "html_url": "http://choosealicense.com/licenses/mit/"
+        }
+    }
+]