初始提交: Gitea 项目代码
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"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/log"
|
||||
"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 {
|
||||
log.Error("JSON Decode failed: %v", err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
owner, repo, err := parseScope(ctx, genRequest.Scope)
|
||||
if err != nil {
|
||||
log.Error("parseScope failed: %v", err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
errMsg := fmt.Sprintf("error while creating runner token: %v", err)
|
||||
log.Error("NewRunnerToken failed: %v", errMsg)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: errMsg,
|
||||
})
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
errMsg := fmt.Sprintf("could not get unactivated runner token: %v", err)
|
||||
log.Error("GetLatestRunnerToken failed: %v", errMsg)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: errMsg,
|
||||
})
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"gitea.dev/modules/private"
|
||||
gitea_context "gitea.dev/services/context"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
// SetDefaultBranch updates the default branch
|
||||
func SetDefaultBranch(ctx *gitea_context.PrivateContext) {
|
||||
ownerName := ctx.PathParam("owner")
|
||||
repoName := ctx.PathParam("repo")
|
||||
branch := ctx.PathParam("branch")
|
||||
|
||||
ctx.Repo.Repository.DefaultBranch = branch
|
||||
if err := gitrepo.SetDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := repo_model.UpdateDefaultBranch(ctx, ctx.Repo.Repository); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := repo_service.AddRepoToLicenseUpdaterQueue(&repo_service.LicenseUpdaterOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
}); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.PlainText(http.StatusOK, "success")
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
git_model "gitea.dev/models/git"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/cache"
|
||||
"gitea.dev/modules/cachegroup"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"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_context "gitea.dev/services/context"
|
||||
pull_service "gitea.dev/services/pull"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
// HookPostReceive updates services and users
|
||||
func HookPostReceive(ctx *gitea_context.PrivateContext) {
|
||||
opts := web.GetForm(ctx).(*private.HookOptions)
|
||||
|
||||
// We don't rely on RepoAssignment here because:
|
||||
// a) we don't need the git repo in this function
|
||||
// OUT OF DATE: we do need the git repo to sync the branch to the db now.
|
||||
// b) our update function will likely change the repository in the db so we will need to refresh it
|
||||
// c) we don't always need the repo
|
||||
|
||||
ownerName := ctx.PathParam("owner")
|
||||
repoName := ctx.PathParam("repo")
|
||||
|
||||
// defer getting the repository at this point - as we should only retrieve it if we're going to call update
|
||||
var (
|
||||
repo *repo_model.Repository
|
||||
gitRepo *git.Repository
|
||||
)
|
||||
defer gitRepo.Close() // it's safe to call Close on a nil pointer
|
||||
|
||||
updates := make([]*repo_module.PushUpdateOptions, 0, len(opts.OldCommitIDs))
|
||||
wasEmpty := false
|
||||
|
||||
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() {
|
||||
if repo == nil {
|
||||
repo = loadRepository(ctx, ownerName, repoName)
|
||||
if ctx.Written() {
|
||||
// Error handled in loadRepository
|
||||
return
|
||||
}
|
||||
wasEmpty = repo.IsEmpty
|
||||
}
|
||||
|
||||
option := &repo_module.PushUpdateOptions{
|
||||
RefFullName: refFullName,
|
||||
OldCommitID: opts.OldCommitIDs[i],
|
||||
NewCommitID: opts.NewCommitIDs[i],
|
||||
PusherID: opts.UserID,
|
||||
PusherName: opts.UserName,
|
||||
RepoUserName: ownerName,
|
||||
RepoName: repoName,
|
||||
}
|
||||
updates = append(updates, option)
|
||||
if repo.IsEmpty && (refFullName.BranchName() == "master" || refFullName.BranchName() == "main") {
|
||||
// put the master/main branch first
|
||||
// FIXME: It doesn't always work, since the master/main branch may not be the first batch of updates.
|
||||
// If the user pushes many branches at once, the Git hook will call the internal API in batches, rather than all at once.
|
||||
// See https://github.com/go-gitea/gitea/blob/cb52b17f92e2d2293f7c003649743464492bca48/cmd/hook.go#L27
|
||||
// If the user executes `git push origin --all` and pushes more than 30 branches, the master/main may not be the default branch.
|
||||
copy(updates[1:], updates)
|
||||
updates[0] = option
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if repo != nil && len(updates) > 0 {
|
||||
branchesToSync := make([]*repo_module.PushUpdateOptions, 0, len(updates))
|
||||
for _, update := range updates {
|
||||
if !update.RefFullName.IsBranch() {
|
||||
continue
|
||||
}
|
||||
if repo == nil {
|
||||
repo = loadRepository(ctx, ownerName, repoName)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
wasEmpty = repo.IsEmpty
|
||||
}
|
||||
|
||||
if update.IsDelRef() {
|
||||
if err := git_model.MarkBranchAsDeleted(ctx, repo.ID, update.RefFullName.BranchName(), update.PusherID); err != nil {
|
||||
log.Error("Failed to mark branch as deleted: %s/%s Error: %v", ownerName, repoName, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
|
||||
Err: fmt.Sprintf("Failed to mark branch as deleted: %s/%s Error: %v", ownerName, repoName, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
} 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 {
|
||||
var err error
|
||||
gitRepo, err = gitrepo.OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
|
||||
Err: fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
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, branchNames, commitIDs, gitRepo.GetCommit); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
|
||||
Err: fmt.Sprintf("Failed to sync branch to DB in repository: %s/%s Error: %v", ownerName, repoName, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := repo_service.PushUpdates(updates); err != nil {
|
||||
log.Error("Failed to Update: %s/%s Total Updates: %d", ownerName, repoName, len(updates))
|
||||
for i, update := range updates {
|
||||
log.Error("Failed to Update: %s/%s Update: %d/%d: Branch: %s", ownerName, repoName, i, len(updates), update.RefFullName.BranchName())
|
||||
}
|
||||
log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
|
||||
|
||||
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
|
||||
Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handle pull request merging, a pull request action should push at least 1 commit
|
||||
if opts.PushTrigger == repo_module.PushTriggerPRMergeToBase {
|
||||
handlePullRequestMerging(ctx, opts, ownerName, repoName, updates)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
isPrivate := opts.GitPushOptions.Bool(private.GitPushOptionRepoPrivate)
|
||||
isTemplate := opts.GitPushOptions.Bool(private.GitPushOptionRepoTemplate)
|
||||
// Handle Push Options
|
||||
if isPrivate.Has() || isTemplate.Has() {
|
||||
// load the repository
|
||||
if repo == nil {
|
||||
repo = loadRepository(ctx, ownerName, repoName)
|
||||
if ctx.Written() {
|
||||
// Error handled in loadRepository
|
||||
return
|
||||
}
|
||||
wasEmpty = repo.IsEmpty
|
||||
}
|
||||
|
||||
pusher, err := loadContextCacheUser(ctx, opts.UserID)
|
||||
if err != nil {
|
||||
log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
|
||||
Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
perm, err := access_model.GetDoerRepoPermission(ctx, repo, pusher)
|
||||
if err != nil {
|
||||
log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
|
||||
Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
if !perm.IsOwner() && !perm.IsAdmin() {
|
||||
ctx.JSON(http.StatusNotFound, private.HookPostReceiveResult{
|
||||
Err: "Permissions denied",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// FIXME: these options are not quite right, for example: changing visibility should do more works than just setting the is_private flag
|
||||
// These options should only be used for "push-to-create"
|
||||
if isPrivate.Has() && repo.IsPrivate != isPrivate.Value() {
|
||||
// TODO: it needs to do more work
|
||||
repo.IsPrivate = isPrivate.Value()
|
||||
if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to change visibility"})
|
||||
}
|
||||
}
|
||||
if isTemplate.Has() && repo.IsTemplate != isTemplate.Value() {
|
||||
repo.IsTemplate = isTemplate.Value()
|
||||
if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_template"); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to change template status"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results := make([]private.HookPostReceiveBranchResult, 0, len(opts.OldCommitIDs))
|
||||
|
||||
// We have to reload the repo in case its state is changed above
|
||||
repo = nil
|
||||
var baseRepo *repo_model.Repository
|
||||
|
||||
// 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() {
|
||||
// First ensure we have the repository loaded, we're allowed pulls requests and we can get the base repo
|
||||
if repo == nil {
|
||||
repo = loadRepository(ctx, ownerName, repoName)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
baseRepo = repo
|
||||
|
||||
if repo.IsFork {
|
||||
if err := repo.GetBaseRepo(ctx); err != nil {
|
||||
log.Error("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
|
||||
Err: fmt.Sprintf("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err),
|
||||
RepoWasEmpty: wasEmpty,
|
||||
})
|
||||
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{
|
||||
RepoWasEmpty: wasEmpty,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
branch := refFullName.BranchName()
|
||||
|
||||
if branch == baseRepo.DefaultBranch {
|
||||
if err := repo_service.AddRepoToLicenseUpdaterQueue(&repo_service.LicenseUpdaterOptions{
|
||||
RepoID: repo.ID,
|
||||
}); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{Err: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// If our branch is the default branch of an unforked repo - there's no PR to create or refer to
|
||||
if !repo.IsFork {
|
||||
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 && !issues_model.IsErrPullRequestNotExist(err) {
|
||||
log.Error("Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
|
||||
Err: fmt.Sprintf(
|
||||
"Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err),
|
||||
RepoWasEmpty: wasEmpty,
|
||||
})
|
||||
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,
|
||||
RepoWasEmpty: wasEmpty,
|
||||
})
|
||||
}
|
||||
|
||||
func loadContextCacheUser(ctx context.Context, id int64) (*user_model.User, error) {
|
||||
return cache.GetWithContextCache(ctx, cachegroup.User, id, user_model.GetUserByID)
|
||||
}
|
||||
|
||||
// handlePullRequestMerging handle pull request merging, a pull request action should push at least 1 commit
|
||||
func handlePullRequestMerging(ctx *gitea_context.PrivateContext, opts *private.HookOptions, ownerName, repoName string, updates []*repo_module.PushUpdateOptions) {
|
||||
if len(updates) == 0 {
|
||||
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
|
||||
Err: fmt.Sprintf("Pushing a merged PR (pr:%d) no commits pushed ", opts.PullRequestID),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
pr, err := issues_model.GetPullRequestByID(ctx, opts.PullRequestID)
|
||||
if err != nil {
|
||||
log.Error("GetPullRequestByID[%d]: %v", opts.PullRequestID, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "GetPullRequestByID failed"})
|
||||
return
|
||||
}
|
||||
|
||||
pusher, err := loadContextCacheUser(ctx, opts.UserID)
|
||||
if err != nil {
|
||||
log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Load pusher user failed"})
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
if _, err := pull_service.SetMerged(ctx, pr, updates[len(updates)-1].NewCommitID, timeutil.TimeStampNow(), pusher, pr.Status); err != nil {
|
||||
log.Error("Failed to update PR to merged: %v", err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to update PR to merged"})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// 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})
|
||||
|
||||
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, "/")
|
||||
handlePullRequestMerging(ctx, &private.HookOptions{
|
||||
PullRequestID: pr.ID,
|
||||
UserID: 2,
|
||||
}, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, []*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})
|
||||
}
|
||||
@@ -0,0 +1,553 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
asymkey_model "gitea.dev/models/asymkey"
|
||||
git_model "gitea.dev/models/git"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
perm_model "gitea.dev/models/perm"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"gitea.dev/modules/log"
|
||||
"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
|
||||
|
||||
// loadedPusher indicates that where the following information are loaded
|
||||
loadedPusher bool
|
||||
user *user_model.User // it's the org user if a DeployKey is used
|
||||
userPerm access_model.Permission
|
||||
deployKeyAccessMode perm_model.AccessMode
|
||||
|
||||
canCreatePullRequest bool
|
||||
checkedCanCreatePullRequest bool
|
||||
|
||||
canWriteCode bool
|
||||
checkedCanWriteCode bool
|
||||
|
||||
protectedTags []*git_model.ProtectedTag
|
||||
gotProtectedTags bool
|
||||
|
||||
env []string
|
||||
|
||||
opts *private.HookOptions
|
||||
|
||||
branchName string
|
||||
}
|
||||
|
||||
// CanWriteCode returns true if pusher can write code
|
||||
func (ctx *preReceiveContext) CanWriteCode() bool {
|
||||
if !ctx.checkedCanWriteCode {
|
||||
if !ctx.loadPusherAndPermission() {
|
||||
return false
|
||||
}
|
||||
ctx.canWriteCode = issues_model.CanMaintainerWriteToBranch(ctx, ctx.userPerm, ctx.branchName, ctx.user) || ctx.deployKeyAccessMode >= perm_model.AccessModeWrite
|
||||
ctx.checkedCanWriteCode = true
|
||||
}
|
||||
return ctx.canWriteCode
|
||||
}
|
||||
|
||||
// AssertCanWriteCode returns true if pusher can write code
|
||||
func (ctx *preReceiveContext) AssertCanWriteCode() bool {
|
||||
if !ctx.CanWriteCode() {
|
||||
if ctx.Written() {
|
||||
return false
|
||||
}
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: "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.checkedCanCreatePullRequest {
|
||||
if !ctx.loadPusherAndPermission() {
|
||||
return false
|
||||
}
|
||||
ctx.canCreatePullRequest = ctx.userPerm.CanRead(unit.TypePullRequests)
|
||||
ctx.checkedCanCreatePullRequest = true
|
||||
}
|
||||
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.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: "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(ctx).(*private.HookOptions)
|
||||
|
||||
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.AssertCanWriteCode()
|
||||
}
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.PlainText(http.StatusOK, "ok")
|
||||
}
|
||||
|
||||
func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, refFullName git.RefName) {
|
||||
branchName := refFullName.BranchName()
|
||||
ctx.branchName = branchName
|
||||
|
||||
if !ctx.AssertCanWriteCode() {
|
||||
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() {
|
||||
log.Warn("Forbidden: Branch: %s is the default branch in %-v and cannot be deleted", branchName, repo)
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: fmt.Sprintf("branch %s is the default branch and cannot be deleted", branchName),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
protectBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branchName)
|
||||
if err != nil {
|
||||
log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err.Error(),
|
||||
})
|
||||
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() {
|
||||
log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo)
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: fmt.Sprintf("branch %s is protected from deletion", branchName),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
isForcePush := false
|
||||
|
||||
// 2. Disallow force pushes to protected branches
|
||||
if oldCommitID != objectFormat.EmptyObjectID().String() {
|
||||
output, _, err := gitrepo.RunCmdString(ctx,
|
||||
repo,
|
||||
gitcmd.NewCommand("rev-list", "--max-count=1").
|
||||
AddDynamicArguments(oldCommitID, "^"+newCommitID).
|
||||
WithEnv(ctx.env),
|
||||
)
|
||||
if err != nil {
|
||||
log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Fail to detect force push: %v", err),
|
||||
})
|
||||
return
|
||||
} else if len(output) > 0 {
|
||||
if protectBranch.CanForcePush {
|
||||
isForcePush = true
|
||||
} else {
|
||||
log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo)
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: fmt.Sprintf("branch %s is protected from force push", branchName),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Enforce require signed commits
|
||||
if protectBranch.RequireSignedCommits {
|
||||
err := verifyCommits(oldCommitID, newCommitID, gitRepo, ctx.env)
|
||||
if err != nil {
|
||||
if !isErrUnverifiedCommit(err) {
|
||||
log.Error("Unable to check commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to check commits from %s to %s: %v", oldCommitID, newCommitID, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
unverifiedCommit := err.(*errUnverifiedCommit).sha
|
||||
log.Warn("Forbidden: Branch: %s in %-v is protected from unverified commit %s", branchName, repo, unverifiedCommit)
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: fmt.Sprintf("branch %s is protected from unverified commit %s", branchName, unverifiedCommit),
|
||||
})
|
||||
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(gitRepo, branchName, oldCommitID, newCommitID, globs, 1, ctx.env)
|
||||
if err != nil {
|
||||
if !pull_service.IsErrFilePathProtected(err) {
|
||||
log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
changedProtectedfiles = true
|
||||
protectedFilePath = err.(pull_service.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.DeployKeyID != 0 {
|
||||
// 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 {
|
||||
user, err := user_model.GetUserByID(ctx, ctx.opts.UserID)
|
||||
if err != nil {
|
||||
log.Error("Unable to GetUserByID for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to GetUserByID for commits from %s to %s: %v", oldCommitID, newCommitID, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
if isForcePush {
|
||||
canPush = !changedProtectedfiles && protectBranch.CanUserForcePush(ctx, user)
|
||||
} else {
|
||||
canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, user)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath)
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: fmt.Sprintf("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(gitRepo, branchName, oldCommitID, newCommitID, globs, ctx.env)
|
||||
if err != nil {
|
||||
log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("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 {
|
||||
log.Warn("Forbidden: User %d is not allowed to force-push to protected branch: %s in %-v", ctx.opts.UserID, branchName, repo)
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: "Not allowed to force-push to protected branch " + branchName,
|
||||
})
|
||||
return
|
||||
}
|
||||
log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v", ctx.opts.UserID, branchName, repo)
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: "Not allowed to push to protected branch " + 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 {
|
||||
log.Error("Unable to get PullRequest %d Error: %v", ctx.opts.PullRequestID, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to get PullRequest %d Error: %v", ctx.opts.PullRequestID, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// although we should have called `loadPusherAndPermission` before, here we call it explicitly again because we need to access ctx.user below
|
||||
if !ctx.loadPusherAndPermission() {
|
||||
// if error occurs, loadPusherAndPermission had written the error response
|
||||
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.userPerm, ctx.user)
|
||||
if err != nil {
|
||||
log.Error("Error calculating if allowed to merge: %v", err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Error calculating if allowed to merge: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !allowedMerge {
|
||||
log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v and is not allowed to merge pr #%d", ctx.opts.UserID, branchName, repo, pr.Index)
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: "Not allowed to push to protected branch " + branchName,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// If we can bypass branch protection we can ignore status checks, reviews and protected files
|
||||
if git_model.CanBypassBranchProtection(ctx, protectBranch, ctx.user, ctx.userPerm.IsAdmin()) {
|
||||
return
|
||||
}
|
||||
|
||||
// Now if we're not an admin - we can't overwrite protected files so fail now
|
||||
if changedProtectedfiles {
|
||||
log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath)
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: fmt.Sprintf("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) {
|
||||
log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", ctx.opts.UserID, branchName, repo, pr.Index, err.Error())
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: fmt.Sprintf("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
|
||||
}
|
||||
log.Error("Unable to check if mergeable: protected branch %s in %-v and pr #%d. Error: %v", ctx.opts.UserID, branchName, repo, pr.Index, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to get status of pull request %d. Error: %v", ctx.opts.PullRequestID, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func preReceiveTag(ctx *preReceiveContext, refFullName git.RefName) {
|
||||
if !ctx.AssertCanWriteCode() {
|
||||
return
|
||||
}
|
||||
|
||||
tagName := refFullName.TagName()
|
||||
|
||||
if !ctx.gotProtectedTags {
|
||||
var err error
|
||||
ctx.protectedTags, err = git_model.GetProtectedTags(ctx, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
log.Error("Unable to get protected tags for %-v Error: %v", ctx.Repo.Repository, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
ctx.gotProtectedTags = true
|
||||
}
|
||||
|
||||
isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, ctx.protectedTags, tagName, ctx.opts.UserID)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if !isAllowed {
|
||||
log.Warn("Forbidden: Tag %s in %-v is protected", tagName, ctx.Repo.Repository)
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: fmt.Sprintf("Tag %s is protected", tagName),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func preReceiveFor(ctx *preReceiveContext, refFullName git.RefName) {
|
||||
if !ctx.AssertCreatePullRequest() {
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Repo.Repository.IsEmpty {
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: "Can't create pull request for an empty repository.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.opts.IsWiki {
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: "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.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: fmt.Sprintf("Unexpected ref: %s", refFullName),
|
||||
})
|
||||
} else {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// loadPusherAndPermission returns false if an error occurs, and it writes the error response
|
||||
func (ctx *preReceiveContext) loadPusherAndPermission() bool {
|
||||
if ctx.loadedPusher {
|
||||
return true
|
||||
}
|
||||
|
||||
if ctx.opts.UserID == user_model.ActionsUserID {
|
||||
taskID := ctx.opts.ActionsTaskID
|
||||
ctx.user = user_model.NewActionsUserWithTaskID(taskID)
|
||||
if taskID == 0 {
|
||||
log.Error("HookPreReceive: ActionsUser with task ID 0")
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: "ActionsUser with task ID 0",
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
userPerm, err := access_model.GetActionsUserRepoPermission(ctx, ctx.Repo.Repository, ctx.user, taskID)
|
||||
if err != nil {
|
||||
log.Error("Unable to get Actions user repo permission for task %d Error: %v", taskID, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to get Actions user repo permission for task %d Error: %v", taskID, err),
|
||||
})
|
||||
return false
|
||||
}
|
||||
ctx.userPerm = userPerm
|
||||
} else {
|
||||
user, err := user_model.GetUserByID(ctx, ctx.opts.UserID)
|
||||
if err != nil {
|
||||
log.Error("Unable to get User id %d Error: %v", ctx.opts.UserID, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to get User id %d Error: %v", ctx.opts.UserID, err),
|
||||
})
|
||||
return false
|
||||
}
|
||||
ctx.user = user
|
||||
userPerm, err := access_model.GetDoerRepoPermission(ctx, ctx.Repo.Repository, user)
|
||||
if err != nil {
|
||||
log.Error("Unable to get Repo permission of repo %s/%s of User %s: %v", ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name, user.Name, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to get Repo permission of repo %s/%s of User %s: %v", ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name, user.Name, err),
|
||||
})
|
||||
return false
|
||||
}
|
||||
ctx.userPerm = userPerm
|
||||
}
|
||||
|
||||
if ctx.opts.DeployKeyID != 0 {
|
||||
deployKey, err := asymkey_model.GetDeployKeyByID(ctx, ctx.opts.DeployKeyID)
|
||||
if err != nil {
|
||||
log.Error("Unable to get DeployKey id %d Error: %v", ctx.opts.DeployKeyID, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to get DeployKey id %d Error: %v", ctx.opts.DeployKeyID, err),
|
||||
})
|
||||
return false
|
||||
}
|
||||
ctx.deployKeyAccessMode = deployKey.Mode
|
||||
}
|
||||
|
||||
ctx.loadedPusher = true
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// 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/log"
|
||||
"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(ctx).(*private.HookOptions)
|
||||
if !git.DefaultFeatures().SupportProcReceive {
|
||||
ctx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
results, err := agit.ProcReceive(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, opts)
|
||||
if err != nil {
|
||||
if errors.Is(err, issues_model.ErrMustCollaborator) {
|
||||
ctx.JSON(http.StatusUnauthorized, private.Response{
|
||||
Err: err.Error(), UserMsg: "You must be a collaborator to create pull request.",
|
||||
})
|
||||
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.JSON(http.StatusUnauthorized, private.Response{
|
||||
Err: err.Error(), UserMsg: "Cannot create pull request because you are blocked by the repository owner.",
|
||||
})
|
||||
} else {
|
||||
log.Error("agit.ProcReceive failed: %v", err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, private.HookProcReceiveResult{
|
||||
Results: results,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"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(oldCommitID, newCommitID string, repo *git.Repository, env []string) error {
|
||||
var command *gitcmd.Command
|
||||
objectFormat, _ := repo.GetObjectFormat()
|
||||
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).
|
||||
WithDir(repo.Path).
|
||||
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
||||
err := readAndVerifyCommitsFromShaReader(stdoutReader, repo, env)
|
||||
return ctx.CancelPipeline(err)
|
||||
}).
|
||||
Run(repo.Ctx)
|
||||
if err != nil && !isErrUnverifiedCommit(err) {
|
||||
log.Error("Unable to check commits from %s to %s in %s: %v", oldCommitID, newCommitID, repo.Path, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func readAndVerifyCommitsFromShaReader(input io.ReadCloser, repo *git.Repository, env []string) error {
|
||||
scanner := bufio.NewScanner(input)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
err := readAndVerifyCommit(line, repo, env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
func readAndVerifyCommit(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).
|
||||
WithDir(repo.Path).
|
||||
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
||||
commit, err := git.CommitFromReader(repo, commitID, stdoutReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
verification := asymkey_service.ParseCommitWithSignature(ctx, commit)
|
||||
if !verification.Verified {
|
||||
return ctx.CancelPipeline(&errUnverifiedCommit{commit.ID.String()})
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
Run(repo.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
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
var testReposDir = "tests/repos/"
|
||||
|
||||
func TestVerifyCommits(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
|
||||
gitRepo, err := git.OpenRepository(t.Context(), testReposDir+"repo1_hook_verification")
|
||||
if err != nil {
|
||||
defer gitRepo.Close()
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
objectFormat, err := gitRepo.GetObjectFormat()
|
||||
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(tc.base, tc.head, gitRepo, nil)
|
||||
if tc.verified {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// 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/http"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/private"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/common"
|
||||
"gitea.dev/routers/web/misc"
|
||||
"gitea.dev/services/context"
|
||||
|
||||
"gitea.com/go-chi/binding"
|
||||
chi_middleware "github.com/go-chi/chi/v5/middleware"
|
||||
)
|
||||
|
||||
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](_ T) any {
|
||||
return func(ctx *context.PrivateContext) {
|
||||
theObj := new(T) // create a new form obj for every request but not use obj directly
|
||||
binding.Bind(ctx.Req, theObj)
|
||||
web.SetForm(ctx, theObj)
|
||||
}
|
||||
}
|
||||
|
||||
// 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(chi_middleware.RealIP)
|
||||
|
||||
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(), bind(private.HookOptions{}), HookPostReceive)
|
||||
r.Post("/hook/proc-receive/{owner}/{repo}", context.OverrideContext(), RepoAssignment, bind(private.HookOptions{}), HookProcReceive)
|
||||
r.Post("/hook/set-default-branch/{owner}/{repo}/{branch}", RepoAssignment, SetDefaultBranch)
|
||||
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.Post("/manager/add-logger", bind(private.LoggerOptions{}), AddLogger)
|
||||
r.Post("/manager/remove-logger/{logger}/{writer}", RemoveLogger)
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/private"
|
||||
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 := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo)
|
||||
if err != nil {
|
||||
log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("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 {
|
||||
log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
if repo.OwnerName == "" {
|
||||
repo.OwnerName = ownerName
|
||||
}
|
||||
return repo
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
asymkey_model "gitea.dev/models/asymkey"
|
||||
"gitea.dev/modules/private"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"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.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
deployKey, err := asymkey_model.GetDeployKeyByRepo(ctx, keyID, repoID)
|
||||
if err != nil {
|
||||
if asymkey_model.IsErrDeployKeyNotExist(err) {
|
||||
ctx.PlainText(http.StatusOK, "success")
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
deployKey.UpdatedUnix = timeutil.TimeStampNow()
|
||||
if err = asymkey_model.UpdateDeployKeyCols(ctx, deployKey, "updated_unix"); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err.Error(),
|
||||
})
|
||||
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.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
authorizedString, err := asymkey_model.AuthorizedStringForKey(publicKey)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err.Error(),
|
||||
UserMsg: "invalid public key",
|
||||
})
|
||||
return
|
||||
}
|
||||
ctx.PlainText(http.StatusOK, authorizedString)
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
stdCtx "context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
"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.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: "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 {
|
||||
log.Error("JSON Decode failed: %v", err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var emails []string
|
||||
if len(mail.To) > 0 {
|
||||
for _, uname := range mail.To {
|
||||
user, err := user_model.GetUserByName(ctx, uname)
|
||||
if err != nil {
|
||||
err := fmt.Sprintf("Failed to get user information: %v", err)
|
||||
log.Error(err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: 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 {
|
||||
err := fmt.Sprintf("Failed to find users: %v", err)
|
||||
log.Error(err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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/setting"
|
||||
"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.JSON(http.StatusInternalServerError, private.Response{
|
||||
UserMsg: fmt.Sprintf("Template error: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
ctx.PlainText(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// FlushQueues flushes all the Queues
|
||||
func FlushQueues(ctx *context.PrivateContext) {
|
||||
opts := web.GetForm(ctx).(*private.FlushOptions)
|
||||
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.JSON(http.StatusRequestTimeout, private.Response{
|
||||
UserMsg: fmt.Sprintf("%v", err),
|
||||
})
|
||||
}
|
||||
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.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("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")
|
||||
}
|
||||
|
||||
// RemoveLogger removes a logger
|
||||
func RemoveLogger(ctx *context.PrivateContext) {
|
||||
logger := ctx.PathParam("logger")
|
||||
writer := ctx.PathParam("writer")
|
||||
err := log.GetManager().GetLogger(logger).RemoveWriter(writer)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Failed to remove log writer: %s %s %v", logger, writer, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
ctx.PlainText(http.StatusOK, fmt.Sprintf("Removed %s %s", logger, writer))
|
||||
}
|
||||
|
||||
// AddLogger adds a logger
|
||||
func AddLogger(ctx *context.PrivateContext) {
|
||||
opts := web.GetForm(ctx).(*private.LoggerOptions)
|
||||
|
||||
if len(opts.Logger) == 0 {
|
||||
opts.Logger = log.DEFAULT
|
||||
}
|
||||
|
||||
writerMode := log.WriterMode{}
|
||||
writerType := opts.Mode
|
||||
|
||||
var flags string
|
||||
var ok bool
|
||||
if flags, ok = opts.Config["flags"].(string); !ok {
|
||||
switch opts.Logger {
|
||||
case "access":
|
||||
flags = ""
|
||||
case "router":
|
||||
flags = "date,time"
|
||||
default:
|
||||
flags = "stdflags"
|
||||
}
|
||||
}
|
||||
writerMode.Flags = log.FlagsFromString(flags)
|
||||
|
||||
if writerMode.Colorize, ok = opts.Config["colorize"].(bool); !ok && opts.Mode == "console" {
|
||||
if _, ok := opts.Config["stderr"]; ok {
|
||||
writerMode.Colorize = log.CanColorStderr
|
||||
} else {
|
||||
writerMode.Colorize = log.CanColorStdout
|
||||
}
|
||||
}
|
||||
|
||||
writerMode.Level = setting.Log.Level
|
||||
if level, ok := opts.Config["level"].(string); ok {
|
||||
writerMode.Level = log.LevelFromString(level)
|
||||
}
|
||||
|
||||
writerMode.StacktraceLevel = setting.Log.StacktraceLogLevel
|
||||
if stacktraceLevel, ok := opts.Config["level"].(string); ok {
|
||||
writerMode.StacktraceLevel = log.LevelFromString(stacktraceLevel)
|
||||
}
|
||||
|
||||
writerMode.Prefix, _ = opts.Config["prefix"].(string)
|
||||
writerMode.Expression, _ = opts.Config["expression"].(string)
|
||||
|
||||
switch writerType {
|
||||
case "console":
|
||||
writerOption := log.WriterConsoleOption{}
|
||||
writerOption.Stderr, _ = opts.Config["stderr"].(bool)
|
||||
writerMode.WriterOption = writerOption
|
||||
case "file":
|
||||
writerOption := log.WriterFileOption{}
|
||||
fileName, _ := opts.Config["filename"].(string)
|
||||
writerOption.FileName = setting.LogPrepareFilenameForWriter(fileName, opts.Writer+".log")
|
||||
writerOption.LogRotate, _ = opts.Config["rotate"].(bool)
|
||||
maxSizeShift, _ := opts.Config["maxsize"].(int)
|
||||
if maxSizeShift == 0 {
|
||||
maxSizeShift = 28
|
||||
}
|
||||
writerOption.MaxSize = 1 << maxSizeShift
|
||||
writerOption.DailyRotate, _ = opts.Config["daily"].(bool)
|
||||
writerOption.MaxDays, _ = opts.Config["maxdays"].(int)
|
||||
if writerOption.MaxDays == 0 {
|
||||
writerOption.MaxDays = 7
|
||||
}
|
||||
writerOption.Compress, _ = opts.Config["compress"].(bool)
|
||||
writerOption.CompressionLevel, _ = opts.Config["compressionLevel"].(int)
|
||||
if writerOption.CompressionLevel == 0 {
|
||||
writerOption.CompressionLevel = -1
|
||||
}
|
||||
writerMode.WriterOption = writerOption
|
||||
case "conn":
|
||||
writerOption := log.WriterConnOption{}
|
||||
writerOption.ReconnectOnMsg, _ = opts.Config["reconnectOnMsg"].(bool)
|
||||
writerOption.Reconnect, _ = opts.Config["reconnect"].(bool)
|
||||
writerOption.Protocol, _ = opts.Config["net"].(string)
|
||||
writerOption.Addr, _ = opts.Config["address"].(string)
|
||||
writerMode.WriterOption = writerOption
|
||||
default:
|
||||
panic("invalid log writer mode: " + writerType)
|
||||
}
|
||||
writer, err := log.NewEventWriter(opts.Writer, writerType, writerMode)
|
||||
if err != nil {
|
||||
log.Error("Failed to create new log writer: %v", err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Failed to create new log writer: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
log.GetManager().GetLogger(opts.Logger).AddWriters(writer)
|
||||
ctx.PlainText(http.StatusOK, "success")
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/private"
|
||||
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 {
|
||||
log.Error("Unable to get stacktrace: %v", err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("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 {
|
||||
log.Error("Unable to write out process stacktrace: %v", err)
|
||||
if !ctx.Written() {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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/modules/private"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// Restart is not implemented for Windows based servers as they can't fork
|
||||
func Restart(ctx *context.PrivateContext) {
|
||||
ctx.JSON(http.StatusNotImplemented, private.Response{
|
||||
UserMsg: "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")
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/private"
|
||||
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.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
params := struct {
|
||||
RepoDir string
|
||||
OwnerName string
|
||||
RepoName string
|
||||
Units []string
|
||||
Validation bool
|
||||
}{}
|
||||
if err = json.Unmarshal(bs, ¶ms); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := migrations.RestoreRepository(
|
||||
ctx,
|
||||
params.RepoDir,
|
||||
params.OwnerName,
|
||||
params.RepoName,
|
||||
params.Units,
|
||||
params.Validation,
|
||||
); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err.Error(),
|
||||
})
|
||||
} else {
|
||||
ctx.PlainText(http.StatusOK, "success")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package private
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
asymkey_model "gitea.dev/models/asymkey"
|
||||
"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/log"
|
||||
"gitea.dev/modules/private"
|
||||
"gitea.dev/modules/setting"
|
||||
"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.JSON(http.StatusBadRequest, private.Response{
|
||||
UserMsg: fmt.Sprintf("Bad key id: %d", keyID),
|
||||
})
|
||||
}
|
||||
results := private.KeyAndOwner{}
|
||||
|
||||
key, err := asymkey_model.GetPublicKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
if asymkey_model.IsErrKeyNotExist(err) {
|
||||
ctx.JSON(http.StatusUnauthorized, private.Response{
|
||||
UserMsg: fmt.Sprintf("Cannot find key: %d", keyID),
|
||||
})
|
||||
return
|
||||
}
|
||||
log.Error("Unable to get public key: %d Error: %v", keyID, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err.Error(),
|
||||
})
|
||||
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.JSON(http.StatusUnauthorized, private.Response{
|
||||
UserMsg: fmt.Sprintf("Cannot find owner with id: %d for key: %d", key.OwnerID, keyID),
|
||||
})
|
||||
return
|
||||
}
|
||||
log.Error("Unable to get owner with id: %d for public key: %d Error: %v", key.OwnerID, keyID, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if !user.IsActive || user.ProhibitLogin {
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: "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")
|
||||
ownerName := ctx.PathParam("owner")
|
||||
repoName := 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{
|
||||
RepoName: repoName,
|
||||
OwnerName: ownerName,
|
||||
KeyID: keyID,
|
||||
}
|
||||
|
||||
// Now because we're not translating things properly let's just default some English strings here
|
||||
modeString := "read"
|
||||
if mode > perm.AccessModeRead {
|
||||
modeString = "write to"
|
||||
}
|
||||
|
||||
// The default unit we're trying to look at is code
|
||||
unitType := unit.TypeCode
|
||||
|
||||
// Unless we're a wiki...
|
||||
if strings.HasSuffix(repoName, ".wiki") {
|
||||
// in which case we need to look at the wiki
|
||||
unitType = unit.TypeWiki
|
||||
// And we'd better munge the reponame and tell downstream we're looking at a wiki
|
||||
results.IsWiki = true
|
||||
results.RepoName = repoName[:len(repoName)-5]
|
||||
}
|
||||
|
||||
owner, err := user_model.GetUserByName(ctx, results.OwnerName)
|
||||
if err != nil {
|
||||
if !user_model.IsErrUserNotExist(err) {
|
||||
log.Error("Unable to get repository owner: %s/%s Error: %v", results.OwnerName, results.RepoName, err)
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: fmt.Sprintf("Unable to get repository owner: %s/%s %v", results.OwnerName, results.RepoName, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if there is a user redirect for the requested owner
|
||||
redirectedUserID, err := user_model.LookupUserRedirect(ctx, results.OwnerName)
|
||||
if err != nil {
|
||||
// User is fetching/cloning a non-existent repository
|
||||
log.Warn("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr())
|
||||
ctx.JSON(http.StatusNotFound, private.Response{
|
||||
UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
redirectUser, err := user_model.GetUserByID(ctx, redirectedUserID)
|
||||
if err != nil {
|
||||
// User is fetching/cloning a non-existent repository
|
||||
log.Warn("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr())
|
||||
ctx.JSON(http.StatusNotFound, private.Response{
|
||||
UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("User %s has been redirected to %s", results.OwnerName, redirectUser.Name)
|
||||
results.OwnerName = redirectUser.Name
|
||||
owner = redirectUser
|
||||
}
|
||||
if !owner.IsOrganization() && !owner.IsActive {
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: "Repository cannot be accessed, you could retry it later",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Now get the Repository and set the results section
|
||||
repoExist := true
|
||||
repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, results.RepoName)
|
||||
if err != nil {
|
||||
if !repo_model.IsErrRepoNotExist(err) {
|
||||
log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, 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/%s has been redirected to %s/%s", results.OwnerName, results.RepoName, redirectedRepo.OwnerName, redirectedRepo.Name)
|
||||
results.RepoName = redirectedRepo.Name
|
||||
results.OwnerName = redirectedRepo.OwnerName
|
||||
repo = redirectedRepo
|
||||
owner.ID = redirectedRepo.OwnerID
|
||||
} else {
|
||||
log.Warn("Repo %s/%s has a redirect to repo with ID %d, but no repo with this ID could be found. Trying without redirect...", results.OwnerName, results.RepoName, redirectedRepoID)
|
||||
}
|
||||
}
|
||||
|
||||
if repo == nil {
|
||||
repoExist = false
|
||||
if mode == perm.AccessModeRead {
|
||||
// User is fetching/cloning a non-existent repository
|
||||
log.Warn("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr())
|
||||
ctx.JSON(http.StatusNotFound, private.Response{
|
||||
UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if repoExist {
|
||||
repo.Owner = owner
|
||||
repo.OwnerName = ownerName
|
||||
results.RepoID = repo.ID
|
||||
|
||||
if repo.IsBeingCreated() {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: "Repository is being created, you could retry after it finished",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if repo.IsBroken() {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: "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.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: fmt.Sprintf("Mirror Repository %s/%s is read-only", results.OwnerName, results.RepoName),
|
||||
})
|
||||
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.JSON(http.StatusNotFound, private.Response{
|
||||
UserMsg: fmt.Sprintf("Cannot find key: %d", keyID),
|
||||
})
|
||||
return
|
||||
}
|
||||
log.Error("Unable to get public key: %d Error: %v", keyID, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to get key: %d Error: %v", keyID, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
results.KeyName = key.Name
|
||||
results.KeyID = key.ID
|
||||
results.UserID = key.OwnerID
|
||||
|
||||
// If repo doesn't exist, deploy key doesn't make sense
|
||||
if !repoExist && key.Type == asymkey_model.KeyTypeDeploy {
|
||||
ctx.JSON(http.StatusNotFound, private.Response{
|
||||
UserMsg: fmt.Sprintf("Cannot find repository %s/%s", results.OwnerName, results.RepoName),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Deploy Keys have ownerID set to 0 therefore we can't use the owner
|
||||
// So now we need to check if the key is a deploy key
|
||||
// We'll keep hold of the deploy key here for permissions checking
|
||||
var deployKey *asymkey_model.DeployKey
|
||||
var user *user_model.User
|
||||
if key.Type == asymkey_model.KeyTypeDeploy {
|
||||
var err error
|
||||
deployKey, err = asymkey_model.GetDeployKeyByRepo(ctx, key.ID, repo.ID)
|
||||
if err != nil {
|
||||
if asymkey_model.IsErrDeployKeyNotExist(err) {
|
||||
ctx.JSON(http.StatusNotFound, private.Response{
|
||||
UserMsg: fmt.Sprintf("Public (Deploy) Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName),
|
||||
})
|
||||
return
|
||||
}
|
||||
log.Error("Unable to get deploy for public (deploy) key: %d in %-v Error: %v", key.ID, repo, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to get Deploy Key for Public Key: %d:%s in %s/%s.", key.ID, key.Name, results.OwnerName, results.RepoName),
|
||||
})
|
||||
return
|
||||
}
|
||||
results.DeployKeyID = deployKey.ID
|
||||
results.KeyName = deployKey.Name
|
||||
|
||||
// FIXME: Deploy keys aren't really the owner of the repo pushing changes
|
||||
// however we don't have good way of representing deploy keys in hook.go
|
||||
// so for now use the owner of the repository
|
||||
results.UserName = results.OwnerName
|
||||
results.UserID = repo.OwnerID
|
||||
if !repo.Owner.KeepEmailPrivate {
|
||||
results.UserEmail = repo.Owner.Email
|
||||
}
|
||||
} else {
|
||||
// Get the user represented by the Key
|
||||
var err error
|
||||
user, err = user_model.GetUserByID(ctx, key.OwnerID)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.JSON(http.StatusUnauthorized, private.Response{
|
||||
UserMsg: fmt.Sprintf("Public Key: %d:%s owner %d does not exist.", key.ID, key.Name, key.OwnerID),
|
||||
})
|
||||
return
|
||||
}
|
||||
log.Error("Unable to get owner: %d for public key: %d:%s Error: %v", key.OwnerID, key.ID, key.Name, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to get Owner: %d for Deploy Key: %d:%s in %s/%s.", key.OwnerID, key.ID, key.Name, ownerName, repoName),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !user.IsActive || user.ProhibitLogin {
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: "Your account is disabled.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
results.UserName = user.Name
|
||||
if !user.KeepEmailPrivate {
|
||||
results.UserEmail = user.Email
|
||||
}
|
||||
}
|
||||
|
||||
// Don't allow pushing if the repo is archived
|
||||
if repoExist && mode > perm.AccessModeRead && repo.IsArchived {
|
||||
ctx.JSON(http.StatusUnauthorized, private.Response{
|
||||
UserMsg: fmt.Sprintf("Repo: %s/%s is archived.", results.OwnerName, results.RepoName),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Permissions checking:
|
||||
if repoExist &&
|
||||
(mode > perm.AccessModeRead ||
|
||||
repo.IsPrivate ||
|
||||
owner.Visibility.IsPrivate() ||
|
||||
(user != nil && user.IsRestricted) || // user will be nil if the key is a deploykey
|
||||
setting.Service.RequireSignInViewStrict) {
|
||||
if key.Type == asymkey_model.KeyTypeDeploy {
|
||||
if deployKey.Mode < mode {
|
||||
ctx.JSON(http.StatusUnauthorized, private.Response{
|
||||
UserMsg: fmt.Sprintf("Deploy Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName),
|
||||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 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
|
||||
}
|
||||
|
||||
perm, err := access_model.GetDoerRepoPermission(ctx, repo, user)
|
||||
if err != nil {
|
||||
log.Error("Unable to get permissions for %-v with key %d in %-v Error: %v", user, key.ID, repo, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to get permissions for user %d:%s with key %d in %s/%s Error: %v", user.ID, user.Name, key.ID, results.OwnerName, results.RepoName, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userMode := perm.UnitAccessMode(unitType)
|
||||
|
||||
if userMode < mode {
|
||||
log.Warn("Failed authentication attempt for %s with key %s (not authorized to %s %s/%s) from %s", user.Name, key.Name, modeString, ownerName, repoName, ctx.RemoteAddr())
|
||||
ctx.JSON(http.StatusUnauthorized, private.Response{
|
||||
UserMsg: fmt.Sprintf("User: %d:%s with Key: %d:%s is not authorized to %s %s/%s.", user.ID, user.Name, key.ID, key.Name, modeString, ownerName, repoName),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We already know we aren't using a deploy key
|
||||
if !repoExist {
|
||||
owner, err := user_model.GetUserByName(ctx, ownerName)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to get owner: %s %v", results.OwnerName, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg {
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: "Push to create is not enabled for organizations.",
|
||||
})
|
||||
return
|
||||
}
|
||||
if !owner.IsOrganization() && !setting.Repository.EnablePushCreateUser {
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: "Push to create is not enabled for users.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
repo, err = repo_service.PushCreateRepo(ctx, user, owner, results.RepoName)
|
||||
if err != nil {
|
||||
log.Error("pushCreateRepo: %v", err)
|
||||
ctx.JSON(http.StatusNotFound, private.Response{
|
||||
UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName),
|
||||
})
|
||||
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.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: "repository wiki is disabled",
|
||||
})
|
||||
return
|
||||
}
|
||||
log.Error("Failed to get the wiki unit in %-v Error: %v", repo, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Failed to get the wiki unit in %s/%s Error: %v", ownerName, repoName, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Finally if we're trying to touch the wiki we should init it
|
||||
if err = wiki_service.InitWiki(ctx, repo); err != nil {
|
||||
log.Error("Failed to initialize the wiki in %-v Error: %v", repo, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Failed to initialize the wiki in %s/%s Error: %v", ownerName, repoName, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Debug("Serv Results:\nIsWiki: %t\nDeployKeyID: %d\nKeyID: %d\tKeyName: %s\nUserName: %s\nUserID: %d\nOwnerName: %s\nRepoName: %s\nRepoID: %d",
|
||||
results.IsWiki,
|
||||
results.DeployKeyID,
|
||||
results.KeyID,
|
||||
results.KeyName,
|
||||
results.UserName,
|
||||
results.UserID,
|
||||
results.OwnerName,
|
||||
results.RepoName,
|
||||
results.RepoID)
|
||||
|
||||
ctx.JSON(http.StatusOK, results)
|
||||
// We will update the keys in a different call.
|
||||
}
|
||||
@@ -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(ctx).(*private.SSHLogOption)
|
||||
|
||||
if opts.IsError {
|
||||
log.Error("ssh: %v", opts.Message)
|
||||
ctx.Status(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("ssh: %v", opts.Message)
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
@@ -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
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+2
@@ -0,0 +1,2 @@
|
||||
x•ŽK
|
||||
1]çÙÒéüAÄS¸ï$Í"32ooð®ŠWð òÞ{›!žæ`–˜JC%¡.˜$Ár]sѱe$ïmòâMƒ·)£÷±(O`ªbtlÐE[:;4–àHÐ1_û�”rayýáþl“é’÷~“ÊEL@cå€Xv…Mþã":µMÛƒG«_}À?Ý
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+2
@@ -0,0 +1,2 @@
|
||||
x•�1
|
||||
!ES{ŠéAwGGa 9EúQgW·Èí#¹AªÞû©ÕZ§/£‹€³Œ–p±ì(¤(�ó®óBhÈÛ¼&ᙟãÝ:pLY`ûÍãU†ð-µzŸÁ°ô†\µ×ZM:�†ü¡¨Êå€óxJ/ûG}:µ3
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+3
@@ -0,0 +1,3 @@
|
||||
x•ŽA
|
||||
Â0E]ç³$™L“ˆx•L2µ]´•�
|
||||
ÞÞê
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
x•’Ë®«FE3æ+zn%44æ!%Qxƒ�Û€s˜AÓ`8Øæëã{£Ì2IM¶j•ª´¥Údèûf²Ìý2�”‚"‡$§e‰¶
|
||||
-(â Ä!´ÝJ"åaŲ@•BaîùHo3 ŸVØòå<$�/)å$JøJD’B¡•H¤§˜ü{¾#ÈRRðûOù«nfšÿF†þOÀ‰
|
||||
âq[°�2„̇~ŒÍô¬Ô÷zjjðë�ÒLÛÅÀ·}prm¬Fqhþä`@Ø«¦ªš®ª¥Õ˜fî?3Ç[7г…ê¨Ð) ^™þuÿÖ¿,µ�Æl7©zÝÿr|&«Ou4�Ø9Ó:µÎQjôû·êÕ1x±õå6ÍQ‡÷ƒÀ%Áåtû‰sò¸íV‰|( V¿�,aL,ù«G~²Ç�¹‹�‹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
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+3
@@ -0,0 +1,3 @@
|
||||
x•ŽA
|
||||
Â0E]ç³$™L“ˆx•L2µ]´•�
|
||||
ÞÞê
|
||||
@@ -0,0 +1 @@
|
||||
d766f2917716d45be24bfa968b8409544941be32
|
||||
@@ -0,0 +1,127 @@
|
||||
# GPG key for abcde@gitea.com
|
||||
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQGNBGTrY3UBDAC2HLBqmMplAV15qSnC7g1c4dV406f5EHNhFr95Nup2My6b2eaf
|
||||
Tlvedv77s8PT/I7F3fy4apOZs5A7w2SsPlLMcQ3ev4uGOsxRtkq5RLy1Yb6SNueX
|
||||
0Da2UVKR5KTC5Q6BWaqxwS0IjKOLZ/xz0Pbe/ClV3bZSKBEY2omkVo3Z0HZ771vB
|
||||
2clPRvGJ/IdeKOsZ3ZytSFXfyiJBdARmeSPmydXLil8+Ibq5iLAeow5PK8hK1TCO
|
||||
nKHzLWNqcNq70tyjoHvcGi70iGjoVEEUgPCLLuU8WmzTJwlvA3BuDzjtaO7TLo/j
|
||||
dE6iqkHtMSS8x+43sAH6hcFRCWAVh/0Uq7n36uGDfNxGnX3YrmX3LR9x5IsBES1r
|
||||
GGWbpxio4o5GIf/Xd+JgDd9rzJCqRuZ3/sW/TxK38htWaVNZV0kMkHUCTc1ctzWp
|
||||
Cm635hbFCHBhPYIp+/z206khkAKDbz/CNuU91Wazsh7KO07wrwDtxfDDbInJ8TfH
|
||||
E2TGjzjQzgChfmcAEQEAAbQXYWJjZGUgPGFiY2RlQGdpdGVhLmNvbT6JAc4EEwEI
|
||||
ADgWIQRo/BkcvP70fnQCv16xVDFkJim4JgUCZOtjdQIbAwULCQgHAgYVCgkICwIE
|
||||
FgIDAQIeAQIXgAAKCRCxVDFkJim4Js6+C/9yIjHqcyM88hQAYQUoiPYfgJ0f2NsD
|
||||
Ai/XypyDaFbRy9Wqm3oKvMr9L9G5xgOXshjRaRWOpODAwLmtVrJfOV5BhxLEcBcO
|
||||
2hDdM3ycp8Gt7+Fx/o0cUjPiiC18hh3K5LRfeE7oYynSJDgjoDNuzIMuyoWuJPNc
|
||||
+IcE4roND55qyyyC9ObrTLz1GgGm1bXtkHhZ1NdOfQ4q8M48K39Jn7pmnmSX3R74
|
||||
CSU6flh/o9AtzGLjU70JUOLFcWnR5D0iEI8mOsdfEHr+p+CvDVG9l4unPhMunT+Q
|
||||
OUwV2DEmqo9P+yIert1ucVTDoSf+FrRaKUHg8r1Tt6T4/4GyIeSxG72NImK0h8jz
|
||||
+bADPZhxuG4UR1Mj8bilqhWgODFPi/5DrDsNMWq1pEvjn6f4pCUx0IDTnPTniOXt
|
||||
afXtAD4Rz0rwJWYqgeJFHgjXzaxBiOE1bhS26NPEvyAa0T9Tj3E73ICMESAmVad2
|
||||
JqO/mVxkLDGWdpXM7qB8bO2YGMOplrTvWaa5AY0EZOtjdQEMAOwevO46JxBo91RC
|
||||
bT7RQ2uz3ZwRKb+P/jIEFST6x8tkCjon31zh6HicBDPNntqXTzStgoHQb7vGhHPV
|
||||
4dxAfrOtVyoHwpi1/+x1jjtZoyIzLEz6RNK/Onu2y/tC5JBnSd5QRdHJgzPm20F8
|
||||
iNZR37c0Mi24fIH4y01aVLfNeBpRt7lWJ+opo2bM3Rh7jJdMpynKkTcA6o9XP6Ig
|
||||
W/dzpOayosclpHhWiJwKV4CovIX/bxawk7sz10Nb4QzcxlWexWnJxNRHIcAkZ9KT
|
||||
XTBpBkBpHCZqsI3+rQoQn5oQAr9JGWJSd4Fmgw7mFjmIF4bjfa2h/BpCoBqE+/25
|
||||
chvWfYkQwrCcyUwD1QYPUBwNvLB+PWb9kYEHD3mLgSSR+fjdG9XdMevu4lT91Gqo
|
||||
/6KJzgzClSs7GoQtb+SZ4deUFw1tlmEQS/BGhbtTb/1566iDidGV5EnSmL/E4/3C
|
||||
bGQqNog8gremF0G0SlWTjD9RMBY13IgisWCC6R4CdkXIYnCWbwARAQABiQG2BBgB
|
||||
CAAgFiEEaPwZHLz+9H50Ar9esVQxZCYpuCYFAmTrY3UCGwwACgkQsVQxZCYpuCb1
|
||||
AAv/dI5YtGxBXaHAMj+lOLmZi5w4t0M7Zafa8tNnWrBwj4KixiXEt52i5YKxuaVD
|
||||
3+/cMqidSDp0M5Cxx0wcmnmg+mdFFcowtXIXuk1TGTcHcOCPoXgF6gfoGimNNE1A
|
||||
w1+EnC4/TbjMCKEM7b2QZ7/CgkBxZJWbScN4Jtawory9LEQqo0/epYJwf+79GHIJ
|
||||
rpODAPiPJEMKmlej23KyoFuusOi17C0vHCf3GZNj4F2So3LOrcs51qTlOum2MdL5
|
||||
oTdqffatzs6p4u5bHBxyRugQlQggTRSK+TXLdxnFXr9ukXjIC2mFir7CCnZHw4e+
|
||||
2JwZfaAom0ZX+pLwrReSop4BPPU2YDzt3XCUk0S9kpiOsN7iFWUMCFreIE50DOxt
|
||||
9406kSGopYKVaifbDl4MdLXM4v+oucLe7/yOViT/dm4FcIytIR+jzC8MaLQTB23e
|
||||
uzm2wOjI1YOwv7Il6PWZyDdU+tyzXcaJ7wSFBeQFZZtqph2TItCeV04HoaKHHc25
|
||||
4akc
|
||||
=OYIo
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
lQWGBGTrY3UBDAC2HLBqmMplAV15qSnC7g1c4dV406f5EHNhFr95Nup2My6b2eaf
|
||||
Tlvedv77s8PT/I7F3fy4apOZs5A7w2SsPlLMcQ3ev4uGOsxRtkq5RLy1Yb6SNueX
|
||||
0Da2UVKR5KTC5Q6BWaqxwS0IjKOLZ/xz0Pbe/ClV3bZSKBEY2omkVo3Z0HZ771vB
|
||||
2clPRvGJ/IdeKOsZ3ZytSFXfyiJBdARmeSPmydXLil8+Ibq5iLAeow5PK8hK1TCO
|
||||
nKHzLWNqcNq70tyjoHvcGi70iGjoVEEUgPCLLuU8WmzTJwlvA3BuDzjtaO7TLo/j
|
||||
dE6iqkHtMSS8x+43sAH6hcFRCWAVh/0Uq7n36uGDfNxGnX3YrmX3LR9x5IsBES1r
|
||||
GGWbpxio4o5GIf/Xd+JgDd9rzJCqRuZ3/sW/TxK38htWaVNZV0kMkHUCTc1ctzWp
|
||||
Cm635hbFCHBhPYIp+/z206khkAKDbz/CNuU91Wazsh7KO07wrwDtxfDDbInJ8TfH
|
||||
E2TGjzjQzgChfmcAEQEAAf4HAwKN54iG/XBl5/UViAmmiESRj3u+uJC9EztalVbj
|
||||
156bjamUHBYIoCH4SBB0l0bR/o9ZN3vE4ZvyF3OyJ0AKF9epjWIuz7S+QIm1NLzk
|
||||
IqwRyfGPsktwtZOF1CsathN4RyJL5/3nB9g4BLYfRARe9lwU0C0HQjBwAVj8m6RN
|
||||
+wMTHZqW7tUN75npgPRLUI30H3GPVm3yLfS88Ol8nd31r7V0JsXZ2/mM9CWF4sUy
|
||||
o1DW3P/rBn49s/x2qL/acEL+5PK7suFBP8Pjp5cwGjnSehoWeOclXgstkg3OEryY
|
||||
2JP74muDVmaEVOAk7wiRjUD7HYuEOm/MbphFyen7QtO8WtN3IRKgNm19v5Skd4AF
|
||||
NW9ZAdQOk2yHw7zyRk7HOPmEbEstbyE1RYWIfgZGjJlEJ2DI5ABwVJJ3W6DRPiZ3
|
||||
owd/JxBUVu/wigIjbg6z6ZQd/bn1XwKyhyTtgyTyILzE1gqtO7xs1XmK3wcww794
|
||||
cVLjqSnAdaeXMt4P+sDA17Wqky0f/jQ9kq7/tv7ipq9jvp9RaQ1ccRsz+mGgBVl+
|
||||
oLg4klKN47ZQGt0SQpLzHLL8SHzY0dz5US+Z2J+hdZia6jEmfilY9r4WPe7djMYz
|
||||
Na908DmcbjfAg4XHPqVRXjgraUiT2YTo2LOV2dHn7550hJ/JshpOVqrJUrjhCgDN
|
||||
usEMK3KXJkFvf6zflMv3t8HMD2SGBfpCJSwDaW+mrmtpR6a5laoZxg/009qZqgpj
|
||||
FuenLuZmgYrHXozMXllwi6MLvSE/ioXrK4fqvpAwzOk6ArqZdWfxoJDYNQKXVL7z
|
||||
Arniq9Ctaag8hr5T+JoZ9wNPNVF/LuEwPTWDur4qpU07KqWt9OFKPsEDNzxVZfNM
|
||||
vtSCYvQ1uUH3CbPLQvPpd5TnyhjwKYtTzyW4OcuZHrWIZp9fZi5QdhWxobqGQiBk
|
||||
+nRNFe0FPVEN0VcNdYJIDKcDLsOYCkGy08tucZnbKtr8JaK7XBSOo9Frg1i/j4Aa
|
||||
GnXWlkMTVAkuxLZPATTOgdBoYmHMYKQvw31aFBrf3QU9c3EEg9UPYFMErVIeBHBB
|
||||
BS+E7QZToHScCG1zezlr4rdqarkz0Yvzc3aduoSAOJHDf/Il+tOkepMne1y5fi72
|
||||
5UT1yWGbXXkTCV/pM6s0pLaEvNHmGvPQ6VGbJ//5w+42PFD1d7yEai53OgSZNs7B
|
||||
+Ie/6Vq5GYzTM0bT3/o7/O1Zi56y791YKaas9wgxOhmMIZ0hsTecQJLJZGotUlOv
|
||||
V7fZUhPRc4ksUeCyM3G0E89ilFtY6NuPcWQ8yMeS4sRRLmie+iaT+kNvAqL5mXvg
|
||||
WNLhFIXPC1gpGLB8lpT5YEY647aPjQEig7QXYWJjZGUgPGFiY2RlQGdpdGVhLmNv
|
||||
bT6JAc4EEwEIADgWIQRo/BkcvP70fnQCv16xVDFkJim4JgUCZOtjdQIbAwULCQgH
|
||||
AgYVCgkICwIEFgIDAQIeAQIXgAAKCRCxVDFkJim4Js6+C/9yIjHqcyM88hQAYQUo
|
||||
iPYfgJ0f2NsDAi/XypyDaFbRy9Wqm3oKvMr9L9G5xgOXshjRaRWOpODAwLmtVrJf
|
||||
OV5BhxLEcBcO2hDdM3ycp8Gt7+Fx/o0cUjPiiC18hh3K5LRfeE7oYynSJDgjoDNu
|
||||
zIMuyoWuJPNc+IcE4roND55qyyyC9ObrTLz1GgGm1bXtkHhZ1NdOfQ4q8M48K39J
|
||||
n7pmnmSX3R74CSU6flh/o9AtzGLjU70JUOLFcWnR5D0iEI8mOsdfEHr+p+CvDVG9
|
||||
l4unPhMunT+QOUwV2DEmqo9P+yIert1ucVTDoSf+FrRaKUHg8r1Tt6T4/4GyIeSx
|
||||
G72NImK0h8jz+bADPZhxuG4UR1Mj8bilqhWgODFPi/5DrDsNMWq1pEvjn6f4pCUx
|
||||
0IDTnPTniOXtafXtAD4Rz0rwJWYqgeJFHgjXzaxBiOE1bhS26NPEvyAa0T9Tj3E7
|
||||
3ICMESAmVad2JqO/mVxkLDGWdpXM7qB8bO2YGMOplrTvWaadBYYEZOtjdQEMAOwe
|
||||
vO46JxBo91RCbT7RQ2uz3ZwRKb+P/jIEFST6x8tkCjon31zh6HicBDPNntqXTzSt
|
||||
goHQb7vGhHPV4dxAfrOtVyoHwpi1/+x1jjtZoyIzLEz6RNK/Onu2y/tC5JBnSd5Q
|
||||
RdHJgzPm20F8iNZR37c0Mi24fIH4y01aVLfNeBpRt7lWJ+opo2bM3Rh7jJdMpynK
|
||||
kTcA6o9XP6IgW/dzpOayosclpHhWiJwKV4CovIX/bxawk7sz10Nb4QzcxlWexWnJ
|
||||
xNRHIcAkZ9KTXTBpBkBpHCZqsI3+rQoQn5oQAr9JGWJSd4Fmgw7mFjmIF4bjfa2h
|
||||
/BpCoBqE+/25chvWfYkQwrCcyUwD1QYPUBwNvLB+PWb9kYEHD3mLgSSR+fjdG9Xd
|
||||
Mevu4lT91Gqo/6KJzgzClSs7GoQtb+SZ4deUFw1tlmEQS/BGhbtTb/1566iDidGV
|
||||
5EnSmL/E4/3CbGQqNog8gremF0G0SlWTjD9RMBY13IgisWCC6R4CdkXIYnCWbwAR
|
||||
AQAB/gcDAgtreHsdznsa9bAha2g+J5zygs7rp95KvqRm4SGrgWPnngMewrHXrJAx
|
||||
REUQFbOYJKvb6+SB47N8BTIh/nEY/B6dpvC36QSHB0XAgkktiOhdS2rTlrq+bKse
|
||||
rZzoM/jbcxS3/cwi4VWH4lQhz7TLZtQxFZDuwyiik8/m5KscMxQrbYJg++4KpFQQ
|
||||
En7RRUO0hEaYdnqQ9t3M8SWLwZn2yK3hzBE0gkQ8CJA3Zokv3DO7FSsAX823O25B
|
||||
X7NgIpmbHCeYK6YV0gjQUKP1o3Sf7DhJzO1iltg0+obNTDl9RoeFgxTVORCdUlGA
|
||||
kPdgoBbAGtadpZlCMThn7FlIn+ogqwQpAcoSTZjX31SOQBBpgMW9yf3GTNk2Nvrn
|
||||
08zIA0hnUWFfc4VY6fbjbX5bF0jpoJ3XG6Hwa1VVRwQGFLxFV23TbZ+baLLuxEBx
|
||||
A86XDC5zWFMwF/7aYL8oeXgoI+499u9G4Gw9G87va7rQXlTQJcHQRqu9YaGcxwOi
|
||||
UslhNtVWz52iIURappUfFaGBRGUvtx2DOTgn4m099nnPaKDUiLmc4bFIHwzyA7Pl
|
||||
RdAmLosrxSyIxHdlUOS/KshucXXKGVoYkJqGLXNQCY6x2zbyBPX9/a/0P59UP/WU
|
||||
qwAHuGbXlToGhSKZzC8KmVs12tyQsAZ/47D+G29kEcRlaey1+N3Uor1jN7D66uyj
|
||||
M1jYFhBudNIuuTR8sfrYjmbYIj8y0bgvF4RN6sU1padoTETadWNyIcFiRMZQ0oQd
|
||||
KJBa3CxdqQZ2EU4a5jkA4UTQE13IySh7eNbYP5VwBgr3Z59gcbouKfFxKBhmPHF2
|
||||
BAmC0VXI2BgqKNqM6QgVj5UKrp41AX4D+iIhyKa0D3rapuIywXg1AtsrAlrOU/Ig
|
||||
tQCj/a0NjIVJpLqVKBUdd4Eea69fDCJGIoaDNyp7qwo+nA1O2oDbc32EryJYUkHm
|
||||
XMoLmx5y+/rxRsRevBv0ojwu3zsx2K93M1wHYd0z+SJsU8QGFinoFgYcmNp/tgMW
|
||||
WtHBN4AijDuDSZAyG+MrWIj3NS4mbajx+utEIn3DC/ofFPlTmgX3OvpOPG1hnhBH
|
||||
xSZUME+znOnqJMpUqnna4jbHEPwvRIXUY6InFKgl1Bu4grww/oo3qi7NwWL0Mcdy
|
||||
qabWhdlEz5N/QBBPWVQllelgI+xTmZoCRUhh1mn+PM900vXXeM/DIALnxEXs9I/m
|
||||
l4wPdLZlCdaKZS8vv33adyS6i9gWfI3NPWxZ2TyqC7nf5D5OK1zKSu3iWx17nXn2
|
||||
ak5hZnaXfzTxuZL3E8KZD/qsDm80c2PXFitogJTih37N6A8UQOJPtWbkfvPiwUvI
|
||||
gw0oouggn0iJQVNoiQG2BBgBCAAgFiEEaPwZHLz+9H50Ar9esVQxZCYpuCYFAmTr
|
||||
Y3UCGwwACgkQsVQxZCYpuCb1AAv/dI5YtGxBXaHAMj+lOLmZi5w4t0M7Zafa8tNn
|
||||
WrBwj4KixiXEt52i5YKxuaVD3+/cMqidSDp0M5Cxx0wcmnmg+mdFFcowtXIXuk1T
|
||||
GTcHcOCPoXgF6gfoGimNNE1Aw1+EnC4/TbjMCKEM7b2QZ7/CgkBxZJWbScN4Jtaw
|
||||
ory9LEQqo0/epYJwf+79GHIJrpODAPiPJEMKmlej23KyoFuusOi17C0vHCf3GZNj
|
||||
4F2So3LOrcs51qTlOum2MdL5oTdqffatzs6p4u5bHBxyRugQlQggTRSK+TXLdxnF
|
||||
Xr9ukXjIC2mFir7CCnZHw4e+2JwZfaAom0ZX+pLwrReSop4BPPU2YDzt3XCUk0S9
|
||||
kpiOsN7iFWUMCFreIE50DOxt9406kSGopYKVaifbDl4MdLXM4v+oucLe7/yOViT/
|
||||
dm4FcIytIR+jzC8MaLQTB23euzm2wOjI1YOwv7Il6PWZyDdU+tyzXcaJ7wSFBeQF
|
||||
ZZtqph2TItCeV04HoaKHHc254akc
|
||||
=PPG4
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
||||
Reference in New Issue
Block a user