mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-14 21:47:38 +02:00
Merge branch 'main' into fix-24635
Signed-off-by: Excellencedev <ademiluyisuccessandexcellence@gmail.com>
This commit is contained in:
commit
7df7f72a71
@ -9,11 +9,11 @@ fi
|
||||
mv ./options/locale/locale_en-US.json ./options/
|
||||
|
||||
# Remove translation under 25% of en_us
|
||||
baselines=$(wc -l "./options/locale_en-US.json" | cut -d" " -f1)
|
||||
baselines=$(cat "./options/locale_en-US.json" | wc -l)
|
||||
baselines=$((baselines / 4))
|
||||
for filename in ./options/locale/*.json; do
|
||||
lines=$(wc -l "$filename" | cut -d" " -f1)
|
||||
if [ $lines -lt $baselines ]; then
|
||||
lines=$(cat "$filename" | wc -l)
|
||||
if [ "$lines" -lt "$baselines" ]; then
|
||||
echo "Removing $filename: $lines/$baselines"
|
||||
rm "$filename"
|
||||
fi
|
||||
|
||||
@ -503,9 +503,6 @@ INTERNAL_TOKEN =
|
||||
;; Password Hash algorithm, either "argon2", "pbkdf2", "scrypt" or "bcrypt"
|
||||
;PASSWORD_HASH_ALGO = pbkdf2
|
||||
;;
|
||||
;; Set false to allow JavaScript to read CSRF cookie
|
||||
;CSRF_COOKIE_HTTP_ONLY = true
|
||||
;;
|
||||
;; Validate against https://haveibeenpwned.com/Passwords to see if a password has been exposed
|
||||
;PASSWORD_CHECK_PWN = false
|
||||
;;
|
||||
|
||||
@ -9,24 +9,38 @@ package git
|
||||
import (
|
||||
"io"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
// Blob represents a Git object.
|
||||
type Blob struct {
|
||||
ID ObjectID
|
||||
ID ObjectID
|
||||
repo *Repository
|
||||
name string
|
||||
}
|
||||
|
||||
gogitEncodedObj plumbing.EncodedObject
|
||||
name string
|
||||
func (b *Blob) gogitEncodedObj() (plumbing.EncodedObject, error) {
|
||||
return b.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, plumbing.Hash(b.ID.RawValue()))
|
||||
}
|
||||
|
||||
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
|
||||
// Calling the Close function on the result will discard all unread output.
|
||||
func (b *Blob) DataAsync() (io.ReadCloser, error) {
|
||||
return b.gogitEncodedObj.Reader()
|
||||
obj, err := b.gogitEncodedObj()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj.Reader()
|
||||
}
|
||||
|
||||
// Size returns the uncompressed size of the blob
|
||||
func (b *Blob) Size() int64 {
|
||||
return b.gogitEncodedObj.Size()
|
||||
obj, err := b.gogitEncodedObj()
|
||||
if err != nil {
|
||||
log.Error("Error getting gogit encoded object for blob %s(%s): %v", b.name, b.ID.String(), err)
|
||||
return 0
|
||||
}
|
||||
return obj.Size()
|
||||
}
|
||||
|
||||
@ -9,5 +9,11 @@ func (repo *Repository) GetBlob(idStr string) (*Blob, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return repo.getBlob(id)
|
||||
if id.IsZero() {
|
||||
return nil, ErrNotExist{id.String(), ""}
|
||||
}
|
||||
return &Blob{
|
||||
ID: id,
|
||||
repo: repo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
func (repo *Repository) getBlob(id ObjectID) (*Blob, error) {
|
||||
encodedObj, err := repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, plumbing.Hash(id.RawValue()))
|
||||
if err != nil {
|
||||
return nil, ErrNotExist{id.String(), ""}
|
||||
}
|
||||
|
||||
return &Blob{
|
||||
ID: id,
|
||||
gogitEncodedObj: encodedObj,
|
||||
}, nil
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
||||
package git
|
||||
|
||||
func (repo *Repository) getBlob(id ObjectID) (*Blob, error) {
|
||||
if id.IsZero() {
|
||||
return nil, ErrNotExist{id.String(), ""}
|
||||
}
|
||||
return &Blob{
|
||||
ID: id,
|
||||
repo: repo,
|
||||
}, nil
|
||||
}
|
||||
@ -53,14 +53,9 @@ func (te *TreeEntry) Size() int64 {
|
||||
|
||||
// Blob returns the blob object the entry
|
||||
func (te *TreeEntry) Blob() *Blob {
|
||||
encodedObj, err := te.ptree.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, te.toGogitTreeEntry().Hash)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &Blob{
|
||||
ID: te.ID,
|
||||
gogitEncodedObj: encodedObj,
|
||||
name: te.Name(),
|
||||
ID: te.ID,
|
||||
repo: te.ptree.repo,
|
||||
name: te.Name(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -255,7 +255,7 @@ func newMarkupRenderer(name string, sec ConfigSection) {
|
||||
}
|
||||
|
||||
// ATTENTION! at the moment, only a safe set like "allow-scripts" are allowed for sandbox mode.
|
||||
// "allow-same-origin" should never be used, it leads to XSS attack, and it makes the JS in iframe can access parent window's config and CSRF token
|
||||
// "allow-same-origin" should NEVER be used, it leads to XSS attack: makes the JS in iframe can access parent window's config and send requests with user's credentials.
|
||||
renderContentSandbox := sec.Key("RENDER_CONTENT_SANDBOX").MustString("allow-scripts allow-popups")
|
||||
if renderContentSandbox == "disabled" {
|
||||
renderContentSandbox = ""
|
||||
|
||||
@ -133,7 +133,7 @@ func loadOAuth2From(rootCfg ConfigProvider) {
|
||||
|
||||
// FIXME: at the moment, no matter oauth2 is enabled or not, it must generate a "oauth2 JWT_SECRET"
|
||||
// Because this secret is also used as GeneralTokenSigningSecret (as a quick not-that-breaking fix for some legacy problems).
|
||||
// Including: CSRF token, account validation token, etc ...
|
||||
// Including: account validation token, etc ...
|
||||
// In main branch, the signing token should be refactored (eg: one unique for LFS/OAuth2/etc ...)
|
||||
jwtSecretBase64 := loadSecret(sec, "JWT_SECRET_URI", "JWT_SECRET")
|
||||
if InstallLock {
|
||||
|
||||
@ -36,8 +36,6 @@ var (
|
||||
PasswordCheckPwn bool
|
||||
SuccessfulTokensCacheSize int
|
||||
DisableQueryAuthToken bool
|
||||
CSRFCookieName = "_csrf"
|
||||
CSRFCookieHTTPOnly = true
|
||||
RecordUserSignupMetadata = false
|
||||
TwoFactorAuthEnforced = false
|
||||
)
|
||||
@ -139,7 +137,6 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
|
||||
log.Fatal("The provided password hash algorithm was invalid: %s", sec.Key("PASSWORD_HASH_ALGO").MustString(""))
|
||||
}
|
||||
|
||||
CSRFCookieHTTPOnly = sec.Key("CSRF_COOKIE_HTTP_ONLY").MustBool(true)
|
||||
PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false)
|
||||
SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20)
|
||||
|
||||
|
||||
@ -131,16 +131,15 @@ func (l *locale) TrString(trKey string, trArgs ...any) string {
|
||||
var format string
|
||||
idx, ok := l.store.trKeyToIdxMap[trKey]
|
||||
if ok {
|
||||
if msg, ok := l.idxToMsgMap[idx]; ok {
|
||||
format = msg // use the found translation
|
||||
} else if def, ok := l.store.localeMap[l.store.defaultLang]; ok {
|
||||
// try to use default locale's translation
|
||||
if msg, ok := def.idxToMsgMap[idx]; ok {
|
||||
format = msg
|
||||
format = l.idxToMsgMap[idx]
|
||||
if format == "" { // missing translation in this locale, fallback to default
|
||||
if def, ok := l.store.localeMap[l.store.defaultLang]; ok {
|
||||
// try to use default locale's translation
|
||||
format = def.idxToMsgMap[idx]
|
||||
}
|
||||
}
|
||||
}
|
||||
if format == "" {
|
||||
if format == "" { // still missing, use the key itself
|
||||
format = html.EscapeString(trKey)
|
||||
}
|
||||
msg, err := Format(format, trArgs...)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,6 @@ package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
@ -52,18 +51,7 @@ func CompareDiff(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
|
||||
infoPath := ctx.PathParam("*")
|
||||
infos := []string{ctx.Repo.Repository.DefaultBranch, ctx.Repo.Repository.DefaultBranch}
|
||||
if infoPath != "" {
|
||||
infos = strings.SplitN(infoPath, "...", 2)
|
||||
if len(infos) != 2 {
|
||||
if infos = strings.SplitN(infoPath, "..", 2); len(infos) != 2 {
|
||||
infos = []string{ctx.Repo.Repository.DefaultBranch, infoPath}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compareResult, closer := parseCompareInfo(ctx, api.CreatePullRequestOption{Base: infos[0], Head: infos[1]})
|
||||
compareInfo, closer := parseCompareInfo(ctx, ctx.PathParam("*"))
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
@ -72,10 +60,10 @@ func CompareDiff(ctx *context.APIContext) {
|
||||
verification := ctx.FormString("verification") == "" || ctx.FormBool("verification")
|
||||
files := ctx.FormString("files") == "" || ctx.FormBool("files")
|
||||
|
||||
apiCommits := make([]*api.Commit, 0, len(compareResult.compareInfo.Commits))
|
||||
apiCommits := make([]*api.Commit, 0, len(compareInfo.Commits))
|
||||
userCache := make(map[string]*user_model.User)
|
||||
for i := 0; i < len(compareResult.compareInfo.Commits); i++ {
|
||||
apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, compareResult.compareInfo.Commits[i], userCache,
|
||||
for i := 0; i < len(compareInfo.Commits); i++ {
|
||||
apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, compareInfo.Commits[i], userCache,
|
||||
convert.ToCommitOptions{
|
||||
Stat: true,
|
||||
Verification: verification,
|
||||
@ -89,7 +77,7 @@ func CompareDiff(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, &api.Compare{
|
||||
TotalCommits: len(compareResult.compareInfo.Commits),
|
||||
TotalCommits: len(compareInfo.Commits),
|
||||
Commits: apiCommits,
|
||||
})
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/routers/common"
|
||||
@ -36,6 +37,7 @@ import (
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
git_service "code.gitea.io/gitea/services/git"
|
||||
"code.gitea.io/gitea/services/gitdiff"
|
||||
issue_service "code.gitea.io/gitea/services/issue"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
@ -414,20 +416,20 @@ func CreatePullRequest(ctx *context.APIContext) {
|
||||
)
|
||||
|
||||
// Get repo/branch information
|
||||
compareResult, closer := parseCompareInfo(ctx, form)
|
||||
compareResult, closer := parseCompareInfo(ctx, form.Base+".."+form.Head)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
defer closer()
|
||||
|
||||
if !compareResult.baseRef.IsBranch() || !compareResult.headRef.IsBranch() {
|
||||
if !compareResult.BaseRef.IsBranch() || !compareResult.HeadRef.IsBranch() {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "Invalid PullRequest: base and head must be branches")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if another PR exists with the same targets
|
||||
existingPr, err := issues_model.GetUnmergedPullRequest(ctx, compareResult.headRepo.ID, ctx.Repo.Repository.ID,
|
||||
compareResult.headRef.ShortName(), compareResult.baseRef.ShortName(),
|
||||
existingPr, err := issues_model.GetUnmergedPullRequest(ctx, compareResult.HeadRepo.ID, ctx.Repo.Repository.ID,
|
||||
compareResult.HeadRef.ShortName(), compareResult.BaseRef.ShortName(),
|
||||
issues_model.PullRequestFlowGithub,
|
||||
)
|
||||
if err != nil {
|
||||
@ -505,13 +507,13 @@ func CreatePullRequest(ctx *context.APIContext) {
|
||||
DeadlineUnix: deadlineUnix,
|
||||
}
|
||||
pr := &issues_model.PullRequest{
|
||||
HeadRepoID: compareResult.headRepo.ID,
|
||||
HeadRepoID: compareResult.HeadRepo.ID,
|
||||
BaseRepoID: repo.ID,
|
||||
HeadBranch: compareResult.headRef.ShortName(),
|
||||
BaseBranch: compareResult.baseRef.ShortName(),
|
||||
HeadRepo: compareResult.headRepo,
|
||||
HeadBranch: compareResult.HeadRef.ShortName(),
|
||||
BaseBranch: compareResult.BaseRef.ShortName(),
|
||||
HeadRepo: compareResult.HeadRepo,
|
||||
BaseRepo: repo,
|
||||
MergeBase: compareResult.compareInfo.MergeBase,
|
||||
MergeBase: compareResult.MergeBase,
|
||||
Type: issues_model.PullRequestGitea,
|
||||
}
|
||||
|
||||
@ -1057,63 +1059,40 @@ func MergePullRequest(ctx *context.APIContext) {
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
type parseCompareInfoResult struct {
|
||||
headRepo *repo_model.Repository
|
||||
headGitRepo *git.Repository
|
||||
compareInfo *pull_service.CompareInfo
|
||||
baseRef git.RefName
|
||||
headRef git.RefName
|
||||
}
|
||||
|
||||
// parseCompareInfo returns non-nil if it succeeds, it always writes to the context and returns nil if it fails
|
||||
func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) (result *parseCompareInfoResult, closer func()) {
|
||||
var err error
|
||||
// Get compared branches information
|
||||
// format: <base branch>...[<head repo>:]<head branch>
|
||||
// base<-head: master...head:feature
|
||||
// same repo: master...feature
|
||||
func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git_service.CompareInfo, closer func()) {
|
||||
baseRepo := ctx.Repo.Repository
|
||||
baseRefToGuess := form.Base
|
||||
|
||||
headUser := ctx.Repo.Owner
|
||||
headRefToGuess := form.Head
|
||||
if headInfos := strings.Split(form.Head, ":"); len(headInfos) == 1 {
|
||||
// If there is no head repository, it means pull request between same repository.
|
||||
// Do nothing here because the head variables have been assigned above.
|
||||
} else if len(headInfos) == 2 {
|
||||
// There is a head repository (the head repository could also be the same base repo)
|
||||
headRefToGuess = headInfos[1]
|
||||
headUser, err = user_model.GetUserOrOrgByName(ctx, headInfos[0])
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.APIErrorNotFound("GetUserByName")
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
} else {
|
||||
ctx.APIErrorNotFound()
|
||||
compareReq, err := common.ParseCompareRouterParam(compareParam)
|
||||
switch {
|
||||
case errors.Is(err, util.ErrInvalidArgument):
|
||||
ctx.APIError(http.StatusBadRequest, err.Error())
|
||||
return nil, nil
|
||||
case err != nil:
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
isSameRepo := ctx.Repo.Owner.ID == headUser.ID
|
||||
|
||||
var headRepo *repo_model.Repository
|
||||
if isSameRepo {
|
||||
headRepo = baseRepo
|
||||
} else {
|
||||
headRepo, err = common.FindHeadRepo(ctx, baseRepo, headUser.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil, nil
|
||||
}
|
||||
if headRepo == nil {
|
||||
ctx.APIErrorNotFound("head repository not found")
|
||||
return nil, nil
|
||||
}
|
||||
// remove the check when we support compare with carets
|
||||
if compareReq.CaretTimes > 0 {
|
||||
ctx.APIError(http.StatusBadRequest, "Unsupported compare syntax with carets")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
_, headRepo, err := common.GetHeadOwnerAndRepo(ctx, baseRepo, compareReq)
|
||||
switch {
|
||||
case errors.Is(err, util.ErrInvalidArgument):
|
||||
ctx.APIError(http.StatusBadRequest, err.Error())
|
||||
return nil, nil
|
||||
case errors.Is(err, util.ErrNotExist):
|
||||
ctx.APIErrorNotFound()
|
||||
return nil, nil
|
||||
case err != nil:
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
isSameRepo := baseRepo.ID == headRepo.ID
|
||||
|
||||
var headGitRepo *git.Repository
|
||||
if isSameRepo {
|
||||
headGitRepo = ctx.Repo.GitRepo
|
||||
@ -1140,8 +1119,8 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
|
||||
}
|
||||
|
||||
if !permBase.CanRead(unit.TypeCode) {
|
||||
log.Trace("Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v", ctx.Doer, baseRepo, permBase)
|
||||
ctx.APIErrorNotFound("Can't read pulls or can't read UnitTypeCode")
|
||||
log.Trace("Permission Denied: User %-v cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v", ctx.Doer, baseRepo, permBase)
|
||||
ctx.APIErrorNotFound("can't read baseRepo UnitTypeCode")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@ -1158,10 +1137,10 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(baseRefToGuess)
|
||||
headRef := headGitRepo.UnstableGuessRefByShortName(headRefToGuess)
|
||||
baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(util.IfZero(compareReq.BaseOriRef, baseRepo.DefaultBranch))
|
||||
headRef := headGitRepo.UnstableGuessRefByShortName(util.IfZero(compareReq.HeadOriRef, headRepo.DefaultBranch))
|
||||
|
||||
log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.Repository.RelativePath(), baseRefToGuess, baseRef, headRefToGuess, headRef)
|
||||
log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.Repository.RelativePath(), compareReq.BaseOriRef, baseRef, compareReq.HeadOriRef, headRef)
|
||||
|
||||
baseRefValid := baseRef.IsBranch() || baseRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName), baseRef.ShortName())
|
||||
headRefValid := headRef.IsBranch() || headRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(headRepo.ObjectFormatName), headRef.ShortName())
|
||||
@ -1171,14 +1150,13 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
compareInfo, err := pull_service.GetCompareInfo(ctx, baseRepo, headRepo, headGitRepo, baseRef.ShortName(), headRef.ShortName(), false, false)
|
||||
compareInfo, err := git_service.GetCompareInfo(ctx, baseRepo, headRepo, headGitRepo, baseRef, headRef, compareReq.DirectComparison(), false)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
result = &parseCompareInfoResult{headRepo: headRepo, headGitRepo: headGitRepo, compareInfo: compareInfo, baseRef: baseRef, headRef: headRef}
|
||||
return result, closer
|
||||
return compareInfo, closer
|
||||
}
|
||||
|
||||
// UpdatePullRequest merge PR's baseBranch into headBranch
|
||||
@ -1422,7 +1400,7 @@ func GetPullRequestCommits(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
var prInfo *pull_service.CompareInfo
|
||||
var compareInfo *git_service.CompareInfo
|
||||
baseGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.BaseRepo)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
@ -1431,19 +1409,18 @@ func GetPullRequestCommits(ctx *context.APIContext) {
|
||||
defer closer.Close()
|
||||
|
||||
if pr.HasMerged {
|
||||
prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.MergeBase, pr.GetGitHeadRefName(), false, false)
|
||||
compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefName(pr.MergeBase), git.RefName(pr.GetGitHeadRefName()), false, false)
|
||||
} else {
|
||||
prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.BaseBranch, pr.GetGitHeadRefName(), false, false)
|
||||
compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefNameFromBranch(pr.BaseBranch), git.RefName(pr.GetGitHeadRefName()), false, false)
|
||||
}
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
commits := prInfo.Commits
|
||||
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
|
||||
totalNumberOfCommits := len(commits)
|
||||
totalNumberOfCommits := len(compareInfo.Commits)
|
||||
totalNumberOfPages := int(math.Ceil(float64(totalNumberOfCommits) / float64(listOptions.PageSize)))
|
||||
|
||||
userCache := make(map[string]*user_model.User)
|
||||
@ -1458,7 +1435,7 @@ func GetPullRequestCommits(ctx *context.APIContext) {
|
||||
|
||||
apiCommits := make([]*api.Commit, 0, limit)
|
||||
for i := start; i < start+limit; i++ {
|
||||
apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, baseGitRepo, commits[i], userCache,
|
||||
apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, baseGitRepo, compareInfo.Commits[i], userCache,
|
||||
convert.ToCommitOptions{
|
||||
Stat: true,
|
||||
Verification: verification,
|
||||
@ -1552,11 +1529,11 @@ func GetPullRequestFiles(ctx *context.APIContext) {
|
||||
|
||||
baseGitRepo := ctx.Repo.GitRepo
|
||||
|
||||
var prInfo *pull_service.CompareInfo
|
||||
var compareInfo *git_service.CompareInfo
|
||||
if pr.HasMerged {
|
||||
prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.MergeBase, pr.GetGitHeadRefName(), true, false)
|
||||
compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefName(pr.MergeBase), git.RefName(pr.GetGitHeadRefName()), true, false)
|
||||
} else {
|
||||
prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.BaseBranch, pr.GetGitHeadRefName(), true, false)
|
||||
compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefNameFromBranch(pr.BaseBranch), git.RefName(pr.GetGitHeadRefName()), true, false)
|
||||
}
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
@ -1569,7 +1546,7 @@ func GetPullRequestFiles(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
startCommitID := prInfo.MergeBase
|
||||
startCommitID := compareInfo.MergeBase
|
||||
endCommitID := headCommitID
|
||||
|
||||
maxLines := setting.Git.MaxGitDiffLines
|
||||
|
||||
@ -38,8 +38,8 @@ func AuthShared(ctx *context.Base, sessionStore auth_service.SessionStore, authM
|
||||
|
||||
// VerifyOptions contains required or check options
|
||||
type VerifyOptions struct {
|
||||
SignInRequired bool
|
||||
SignOutRequired bool
|
||||
AdminRequired bool
|
||||
DisableCSRF bool
|
||||
SignInRequired bool
|
||||
SignOutRequired bool
|
||||
AdminRequired bool
|
||||
DisableCrossOriginProtection bool
|
||||
}
|
||||
|
||||
@ -5,22 +5,110 @@ package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// CompareInfo represents the collected results from ParseCompareInfo
|
||||
type CompareInfo struct {
|
||||
HeadUser *user_model.User
|
||||
HeadRepo *repo_model.Repository
|
||||
HeadGitRepo *git.Repository
|
||||
CompareInfo *pull_service.CompareInfo
|
||||
BaseBranch string
|
||||
HeadBranch string
|
||||
DirectComparison bool
|
||||
type CompareRouterReq struct {
|
||||
BaseOriRef string
|
||||
HeadOwner string
|
||||
HeadRepoName string
|
||||
HeadOriRef string
|
||||
CaretTimes int // ^ times after base ref
|
||||
DotTimes int
|
||||
}
|
||||
|
||||
func (cr *CompareRouterReq) DirectComparison() bool {
|
||||
return cr.DotTimes == 2 || cr.DotTimes == 0
|
||||
}
|
||||
|
||||
func parseBase(base string) (string, int) {
|
||||
parts := strings.SplitN(base, "^", 2)
|
||||
if len(parts) == 1 {
|
||||
return base, 0
|
||||
}
|
||||
return parts[0], len(parts[1]) + 1
|
||||
}
|
||||
|
||||
func parseHead(head string) (string, string, string) {
|
||||
paths := strings.SplitN(head, ":", 2)
|
||||
if len(paths) == 1 {
|
||||
return "", "", paths[0]
|
||||
}
|
||||
ownerRepo := strings.SplitN(paths[0], "/", 2)
|
||||
if len(ownerRepo) == 1 {
|
||||
return paths[0], "", paths[1]
|
||||
}
|
||||
return ownerRepo[0], ownerRepo[1], paths[1]
|
||||
}
|
||||
|
||||
// ParseCompareRouterParam Get compare information from the router parameter.
|
||||
// A full compare url is of the form:
|
||||
//
|
||||
// 1. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headBranch}
|
||||
// 2. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}:{:headBranch}
|
||||
// 3. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}/{:headRepoName}:{:headBranch}
|
||||
// 4. /{:baseOwner}/{:baseRepoName}/compare/{:headBranch}
|
||||
// 5. /{:baseOwner}/{:baseRepoName}/compare/{:headOwner}:{:headBranch}
|
||||
// 6. /{:baseOwner}/{:baseRepoName}/compare/{:headOwner}/{:headRepoName}:{:headBranch}
|
||||
//
|
||||
// Here we obtain the infoPath "{:baseBranch}...[{:headOwner}/{:headRepoName}:]{:headBranch}" as ctx.PathParam("*")
|
||||
// with the :baseRepo in ctx.Repo.
|
||||
//
|
||||
// Note: Generally :headRepoName is not provided here - we are only passed :headOwner.
|
||||
//
|
||||
// How do we determine the :headRepo?
|
||||
//
|
||||
// 1. If :headOwner is not set then the :headRepo = :baseRepo
|
||||
// 2. If :headOwner is set - then look for the fork of :baseRepo owned by :headOwner
|
||||
// 3. But... :baseRepo could be a fork of :headOwner's repo - so check that
|
||||
// 4. Now, :baseRepo and :headRepos could be forks of the same repo - so check that
|
||||
//
|
||||
// format: <base branch>...[<head repo>:]<head branch>
|
||||
// base<-head: master...head:feature
|
||||
// same repo: master...feature
|
||||
func ParseCompareRouterParam(routerParam string) (*CompareRouterReq, error) {
|
||||
if routerParam == "" {
|
||||
return &CompareRouterReq{}, nil
|
||||
}
|
||||
|
||||
var basePart, headPart string
|
||||
dotTimes := 3
|
||||
parts := strings.Split(routerParam, "...")
|
||||
if len(parts) > 2 {
|
||||
return nil, util.NewInvalidArgumentErrorf("invalid compare router: %s", routerParam)
|
||||
}
|
||||
if len(parts) != 2 {
|
||||
parts = strings.Split(routerParam, "..")
|
||||
if len(parts) == 1 {
|
||||
headOwnerName, headRepoName, headRef := parseHead(routerParam)
|
||||
return &CompareRouterReq{
|
||||
HeadOriRef: headRef,
|
||||
HeadOwner: headOwnerName,
|
||||
HeadRepoName: headRepoName,
|
||||
DotTimes: dotTimes,
|
||||
}, nil
|
||||
} else if len(parts) > 2 {
|
||||
return nil, util.NewInvalidArgumentErrorf("invalid compare router: %s", routerParam)
|
||||
}
|
||||
dotTimes = 2
|
||||
}
|
||||
basePart, headPart = parts[0], parts[1]
|
||||
|
||||
baseRef, caretTimes := parseBase(basePart)
|
||||
headOwnerName, headRepoName, headRef := parseHead(headPart)
|
||||
|
||||
return &CompareRouterReq{
|
||||
BaseOriRef: baseRef,
|
||||
HeadOriRef: headRef,
|
||||
HeadOwner: headOwnerName,
|
||||
HeadRepoName: headRepoName,
|
||||
CaretTimes: caretTimes,
|
||||
DotTimes: dotTimes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// maxForkTraverseLevel defines the maximum levels to traverse when searching for the head repository.
|
||||
@ -45,6 +133,48 @@ func FindHeadRepo(ctx context.Context, baseRepo *repo_model.Repository, headUser
|
||||
return findHeadRepoFromRootBase(ctx, baseRepo, headUserID, maxForkTraverseLevel)
|
||||
}
|
||||
|
||||
func GetHeadOwnerAndRepo(ctx context.Context, baseRepo *repo_model.Repository, compareReq *CompareRouterReq) (headOwner *user_model.User, headRepo *repo_model.Repository, err error) {
|
||||
if compareReq.HeadOwner == "" {
|
||||
if compareReq.HeadRepoName != "" { // unsupported syntax
|
||||
return nil, nil, util.ErrorWrap(util.ErrInvalidArgument, "head owner must be specified when head repo name is given")
|
||||
}
|
||||
|
||||
return baseRepo.Owner, baseRepo, nil
|
||||
}
|
||||
|
||||
if compareReq.HeadOwner == baseRepo.Owner.Name {
|
||||
headOwner = baseRepo.Owner
|
||||
} else {
|
||||
headOwner, err = user_model.GetUserOrOrgByName(ctx, compareReq.HeadOwner)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
if compareReq.HeadRepoName == "" {
|
||||
if headOwner.ID == baseRepo.OwnerID {
|
||||
headRepo = baseRepo
|
||||
} else {
|
||||
headRepo, err = FindHeadRepo(ctx, baseRepo, headOwner.ID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if headRepo == nil {
|
||||
return nil, nil, util.ErrorWrap(util.ErrInvalidArgument, "the user %s does not have a fork of the base repository", headOwner.Name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if compareReq.HeadOwner == baseRepo.Owner.Name && compareReq.HeadRepoName == baseRepo.Name {
|
||||
headRepo = baseRepo
|
||||
} else {
|
||||
headRepo, err = repo_model.GetRepositoryByName(ctx, headOwner.ID, compareReq.HeadRepoName)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return headOwner, headRepo, nil
|
||||
}
|
||||
|
||||
func findHeadRepoFromRootBase(ctx context.Context, baseRepo *repo_model.Repository, headUserID int64, traverseLevel int) (*repo_model.Repository, error) {
|
||||
if traverseLevel == 0 {
|
||||
return nil, nil
|
||||
|
||||
151
routers/common/compare_test.go
Normal file
151
routers/common/compare_test.go
Normal file
@ -0,0 +1,151 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCompareRouterReq(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
|
||||
kases := []struct {
|
||||
router string
|
||||
CompareRouterReq *CompareRouterReq
|
||||
}{
|
||||
{
|
||||
router: "",
|
||||
CompareRouterReq: &CompareRouterReq{
|
||||
BaseOriRef: "",
|
||||
HeadOriRef: "",
|
||||
DotTimes: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
router: "main...develop",
|
||||
CompareRouterReq: &CompareRouterReq{
|
||||
BaseOriRef: "main",
|
||||
HeadOriRef: "develop",
|
||||
DotTimes: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
router: "main..develop",
|
||||
CompareRouterReq: &CompareRouterReq{
|
||||
BaseOriRef: "main",
|
||||
HeadOriRef: "develop",
|
||||
DotTimes: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
router: "main^...develop",
|
||||
CompareRouterReq: &CompareRouterReq{
|
||||
BaseOriRef: "main",
|
||||
HeadOriRef: "develop",
|
||||
CaretTimes: 1,
|
||||
DotTimes: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
router: "main^^^^^...develop",
|
||||
CompareRouterReq: &CompareRouterReq{
|
||||
BaseOriRef: "main",
|
||||
HeadOriRef: "develop",
|
||||
CaretTimes: 5,
|
||||
DotTimes: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
router: "develop",
|
||||
CompareRouterReq: &CompareRouterReq{
|
||||
HeadOriRef: "develop",
|
||||
DotTimes: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
router: "lunny/forked_repo:develop",
|
||||
CompareRouterReq: &CompareRouterReq{
|
||||
HeadOwner: "lunny",
|
||||
HeadRepoName: "forked_repo",
|
||||
HeadOriRef: "develop",
|
||||
DotTimes: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
router: "main...lunny/forked_repo:develop",
|
||||
CompareRouterReq: &CompareRouterReq{
|
||||
BaseOriRef: "main",
|
||||
HeadOwner: "lunny",
|
||||
HeadRepoName: "forked_repo",
|
||||
HeadOriRef: "develop",
|
||||
DotTimes: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
router: "main...lunny/forked_repo:develop",
|
||||
CompareRouterReq: &CompareRouterReq{
|
||||
BaseOriRef: "main",
|
||||
HeadOwner: "lunny",
|
||||
HeadRepoName: "forked_repo",
|
||||
HeadOriRef: "develop",
|
||||
DotTimes: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
router: "main^...lunny/forked_repo:develop",
|
||||
CompareRouterReq: &CompareRouterReq{
|
||||
BaseOriRef: "main",
|
||||
HeadOwner: "lunny",
|
||||
HeadRepoName: "forked_repo",
|
||||
HeadOriRef: "develop",
|
||||
DotTimes: 3,
|
||||
CaretTimes: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
router: "v1.0...v1.1",
|
||||
CompareRouterReq: &CompareRouterReq{
|
||||
BaseOriRef: "v1.0",
|
||||
HeadOriRef: "v1.1",
|
||||
DotTimes: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
router: "teabot-patch-1...v0.0.1",
|
||||
CompareRouterReq: &CompareRouterReq{
|
||||
BaseOriRef: "teabot-patch-1",
|
||||
HeadOriRef: "v0.0.1",
|
||||
DotTimes: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
router: "teabot:feature1",
|
||||
CompareRouterReq: &CompareRouterReq{
|
||||
HeadOwner: "teabot",
|
||||
HeadOriRef: "feature1",
|
||||
DotTimes: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
router: "8eb19a5ae19abae15c0666d4ab98906139a7f439...283c030497b455ecfa759d4649f9f8b45158742e",
|
||||
CompareRouterReq: &CompareRouterReq{
|
||||
BaseOriRef: "8eb19a5ae19abae15c0666d4ab98906139a7f439",
|
||||
HeadOriRef: "283c030497b455ecfa759d4649f9f8b45158742e",
|
||||
DotTimes: 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, kase := range kases {
|
||||
t.Run(kase.router, func(t *testing.T) {
|
||||
r, err := ParseCompareRouterParam(kase.router)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, kase.CompareRouterReq, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -102,7 +102,6 @@ func autoSignIn(ctx *context.Context) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
ctx.Csrf.PrepareForSessionUser(ctx)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@ -357,9 +356,6 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
|
||||
ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
|
||||
}
|
||||
|
||||
// force to generate a new CSRF token
|
||||
ctx.Csrf.PrepareForSessionUser(ctx)
|
||||
|
||||
// Register last login
|
||||
if err := user_service.UpdateUser(ctx, u, &user_service.UpdateOptions{SetLastLogin: true}); err != nil {
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
@ -403,7 +399,6 @@ func HandleSignOut(ctx *context.Context) {
|
||||
_ = ctx.Session.Flush()
|
||||
_ = ctx.Session.Destroy(ctx.Resp, ctx.Req)
|
||||
ctx.DeleteSiteCookie(setting.CookieRememberName)
|
||||
ctx.Csrf.DeleteCookie(ctx)
|
||||
middleware.DeleteRedirectToCookie(ctx.Resp)
|
||||
}
|
||||
|
||||
@ -811,8 +806,6 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Csrf.PrepareForSessionUser(ctx)
|
||||
|
||||
if err := resetLocale(ctx, user); err != nil {
|
||||
ctx.ServerError("resetLocale", err)
|
||||
return
|
||||
|
||||
@ -393,9 +393,6 @@ func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_m
|
||||
return
|
||||
}
|
||||
|
||||
// force to generate a new CSRF token
|
||||
ctx.Csrf.PrepareForSessionUser(ctx)
|
||||
|
||||
if err := resetLocale(ctx, u); err != nil {
|
||||
ctx.ServerError("resetLocale", err)
|
||||
return
|
||||
|
||||
@ -22,5 +22,5 @@ func addOwnerRepoGitHTTPRouters(m *web.Router) {
|
||||
m.Methods("GET,OPTIONS", "/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38,62}}", repo.GetLooseObject)
|
||||
m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.pack", repo.GetPackFile)
|
||||
m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile)
|
||||
}, optSignInIgnoreCsrf, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb())
|
||||
}, repo.HTTPGitEnabledHandler, repo.CorsHandler(), optSignInFromAnyOrigin, context.UserAssignmentWeb())
|
||||
}
|
||||
|
||||
@ -41,8 +41,8 @@ import (
|
||||
"code.gitea.io/gitea/routers/common"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/context/upload"
|
||||
git_service "code.gitea.io/gitea/services/git"
|
||||
"code.gitea.io/gitea/services/gitdiff"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
|
||||
@ -192,133 +192,64 @@ func setCsvCompareContext(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// ParseCompareInfo parse compare info between two commit for preparing comparing references
|
||||
func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
|
||||
func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo {
|
||||
baseRepo := ctx.Repo.Repository
|
||||
ci := &common.CompareInfo{}
|
||||
|
||||
fileOnly := ctx.FormBool("file-only")
|
||||
|
||||
// Get compared branches information
|
||||
// A full compare url is of the form:
|
||||
//
|
||||
// 1. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headBranch}
|
||||
// 2. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}:{:headBranch}
|
||||
// 3. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}/{:headRepoName}:{:headBranch}
|
||||
// 4. /{:baseOwner}/{:baseRepoName}/compare/{:headBranch}
|
||||
// 5. /{:baseOwner}/{:baseRepoName}/compare/{:headOwner}:{:headBranch}
|
||||
// 6. /{:baseOwner}/{:baseRepoName}/compare/{:headOwner}/{:headRepoName}:{:headBranch}
|
||||
//
|
||||
// Here we obtain the infoPath "{:baseBranch}...[{:headOwner}/{:headRepoName}:]{:headBranch}" as ctx.PathParam("*")
|
||||
// with the :baseRepo in ctx.Repo.
|
||||
//
|
||||
// Note: Generally :headRepoName is not provided here - we are only passed :headOwner.
|
||||
//
|
||||
// How do we determine the :headRepo?
|
||||
//
|
||||
// 1. If :headOwner is not set then the :headRepo = :baseRepo
|
||||
// 2. If :headOwner is set - then look for the fork of :baseRepo owned by :headOwner
|
||||
// 3. But... :baseRepo could be a fork of :headOwner's repo - so check that
|
||||
// 4. Now, :baseRepo and :headRepos could be forks of the same repo - so check that
|
||||
//
|
||||
// format: <base branch>...[<head repo>:]<head branch>
|
||||
// base<-head: master...head:feature
|
||||
// same repo: master...feature
|
||||
|
||||
var (
|
||||
isSameRepo bool
|
||||
infoPath string
|
||||
err error
|
||||
)
|
||||
|
||||
infoPath = ctx.PathParam("*")
|
||||
var infos []string
|
||||
if infoPath == "" {
|
||||
infos = []string{baseRepo.DefaultBranch, baseRepo.DefaultBranch}
|
||||
} else {
|
||||
infos = strings.SplitN(infoPath, "...", 2)
|
||||
if len(infos) != 2 {
|
||||
if infos = strings.SplitN(infoPath, "..", 2); len(infos) == 2 {
|
||||
ci.DirectComparison = true
|
||||
ctx.Data["PageIsComparePull"] = false
|
||||
} else {
|
||||
infos = []string{baseRepo.DefaultBranch, infoPath}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["BaseName"] = baseRepo.OwnerName
|
||||
ci.BaseBranch = infos[0]
|
||||
ctx.Data["BaseBranch"] = ci.BaseBranch
|
||||
|
||||
// If there is no head repository, it means compare between same repository.
|
||||
headInfos := strings.Split(infos[1], ":")
|
||||
if len(headInfos) == 1 {
|
||||
isSameRepo = true
|
||||
ci.HeadUser = ctx.Repo.Owner
|
||||
ci.HeadBranch = headInfos[0]
|
||||
} else if len(headInfos) == 2 {
|
||||
headInfosSplit := strings.Split(headInfos[0], "/")
|
||||
if len(headInfosSplit) == 1 {
|
||||
ci.HeadUser, err = user_model.GetUserOrOrgByName(ctx, headInfos[0])
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.NotFound(nil)
|
||||
} else {
|
||||
ctx.ServerError("GetUserByName", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
ci.HeadBranch = headInfos[1]
|
||||
isSameRepo = ci.HeadUser.ID == ctx.Repo.Owner.ID
|
||||
if isSameRepo {
|
||||
ci.HeadRepo = baseRepo
|
||||
}
|
||||
} else {
|
||||
ci.HeadRepo, err = repo_model.GetRepositoryByOwnerAndName(ctx, headInfosSplit[0], headInfosSplit[1])
|
||||
if err != nil {
|
||||
if repo_model.IsErrRepoNotExist(err) {
|
||||
ctx.NotFound(nil)
|
||||
} else {
|
||||
ctx.ServerError("GetRepositoryByOwnerAndName", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := ci.HeadRepo.LoadOwner(ctx); err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.NotFound(nil)
|
||||
} else {
|
||||
ctx.ServerError("GetUserByName", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
ci.HeadBranch = headInfos[1]
|
||||
ci.HeadUser = ci.HeadRepo.Owner
|
||||
isSameRepo = ci.HeadRepo.ID == ctx.Repo.Repository.ID
|
||||
}
|
||||
} else {
|
||||
ctx.NotFound(nil)
|
||||
compareReq, err := common.ParseCompareRouterParam(ctx.PathParam("*"))
|
||||
switch {
|
||||
case errors.Is(err, util.ErrInvalidArgument):
|
||||
ctx.HTTPError(http.StatusBadRequest, err.Error())
|
||||
return nil
|
||||
case err != nil:
|
||||
ctx.ServerError("ParseCompareRouterParam", err)
|
||||
return nil
|
||||
}
|
||||
ctx.Data["HeadUser"] = ci.HeadUser
|
||||
ctx.Data["HeadBranch"] = ci.HeadBranch
|
||||
// remove the check when we support compare with carets
|
||||
if compareReq.CaretTimes > 0 {
|
||||
ctx.HTTPError(http.StatusBadRequest, "Unsupported compare syntax with carets")
|
||||
return nil
|
||||
}
|
||||
|
||||
headOwner, headRepo, err := common.GetHeadOwnerAndRepo(ctx, baseRepo, compareReq)
|
||||
switch {
|
||||
case errors.Is(err, util.ErrInvalidArgument):
|
||||
ctx.HTTPError(http.StatusBadRequest, err.Error())
|
||||
return nil
|
||||
case errors.Is(err, util.ErrNotExist):
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
case err != nil:
|
||||
ctx.ServerError("GetHeadOwnerAndRepo", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
baseBranch := util.IfZero(compareReq.BaseOriRef, baseRepo.DefaultBranch)
|
||||
headBranch := util.IfZero(compareReq.HeadOriRef, headRepo.DefaultBranch)
|
||||
isSameRepo := baseRepo.ID == headRepo.ID
|
||||
|
||||
ctx.Data["BaseName"] = baseRepo.OwnerName
|
||||
ctx.Data["BaseBranch"] = baseBranch
|
||||
ctx.Data["HeadUser"] = headOwner
|
||||
ctx.Data["HeadBranch"] = headBranch
|
||||
ctx.Repo.PullRequest.SameRepo = isSameRepo
|
||||
|
||||
// Check if base branch is valid.
|
||||
baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(ci.BaseBranch)
|
||||
baseIsBranch, _ := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, ci.BaseBranch)
|
||||
baseIsTag := gitrepo.IsTagExist(ctx, ctx.Repo.Repository, ci.BaseBranch)
|
||||
baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(baseBranch)
|
||||
baseIsBranch, _ := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, baseBranch)
|
||||
baseIsTag := gitrepo.IsTagExist(ctx, ctx.Repo.Repository, baseBranch)
|
||||
|
||||
if !baseIsCommit && !baseIsBranch && !baseIsTag {
|
||||
// Check if baseBranch is short sha commit hash
|
||||
if baseCommit, _ := ctx.Repo.GitRepo.GetCommit(ci.BaseBranch); baseCommit != nil {
|
||||
ci.BaseBranch = baseCommit.ID.String()
|
||||
ctx.Data["BaseBranch"] = ci.BaseBranch
|
||||
if baseCommit, _ := ctx.Repo.GitRepo.GetCommit(baseBranch); baseCommit != nil {
|
||||
baseBranch = baseCommit.ID.String()
|
||||
ctx.Data["BaseBranch"] = baseBranch
|
||||
baseIsCommit = true
|
||||
} else if ci.BaseBranch == ctx.Repo.GetObjectFormat().EmptyObjectID().String() {
|
||||
} else if baseBranch == ctx.Repo.GetObjectFormat().EmptyObjectID().String() {
|
||||
if isSameRepo {
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ci.HeadBranch))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(headBranch))
|
||||
} else {
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ci.HeadRepo.FullName()) + ":" + util.PathEscapeSegments(ci.HeadBranch))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(headRepo.FullName()) + ":" + util.PathEscapeSegments(headBranch))
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
@ -368,31 +299,31 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
|
||||
}
|
||||
}
|
||||
|
||||
has := ci.HeadRepo != nil
|
||||
has := headRepo != nil
|
||||
// 3. If the base is a forked from "RootRepo" and the owner of
|
||||
// the "RootRepo" is the :headUser - set headRepo to that
|
||||
if !has && rootRepo != nil && rootRepo.OwnerID == ci.HeadUser.ID {
|
||||
ci.HeadRepo = rootRepo
|
||||
if !has && rootRepo != nil && rootRepo.OwnerID == headOwner.ID {
|
||||
headRepo = rootRepo
|
||||
has = true
|
||||
}
|
||||
|
||||
// 4. If the ctx.Doer has their own fork of the baseRepo and the headUser is the ctx.Doer
|
||||
// set the headRepo to the ownFork
|
||||
if !has && ownForkRepo != nil && ownForkRepo.OwnerID == ci.HeadUser.ID {
|
||||
ci.HeadRepo = ownForkRepo
|
||||
if !has && ownForkRepo != nil && ownForkRepo.OwnerID == headOwner.ID {
|
||||
headRepo = ownForkRepo
|
||||
has = true
|
||||
}
|
||||
|
||||
// 5. If the headOwner has a fork of the baseRepo - use that
|
||||
if !has {
|
||||
ci.HeadRepo = repo_model.GetForkedRepo(ctx, ci.HeadUser.ID, baseRepo.ID)
|
||||
has = ci.HeadRepo != nil
|
||||
headRepo = repo_model.GetForkedRepo(ctx, headOwner.ID, baseRepo.ID)
|
||||
has = headRepo != nil
|
||||
}
|
||||
|
||||
// 6. If the baseRepo is a fork and the headUser has a fork of that use that
|
||||
if !has && baseRepo.IsFork {
|
||||
ci.HeadRepo = repo_model.GetForkedRepo(ctx, ci.HeadUser.ID, baseRepo.ForkID)
|
||||
has = ci.HeadRepo != nil
|
||||
headRepo = repo_model.GetForkedRepo(ctx, headOwner.ID, baseRepo.ForkID)
|
||||
has = headRepo != nil
|
||||
}
|
||||
|
||||
// 7. Otherwise if we're not the same repo and haven't found a repo give up
|
||||
@ -401,11 +332,11 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
|
||||
}
|
||||
|
||||
// 8. Finally open the git repo
|
||||
var headGitRepo *git.Repository
|
||||
if isSameRepo {
|
||||
ci.HeadRepo = ctx.Repo.Repository
|
||||
ci.HeadGitRepo = ctx.Repo.GitRepo
|
||||
headGitRepo = ctx.Repo.GitRepo
|
||||
} else if has {
|
||||
ci.HeadGitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ci.HeadRepo)
|
||||
headGitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, headRepo)
|
||||
if err != nil {
|
||||
ctx.ServerError("RepositoryFromRequestContextOrOpen", err)
|
||||
return nil
|
||||
@ -415,7 +346,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx.Data["HeadRepo"] = ci.HeadRepo
|
||||
ctx.Data["HeadRepo"] = headRepo
|
||||
ctx.Data["BaseCompareRepo"] = ctx.Repo.Repository
|
||||
|
||||
// Now we need to assert that the ctx.Doer has permission to read
|
||||
@ -440,7 +371,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
|
||||
// If we're not merging from the same repo:
|
||||
if !isSameRepo {
|
||||
// Assert ctx.Doer has permission to read headRepo's codes
|
||||
permHead, err := access_model.GetUserRepoPermission(ctx, ci.HeadRepo, ctx.Doer)
|
||||
permHead, err := access_model.GetUserRepoPermission(ctx, headRepo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
return nil
|
||||
@ -449,7 +380,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
|
||||
if log.IsTrace() {
|
||||
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v",
|
||||
ctx.Doer,
|
||||
ci.HeadRepo,
|
||||
headRepo,
|
||||
permHead)
|
||||
}
|
||||
ctx.NotFound(nil)
|
||||
@ -463,7 +394,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
|
||||
// 2. the computed head
|
||||
// then get the branches of it
|
||||
if rootRepo != nil &&
|
||||
rootRepo.ID != ci.HeadRepo.ID &&
|
||||
rootRepo.ID != headRepo.ID &&
|
||||
rootRepo.ID != baseRepo.ID {
|
||||
canRead := access_model.CheckRepoUnitUser(ctx, rootRepo, ctx.Doer, unit.TypeCode)
|
||||
if canRead {
|
||||
@ -487,7 +418,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
|
||||
// 3. The rootRepo (if we have one)
|
||||
// then get the branches from it.
|
||||
if ownForkRepo != nil &&
|
||||
ownForkRepo.ID != ci.HeadRepo.ID &&
|
||||
ownForkRepo.ID != headRepo.ID &&
|
||||
ownForkRepo.ID != baseRepo.ID &&
|
||||
(rootRepo == nil || ownForkRepo.ID != rootRepo.ID) {
|
||||
canRead := access_model.CheckRepoUnitUser(ctx, ownForkRepo, ctx.Doer, unit.TypeCode)
|
||||
@ -506,14 +437,14 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
|
||||
}
|
||||
|
||||
// Check if head branch is valid.
|
||||
headIsCommit := ci.HeadGitRepo.IsCommitExist(ci.HeadBranch)
|
||||
headIsBranch, _ := git_model.IsBranchExist(ctx, ci.HeadRepo.ID, ci.HeadBranch)
|
||||
headIsTag := gitrepo.IsTagExist(ctx, ci.HeadRepo, ci.HeadBranch)
|
||||
headIsCommit := headGitRepo.IsCommitExist(headBranch)
|
||||
headIsBranch, _ := git_model.IsBranchExist(ctx, headRepo.ID, headBranch)
|
||||
headIsTag := gitrepo.IsTagExist(ctx, headRepo, headBranch)
|
||||
if !headIsCommit && !headIsBranch && !headIsTag {
|
||||
// Check if headBranch is short sha commit hash
|
||||
if headCommit, _ := ci.HeadGitRepo.GetCommit(ci.HeadBranch); headCommit != nil {
|
||||
ci.HeadBranch = headCommit.ID.String()
|
||||
ctx.Data["HeadBranch"] = ci.HeadBranch
|
||||
if headCommit, _ := headGitRepo.GetCommit(headBranch); headCommit != nil {
|
||||
headBranch = headCommit.ID.String()
|
||||
ctx.Data["HeadBranch"] = headBranch
|
||||
headIsCommit = true
|
||||
} else {
|
||||
ctx.NotFound(nil)
|
||||
@ -540,41 +471,41 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
|
||||
return nil
|
||||
}
|
||||
|
||||
baseBranchRef := ci.BaseBranch
|
||||
baseBranchRef := git.RefName(baseBranch)
|
||||
if baseIsBranch {
|
||||
baseBranchRef = git.BranchPrefix + ci.BaseBranch
|
||||
baseBranchRef = git.RefNameFromBranch(baseBranch)
|
||||
} else if baseIsTag {
|
||||
baseBranchRef = git.TagPrefix + ci.BaseBranch
|
||||
baseBranchRef = git.RefNameFromTag(baseBranch)
|
||||
}
|
||||
headBranchRef := ci.HeadBranch
|
||||
headBranchRef := git.RefName(headBranch)
|
||||
if headIsBranch {
|
||||
headBranchRef = git.BranchPrefix + ci.HeadBranch
|
||||
headBranchRef = git.RefNameFromBranch(headBranch)
|
||||
} else if headIsTag {
|
||||
headBranchRef = git.TagPrefix + ci.HeadBranch
|
||||
headBranchRef = git.RefNameFromTag(headBranch)
|
||||
}
|
||||
|
||||
ci.CompareInfo, err = pull_service.GetCompareInfo(ctx, baseRepo, ci.HeadRepo, ci.HeadGitRepo, baseBranchRef, headBranchRef, ci.DirectComparison, fileOnly)
|
||||
compareInfo, err := git_service.GetCompareInfo(ctx, baseRepo, headRepo, headGitRepo, baseBranchRef, headBranchRef, compareReq.DirectComparison(), fileOnly)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetCompareInfo", err)
|
||||
return nil
|
||||
}
|
||||
if ci.DirectComparison {
|
||||
ctx.Data["BeforeCommitID"] = ci.CompareInfo.BaseCommitID
|
||||
if compareReq.DirectComparison() {
|
||||
ctx.Data["BeforeCommitID"] = compareInfo.BaseCommitID
|
||||
} else {
|
||||
ctx.Data["BeforeCommitID"] = ci.CompareInfo.MergeBase
|
||||
ctx.Data["BeforeCommitID"] = compareInfo.MergeBase
|
||||
}
|
||||
|
||||
return ci
|
||||
return compareInfo
|
||||
}
|
||||
|
||||
// PrepareCompareDiff renders compare diff page
|
||||
func PrepareCompareDiff(
|
||||
ctx *context.Context,
|
||||
ci *common.CompareInfo,
|
||||
ci *git_service.CompareInfo,
|
||||
whitespaceBehavior gitcmd.TrustedCmdArgs,
|
||||
) (nothingToCompare bool) {
|
||||
repo := ctx.Repo.Repository
|
||||
headCommitID := ci.CompareInfo.HeadCommitID
|
||||
headCommitID := ci.HeadCommitID
|
||||
|
||||
ctx.Data["CommitRepoLink"] = ci.HeadRepo.Link()
|
||||
ctx.Data["AfterCommitID"] = headCommitID
|
||||
@ -586,17 +517,15 @@ func PrepareCompareDiff(
|
||||
ctx.Data["TitleQuery"] = newPrFormTitle
|
||||
ctx.Data["BodyQuery"] = newPrFormBody
|
||||
|
||||
if (headCommitID == ci.CompareInfo.MergeBase && !ci.DirectComparison) ||
|
||||
headCommitID == ci.CompareInfo.BaseCommitID {
|
||||
if (headCommitID == ci.MergeBase && !ci.DirectComparison) ||
|
||||
headCommitID == ci.BaseCommitID {
|
||||
ctx.Data["IsNothingToCompare"] = true
|
||||
if unit, err := repo.GetUnit(ctx, unit.TypePullRequests); err == nil {
|
||||
config := unit.PullRequestsConfig()
|
||||
|
||||
if !config.AutodetectManualMerge {
|
||||
allowEmptyPr := !(ci.BaseBranch == ci.HeadBranch && ctx.Repo.Repository.Name == ci.HeadRepo.Name)
|
||||
ctx.Data["AllowEmptyPr"] = allowEmptyPr
|
||||
|
||||
return !allowEmptyPr
|
||||
ctx.Data["AllowEmptyPr"] = !ci.IsSameRef()
|
||||
return ci.IsSameRef()
|
||||
}
|
||||
|
||||
ctx.Data["AllowEmptyPr"] = false
|
||||
@ -604,9 +533,9 @@ func PrepareCompareDiff(
|
||||
return true
|
||||
}
|
||||
|
||||
beforeCommitID := ci.CompareInfo.MergeBase
|
||||
beforeCommitID := ci.MergeBase
|
||||
if ci.DirectComparison {
|
||||
beforeCommitID = ci.CompareInfo.BaseCommitID
|
||||
beforeCommitID = ci.BaseCommitID
|
||||
}
|
||||
|
||||
maxLines, maxFiles := setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles
|
||||
@ -674,7 +603,7 @@ func PrepareCompareDiff(
|
||||
return false
|
||||
}
|
||||
|
||||
commits, err := processGitCommits(ctx, ci.CompareInfo.Commits)
|
||||
commits, err := processGitCommits(ctx, ci.Commits)
|
||||
if err != nil {
|
||||
ctx.ServerError("processGitCommits", err)
|
||||
return false
|
||||
@ -682,7 +611,7 @@ func PrepareCompareDiff(
|
||||
ctx.Data["Commits"] = commits
|
||||
ctx.Data["CommitCount"] = len(commits)
|
||||
|
||||
title := ci.HeadBranch
|
||||
title := ci.HeadRef.ShortName()
|
||||
if len(commits) == 1 {
|
||||
c := commits[0]
|
||||
title = strings.TrimSpace(c.UserCommit.Summary())
|
||||
@ -706,10 +635,10 @@ func PrepareCompareDiff(
|
||||
}
|
||||
|
||||
ctx.Data["title"] = title
|
||||
ctx.Data["Username"] = ci.HeadUser.Name
|
||||
ctx.Data["Username"] = ci.HeadRepo.OwnerName
|
||||
ctx.Data["Reponame"] = ci.HeadRepo.Name
|
||||
|
||||
setCompareContext(ctx, beforeCommit, headCommit, ci.HeadUser.Name, repo.Name)
|
||||
setCompareContext(ctx, beforeCommit, headCommit, ci.HeadRepo.OwnerName, repo.Name)
|
||||
|
||||
return false
|
||||
}
|
||||
@ -790,7 +719,7 @@ func CompareDiff(ctx *context.Context) {
|
||||
ctx.Data["HeadTags"] = headTags
|
||||
|
||||
if ctx.Data["PageIsComparePull"] == true {
|
||||
pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, ctx.Repo.Repository.ID, ci.HeadBranch, ci.BaseBranch, issues_model.PullRequestFlowGithub)
|
||||
pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, ctx.Repo.Repository.ID, ci.HeadRef.ShortName(), ci.BaseRef.ShortName(), issues_model.PullRequestFlowGithub)
|
||||
if err != nil {
|
||||
if !issues_model.IsErrPullRequestNotExist(err) {
|
||||
ctx.ServerError("GetUnmergedPullRequest", err)
|
||||
|
||||
@ -44,6 +44,7 @@ import (
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/context/upload"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
git_service "code.gitea.io/gitea/services/git"
|
||||
"code.gitea.io/gitea/services/gitdiff"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
@ -256,7 +257,7 @@ func GetMergedBaseCommitID(ctx *context.Context, issue *issues_model.Issue) stri
|
||||
return baseCommit
|
||||
}
|
||||
|
||||
func preparePullViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_service.CompareInfo {
|
||||
func preparePullViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git_service.CompareInfo {
|
||||
if !issue.IsPull {
|
||||
return nil
|
||||
}
|
||||
@ -267,7 +268,7 @@ func preparePullViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *p
|
||||
}
|
||||
|
||||
// prepareMergedViewPullInfo show meta information for a merged pull request view page
|
||||
func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_service.CompareInfo {
|
||||
func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git_service.CompareInfo {
|
||||
pull := issue.PullRequest
|
||||
|
||||
setMergeTarget(ctx, pull)
|
||||
@ -275,8 +276,8 @@ func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue)
|
||||
|
||||
baseCommit := GetMergedBaseCommitID(ctx, issue)
|
||||
|
||||
compareInfo, err := pull_service.GetCompareInfo(ctx, ctx.Repo.Repository, ctx.Repo.Repository, ctx.Repo.GitRepo,
|
||||
baseCommit, pull.GetGitHeadRefName(), false, false)
|
||||
compareInfo, err := git_service.GetCompareInfo(ctx, ctx.Repo.Repository, ctx.Repo.Repository, ctx.Repo.GitRepo,
|
||||
git.RefName(baseCommit), git.RefName(pull.GetGitHeadRefName()), false, false)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "fatal: Not a valid object name") || strings.Contains(err.Error(), "unknown revision or path not in the working tree") {
|
||||
ctx.Data["IsPullRequestBroken"] = true
|
||||
@ -321,7 +322,7 @@ type pullCommitStatusCheckData struct {
|
||||
}
|
||||
|
||||
// prepareViewPullInfo show meta information for a pull request preview page
|
||||
func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_service.CompareInfo {
|
||||
func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git_service.CompareInfo {
|
||||
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
@ -383,8 +384,8 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
|
||||
ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(commitStatuses)
|
||||
}
|
||||
|
||||
compareInfo, err := pull_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo,
|
||||
pull.MergeBase, pull.GetGitHeadRefName(), false, false)
|
||||
compareInfo, err := git_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo,
|
||||
git.RefName(pull.MergeBase), git.RefName(pull.GetGitHeadRefName()), false, false)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "fatal: Not a valid object name") {
|
||||
ctx.Data["IsPullRequestBroken"] = true
|
||||
@ -550,8 +551,8 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
|
||||
}
|
||||
}
|
||||
|
||||
compareInfo, err := pull_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo,
|
||||
git.BranchPrefix+pull.BaseBranch, pull.GetGitHeadRefName(), false, false)
|
||||
compareInfo, err := git_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo,
|
||||
git.RefNameFromBranch(pull.BaseBranch), git.RefName(pull.GetGitHeadRefName()), false, false)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "fatal: Not a valid object name") {
|
||||
ctx.Data["IsPullRequestBroken"] = true
|
||||
@ -1341,7 +1342,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// Check if a pull request already exists with the same head and base branch.
|
||||
pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, repo.ID, ci.HeadBranch, ci.BaseBranch, issues_model.PullRequestFlowGithub)
|
||||
pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, repo.ID, ci.HeadRef.ShortName(), ci.BaseRef.ShortName(), issues_model.PullRequestFlowGithub)
|
||||
if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
|
||||
ctx.ServerError("GetUnmergedPullRequest", err)
|
||||
return
|
||||
@ -1371,11 +1372,11 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
||||
pullRequest := &issues_model.PullRequest{
|
||||
HeadRepoID: ci.HeadRepo.ID,
|
||||
BaseRepoID: repo.ID,
|
||||
HeadBranch: ci.HeadBranch,
|
||||
BaseBranch: ci.BaseBranch,
|
||||
HeadBranch: ci.HeadRef.ShortName(),
|
||||
BaseBranch: ci.BaseRef.ShortName(),
|
||||
HeadRepo: ci.HeadRepo,
|
||||
BaseRepo: repo,
|
||||
MergeBase: ci.CompareInfo.MergeBase,
|
||||
MergeBase: ci.MergeBase,
|
||||
Type: issues_model.PullRequestGitea,
|
||||
AllowMaintainerEdit: form.AllowMaintainerEdit,
|
||||
}
|
||||
|
||||
@ -325,7 +325,7 @@ func loadKeysData(ctx *context.Context) {
|
||||
ctx.Data["GPGKeys"] = gpgkeys
|
||||
tokenToSign := asymkey_model.VerificationToken(ctx.Doer, 1)
|
||||
|
||||
// generate a new aes cipher using the csrfToken
|
||||
// generate a new aes cipher using the token
|
||||
ctx.Data["TokenToSign"] = tokenToSign
|
||||
|
||||
principals, err := db.Find[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{
|
||||
|
||||
@ -129,13 +129,13 @@ func webAuth(authMethod auth_service.Method) func(*context.Context) {
|
||||
// ensure the session uid is deleted
|
||||
_ = ctx.Session.Delete("uid")
|
||||
}
|
||||
|
||||
ctx.Csrf.PrepareForSessionUser(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// verifyAuthWithOptions checks authentication according to options
|
||||
func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Context) {
|
||||
crossOriginProtection := http.NewCrossOriginProtection()
|
||||
|
||||
return func(ctx *context.Context) {
|
||||
// Check prohibit login users.
|
||||
if ctx.IsSigned {
|
||||
@ -178,9 +178,9 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Cont
|
||||
return
|
||||
}
|
||||
|
||||
if !options.SignOutRequired && !options.DisableCSRF && ctx.Req.Method == http.MethodPost {
|
||||
ctx.Csrf.Validate(ctx)
|
||||
if ctx.Written() {
|
||||
if !options.SignOutRequired && !options.DisableCrossOriginProtection {
|
||||
if err := crossOriginProtection.Check(ctx.Req); err != nil {
|
||||
http.Error(ctx.Resp, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -292,7 +292,12 @@ func Routes() *web.Router {
|
||||
return routes
|
||||
}
|
||||
|
||||
var optSignInIgnoreCsrf = verifyAuthWithOptions(&common.VerifyOptions{DisableCSRF: true})
|
||||
// optSignInFromAnyOrigin means that the user can (optionally) be signed in from any origin (no cross-origin protection)
|
||||
// - With CORS middleware: CORS middleware does the preflight request handling, the requests has Sec-Fetch-Site header.
|
||||
// The CORS mechanism already protects cross-origin requests, and the CrossOriginProtection has no "allowed origin" list, so disable CrossOriginProtection.
|
||||
// - For non-browser client requests: git clone via http, no Sec-Fetch-Site header.
|
||||
// Such requests are not cross-origin requests, so disable CrossOriginProtection.
|
||||
var optSignInFromAnyOrigin = verifyAuthWithOptions(&common.VerifyOptions{DisableCrossOriginProtection: true})
|
||||
|
||||
// registerWebRoutes register routes
|
||||
func registerWebRoutes(m *web.Router) {
|
||||
@ -489,7 +494,7 @@ func registerWebRoutes(m *web.Router) {
|
||||
m.Post("/-/markup", reqSignIn, web.Bind(structs.MarkupOption{}), misc.Markup)
|
||||
|
||||
m.Get("/-/web-theme/list", misc.WebThemeList)
|
||||
m.Post("/-/web-theme/apply", optSignInIgnoreCsrf, misc.WebThemeApply)
|
||||
m.Post("/-/web-theme/apply", optSignIn, misc.WebThemeApply)
|
||||
|
||||
m.Group("/explore", func() {
|
||||
m.Get("", func(ctx *context.Context) {
|
||||
@ -565,12 +570,14 @@ func registerWebRoutes(m *web.Router) {
|
||||
m.Post("/grant", web.Bind(forms.GrantApplicationForm{}), auth.GrantApplicationOAuth)
|
||||
// TODO manage redirection
|
||||
m.Post("/authorize", web.Bind(forms.AuthorizationForm{}), auth.AuthorizeOAuth)
|
||||
}, optSignInIgnoreCsrf, reqSignIn)
|
||||
}, reqSignIn)
|
||||
|
||||
m.Methods("GET, POST, OPTIONS", "/userinfo", optionsCorsHandler(), optSignInIgnoreCsrf, auth.InfoOAuth)
|
||||
m.Methods("POST, OPTIONS", "/access_token", optionsCorsHandler(), web.Bind(forms.AccessTokenForm{}), optSignInIgnoreCsrf, auth.AccessTokenOAuth)
|
||||
m.Methods("GET, OPTIONS", "/keys", optionsCorsHandler(), optSignInIgnoreCsrf, auth.OIDCKeys)
|
||||
m.Methods("POST, OPTIONS", "/introspect", optionsCorsHandler(), web.Bind(forms.IntrospectTokenForm{}), optSignInIgnoreCsrf, auth.IntrospectOAuth)
|
||||
m.Group("", func() {
|
||||
m.Methods("GET, POST, OPTIONS", "/userinfo", auth.InfoOAuth)
|
||||
m.Methods("POST, OPTIONS", "/access_token", web.Bind(forms.AccessTokenForm{}), auth.AccessTokenOAuth)
|
||||
m.Methods("GET, OPTIONS", "/keys", auth.OIDCKeys)
|
||||
m.Methods("POST, OPTIONS", "/introspect", web.Bind(forms.IntrospectTokenForm{}), auth.IntrospectOAuth)
|
||||
}, optionsCorsHandler(), optSignInFromAnyOrigin)
|
||||
}, oauth2Enabled)
|
||||
|
||||
m.Group("/user/settings", func() {
|
||||
@ -1655,7 +1662,7 @@ func registerWebRoutes(m *web.Router) {
|
||||
m.Post("/action/{action:accept_transfer|reject_transfer}", reqSignIn, repo.ActionTransfer)
|
||||
}, optSignIn, context.RepoAssignment)
|
||||
|
||||
common.AddOwnerRepoGitLFSRoutes(m, optSignInIgnoreCsrf, lfsServerEnabled) // "/{username}/{reponame}/{lfs-paths}": git-lfs support
|
||||
common.AddOwnerRepoGitLFSRoutes(m, lfsServerEnabled, repo.CorsHandler(), optSignInFromAnyOrigin) // "/{username}/{reponame}/{lfs-paths}": git-lfs support, see also addOwnerRepoGitHTTPRouters
|
||||
|
||||
addOwnerRepoGitHTTPRouters(m) // "/{username}/{reponame}/{git-paths}": git http support
|
||||
|
||||
|
||||
@ -19,7 +19,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/session"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
gitea_context "code.gitea.io/gitea/services/context"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
|
||||
@ -162,9 +161,4 @@ func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore
|
||||
}
|
||||
|
||||
middleware.SetLocaleCookie(resp, user.Language, 0)
|
||||
|
||||
// force to generate a new CSRF token
|
||||
if ctx := gitea_context.GetWebContext(req.Context()); ctx != nil {
|
||||
ctx.Csrf.PrepareForSessionUser(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,7 +227,7 @@ func APIContexter() func(http.Handler) http.Handler {
|
||||
|
||||
ctx.SetContextValue(apiContextKey, ctx)
|
||||
|
||||
// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
|
||||
// FIXME: GLOBAL-PARSE-FORM: see more details in another FIXME comment
|
||||
if ctx.Req.Method == http.MethodPost && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") {
|
||||
if !ctx.ParseMultipartForm() {
|
||||
return
|
||||
|
||||
@ -6,14 +6,12 @@ package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
@ -48,7 +46,6 @@ type Context struct {
|
||||
PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData`
|
||||
|
||||
Cache cache.StringCache
|
||||
Csrf CSRFProtector
|
||||
Flash *middleware.Flash
|
||||
Session session.Store
|
||||
|
||||
@ -143,18 +140,6 @@ func NewWebContext(base *Base, render Render, session session.Store) *Context {
|
||||
// Contexter initializes a classic context for a request.
|
||||
func Contexter() func(next http.Handler) http.Handler {
|
||||
rnd := templates.HTMLRenderer()
|
||||
csrfOpts := CsrfOptions{
|
||||
Secret: hex.EncodeToString(setting.GetGeneralTokenSigningSecret()),
|
||||
Cookie: setting.CSRFCookieName,
|
||||
Secure: setting.SessionConfig.Secure,
|
||||
CookieHTTPOnly: setting.CSRFCookieHTTPOnly,
|
||||
CookieDomain: setting.SessionConfig.Domain,
|
||||
CookiePath: setting.SessionConfig.CookiePath,
|
||||
SameSite: setting.SessionConfig.SameSite,
|
||||
}
|
||||
if !setting.IsProd {
|
||||
CsrfTokenRegenerationInterval = 5 * time.Second // in dev, re-generate the tokens more aggressively for debug purpose
|
||||
}
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
base := NewBaseContext(resp, req)
|
||||
@ -167,8 +152,6 @@ func Contexter() func(next http.Handler) http.Handler {
|
||||
ctx.PageData = map[string]any{}
|
||||
ctx.Data["PageData"] = ctx.PageData
|
||||
|
||||
ctx.Csrf = NewCSRFProtector(csrfOpts)
|
||||
|
||||
// get the last flash message from cookie
|
||||
lastFlashCookie, lastFlashMsg := middleware.GetSiteCookieFlashMessage(ctx, ctx.Req, CookieNameFlash)
|
||||
if vals, _ := url.ParseQuery(lastFlashCookie); len(vals) > 0 {
|
||||
@ -184,7 +167,10 @@ func Contexter() func(next http.Handler) http.Handler {
|
||||
}
|
||||
})
|
||||
|
||||
// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
|
||||
// FIXME: GLOBAL-PARSE-FORM: this ParseMultipartForm was used for parsing the csrf token from multipart/form-data
|
||||
// We have dropped the csrf token, so ideally this global ParseMultipartForm should be removed.
|
||||
// When removing this, we need to avoid regressions in the handler functions because Golang's http framework is quite fragile
|
||||
// and developers sometimes need to manually parse the form before accessing some values.
|
||||
if ctx.Req.Method == http.MethodPost && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") {
|
||||
if !ctx.ParseMultipartForm() {
|
||||
return
|
||||
|
||||
@ -25,13 +25,11 @@ func removeSessionCookieHeader(w http.ResponseWriter) {
|
||||
}
|
||||
|
||||
// SetSiteCookie convenience function to set most cookies consistently
|
||||
// CSRF and a few others are the exception here
|
||||
func (ctx *Context) SetSiteCookie(name, value string, maxAge int) {
|
||||
middleware.SetSiteCookie(ctx.Resp, name, value, maxAge)
|
||||
}
|
||||
|
||||
// DeleteSiteCookie convenience function to delete most cookies consistently
|
||||
// CSRF and a few others are the exception here
|
||||
func (ctx *Context) DeleteSiteCookie(name string) {
|
||||
middleware.SetSiteCookie(ctx.Resp, name, "", -1)
|
||||
}
|
||||
|
||||
@ -1,168 +0,0 @@
|
||||
// Copyright 2013 Martini Authors
|
||||
// Copyright 2014 The Macaron Authors
|
||||
// Copyright 2021 The Gitea Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// a middleware that generates and validates CSRF tokens.
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
const (
|
||||
CsrfHeaderName = "X-Csrf-Token"
|
||||
CsrfFormName = "_csrf"
|
||||
)
|
||||
|
||||
// CSRFProtector represents a CSRF protector and is used to get the current token and validate the token.
|
||||
type CSRFProtector interface {
|
||||
// PrepareForSessionUser prepares the csrf protector for the current session user.
|
||||
PrepareForSessionUser(ctx *Context)
|
||||
// Validate validates the csrf token in http context.
|
||||
Validate(ctx *Context)
|
||||
// DeleteCookie deletes the csrf cookie
|
||||
DeleteCookie(ctx *Context)
|
||||
}
|
||||
|
||||
type csrfProtector struct {
|
||||
opt CsrfOptions
|
||||
// id must be unique per user.
|
||||
id string
|
||||
// token is the valid one which will be used by end user and passed via header, cookie, or hidden form value.
|
||||
token string
|
||||
}
|
||||
|
||||
// CsrfOptions maintains options to manage behavior of Generate.
|
||||
type CsrfOptions struct {
|
||||
// The global secret value used to generate Tokens.
|
||||
Secret string
|
||||
// Cookie value used to set and get token.
|
||||
Cookie string
|
||||
// Cookie domain.
|
||||
CookieDomain string
|
||||
// Cookie path.
|
||||
CookiePath string
|
||||
CookieHTTPOnly bool
|
||||
// SameSite set the cookie SameSite type
|
||||
SameSite http.SameSite
|
||||
// Set the Secure flag to true on the cookie.
|
||||
Secure bool
|
||||
// sessionKey is the key used for getting the unique ID per user.
|
||||
sessionKey string
|
||||
// oldSessionKey saves old value corresponding to sessionKey.
|
||||
oldSessionKey string
|
||||
}
|
||||
|
||||
func newCsrfCookie(opt *CsrfOptions, value string) *http.Cookie {
|
||||
return &http.Cookie{
|
||||
Name: opt.Cookie,
|
||||
Value: value,
|
||||
Path: opt.CookiePath,
|
||||
Domain: opt.CookieDomain,
|
||||
MaxAge: int(CsrfTokenTimeout.Seconds()),
|
||||
Secure: opt.Secure,
|
||||
HttpOnly: opt.CookieHTTPOnly,
|
||||
SameSite: opt.SameSite,
|
||||
}
|
||||
}
|
||||
|
||||
func NewCSRFProtector(opt CsrfOptions) CSRFProtector {
|
||||
if opt.Secret == "" {
|
||||
panic("CSRF secret is empty but it must be set") // it shouldn't happen because it is always set in code
|
||||
}
|
||||
opt.Cookie = util.IfZero(opt.Cookie, "_csrf")
|
||||
opt.CookiePath = util.IfZero(opt.CookiePath, "/")
|
||||
opt.sessionKey = "uid"
|
||||
opt.oldSessionKey = "_old_" + opt.sessionKey
|
||||
return &csrfProtector{opt: opt}
|
||||
}
|
||||
|
||||
func (c *csrfProtector) PrepareForSessionUser(ctx *Context) {
|
||||
c.id = "0"
|
||||
if uidAny := ctx.Session.Get(c.opt.sessionKey); uidAny != nil {
|
||||
switch uidVal := uidAny.(type) {
|
||||
case string:
|
||||
c.id = uidVal
|
||||
case int64:
|
||||
c.id = strconv.FormatInt(uidVal, 10)
|
||||
default:
|
||||
log.Error("invalid uid type in session: %T", uidAny)
|
||||
}
|
||||
}
|
||||
|
||||
oldUID := ctx.Session.Get(c.opt.oldSessionKey)
|
||||
uidChanged := oldUID == nil || oldUID.(string) != c.id
|
||||
cookieToken := ctx.GetSiteCookie(c.opt.Cookie)
|
||||
|
||||
needsNew := true
|
||||
if uidChanged {
|
||||
_ = ctx.Session.Set(c.opt.oldSessionKey, c.id)
|
||||
} else if cookieToken != "" {
|
||||
// If cookie token present, re-use existing unexpired token, else generate a new one.
|
||||
if issueTime, ok := ParseCsrfToken(cookieToken); ok {
|
||||
dur := time.Since(issueTime) // issueTime is not a monotonic-clock, the server time may change a lot to an early time.
|
||||
if dur >= -CsrfTokenRegenerationInterval && dur <= CsrfTokenRegenerationInterval {
|
||||
c.token = cookieToken
|
||||
needsNew = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if needsNew {
|
||||
c.token = GenerateCsrfToken(c.opt.Secret, c.id, "POST", time.Now())
|
||||
ctx.Resp.Header().Add("Set-Cookie", newCsrfCookie(&c.opt, c.token).String())
|
||||
}
|
||||
|
||||
ctx.Data["CsrfToken"] = c.token
|
||||
ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + template.HTMLEscapeString(c.token) + `">`)
|
||||
}
|
||||
|
||||
func (c *csrfProtector) validateToken(ctx *Context, token string) {
|
||||
if !ValidCsrfToken(token, c.opt.Secret, c.id, "POST", time.Now()) {
|
||||
c.DeleteCookie(ctx)
|
||||
// currently, there should be no access to the APIPath with CSRF token. because templates shouldn't use the `/api/` endpoints.
|
||||
// FIXME: distinguish what the response is for: HTML (web page) or JSON (fetch)
|
||||
http.Error(ctx.Resp, "Invalid CSRF token.", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate should be used as a per route middleware. It attempts to get a token from an "X-Csrf-Token"
|
||||
// HTTP header and then a "_csrf" form value. If one of these is found, the token will be validated.
|
||||
// If this validation fails, http.StatusBadRequest is sent.
|
||||
func (c *csrfProtector) Validate(ctx *Context) {
|
||||
if token := ctx.Req.Header.Get(CsrfHeaderName); token != "" {
|
||||
c.validateToken(ctx, token)
|
||||
return
|
||||
}
|
||||
if token := ctx.Req.FormValue(CsrfFormName); token != "" {
|
||||
c.validateToken(ctx, token)
|
||||
return
|
||||
}
|
||||
c.validateToken(ctx, "") // no csrf token, use an empty token to respond error
|
||||
}
|
||||
|
||||
func (c *csrfProtector) DeleteCookie(ctx *Context) {
|
||||
cookie := newCsrfCookie(&c.opt, "")
|
||||
cookie.MaxAge = -1
|
||||
ctx.Resp.Header().Add("Set-Cookie", cookie.String())
|
||||
}
|
||||
@ -1,99 +0,0 @@
|
||||
// Copyright 2012 Google Inc. All Rights Reserved.
|
||||
// Copyright 2014 The Macaron Authors
|
||||
// Copyright 2020 The Gitea Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CsrfTokenTimeout represents the duration that XSRF tokens are valid.
|
||||
// It is exported so clients may set cookie timeouts that match generated tokens.
|
||||
const CsrfTokenTimeout = 24 * time.Hour
|
||||
|
||||
// CsrfTokenRegenerationInterval is the interval between token generations, old tokens are still valid before CsrfTokenTimeout
|
||||
var CsrfTokenRegenerationInterval = 10 * time.Minute
|
||||
|
||||
var csrfTokenSep = []byte(":")
|
||||
|
||||
// GenerateCsrfToken returns a URL-safe secure XSRF token that expires in CsrfTokenTimeout hours.
|
||||
// key is a secret key for your application.
|
||||
// userID is a unique identifier for the user.
|
||||
// actionID is the action the user is taking (e.g. POSTing to a particular path).
|
||||
func GenerateCsrfToken(key, userID, actionID string, now time.Time) string {
|
||||
nowUnixNano := now.UnixNano()
|
||||
nowUnixNanoStr := strconv.FormatInt(nowUnixNano, 10)
|
||||
h := hmac.New(sha1.New, []byte(key))
|
||||
h.Write([]byte(strings.ReplaceAll(userID, ":", "_")))
|
||||
h.Write(csrfTokenSep)
|
||||
h.Write([]byte(strings.ReplaceAll(actionID, ":", "_")))
|
||||
h.Write(csrfTokenSep)
|
||||
h.Write([]byte(nowUnixNanoStr))
|
||||
tok := fmt.Sprintf("%s:%s", h.Sum(nil), nowUnixNanoStr)
|
||||
return base64.RawURLEncoding.EncodeToString([]byte(tok))
|
||||
}
|
||||
|
||||
func ParseCsrfToken(token string) (issueTime time.Time, ok bool) {
|
||||
data, err := base64.RawURLEncoding.DecodeString(token)
|
||||
if err != nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
pos := bytes.LastIndex(data, csrfTokenSep)
|
||||
if pos == -1 {
|
||||
return time.Time{}, false
|
||||
}
|
||||
nanos, err := strconv.ParseInt(string(data[pos+1:]), 10, 64)
|
||||
if err != nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
return time.Unix(0, nanos), true
|
||||
}
|
||||
|
||||
// ValidCsrfToken returns true if token is a valid and unexpired token returned by Generate.
|
||||
func ValidCsrfToken(token, key, userID, actionID string, now time.Time) bool {
|
||||
issueTime, ok := ParseCsrfToken(token)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check that the token is not expired.
|
||||
if now.Sub(issueTime) >= CsrfTokenTimeout {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check that the token is not from the future.
|
||||
// Allow 1-minute grace period in case the token is being verified on a
|
||||
// machine whose clock is behind the machine that issued the token.
|
||||
if issueTime.After(now.Add(1 * time.Minute)) {
|
||||
return false
|
||||
}
|
||||
|
||||
expected := GenerateCsrfToken(key, userID, actionID, issueTime)
|
||||
|
||||
// Check that the token matches the expected value.
|
||||
// Use constant time comparison to avoid timing attacks.
|
||||
return subtle.ConstantTimeCompare([]byte(token), []byte(expected)) == 1
|
||||
}
|
||||
@ -1,91 +0,0 @@
|
||||
// Copyright 2012 Google Inc. All Rights Reserved.
|
||||
// Copyright 2014 The Macaron Authors
|
||||
// Copyright 2020 The Gitea Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
key = "quay"
|
||||
userID = "12345678"
|
||||
actionID = "POST /form"
|
||||
)
|
||||
|
||||
var (
|
||||
now = time.Now()
|
||||
oneMinuteFromNow = now.Add(1 * time.Minute)
|
||||
)
|
||||
|
||||
func Test_ValidToken(t *testing.T) {
|
||||
t.Run("Validate token", func(t *testing.T) {
|
||||
tok := GenerateCsrfToken(key, userID, actionID, now)
|
||||
assert.True(t, ValidCsrfToken(tok, key, userID, actionID, oneMinuteFromNow))
|
||||
assert.True(t, ValidCsrfToken(tok, key, userID, actionID, now.Add(CsrfTokenTimeout-1*time.Nanosecond)))
|
||||
assert.True(t, ValidCsrfToken(tok, key, userID, actionID, now.Add(-1*time.Minute)))
|
||||
})
|
||||
}
|
||||
|
||||
// Test_SeparatorReplacement tests that separators are being correctly substituted
|
||||
func Test_SeparatorReplacement(t *testing.T) {
|
||||
t.Run("Test two separator replacements", func(t *testing.T) {
|
||||
assert.NotEqual(t, GenerateCsrfToken("foo:bar", "baz", "wah", now),
|
||||
GenerateCsrfToken("foo", "bar:baz", "wah", now))
|
||||
})
|
||||
}
|
||||
|
||||
func Test_InvalidToken(t *testing.T) {
|
||||
t.Run("Test invalid tokens", func(t *testing.T) {
|
||||
invalidTokenTests := []struct {
|
||||
name, key, userID, actionID string
|
||||
t time.Time
|
||||
}{
|
||||
{"Bad key", "foobar", userID, actionID, oneMinuteFromNow},
|
||||
{"Bad userID", key, "foobar", actionID, oneMinuteFromNow},
|
||||
{"Bad actionID", key, userID, "foobar", oneMinuteFromNow},
|
||||
{"Expired", key, userID, actionID, now.Add(CsrfTokenTimeout)},
|
||||
{"More than 1 minute from the future", key, userID, actionID, now.Add(-1*time.Nanosecond - 1*time.Minute)},
|
||||
}
|
||||
|
||||
tok := GenerateCsrfToken(key, userID, actionID, now)
|
||||
for _, itt := range invalidTokenTests {
|
||||
assert.False(t, ValidCsrfToken(tok, itt.key, itt.userID, itt.actionID, itt.t))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test_ValidateBadData primarily tests that no unexpected panics are triggered during parsing
|
||||
func Test_ValidateBadData(t *testing.T) {
|
||||
t.Run("Validate bad data", func(t *testing.T) {
|
||||
badDataTests := []struct {
|
||||
name, tok string
|
||||
}{
|
||||
{"Invalid Base64", "ASDab24(@)$*=="},
|
||||
{"No delimiter", base64.URLEncoding.EncodeToString([]byte("foobar12345678"))},
|
||||
{"Invalid time", base64.URLEncoding.EncodeToString([]byte("foobar:foobar"))},
|
||||
}
|
||||
|
||||
for _, bdt := range badDataTests {
|
||||
assert.False(t, ValidCsrfToken(bdt.tok, key, userID, actionID, oneMinuteFromNow))
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pull
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -18,15 +18,29 @@ import (
|
||||
|
||||
// CompareInfo represents needed information for comparing references.
|
||||
type CompareInfo struct {
|
||||
MergeBase string
|
||||
BaseCommitID string
|
||||
HeadCommitID string
|
||||
Commits []*git.Commit
|
||||
NumFiles int
|
||||
BaseRepo *repo_model.Repository
|
||||
BaseRef git.RefName
|
||||
BaseCommitID string
|
||||
HeadRepo *repo_model.Repository
|
||||
HeadGitRepo *git.Repository
|
||||
HeadRef git.RefName
|
||||
HeadCommitID string
|
||||
DirectComparison bool
|
||||
MergeBase string
|
||||
Commits []*git.Commit
|
||||
NumFiles int
|
||||
}
|
||||
|
||||
func (ci *CompareInfo) IsSameRepository() bool {
|
||||
return ci.BaseRepo.ID == ci.HeadRepo.ID
|
||||
}
|
||||
|
||||
func (ci *CompareInfo) IsSameRef() bool {
|
||||
return ci.IsSameRepository() && ci.BaseRef == ci.HeadRef
|
||||
}
|
||||
|
||||
// GetCompareInfo generates and returns compare information between base and head branches of repositories.
|
||||
func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Repository, headGitRepo *git.Repository, baseBranch, headBranch string, directComparison, fileOnly bool) (_ *CompareInfo, err error) {
|
||||
func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Repository, headGitRepo *git.Repository, baseRef, headRef git.RefName, directComparison, fileOnly bool) (_ *CompareInfo, err error) {
|
||||
var (
|
||||
remoteBranch string
|
||||
tmpRemote string
|
||||
@ -46,14 +60,22 @@ func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Reposito
|
||||
}()
|
||||
}
|
||||
|
||||
compareInfo := new(CompareInfo)
|
||||
|
||||
compareInfo.HeadCommitID, err = gitrepo.GetFullCommitID(ctx, headRepo, headBranch)
|
||||
if err != nil {
|
||||
compareInfo.HeadCommitID = headBranch
|
||||
compareInfo := &CompareInfo{
|
||||
BaseRepo: baseRepo,
|
||||
BaseRef: baseRef,
|
||||
HeadRepo: headRepo,
|
||||
HeadGitRepo: headGitRepo,
|
||||
HeadRef: headRef,
|
||||
DirectComparison: directComparison,
|
||||
}
|
||||
|
||||
compareInfo.MergeBase, remoteBranch, err = headGitRepo.GetMergeBase(tmpRemote, baseBranch, headBranch)
|
||||
compareInfo.HeadCommitID, err = gitrepo.GetFullCommitID(ctx, headRepo, headRef.String())
|
||||
if err != nil {
|
||||
compareInfo.HeadCommitID = headRef.String()
|
||||
}
|
||||
|
||||
// FIXME: It seems we don't need mergebase if it's a direct comparison?
|
||||
compareInfo.MergeBase, remoteBranch, err = headGitRepo.GetMergeBase(tmpRemote, baseRef.String(), headRef.String())
|
||||
if err == nil {
|
||||
compareInfo.BaseCommitID, err = gitrepo.GetFullCommitID(ctx, headRepo, remoteBranch)
|
||||
if err != nil {
|
||||
@ -68,7 +90,7 @@ func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Reposito
|
||||
|
||||
// We have a common base - therefore we know that ... should work
|
||||
if !fileOnly {
|
||||
compareInfo.Commits, err = headGitRepo.ShowPrettyFormatLogToList(ctx, baseCommitID+separator+headBranch)
|
||||
compareInfo.Commits, err = headGitRepo.ShowPrettyFormatLogToList(ctx, baseCommitID+separator+headRef.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ShowPrettyFormatLogToList: %w", err)
|
||||
}
|
||||
@ -87,7 +109,7 @@ func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Reposito
|
||||
// Count number of changed files.
|
||||
// This probably should be removed as we need to use shortstat elsewhere
|
||||
// Now there is git diff --shortstat but this appears to be slower than simply iterating with --nameonly
|
||||
compareInfo.NumFiles, err = headGitRepo.GetDiffNumChangedFiles(remoteBranch, headBranch, directComparison)
|
||||
compareInfo.NumFiles, err = headGitRepo.GetDiffNumChangedFiles(remoteBranch, headRef.String(), directComparison)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -14,7 +14,6 @@ import (
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -28,6 +27,7 @@ import (
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/auth/httpauth"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
lfs_module "code.gitea.io/gitea/modules/lfs"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@ -44,6 +44,7 @@ type requestContext struct {
|
||||
Repo string
|
||||
Authorization string
|
||||
Method string
|
||||
RepoGitURL string
|
||||
}
|
||||
|
||||
// Claims is a JWT Token Claims
|
||||
@ -83,17 +84,17 @@ func GetLFSAuthTokenWithBearer(opts AuthTokenOptions) (string, error) {
|
||||
|
||||
// DownloadLink builds a URL to download the object.
|
||||
func (rc *requestContext) DownloadLink(p lfs_module.Pointer) string {
|
||||
return setting.AppURL + path.Join(url.PathEscape(rc.User), url.PathEscape(rc.Repo+".git"), "info/lfs/objects", url.PathEscape(p.Oid))
|
||||
return rc.RepoGitURL + "/info/lfs/objects/" + url.PathEscape(p.Oid)
|
||||
}
|
||||
|
||||
// UploadLink builds a URL to upload the object.
|
||||
func (rc *requestContext) UploadLink(p lfs_module.Pointer) string {
|
||||
return setting.AppURL + path.Join(url.PathEscape(rc.User), url.PathEscape(rc.Repo+".git"), "info/lfs/objects", url.PathEscape(p.Oid), strconv.FormatInt(p.Size, 10))
|
||||
return rc.RepoGitURL + "/info/lfs/objects/" + url.PathEscape(p.Oid) + "/" + strconv.FormatInt(p.Size, 10)
|
||||
}
|
||||
|
||||
// VerifyLink builds a URL for verifying the object.
|
||||
func (rc *requestContext) VerifyLink(p lfs_module.Pointer) string {
|
||||
return setting.AppURL + path.Join(url.PathEscape(rc.User), url.PathEscape(rc.Repo+".git"), "info/lfs/verify")
|
||||
return rc.RepoGitURL + "/info/lfs/verify"
|
||||
}
|
||||
|
||||
// CheckAcceptMediaType checks if the client accepts the LFS media type.
|
||||
@ -421,11 +422,14 @@ func decodeJSON(req *http.Request, v any) error {
|
||||
}
|
||||
|
||||
func getRequestContext(ctx *context.Context) *requestContext {
|
||||
ownerName := ctx.PathParam("username")
|
||||
repoName := strings.TrimSuffix(ctx.PathParam("reponame"), ".git")
|
||||
return &requestContext{
|
||||
User: ctx.PathParam("username"),
|
||||
Repo: strings.TrimSuffix(ctx.PathParam("reponame"), ".git"),
|
||||
User: ownerName,
|
||||
Repo: repoName,
|
||||
Authorization: ctx.Req.Header.Get("Authorization"),
|
||||
Method: ctx.Req.Method,
|
||||
RepoGitURL: httplib.GuessCurrentAppURL(ctx) + url.PathEscape(ownerName) + "/" + url.PathEscape(repoName+".git"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -34,6 +34,7 @@ import (
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
git_service "code.gitea.io/gitea/services/git"
|
||||
issue_service "code.gitea.io/gitea/services/issue"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
)
|
||||
@ -1066,14 +1067,14 @@ func GetPullCommits(ctx context.Context, baseGitRepo *git.Repository, doer *user
|
||||
if pull.HasMerged {
|
||||
baseBranch = pull.MergeBase
|
||||
}
|
||||
prInfo, err := GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo, baseBranch, pull.GetGitHeadRefName(), true, false)
|
||||
compareInfo, err := git_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo, git.RefNameFromBranch(baseBranch), git.RefName(pull.GetGitHeadRefName()), true, false)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
commits := make([]CommitInfo, 0, len(prInfo.Commits))
|
||||
commits := make([]CommitInfo, 0, len(compareInfo.Commits))
|
||||
|
||||
for _, commit := range prInfo.Commits {
|
||||
for _, commit := range compareInfo.Commits {
|
||||
var committerOrAuthorName string
|
||||
var commitTime time.Time
|
||||
if commit.Author != nil {
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form" action="{{.Link}}" method="post">
|
||||
{{template "base/disable_form_autofill"}}
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="id" value="{{.Source.ID}}">
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "admin.auths.auth_type"}}</label>
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form" action="{{.Link}}" method="post">
|
||||
{{template "base/disable_form_autofill"}}
|
||||
{{.CsrfTokenHtml}}
|
||||
<!-- Types and name -->
|
||||
<div class="inline required field {{if .Err_Type}}error{{end}}">
|
||||
<label>{{ctx.Locale.Tr "admin.auths.auth_type"}}</label>
|
||||
|
||||
@ -228,7 +228,6 @@
|
||||
<dt class="tw-py-1 tw-flex tw-items-center">{{ctx.Locale.Tr "admin.config.send_test_mail"}}</dt>
|
||||
<dd class="tw-py-0">
|
||||
<form class="ui form ignore-dirty" action="{{AppSubUrl}}/-/admin/config/test_mail" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="ui tiny input">
|
||||
<input type="email" name="email" placeholder="{{ctx.Locale.Tr "admin.config.test_email_placeholder"}}" size="29" required>
|
||||
</div>
|
||||
@ -260,7 +259,6 @@
|
||||
<dt class="tw-py-1 tw-flex tw-items-center">{{ctx.Locale.Tr "admin.config.cache_test"}}</dt>
|
||||
<dd class="tw-py-0">
|
||||
<form class="ui form ignore-dirty" action="{{AppSubUrl}}/-/admin/config/test_cache" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<button class="ui tiny primary button">{{ctx.Locale.Tr "test"}}</button>
|
||||
</form>
|
||||
</dd>
|
||||
|
||||
@ -32,7 +32,6 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<input type="hidden" name="from" value="monitor">
|
||||
{{.CsrfTokenHtml}}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -10,7 +10,6 @@
|
||||
</h4>
|
||||
<div class="ui attached table segment">
|
||||
<form method="post" action="{{AppSubUrl}}/-/admin">
|
||||
{{.CsrfTokenHtml}}
|
||||
<table class="ui very basic table tw-mt-0 tw-px-4">
|
||||
<tbody>
|
||||
<tr>
|
||||
|
||||
@ -83,8 +83,6 @@
|
||||
<form class="content ui form" action="{{AppSubUrl}}/-/admin/emails/activate" method="post">
|
||||
<p class="center">{{ctx.Locale.Tr "admin.emails.change_email_text"}}</p>
|
||||
|
||||
{{$.CsrfTokenHtml}}
|
||||
|
||||
<input type="hidden" name="sort" value="{{.SortType}}">
|
||||
<input type="hidden" name="q" value="{{.Keyword}}">
|
||||
<input type="hidden" name="is_primary" value="{{.IsPrimary}}">
|
||||
|
||||
@ -34,7 +34,6 @@
|
||||
<th></th>
|
||||
<th colspan="5">
|
||||
<form class="tw-float-right" method="post" action="{{AppSubUrl}}/-/admin/notices/empty">
|
||||
{{.CsrfTokenHtml}}
|
||||
<button type="submit" class="ui red small button">{{ctx.Locale.Tr "admin.notices.delete_all"}}</button>
|
||||
</form>
|
||||
<div class="ui floating upward dropdown small button">
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
{{ctx.Locale.Tr "admin.packages.unreferenced_size" (FileSize .TotalUnreferencedBlobSize)}})
|
||||
<div class="ui right">
|
||||
<form method="post" action="{{AppSubUrl}}/-/admin/packages/cleanup">
|
||||
{{.CsrfTokenHtml}}
|
||||
<button class="ui primary tiny button">{{ctx.Locale.Tr "admin.packages.cleanup"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
@ -90,7 +89,6 @@
|
||||
</div>
|
||||
|
||||
<form class="ui small modal form-fetch-action" method="post" id="admin-package-delete-modal">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="header">{{svg "octicon-trash"}} {{ctx.Locale.Tr "packages.settings.delete"}}</div>
|
||||
<div class="content">
|
||||
{{ctx.Locale.Tr "packages.settings.delete.notice" (HTMLFormat `<span class="%s"></span>` "package-name") (HTMLFormat `<span class="%s"></span>` "package-version")}}
|
||||
|
||||
@ -31,7 +31,6 @@
|
||||
{{else}}
|
||||
{{$sum}}
|
||||
<form action="{{$.Link}}/remove-all-items" method="post" class="tw-inline-block tw-ml-4">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<button class="ui tiny basic red button">{{ctx.Locale.Tr "admin.monitor.queue.settings.remove_all_items"}}</button>
|
||||
</form>
|
||||
{{end}}
|
||||
@ -47,7 +46,6 @@
|
||||
<div class="ui attached segment">
|
||||
<p>{{ctx.Locale.Tr "admin.monitor.queue.settings.desc"}}</p>
|
||||
<form method="post" action="{{.Link}}/set">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<div class="ui form">
|
||||
<div class="inline field">
|
||||
<label for="max-number">{{ctx.Locale.Tr "admin.monitor.queue.settings.maxnumberworkers"}}</label>
|
||||
|
||||
@ -102,7 +102,6 @@
|
||||
</div>
|
||||
|
||||
<form class="ui small modal form-fetch-action" id="admin-repo-delete-modal" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="header">{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.settings.delete"}}</div>
|
||||
<div class="content">
|
||||
<p>{{ctx.Locale.Tr "repo.settings.delete_desc"}}</p>
|
||||
|
||||
@ -32,7 +32,6 @@
|
||||
<p>{{ctx.Locale.Tr "repo.adopt_preexisting_content" $dir}}</p>
|
||||
</div>
|
||||
<form class="ui form" method="post" action="{{AppSubUrl}}/-/admin/repos/unadopted">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<input type="hidden" name="id" value="{{$dir}}">
|
||||
<input type="hidden" name="action" value="adopt">
|
||||
<input type="hidden" name="q" value="{{$.Keyword}}">
|
||||
@ -49,7 +48,6 @@
|
||||
<p>{{ctx.Locale.Tr "repo.delete_preexisting_content" $dir}}</p>
|
||||
</div>
|
||||
<form class="ui form" method="post" action="{{AppSubUrl}}/-/admin/repos/unadopted">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<input type="hidden" name="id" value="{{$dir}}">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="q" value="{{$.Keyword}}">
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form" action="./edit" method="post">
|
||||
{{template "base/disable_form_autofill"}}
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="field {{if .Err_UserName}}error{{end}}">
|
||||
<label for="user_name">{{ctx.Locale.Tr "username"}}</label>
|
||||
<input id="user_name" name="user_name" value="{{.User.Name}}" maxlength="40">
|
||||
@ -173,7 +172,6 @@
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form" action="./avatar" method="post" enctype="multipart/form-data">
|
||||
{{.CsrfTokenHtml}}
|
||||
{{if not .DisableGravatar}}
|
||||
<div class="inline field">
|
||||
<div class="ui radio checkbox">
|
||||
@ -214,7 +212,6 @@
|
||||
<form class="ui form" method="post" action="./delete">
|
||||
<div class="content">
|
||||
<p>{{ctx.Locale.Tr "settings.delete_account_desc"}}</p>
|
||||
{{$.CsrfTokenHtml}}
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<label for="purge">{{ctx.Locale.Tr "admin.users.purge"}}</label>
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form" action="{{.Link}}" method="post">
|
||||
{{template "base/disable_form_autofill"}}
|
||||
{{.CsrfTokenHtml}}
|
||||
<!-- Types and name -->
|
||||
<div class="inline required field {{if .Err_LoginType}}error{{end}}">
|
||||
<label>{{ctx.Locale.Tr "admin.users.auth_source"}}</label>
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
{{template "base/head_script" .}}
|
||||
{{template "custom/header" .}}
|
||||
</head>
|
||||
<body hx-headers='{"x-csrf-token": "{{.CsrfToken}}"}' hx-swap="outerHTML" hx-ext="morph" hx-push-url="false">
|
||||
<body hx-swap="outerHTML" hx-ext="morph" hx-push-url="false">
|
||||
{{template "custom/body_outer_pre" .}}
|
||||
|
||||
<div class="full height">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user