gitsources: update api

* Generalize to use repopath instead of (owner, reponame)
This commit is contained in:
Simone Gotti 2019-04-03 15:01:21 +02:00
parent a2cd92c70b
commit 4cf72748b4
9 changed files with 126 additions and 108 deletions

View File

@ -127,12 +127,12 @@ func (c *Client) GetUserInfo() (*gitsource.UserInfo, error) {
return nil, nil
}
func (c *Client) GetRepoInfo(owner, reponame string) (*gitsource.RepoInfo, error) {
func (c *Client) GetRepoInfo(repopath string) (*gitsource.RepoInfo, error) {
return nil, nil
}
func (c *Client) GetFile(owner, repo, commit, file string) ([]byte, error) {
resp, err := c.getResponse("GET", fmt.Sprintf("%s/%s/raw/%s/%s", owner, repo, commit, file), nil, nil, nil)
func (c *Client) GetFile(repopath, commit, file string) ([]byte, error) {
resp, err := c.getResponse("GET", fmt.Sprintf("%s/raw/%s/%s", repopath, commit, file), nil, nil, nil)
if err != nil {
return nil, err
}
@ -142,27 +142,27 @@ func (c *Client) GetFile(owner, repo, commit, file string) ([]byte, error) {
return data, err
}
func (c *Client) CreateDeployKey(owner, repo, title, pubKey string, readonly bool) error {
func (c *Client) CreateDeployKey(repopath, title, pubKey string, readonly bool) error {
return nil
}
func (c *Client) DeleteDeployKey(owner, repo, title string) error {
func (c *Client) DeleteDeployKey(repopath, title string) error {
return nil
}
func (c *Client) UpdateDeployKey(owner, repo, title, pubKey string, readonly bool) error {
func (c *Client) UpdateDeployKey(repopath, title, pubKey string, readonly bool) error {
return nil
}
func (c *Client) CreateRepoWebhook(owner, repo, url, secret string) error {
func (c *Client) CreateRepoWebhook(repopath, url, secret string) error {
return nil
}
func (c *Client) DeleteRepoWebhook(owner, repo, u string) error {
func (c *Client) DeleteRepoWebhook(repopath, u string) error {
return nil
}
func (c *Client) CreateStatus(owner, repo, commitSHA string, status gitsource.CommitStatus, targetURL, description, context string) error {
func (c *Client) CreateStatus(repopath, commitSHA string, status gitsource.CommitStatus, targetURL, description, context string) error {
return nil
}

View File

@ -20,6 +20,7 @@ import (
"io"
"log"
"net/http"
"path"
"strconv"
"strings"
@ -107,10 +108,8 @@ func webhookDataFromPush(hook *pushHook) (*types.WebhookData, error) {
Sender: sender,
Repo: types.WebhookDataRepo{
Name: hook.Repo.Name,
Owner: hook.Repo.Owner.Username,
FullName: hook.Repo.FullName,
RepoURL: hook.Repo.URL,
Path: path.Join(hook.Repo.Owner.Username, hook.Repo.Name),
WebURL: hook.Repo.URL,
},
}
@ -143,10 +142,8 @@ func webhookDataFromPullRequest(hook *pullRequestHook) *types.WebhookData {
PullRequestLink: hook.PullRequest.URL,
Repo: types.WebhookDataRepo{
Name: hook.Repo.Name,
Owner: hook.Repo.Owner.Username,
FullName: hook.Repo.FullName,
RepoURL: hook.Repo.URL,
Path: path.Join(hook.Repo.Owner.Username, hook.Repo.Name),
WebURL: hook.Repo.URL,
},
}
return build

View File

@ -18,6 +18,7 @@ import (
"crypto/tls"
"net/http"
"strconv"
"strings"
gitsource "github.com/sorintlab/agola/internal/gitsources"
@ -56,6 +57,14 @@ func fromCommitStatus(status gitsource.CommitStatus) gitea.StatusState {
}
}
func parseRepoPath(repopath string) (string, string, error) {
parts := strings.Split(repopath, "/")
if len(parts) != 2 {
return "", "", errors.Errorf("wrong gitea repo path: %q", repopath)
}
return parts[0], parts[1], nil
}
func New(opts Opts) (*Client, error) {
client := gitea.NewClient(opts.URL, opts.Token)
if opts.SkipVerify {
@ -111,7 +120,11 @@ func (c *Client) GetUserInfo() (*gitsource.UserInfo, error) {
}, nil
}
func (c *Client) GetRepoInfo(owner, reponame string) (*gitsource.RepoInfo, error) {
func (c *Client) GetRepoInfo(repopath string) (*gitsource.RepoInfo, error) {
owner, reponame, err := parseRepoPath(repopath)
if err != nil {
return nil, err
}
repo, err := c.client.GetRepo(owner, reponame)
if err != nil {
return nil, err
@ -123,13 +136,21 @@ func (c *Client) GetRepoInfo(owner, reponame string) (*gitsource.RepoInfo, error
}, nil
}
func (c *Client) GetFile(owner, repo, commit, file string) ([]byte, error) {
data, err := c.client.GetFile(owner, repo, commit, file)
func (c *Client) GetFile(repopath, commit, file string) ([]byte, error) {
owner, reponame, err := parseRepoPath(repopath)
if err != nil {
return nil, err
}
data, err := c.client.GetFile(owner, reponame, commit, file)
return data, err
}
func (c *Client) CreateDeployKey(owner, repo, title, pubKey string, readonly bool) error {
_, err := c.client.CreateDeployKey(owner, repo, gitea.CreateKeyOption{
func (c *Client) CreateDeployKey(repopath, title, pubKey string, readonly bool) error {
owner, reponame, err := parseRepoPath(repopath)
if err != nil {
return err
}
_, err = c.client.CreateDeployKey(owner, reponame, gitea.CreateKeyOption{
Title: title,
Key: pubKey,
ReadOnly: readonly,
@ -138,12 +159,16 @@ func (c *Client) CreateDeployKey(owner, repo, title, pubKey string, readonly boo
return errors.Wrapf(err, "error creating deploy key")
}
func (c *Client) UpdateDeployKey(owner, repo, title, pubKey string, readonly bool) error {
func (c *Client) UpdateDeployKey(repopath, title, pubKey string, readonly bool) error {
owner, reponame, err := parseRepoPath(repopath)
if err != nil {
return err
}
// NOTE(sgotti) gitea has a bug where if we delete and remove the same key with
// the same value it is correctly readded and the admin must force a
// authorized_keys regeneration on the server. To avoid this we update it only
// when the public key value has changed
keys, err := c.client.ListDeployKeys(owner, repo)
keys, err := c.client.ListDeployKeys(owner, reponame)
if err != nil {
return errors.Wrapf(err, "error retrieving existing deploy keys")
}
@ -153,13 +178,13 @@ func (c *Client) UpdateDeployKey(owner, repo, title, pubKey string, readonly boo
if key.Key == pubKey {
return nil
}
if err := c.client.DeleteDeployKey(owner, repo, key.ID); err != nil {
if err := c.client.DeleteDeployKey(owner, reponame, key.ID); err != nil {
return errors.Wrapf(err, "error removing existing deploy key")
}
}
}
if _, err := c.client.CreateDeployKey(owner, repo, gitea.CreateKeyOption{
if _, err := c.client.CreateDeployKey(owner, reponame, gitea.CreateKeyOption{
Title: title,
Key: pubKey,
ReadOnly: readonly,
@ -170,15 +195,19 @@ func (c *Client) UpdateDeployKey(owner, repo, title, pubKey string, readonly boo
return nil
}
func (c *Client) DeleteDeployKey(owner, repo, title string) error {
keys, err := c.client.ListDeployKeys(owner, repo)
func (c *Client) DeleteDeployKey(repopath, title string) error {
owner, reponame, err := parseRepoPath(repopath)
if err != nil {
return err
}
keys, err := c.client.ListDeployKeys(owner, reponame)
if err != nil {
return errors.Wrapf(err, "error retrieving existing deploy keys")
}
for _, key := range keys {
if key.Title == title {
if err := c.client.DeleteDeployKey(owner, repo, key.ID); err != nil {
if err := c.client.DeleteDeployKey(owner, reponame, key.ID); err != nil {
return errors.Wrapf(err, "error removing existing deploy key")
}
}
@ -187,7 +216,12 @@ func (c *Client) DeleteDeployKey(owner, repo, title string) error {
return nil
}
func (c *Client) CreateRepoWebhook(owner, repo, url, secret string) error {
func (c *Client) CreateRepoWebhook(repopath, url, secret string) error {
owner, reponame, err := parseRepoPath(repopath)
if err != nil {
return err
}
opts := gitea.CreateHookOption{
Type: "gitea",
Config: map[string]string{
@ -198,13 +232,18 @@ func (c *Client) CreateRepoWebhook(owner, repo, url, secret string) error {
Events: []string{"push", "pull_request"},
Active: true,
}
_, err := c.client.CreateRepoHook(owner, repo, opts)
_, err = c.client.CreateRepoHook(owner, reponame, opts)
return errors.Wrapf(err, "error creating repository webhook")
}
func (c *Client) DeleteRepoWebhook(owner, repo, u string) error {
hooks, err := c.client.ListRepoHooks(owner, repo)
func (c *Client) DeleteRepoWebhook(repopath, u string) error {
owner, reponame, err := parseRepoPath(repopath)
if err != nil {
return err
}
hooks, err := c.client.ListRepoHooks(owner, reponame)
if err != nil {
return errors.Wrapf(err, "error retrieving repository webhooks")
}
@ -213,7 +252,7 @@ func (c *Client) DeleteRepoWebhook(owner, repo, u string) error {
// projects
for _, hook := range hooks {
if hook.Config["url"] == u {
if err := c.client.DeleteRepoHook(owner, repo, hook.ID); err != nil {
if err := c.client.DeleteRepoHook(owner, reponame, hook.ID); err != nil {
return errors.Wrapf(err, "error deleting existing repository webhook")
}
}

View File

@ -19,6 +19,7 @@ import (
"fmt"
"io"
"net/http"
"path"
"strconv"
"strings"
@ -108,10 +109,8 @@ func webhookDataFromPush(hook *pushHook) (*types.WebhookData, error) {
Sender: sender,
Repo: types.WebhookDataRepo{
Name: hook.Repo.Name,
Owner: hook.Repo.Owner.Username,
FullName: hook.Repo.FullName,
RepoURL: hook.Repo.URL,
Path: path.Join(hook.Repo.Owner.Username, hook.Repo.Name),
WebURL: hook.Repo.URL,
},
}
@ -130,7 +129,7 @@ func webhookDataFromPush(hook *pushHook) (*types.WebhookData, error) {
whd.Message = fmt.Sprintf("Tag %s", whd.Tag)
default:
// ignore received webhook since it doesn't have a ref we're interested in
return nil, fmt.Errorf("Unsupported webhook ref %q", hook.Ref)
return nil, fmt.Errorf("unsupported webhook ref %q", hook.Ref)
}
return whd, nil
@ -154,10 +153,8 @@ func webhookDataFromPullRequest(hook *pullRequestHook) *types.WebhookData {
PullRequestLink: hook.PullRequest.URL,
Repo: types.WebhookDataRepo{
Name: hook.Repo.Name,
Owner: hook.Repo.Owner.Username,
FullName: hook.Repo.FullName,
RepoURL: hook.Repo.URL,
Path: path.Join(hook.Repo.Owner.Username, hook.Repo.Name),
WebURL: hook.Repo.URL,
},
}
return build

View File

@ -21,7 +21,6 @@ import (
"fmt"
"net"
"net/http"
"path"
"strconv"
"time"
@ -121,8 +120,8 @@ func (c *Client) RequestOauth2Token(callbackURL, code string) (*oauth2.Token, er
return token, nil
}
func (c *Client) GetRepoInfo(owner, reponame string) (*gitsource.RepoInfo, error) {
repo, _, err := c.client.Projects.GetProject(path.Join(owner, reponame))
func (c *Client) GetRepoInfo(repopath string) (*gitsource.RepoInfo, error) {
repo, _, err := c.client.Projects.GetProject(repopath)
if err != nil {
return nil, err
}
@ -145,8 +144,8 @@ func (c *Client) GetUserInfo() (*gitsource.UserInfo, error) {
}, nil
}
func (c *Client) GetFile(owner, repo, commit, file string) ([]byte, error) {
f, _, err := c.client.RepositoryFiles.GetFile(path.Join(owner, repo), file, &gitlab.GetFileOptions{Ref: gitlab.String(commit)})
func (c *Client) GetFile(repopath, commit, file string) ([]byte, error) {
f, _, err := c.client.RepositoryFiles.GetFile(repopath, file, &gitlab.GetFileOptions{Ref: gitlab.String(commit)})
data, err := base64.StdEncoding.DecodeString(f.Content)
if err != nil {
return nil, err
@ -154,8 +153,8 @@ func (c *Client) GetFile(owner, repo, commit, file string) ([]byte, error) {
return data, err
}
func (c *Client) CreateDeployKey(owner, repo, title, pubKey string, readonly bool) error {
_, _, err := c.client.DeployKeys.AddDeployKey(path.Join(owner, repo), &gitlab.AddDeployKeyOptions{
func (c *Client) CreateDeployKey(repopath, title, pubKey string, readonly bool) error {
_, _, err := c.client.DeployKeys.AddDeployKey(repopath, &gitlab.AddDeployKeyOptions{
Title: gitlab.String(title),
Key: gitlab.String(pubKey),
})
@ -163,8 +162,8 @@ func (c *Client) CreateDeployKey(owner, repo, title, pubKey string, readonly boo
return errors.Wrapf(err, "error creating deploy key")
}
func (c *Client) UpdateDeployKey(owner, repo, title, pubKey string, readonly bool) error {
keys, _, err := c.client.DeployKeys.ListProjectDeployKeys(path.Join(owner, repo), nil)
func (c *Client) UpdateDeployKey(repopath, title, pubKey string, readonly bool) error {
keys, _, err := c.client.DeployKeys.ListProjectDeployKeys(repopath, nil)
if err != nil {
return errors.Wrapf(err, "error retrieving existing deploy keys")
}
@ -174,13 +173,13 @@ func (c *Client) UpdateDeployKey(owner, repo, title, pubKey string, readonly boo
if key.Key == pubKey {
return nil
}
if _, err := c.client.DeployKeys.DeleteDeployKey(path.Join(owner, repo), key.ID); err != nil {
if _, err := c.client.DeployKeys.DeleteDeployKey(repopath, key.ID); err != nil {
return errors.Wrapf(err, "error removing existing deploy key")
}
}
}
if _, _, err := c.client.DeployKeys.AddDeployKey(path.Join(owner, repo), &gitlab.AddDeployKeyOptions{
if _, _, err := c.client.DeployKeys.AddDeployKey(repopath, &gitlab.AddDeployKeyOptions{
Title: &title,
Key: &pubKey,
}); err != nil {
@ -190,15 +189,15 @@ func (c *Client) UpdateDeployKey(owner, repo, title, pubKey string, readonly boo
return nil
}
func (c *Client) DeleteDeployKey(owner, repo, title string) error {
keys, _, err := c.client.DeployKeys.ListProjectDeployKeys(path.Join(owner, repo), nil)
func (c *Client) DeleteDeployKey(repopath, title string) error {
keys, _, err := c.client.DeployKeys.ListProjectDeployKeys(repopath, nil)
if err != nil {
return errors.Wrapf(err, "error retrieving existing deploy keys")
}
for _, key := range keys {
if key.Title == title {
if _, err := c.client.DeployKeys.DeleteDeployKey(path.Join(owner, repo), key.ID); err != nil {
if _, err := c.client.DeployKeys.DeleteDeployKey(repopath, key.ID); err != nil {
return errors.Wrapf(err, "error removing existing deploy key")
}
}
@ -207,18 +206,19 @@ func (c *Client) DeleteDeployKey(owner, repo, title string) error {
return nil
}
func (c *Client) CreateRepoWebhook(owner, repo, url, secret string) error {
func (c *Client) CreateRepoWebhook(repopath, url, secret string) error {
opts := &gitlab.AddProjectHookOptions{
URL: gitlab.String(url),
PushEvents: gitlab.Bool(true),
URL: gitlab.String(url),
PushEvents: gitlab.Bool(true),
MergeRequestsEvents: gitlab.Bool(true),
}
_, _, err := c.client.Projects.AddProjectHook(path.Join(owner, repo), opts)
_, _, err := c.client.Projects.AddProjectHook(repopath, opts)
return errors.Wrapf(err, "error creating repository webhook")
}
func (c *Client) DeleteRepoWebhook(owner, repo, u string) error {
hooks, _, err := c.client.Projects.ListProjectHooks(path.Join(owner, repo), nil)
func (c *Client) DeleteRepoWebhook(repopath, u string) error {
hooks, _, err := c.client.Projects.ListProjectHooks(repopath, nil)
if err != nil {
return errors.Wrapf(err, "error retrieving repository webhooks")
}
@ -227,7 +227,7 @@ func (c *Client) DeleteRepoWebhook(owner, repo, u string) error {
// projects
for _, hook := range hooks {
if hook.URL == u {
if _, err := c.client.Projects.DeleteProjectHook(path.Join(owner, repo), hook.ID); err != nil {
if _, err := c.client.Projects.DeleteProjectHook(repopath, hook.ID); err != nil {
return errors.Wrapf(err, "error deleting existing repository webhook")
}
}
@ -237,6 +237,5 @@ func (c *Client) DeleteRepoWebhook(owner, repo, u string) error {
}
func (c *Client) ParseWebhook(r *http.Request) (*types.WebhookData, error) {
hookEvent := "X-Gitea-Event"
return nil, errors.Errorf("unknown webhook event type: %q", r.Header.Get(hookEvent))
return nil, errors.Errorf("unimplemented")
}

View File

@ -30,13 +30,13 @@ const (
)
type GitSource interface {
GetRepoInfo(owner, repo string) (*RepoInfo, error)
GetFile(owner, repo, commit, file string) ([]byte, error)
DeleteDeployKey(owner, repo, title string) error
CreateDeployKey(owner, repo, title, pubKey string, readonly bool) error
UpdateDeployKey(owner, repo, title, pubKey string, readonly bool) error
DeleteRepoWebhook(owner, repo, url string) error
CreateRepoWebhook(owner, repo, url, secret string) error
GetRepoInfo(repopath string) (*RepoInfo, error)
GetFile(repopath, commit, file string) ([]byte, error)
DeleteDeployKey(repopath, title string) error
CreateDeployKey(repopath, title, pubKey string, readonly bool) error
UpdateDeployKey(repopath, title, pubKey string, readonly bool) error
DeleteRepoWebhook(repopath, url string) error
CreateRepoWebhook(repopath, url, secret string) error
ParseWebhook(r *http.Request) (*types.WebhookData, error)
}

View File

@ -18,7 +18,6 @@ import (
"context"
"fmt"
"path"
"strings"
"github.com/sorintlab/agola/internal/services/gateway/common"
"github.com/sorintlab/agola/internal/services/types"
@ -67,11 +66,7 @@ func (c *CommandHandler) CreateProject(ctx context.Context, req *CreateProjectRe
return nil, errors.Wrapf(err, "failed to create gitsource client")
}
repoOwner := strings.TrimPrefix(path.Dir(req.RepoPath), "/")
repoName := path.Base(req.RepoPath)
c.log.Infof("repoOwner: %s, repoName: %s", repoOwner, repoName)
repo, err := gitsource.GetRepoInfo(repoOwner, repoName)
repo, err := gitsource.GetRepoInfo(req.RepoPath)
if err != nil {
return nil, errors.Wrapf(err, "failed to get repository info from gitsource")
}
@ -113,16 +108,14 @@ func (c *CommandHandler) CreateProject(ctx context.Context, req *CreateProjectRe
c.log.Infof("project %s created, ID: %s", p.Name, p.ID)
return p, c.SetupProject(ctx, rs, la, &SetupProjectRequest{
Project: p,
RepoOwner: repoOwner,
RepoName: repoName,
Project: p,
RepoPath: req.RepoPath,
})
}
type SetupProjectRequest struct {
Project *types.Project
RepoOwner string
RepoName string
Project *types.Project
RepoPath string
}
func (c *CommandHandler) SetupProject(ctx context.Context, rs *types.RemoteSource, la *types.LinkedAccount, conf *SetupProjectRequest) error {
@ -143,15 +136,15 @@ func (c *CommandHandler) SetupProject(ctx context.Context, rs *types.RemoteSourc
// will trigger multiple different runs
deployKeyName := fmt.Sprintf("agola deploy key - %s", conf.Project.ID)
c.log.Infof("creating/updating deploy key: %s", string(pubKey))
if err := gitsource.UpdateDeployKey(conf.RepoOwner, conf.RepoName, deployKeyName, string(pubKey), true); err != nil {
if err := gitsource.UpdateDeployKey(conf.RepoPath, deployKeyName, string(pubKey), true); err != nil {
return errors.Wrapf(err, "failed to create deploy key")
}
c.log.Infof("deleting existing webhooks")
if err := gitsource.DeleteRepoWebhook(conf.RepoOwner, conf.RepoName, webhookURL); err != nil {
if err := gitsource.DeleteRepoWebhook(conf.RepoPath, webhookURL); err != nil {
return errors.Wrapf(err, "failed to delete repository webhook")
}
c.log.Infof("creating webhook to url: %s", webhookURL)
if err := gitsource.CreateRepoWebhook(conf.RepoOwner, conf.RepoName, webhookURL, ""); err != nil {
if err := gitsource.CreateRepoWebhook(conf.RepoPath, webhookURL, ""); err != nil {
return errors.Wrapf(err, "failed to create repository webhook")
}
@ -180,12 +173,8 @@ func (c *CommandHandler) ReconfigProject(ctx context.Context, projectID string)
return errors.Wrapf(err, "failed to get remote source %q", la.RemoteSourceID)
}
repoOwner := strings.TrimPrefix(path.Dir(p.RepoPath), "/")
repoName := path.Base(p.RepoPath)
return c.SetupProject(ctx, rs, la, &SetupProjectRequest{
Project: p,
RepoOwner: repoOwner,
RepoName: repoName,
Project: p,
RepoPath: p.RepoPath,
})
}

View File

@ -231,7 +231,7 @@ func (h *webhooksHandler) handleWebhook(r *http.Request) (int, string, error) {
h.log.Debugf("user: %s", util.Dump(user))
userID = user.ID
cloneURL = fmt.Sprintf("%s/%s/%s", h.apiExposedURL+"/repos", webhookData.Repo.Owner, webhookData.Repo.Name)
cloneURL = fmt.Sprintf("%s/%s", h.apiExposedURL+"/repos", webhookData.Repo.Path)
runType = types.RunTypeUser
}
@ -240,7 +240,7 @@ func (h *webhooksHandler) handleWebhook(r *http.Request) (int, string, error) {
var data []byte
err := util.ExponentialBackoff(util.FetchFileBackoff, func() (bool, error) {
var err error
data, err = gitSource.GetFile(webhookData.Repo.Owner, webhookData.Repo.Name, webhookData.CommitSHA, agolaDefaultConfigPath)
data, err = gitSource.GetFile(webhookData.Repo.Path, webhookData.CommitSHA, agolaDefaultConfigPath)
if err == nil {
return true, nil
}

View File

@ -33,14 +33,13 @@ type WebhookData struct {
Event WebhookEvent `json:"event,omitempty"`
ProjectID string `json:"project_id,omitempty"`
CompareLink string `json:"compare_link,omitempty"` // Pimray link to source. It can be the commit
CommitLink string `json:"commit_link,omitempty"` // Pimray link to source. It can be the commit
CommitSHA string `json:"commit_sha,omitempty"` // commit SHA (SHA1 but also future SHA like SHA256)
OldCommitSHA string `json:"old_commit_sha,omitempty"` // commit SHA of the head before this push
Ref string `json:"ref,omitempty"` // Ref containing the commit SHA
Message string `json:"message,omitempty"` // Message to use (Push last commit message summary, PR title, Tag message etc...)
Sender string `json:"sender,omitempty"`
Avatar string `json:"avatar,omitempty"`
CompareLink string `json:"compare_link,omitempty"` // Pimray link to source. It can be the commit
CommitLink string `json:"commit_link,omitempty"` // Pimray link to source. It can be the commit
CommitSHA string `json:"commit_sha,omitempty"` // commit SHA (SHA1 but also future SHA like SHA256)
Ref string `json:"ref,omitempty"` // Ref containing the commit SHA
Message string `json:"message,omitempty"` // Message to use (Push last commit message summary, PR title, Tag message etc...)
Sender string `json:"sender,omitempty"`
Avatar string `json:"avatar,omitempty"`
Branch string `json:"branch,omitempty"`
BranchLink string `json:"branch_link,omitempty"`
@ -56,8 +55,6 @@ type WebhookData struct {
}
type WebhookDataRepo struct {
Name string `json:"name,omitempty"`
Owner string `json:"owner,omitempty"`
FullName string `json:"full_name,omitempty"`
RepoURL string `json:"repo_url,omitempty"`
WebURL string `json:"web_url,omitempty"`
Path string `json:"path,omitempty"`
}