gitea-github-proxy/api/routes_rest_api.go

474 lines
13 KiB
Go

package api
import (
gitea "code.gitea.io/sdk/gitea"
"encoding/json"
"fmt"
"git.lumeweb.com/LumeWeb/gitea-github-proxy/config"
"github.com/google/go-github/v59/github"
"github.com/gorilla/mux"
"go.uber.org/zap"
"net/http"
"strconv"
)
type restApi struct {
config *config.Config
logger *zap.Logger
}
func newRestApi(cfg *config.Config, logger *zap.Logger) *restApi {
return &restApi{config: cfg, logger: logger}
}
func (r restApi) handlerGetPullRequestFiles(w http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
owner := vars["owner"]
repo := vars["repo"]
pullNumber := vars["pull_number"]
client := r.getClientOrError(w)
if client == nil {
return
}
parsedPullNumber, err := strconv.ParseInt(pullNumber, 10, 64)
if err != nil {
http.Error(w, "Failed to parse pull number", http.StatusBadRequest)
r.logger.Error("Failed to parse pull number", zap.Error(err))
return
}
files, r2, err := client.ListPullRequestFiles(owner, repo, parsedPullNumber, gitea.ListPullRequestFilesOptions{
ListOptions: r.getPagingOptions(request),
})
if err != nil {
http.Error(w, "Failed to get pull request files", http.StatusInternalServerError)
r.logger.Error("Failed to get pull request files", zap.Error(err))
return
}
githubFiles := make([]*github.CommitFile, len(files))
for i, file := range files {
githubFiles[i] = convertCommitFile(file)
}
r.sendPagingHeaders(w, r2)
r.respond(w, http.StatusOK, githubFiles)
}
func (r restApi) getPagingOptions(request *http.Request) gitea.ListOptions {
page, _ := strconv.Atoi(request.URL.Query().Get("page"))
perPage, _ := strconv.Atoi(request.URL.Query().Get("per_page"))
return gitea.ListOptions{
Page: page,
PageSize: perPage,
}
}
func (r restApi) sendPagingHeaders(w http.ResponseWriter, apiResponse *gitea.Response) {
links := []string{}
baseURL := fmt.Sprintf("https://%s", r.config.Domain)
joinStrings := func(elements []string, separator string) string {
if len(elements) == 0 {
return ""
}
result := elements[0]
for i := 1; i < len(elements); i++ {
result += separator + elements[i]
}
return result
}
joinLinks := func(links []string) string {
return fmt.Sprintf("%s", joinStrings(links, ", "))
}
if apiResponse.FirstPage > 0 {
links = append(links, fmt.Sprintf(`<%s?page=%d>; rel="first"`, baseURL, apiResponse.FirstPage))
}
if apiResponse.PrevPage > 0 {
links = append(links, fmt.Sprintf(`<%s?page=%d>; rel="prev"`, baseURL, apiResponse.PrevPage))
}
if apiResponse.NextPage > 0 {
links = append(links, fmt.Sprintf(`<%s?page=%d>; rel="next"`, baseURL, apiResponse.NextPage))
}
if apiResponse.LastPage > 0 {
links = append(links, fmt.Sprintf(`<%s?page=%d>; rel="last"`, baseURL, apiResponse.LastPage))
}
if len(links) > 0 {
w.Header().Add("Link", fmt.Sprintf("%s", joinLinks(links)))
}
}
func (r restApi) respond(w http.ResponseWriter, status int, data interface{}) {
jsonData, err := json.Marshal(data)
if err != nil {
http.Error(w, "Failed to marshal response", http.StatusInternalServerError)
r.logger.Error("Failed to marshal response", zap.Error(err))
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if data != nil {
_, _ = w.Write(jsonData)
}
}
func (r restApi) getClientOrError(w http.ResponseWriter) *gitea.Client {
client, err := getClient(ClientParams{
Config: r.config,
})
if err != nil {
http.Error(w, "Failed to get Gitea client", http.StatusInternalServerError)
r.logger.Error("Failed to get Gitea client", zap.Error(err))
return nil
}
return client
}
func (r restApi) handlerGetTree(w http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
owner := vars["owner"]
repo := vars["repo"]
treeSha := vars["tree_sha"]
recursive := false
if request.URL.Query().Has("recursive") {
recursive = true
}
client := r.getClientOrError(w)
if client == nil {
return
}
tree, r2, err := client.GetTrees(owner, repo, treeSha, recursive)
if err != nil {
http.Error(w, "Failed to get tree", http.StatusInternalServerError)
r.logger.Error("Failed to get tree", zap.Error(err))
return
}
treeResponse := convertGitTree(tree)
r.sendPagingHeaders(w, r2)
r.respond(w, http.StatusOK, treeResponse)
}
func (r restApi) handleGetIssueComments(w http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
owner := vars["owner"]
repo := vars["repo"]
issueNumber := vars["issue_number"]
client := r.getClientOrError(w)
if client == nil {
return
}
issueNumberInt, err := strconv.Atoi(issueNumber)
if err != nil {
http.Error(w, "Failed to parse issue number", http.StatusBadRequest)
r.logger.Error("Failed to parse issue number", zap.Error(err))
return
}
comments, r2, err := client.ListIssueComments(owner, repo, int64(issueNumberInt), gitea.ListIssueCommentOptions{ListOptions: r.getPagingOptions(request)})
if err != nil {
http.Error(w, "Failed to get issue comments", http.StatusInternalServerError)
r.logger.Error("Failed to get issue comments", zap.Error(err))
return
}
ghComments := make([]*github.IssueComment, len(comments))
for i, comment := range comments {
reactions, _, _ := client.GetIssueCommentReactions(owner, repo, comment.ID)
ghComments[i] = convertIssueComment(comment, reactions)
}
r.sendPagingHeaders(w, r2)
r.respond(w, http.StatusOK, ghComments)
}
func (r restApi) handlerCreateIssueComment(w http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
owner := vars["owner"]
repo := vars["repo"]
issueNumber := vars["issue_number"]
client := r.getClientOrError(w)
if client == nil {
return
}
ghComment := github.IssueComment{}
if err := r.parseJsonBody(request, &ghComment); err != nil {
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
r.logger.Error("Failed to parse request body", zap.Error(err))
}
issueNumberInt, err := strconv.Atoi(issueNumber)
if err != nil {
http.Error(w, "Failed to parse issue number", http.StatusBadRequest)
r.logger.Error("Failed to parse issue number", zap.Error(err))
return
}
comment := gitea.CreateIssueCommentOption{
Body: ghComment.GetBody(),
}
commentResponse, _, err := client.CreateIssueComment(owner, repo, int64(issueNumberInt), comment)
if err != nil {
http.Error(w, "Failed to create comment", http.StatusInternalServerError)
r.logger.Error("Failed to create comment", zap.Error(err))
return
}
newGhComment := convertIssueComment(commentResponse, nil)
r.respond(w, http.StatusCreated, newGhComment)
}
func (r restApi) handlerUpdateIssueComment(w http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
owner := vars["owner"]
repo := vars["repo"]
commentID := vars["comment_id"]
client := r.getClientOrError(w)
if client == nil {
return
}
ghComment := github.IssueComment{}
if err := r.parseJsonBody(request, &ghComment); err != nil {
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
r.logger.Error("Failed to parse request body", zap.Error(err))
}
commentIDInt, err := strconv.Atoi(commentID)
if err != nil {
http.Error(w, "Failed to parse comment ID", http.StatusBadRequest)
r.logger.Error("Failed to parse comment ID", zap.Error(err))
return
}
comment := gitea.EditIssueCommentOption{
Body: ghComment.GetBody(),
}
commentResponse, _, err := client.EditIssueComment(owner, repo, int64(commentIDInt), comment)
if err != nil {
http.Error(w, "Failed to update comment", http.StatusInternalServerError)
r.logger.Error("Failed to update comment", zap.Error(err))
return
}
newGhComment := convertIssueComment(commentResponse, nil)
r.respond(w, http.StatusOK, newGhComment)
}
func (r restApi) handlerCreateRelease(w http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
owner := vars["owner"]
repo := vars["repo"]
client := r.getClientOrError(w)
if client == nil {
return
}
ghRelease := github.RepositoryRelease{}
if err := r.parseJsonBody(request, &ghRelease); err != nil {
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
r.logger.Error("Failed to parse request body", zap.Error(err))
}
release := gitea.CreateReleaseOption{
TagName: ghRelease.GetTagName(),
Target: ghRelease.GetTargetCommitish(),
Title: ghRelease.GetName(),
Note: ghRelease.GetBody(),
IsDraft: ghRelease.GetDraft(),
IsPrerelease: ghRelease.GetPrerelease(),
}
releaseResponse, r2, err := client.CreateRelease(owner, repo, release)
if err != nil {
http.Error(w, "Failed to create release", http.StatusInternalServerError)
r.logger.Error("Failed to create release", zap.Error(err))
return
}
newGhRelease := convertRelease(releaseResponse)
r.sendPagingHeaders(w, r2)
r.respond(w, http.StatusCreated, newGhRelease)
}
func (r restApi) handlerCreatePullRequest(w http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
owner := vars["owner"]
repo := vars["repo"]
client := r.getClientOrError(w)
if client == nil {
return
}
ghPullRequest := github.NewPullRequest{}
if err := r.parseJsonBody(request, &ghPullRequest); err != nil {
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
r.logger.Error("Failed to parse request body", zap.Error(err))
}
pullRequest := gitea.CreatePullRequestOption{
Title: ghPullRequest.GetTitle(),
Head: ghPullRequest.GetHead(),
Base: ghPullRequest.GetBase(),
Body: ghPullRequest.GetBody(),
}
pullRequestResponse, r2, err := client.CreatePullRequest(owner, repo, pullRequest)
if err != nil {
http.Error(w, "Failed to create pull request", http.StatusInternalServerError)
r.logger.Error("Failed to create pull request", zap.Error(err))
return
}
newGhPullRequest := convertPullRequest(pullRequestResponse)
r.sendPagingHeaders(w, r2)
r.respond(w, http.StatusCreated, newGhPullRequest)
}
func (r restApi) handlerUpdatePullRequest(w http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
owner := vars["owner"]
repo := vars["repo"]
pullNumber := vars["pull_number"]
parsedPullNumber, err := strconv.ParseInt(pullNumber, 10, 64)
if err != nil {
http.Error(w, "Failed to parse pull number", http.StatusBadRequest)
r.logger.Error("Failed to parse pull number", zap.Error(err))
return
}
client := r.getClientOrError(w)
if client == nil {
return
}
type pullRequestUpdate struct {
Title *string `json:"title,omitempty"`
Body *string `json:"body,omitempty"`
State *string `json:"state,omitempty"`
Base *string `json:"base,omitempty"`
MaintainerCanModify *bool `json:"maintainer_can_modify,omitempty"`
}
ghPullRequest := pullRequestUpdate{}
pullRequest := gitea.EditPullRequestOption{}
if err := r.parseJsonBody(request, &ghPullRequest); err != nil {
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
r.logger.Error("Failed to parse request body", zap.Error(err))
}
if ghPullRequest.State != nil {
if *ghPullRequest.State != "open" && *ghPullRequest.State != "closed" {
http.Error(w, "Invalid state", http.StatusBadRequest)
return
}
state := *ghPullRequest.State
stateType := gitea.StateType(state)
pullRequest.State = &stateType
}
if ghPullRequest.Title != nil {
pullRequest.Title = *ghPullRequest.Title
}
if ghPullRequest.Body != nil {
pullRequest.Body = *ghPullRequest.Body
}
pullRequestResponse, r2, err := client.EditPullRequest(owner, repo, parsedPullNumber, pullRequest)
if err != nil {
http.Error(w, "Failed to update pull request", http.StatusInternalServerError)
r.logger.Error("Failed to update pull request", zap.Error(err))
return
}
newGhPullRequest := convertPullRequest(pullRequestResponse)
r.sendPagingHeaders(w, r2)
r.respond(w, http.StatusOK, newGhPullRequest)
}
func (r restApi) parseJsonBody(request *http.Request, obj interface{}) error {
if obj == nil {
obj = make(map[string]interface{})
}
decoder := json.NewDecoder(request.Body)
return decoder.Decode(obj)
}
func setupRestRoutes(params RouteParams) {
logger := params.Logger
cfg := params.Config
r := params.R
restApi := newRestApi(cfg, logger)
setupRoutes := func(r *mux.Router) {
r.HandleFunc("/repos/{owner}/{repo}/pulls/{pull_number}/files", restApi.handlerGetPullRequestFiles).Methods("GET")
r.HandleFunc("/repos/{owner}/{repo}/git/trees/{tree_sha}", restApi.handlerGetTree).Methods("GET")
// Comment routes
r.HandleFunc("/repos/{owner}/{repo}/issues/{issue_number}/comments", restApi.handleGetIssueComments).Methods("GET")
r.HandleFunc("/repos/{owner}/{repo}/issues/{issue_number}/comments", restApi.handlerCreateIssueComment).Methods("POST")
r.HandleFunc("/repos/{owner}/{repo}/issues/comments/{comment_id}", restApi.handlerUpdateIssueComment).Methods("PATCH")
// Repo Release routes
r.HandleFunc("/repos/{owner}/{repo}/releases", restApi.handlerCreateRelease).Methods("POST")
// Pull Request routes
r.HandleFunc("/repos/{owner}/{repo}/pulls", restApi.handlerCreatePullRequest).Methods("POST")
r.HandleFunc("/repos/{owner}/{repo}/pulls/{pull_number}", restApi.handlerUpdatePullRequest).Methods("PATCH")
}
restRouter := r.PathPrefix("/api").Subrouter()
restRouter.Use(githubRestVerifyMiddleware(params.Db))
restRouter.Use(githubRestRequireAuthMiddleware(params.Config))
setupRoutes(restRouter)
setupRoutes(restRouter.PathPrefix("/v3").Subrouter())
}