Merge branch 'main' into fix-24635

Signed-off-by: Excellencedev <ademiluyisuccessandexcellence@gmail.com>
This commit is contained in:
Excellencedev authored and GitHub committed 2025-12-26 08:23:33 +01:00
commit 7df7f72a71
276 files changed
+71967 -199096

No files matched your search

+3 -3
View File
@@ -9,11 +9,11 @@ fi
mv ./options/locale/locale_en-US.json ./options/ mv ./options/locale/locale_en-US.json ./options/
# Remove translation under 25% of en_us # 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)) baselines=$((baselines / 4))
for filename in ./options/locale/*.json; do for filename in ./options/locale/*.json; do
lines=$(wc -l "$filename" | cut -d" " -f1) lines=$(cat "$filename" | wc -l)
if [ $lines -lt $baselines ]; then if [ "$lines" -lt "$baselines" ]; then
echo "Removing $filename: $lines/$baselines" echo "Removing $filename: $lines/$baselines"
rm "$filename" rm "$filename"
fi fi
-3
View File
@@ -503,9 +503,6 @@ INTERNAL_TOKEN =
;; Password Hash algorithm, either "argon2", "pbkdf2", "scrypt" or "bcrypt" ;; Password Hash algorithm, either "argon2", "pbkdf2", "scrypt" or "bcrypt"
;PASSWORD_HASH_ALGO = pbkdf2 ;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 ;; Validate against https://haveibeenpwned.com/Passwords to see if a password has been exposed
;PASSWORD_CHECK_PWN = false ;PASSWORD_CHECK_PWN = false
;; ;;
+19 -5
View File
@@ -9,24 +9,38 @@ package git
import ( import (
"io" "io"
"code.gitea.io/gitea/modules/log"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
) )
// Blob represents a Git object. // Blob represents a Git object.
type Blob struct { type Blob struct {
ID ObjectID ID ObjectID
repo *Repository
name string
}
gogitEncodedObj plumbing.EncodedObject func (b *Blob) gogitEncodedObj() (plumbing.EncodedObject, error) {
name string 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. // 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. // Calling the Close function on the result will discard all unread output.
func (b *Blob) DataAsync() (io.ReadCloser, error) { 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 // Size returns the uncompressed size of the blob
func (b *Blob) Size() int64 { 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()
} }
+7 -1
View File
@@ -9,5 +9,11 @@ func (repo *Repository) GetBlob(idStr string) (*Blob, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return repo.getBlob(id) if id.IsZero() {
return nil, ErrNotExist{id.String(), ""}
}
return &Blob{
ID: id,
repo: repo,
}, nil
} }
-22
View File
@@ -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
}
-16
View File
@@ -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
}
+3 -8
View File
@@ -53,14 +53,9 @@ func (te *TreeEntry) Size() int64 {
// Blob returns the blob object the entry // Blob returns the blob object the entry
func (te *TreeEntry) Blob() *Blob { 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{ return &Blob{
ID: te.ID, ID: te.ID,
gogitEncodedObj: encodedObj, repo: te.ptree.repo,
name: te.Name(), name: te.Name(),
} }
} }
+1 -1
View File
@@ -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. // 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") renderContentSandbox := sec.Key("RENDER_CONTENT_SANDBOX").MustString("allow-scripts allow-popups")
if renderContentSandbox == "disabled" { if renderContentSandbox == "disabled" {
renderContentSandbox = "" renderContentSandbox = ""
+1 -1
View File
@@ -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" // 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). // 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 ...) // In main branch, the signing token should be refactored (eg: one unique for LFS/OAuth2/etc ...)
jwtSecretBase64 := loadSecret(sec, "JWT_SECRET_URI", "JWT_SECRET") jwtSecretBase64 := loadSecret(sec, "JWT_SECRET_URI", "JWT_SECRET")
if InstallLock { if InstallLock {
-3
View File
@@ -36,8 +36,6 @@ var (
PasswordCheckPwn bool PasswordCheckPwn bool
SuccessfulTokensCacheSize int SuccessfulTokensCacheSize int
DisableQueryAuthToken bool DisableQueryAuthToken bool
CSRFCookieName = "_csrf"
CSRFCookieHTTPOnly = true
RecordUserSignupMetadata = false RecordUserSignupMetadata = false
TwoFactorAuthEnforced = 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("")) 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) PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false)
SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20) SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20)
+6 -7
View File
@@ -131,16 +131,15 @@ func (l *locale) TrString(trKey string, trArgs ...any) string {
var format string var format string
idx, ok := l.store.trKeyToIdxMap[trKey] idx, ok := l.store.trKeyToIdxMap[trKey]
if ok { if ok {
if msg, ok := l.idxToMsgMap[idx]; ok { format = l.idxToMsgMap[idx]
format = msg // use the found translation if format == "" { // missing translation in this locale, fallback to default
} else if def, ok := l.store.localeMap[l.store.defaultLang]; ok { if def, ok := l.store.localeMap[l.store.defaultLang]; ok {
// try to use default locale's translation // try to use default locale's translation
if msg, ok := def.idxToMsgMap[idx]; ok { format = def.idxToMsgMap[idx]
format = msg
} }
} }
} }
if format == "" { if format == "" { // still missing, use the key itself
format = html.EscapeString(trKey) format = html.EscapeString(trKey)
} }
msg, err := Format(format, trArgs...) 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 -17
View File
@@ -5,7 +5,6 @@ package repo
import ( import (
"net/http" "net/http"
"strings"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
@@ -52,18 +51,7 @@ func CompareDiff(ctx *context.APIContext) {
} }
} }
infoPath := ctx.PathParam("*") compareInfo, closer := parseCompareInfo(ctx, 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]})
if ctx.Written() { if ctx.Written() {
return return
} }
@@ -72,10 +60,10 @@ func CompareDiff(ctx *context.APIContext) {
verification := ctx.FormString("verification") == "" || ctx.FormBool("verification") verification := ctx.FormString("verification") == "" || ctx.FormBool("verification")
files := ctx.FormString("files") == "" || ctx.FormBool("files") 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) userCache := make(map[string]*user_model.User)
for i := 0; i < len(compareResult.compareInfo.Commits); i++ { for i := 0; i < len(compareInfo.Commits); i++ {
apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, compareResult.compareInfo.Commits[i], userCache, apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, compareInfo.Commits[i], userCache,
convert.ToCommitOptions{ convert.ToCommitOptions{
Stat: true, Stat: true,
Verification: verification, Verification: verification,
@@ -89,7 +77,7 @@ func CompareDiff(ctx *context.APIContext) {
} }
ctx.JSON(http.StatusOK, &api.Compare{ ctx.JSON(http.StatusOK, &api.Compare{
TotalCommits: len(compareResult.compareInfo.Commits), TotalCommits: len(compareInfo.Commits),
Commits: apiCommits, Commits: apiCommits,
}) })
} }
+54 -77
View File
@@ -28,6 +28,7 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/routers/common"
@@ -36,6 +37,7 @@ import (
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
git_service "code.gitea.io/gitea/services/git"
"code.gitea.io/gitea/services/gitdiff" "code.gitea.io/gitea/services/gitdiff"
issue_service "code.gitea.io/gitea/services/issue" issue_service "code.gitea.io/gitea/services/issue"
notify_service "code.gitea.io/gitea/services/notify" notify_service "code.gitea.io/gitea/services/notify"
@@ -414,20 +416,20 @@ func CreatePullRequest(ctx *context.APIContext) {
) )
// Get repo/branch information // Get repo/branch information
compareResult, closer := parseCompareInfo(ctx, form) compareResult, closer := parseCompareInfo(ctx, form.Base+".."+form.Head)
if ctx.Written() { if ctx.Written() {
return return
} }
defer closer() 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") ctx.APIError(http.StatusUnprocessableEntity, "Invalid PullRequest: base and head must be branches")
return return
} }
// Check if another PR exists with the same targets // Check if another PR exists with the same targets
existingPr, err := issues_model.GetUnmergedPullRequest(ctx, compareResult.headRepo.ID, ctx.Repo.Repository.ID, existingPr, err := issues_model.GetUnmergedPullRequest(ctx, compareResult.HeadRepo.ID, ctx.Repo.Repository.ID,
compareResult.headRef.ShortName(), compareResult.baseRef.ShortName(), compareResult.HeadRef.ShortName(), compareResult.BaseRef.ShortName(),
issues_model.PullRequestFlowGithub, issues_model.PullRequestFlowGithub,
) )
if err != nil { if err != nil {
@@ -505,13 +507,13 @@ func CreatePullRequest(ctx *context.APIContext) {
DeadlineUnix: deadlineUnix, DeadlineUnix: deadlineUnix,
} }
pr := &issues_model.PullRequest{ pr := &issues_model.PullRequest{
HeadRepoID: compareResult.headRepo.ID, HeadRepoID: compareResult.HeadRepo.ID,
BaseRepoID: repo.ID, BaseRepoID: repo.ID,
HeadBranch: compareResult.headRef.ShortName(), HeadBranch: compareResult.HeadRef.ShortName(),
BaseBranch: compareResult.baseRef.ShortName(), BaseBranch: compareResult.BaseRef.ShortName(),
HeadRepo: compareResult.headRepo, HeadRepo: compareResult.HeadRepo,
BaseRepo: repo, BaseRepo: repo,
MergeBase: compareResult.compareInfo.MergeBase, MergeBase: compareResult.MergeBase,
Type: issues_model.PullRequestGitea, Type: issues_model.PullRequestGitea,
} }
@@ -1057,63 +1059,40 @@ func MergePullRequest(ctx *context.APIContext) {
ctx.Status(http.StatusOK) 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 // 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()) { func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git_service.CompareInfo, 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
baseRepo := ctx.Repo.Repository baseRepo := ctx.Repo.Repository
baseRefToGuess := form.Base compareReq, err := common.ParseCompareRouterParam(compareParam)
switch {
headUser := ctx.Repo.Owner case errors.Is(err, util.ErrInvalidArgument):
headRefToGuess := form.Head ctx.APIError(http.StatusBadRequest, err.Error())
if headInfos := strings.Split(form.Head, ":"); len(headInfos) == 1 { return nil, nil
// If there is no head repository, it means pull request between same repository. case err != nil:
// Do nothing here because the head variables have been assigned above. ctx.APIErrorInternal(err)
} 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()
return nil, nil return nil, nil
} }
isSameRepo := ctx.Repo.Owner.ID == headUser.ID // remove the check when we support compare with carets
if compareReq.CaretTimes > 0 {
var headRepo *repo_model.Repository ctx.APIError(http.StatusBadRequest, "Unsupported compare syntax with carets")
if isSameRepo { return nil, nil
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
}
} }
_, 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 var headGitRepo *git.Repository
if isSameRepo { if isSameRepo {
headGitRepo = ctx.Repo.GitRepo headGitRepo = ctx.Repo.GitRepo
@@ -1140,8 +1119,8 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
} }
if !permBase.CanRead(unit.TypeCode) { 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) 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 pulls or can't read UnitTypeCode") ctx.APIErrorNotFound("can't read baseRepo UnitTypeCode")
return nil, nil return nil, nil
} }
@@ -1158,10 +1137,10 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
return nil, nil return nil, nil
} }
baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(baseRefToGuess) baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(util.IfZero(compareReq.BaseOriRef, baseRepo.DefaultBranch))
headRef := headGitRepo.UnstableGuessRefByShortName(headRefToGuess) 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()) 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()) 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 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 { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return nil, nil return nil, nil
} }
result = &parseCompareInfoResult{headRepo: headRepo, headGitRepo: headGitRepo, compareInfo: compareInfo, baseRef: baseRef, headRef: headRef} return compareInfo, closer
return result, closer
} }
// UpdatePullRequest merge PR's baseBranch into headBranch // UpdatePullRequest merge PR's baseBranch into headBranch
@@ -1422,7 +1400,7 @@ func GetPullRequestCommits(ctx *context.APIContext) {
return return
} }
var prInfo *pull_service.CompareInfo var compareInfo *git_service.CompareInfo
baseGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.BaseRepo) baseGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.BaseRepo)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
@@ -1431,19 +1409,18 @@ func GetPullRequestCommits(ctx *context.APIContext) {
defer closer.Close() defer closer.Close()
if pr.HasMerged { 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 { } 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 { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
commits := prInfo.Commits
listOptions := utils.GetListOptions(ctx) listOptions := utils.GetListOptions(ctx)
totalNumberOfCommits := len(commits) totalNumberOfCommits := len(compareInfo.Commits)
totalNumberOfPages := int(math.Ceil(float64(totalNumberOfCommits) / float64(listOptions.PageSize))) totalNumberOfPages := int(math.Ceil(float64(totalNumberOfCommits) / float64(listOptions.PageSize)))
userCache := make(map[string]*user_model.User) userCache := make(map[string]*user_model.User)
@@ -1458,7 +1435,7 @@ func GetPullRequestCommits(ctx *context.APIContext) {
apiCommits := make([]*api.Commit, 0, limit) apiCommits := make([]*api.Commit, 0, limit)
for i := start; i < start+limit; i++ { 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{ convert.ToCommitOptions{
Stat: true, Stat: true,
Verification: verification, Verification: verification,
@@ -1552,11 +1529,11 @@ func GetPullRequestFiles(ctx *context.APIContext) {
baseGitRepo := ctx.Repo.GitRepo baseGitRepo := ctx.Repo.GitRepo
var prInfo *pull_service.CompareInfo var compareInfo *git_service.CompareInfo
if pr.HasMerged { 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 { } 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 { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
@@ -1569,7 +1546,7 @@ func GetPullRequestFiles(ctx *context.APIContext) {
return return
} }
startCommitID := prInfo.MergeBase startCommitID := compareInfo.MergeBase
endCommitID := headCommitID endCommitID := headCommitID
maxLines := setting.Git.MaxGitDiffLines maxLines := setting.Git.MaxGitDiffLines
+4 -4
View File
@@ -38,8 +38,8 @@ func AuthShared(ctx *context.Base, sessionStore auth_service.SessionStore, authM
// VerifyOptions contains required or check options // VerifyOptions contains required or check options
type VerifyOptions struct { type VerifyOptions struct {
SignInRequired bool SignInRequired bool
SignOutRequired bool SignOutRequired bool
AdminRequired bool AdminRequired bool
DisableCSRF bool DisableCrossOriginProtection bool
} }
+141 -11
View File
@@ -5,22 +5,110 @@ package common
import ( import (
"context" "context"
"strings"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/util"
pull_service "code.gitea.io/gitea/services/pull"
) )
// CompareInfo represents the collected results from ParseCompareInfo type CompareRouterReq struct {
type CompareInfo struct { BaseOriRef string
HeadUser *user_model.User HeadOwner string
HeadRepo *repo_model.Repository HeadRepoName string
HeadGitRepo *git.Repository HeadOriRef string
CompareInfo *pull_service.CompareInfo CaretTimes int // ^ times after base ref
BaseBranch string DotTimes int
HeadBranch string }
DirectComparison bool
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. // 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) 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) { func findHeadRepoFromRootBase(ctx context.Context, baseRepo *repo_model.Repository, headUserID int64, traverseLevel int) (*repo_model.Repository, error) {
if traverseLevel == 0 { if traverseLevel == 0 {
return nil, nil return nil, nil
+151
View 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)
})
}
}
-7
View File
@@ -102,7 +102,6 @@ func autoSignIn(ctx *context.Context) (bool, error) {
return false, err return false, err
} }
ctx.Csrf.PrepareForSessionUser(ctx)
return true, nil 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) ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
} }
// force to generate a new CSRF token
ctx.Csrf.PrepareForSessionUser(ctx)
// Register last login // Register last login
if err := user_service.UpdateUser(ctx, u, &user_service.UpdateOptions{SetLastLogin: true}); err != nil { if err := user_service.UpdateUser(ctx, u, &user_service.UpdateOptions{SetLastLogin: true}); err != nil {
ctx.ServerError("UpdateUser", err) ctx.ServerError("UpdateUser", err)
@@ -403,7 +399,6 @@ func HandleSignOut(ctx *context.Context) {
_ = ctx.Session.Flush() _ = ctx.Session.Flush()
_ = ctx.Session.Destroy(ctx.Resp, ctx.Req) _ = ctx.Session.Destroy(ctx.Resp, ctx.Req)
ctx.DeleteSiteCookie(setting.CookieRememberName) ctx.DeleteSiteCookie(setting.CookieRememberName)
ctx.Csrf.DeleteCookie(ctx)
middleware.DeleteRedirectToCookie(ctx.Resp) middleware.DeleteRedirectToCookie(ctx.Resp)
} }
@@ -811,8 +806,6 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
return return
} }
ctx.Csrf.PrepareForSessionUser(ctx)
if err := resetLocale(ctx, user); err != nil { if err := resetLocale(ctx, user); err != nil {
ctx.ServerError("resetLocale", err) ctx.ServerError("resetLocale", err)
return return
-3
View File
@@ -393,9 +393,6 @@ func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_m
return return
} }
// force to generate a new CSRF token
ctx.Csrf.PrepareForSessionUser(ctx)
if err := resetLocale(ctx, u); err != nil { if err := resetLocale(ctx, u); err != nil {
ctx.ServerError("resetLocale", err) ctx.ServerError("resetLocale", err)
return return
+1 -1
View File
@@ -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/{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}}.pack", repo.GetPackFile)
m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile) 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())
} }
+92 -163
View File
@@ -41,8 +41,8 @@ import (
"code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload" "code.gitea.io/gitea/services/context/upload"
git_service "code.gitea.io/gitea/services/git"
"code.gitea.io/gitea/services/gitdiff" "code.gitea.io/gitea/services/gitdiff"
pull_service "code.gitea.io/gitea/services/pull"
user_service "code.gitea.io/gitea/services/user" 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 // 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 baseRepo := ctx.Repo.Repository
ci := &common.CompareInfo{}
fileOnly := ctx.FormBool("file-only") fileOnly := ctx.FormBool("file-only")
// Get compared branches information compareReq, err := common.ParseCompareRouterParam(ctx.PathParam("*"))
// A full compare url is of the form: switch {
// case errors.Is(err, util.ErrInvalidArgument):
// 1. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headBranch} ctx.HTTPError(http.StatusBadRequest, err.Error())
// 2. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}:{:headBranch} return nil
// 3. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}/{:headRepoName}:{:headBranch} case err != nil:
// 4. /{:baseOwner}/{:baseRepoName}/compare/{:headBranch} ctx.ServerError("ParseCompareRouterParam", err)
// 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)
return nil return nil
} }
ctx.Data["HeadUser"] = ci.HeadUser // remove the check when we support compare with carets
ctx.Data["HeadBranch"] = ci.HeadBranch 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 ctx.Repo.PullRequest.SameRepo = isSameRepo
// Check if base branch is valid. // Check if base branch is valid.
baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(ci.BaseBranch) baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(baseBranch)
baseIsBranch, _ := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, ci.BaseBranch) baseIsBranch, _ := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, baseBranch)
baseIsTag := gitrepo.IsTagExist(ctx, ctx.Repo.Repository, ci.BaseBranch) baseIsTag := gitrepo.IsTagExist(ctx, ctx.Repo.Repository, baseBranch)
if !baseIsCommit && !baseIsBranch && !baseIsTag { if !baseIsCommit && !baseIsBranch && !baseIsTag {
// Check if baseBranch is short sha commit hash // Check if baseBranch is short sha commit hash
if baseCommit, _ := ctx.Repo.GitRepo.GetCommit(ci.BaseBranch); baseCommit != nil { if baseCommit, _ := ctx.Repo.GitRepo.GetCommit(baseBranch); baseCommit != nil {
ci.BaseBranch = baseCommit.ID.String() baseBranch = baseCommit.ID.String()
ctx.Data["BaseBranch"] = ci.BaseBranch ctx.Data["BaseBranch"] = baseBranch
baseIsCommit = true baseIsCommit = true
} else if ci.BaseBranch == ctx.Repo.GetObjectFormat().EmptyObjectID().String() { } else if baseBranch == ctx.Repo.GetObjectFormat().EmptyObjectID().String() {
if isSameRepo { if isSameRepo {
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ci.HeadBranch)) ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(headBranch))
} else { } 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 return nil
} else { } 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 // 3. If the base is a forked from "RootRepo" and the owner of
// the "RootRepo" is the :headUser - set headRepo to that // the "RootRepo" is the :headUser - set headRepo to that
if !has && rootRepo != nil && rootRepo.OwnerID == ci.HeadUser.ID { if !has && rootRepo != nil && rootRepo.OwnerID == headOwner.ID {
ci.HeadRepo = rootRepo headRepo = rootRepo
has = true has = true
} }
// 4. If the ctx.Doer has their own fork of the baseRepo and the headUser is the ctx.Doer // 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 // set the headRepo to the ownFork
if !has && ownForkRepo != nil && ownForkRepo.OwnerID == ci.HeadUser.ID { if !has && ownForkRepo != nil && ownForkRepo.OwnerID == headOwner.ID {
ci.HeadRepo = ownForkRepo headRepo = ownForkRepo
has = true has = true
} }
// 5. If the headOwner has a fork of the baseRepo - use that // 5. If the headOwner has a fork of the baseRepo - use that
if !has { if !has {
ci.HeadRepo = repo_model.GetForkedRepo(ctx, ci.HeadUser.ID, baseRepo.ID) headRepo = repo_model.GetForkedRepo(ctx, headOwner.ID, baseRepo.ID)
has = ci.HeadRepo != nil has = headRepo != nil
} }
// 6. If the baseRepo is a fork and the headUser has a fork of that use that // 6. If the baseRepo is a fork and the headUser has a fork of that use that
if !has && baseRepo.IsFork { if !has && baseRepo.IsFork {
ci.HeadRepo = repo_model.GetForkedRepo(ctx, ci.HeadUser.ID, baseRepo.ForkID) headRepo = repo_model.GetForkedRepo(ctx, headOwner.ID, baseRepo.ForkID)
has = ci.HeadRepo != nil has = headRepo != nil
} }
// 7. Otherwise if we're not the same repo and haven't found a repo give up // 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 // 8. Finally open the git repo
var headGitRepo *git.Repository
if isSameRepo { if isSameRepo {
ci.HeadRepo = ctx.Repo.Repository headGitRepo = ctx.Repo.GitRepo
ci.HeadGitRepo = ctx.Repo.GitRepo
} else if has { } else if has {
ci.HeadGitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ci.HeadRepo) headGitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, headRepo)
if err != nil { if err != nil {
ctx.ServerError("RepositoryFromRequestContextOrOpen", err) ctx.ServerError("RepositoryFromRequestContextOrOpen", err)
return nil return nil
@@ -415,7 +346,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
return nil return nil
} }
ctx.Data["HeadRepo"] = ci.HeadRepo ctx.Data["HeadRepo"] = headRepo
ctx.Data["BaseCompareRepo"] = ctx.Repo.Repository ctx.Data["BaseCompareRepo"] = ctx.Repo.Repository
// Now we need to assert that the ctx.Doer has permission to read // 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 we're not merging from the same repo:
if !isSameRepo { if !isSameRepo {
// Assert ctx.Doer has permission to read headRepo's codes // 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 { if err != nil {
ctx.ServerError("GetUserRepoPermission", err) ctx.ServerError("GetUserRepoPermission", err)
return nil return nil
@@ -449,7 +380,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
if log.IsTrace() { if log.IsTrace() {
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v", log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v",
ctx.Doer, ctx.Doer,
ci.HeadRepo, headRepo,
permHead) permHead)
} }
ctx.NotFound(nil) ctx.NotFound(nil)
@@ -463,7 +394,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
// 2. the computed head // 2. the computed head
// then get the branches of it // then get the branches of it
if rootRepo != nil && if rootRepo != nil &&
rootRepo.ID != ci.HeadRepo.ID && rootRepo.ID != headRepo.ID &&
rootRepo.ID != baseRepo.ID { rootRepo.ID != baseRepo.ID {
canRead := access_model.CheckRepoUnitUser(ctx, rootRepo, ctx.Doer, unit.TypeCode) canRead := access_model.CheckRepoUnitUser(ctx, rootRepo, ctx.Doer, unit.TypeCode)
if canRead { if canRead {
@@ -487,7 +418,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
// 3. The rootRepo (if we have one) // 3. The rootRepo (if we have one)
// then get the branches from it. // then get the branches from it.
if ownForkRepo != nil && if ownForkRepo != nil &&
ownForkRepo.ID != ci.HeadRepo.ID && ownForkRepo.ID != headRepo.ID &&
ownForkRepo.ID != baseRepo.ID && ownForkRepo.ID != baseRepo.ID &&
(rootRepo == nil || ownForkRepo.ID != rootRepo.ID) { (rootRepo == nil || ownForkRepo.ID != rootRepo.ID) {
canRead := access_model.CheckRepoUnitUser(ctx, ownForkRepo, ctx.Doer, unit.TypeCode) 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. // Check if head branch is valid.
headIsCommit := ci.HeadGitRepo.IsCommitExist(ci.HeadBranch) headIsCommit := headGitRepo.IsCommitExist(headBranch)
headIsBranch, _ := git_model.IsBranchExist(ctx, ci.HeadRepo.ID, ci.HeadBranch) headIsBranch, _ := git_model.IsBranchExist(ctx, headRepo.ID, headBranch)
headIsTag := gitrepo.IsTagExist(ctx, ci.HeadRepo, ci.HeadBranch) headIsTag := gitrepo.IsTagExist(ctx, headRepo, headBranch)
if !headIsCommit && !headIsBranch && !headIsTag { if !headIsCommit && !headIsBranch && !headIsTag {
// Check if headBranch is short sha commit hash // Check if headBranch is short sha commit hash
if headCommit, _ := ci.HeadGitRepo.GetCommit(ci.HeadBranch); headCommit != nil { if headCommit, _ := headGitRepo.GetCommit(headBranch); headCommit != nil {
ci.HeadBranch = headCommit.ID.String() headBranch = headCommit.ID.String()
ctx.Data["HeadBranch"] = ci.HeadBranch ctx.Data["HeadBranch"] = headBranch
headIsCommit = true headIsCommit = true
} else { } else {
ctx.NotFound(nil) ctx.NotFound(nil)
@@ -540,41 +471,41 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
return nil return nil
} }
baseBranchRef := ci.BaseBranch baseBranchRef := git.RefName(baseBranch)
if baseIsBranch { if baseIsBranch {
baseBranchRef = git.BranchPrefix + ci.BaseBranch baseBranchRef = git.RefNameFromBranch(baseBranch)
} else if baseIsTag { } else if baseIsTag {
baseBranchRef = git.TagPrefix + ci.BaseBranch baseBranchRef = git.RefNameFromTag(baseBranch)
} }
headBranchRef := ci.HeadBranch headBranchRef := git.RefName(headBranch)
if headIsBranch { if headIsBranch {
headBranchRef = git.BranchPrefix + ci.HeadBranch headBranchRef = git.RefNameFromBranch(headBranch)
} else if headIsTag { } 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 { if err != nil {
ctx.ServerError("GetCompareInfo", err) ctx.ServerError("GetCompareInfo", err)
return nil return nil
} }
if ci.DirectComparison { if compareReq.DirectComparison() {
ctx.Data["BeforeCommitID"] = ci.CompareInfo.BaseCommitID ctx.Data["BeforeCommitID"] = compareInfo.BaseCommitID
} else { } else {
ctx.Data["BeforeCommitID"] = ci.CompareInfo.MergeBase ctx.Data["BeforeCommitID"] = compareInfo.MergeBase
} }
return ci return compareInfo
} }
// PrepareCompareDiff renders compare diff page // PrepareCompareDiff renders compare diff page
func PrepareCompareDiff( func PrepareCompareDiff(
ctx *context.Context, ctx *context.Context,
ci *common.CompareInfo, ci *git_service.CompareInfo,
whitespaceBehavior gitcmd.TrustedCmdArgs, whitespaceBehavior gitcmd.TrustedCmdArgs,
) (nothingToCompare bool) { ) (nothingToCompare bool) {
repo := ctx.Repo.Repository repo := ctx.Repo.Repository
headCommitID := ci.CompareInfo.HeadCommitID headCommitID := ci.HeadCommitID
ctx.Data["CommitRepoLink"] = ci.HeadRepo.Link() ctx.Data["CommitRepoLink"] = ci.HeadRepo.Link()
ctx.Data["AfterCommitID"] = headCommitID ctx.Data["AfterCommitID"] = headCommitID
@@ -586,17 +517,15 @@ func PrepareCompareDiff(
ctx.Data["TitleQuery"] = newPrFormTitle ctx.Data["TitleQuery"] = newPrFormTitle
ctx.Data["BodyQuery"] = newPrFormBody ctx.Data["BodyQuery"] = newPrFormBody
if (headCommitID == ci.CompareInfo.MergeBase && !ci.DirectComparison) || if (headCommitID == ci.MergeBase && !ci.DirectComparison) ||
headCommitID == ci.CompareInfo.BaseCommitID { headCommitID == ci.BaseCommitID {
ctx.Data["IsNothingToCompare"] = true ctx.Data["IsNothingToCompare"] = true
if unit, err := repo.GetUnit(ctx, unit.TypePullRequests); err == nil { if unit, err := repo.GetUnit(ctx, unit.TypePullRequests); err == nil {
config := unit.PullRequestsConfig() config := unit.PullRequestsConfig()
if !config.AutodetectManualMerge { if !config.AutodetectManualMerge {
allowEmptyPr := !(ci.BaseBranch == ci.HeadBranch && ctx.Repo.Repository.Name == ci.HeadRepo.Name) ctx.Data["AllowEmptyPr"] = !ci.IsSameRef()
ctx.Data["AllowEmptyPr"] = allowEmptyPr return ci.IsSameRef()
return !allowEmptyPr
} }
ctx.Data["AllowEmptyPr"] = false ctx.Data["AllowEmptyPr"] = false
@@ -604,9 +533,9 @@ func PrepareCompareDiff(
return true return true
} }
beforeCommitID := ci.CompareInfo.MergeBase beforeCommitID := ci.MergeBase
if ci.DirectComparison { if ci.DirectComparison {
beforeCommitID = ci.CompareInfo.BaseCommitID beforeCommitID = ci.BaseCommitID
} }
maxLines, maxFiles := setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles maxLines, maxFiles := setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles
@@ -674,7 +603,7 @@ func PrepareCompareDiff(
return false return false
} }
commits, err := processGitCommits(ctx, ci.CompareInfo.Commits) commits, err := processGitCommits(ctx, ci.Commits)
if err != nil { if err != nil {
ctx.ServerError("processGitCommits", err) ctx.ServerError("processGitCommits", err)
return false return false
@@ -682,7 +611,7 @@ func PrepareCompareDiff(
ctx.Data["Commits"] = commits ctx.Data["Commits"] = commits
ctx.Data["CommitCount"] = len(commits) ctx.Data["CommitCount"] = len(commits)
title := ci.HeadBranch title := ci.HeadRef.ShortName()
if len(commits) == 1 { if len(commits) == 1 {
c := commits[0] c := commits[0]
title = strings.TrimSpace(c.UserCommit.Summary()) title = strings.TrimSpace(c.UserCommit.Summary())
@@ -706,10 +635,10 @@ func PrepareCompareDiff(
} }
ctx.Data["title"] = title ctx.Data["title"] = title
ctx.Data["Username"] = ci.HeadUser.Name ctx.Data["Username"] = ci.HeadRepo.OwnerName
ctx.Data["Reponame"] = ci.HeadRepo.Name 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 return false
} }
@@ -790,7 +719,7 @@ func CompareDiff(ctx *context.Context) {
ctx.Data["HeadTags"] = headTags ctx.Data["HeadTags"] = headTags
if ctx.Data["PageIsComparePull"] == true { 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 err != nil {
if !issues_model.IsErrPullRequestNotExist(err) { if !issues_model.IsErrPullRequestNotExist(err) {
ctx.ServerError("GetUnmergedPullRequest", err) ctx.ServerError("GetUnmergedPullRequest", err)
+14 -13
View File
@@ -44,6 +44,7 @@ import (
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload" "code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
git_service "code.gitea.io/gitea/services/git"
"code.gitea.io/gitea/services/gitdiff" "code.gitea.io/gitea/services/gitdiff"
notify_service "code.gitea.io/gitea/services/notify" notify_service "code.gitea.io/gitea/services/notify"
pull_service "code.gitea.io/gitea/services/pull" pull_service "code.gitea.io/gitea/services/pull"
@@ -256,7 +257,7 @@ func GetMergedBaseCommitID(ctx *context.Context, issue *issues_model.Issue) stri
return baseCommit 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 { if !issue.IsPull {
return nil 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 // 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 pull := issue.PullRequest
setMergeTarget(ctx, pull) setMergeTarget(ctx, pull)
@@ -275,8 +276,8 @@ func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue)
baseCommit := GetMergedBaseCommitID(ctx, issue) baseCommit := GetMergedBaseCommitID(ctx, issue)
compareInfo, err := pull_service.GetCompareInfo(ctx, ctx.Repo.Repository, ctx.Repo.Repository, ctx.Repo.GitRepo, compareInfo, err := git_service.GetCompareInfo(ctx, ctx.Repo.Repository, ctx.Repo.Repository, ctx.Repo.GitRepo,
baseCommit, pull.GetGitHeadRefName(), false, false) git.RefName(baseCommit), git.RefName(pull.GetGitHeadRefName()), false, false)
if err != nil { 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") { 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 ctx.Data["IsPullRequestBroken"] = true
@@ -321,7 +322,7 @@ type pullCommitStatusCheckData struct {
} }
// prepareViewPullInfo show meta information for a pull request preview page // 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 ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
repo := ctx.Repo.Repository 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) ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(commitStatuses)
} }
compareInfo, err := pull_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo, compareInfo, err := git_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo,
pull.MergeBase, pull.GetGitHeadRefName(), false, false) git.RefName(pull.MergeBase), git.RefName(pull.GetGitHeadRefName()), false, false)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "fatal: Not a valid object name") { if strings.Contains(err.Error(), "fatal: Not a valid object name") {
ctx.Data["IsPullRequestBroken"] = true 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, compareInfo, err := git_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo,
git.BranchPrefix+pull.BaseBranch, pull.GetGitHeadRefName(), false, false) git.RefNameFromBranch(pull.BaseBranch), git.RefName(pull.GetGitHeadRefName()), false, false)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "fatal: Not a valid object name") { if strings.Contains(err.Error(), "fatal: Not a valid object name") {
ctx.Data["IsPullRequestBroken"] = true 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. // 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) { if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
ctx.ServerError("GetUnmergedPullRequest", err) ctx.ServerError("GetUnmergedPullRequest", err)
return return
@@ -1371,11 +1372,11 @@ func CompareAndPullRequestPost(ctx *context.Context) {
pullRequest := &issues_model.PullRequest{ pullRequest := &issues_model.PullRequest{
HeadRepoID: ci.HeadRepo.ID, HeadRepoID: ci.HeadRepo.ID,
BaseRepoID: repo.ID, BaseRepoID: repo.ID,
HeadBranch: ci.HeadBranch, HeadBranch: ci.HeadRef.ShortName(),
BaseBranch: ci.BaseBranch, BaseBranch: ci.BaseRef.ShortName(),
HeadRepo: ci.HeadRepo, HeadRepo: ci.HeadRepo,
BaseRepo: repo, BaseRepo: repo,
MergeBase: ci.CompareInfo.MergeBase, MergeBase: ci.MergeBase,
Type: issues_model.PullRequestGitea, Type: issues_model.PullRequestGitea,
AllowMaintainerEdit: form.AllowMaintainerEdit, AllowMaintainerEdit: form.AllowMaintainerEdit,
} }
+1 -1
View File
@@ -325,7 +325,7 @@ func loadKeysData(ctx *context.Context) {
ctx.Data["GPGKeys"] = gpgkeys ctx.Data["GPGKeys"] = gpgkeys
tokenToSign := asymkey_model.VerificationToken(ctx.Doer, 1) 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 ctx.Data["TokenToSign"] = tokenToSign
principals, err := db.Find[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{ principals, err := db.Find[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{
+20 -13
View File
@@ -129,13 +129,13 @@ func webAuth(authMethod auth_service.Method) func(*context.Context) {
// ensure the session uid is deleted // ensure the session uid is deleted
_ = ctx.Session.Delete("uid") _ = ctx.Session.Delete("uid")
} }
ctx.Csrf.PrepareForSessionUser(ctx)
} }
} }
// verifyAuthWithOptions checks authentication according to options // verifyAuthWithOptions checks authentication according to options
func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Context) { func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Context) {
crossOriginProtection := http.NewCrossOriginProtection()
return func(ctx *context.Context) { return func(ctx *context.Context) {
// Check prohibit login users. // Check prohibit login users.
if ctx.IsSigned { if ctx.IsSigned {
@@ -178,9 +178,9 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Cont
return return
} }
if !options.SignOutRequired && !options.DisableCSRF && ctx.Req.Method == http.MethodPost { if !options.SignOutRequired && !options.DisableCrossOriginProtection {
ctx.Csrf.Validate(ctx) if err := crossOriginProtection.Check(ctx.Req); err != nil {
if ctx.Written() { http.Error(ctx.Resp, err.Error(), http.StatusForbidden)
return return
} }
} }
@@ -292,7 +292,12 @@ func Routes() *web.Router {
return routes 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 // registerWebRoutes register routes
func registerWebRoutes(m *web.Router) { 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.Post("/-/markup", reqSignIn, web.Bind(structs.MarkupOption{}), misc.Markup)
m.Get("/-/web-theme/list", misc.WebThemeList) 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.Group("/explore", func() {
m.Get("", func(ctx *context.Context) { m.Get("", func(ctx *context.Context) {
@@ -565,12 +570,14 @@ func registerWebRoutes(m *web.Router) {
m.Post("/grant", web.Bind(forms.GrantApplicationForm{}), auth.GrantApplicationOAuth) m.Post("/grant", web.Bind(forms.GrantApplicationForm{}), auth.GrantApplicationOAuth)
// TODO manage redirection // TODO manage redirection
m.Post("/authorize", web.Bind(forms.AuthorizationForm{}), auth.AuthorizeOAuth) m.Post("/authorize", web.Bind(forms.AuthorizationForm{}), auth.AuthorizeOAuth)
}, optSignInIgnoreCsrf, reqSignIn) }, reqSignIn)
m.Methods("GET, POST, OPTIONS", "/userinfo", optionsCorsHandler(), optSignInIgnoreCsrf, auth.InfoOAuth) m.Group("", func() {
m.Methods("POST, OPTIONS", "/access_token", optionsCorsHandler(), web.Bind(forms.AccessTokenForm{}), optSignInIgnoreCsrf, auth.AccessTokenOAuth) m.Methods("GET, POST, OPTIONS", "/userinfo", auth.InfoOAuth)
m.Methods("GET, OPTIONS", "/keys", optionsCorsHandler(), optSignInIgnoreCsrf, auth.OIDCKeys) m.Methods("POST, OPTIONS", "/access_token", web.Bind(forms.AccessTokenForm{}), auth.AccessTokenOAuth)
m.Methods("POST, OPTIONS", "/introspect", optionsCorsHandler(), web.Bind(forms.IntrospectTokenForm{}), optSignInIgnoreCsrf, auth.IntrospectOAuth) m.Methods("GET, OPTIONS", "/keys", auth.OIDCKeys)
m.Methods("POST, OPTIONS", "/introspect", web.Bind(forms.IntrospectTokenForm{}), auth.IntrospectOAuth)
}, optionsCorsHandler(), optSignInFromAnyOrigin)
}, oauth2Enabled) }, oauth2Enabled)
m.Group("/user/settings", func() { 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) m.Post("/action/{action:accept_transfer|reject_transfer}", reqSignIn, repo.ActionTransfer)
}, optSignIn, context.RepoAssignment) }, 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 addOwnerRepoGitHTTPRouters(m) // "/{username}/{reponame}/{git-paths}": git http support
-6
View File
@@ -19,7 +19,6 @@ import (
"code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
gitea_context "code.gitea.io/gitea/services/context"
user_service "code.gitea.io/gitea/services/user" 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) 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)
}
} }
+1 -1
View File
@@ -227,7 +227,7 @@ func APIContexter() func(http.Handler) http.Handler {
ctx.SetContextValue(apiContextKey, ctx) 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.Req.Method == http.MethodPost && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") {
if !ctx.ParseMultipartForm() { if !ctx.ParseMultipartForm() {
return return
+4 -18
View File
@@ -6,14 +6,12 @@ package context
import ( import (
"context" "context"
"encoding/hex"
"fmt" "fmt"
"html/template" "html/template"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"time"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" 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` PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData`
Cache cache.StringCache Cache cache.StringCache
Csrf CSRFProtector
Flash *middleware.Flash Flash *middleware.Flash
Session session.Store 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. // Contexter initializes a classic context for a request.
func Contexter() func(next http.Handler) http.Handler { func Contexter() func(next http.Handler) http.Handler {
rnd := templates.HTMLRenderer() 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 func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
base := NewBaseContext(resp, req) base := NewBaseContext(resp, req)
@@ -167,8 +152,6 @@ func Contexter() func(next http.Handler) http.Handler {
ctx.PageData = map[string]any{} ctx.PageData = map[string]any{}
ctx.Data["PageData"] = ctx.PageData ctx.Data["PageData"] = ctx.PageData
ctx.Csrf = NewCSRFProtector(csrfOpts)
// get the last flash message from cookie // get the last flash message from cookie
lastFlashCookie, lastFlashMsg := middleware.GetSiteCookieFlashMessage(ctx, ctx.Req, CookieNameFlash) lastFlashCookie, lastFlashMsg := middleware.GetSiteCookieFlashMessage(ctx, ctx.Req, CookieNameFlash)
if vals, _ := url.ParseQuery(lastFlashCookie); len(vals) > 0 { 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.Req.Method == http.MethodPost && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") {
if !ctx.ParseMultipartForm() { if !ctx.ParseMultipartForm() {
return return
-2
View File
@@ -25,13 +25,11 @@ func removeSessionCookieHeader(w http.ResponseWriter) {
} }
// SetSiteCookie convenience function to set most cookies consistently // 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) { func (ctx *Context) SetSiteCookie(name, value string, maxAge int) {
middleware.SetSiteCookie(ctx.Resp, name, value, maxAge) middleware.SetSiteCookie(ctx.Resp, name, value, maxAge)
} }
// DeleteSiteCookie convenience function to delete most cookies consistently // DeleteSiteCookie convenience function to delete most cookies consistently
// CSRF and a few others are the exception here
func (ctx *Context) DeleteSiteCookie(name string) { func (ctx *Context) DeleteSiteCookie(name string) {
middleware.SetSiteCookie(ctx.Resp, name, "", -1) middleware.SetSiteCookie(ctx.Resp, name, "", -1)
} }
-168
View File
@@ -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())
}
-99
View File
@@ -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
}
-91
View File
@@ -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. // Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package pull package git
import ( import (
"context" "context"
@@ -18,15 +18,29 @@ import (
// CompareInfo represents needed information for comparing references. // CompareInfo represents needed information for comparing references.
type CompareInfo struct { type CompareInfo struct {
MergeBase string BaseRepo *repo_model.Repository
BaseCommitID string BaseRef git.RefName
HeadCommitID string BaseCommitID string
Commits []*git.Commit HeadRepo *repo_model.Repository
NumFiles int 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. // 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 ( var (
remoteBranch string remoteBranch string
tmpRemote string tmpRemote string
@@ -46,14 +60,22 @@ func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Reposito
}() }()
} }
compareInfo := new(CompareInfo) compareInfo := &CompareInfo{
BaseRepo: baseRepo,
compareInfo.HeadCommitID, err = gitrepo.GetFullCommitID(ctx, headRepo, headBranch) BaseRef: baseRef,
if err != nil { HeadRepo: headRepo,
compareInfo.HeadCommitID = headBranch 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 { if err == nil {
compareInfo.BaseCommitID, err = gitrepo.GetFullCommitID(ctx, headRepo, remoteBranch) compareInfo.BaseCommitID, err = gitrepo.GetFullCommitID(ctx, headRepo, remoteBranch)
if err != nil { 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 // We have a common base - therefore we know that ... should work
if !fileOnly { if !fileOnly {
compareInfo.Commits, err = headGitRepo.ShowPrettyFormatLogToList(ctx, baseCommitID+separator+headBranch) compareInfo.Commits, err = headGitRepo.ShowPrettyFormatLogToList(ctx, baseCommitID+separator+headRef.String())
if err != nil { if err != nil {
return nil, fmt.Errorf("ShowPrettyFormatLogToList: %w", err) 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. // Count number of changed files.
// This probably should be removed as we need to use shortstat elsewhere // 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 // 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 { if err != nil {
return nil, err return nil, err
} }
+10 -6
View File
@@ -14,7 +14,6 @@ import (
"maps" "maps"
"net/http" "net/http"
"net/url" "net/url"
"path"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@@ -28,6 +27,7 @@ import (
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/httpauth" "code.gitea.io/gitea/modules/auth/httpauth"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
lfs_module "code.gitea.io/gitea/modules/lfs" lfs_module "code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@@ -44,6 +44,7 @@ type requestContext struct {
Repo string Repo string
Authorization string Authorization string
Method string Method string
RepoGitURL string
} }
// Claims is a JWT Token Claims // Claims is a JWT Token Claims
@@ -83,17 +84,17 @@ func GetLFSAuthTokenWithBearer(opts AuthTokenOptions) (string, error) {
// DownloadLink builds a URL to download the object. // DownloadLink builds a URL to download the object.
func (rc *requestContext) DownloadLink(p lfs_module.Pointer) string { 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. // UploadLink builds a URL to upload the object.
func (rc *requestContext) UploadLink(p lfs_module.Pointer) string { 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. // VerifyLink builds a URL for verifying the object.
func (rc *requestContext) VerifyLink(p lfs_module.Pointer) string { 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. // 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 { func getRequestContext(ctx *context.Context) *requestContext {
ownerName := ctx.PathParam("username")
repoName := strings.TrimSuffix(ctx.PathParam("reponame"), ".git")
return &requestContext{ return &requestContext{
User: ctx.PathParam("username"), User: ownerName,
Repo: strings.TrimSuffix(ctx.PathParam("reponame"), ".git"), Repo: repoName,
Authorization: ctx.Req.Header.Get("Authorization"), Authorization: ctx.Req.Header.Get("Authorization"),
Method: ctx.Req.Method, Method: ctx.Req.Method,
RepoGitURL: httplib.GuessCurrentAppURL(ctx) + url.PathEscape(ownerName) + "/" + url.PathEscape(repoName+".git"),
} }
} }
+4 -3
View File
@@ -34,6 +34,7 @@ import (
repo_module "code.gitea.io/gitea/modules/repository" repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
git_service "code.gitea.io/gitea/services/git"
issue_service "code.gitea.io/gitea/services/issue" issue_service "code.gitea.io/gitea/services/issue"
notify_service "code.gitea.io/gitea/services/notify" 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 { if pull.HasMerged {
baseBranch = pull.MergeBase 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 { if err != nil {
return nil, "", err 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 committerOrAuthorName string
var commitTime time.Time var commitTime time.Time
if commit.Author != nil { if commit.Author != nil {
-1
View File
@@ -6,7 +6,6 @@
<div class="ui attached segment"> <div class="ui attached segment">
<form class="ui form" action="{{.Link}}" method="post"> <form class="ui form" action="{{.Link}}" method="post">
{{template "base/disable_form_autofill"}} {{template "base/disable_form_autofill"}}
{{.CsrfTokenHtml}}
<input type="hidden" name="id" value="{{.Source.ID}}"> <input type="hidden" name="id" value="{{.Source.ID}}">
<div class="inline field"> <div class="inline field">
<label>{{ctx.Locale.Tr "admin.auths.auth_type"}}</label> <label>{{ctx.Locale.Tr "admin.auths.auth_type"}}</label>
-1
View File
@@ -6,7 +6,6 @@
<div class="ui attached segment"> <div class="ui attached segment">
<form class="ui form" action="{{.Link}}" method="post"> <form class="ui form" action="{{.Link}}" method="post">
{{template "base/disable_form_autofill"}} {{template "base/disable_form_autofill"}}
{{.CsrfTokenHtml}}
<!-- Types and name --> <!-- Types and name -->
<div class="inline required field {{if .Err_Type}}error{{end}}"> <div class="inline required field {{if .Err_Type}}error{{end}}">
<label>{{ctx.Locale.Tr "admin.auths.auth_type"}}</label> <label>{{ctx.Locale.Tr "admin.auths.auth_type"}}</label>
-2
View File
@@ -228,7 +228,6 @@
<dt class="tw-py-1 tw-flex tw-items-center">{{ctx.Locale.Tr "admin.config.send_test_mail"}}</dt> <dt class="tw-py-1 tw-flex tw-items-center">{{ctx.Locale.Tr "admin.config.send_test_mail"}}</dt>
<dd class="tw-py-0"> <dd class="tw-py-0">
<form class="ui form ignore-dirty" action="{{AppSubUrl}}/-/admin/config/test_mail" method="post"> <form class="ui form ignore-dirty" action="{{AppSubUrl}}/-/admin/config/test_mail" method="post">
{{.CsrfTokenHtml}}
<div class="ui tiny input"> <div class="ui tiny input">
<input type="email" name="email" placeholder="{{ctx.Locale.Tr "admin.config.test_email_placeholder"}}" size="29" required> <input type="email" name="email" placeholder="{{ctx.Locale.Tr "admin.config.test_email_placeholder"}}" size="29" required>
</div> </div>
@@ -260,7 +259,6 @@
<dt class="tw-py-1 tw-flex tw-items-center">{{ctx.Locale.Tr "admin.config.cache_test"}}</dt> <dt class="tw-py-1 tw-flex tw-items-center">{{ctx.Locale.Tr "admin.config.cache_test"}}</dt>
<dd class="tw-py-0"> <dd class="tw-py-0">
<form class="ui form ignore-dirty" action="{{AppSubUrl}}/-/admin/config/test_cache" method="post"> <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> <button class="ui tiny primary button">{{ctx.Locale.Tr "test"}}</button>
</form> </form>
</dd> </dd>
-1
View File
@@ -32,7 +32,6 @@
</tbody> </tbody>
</table> </table>
<input type="hidden" name="from" value="monitor"> <input type="hidden" name="from" value="monitor">
{{.CsrfTokenHtml}}
</form> </form>
</div> </div>
</div> </div>
-1
View File
@@ -10,7 +10,6 @@
</h4> </h4>
<div class="ui attached table segment"> <div class="ui attached table segment">
<form method="post" action="{{AppSubUrl}}/-/admin"> <form method="post" action="{{AppSubUrl}}/-/admin">
{{.CsrfTokenHtml}}
<table class="ui very basic table tw-mt-0 tw-px-4"> <table class="ui very basic table tw-mt-0 tw-px-4">
<tbody> <tbody>
<tr> <tr>
-2
View File
@@ -83,8 +83,6 @@
<form class="content ui form" action="{{AppSubUrl}}/-/admin/emails/activate" method="post"> <form class="content ui form" action="{{AppSubUrl}}/-/admin/emails/activate" method="post">
<p class="center">{{ctx.Locale.Tr "admin.emails.change_email_text"}}</p> <p class="center">{{ctx.Locale.Tr "admin.emails.change_email_text"}}</p>
{{$.CsrfTokenHtml}}
<input type="hidden" name="sort" value="{{.SortType}}"> <input type="hidden" name="sort" value="{{.SortType}}">
<input type="hidden" name="q" value="{{.Keyword}}"> <input type="hidden" name="q" value="{{.Keyword}}">
<input type="hidden" name="is_primary" value="{{.IsPrimary}}"> <input type="hidden" name="is_primary" value="{{.IsPrimary}}">
-1
View File
@@ -34,7 +34,6 @@
<th></th> <th></th>
<th colspan="5"> <th colspan="5">
<form class="tw-float-right" method="post" action="{{AppSubUrl}}/-/admin/notices/empty"> <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> <button type="submit" class="ui red small button">{{ctx.Locale.Tr "admin.notices.delete_all"}}</button>
</form> </form>
<div class="ui floating upward dropdown small button"> <div class="ui floating upward dropdown small button">
-2
View File
@@ -6,7 +6,6 @@
{{ctx.Locale.Tr "admin.packages.unreferenced_size" (FileSize .TotalUnreferencedBlobSize)}}) {{ctx.Locale.Tr "admin.packages.unreferenced_size" (FileSize .TotalUnreferencedBlobSize)}})
<div class="ui right"> <div class="ui right">
<form method="post" action="{{AppSubUrl}}/-/admin/packages/cleanup"> <form method="post" action="{{AppSubUrl}}/-/admin/packages/cleanup">
{{.CsrfTokenHtml}}
<button class="ui primary tiny button">{{ctx.Locale.Tr "admin.packages.cleanup"}}</button> <button class="ui primary tiny button">{{ctx.Locale.Tr "admin.packages.cleanup"}}</button>
</form> </form>
</div> </div>
@@ -90,7 +89,6 @@
</div> </div>
<form class="ui small modal form-fetch-action" method="post" id="admin-package-delete-modal"> <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="header">{{svg "octicon-trash"}} {{ctx.Locale.Tr "packages.settings.delete"}}</div>
<div class="content"> <div class="content">
{{ctx.Locale.Tr "packages.settings.delete.notice" (HTMLFormat `<span class="%s"></span>` "package-name") (HTMLFormat `<span class="%s"></span>` "package-version")}} {{ctx.Locale.Tr "packages.settings.delete.notice" (HTMLFormat `<span class="%s"></span>` "package-name") (HTMLFormat `<span class="%s"></span>` "package-version")}}
-2
View File
@@ -31,7 +31,6 @@
{{else}} {{else}}
{{$sum}} {{$sum}}
<form action="{{$.Link}}/remove-all-items" method="post" class="tw-inline-block tw-ml-4"> <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> <button class="ui tiny basic red button">{{ctx.Locale.Tr "admin.monitor.queue.settings.remove_all_items"}}</button>
</form> </form>
{{end}} {{end}}
@@ -47,7 +46,6 @@
<div class="ui attached segment"> <div class="ui attached segment">
<p>{{ctx.Locale.Tr "admin.monitor.queue.settings.desc"}}</p> <p>{{ctx.Locale.Tr "admin.monitor.queue.settings.desc"}}</p>
<form method="post" action="{{.Link}}/set"> <form method="post" action="{{.Link}}/set">
{{$.CsrfTokenHtml}}
<div class="ui form"> <div class="ui form">
<div class="inline field"> <div class="inline field">
<label for="max-number">{{ctx.Locale.Tr "admin.monitor.queue.settings.maxnumberworkers"}}</label> <label for="max-number">{{ctx.Locale.Tr "admin.monitor.queue.settings.maxnumberworkers"}}</label>
-1
View File
@@ -102,7 +102,6 @@
</div> </div>
<form class="ui small modal form-fetch-action" id="admin-repo-delete-modal" method="post"> <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="header">{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.settings.delete"}}</div>
<div class="content"> <div class="content">
<p>{{ctx.Locale.Tr "repo.settings.delete_desc"}}</p> <p>{{ctx.Locale.Tr "repo.settings.delete_desc"}}</p>
-2
View File
@@ -32,7 +32,6 @@
<p>{{ctx.Locale.Tr "repo.adopt_preexisting_content" $dir}}</p> <p>{{ctx.Locale.Tr "repo.adopt_preexisting_content" $dir}}</p>
</div> </div>
<form class="ui form" method="post" action="{{AppSubUrl}}/-/admin/repos/unadopted"> <form class="ui form" method="post" action="{{AppSubUrl}}/-/admin/repos/unadopted">
{{$.CsrfTokenHtml}}
<input type="hidden" name="id" value="{{$dir}}"> <input type="hidden" name="id" value="{{$dir}}">
<input type="hidden" name="action" value="adopt"> <input type="hidden" name="action" value="adopt">
<input type="hidden" name="q" value="{{$.Keyword}}"> <input type="hidden" name="q" value="{{$.Keyword}}">
@@ -49,7 +48,6 @@
<p>{{ctx.Locale.Tr "repo.delete_preexisting_content" $dir}}</p> <p>{{ctx.Locale.Tr "repo.delete_preexisting_content" $dir}}</p>
</div> </div>
<form class="ui form" method="post" action="{{AppSubUrl}}/-/admin/repos/unadopted"> <form class="ui form" method="post" action="{{AppSubUrl}}/-/admin/repos/unadopted">
{{$.CsrfTokenHtml}}
<input type="hidden" name="id" value="{{$dir}}"> <input type="hidden" name="id" value="{{$dir}}">
<input type="hidden" name="action" value="delete"> <input type="hidden" name="action" value="delete">
<input type="hidden" name="q" value="{{$.Keyword}}"> <input type="hidden" name="q" value="{{$.Keyword}}">
-3
View File
@@ -6,7 +6,6 @@
<div class="ui attached segment"> <div class="ui attached segment">
<form class="ui form" action="./edit" method="post"> <form class="ui form" action="./edit" method="post">
{{template "base/disable_form_autofill"}} {{template "base/disable_form_autofill"}}
{{.CsrfTokenHtml}}
<div class="field {{if .Err_UserName}}error{{end}}"> <div class="field {{if .Err_UserName}}error{{end}}">
<label for="user_name">{{ctx.Locale.Tr "username"}}</label> <label for="user_name">{{ctx.Locale.Tr "username"}}</label>
<input id="user_name" name="user_name" value="{{.User.Name}}" maxlength="40"> <input id="user_name" name="user_name" value="{{.User.Name}}" maxlength="40">
@@ -173,7 +172,6 @@
</h4> </h4>
<div class="ui attached segment"> <div class="ui attached segment">
<form class="ui form" action="./avatar" method="post" enctype="multipart/form-data"> <form class="ui form" action="./avatar" method="post" enctype="multipart/form-data">
{{.CsrfTokenHtml}}
{{if not .DisableGravatar}} {{if not .DisableGravatar}}
<div class="inline field"> <div class="inline field">
<div class="ui radio checkbox"> <div class="ui radio checkbox">
@@ -214,7 +212,6 @@
<form class="ui form" method="post" action="./delete"> <form class="ui form" method="post" action="./delete">
<div class="content"> <div class="content">
<p>{{ctx.Locale.Tr "settings.delete_account_desc"}}</p> <p>{{ctx.Locale.Tr "settings.delete_account_desc"}}</p>
{{$.CsrfTokenHtml}}
<div class="field"> <div class="field">
<div class="ui checkbox"> <div class="ui checkbox">
<label for="purge">{{ctx.Locale.Tr "admin.users.purge"}}</label> <label for="purge">{{ctx.Locale.Tr "admin.users.purge"}}</label>
-1
View File
@@ -6,7 +6,6 @@
<div class="ui attached segment"> <div class="ui attached segment">
<form class="ui form" action="{{.Link}}" method="post"> <form class="ui form" action="{{.Link}}" method="post">
{{template "base/disable_form_autofill"}} {{template "base/disable_form_autofill"}}
{{.CsrfTokenHtml}}
<!-- Types and name --> <!-- Types and name -->
<div class="inline required field {{if .Err_LoginType}}error{{end}}"> <div class="inline required field {{if .Err_LoginType}}error{{end}}">
<label>{{ctx.Locale.Tr "admin.users.auth_source"}}</label> <label>{{ctx.Locale.Tr "admin.users.auth_source"}}</label>
+1 -1
View File
@@ -23,7 +23,7 @@
{{template "base/head_script" .}} {{template "base/head_script" .}}
{{template "custom/header" .}} {{template "custom/header" .}}
</head> </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" .}} {{template "custom/body_outer_pre" .}}
<div class="full height"> <div class="full height">
Loaded 100 of 276 files, more files were not shown because too many files have changed in this diff. Show more