making progress
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/forgeo/forgebucket/internal/config"
|
||||
"github.com/forgeo/forgebucket/internal/models"
|
||||
)
|
||||
|
||||
type GitHTTPHandler struct {
|
||||
db *xorm.Engine
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewGitHTTPHandler(db *xorm.Engine, cfg *config.Config) *GitHTTPHandler {
|
||||
return &GitHTTPHandler{db: db, cfg: cfg}
|
||||
}
|
||||
|
||||
// ServeGit is the entry point for all git smart-HTTP requests.
|
||||
// URL shape: /{owner}/{repo}.git/{service}
|
||||
func (h *GitHTTPHandler) ServeGit(w http.ResponseWriter, r *http.Request) {
|
||||
owner := chi.URLParam(r, "owner")
|
||||
repoGit := chi.URLParam(r, "repoGit") // e.g. "myrepo.git"
|
||||
repoName := strings.TrimSuffix(repoGit, ".git")
|
||||
|
||||
// Resolve repo from DB
|
||||
var ownerUser models.User
|
||||
if found, _ := h.db.Where("username = ?", owner).Get(&ownerUser); !found {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
var repo models.Repository
|
||||
if found, _ := h.db.Where("owner_id = ? AND name = ?", ownerUser.ID, repoName).Get(&repo); !found {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Determine service from URL / query string
|
||||
service := r.URL.Query().Get("service")
|
||||
if service == "" {
|
||||
// POST bodies encode the service in the path
|
||||
path := r.URL.Path
|
||||
if strings.HasSuffix(path, "/git-upload-pack") {
|
||||
service = "git-upload-pack"
|
||||
} else if strings.HasSuffix(path, "/git-receive-pack") {
|
||||
service = "git-receive-pack"
|
||||
}
|
||||
}
|
||||
|
||||
// Require authentication for push; allow anonymous read for public repos
|
||||
var authedUser string
|
||||
user, authed := h.basicAuth(r)
|
||||
if authed {
|
||||
authedUser = user
|
||||
} else if service == "git-receive-pack" || repo.IsPrivate {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="ForgeBucket"`)
|
||||
http.Error(w, "authentication required", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Build PATH_INFO: /{reponame}.git/{suffix}
|
||||
// chi wildcard gives us everything after /{owner}/{repoGit}
|
||||
suffix := chi.URLParam(r, "*")
|
||||
if suffix == "" {
|
||||
suffix = "/"
|
||||
} else if !strings.HasPrefix(suffix, "/") {
|
||||
suffix = "/" + suffix
|
||||
}
|
||||
pathInfo := "/" + repoGit + suffix
|
||||
|
||||
projectRoot := filepath.Dir(repo.DiskPath)
|
||||
|
||||
env := []string{
|
||||
"GIT_PROJECT_ROOT=" + projectRoot,
|
||||
"GIT_HTTP_EXPORT_ALL=1",
|
||||
"PATH_INFO=" + pathInfo,
|
||||
"QUERY_STRING=" + r.URL.RawQuery,
|
||||
"REQUEST_METHOD=" + r.Method,
|
||||
"SERVER_PROTOCOL=HTTP/1.1",
|
||||
"SERVER_SOFTWARE=ForgeBucket/1.0",
|
||||
"REMOTE_ADDR=" + r.RemoteAddr,
|
||||
"HOME=/tmp",
|
||||
"GIT_TERMINAL_PROMPT=0",
|
||||
}
|
||||
if ct := r.Header.Get("Content-Type"); ct != "" {
|
||||
env = append(env, "CONTENT_TYPE="+ct)
|
||||
}
|
||||
if cl := r.ContentLength; cl > 0 {
|
||||
env = append(env, "CONTENT_LENGTH="+strconv.FormatInt(cl, 10))
|
||||
}
|
||||
if authedUser != "" {
|
||||
env = append(env, "REMOTE_USER="+authedUser)
|
||||
}
|
||||
|
||||
gitExec, err := exec.LookPath("git")
|
||||
if err != nil {
|
||||
http.Error(w, "git not found on server", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := runGitBackend(r.Context(), w, r.Body, gitExec, env); err != nil {
|
||||
http.Error(w, fmt.Sprintf("git http-backend: %v", err), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// basicAuth validates HTTP Basic Auth credentials against the DB.
|
||||
func (h *GitHTTPHandler) basicAuth(r *http.Request) (username string, ok bool) {
|
||||
u, p, hasAuth := r.BasicAuth()
|
||||
if !hasAuth {
|
||||
return "", false
|
||||
}
|
||||
var user models.User
|
||||
found, _ := h.db.Where("username = ?", u).Get(&user)
|
||||
if !found {
|
||||
return "", false
|
||||
}
|
||||
if bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(p)) != nil {
|
||||
return "", false
|
||||
}
|
||||
return u, true
|
||||
}
|
||||
|
||||
// runGitBackend executes `git http-backend` as a CGI subprocess and forwards
|
||||
// its response (headers + body) to the HTTP response writer.
|
||||
func runGitBackend(ctx context.Context, w http.ResponseWriter, body io.Reader, gitExec string, env []string) error {
|
||||
cmd := exec.CommandContext(ctx, gitExec, "http-backend")
|
||||
cmd.Env = env
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
cmd.Stdout = pw
|
||||
cmd.Stdin = body
|
||||
|
||||
var stderrBuf strings.Builder
|
||||
cmd.Stderr = &stderrBuf
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("start: %w", err)
|
||||
}
|
||||
|
||||
// Close write-end of pipe once git finishes
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
pw.Close()
|
||||
done <- err
|
||||
}()
|
||||
|
||||
// Parse CGI response: headers then body
|
||||
br := bufio.NewReader(pr)
|
||||
statusCode := http.StatusOK
|
||||
|
||||
for {
|
||||
line, err := br.ReadString('\n')
|
||||
line = strings.TrimRight(line, "\r\n")
|
||||
if line == "" {
|
||||
break
|
||||
}
|
||||
if strings.HasPrefix(line, "Status:") {
|
||||
rest := strings.TrimSpace(strings.TrimPrefix(line, "Status:"))
|
||||
if parts := strings.SplitN(rest, " ", 2); len(parts) >= 1 {
|
||||
if code, e := strconv.Atoi(parts[0]); e == nil {
|
||||
statusCode = code
|
||||
}
|
||||
}
|
||||
} else if idx := strings.Index(line, ":"); idx > 0 {
|
||||
key := strings.TrimSpace(line[:idx])
|
||||
val := strings.TrimSpace(line[idx+1:])
|
||||
w.Header().Set(key, val)
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(statusCode)
|
||||
io.Copy(w, br) //nolint:errcheck
|
||||
|
||||
waitErr := <-done
|
||||
if waitErr != nil && stderrBuf.Len() > 0 {
|
||||
return fmt.Errorf("%w: %s", waitErr, stderrBuf.String())
|
||||
}
|
||||
return waitErr
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/forgeo/forgebucket/internal/api/middleware"
|
||||
"github.com/forgeo/forgebucket/internal/models"
|
||||
)
|
||||
|
||||
type IssueHandler struct {
|
||||
db *xorm.Engine
|
||||
}
|
||||
|
||||
func NewIssueHandler(db *xorm.Engine) *IssueHandler {
|
||||
return &IssueHandler{db: db}
|
||||
}
|
||||
|
||||
type issueResponse struct {
|
||||
models.Issue
|
||||
AuthorName string `json:"authorName"`
|
||||
}
|
||||
|
||||
func (h *IssueHandler) enrichIssue(issue *models.Issue) issueResponse {
|
||||
var author models.User
|
||||
h.db.ID(issue.AuthorID).Get(&author)
|
||||
return issueResponse{Issue: *issue, AuthorName: author.Username}
|
||||
}
|
||||
|
||||
func (h *IssueHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
repoID, ok := h.repoID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
state := r.URL.Query().Get("state")
|
||||
if state == "" {
|
||||
state = "open"
|
||||
}
|
||||
|
||||
var issues []models.Issue
|
||||
if err := h.db.Where("repo_id = ? AND state = ?", repoID, state).
|
||||
OrderBy("id DESC").Find(&issues); err != nil {
|
||||
jsonError(w, "could not list issues", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]issueResponse, len(issues))
|
||||
for i := range issues {
|
||||
result[i] = h.enrichIssue(&issues[i])
|
||||
}
|
||||
if result == nil {
|
||||
result = []issueResponse{}
|
||||
}
|
||||
jsonOK(w, result)
|
||||
}
|
||||
|
||||
func (h *IssueHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
repoID, ok := h.repoID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
authorID, _ := middleware.UserIDFromContext(r.Context())
|
||||
|
||||
var body struct {
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Title == "" {
|
||||
jsonError(w, "title is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-increment issue number per repo
|
||||
count, _ := h.db.Where("repo_id = ?", repoID).Count(&models.Issue{})
|
||||
number := int(count) + 1
|
||||
|
||||
issue := &models.Issue{
|
||||
RepoID: repoID,
|
||||
AuthorID: authorID,
|
||||
Number: number,
|
||||
Title: body.Title,
|
||||
Body: body.Body,
|
||||
State: models.IssueStateOpen,
|
||||
}
|
||||
if _, err := h.db.Insert(issue); err != nil {
|
||||
jsonError(w, "could not create issue", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(h.enrichIssue(issue))
|
||||
}
|
||||
|
||||
func (h *IssueHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
issue, ok := h.lookupIssue(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
jsonOK(w, h.enrichIssue(issue))
|
||||
}
|
||||
|
||||
func (h *IssueHandler) Close(w http.ResponseWriter, r *http.Request) {
|
||||
issue, ok := h.lookupIssue(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
issue.State = models.IssueStateClosed
|
||||
if _, err := h.db.ID(issue.ID).Cols("state").Update(issue); err != nil {
|
||||
jsonError(w, "could not close issue", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
jsonOK(w, h.enrichIssue(issue))
|
||||
}
|
||||
|
||||
func (h *IssueHandler) Reopen(w http.ResponseWriter, r *http.Request) {
|
||||
issue, ok := h.lookupIssue(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
issue.State = models.IssueStateOpen
|
||||
if _, err := h.db.ID(issue.ID).Cols("state").Update(issue); err != nil {
|
||||
jsonError(w, "could not reopen issue", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
jsonOK(w, h.enrichIssue(issue))
|
||||
}
|
||||
|
||||
func (h *IssueHandler) repoID(w http.ResponseWriter, r *http.Request) (int64, bool) {
|
||||
ownerName := chi.URLParam(r, "owner")
|
||||
repoName := chi.URLParam(r, "repo")
|
||||
|
||||
var owner models.User
|
||||
if found, _ := h.db.Where("username = ?", ownerName).Get(&owner); !found {
|
||||
jsonError(w, "repository not found", http.StatusNotFound)
|
||||
return 0, false
|
||||
}
|
||||
var repo models.Repository
|
||||
if found, _ := h.db.Where("owner_id = ? AND name = ?", owner.ID, repoName).Get(&repo); !found {
|
||||
jsonError(w, "repository not found", http.StatusNotFound)
|
||||
return 0, false
|
||||
}
|
||||
return repo.ID, true
|
||||
}
|
||||
|
||||
func (h *IssueHandler) lookupIssue(w http.ResponseWriter, r *http.Request) (*models.Issue, bool) {
|
||||
repoID, ok := h.repoID(w, r)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
issueNum, err := strconv.Atoi(chi.URLParam(r, "issueNum"))
|
||||
if err != nil {
|
||||
jsonError(w, "invalid issue number", http.StatusBadRequest)
|
||||
return nil, false
|
||||
}
|
||||
var issue models.Issue
|
||||
if found, _ := h.db.Where("repo_id = ? AND number = ?", repoID, issueNum).Get(&issue); !found {
|
||||
jsonError(w, "issue not found", http.StatusNotFound)
|
||||
return nil, false
|
||||
}
|
||||
return &issue, true
|
||||
}
|
||||
@@ -51,13 +51,9 @@ func (h *RepoHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch owner username once (all repos belong to the same user in this query)
|
||||
var owner models.User
|
||||
h.db.ID(userID).Get(&owner)
|
||||
|
||||
result := make([]repoResponse, len(repos))
|
||||
for i, repo := range repos {
|
||||
result[i] = repoResponse{Repository: repo, OwnerName: owner.Username}
|
||||
for i := range repos {
|
||||
result[i] = h.withOwnerName(&repos[i])
|
||||
}
|
||||
jsonOK(w, result)
|
||||
}
|
||||
@@ -193,6 +189,64 @@ func (h *RepoHandler) Commits(w http.ResponseWriter, r *http.Request) {
|
||||
jsonOK(w, commits)
|
||||
}
|
||||
|
||||
func (h *RepoHandler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
repo, ok := h.lookupRepo(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Description *string `json:"description"`
|
||||
IsPrivate *bool `json:"isPrivate"`
|
||||
DefaultBranch *string `json:"defaultBranch"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
jsonError(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
cols := []string{}
|
||||
if body.Description != nil {
|
||||
repo.Description = *body.Description
|
||||
cols = append(cols, "description")
|
||||
}
|
||||
if body.IsPrivate != nil {
|
||||
repo.IsPrivate = *body.IsPrivate
|
||||
cols = append(cols, "is_private")
|
||||
}
|
||||
if body.DefaultBranch != nil {
|
||||
repo.DefaultBranch = *body.DefaultBranch
|
||||
cols = append(cols, "default_branch")
|
||||
}
|
||||
|
||||
if len(cols) > 0 {
|
||||
if _, err := h.db.ID(repo.ID).Cols(cols...).Update(repo); err != nil {
|
||||
jsonError(w, "could not update repository", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
jsonOK(w, h.withOwnerName(repo))
|
||||
}
|
||||
|
||||
func (h *RepoHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
repo, ok := h.lookupRepo(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
callerID, _ := middleware.UserIDFromContext(r.Context())
|
||||
if callerID != repo.OwnerID {
|
||||
jsonError(w, "only the owner can delete a repository", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := h.db.ID(repo.ID).Delete(&models.Repository{}); err != nil {
|
||||
jsonError(w, "could not delete repository", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *RepoHandler) Diff(w http.ResponseWriter, r *http.Request) {
|
||||
repo, ok := h.lookupRepo(w, r)
|
||||
if !ok {
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/forgeo/forgebucket/internal/api/middleware"
|
||||
"github.com/forgeo/forgebucket/internal/models"
|
||||
)
|
||||
|
||||
type SSHKeyHandler struct {
|
||||
db *xorm.Engine
|
||||
}
|
||||
|
||||
func NewSSHKeyHandler(db *xorm.Engine) *SSHKeyHandler {
|
||||
return &SSHKeyHandler{db: db}
|
||||
}
|
||||
|
||||
func (h *SSHKeyHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
userID, _ := middleware.UserIDFromContext(r.Context())
|
||||
var keys []models.SSHKey
|
||||
if err := h.db.Where("user_id = ?", userID).Find(&keys); err != nil {
|
||||
jsonError(w, "could not list SSH keys", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if keys == nil {
|
||||
keys = []models.SSHKey{}
|
||||
}
|
||||
jsonOK(w, keys)
|
||||
}
|
||||
|
||||
func (h *SSHKeyHandler) Add(w http.ResponseWriter, r *http.Request) {
|
||||
userID, _ := middleware.UserIDFromContext(r.Context())
|
||||
|
||||
var body struct {
|
||||
Title string `json:"title"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
jsonError(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if body.Title == "" || body.PublicKey == "" {
|
||||
jsonError(w, "title and publicKey are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse and validate the public key
|
||||
pubKeyBytes := []byte(strings.TrimSpace(body.PublicKey))
|
||||
pub, _, _, _, err := ssh.ParseAuthorizedKey(pubKeyBytes)
|
||||
if err != nil {
|
||||
jsonError(w, "invalid SSH public key format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Compute MD5 fingerprint (standard display format)
|
||||
fingerprint := fingerprintMD5(pub)
|
||||
|
||||
key := &models.SSHKey{
|
||||
UserID: userID,
|
||||
Title: body.Title,
|
||||
Fingerprint: fingerprint,
|
||||
PublicKey: strings.TrimSpace(body.PublicKey),
|
||||
}
|
||||
if _, err := h.db.Insert(key); err != nil {
|
||||
jsonError(w, "key already exists or could not be saved", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(key)
|
||||
}
|
||||
|
||||
func (h *SSHKeyHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
userID, _ := middleware.UserIDFromContext(r.Context())
|
||||
keyID, err := strconv.ParseInt(chi.URLParam(r, "keyID"), 10, 64)
|
||||
if err != nil {
|
||||
jsonError(w, "invalid key ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
n, err := h.db.Where("id = ? AND user_id = ?", keyID, userID).Delete(&models.SSHKey{})
|
||||
if err != nil || n == 0 {
|
||||
jsonError(w, "key not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func fingerprintMD5(pub ssh.PublicKey) string {
|
||||
hash := md5.Sum(pub.Marshal())
|
||||
parts := make([]string, len(hash))
|
||||
for i, b := range hash {
|
||||
parts[i] = fmt.Sprintf("%02x", b)
|
||||
}
|
||||
return strings.Join(parts, ":")
|
||||
}
|
||||
+43
-5
@@ -35,11 +35,35 @@ func New(cfg *config.Config, engine *xorm.Engine, store sessions.Store, staticFi
|
||||
csrf := middleware.CSRF(!cfg.Debug)
|
||||
auth := middleware.NewAuth(store)
|
||||
|
||||
repoH := handlers.NewRepoHandler(engine, cfg)
|
||||
userH := handlers.NewUserHandler(engine, store)
|
||||
prH := handlers.NewPRHandler(engine)
|
||||
pipeH := handlers.NewPipelineHandler(engine)
|
||||
wsH := handlers.NewWSHandler()
|
||||
repoH := handlers.NewRepoHandler(engine, cfg)
|
||||
userH := handlers.NewUserHandler(engine, store)
|
||||
prH := handlers.NewPRHandler(engine)
|
||||
pipeH := handlers.NewPipelineHandler(engine)
|
||||
wsH := handlers.NewWSHandler()
|
||||
gitH := handlers.NewGitHTTPHandler(engine, cfg)
|
||||
issueH := handlers.NewIssueHandler(engine)
|
||||
sshKeyH := handlers.NewSSHKeyHandler(engine)
|
||||
|
||||
// ── Git smart-HTTP transport ───────────────────────────────────────────────
|
||||
// These routes MUST be registered before the SPA catch-all and outside CSRF.
|
||||
// Git clients use HTTP Basic Auth, not the cookie/CSRF flow.
|
||||
r.Route("/{owner}/{repoGit}", func(r chi.Router) {
|
||||
// Only activate for paths ending in .git
|
||||
r.Use(func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
rg := chi.URLParam(req, "repoGit")
|
||||
if len(rg) < 5 || rg[len(rg)-4:] != ".git" {
|
||||
// Not a git URL — skip to the next router
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
})
|
||||
r.Get("/info/refs", gitH.ServeGit)
|
||||
r.Post("/git-upload-pack", gitH.ServeGit)
|
||||
r.Post("/git-receive-pack", gitH.ServeGit)
|
||||
})
|
||||
|
||||
r.Route("/api/v1", func(r chi.Router) {
|
||||
|
||||
@@ -71,11 +95,18 @@ func New(cfg *config.Config, engine *xorm.Engine, store sessions.Store, staticFi
|
||||
|
||||
r.Get("/me", userH.Me)
|
||||
|
||||
// SSH key management
|
||||
r.Get("/user/keys", sshKeyH.List)
|
||||
r.With(csrf).Post("/user/keys", sshKeyH.Add)
|
||||
r.With(csrf).Delete("/user/keys/{keyID}", sshKeyH.Delete)
|
||||
|
||||
r.Route("/repos", func(r chi.Router) {
|
||||
r.Get("/", repoH.List)
|
||||
r.With(csrf).Post("/", repoH.Create)
|
||||
r.Route("/{owner}/{repo}", func(r chi.Router) {
|
||||
r.Get("/", repoH.Get)
|
||||
r.With(csrf).Patch("/", repoH.Update)
|
||||
r.With(csrf).Delete("/", repoH.Delete)
|
||||
r.Get("/tree", repoH.Tree)
|
||||
r.Get("/blob", repoH.Blob)
|
||||
r.Get("/commits", repoH.Commits)
|
||||
@@ -87,6 +118,13 @@ func New(cfg *config.Config, engine *xorm.Engine, store sessions.Store, staticFi
|
||||
r.With(csrf).Post("/{prID}/merge", prH.Merge)
|
||||
r.With(csrf).Post("/{prID}/close", prH.Close)
|
||||
})
|
||||
r.Route("/issues", func(r chi.Router) {
|
||||
r.Get("/", issueH.List)
|
||||
r.With(csrf).Post("/", issueH.Create)
|
||||
r.Get("/{issueNum}", issueH.Get)
|
||||
r.With(csrf).Post("/{issueNum}/close", issueH.Close)
|
||||
r.With(csrf).Post("/{issueNum}/reopen", issueH.Reopen)
|
||||
})
|
||||
r.Route("/pipelines", func(r chi.Router) {
|
||||
r.Get("/", pipeH.List)
|
||||
r.Get("/{runID}", pipeH.Get)
|
||||
|
||||
Reference in New Issue
Block a user