snapshot of https://github.com/go-gitea/gitea.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
cache-seeder / prune (push) Has been skipped
cache-seeder / frontend (push) Failing after 3s
cache-seeder / python (push) Failing after 3s
cache-seeder / gobuild (push) Failing after 4s
cache-seeder / lint (bindata, lint-backend) (push) Failing after 3s
giteabot backport / giteabot (push) Has been skipped
giteabot / giteabot (push) Has been skipped
cache-prune / prune (push) Has been skipped
release-nightly-snapcraft / build-and-publish (ubuntu-24.04) (push) Has been cancelled
release-nightly-snapcraft / build-and-publish (ubuntu-24.04-arm) (push) Has been cancelled
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-container (push) Has been cancelled
cron-renovate / cron-renovate (push) Has been skipped
Some checks failed
cache-seeder / prune (push) Has been skipped
cache-seeder / frontend (push) Failing after 3s
cache-seeder / python (push) Failing after 3s
cache-seeder / gobuild (push) Failing after 4s
cache-seeder / lint (bindata, lint-backend) (push) Failing after 3s
giteabot backport / giteabot (push) Has been skipped
giteabot / giteabot (push) Has been skipped
cache-prune / prune (push) Has been skipped
release-nightly-snapcraft / build-and-publish (ubuntu-24.04) (push) Has been cancelled
release-nightly-snapcraft / build-and-publish (ubuntu-24.04-arm) (push) Has been cancelled
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-container (push) Has been cancelled
cron-renovate / cron-renovate (push) Has been skipped
This commit is contained in:
77
routers/private/actions.go
Normal file
77
routers/private/actions.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
actions_model "gitea.dev/models/actions"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/private"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// GenerateActionsRunnerToken generates a new runner token for a given scope
|
||||
func GenerateActionsRunnerToken(ctx *context.PrivateContext) {
|
||||
var genRequest private.GenerateTokenRequest
|
||||
rd := ctx.Req.Body
|
||||
defer rd.Close()
|
||||
|
||||
if err := json.NewDecoder(rd).Decode(&genRequest); err != nil {
|
||||
ctx.PrivateInternalErrorf("JSON Decode failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
owner, repo, err := parseScope(ctx, genRequest.Scope)
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("parseScope failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := actions_model.GetLatestRunnerToken(ctx, owner, repo)
|
||||
if errors.Is(err, util.ErrNotExist) || (token != nil && !token.IsActive) {
|
||||
token, err = actions_model.NewRunnerToken(ctx, owner, repo)
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("error while creating runner token: %v", err)
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
ctx.PrivateInternalErrorf("could not get unactivated runner token: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.PlainText(http.StatusOK, token.Token)
|
||||
}
|
||||
|
||||
func parseScope(ctx *context.PrivateContext, scope string) (ownerID, repoID int64, err error) {
|
||||
ownerID = 0
|
||||
repoID = 0
|
||||
if scope == "" {
|
||||
return ownerID, repoID, nil
|
||||
}
|
||||
|
||||
ownerName, repoName, found := strings.Cut(scope, "/")
|
||||
|
||||
u, err := user_model.GetUserByName(ctx, ownerName)
|
||||
if err != nil {
|
||||
return ownerID, repoID, err
|
||||
}
|
||||
ownerID = u.ID
|
||||
|
||||
if !found {
|
||||
return ownerID, repoID, nil
|
||||
}
|
||||
|
||||
r, err := repo_model.GetRepositoryByName(ctx, u.ID, repoName)
|
||||
if err != nil {
|
||||
return ownerID, repoID, err
|
||||
}
|
||||
repoID = r.ID
|
||||
return ownerID, repoID, nil
|
||||
}
|
||||
293
routers/private/hook_post_receive.go
Normal file
293
routers/private/hook_post_receive.go
Normal file
@@ -0,0 +1,293 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
git_model "gitea.dev/models/git"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/private"
|
||||
repo_module "gitea.dev/modules/repository"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/audit"
|
||||
gitea_context "gitea.dev/services/context"
|
||||
pull_service "gitea.dev/services/pull"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
func hookPostReceiveCollectPushUpdates(opts *private.HookOptions, repo *repo_model.Repository) []*repo_module.PushUpdateOptions {
|
||||
updates := make([]*repo_module.PushUpdateOptions, 0, len(opts.OldCommitIDs))
|
||||
for i := range opts.OldCommitIDs {
|
||||
refFullName := opts.RefFullNames[i]
|
||||
|
||||
// Only trigger activity updates for changes to branches or
|
||||
// tags. Updates to other refs (eg, refs/notes, refs/changes,
|
||||
// or other less-standard refs spaces are ignored since there
|
||||
// may be a very large number of them).
|
||||
if refFullName.IsBranch() || refFullName.IsTag() {
|
||||
option := &repo_module.PushUpdateOptions{
|
||||
RefFullName: refFullName,
|
||||
OldCommitID: opts.OldCommitIDs[i],
|
||||
NewCommitID: opts.NewCommitIDs[i],
|
||||
PusherID: opts.UserID,
|
||||
PusherName: opts.UserName,
|
||||
RepoUserName: repo.OwnerName,
|
||||
RepoName: repo.Name,
|
||||
}
|
||||
updates = append(updates, option)
|
||||
}
|
||||
}
|
||||
return updates
|
||||
}
|
||||
|
||||
func hookPostReceiveSyncDatabaseBranches(ctx *gitea_context.PrivateContext, opts *private.HookOptions, repo *repo_model.Repository, updates []*repo_module.PushUpdateOptions) bool {
|
||||
branchesToSync := make([]*repo_module.PushUpdateOptions, 0, len(updates))
|
||||
for _, update := range updates {
|
||||
if !update.RefFullName.IsBranch() {
|
||||
continue
|
||||
}
|
||||
if update.IsDelRef() {
|
||||
if err := git_model.MarkBranchAsDeleted(ctx, repo.ID, update.RefFullName.BranchName(), update.PusherID); err != nil {
|
||||
ctx.PrivateInternalErrorf("failed to mark branch %s as deleted: %v", update.RefFullName, err)
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
branchesToSync = append(branchesToSync, update)
|
||||
// TODO: should we return the error and return the error when pushing? Currently it will log the error and not prevent the pushing
|
||||
pull_service.UpdatePullsRefs(ctx, repo, update)
|
||||
}
|
||||
}
|
||||
|
||||
if len(branchesToSync) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
gitRepo, err := git.RepositoryFromRequestContextOrOpen(ctx, repo)
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("failed to open repository: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
branchNames := make([]string, 0, len(branchesToSync))
|
||||
commitIDs := make([]string, 0, len(branchesToSync))
|
||||
for _, update := range branchesToSync {
|
||||
branchNames = append(branchNames, update.RefFullName.BranchName())
|
||||
commitIDs = append(commitIDs, update.NewCommitID)
|
||||
}
|
||||
|
||||
if err = repo_service.SyncBranchesToDB(ctx, repo.ID, opts.UserID, gitRepo, branchNames, commitIDs); err != nil {
|
||||
ctx.PrivateInternalErrorf("failed to sync branch to DB: %v", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// HookPostReceive updates services and users
|
||||
func HookPostReceive(ctx *gitea_context.PrivateContext) {
|
||||
opts := web.GetForm[*private.HookOptions](ctx)
|
||||
if opts.IsWiki {
|
||||
setting.PanicInDevOrTesting("wiki hook-post-receive is not supported")
|
||||
return
|
||||
}
|
||||
if !loadContextDoerPermission(ctx, opts.UserID, opts.UserExtDoerData) {
|
||||
return
|
||||
}
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
// first, collect updates and sync branches
|
||||
updates := hookPostReceiveCollectPushUpdates(opts, repo)
|
||||
if !hookPostReceiveSyncDatabaseBranches(ctx, opts, repo, updates) {
|
||||
return
|
||||
}
|
||||
hookPostReceiveSyncRepoDefaultBranch(ctx, opts, repo)
|
||||
|
||||
// handle pull request merging, a pull request action should push at least 1 commit
|
||||
if opts.PushTrigger == repo_module.PushTriggerPRMergeToBase {
|
||||
if !hookPostReceiveHandlePullRequestMerging(ctx, opts, updates) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !hookPostReceiveUpdateRepoByOptions(ctx, opts, repo) {
|
||||
return
|
||||
}
|
||||
|
||||
// push async updates
|
||||
if err := repo_service.PushUpdates(updates...); err != nil {
|
||||
ctx.PrivateInternalErrorf("failed to push updates: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
hookPostReceiveRespondWithTrailer(ctx, opts, repo)
|
||||
}
|
||||
|
||||
func hookPostReceiveUpdateRepoByOptions(ctx *gitea_context.PrivateContext, opts *private.HookOptions, repo *repo_model.Repository) bool {
|
||||
isPrivate := opts.GitPushOptions.Bool(private.GitPushOptionRepoPrivate)
|
||||
isTemplate := opts.GitPushOptions.Bool(private.GitPushOptionRepoTemplate)
|
||||
// Handle Push Options
|
||||
if isPrivate.Has() || isTemplate.Has() {
|
||||
if !ctx.Repo.Permission.IsAdmin() {
|
||||
ctx.PrivateUserErrorf(http.StatusNotFound, "permission denied")
|
||||
return false
|
||||
}
|
||||
|
||||
// Only honor these options while the repo is still empty (the push-to-create
|
||||
// case). On a populated repo a bare "git push -o repo.private=..." would
|
||||
// silently flip visibility, bypassing the audit log, webhooks and notifications.
|
||||
if !repo.IsEmpty {
|
||||
return true
|
||||
}
|
||||
|
||||
// The repo is empty and being initialized by this push, so there is no
|
||||
// dependent state (webhooks, notifications, visibility fan-out) to reconcile
|
||||
// yet; setting the flags directly is sufficient in this push-to-create case.
|
||||
if isPrivate.Has() && repo.IsPrivate != isPrivate.Value() {
|
||||
repo.IsPrivate = isPrivate.Value()
|
||||
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {
|
||||
log.Error("failed to update repo is_private: %v", err)
|
||||
} else {
|
||||
audit.RecordAs(ctx, ctx.Doer, audit_model.RepositoryVisibility, repo, "visibility", repo.IsPrivate)
|
||||
}
|
||||
}
|
||||
if isTemplate.Has() && repo.IsTemplate != isTemplate.Value() {
|
||||
repo.IsTemplate = isTemplate.Value()
|
||||
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_template"); err != nil {
|
||||
log.Error("failed to update repo is_template: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func hookPostReceiveRespondWithTrailer(ctx *gitea_context.PrivateContext, opts *private.HookOptions, repo *repo_model.Repository) {
|
||||
results := make([]private.HookPostReceiveBranchResult, 0, len(opts.OldCommitIDs))
|
||||
baseRepo := repo
|
||||
if repo.IsFork {
|
||||
if err := repo.GetBaseRepo(ctx); err != nil {
|
||||
ctx.PrivateInternalErrorf("failed to load base repo: %v", err)
|
||||
return
|
||||
}
|
||||
if repo.BaseRepo.AllowsPulls(ctx) {
|
||||
baseRepo = repo.BaseRepo
|
||||
}
|
||||
}
|
||||
|
||||
if !baseRepo.AllowsPulls(ctx) {
|
||||
// We can stop there's no need to go any further
|
||||
ctx.JSON(http.StatusOK, private.HookPostReceiveResult{})
|
||||
return
|
||||
}
|
||||
|
||||
// Now handle the pull request notification trailers
|
||||
for i := range opts.OldCommitIDs {
|
||||
refFullName := opts.RefFullNames[i]
|
||||
newCommitID := opts.NewCommitIDs[i]
|
||||
|
||||
// If we've pushed a branch (and not deleted it)
|
||||
if !git.IsEmptyCommitID(newCommitID) && refFullName.IsBranch() {
|
||||
branch := refFullName.BranchName()
|
||||
|
||||
if branch == baseRepo.DefaultBranch && !repo.IsFork {
|
||||
// If our branch is the default branch of an unforked repo - there's no PR to create or refer to
|
||||
results = append(results, private.HookPostReceiveBranchResult{})
|
||||
continue
|
||||
}
|
||||
|
||||
pr, err := issues_model.GetUnmergedPullRequest(ctx, repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch, issues_model.PullRequestFlowGithub)
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
ctx.PrivateInternalErrorf("failed to get active PR for branch %s: %v", branch, err)
|
||||
return
|
||||
}
|
||||
if pr == nil {
|
||||
results = append(results, private.HookPostReceiveBranchResult{
|
||||
Message: setting.Git.PullRequestPushMessage && baseRepo.AllowsPulls(ctx),
|
||||
Create: true,
|
||||
Branch: branch,
|
||||
URL: fmt.Sprintf("%s/pulls/new/%s", repo.HTMLURL(), util.PathEscapeSegments(branch)),
|
||||
})
|
||||
} else {
|
||||
results = append(results, private.HookPostReceiveBranchResult{
|
||||
Message: setting.Git.PullRequestPushMessage && baseRepo.AllowsPulls(ctx),
|
||||
Create: false,
|
||||
Branch: branch,
|
||||
URL: fmt.Sprintf("%s/pulls/%d", baseRepo.HTMLURL(), pr.Index),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.JSON(http.StatusOK, private.HookPostReceiveResult{Results: results})
|
||||
}
|
||||
|
||||
// hookPostReceiveHandlePullRequestMerging handle pull request merging, a pull request action should push at least 1 commit
|
||||
func hookPostReceiveHandlePullRequestMerging(ctx *gitea_context.PrivateContext, opts *private.HookOptions, updates []*repo_module.PushUpdateOptions) bool {
|
||||
if len(updates) == 0 {
|
||||
ctx.PrivateInternalErrorf("Pushing a merged PR (pr:%d) no commits pushed ", opts.PullRequestID)
|
||||
return false
|
||||
}
|
||||
|
||||
pr, err := issues_model.GetPullRequestByID(ctx, opts.PullRequestID)
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("failed to get pull request %d: %v", opts.PullRequestID, err)
|
||||
return false
|
||||
}
|
||||
|
||||
// FIXME: Maybe we need a `PullRequestStatusMerged` status for PRs that are merged, currently we use the previous status
|
||||
// here to keep it as before, that maybe PullRequestStatusMergeable
|
||||
_, err = pull_service.SetMerged(ctx, pr, updates[len(updates)-1].NewCommitID, timeutil.TimeStampNow(), ctx.Doer, pr.Status)
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("failed to set pr %d to merged: %v", pr.ID, err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func hookPostReceiveSyncRepoDefaultBranch(ctx *gitea_context.PrivateContext, opts *private.HookOptions, repo *repo_model.Repository) {
|
||||
hasBranch := false
|
||||
for _, refFullName := range opts.RefFullNames {
|
||||
if hasBranch = refFullName.IsBranch(); hasBranch {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasBranch {
|
||||
return
|
||||
}
|
||||
gitRepo, err := git.RepositoryFromRequestContextOrOpen(ctx, repo)
|
||||
if err != nil {
|
||||
log.Error("failed to open git repo: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// if default branch doesn't exist, try to guess one from existing git repo
|
||||
_, err = gitRepo.GetBranchCommitID(ctx, repo.DefaultBranch)
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
for _, guessBranchName := range []string{"main", "master"} {
|
||||
if _, err = gitRepo.GetBranchCommitID(ctx, guessBranchName); err == nil {
|
||||
repo.DefaultBranch = guessBranchName
|
||||
err = repo_model.UpdateDefaultBranch(ctx, repo)
|
||||
if err != nil {
|
||||
log.Error("failed to update default branch: %v", err)
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if default branch was pushed, always keep the HEAD ref in sync
|
||||
for _, refFullName := range opts.RefFullNames {
|
||||
if refFullName.IsBranch() && refFullName.BranchName() == repo.DefaultBranch {
|
||||
_ = git.SetDefaultBranch(ctx, repo, repo.DefaultBranch)
|
||||
}
|
||||
}
|
||||
}
|
||||
49
routers/private/hook_post_receive_test.go
Normal file
49
routers/private/hook_post_receive_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
issues_model "gitea.dev/models/issues"
|
||||
pull_model "gitea.dev/models/pull"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/private"
|
||||
repo_module "gitea.dev/modules/repository"
|
||||
"gitea.dev/services/contexttest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHandlePullRequestMerging(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
pr, err := issues_model.GetUnmergedPullRequest(t.Context(), 1, 1, "branch2", "master", issues_model.PullRequestFlowGithub)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, pr.LoadBaseRepo(t.Context()))
|
||||
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
err = pull_model.ScheduleAutoMerge(t.Context(), user1, pr.ID, repo_model.MergeStyleSquash, "squash merge a pr", false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
autoMerge := unittest.AssertExistsAndLoadBean(t, &pull_model.AutoMerge{PullID: pr.ID})
|
||||
|
||||
ctx, resp := contexttest.MockPrivateContext(t, "/")
|
||||
ctx.Doer = user2
|
||||
hookPostReceiveHandlePullRequestMerging(ctx, &private.HookOptions{
|
||||
PullRequestID: pr.ID,
|
||||
UserID: 2,
|
||||
}, []*repo_module.PushUpdateOptions{
|
||||
{NewCommitID: "01234567"},
|
||||
})
|
||||
assert.Empty(t, resp.Body.String())
|
||||
pr, err = issues_model.GetPullRequestByID(t.Context(), pr.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, pr.HasMerged)
|
||||
assert.Equal(t, "01234567", pr.MergedCommitID)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{ID: autoMerge.ID})
|
||||
}
|
||||
385
routers/private/hook_pre_receive.go
Normal file
385
routers/private/hook_pre_receive.go
Normal file
@@ -0,0 +1,385 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
git_model "gitea.dev/models/git"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/private"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/agit"
|
||||
gitea_context "gitea.dev/services/context"
|
||||
pull_service "gitea.dev/services/pull"
|
||||
)
|
||||
|
||||
type preReceiveContext struct {
|
||||
*gitea_context.PrivateContext
|
||||
env []string
|
||||
opts *private.HookOptions
|
||||
|
||||
// this context should only contain shared variables, mutable variables like "current branch name" shouldn't be put here
|
||||
canWriteCodeUnitCached *bool
|
||||
canCreatePullRequest *bool
|
||||
protectedTags []*git_model.ProtectedTag
|
||||
}
|
||||
|
||||
func (ctx *preReceiveContext) canWriteCodeUnit() bool {
|
||||
if ctx.canWriteCodeUnitCached == nil {
|
||||
ctx.canWriteCodeUnitCached = new(ctx.Repo.Permission.CanWrite(unit.TypeCode))
|
||||
}
|
||||
return *ctx.canWriteCodeUnitCached
|
||||
}
|
||||
|
||||
// canWriteCodeRef returns true if pusher can write to the code ref (branch/tag/commit)
|
||||
func (ctx *preReceiveContext) canWriteCodeRef(refFullName git.RefName) bool {
|
||||
if ctx.canWriteCodeUnit() {
|
||||
return true
|
||||
}
|
||||
// then check whether if the pusher is a maintainer who can write the PR author's head repo branch
|
||||
if !refFullName.IsBranch() {
|
||||
return false
|
||||
}
|
||||
return issues_model.CanMaintainerWriteToBranch(ctx, ctx.Repo.Permission, refFullName.BranchName(), ctx.Doer)
|
||||
}
|
||||
|
||||
// assertCanWriteRef returns true if pusher can write to the code ref, otherwise it responds with 403 Forbidden and returns false
|
||||
func (ctx *preReceiveContext) assertCanWriteRef(refFullName git.RefName) bool {
|
||||
if !ctx.canWriteCodeRef(refFullName) {
|
||||
if ctx.Written() {
|
||||
return false
|
||||
}
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "User permission denied for writing.")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// CanCreatePullRequest returns true if pusher can create pull requests
|
||||
func (ctx *preReceiveContext) CanCreatePullRequest() bool {
|
||||
if ctx.canCreatePullRequest == nil {
|
||||
ctx.canCreatePullRequest = new(ctx.Repo.Permission.CanRead(unit.TypePullRequests))
|
||||
}
|
||||
return *ctx.canCreatePullRequest
|
||||
}
|
||||
|
||||
// AssertCreatePullRequest returns true if can create pull requests
|
||||
func (ctx *preReceiveContext) AssertCreatePullRequest() bool {
|
||||
if !ctx.CanCreatePullRequest() {
|
||||
if ctx.Written() {
|
||||
return false
|
||||
}
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "User permission denied for creating pull-request.")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// HookPreReceive checks whether a individual commit is acceptable
|
||||
func HookPreReceive(ctx *gitea_context.PrivateContext) {
|
||||
opts := web.GetForm[*private.HookOptions](ctx)
|
||||
if !loadContextDoerPermission(ctx, opts.UserID, opts.UserExtDoerData) {
|
||||
return
|
||||
}
|
||||
|
||||
ourCtx := &preReceiveContext{
|
||||
PrivateContext: ctx,
|
||||
env: generateGitEnv(opts), // Generate git environment for checking commits
|
||||
opts: opts,
|
||||
}
|
||||
|
||||
// Iterate across the provided old commit IDs
|
||||
for i := range opts.OldCommitIDs {
|
||||
oldCommitID := opts.OldCommitIDs[i]
|
||||
newCommitID := opts.NewCommitIDs[i]
|
||||
refFullName := opts.RefFullNames[i]
|
||||
|
||||
switch {
|
||||
case refFullName.IsBranch():
|
||||
preReceiveBranch(ourCtx, oldCommitID, newCommitID, refFullName)
|
||||
case refFullName.IsTag():
|
||||
preReceiveTag(ourCtx, refFullName)
|
||||
case git.DefaultFeatures().SupportProcReceive && refFullName.IsFor():
|
||||
preReceiveFor(ourCtx, refFullName)
|
||||
default:
|
||||
ourCtx.assertCanWriteRef(refFullName)
|
||||
}
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.PlainText(http.StatusOK, "ok")
|
||||
}
|
||||
|
||||
func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, refFullName git.RefName) {
|
||||
branchName := refFullName.BranchName()
|
||||
|
||||
if !ctx.assertCanWriteRef(refFullName) {
|
||||
return
|
||||
}
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
gitRepo := ctx.Repo.GitRepo
|
||||
objectFormat := ctx.Repo.GetObjectFormat()
|
||||
|
||||
defaultBranch := repo.DefaultBranch
|
||||
if ctx.opts.IsWiki && repo.DefaultWikiBranch != "" {
|
||||
defaultBranch = repo.DefaultWikiBranch
|
||||
}
|
||||
if branchName == defaultBranch && newCommitID == objectFormat.EmptyObjectID().String() {
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "Branch %s is the default branch and cannot be deleted", branchName)
|
||||
return
|
||||
}
|
||||
|
||||
protectBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branchName)
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("Unable to get protected branch: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Allow pushes to non-protected branches
|
||||
if protectBranch == nil {
|
||||
return
|
||||
}
|
||||
protectBranch.Repo = repo
|
||||
|
||||
// This ref is a protected branch.
|
||||
//
|
||||
// First of all we need to enforce absolutely:
|
||||
//
|
||||
// 1. Detect and prevent deletion of the branch
|
||||
if newCommitID == objectFormat.EmptyObjectID().String() {
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "Branch %s is protected from deletion", branchName)
|
||||
return
|
||||
}
|
||||
|
||||
isForcePush := false
|
||||
|
||||
// 2. Disallow force pushes to protected branches
|
||||
if oldCommitID != objectFormat.EmptyObjectID().String() {
|
||||
output, _, err := gitcmd.NewCommand("rev-list", "--max-count=1").
|
||||
AddDynamicArguments(oldCommitID, "^"+newCommitID).
|
||||
WithEnv(ctx.env).WithRepo(repo).RunStdString(ctx)
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("Unable to detect force push between %s and %s in %s: %v", oldCommitID, newCommitID, repo.FullName(), err)
|
||||
return
|
||||
} else if len(output) > 0 {
|
||||
if protectBranch.CanForcePush {
|
||||
isForcePush = true
|
||||
} else {
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "Branch %s is protected from force push", branchName)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Enforce require signed commits
|
||||
if protectBranch.RequireSignedCommits {
|
||||
err := verifyCommits(ctx, oldCommitID, newCommitID, gitRepo, ctx.env)
|
||||
if err != nil {
|
||||
errUnverified, ok := err.(*errUnverifiedCommit)
|
||||
if !ok {
|
||||
ctx.PrivateInternalErrorf("Unable to check commits from %s to %s: %v", oldCommitID, newCommitID, err)
|
||||
return
|
||||
}
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "Branch %s is protected from unverified commit %s", branchName, errUnverified.sha)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Now there are several tests which can be overridden:
|
||||
//
|
||||
// 4. Check protected file patterns - this is overridable from the UI
|
||||
changedProtectedfiles := false
|
||||
protectedFilePath := ""
|
||||
|
||||
globs := protectBranch.GetProtectedFilePatterns()
|
||||
if len(globs) > 0 {
|
||||
_, err := pull_service.CheckFileProtection(ctx, gitRepo, branchName, oldCommitID, newCommitID, globs, 1, ctx.env)
|
||||
if err != nil {
|
||||
errFilePathProtected, ok := errors.AsType[pull_service.ErrFilePathProtected](err)
|
||||
if !ok {
|
||||
ctx.PrivateInternalErrorf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err)
|
||||
return
|
||||
}
|
||||
|
||||
changedProtectedfiles = true
|
||||
protectedFilePath = errFilePathProtected.Path
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Check if the doer is allowed to push (and force-push if the incoming push is a force-push)
|
||||
var canPush bool
|
||||
if ctx.opts.UserID == user_model.DeployKeyUserID {
|
||||
// This flag is only ever true if protectBranch.CanForcePush is true
|
||||
if isForcePush {
|
||||
canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableForcePushAllowlist || protectBranch.ForcePushAllowlistDeployKeys)
|
||||
} else {
|
||||
canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys)
|
||||
}
|
||||
} else {
|
||||
if isForcePush {
|
||||
canPush = !changedProtectedfiles && protectBranch.CanUserForcePush(ctx, ctx.Doer)
|
||||
} else {
|
||||
canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, ctx.Doer)
|
||||
}
|
||||
}
|
||||
|
||||
// 6. If we're not allowed to push directly
|
||||
if !canPush {
|
||||
// Is this is a merge from the UI/API?
|
||||
if ctx.opts.PullRequestID == 0 {
|
||||
// 6a. If we're not merging from the UI/API then there are two ways we got here:
|
||||
//
|
||||
// We are changing a protected file, and we're not allowed to do that
|
||||
if changedProtectedfiles {
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "Branch %s is protected from changing file %s", branchName, protectedFilePath)
|
||||
return
|
||||
}
|
||||
|
||||
// Allow commits that only touch unprotected files
|
||||
globs := protectBranch.GetUnprotectedFilePatterns()
|
||||
if len(globs) > 0 {
|
||||
unprotectedFilesOnly, err := pull_service.CheckUnprotectedFiles(ctx, gitRepo, branchName, oldCommitID, newCommitID, globs, ctx.env)
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err)
|
||||
return
|
||||
}
|
||||
if unprotectedFilesOnly {
|
||||
// Commit only touches unprotected files, this is allowed
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Or we're simply not able to push to this protected branch
|
||||
if isForcePush {
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "Not allowed to force-push to protected branch %s", branchName)
|
||||
return
|
||||
}
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "Not allowed to push to protected branch %s", branchName)
|
||||
return
|
||||
}
|
||||
// 6b. Merge (from UI or API)
|
||||
|
||||
// Get the PR, user and permissions for the user in the repository
|
||||
pr, err := issues_model.GetPullRequestByID(ctx, ctx.opts.PullRequestID)
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("Unable to get PullRequest %d Error: %v", ctx.opts.PullRequestID, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Now check if the user is allowed to merge PRs for this repository
|
||||
// Note: we can use ctx.perm and ctx.user directly as they will have been loaded above
|
||||
allowedMerge, err := pull_service.IsUserAllowedToMerge(ctx, pr, ctx.Repo.Permission, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("Error calculating if allowed to merge: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !allowedMerge {
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "Not allowed to push to protected branch %s", branchName)
|
||||
return
|
||||
}
|
||||
|
||||
// If we can bypass branch protection we can ignore status checks, reviews and protected files
|
||||
if git_model.CanBypassBranchProtection(ctx, protectBranch, ctx.Doer, ctx.Repo.Permission.IsAdmin()) {
|
||||
return
|
||||
}
|
||||
|
||||
// Now if we're not an admin - we can't overwrite protected files so fail now
|
||||
if changedProtectedfiles {
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "Branch %s is protected from changing file %s", branchName, protectedFilePath)
|
||||
return
|
||||
}
|
||||
|
||||
// Check all status checks and reviews are ok
|
||||
if err := pull_service.CheckPullBranchProtections(ctx, pr, true); err != nil {
|
||||
if errors.Is(err, pull_service.ErrNotReadyToMerge) {
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, ctx.opts.PullRequestID, err.Error())
|
||||
return
|
||||
}
|
||||
ctx.PrivateInternalErrorf("Unable to get status of pull request %d: %v", ctx.opts.PullRequestID, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func preReceiveTag(ctx *preReceiveContext, refFullName git.RefName) {
|
||||
if !ctx.assertCanWriteRef(refFullName) {
|
||||
return
|
||||
}
|
||||
|
||||
tagName := refFullName.TagName()
|
||||
|
||||
if ctx.protectedTags == nil {
|
||||
var err error
|
||||
ctx.protectedTags, err = git_model.GetProtectedTags(ctx, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("Unable to get protected tags: %v", err)
|
||||
return
|
||||
}
|
||||
ctx.protectedTags = util.SliceNilAsEmpty(ctx.protectedTags)
|
||||
}
|
||||
|
||||
isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, ctx.protectedTags, tagName, ctx.opts.UserID)
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("unable to check allowed tags: %v", err)
|
||||
return
|
||||
}
|
||||
if !isAllowed {
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "Tag %s is protected", tagName)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func preReceiveFor(ctx *preReceiveContext, refFullName git.RefName) {
|
||||
if !ctx.AssertCreatePullRequest() {
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Repo.Repository.IsEmpty {
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "Can't create pull request for an empty repository.")
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.opts.IsWiki {
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "Pull requests are not supported on the wiki.")
|
||||
return
|
||||
}
|
||||
|
||||
_, _, err := agit.GetAgitBranchInfo(ctx, ctx.Repo.Repository.ID, refFullName.ForBranchName())
|
||||
if err != nil {
|
||||
if !errors.Is(err, util.ErrNotExist) {
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "Unexpected ref: %s", refFullName)
|
||||
} else {
|
||||
ctx.PrivateInternalErrorf("Unable to get branch info for ref %s: %v", refFullName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateGitEnv(opts *private.HookOptions) (env []string) {
|
||||
env = os.Environ()
|
||||
if opts.GitAlternativeObjectDirectories != "" {
|
||||
env = append(env,
|
||||
private.GitAlternativeObjectDirectories+"="+opts.GitAlternativeObjectDirectories)
|
||||
}
|
||||
if opts.GitObjectDirectory != "" {
|
||||
env = append(env,
|
||||
private.GitObjectDirectory+"="+opts.GitObjectDirectory)
|
||||
}
|
||||
if opts.GitQuarantinePath != "" {
|
||||
env = append(env,
|
||||
private.GitQuarantinePath+"="+opts.GitQuarantinePath)
|
||||
}
|
||||
return env
|
||||
}
|
||||
64
routers/private/hook_pre_receive_test.go
Normal file
64
routers/private/hook_pre_receive_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
issues_model "gitea.dev/models/issues"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/services/contexttest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestPreReceiveCanWriteCodePerBranch ensures the maintainer-edit write grant is evaluated against
|
||||
// the exact ref being pushed on every call, derived from that ref rather than shared mutable state.
|
||||
// Otherwise, a per-branch grant (an open PR with "allow edits from maintainers") could be batched
|
||||
// together with a protected branch or a tag to escalate into full repository write.
|
||||
func TestPreReceiveCanWriteCodePerBranch(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
|
||||
headRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
|
||||
require.NoError(t, baseRepo.LoadOwner(t.Context()))
|
||||
require.NoError(t, headRepo.LoadOwner(t.Context()))
|
||||
|
||||
// An open PR from the head repo owner, with maintainer edits allowed: this grants the base
|
||||
// repo owner write access to exactly this head branch and nothing else.
|
||||
pr := &issues_model.PullRequest{
|
||||
Issue: &issues_model.Issue{
|
||||
RepoID: baseRepo.ID,
|
||||
PosterID: headRepo.OwnerID,
|
||||
},
|
||||
HeadRepoID: headRepo.ID,
|
||||
BaseRepoID: baseRepo.ID,
|
||||
HeadBranch: "granted-branch",
|
||||
BaseBranch: "master",
|
||||
AllowMaintainerEdit: true,
|
||||
}
|
||||
require.NoError(t, issues_model.NewPullRequest(t.Context(), baseRepo, pr.Issue, nil, nil, pr))
|
||||
|
||||
// The pusher is the base repo owner (the maintainer) with only read access on the head repo.
|
||||
mockCtx, _ := contexttest.MockPrivateContext(t, "/")
|
||||
ctx := &preReceiveContext{PrivateContext: mockCtx}
|
||||
ctx.SetPathParam("owner", headRepo.OwnerName)
|
||||
ctx.SetPathParam("repo", headRepo.Name)
|
||||
RepoAssignment(ctx.PrivateContext)
|
||||
loadContextDoerPermission(ctx.PrivateContext, baseRepo.OwnerID, "")
|
||||
|
||||
// The granted branch must be writable...
|
||||
assert.True(t, ctx.canWriteCodeRef(git.RefNameFromBranch("granted-branch")))
|
||||
|
||||
// ...but another branch in the same push must NOT inherit that grant.
|
||||
assert.False(t, ctx.canWriteCodeRef(git.RefNameFromBranch("master")))
|
||||
|
||||
// ...and a tag sharing the granted branch's name must NOT inherit it either: the grant is
|
||||
// scoped to PR head branches, so a non-branch ref can never match it. (A tag ref already
|
||||
// yields an empty branch name, so this guards the per-ref evaluation, not the IsBranch check.)
|
||||
assert.False(t, ctx.canWriteCodeRef(git.RefNameFromTag("granted-branch")))
|
||||
}
|
||||
52
routers/private/hook_proc_receive.go
Normal file
52
routers/private/hook_proc_receive.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
issues_model "gitea.dev/models/issues"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/private"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/agit"
|
||||
gitea_context "gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// HookProcReceive proc-receive hook - only handles agit Proc-Receive requests at present
|
||||
func HookProcReceive(ctx *gitea_context.PrivateContext) {
|
||||
opts := web.GetForm[*private.HookOptions](ctx)
|
||||
if !git.DefaultFeatures().SupportProcReceive {
|
||||
ctx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if !loadContextDoerPermission(ctx, opts.UserID, opts.UserExtDoerData) {
|
||||
return
|
||||
}
|
||||
|
||||
results, err := agit.ProcReceive(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, &agit.ProcReceiveOptions{
|
||||
OldCommitIDs: opts.OldCommitIDs,
|
||||
NewCommitIDs: opts.NewCommitIDs,
|
||||
RefFullNames: opts.RefFullNames,
|
||||
GitPushOptions: opts.GitPushOptions,
|
||||
Doer: ctx.Doer,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, issues_model.ErrMustCollaborator) {
|
||||
ctx.PrivateUserErrorf(http.StatusUnauthorized, "You must be a collaborator to create pull request.")
|
||||
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.PrivateUserErrorf(http.StatusUnauthorized, "Cannot create pull request because you are blocked by the repository owner.")
|
||||
} else {
|
||||
ctx.PrivateInternalErrorf("agit.ProcReceive failed: %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, private.HookProcReceiveResult{
|
||||
Results: results,
|
||||
})
|
||||
}
|
||||
92
routers/private/hook_verification.go
Normal file
92
routers/private/hook_verification.go
Normal file
@@ -0,0 +1,92 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/log"
|
||||
asymkey_service "gitea.dev/services/asymkey"
|
||||
)
|
||||
|
||||
// This file contains commit verification functions for refs passed across in hooks
|
||||
|
||||
func verifyCommits(ctx context.Context, oldCommitID, newCommitID string, repo *git.Repository, env []string) error {
|
||||
var command *gitcmd.Command
|
||||
objectFormat, _ := repo.GetObjectFormat(ctx)
|
||||
if oldCommitID == objectFormat.EmptyObjectID().String() {
|
||||
// When creating a new branch, the oldCommitID is empty, by using "newCommitID --not --all":
|
||||
// List commits that are reachable by following the newCommitID, exclude "all" existing heads/tags commits
|
||||
// So, it only lists the new commits received, doesn't list the commits already present in the receiving repository
|
||||
command = gitcmd.NewCommand("rev-list").AddDynamicArguments(newCommitID).AddArguments("--not", "--all")
|
||||
} else {
|
||||
command = gitcmd.NewCommand("rev-list").AddDynamicArguments(oldCommitID + "..." + newCommitID)
|
||||
}
|
||||
// This is safe as force pushes are already forbidden
|
||||
stdoutReader, stdoutReaderClose := command.MakeStdoutPipe()
|
||||
defer stdoutReaderClose()
|
||||
|
||||
err := command.WithEnv(env).
|
||||
WithRepo(repo).
|
||||
WithPipelineFunc(func(gitCtx gitcmd.Context) error {
|
||||
err := readAndVerifyCommitsFromShaReader(ctx, stdoutReader, repo, env)
|
||||
return gitCtx.CancelPipeline(err)
|
||||
}).
|
||||
Run(ctx)
|
||||
if err != nil && !isErrUnverifiedCommit(err) {
|
||||
log.Error("Unable to check commits from %s to %s in %s: %v", oldCommitID, newCommitID, repo.LogString(), err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func readAndVerifyCommitsFromShaReader(ctx context.Context, input io.ReadCloser, repo *git.Repository, env []string) error {
|
||||
scanner := bufio.NewScanner(input)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
err := readAndVerifyCommit(ctx, line, repo, env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
func readAndVerifyCommit(ctx context.Context, sha string, repo *git.Repository, env []string) error {
|
||||
commitID := git.MustIDFromString(sha)
|
||||
cmd := gitcmd.NewCommand("cat-file", "commit").AddDynamicArguments(sha)
|
||||
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||
defer stdoutReaderClose()
|
||||
|
||||
return cmd.WithEnv(env).
|
||||
WithRepo(repo).
|
||||
WithPipelineFunc(func(gitCtx gitcmd.Context) error {
|
||||
commit, err := git.CommitFromReader(commitID, stdoutReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
verification := asymkey_service.ParseCommitWithSignature(ctx, commit)
|
||||
if !verification.Verified {
|
||||
return gitCtx.CancelPipeline(&errUnverifiedCommit{commit.ID.String()})
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
Run(ctx)
|
||||
}
|
||||
|
||||
type errUnverifiedCommit struct {
|
||||
sha string
|
||||
}
|
||||
|
||||
func (e *errUnverifiedCommit) Error() string {
|
||||
return "Unverified commit: " + e.sha
|
||||
}
|
||||
|
||||
func isErrUnverifiedCommit(err error) bool {
|
||||
_, ok := err.(*errUnverifiedCommit)
|
||||
return ok
|
||||
}
|
||||
44
routers/private/hook_verification_test.go
Normal file
44
routers/private/hook_verification_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/git"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestVerifyCommits(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
|
||||
gitRepo, err := git.OpenRepositoryLocal(t.Context(), "tests/repos/repo1_hook_verification")
|
||||
require.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
|
||||
objectFormat, err := gitRepo.GetObjectFormat(t.Context())
|
||||
assert.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
base, head string
|
||||
verified bool
|
||||
}{
|
||||
{"72920278f2f999e3005801e5d5b8ab8139d3641c", "d766f2917716d45be24bfa968b8409544941be32", true},
|
||||
{objectFormat.EmptyObjectID().String(), "93eac826f6188f34646cea81bf426aa5ba7d3bfe", true}, // New branch with verified commit
|
||||
{"9779d17a04f1e2640583d35703c62460b2d86e0a", "72920278f2f999e3005801e5d5b8ab8139d3641c", false},
|
||||
{objectFormat.EmptyObjectID().String(), "9ce3f779ae33f31fce17fac3c512047b75d7498b", false}, // New branch with unverified commit
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
err = verifyCommits(t.Context(), tc.base, tc.head, gitRepo, nil)
|
||||
if tc.verified {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
112
routers/private/internal.go
Normal file
112
routers/private/internal.go
Normal file
@@ -0,0 +1,112 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package private contains all internal routes. The package name "internal" isn't usable because Golang reserves it for disabling cross-package usage.
|
||||
package private
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/private"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/validation"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/modules/web/middleware"
|
||||
"gitea.dev/routers/common"
|
||||
"gitea.dev/routers/web/misc"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
func authInternal(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
if setting.InternalToken == "" {
|
||||
log.Warn(`The INTERNAL_TOKEN setting is missing from the configuration file: %q, internal API can't work.`, setting.CustomConf)
|
||||
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
tokens := req.Header.Get("X-Gitea-Internal-Auth") // TODO: use something like JWT or HMAC to avoid passing the token in the clear
|
||||
after, found := strings.CutPrefix(tokens, "Bearer ")
|
||||
authSucceeded := found && subtle.ConstantTimeCompare([]byte(after), []byte(setting.InternalToken)) == 1
|
||||
if !authSucceeded {
|
||||
log.Debug("Forbidden attempt to access internal url: Authorization header: %s", tokens)
|
||||
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
|
||||
// bind binding an obj to a handler
|
||||
func bind[T any](tmpl T) any {
|
||||
return func(ctx *context.PrivateContext) {
|
||||
form, errs := middleware.BindFormAny(ctx.Req, validation.Binder(), tmpl)
|
||||
if len(errs) > 0 {
|
||||
errMsg, _, _ := middleware.BuildValidationErrorForUser(form, ctx.Locale, errs)
|
||||
ctx.PrivateInternalErrorf("invalid request: %v", errMsg)
|
||||
}
|
||||
web.SetForm(ctx, form)
|
||||
}
|
||||
}
|
||||
|
||||
// setRealIP sets RemoteAddr from the trusted X-Real-IP header set by the internal API
|
||||
// client (see modules/private.NewInternalRequest); the internal API is gated by InternalToken.
|
||||
// It replaces chi's deprecated middleware.RealIP, which is unsafe on public-facing endpoints.
|
||||
func setRealIP(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
if ip := req.Header.Get("X-Real-IP"); net.ParseIP(ip) != nil {
|
||||
req.RemoteAddr = ip
|
||||
}
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
|
||||
// Routes registers all internal APIs routes to web application.
|
||||
// These APIs will be invoked by internal commands for example `gitea serv` and etc.
|
||||
func Routes() *web.Router {
|
||||
r := web.NewRouter()
|
||||
r.AfterRouting(context.PrivateContexter())
|
||||
r.AfterRouting(authInternal)
|
||||
// Log the real ip address of the request from SSH is really helpful for diagnosing sometimes.
|
||||
// Since internal API will be sent only from Gitea sub commands and it's under control (checked by InternalToken), we can trust the headers.
|
||||
r.AfterRouting(setRealIP)
|
||||
r.AfterRouting(common.AuditOrigin(audit_model.OriginSystem))
|
||||
|
||||
r.Get("/dummy", misc.DummyOK)
|
||||
r.Post("/ssh/authorized_keys", AuthorizedPublicKeyByContent)
|
||||
r.Post("/ssh/{id}/update/{repoid}", UpdatePublicKeyInRepo)
|
||||
r.Post("/ssh/log", bind(private.SSHLogOption{}), SSHLog)
|
||||
r.Post("/hook/pre-receive/{owner}/{repo}", RepoAssignment, bind(private.HookOptions{}), HookPreReceive)
|
||||
r.Post("/hook/post-receive/{owner}/{repo}", context.OverrideContext(), RepoAssignment, bind(private.HookOptions{}), HookPostReceive)
|
||||
r.Post("/hook/proc-receive/{owner}/{repo}", context.OverrideContext(), RepoAssignment, bind(private.HookOptions{}), HookProcReceive)
|
||||
r.Get("/serv/none/{keyid}", ServNoCommand)
|
||||
r.Get("/serv/command/{keyid}/{owner}/{repo}", ServCommand)
|
||||
r.Post("/manager/shutdown", Shutdown)
|
||||
r.Post("/manager/restart", Restart)
|
||||
r.Post("/manager/reload-templates", ReloadTemplates)
|
||||
r.Post("/manager/flush-queues", bind(private.FlushOptions{}), FlushQueues)
|
||||
r.Post("/manager/pause-logging", PauseLogging)
|
||||
r.Post("/manager/resume-logging", ResumeLogging)
|
||||
r.Post("/manager/release-and-reopen-logging", ReleaseReopenLogging)
|
||||
r.Post("/manager/set-log-sql", SetLogSQL)
|
||||
r.Get("/manager/processes", Processes)
|
||||
r.Post("/mail/send", SendEmail)
|
||||
r.Post("/restore_repo", RestoreRepo)
|
||||
r.Post("/actions/generate_actions_runner_token", GenerateActionsRunnerToken)
|
||||
|
||||
r.Group("/repo", func() {
|
||||
// FIXME: it is not right to use context.Contexter here because all routes here should use PrivateContext
|
||||
// Fortunately, the LFS handlers are able to handle requests without a complete web context
|
||||
common.AddOwnerRepoGitLFSRoutes(r, func(ctx *context.PrivateContext) {
|
||||
webContext := &context.Context{Base: ctx.Base} // see above, it shouldn't manually construct the web context
|
||||
ctx.SetContextValue(context.WebContextKey, webContext) // FIXME: this is not ideal but no other way at the moment
|
||||
})
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
60
routers/private/internal_repo.go
Normal file
60
routers/private/internal_repo.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"gitea.dev/models/perm/access"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/user"
|
||||
"gitea.dev/modules/git"
|
||||
gitea_context "gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// This file contains common functions relating to setting the Repository for the internal routes
|
||||
|
||||
// RepoAssignment assigns the repository and git repository to the private context
|
||||
func RepoAssignment(ctx *gitea_context.PrivateContext) {
|
||||
ownerName := ctx.PathParam("owner")
|
||||
repoName := ctx.PathParam("repo")
|
||||
|
||||
repo := loadRepository(ctx, ownerName, repoName)
|
||||
if ctx.Written() {
|
||||
// Error handled in loadRepository
|
||||
return
|
||||
}
|
||||
|
||||
gitRepo, err := git.RepositoryFromRequestContextOrOpen(ctx, repo)
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err)
|
||||
return
|
||||
}
|
||||
ctx.Repo = &gitea_context.Repository{Repository: repo, GitRepo: gitRepo}
|
||||
}
|
||||
|
||||
func loadRepository(ctx *gitea_context.PrivateContext, ownerName, repoName string) *repo_model.Repository {
|
||||
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName)
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err)
|
||||
return nil
|
||||
}
|
||||
if repo.OwnerName == "" {
|
||||
repo.OwnerName = ownerName
|
||||
}
|
||||
return repo
|
||||
}
|
||||
|
||||
func loadContextDoerPermission(ctx *gitea_context.PrivateContext, userID int64, extDoerData string) bool {
|
||||
doer, err := user.GetDoerPermissionUser(ctx, userID, extDoerData)
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("Failed to get user: %d, error: %v", userID, err)
|
||||
return false
|
||||
}
|
||||
ctx.Doer = doer
|
||||
ctx.Repo.Permission, err = access.GetDoerRepoPermission(ctx, ctx.Repo.Repository, doer)
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("Failed to get permission for user: %d, error: %v", userID, err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
57
routers/private/key.go
Normal file
57
routers/private/key.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
asymkey_model "gitea.dev/models/asymkey"
|
||||
deploykey_model "gitea.dev/models/deploykey"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// UpdatePublicKeyInRepo update public key and deploy key updates
|
||||
func UpdatePublicKeyInRepo(ctx *context.PrivateContext) {
|
||||
keyID := ctx.PathParamInt64("id")
|
||||
repoID := ctx.PathParamInt64("repoid")
|
||||
if err := asymkey_model.UpdatePublicKeyUpdated(ctx, keyID); err != nil {
|
||||
ctx.PrivateInternalErrorf("%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
deployKey, err := deploykey_model.GetDeployKeyByRepoPublicKey(ctx, repoID, keyID)
|
||||
if err != nil {
|
||||
if deploykey_model.IsErrDeployKeyNotExist(err) {
|
||||
ctx.PlainText(http.StatusOK, "success")
|
||||
return
|
||||
}
|
||||
ctx.PrivateInternalErrorf("%v", err)
|
||||
return
|
||||
}
|
||||
if err = deploykey_model.UpdateDeployKeyLastUsed(ctx, deployKey.ID); err != nil {
|
||||
ctx.PrivateInternalErrorf("%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.PlainText(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// AuthorizedPublicKeyByContent searches content as prefix (without comment part)
|
||||
// and returns public key found.
|
||||
func AuthorizedPublicKeyByContent(ctx *context.PrivateContext) {
|
||||
content := ctx.FormString("content")
|
||||
|
||||
publicKey, err := asymkey_model.SearchPublicKeyByContent(ctx, content)
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
authorizedString, err := asymkey_model.AuthorizedStringForKey(publicKey)
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("%v", err)
|
||||
return
|
||||
}
|
||||
ctx.PlainText(http.StatusOK, authorizedString)
|
||||
}
|
||||
77
routers/private/mail.go
Normal file
77
routers/private/mail.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
stdCtx "context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/private"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/mailer"
|
||||
sender_service "gitea.dev/services/mailer/sender"
|
||||
)
|
||||
|
||||
// SendEmail pushes messages to mail queue
|
||||
//
|
||||
// It doesn't wait before each message will be processed
|
||||
func SendEmail(ctx *context.PrivateContext) {
|
||||
if setting.MailService == nil {
|
||||
ctx.PrivateInternalErrorf("Mail service is not enabled.")
|
||||
return
|
||||
}
|
||||
|
||||
var mail private.Email
|
||||
rd := ctx.Req.Body
|
||||
defer rd.Close()
|
||||
|
||||
if err := json.NewDecoder(rd).Decode(&mail); err != nil {
|
||||
ctx.PrivateInternalErrorf("JSON Decode failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var emails []string
|
||||
if len(mail.To) > 0 {
|
||||
for _, uname := range mail.To {
|
||||
user, err := user_model.GetUserByName(ctx, uname)
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("Failed to get user information: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if user != nil && len(user.Email) > 0 {
|
||||
emails = append(emails, user.Email)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err := db.Iterate(ctx, nil, func(ctx stdCtx.Context, user *user_model.User) error {
|
||||
if len(user.Email) > 0 && user.IsActive {
|
||||
emails = append(emails, user.Email)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("Failed to find users: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sendEmail(ctx, mail.Subject, mail.Message, emails)
|
||||
}
|
||||
|
||||
func sendEmail(ctx *context.PrivateContext, subject, message string, to []string) {
|
||||
for _, email := range to {
|
||||
msg := sender_service.NewMessage(email, subject, message)
|
||||
mailer.SendAsync(msg)
|
||||
}
|
||||
|
||||
wasSent := strconv.Itoa(len(to))
|
||||
|
||||
ctx.PlainText(http.StatusOK, wasSent)
|
||||
}
|
||||
14
routers/private/main_test.go
Normal file
14
routers/private/main_test.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/unittest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m)
|
||||
}
|
||||
80
routers/private/manager.go
Normal file
80
routers/private/manager.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/graceful/releasereopen"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/private"
|
||||
"gitea.dev/modules/queue"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// ReloadTemplates reloads all the templates
|
||||
func ReloadTemplates(ctx *context.PrivateContext) {
|
||||
err := templates.ReloadAllTemplates()
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("Template error: %v", err)
|
||||
return
|
||||
}
|
||||
ctx.PlainText(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// FlushQueues flushes all the Queues
|
||||
func FlushQueues(ctx *context.PrivateContext) {
|
||||
opts := web.GetForm[*private.FlushOptions](ctx)
|
||||
if opts.NonBlocking {
|
||||
// Save the hammer ctx here - as a new one is created each time you call this.
|
||||
baseCtx := graceful.GetManager().HammerContext()
|
||||
go func() {
|
||||
err := queue.GetManager().FlushAll(baseCtx, opts.Timeout)
|
||||
if err != nil {
|
||||
log.Error("Flushing request timed-out with error: %v", err)
|
||||
}
|
||||
}()
|
||||
ctx.JSON(http.StatusAccepted, private.Response{
|
||||
UserMsg: "Flushing",
|
||||
})
|
||||
return
|
||||
}
|
||||
err := queue.GetManager().FlushAll(ctx, opts.Timeout)
|
||||
if err != nil {
|
||||
ctx.PrivateUserErrorf(http.StatusRequestTimeout, "%v", err)
|
||||
return
|
||||
}
|
||||
ctx.PlainText(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// PauseLogging pauses logging
|
||||
func PauseLogging(ctx *context.PrivateContext) {
|
||||
log.GetManager().PauseAll()
|
||||
ctx.PlainText(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// ResumeLogging resumes logging
|
||||
func ResumeLogging(ctx *context.PrivateContext) {
|
||||
log.GetManager().ResumeAll()
|
||||
ctx.PlainText(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// ReleaseReopenLogging releases and reopens logging files
|
||||
func ReleaseReopenLogging(ctx *context.PrivateContext) {
|
||||
if err := releasereopen.GetManager().ReleaseReopen(); err != nil {
|
||||
ctx.PrivateInternalErrorf("Error during release and reopen: %v", err)
|
||||
return
|
||||
}
|
||||
ctx.PlainText(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// SetLogSQL re-sets database SQL logging
|
||||
func SetLogSQL(ctx *context.PrivateContext) {
|
||||
db.SetLogSQL(ctx, ctx.FormBool("on"))
|
||||
ctx.PlainText(http.StatusOK, "success")
|
||||
}
|
||||
152
routers/private/manager_process.go
Normal file
152
routers/private/manager_process.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
process_module "gitea.dev/modules/process"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// Processes prints out the processes
|
||||
func Processes(ctx *context.PrivateContext) {
|
||||
pid := ctx.FormString("cancel-pid")
|
||||
if pid != "" {
|
||||
process_module.GetManager().Cancel(process_module.IDType(pid))
|
||||
runtime.Gosched()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
flat := ctx.FormBool("flat")
|
||||
noSystem := ctx.FormBool("no-system")
|
||||
stacktraces := ctx.FormBool("stacktraces")
|
||||
json := ctx.FormBool("json")
|
||||
|
||||
var processes []*process_module.Process
|
||||
goroutineCount := int64(0)
|
||||
var processCount int
|
||||
var err error
|
||||
if stacktraces {
|
||||
processes, processCount, goroutineCount, err = process_module.GetManager().ProcessStacktraces(flat, noSystem)
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("Failed to get stacktraces: %v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
processes, processCount = process_module.GetManager().Processes(flat, noSystem)
|
||||
}
|
||||
|
||||
if json {
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"TotalNumberOfGoroutines": goroutineCount,
|
||||
"TotalNumberOfProcesses": processCount,
|
||||
"Processes": processes,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8")
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
|
||||
if err := writeProcesses(ctx.Resp, processes, processCount, goroutineCount, "", flat); err != nil {
|
||||
if !ctx.Written() {
|
||||
ctx.PrivateInternalErrorf("Failed to get stacktraces: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func writeProcesses(out io.Writer, processes []*process_module.Process, processCount int, goroutineCount int64, indent string, flat bool) error {
|
||||
if goroutineCount > 0 {
|
||||
if _, err := fmt.Fprintf(out, "%sTotal Number of Goroutines: %d\n", indent, goroutineCount); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := fmt.Fprintf(out, "%sTotal Number of Processes: %d\n", indent, processCount); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(processes) > 0 {
|
||||
if err := writeProcess(out, processes[0], " ", flat); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(processes) > 1 {
|
||||
for _, process := range processes[1:] {
|
||||
if _, err := fmt.Fprintf(out, "%s | \n", indent); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeProcess(out, process, " ", flat); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeProcess(out io.Writer, process *process_module.Process, indent string, flat bool) error {
|
||||
sb := &bytes.Buffer{}
|
||||
if flat {
|
||||
if process.ParentPID != "" {
|
||||
_, _ = fmt.Fprintf(sb, "%s+ PID: %s\t\tType: %s\n", indent, process.PID, process.Type)
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(sb, "%s+ PID: %s:%s\tType: %s\n", indent, process.ParentPID, process.PID, process.Type)
|
||||
}
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(sb, "%s+ PID: %s\tType: %s\n", indent, process.PID, process.Type)
|
||||
}
|
||||
indent += "| "
|
||||
|
||||
_, _ = fmt.Fprintf(sb, "%sDescription: %s\n", indent, process.Description)
|
||||
_, _ = fmt.Fprintf(sb, "%sStart: %s\n", indent, process.Start)
|
||||
|
||||
if len(process.Stacks) > 0 {
|
||||
_, _ = fmt.Fprintf(sb, "%sGoroutines:\n", indent)
|
||||
for _, stack := range process.Stacks {
|
||||
indent := indent + " "
|
||||
_, _ = fmt.Fprintf(sb, "%s+ Description: %s", indent, stack.Description)
|
||||
if stack.Count > 1 {
|
||||
_, _ = fmt.Fprintf(sb, "* %d", stack.Count)
|
||||
}
|
||||
_, _ = fmt.Fprintf(sb, "\n")
|
||||
indent += "| "
|
||||
if len(stack.Labels) > 0 {
|
||||
_, _ = fmt.Fprintf(sb, "%sLabels: %q:%q", indent, stack.Labels[0].Name, stack.Labels[0].Value)
|
||||
|
||||
if len(stack.Labels) > 1 {
|
||||
for _, label := range stack.Labels[1:] {
|
||||
_, _ = fmt.Fprintf(sb, ", %q:%q", label.Name, label.Value)
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprintf(sb, "\n")
|
||||
}
|
||||
_, _ = fmt.Fprintf(sb, "%sStack:\n", indent)
|
||||
indent += " "
|
||||
for _, entry := range stack.Entry {
|
||||
_, _ = fmt.Fprintf(sb, "%s+ %s\n", indent, entry.Function)
|
||||
_, _ = fmt.Fprintf(sb, "%s| %s:%d\n", indent, entry.File, entry.Line)
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, err := out.Write(sb.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
sb.Reset()
|
||||
if len(process.Children) > 0 {
|
||||
if _, err := fmt.Fprintf(out, "%sChildren:\n", indent); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, child := range process.Children {
|
||||
if err := writeProcess(out, child, indent+" ", flat); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
25
routers/private/manager_unix.go
Normal file
25
routers/private/manager_unix.go
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// Restart causes the server to perform a graceful restart
|
||||
func Restart(ctx *context.PrivateContext) {
|
||||
graceful.GetManager().DoGracefulRestart()
|
||||
ctx.PlainText(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// Shutdown causes the server to perform a graceful shutdown
|
||||
func Shutdown(ctx *context.PrivateContext) {
|
||||
graceful.GetManager().DoGracefulShutdown()
|
||||
ctx.PlainText(http.StatusOK, "success")
|
||||
}
|
||||
24
routers/private/manager_windows.go
Normal file
24
routers/private/manager_windows.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build windows
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// Restart is not implemented for Windows based servers as they can't fork
|
||||
func Restart(ctx *context.PrivateContext) {
|
||||
ctx.PrivateUserErrorf(http.StatusNotImplemented, "windows servers cannot be gracefully restarted - shutdown and restart manually")
|
||||
}
|
||||
|
||||
// Shutdown causes the server to perform a graceful shutdown
|
||||
func Shutdown(ctx *context.PrivateContext) {
|
||||
graceful.GetManager().DoGracefulShutdown()
|
||||
ctx.PlainText(http.StatusOK, "success")
|
||||
}
|
||||
46
routers/private/restore_repo.go
Normal file
46
routers/private/restore_repo.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/modules/json"
|
||||
myCtx "gitea.dev/services/context"
|
||||
"gitea.dev/services/migrations"
|
||||
)
|
||||
|
||||
// RestoreRepo restore a repository from data
|
||||
func RestoreRepo(ctx *myCtx.PrivateContext) {
|
||||
bs, err := io.ReadAll(ctx.Req.Body)
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("%v", err)
|
||||
return
|
||||
}
|
||||
params := struct {
|
||||
RepoDir string
|
||||
OwnerName string
|
||||
RepoName string
|
||||
Units []string
|
||||
Validation bool
|
||||
}{}
|
||||
if err = json.Unmarshal(bs, ¶ms); err != nil {
|
||||
ctx.PrivateInternalErrorf("%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := migrations.RestoreRepository(
|
||||
ctx,
|
||||
params.RepoDir,
|
||||
params.OwnerName,
|
||||
params.RepoName,
|
||||
params.Units,
|
||||
params.Validation,
|
||||
); err != nil {
|
||||
ctx.PrivateInternalErrorf("%v", err)
|
||||
} else {
|
||||
ctx.PlainText(http.StatusOK, "success")
|
||||
}
|
||||
}
|
||||
308
routers/private/serv.go
Normal file
308
routers/private/serv.go
Normal file
@@ -0,0 +1,308 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
asymkey_model "gitea.dev/models/asymkey"
|
||||
deploykey_model "gitea.dev/models/deploykey"
|
||||
"gitea.dev/models/perm"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/git/gitrepo"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/private"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
wiki_service "gitea.dev/services/wiki"
|
||||
)
|
||||
|
||||
// ServNoCommand returns information about the provided keyid
|
||||
func ServNoCommand(ctx *context.PrivateContext) {
|
||||
keyID := ctx.PathParamInt64("keyid")
|
||||
if keyID <= 0 {
|
||||
ctx.PrivateUserErrorf(http.StatusBadRequest, "Bad key id: %d", keyID)
|
||||
return
|
||||
}
|
||||
results := private.KeyAndOwner{}
|
||||
|
||||
key, err := asymkey_model.GetPublicKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
if asymkey_model.IsErrKeyNotExist(err) {
|
||||
ctx.PrivateUserErrorf(http.StatusUnauthorized, "Cannot find key: %d", keyID)
|
||||
return
|
||||
}
|
||||
ctx.PrivateInternalErrorf("Unable to get public key: %d Error: %v", keyID, err)
|
||||
return
|
||||
}
|
||||
results.Key = key
|
||||
|
||||
if key.Type == asymkey_model.KeyTypeUser || key.Type == asymkey_model.KeyTypePrincipal {
|
||||
user, err := user_model.GetUserByID(ctx, key.OwnerID)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.PrivateUserErrorf(http.StatusUnauthorized, "Cannot find owner with id: %d for key: %d", key.OwnerID, keyID)
|
||||
return
|
||||
}
|
||||
ctx.PrivateInternalErrorf("Unable to get owner with id: %d for public key: %d Error: %v", key.OwnerID, keyID, err)
|
||||
return
|
||||
}
|
||||
if !user.IsActive || user.ProhibitLogin {
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "Your account is disabled.")
|
||||
return
|
||||
}
|
||||
results.Owner = user
|
||||
}
|
||||
ctx.JSON(http.StatusOK, &results)
|
||||
}
|
||||
|
||||
// ServCommand returns information about the provided keyid
|
||||
func ServCommand(ctx *context.PrivateContext) {
|
||||
keyID := ctx.PathParamInt64("keyid")
|
||||
reqOwnerName := ctx.PathParam("owner")
|
||||
reqRepoName := ctx.PathParam("repo")
|
||||
mode := perm.AccessMode(ctx.FormInt("mode"))
|
||||
verb := ctx.FormString("verb")
|
||||
|
||||
// Set the basic parts of the results to return
|
||||
results := private.ServCommandResults{
|
||||
OwnerName: reqOwnerName, // it might be changed if there is "renamed user redirection"
|
||||
RepoName: reqRepoName, // it might be changed if there is "renamed repo redirection", or the repo is a wiki
|
||||
PublicKeyID: keyID,
|
||||
}
|
||||
repoLogName := reqOwnerName + "/" + reqRepoName
|
||||
|
||||
if reqWikiRepoName, ok := strings.CutSuffix(reqRepoName, ".wiki"); ok {
|
||||
// in which case we need to look at the wiki, trim the ".wiki" suffix, only use the main repo name
|
||||
results.IsWiki = true
|
||||
results.RepoName = reqWikiRepoName
|
||||
}
|
||||
unitType := util.Iif(results.IsWiki, unit.TypeWiki, unit.TypeCode)
|
||||
modeString := mode.ToString()
|
||||
|
||||
owner, err := user_model.GetUserByName(ctx, results.OwnerName)
|
||||
if err != nil {
|
||||
if !user_model.IsErrUserNotExist(err) {
|
||||
ctx.PrivateInternalErrorf("Unable to get repository owner for %s, error: %v", repoLogName, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if there is a user redirect for the requested owner
|
||||
redirectedUserID, err := user_model.LookupUserRedirect(ctx, results.OwnerName)
|
||||
if err != nil {
|
||||
ctx.PrivateUserErrorf(http.StatusNotFound, "Cannot find repository %s", repoLogName)
|
||||
return
|
||||
}
|
||||
redirectUser, err := user_model.GetUserByID(ctx, redirectedUserID)
|
||||
if err != nil {
|
||||
ctx.PrivateUserErrorf(http.StatusNotFound, "Cannot find repository: %s", repoLogName)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("User %s has been redirected to %s", results.OwnerName, redirectUser.Name)
|
||||
results.OwnerName = redirectUser.Name
|
||||
owner = redirectUser
|
||||
}
|
||||
|
||||
// Now get the Repository and set the results section
|
||||
repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, results.RepoName)
|
||||
if err != nil {
|
||||
if !repo_model.IsErrRepoNotExist(err) {
|
||||
ctx.PrivateInternalErrorf("Unable to get repository %s, error: %v", repoLogName, err)
|
||||
return
|
||||
}
|
||||
|
||||
redirectedRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, results.RepoName)
|
||||
if err == nil {
|
||||
redirectedRepo, err := repo_model.GetRepositoryByID(ctx, redirectedRepoID)
|
||||
if err == nil {
|
||||
log.Info("Repository %s has been redirected to %s/%s", repoLogName, redirectedRepo.OwnerName, redirectedRepo.Name)
|
||||
repo = redirectedRepo
|
||||
if err = repo.LoadOwner(ctx); err != nil {
|
||||
ctx.PrivateInternalErrorf("Unable to repository owner %d", repo.OwnerID)
|
||||
return
|
||||
}
|
||||
owner = repo.Owner
|
||||
results.RepoName = redirectedRepo.Name
|
||||
results.OwnerName = redirectedRepo.OwnerName
|
||||
repoLogName = results.OwnerName + "/" + results.RepoName + util.Iif(results.IsWiki, ".wiki", "")
|
||||
} else {
|
||||
log.Warn("Repo %s has a redirect to repo with ID %d, but no repo with this ID could be found. Trying without redirect...", repoLogName, redirectedRepoID)
|
||||
}
|
||||
}
|
||||
|
||||
if repo == nil {
|
||||
if mode == perm.AccessModeRead {
|
||||
// User is fetching/cloning a non-existent repository
|
||||
ctx.PrivateUserErrorf(http.StatusNotFound, "Cannot find repository %s", repoLogName)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !owner.IsOrganization() && !owner.IsActive {
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "Repository cannot be accessed, the owner is inactive.")
|
||||
return
|
||||
}
|
||||
|
||||
if repo != nil {
|
||||
repo.Owner = owner
|
||||
repo.OwnerName = owner.Name
|
||||
results.RepoID = repo.ID
|
||||
|
||||
if repo.IsBeingCreated() {
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "Repository is being created, you could retry after it finished")
|
||||
return
|
||||
}
|
||||
|
||||
if repo.IsBroken() {
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "Repository is in a broken state")
|
||||
return
|
||||
}
|
||||
|
||||
// We can shortcut at this point if the repo is a mirror
|
||||
if mode > perm.AccessModeRead && repo.IsMirror {
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "Mirror Repository %s is read-only", repoLogName)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get the Public Key represented by the keyID
|
||||
key, err := asymkey_model.GetPublicKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
if asymkey_model.IsErrKeyNotExist(err) {
|
||||
ctx.PrivateUserErrorf(http.StatusNotFound, "Cannot find key: %d", keyID)
|
||||
return
|
||||
}
|
||||
ctx.PrivateInternalErrorf("Unable to get key: %d, error: %v", keyID, err)
|
||||
return
|
||||
}
|
||||
results.PublicKeyID = key.ID
|
||||
|
||||
var deployKey *deploykey_model.DeployKey
|
||||
var user *user_model.User
|
||||
if key.Type == asymkey_model.KeyTypeDeploy {
|
||||
if repo == nil {
|
||||
ctx.PrivateUserErrorf(http.StatusNotFound, "Cannot find repository %s", repoLogName)
|
||||
return
|
||||
}
|
||||
deployKey, err = deploykey_model.GetDeployKeyByRepoPublicKey(ctx, repo.ID, key.ID)
|
||||
if err != nil {
|
||||
if deploykey_model.IsErrDeployKeyNotExist(err) {
|
||||
ctx.PrivateUserErrorf(http.StatusNotFound, "Deploy-key %d:%s has no %q permission for %s.", key.ID, key.Name, modeString, repoLogName)
|
||||
return
|
||||
}
|
||||
ctx.PrivateInternalErrorf("Unable to get deploy for public (deploy) key %d for %s, error: %v", key.ID, repoLogName, err)
|
||||
return
|
||||
}
|
||||
user = user_model.NewDeployKeyUserWithKeyID(deployKey.ID)
|
||||
} else {
|
||||
// Get the user represented by the Key
|
||||
user, err = user_model.GetUserByID(ctx, key.OwnerID)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.PrivateUserErrorf(http.StatusUnauthorized, "Public key %d:%s owner %d does not exist.", key.ID, key.Name, key.OwnerID)
|
||||
return
|
||||
}
|
||||
ctx.PrivateInternalErrorf("Unable to get key owner %d for public key %d:%s, error: %v", key.OwnerID, key.ID, key.Name, err)
|
||||
return
|
||||
}
|
||||
if !user.IsActive || user.ProhibitLogin {
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "Your account is disabled.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
results.UserID = user.ID
|
||||
results.UserName = user.Name
|
||||
if !user.KeepEmailPrivate {
|
||||
results.UserEmail = user.Email
|
||||
}
|
||||
if user.ExtDoerData != nil {
|
||||
results.UserExtDoerData = user.ExtDoerData.EncodeToString()
|
||||
}
|
||||
|
||||
// Don't allow pushing if the repo is archived
|
||||
if repo != nil && mode > perm.AccessModeRead && repo.IsArchived {
|
||||
ctx.PrivateUserErrorf(http.StatusUnauthorized, "Repo %s is archived.", repoLogName)
|
||||
return
|
||||
}
|
||||
|
||||
// Permissions checking:
|
||||
if repo != nil &&
|
||||
(mode > perm.AccessModeRead ||
|
||||
repo.IsPrivate ||
|
||||
owner.Visibility.IsPrivate() ||
|
||||
user.IsRestricted ||
|
||||
setting.Service.RequireSignInViewStrict) {
|
||||
// Because of the special ref "refs/for" (AGit) we will need to delay write permission check,
|
||||
// AGit flow needs to write its own ref when the doer has "reader" permission (allowing to create PR).
|
||||
// The real permission check is done in HookPreReceive (routers/private/hook_pre_receive.go).
|
||||
// Here it should relax the permission check for "git push (git-receive-pack)", but not for others like LFS operations.
|
||||
if git.DefaultFeatures().SupportProcReceive && unitType == unit.TypeCode && verb == git.CmdVerbReceivePack {
|
||||
mode = perm.AccessModeRead
|
||||
}
|
||||
|
||||
userPerm, err := access_model.GetDoerRepoPermission(ctx, repo, user)
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("Unable to get permissions for %-v with key %d in %-v, error: %v", user, key.ID, repo, err)
|
||||
return
|
||||
}
|
||||
|
||||
userMode := userPerm.UnitAccessMode(unitType)
|
||||
if userMode < mode {
|
||||
ctx.PrivateUserErrorf(http.StatusUnauthorized, "User key %d:%s has no %q permission for %s", key.ID, key.Name, modeString, repoLogName)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if repo == nil {
|
||||
if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg {
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "Push to create is not enabled for organizations.")
|
||||
return
|
||||
}
|
||||
if !owner.IsOrganization() && !setting.Repository.EnablePushCreateUser {
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "Push to create is not enabled for users.")
|
||||
return
|
||||
}
|
||||
|
||||
repo, err = repo_service.PushCreateRepo(ctx, user, owner, results.RepoName)
|
||||
if err != nil {
|
||||
ctx.PrivateInternalErrorf("pushCreateRepo: %v", err)
|
||||
return
|
||||
}
|
||||
results.RepoID = repo.ID
|
||||
}
|
||||
|
||||
if results.IsWiki {
|
||||
// Ensure the wiki is enabled before we allow access to it
|
||||
if _, err := repo.GetUnit(ctx, unit.TypeWiki); err != nil {
|
||||
if repo_model.IsErrUnitTypeNotExist(err) {
|
||||
ctx.PrivateUserErrorf(http.StatusForbidden, "repository wiki is disabled")
|
||||
return
|
||||
}
|
||||
ctx.PrivateInternalErrorf("Failed to get the wiki unit in %-v, error: %v", repo, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Finally if we're trying to touch the wiki we should init it
|
||||
if err = wiki_service.InitWiki(ctx, repo); err != nil {
|
||||
ctx.PrivateInternalErrorf("Failed to initialize the wiki in %-v, error: %v", repo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
gitRepo := util.Iif(results.IsWiki, repo.WikiStorageRepo(), repo.CodeStorageRepo())
|
||||
results.RepoStoragePath = gitrepo.RepoLocalPath(gitRepo)
|
||||
log.Debug("Serv Results: %+v", results)
|
||||
ctx.JSON(http.StatusOK, results)
|
||||
// We will update the keys in a different call.
|
||||
}
|
||||
33
routers/private/ssh_log.go
Normal file
33
routers/private/ssh_log.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/private"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// SSHLog hook to response ssh log
|
||||
func SSHLog(ctx *context.PrivateContext) {
|
||||
if !setting.Log.EnableSSHLog {
|
||||
ctx.Status(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
opts := web.GetForm[*private.SSHLogOption](ctx)
|
||||
|
||||
if opts.IsError {
|
||||
log.Error("ssh: %v", opts.Message)
|
||||
ctx.Status(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("ssh: %v", opts.Message)
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
1
routers/private/tests/repos/repo1_hook_verification/HEAD
Normal file
1
routers/private/tests/repos/repo1_hook_verification/HEAD
Normal file
@@ -0,0 +1 @@
|
||||
ref: refs/heads/main
|
||||
@@ -0,0 +1,6 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = false
|
||||
bare = true
|
||||
symlinks = false
|
||||
ignorecase = true
|
||||
@@ -0,0 +1 @@
|
||||
d766f2917716d45be24bfa968b8409544941be32 refs/heads/main
|
||||
@@ -0,0 +1 @@
|
||||
0000000000000000000000000000000000000000 d766f2917716d45be24bfa968b8409544941be32 Gitea <gitea@fake.local> 1693148474 +0800 push
|
||||
@@ -0,0 +1 @@
|
||||
0000000000000000000000000000000000000000 d766f2917716d45be24bfa968b8409544941be32 Gitea <gitea@fake.local> 1693148474 +0800 push
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
x•ŽK
|
||||
1]çÙÒéüAÄS¸ï$Í"32ooð®ŠWð òÞ{›!žæ`–˜JC%¡.˜$Ár]sѱe$ïmòâMƒ·)£÷±(O`ªbtlÐE[:;4–àHÐ1_û<5F>”rayýáþl“é’÷~“ÊEL@cå€Xv…Mþã":µMÛƒG«_}À?Ý
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
x•<>1
|
||||
!ES{ŠéAwGGa 9EúQgW·Èí#¹AªÞû©ÕZ§/£‹€³Œ–p±ì(¤(<28>ó®óBhÈÛ¼&ᙟãÝ:pLY`ûÍãU†ð-µzŸÁ°ô†\µ×ZM:<3A>†ü¡¨Êå€óxJ/ûG}:µ3
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
x•ŽA
|
||||
Â0E]ç³$™L“ˆx•L2µ]´•<C2B4>
|
||||
ÞÞê
|
||||
@@ -0,0 +1,3 @@
|
||||
x•’Ë®«FE3æ+zn%44æ!%Qxƒ<78>Û€s˜AÓ`8Øæëã{£Ì2IM¶j•ª´¥Údèûf²Ìý2<C3BD>”‚"‡$§e‰¶
|
||||
-(â Ä!´ÝJ"åaŲ@•BaîùHo3 ŸVØòå<$<24>/)å$JøJD’B¡•H¤§˜ü{¾#ÈRRðûOù«nfšÿF†þOÀ‰
|
||||
âq[°<>2„̇~ŒÍô¬Ô÷zjjðë<C3B0>ÒLÛÅÀ·}prm¬Fqhþä`@Ø«¦ªš®ª¥Õ˜fî?3Ç[7г…ê¨Ð) ^™þuÿÖ¿,µ<>Æl7©zÝÿr|&«Ou4<75>Ø9Ó:µÎQjôû·êÕ1x±õå6ÍQ‡÷ƒÀ%Áåtû‰sò¸íV‰|( V¿<56>,aL,ù«G~²Ç<16>¹‹<C2B9>‹r¥ùûî@·`·Àþ$[! XËŠep©Œæ[8 oýä(›« k£Z´Î³yóeÐ¹ÙÆÄ«Y²¿kÖd€¯6•3¾;3ÜÔ RÔiÞ‹dYÓDk91V]/Cê#º¾&ÿêpo´Fáb¯‹¶}§¹ô¦òuW&]+m xaqdÜIõX¯þ3
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
x•ŽA
|
||||
Â0E]ç³$™L“ˆx•L2µ]´•<C2B4>
|
||||
ÞÞê
|
||||
@@ -0,0 +1 @@
|
||||
d766f2917716d45be24bfa968b8409544941be32
|
||||
@@ -0,0 +1,127 @@
|
||||
# GPG key for abcde@gitea.com
|
||||
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
4akc
|
||||
=OYIo
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
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
|
||||
ZZtqph2TItCeV04HoaKHHc254akc
|
||||
=PPG4
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
||||
Reference in New Issue
Block a user