From 372d24b84bc6f4c5562792009c6b6e6a4aeb85f8 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 12 Nov 2025 19:44:49 +0800 Subject: [PATCH 1/8] Limit reading bytes instead of ReadAll (#35928) --- modules/actions/workflows.go | 4 +-- modules/issue/template/unmarshal.go | 3 +- modules/packages/nuget/metadata.go | 2 +- modules/packages/pub/metadata.go | 2 +- modules/util/io.go | 2 +- routers/web/repo/wiki.go | 2 +- services/issue/template.go | 4 +-- services/repository/generate.go | 46 ++++++++++++++++------------ services/repository/generate_test.go | 26 ++++++++++++++++ services/webhook/deliver.go | 3 +- 10 files changed, 64 insertions(+), 30 deletions(-) diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 69f71bf651..26a6ebc370 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -5,7 +5,6 @@ package actions import ( "bytes" - "io" "slices" "strings" @@ -13,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/glob" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" "github.com/nektos/act/pkg/jobparser" @@ -77,7 +77,7 @@ func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) { if err != nil { return nil, err } - content, err := io.ReadAll(f) + content, err := util.ReadWithLimit(f, 1024*1024) _ = f.Close() if err != nil { return nil, err diff --git a/modules/issue/template/unmarshal.go b/modules/issue/template/unmarshal.go index 1d8e9dd02d..ceab6babf4 100644 --- a/modules/issue/template/unmarshal.go +++ b/modules/issue/template/unmarshal.go @@ -5,7 +5,6 @@ package template import ( "fmt" - "io" "path" "strconv" @@ -76,7 +75,7 @@ func unmarshalFromEntry(entry *git.TreeEntry, filename string) (*api.IssueTempla } defer r.Close() - content, err := io.ReadAll(r) + content, err := util.ReadWithLimit(r, 1024*1024) if err != nil { return nil, fmt.Errorf("read all: %w", err) } diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go index 513b4dd2b9..5124627395 100644 --- a/modules/packages/nuget/metadata.go +++ b/modules/packages/nuget/metadata.go @@ -216,7 +216,7 @@ func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) { if p.Metadata.Readme != "" { f, err := archive.Open(p.Metadata.Readme) if err == nil { - buf, _ := io.ReadAll(f) + buf, _ := util.ReadWithLimit(f, 1024*1024) m.Readme = string(buf) _ = f.Close() } diff --git a/modules/packages/pub/metadata.go b/modules/packages/pub/metadata.go index 9b00472eb2..a2cf6b728a 100644 --- a/modules/packages/pub/metadata.go +++ b/modules/packages/pub/metadata.go @@ -89,7 +89,7 @@ func ParsePackage(r io.Reader) (*Package, error) { return nil, err } } else if strings.EqualFold(hd.Name, "readme.md") { - data, err := io.ReadAll(tr) + data, err := util.ReadWithLimit(tr, 1024*1024) if err != nil { return nil, err } diff --git a/modules/util/io.go b/modules/util/io.go index b3dde9d1f6..f5a3d320e5 100644 --- a/modules/util/io.go +++ b/modules/util/io.go @@ -29,7 +29,7 @@ func ReadAtMost(r io.Reader, buf []byte) (n int, err error) { // ReadWithLimit reads at most "limit" bytes from r into buf. // If EOF or ErrUnexpectedEOF occurs while reading, err will be nil. func ReadWithLimit(r io.Reader, n int) (buf []byte, err error) { - return readWithLimit(r, 1024, n) + return readWithLimit(r, 4*1024, n) } func readWithLimit(r io.Reader, batch, limit int) ([]byte, error) { diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index e7c34ba1d6..542ac9c731 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -133,7 +133,7 @@ func wikiContentsByEntry(ctx *context.Context, entry *git.TreeEntry) []byte { return nil } defer reader.Close() - content, err := io.ReadAll(reader) + content, err := util.ReadWithLimit(reader, 5*1024*1024) // 5MB should be enough for a wiki page if err != nil { ctx.ServerError("ReadAll", err) return nil diff --git a/services/issue/template.go b/services/issue/template.go index 4b0f1aa987..99977c67cf 100644 --- a/services/issue/template.go +++ b/services/issue/template.go @@ -5,7 +5,6 @@ package issue import ( "fmt" - "io" "net/url" "path" "strings" @@ -15,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/issue/template" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "gopkg.in/yaml.v3" ) @@ -65,7 +65,7 @@ func GetTemplateConfig(gitRepo *git.Repository, path string, commit *git.Commit) defer reader.Close() - configContent, err := io.ReadAll(reader) + configContent, err := util.ReadWithLimit(reader, 1024*1024) if err != nil { return GetDefaultTemplateConfig(), err } diff --git a/services/repository/generate.go b/services/repository/generate.go index 062c6f4fb1..caf15265a0 100644 --- a/services/repository/generate.go +++ b/services/repository/generate.go @@ -7,7 +7,9 @@ import ( "bufio" "bytes" "context" + "errors" "fmt" + "io/fs" "os" "path/filepath" "regexp" @@ -138,31 +140,37 @@ func (gt *giteaTemplateFileMatcher) Match(s string) bool { return false } -func readGiteaTemplateFile(tmpDir string) (*giteaTemplateFileMatcher, error) { - localPath := filepath.Join(tmpDir, ".gitea", "template") - if _, err := os.Stat(localPath); os.IsNotExist(err) { - return nil, nil - } else if err != nil { +func readLocalTmpRepoFileContent(localPath string, limit int) ([]byte, error) { + ok, err := util.IsRegularFile(localPath) + if err != nil { return nil, err + } else if !ok { + return nil, fs.ErrNotExist } - content, err := os.ReadFile(localPath) + f, err := os.Open(localPath) if err != nil { return nil, err } + defer f.Close() + return util.ReadWithLimit(f, limit) +} + +func readGiteaTemplateFile(tmpDir string) (*giteaTemplateFileMatcher, error) { + localPath := filepath.Join(tmpDir, ".gitea", "template") + content, err := readLocalTmpRepoFileContent(localPath, 1024*1024) + if err != nil { + return nil, err + } return newGiteaTemplateFileMatcher(localPath, content), nil } func substGiteaTemplateFile(ctx context.Context, tmpDir, tmpDirSubPath string, templateRepo, generateRepo *repo_model.Repository) error { tmpFullPath := filepath.Join(tmpDir, tmpDirSubPath) - if ok, err := util.IsRegularFile(tmpFullPath); !ok { - return err - } - - content, err := os.ReadFile(tmpFullPath) + content, err := readLocalTmpRepoFileContent(tmpFullPath, 1024*1024) if err != nil { - return err + return util.Iif(errors.Is(err, fs.ErrNotExist), nil, err) } if err := util.Remove(tmpFullPath); err != nil { return err @@ -172,7 +180,7 @@ func substGiteaTemplateFile(ctx context.Context, tmpDir, tmpDirSubPath string, t substSubPath := filepath.Clean(filePathSanitize(generateExpansion(ctx, tmpDirSubPath, templateRepo, generateRepo))) newLocalPath := filepath.Join(tmpDir, substSubPath) regular, err := util.IsRegularFile(newLocalPath) - if canWrite := regular || os.IsNotExist(err); !canWrite { + if canWrite := regular || errors.Is(err, fs.ErrNotExist); !canWrite { return nil } if err := os.MkdirAll(filepath.Dir(newLocalPath), 0o755); err != nil { @@ -242,15 +250,15 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r // Variable expansion fileMatcher, err := readGiteaTemplateFile(tmpDir) - if err != nil { - return fmt.Errorf("readGiteaTemplateFile: %w", err) - } - - if fileMatcher != nil { + if err == nil { err = processGiteaTemplateFile(ctx, tmpDir, templateRepo, generateRepo, fileMatcher) if err != nil { - return err + return fmt.Errorf("processGiteaTemplateFile: %w", err) } + } else if errors.Is(err, fs.ErrNotExist) { + log.Debug("skip processing repo template files: no available .gitea/template") + } else { + return fmt.Errorf("readGiteaTemplateFile: %w", err) } if err = git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil { diff --git a/services/repository/generate_test.go b/services/repository/generate_test.go index 19b84c7bde..9c01911ded 100644 --- a/services/repository/generate_test.go +++ b/services/repository/generate_test.go @@ -4,6 +4,7 @@ package repository import ( + "io/fs" "os" "path/filepath" "testing" @@ -175,6 +176,31 @@ func TestProcessGiteaTemplateFile(t *testing.T) { // subst from a link, skip, and the target is unchanged assertSymLink("subst-${TEMPLATE_NAME}-from-link", tmpDir+"/sub/link-target") } + + { + templateFilePath := tmpDir + "/.gitea/template" + + _ = os.Remove(templateFilePath) + _, err := os.Lstat(templateFilePath) + require.ErrorIs(t, err, fs.ErrNotExist) + _, err = readGiteaTemplateFile(tmpDir) // no template file + require.ErrorIs(t, err, fs.ErrNotExist) + + _ = os.WriteFile(templateFilePath+".target", []byte("test-data-target"), 0o644) + _ = os.Symlink(templateFilePath+".target", templateFilePath) + content, _ := os.ReadFile(templateFilePath) + require.Equal(t, "test-data-target", string(content)) + _, err = readGiteaTemplateFile(tmpDir) // symlinked template file + require.ErrorIs(t, err, fs.ErrNotExist) + + _ = os.Remove(templateFilePath) + _ = os.WriteFile(templateFilePath, []byte("test-data-regular"), 0o644) + content, _ = os.ReadFile(templateFilePath) + require.Equal(t, "test-data-regular", string(content)) + fm, err := readGiteaTemplateFile(tmpDir) // regular template file + require.NoError(t, err) + assert.Len(t, fm.globs, 1) + } } func TestTransformers(t *testing.T) { diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go index b6611a3576..58fba9f68d 100644 --- a/services/webhook/deliver.go +++ b/services/webhook/deliver.go @@ -30,6 +30,7 @@ import ( "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" ) @@ -264,7 +265,7 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error { t.ResponseInfo.Headers[k] = strings.Join(vals, ",") } - p, err := io.ReadAll(resp.Body) + p, err := util.ReadWithLimit(resp.Body, 1024*1024) if err != nil { t.ResponseInfo.Body = fmt.Sprintf("read body: %s", err) return fmt.Errorf("unable to deliver webhook task[%d] in %s as unable to read response body: %w", t.ID, w.URL, err) From b95fd7e13ed4063d123b488df74a98cbfcfed85b Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 13 Nov 2025 07:03:13 +0800 Subject: [PATCH 2/8] Don't show unnecessary error message to end users for DeleteBranchAfterMerge (#35937) --- routers/web/repo/pull.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 17e3bf2b78..4353e00840 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1243,7 +1243,11 @@ func MergePullRequest(ctx *context.Context) { func deleteBranchAfterMergeAndFlashMessage(ctx *context.Context, prID int64) { var fullBranchName string err := repo_service.DeleteBranchAfterMerge(ctx, ctx.Doer, prID, &fullBranchName) - if errTr := util.ErrorAsTranslatable(err); errTr != nil { + if errors.Is(err, util.ErrPermissionDenied) || errors.Is(err, util.ErrNotExist) { + // no need to show error to end users if no permission or branch not exist + log.Debug("DeleteBranchAfterMerge (ignore unnecessary error): %v", err) + return + } else if errTr := util.ErrorAsTranslatable(err); errTr != nil { ctx.Flash.Error(errTr.Translate(ctx.Locale)) return } else if err == nil { From 1f3558b65c0c7121bc0b7b75316e1ec9317bd703 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 14 Nov 2025 08:31:11 +0800 Subject: [PATCH 3/8] Fix corrupted external render content (#35946) Fix #35944 --- routers/web/repo/view_file.go | 11 +-- .../user30/renderer.git/HEAD | 1 - .../user30/renderer.git/config | 6 -- .../user30/renderer.git/description | 1 - .../user30/renderer.git/hooks/post-receive | 15 ---- .../renderer.git/hooks/post-receive.d/gitea | 2 - .../user30/renderer.git/hooks/pre-receive | 15 ---- .../renderer.git/hooks/pre-receive.d/gitea | 2 - .../user30/renderer.git/hooks/update | 14 ---- .../user30/renderer.git/hooks/update.d/gitea | 2 - .../06/0d5c2acd8bf4b6f14010acd1a73d73392ec46e | Bin 56 -> 0 bytes .../45/14a93050edb2c3165bdd0a3c03be063e879e68 | Bin 50 -> 0 bytes .../c9/61cc4d1ba6b7ee1ba228a9a02b00b7746d8033 | Bin 789 -> 0 bytes .../user30/renderer.git/packed-refs | 2 - .../user30/renderer.git/refs/.keep | 0 tests/integration/markup_external_test.go | 75 +++++++++++++----- tests/sqlite.ini.tmpl | 2 +- 17 files changed, 60 insertions(+), 88 deletions(-) delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/HEAD delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/config delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/description delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/hooks/post-receive delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/hooks/post-receive.d/gitea delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/hooks/pre-receive delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/hooks/pre-receive.d/gitea delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/hooks/update delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/hooks/update.d/gitea delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/objects/06/0d5c2acd8bf4b6f14010acd1a73d73392ec46e delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/objects/45/14a93050edb2c3165bdd0a3c03be063e879e68 delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/objects/c9/61cc4d1ba6b7ee1ba228a9a02b00b7746d8033 delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/packed-refs delete mode 100644 tests/gitea-repositories-meta/user30/renderer.git/refs/.keep diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index ea3920439d..167cd5f927 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -92,8 +92,6 @@ func handleFileViewRenderMarkup(ctx *context.Context, filename string, sniffedTy ctx.ServerError("Render", err) return true } - // to prevent iframe from loading third-party url - ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'") return true } @@ -241,14 +239,17 @@ func prepareFileView(ctx *context.Context, entry *git.TreeEntry) { // * IsRenderableXxx: some files are rendered by backend "markup" engine, some are rendered by frontend (pdf, 3d) // * DefaultViewMode: when there is no "display" query parameter, which view mode should be used by default, source or rendered - utf8Reader := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{}) + contentReader := io.MultiReader(bytes.NewReader(buf), dataRc) + if fInfo.st.IsRepresentableAsText() { + contentReader = charset.ToUTF8WithFallbackReader(contentReader, charset.ConvertOpts{}) + } switch { case fInfo.blobOrLfsSize >= setting.UI.MaxDisplayFileSize: ctx.Data["IsFileTooLarge"] = true - case handleFileViewRenderMarkup(ctx, entry.Name(), fInfo.st, buf, utf8Reader): + case handleFileViewRenderMarkup(ctx, entry.Name(), fInfo.st, buf, contentReader): // it also sets ctx.Data["FileContent"] and more ctx.Data["IsMarkup"] = true - case handleFileViewRenderSource(ctx, entry.Name(), attrs, fInfo, utf8Reader): + case handleFileViewRenderSource(ctx, entry.Name(), attrs, fInfo, contentReader): // it also sets ctx.Data["FileContent"] and more ctx.Data["IsDisplayingSource"] = true case handleFileViewRenderImage(ctx, fInfo, buf): diff --git a/tests/gitea-repositories-meta/user30/renderer.git/HEAD b/tests/gitea-repositories-meta/user30/renderer.git/HEAD deleted file mode 100644 index cb089cd89a..0000000000 --- a/tests/gitea-repositories-meta/user30/renderer.git/HEAD +++ /dev/null @@ -1 +0,0 @@ -ref: refs/heads/master diff --git a/tests/gitea-repositories-meta/user30/renderer.git/config b/tests/gitea-repositories-meta/user30/renderer.git/config deleted file mode 100644 index e6da231579..0000000000 --- a/tests/gitea-repositories-meta/user30/renderer.git/config +++ /dev/null @@ -1,6 +0,0 @@ -[core] - repositoryformatversion = 0 - filemode = true - bare = true - ignorecase = true - precomposeunicode = true diff --git a/tests/gitea-repositories-meta/user30/renderer.git/description b/tests/gitea-repositories-meta/user30/renderer.git/description deleted file mode 100644 index 04c23973b8..0000000000 --- a/tests/gitea-repositories-meta/user30/renderer.git/description +++ /dev/null @@ -1 +0,0 @@ -The repository will be used to test third-party renderer in TestExternalMarkupRenderer diff --git a/tests/gitea-repositories-meta/user30/renderer.git/hooks/post-receive b/tests/gitea-repositories-meta/user30/renderer.git/hooks/post-receive deleted file mode 100644 index f1f2709ddd..0000000000 --- a/tests/gitea-repositories-meta/user30/renderer.git/hooks/post-receive +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -data=$(cat) -exitcodes="" -hookname=$(basename $0) -GIT_DIR=${GIT_DIR:-$(dirname $0)} - -for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do -test -x "${hook}" && test -f "${hook}" || continue -echo "${data}" | "${hook}" -exitcodes="${exitcodes} $?" -done - -for i in ${exitcodes}; do -[ ${i} -eq 0 ] || exit ${i} -done diff --git a/tests/gitea-repositories-meta/user30/renderer.git/hooks/post-receive.d/gitea b/tests/gitea-repositories-meta/user30/renderer.git/hooks/post-receive.d/gitea deleted file mode 100644 index 43a948da3a..0000000000 --- a/tests/gitea-repositories-meta/user30/renderer.git/hooks/post-receive.d/gitea +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" post-receive diff --git a/tests/gitea-repositories-meta/user30/renderer.git/hooks/pre-receive b/tests/gitea-repositories-meta/user30/renderer.git/hooks/pre-receive deleted file mode 100644 index f1f2709ddd..0000000000 --- a/tests/gitea-repositories-meta/user30/renderer.git/hooks/pre-receive +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -data=$(cat) -exitcodes="" -hookname=$(basename $0) -GIT_DIR=${GIT_DIR:-$(dirname $0)} - -for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do -test -x "${hook}" && test -f "${hook}" || continue -echo "${data}" | "${hook}" -exitcodes="${exitcodes} $?" -done - -for i in ${exitcodes}; do -[ ${i} -eq 0 ] || exit ${i} -done diff --git a/tests/gitea-repositories-meta/user30/renderer.git/hooks/pre-receive.d/gitea b/tests/gitea-repositories-meta/user30/renderer.git/hooks/pre-receive.d/gitea deleted file mode 100644 index 49d0940636..0000000000 --- a/tests/gitea-repositories-meta/user30/renderer.git/hooks/pre-receive.d/gitea +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" pre-receive diff --git a/tests/gitea-repositories-meta/user30/renderer.git/hooks/update b/tests/gitea-repositories-meta/user30/renderer.git/hooks/update deleted file mode 100644 index df5bd27f10..0000000000 --- a/tests/gitea-repositories-meta/user30/renderer.git/hooks/update +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -exitcodes="" -hookname=$(basename $0) -GIT_DIR=${GIT_DIR:-$(dirname $0)} - -for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do -test -x "${hook}" && test -f "${hook}" || continue -"${hook}" $1 $2 $3 -exitcodes="${exitcodes} $?" -done - -for i in ${exitcodes}; do -[ ${i} -eq 0 ] || exit ${i} -done diff --git a/tests/gitea-repositories-meta/user30/renderer.git/hooks/update.d/gitea b/tests/gitea-repositories-meta/user30/renderer.git/hooks/update.d/gitea deleted file mode 100644 index 38101c2426..0000000000 --- a/tests/gitea-repositories-meta/user30/renderer.git/hooks/update.d/gitea +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" update $1 $2 $3 diff --git a/tests/gitea-repositories-meta/user30/renderer.git/objects/06/0d5c2acd8bf4b6f14010acd1a73d73392ec46e b/tests/gitea-repositories-meta/user30/renderer.git/objects/06/0d5c2acd8bf4b6f14010acd1a73d73392ec46e deleted file mode 100644 index 994f25602cd6d8b6a48735b7520ee0dc1f6c307b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56 zcmV-80LTA$0V^p=O;s?qWH2-^Ff%bx2y%6F@paY9D9O!Xa1~i;5b$=>VX^4DTsF-6 O*zDTpWdHz1$qy%Vr5Mrx diff --git a/tests/gitea-repositories-meta/user30/renderer.git/objects/45/14a93050edb2c3165bdd0a3c03be063e879e68 b/tests/gitea-repositories-meta/user30/renderer.git/objects/45/14a93050edb2c3165bdd0a3c03be063e879e68 deleted file mode 100644 index b1fff27753835c235faa542aeeb6708413501109..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50 zcmV-20L}k+0ZYosPf{>8XRt}hEVJX{EJ-acQAn*QNiE7t%uy&x%}YrwN-g5D(FaQd I08PXWv=3tx761SM diff --git a/tests/gitea-repositories-meta/user30/renderer.git/objects/c9/61cc4d1ba6b7ee1ba228a9a02b00b7746d8033 b/tests/gitea-repositories-meta/user30/renderer.git/objects/c9/61cc4d1ba6b7ee1ba228a9a02b00b7746d8033 deleted file mode 100644 index 66488767ae90c8665d6a1ce53faa21a44d6421d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 789 zcmV+w1M2*E0iBdd6RJiOg|p^Y^qxwXS{keJQelum6a#up&z~m`^d0ZaKV(IR{ye{jOY!1cC%%%V!vh8I{gc z*^l5bcijiXRoa8!-(PL!?v-_a7HwKJzu~_9190MIBXTS+ZUD_nEK7aqH*?DV*Ksiv z&#C~w{x!-Ts!9jzD2 z_XdAlFzj`?wdZzI%W6@3|8fdoI3llLm>|NZy5Am&(qDU$+gDRbcjWL2FDGcxmT+Lr zW;S5sCFrAKt4n$QxJjW*0m%`^hlS>2K(Cj%*3coTqgpC;yKOu?8Cplmgo~cYEp2GD zuM*i^8yyrc(N?QA1PT!QgADnzEbIR35UGbkbKP~h3GKo{?VV(sa{G9g$DU11*+p+# z{gBNn`rKML*`(1FxW~`okU%0jrS=UPd{0~-G96qUuaEroxD~j(+(I*Ev}adVzA7kA z`AVei*)@oao)l=vLsxG7vcIzD=Um@k(mNRIv@iStKN%*v#gpt}Ee-qEr8b%EWVLP5 zjZEe^?ly)e3K)w!Kk4E^xOZAus#65kvHCPurWKy8bBcV+@V!t+L^?I7=aQ?DPsYrv zo4wT_hTJGHiSSKN`T^3K_xPB^Cw9-4ZY00RNGgq&=476$3=d(jd&xoERPSZxTA`D3 z7}9k`0l}`<4h@)92n^ZK;{E&FSm|ZqCkDF0(t2Cc##>WKJUT6Bt449p#K(hKS`U&) zm;x7=VO7;-P3v@ZRIyvZd$Old<{bUp4Xf7Vve367%MKO3Xj))P%^5CF`3iOJy<92K z(aS-&B;3l`F~gZ2$I{P@NX|UcFY7QjAsXwfd>n>oqV*$vRz&Nu&wU#DcF`O64+Y{S znUiaT3=-DiZ48*)JR=*;3ER|a;Jf$^swEt=h TRdwGtFpT{@!xQxrb9G4{#m9=~ diff --git a/tests/gitea-repositories-meta/user30/renderer.git/packed-refs b/tests/gitea-repositories-meta/user30/renderer.git/packed-refs deleted file mode 100644 index 63f8af0f12..0000000000 --- a/tests/gitea-repositories-meta/user30/renderer.git/packed-refs +++ /dev/null @@ -1,2 +0,0 @@ -# pack-refs with: peeled fully-peeled sorted -c961cc4d1ba6b7ee1ba228a9a02b00b7746d8033 refs/heads/master diff --git a/tests/gitea-repositories-meta/user30/renderer.git/refs/.keep b/tests/gitea-repositories-meta/user30/renderer.git/refs/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integration/markup_external_test.go b/tests/integration/markup_external_test.go index 9985333cd7..b965766b5c 100644 --- a/tests/integration/markup_external_test.go +++ b/tests/integration/markup_external_test.go @@ -12,6 +12,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/external" "code.gitea.io/gitea/modules/setting" @@ -25,29 +26,45 @@ import ( func TestExternalMarkupRenderer(t *testing.T) { defer tests.PrepareTestEnv(t)() if !setting.Database.Type.IsSQLite3() { - t.Skip() + t.Skip("only SQLite3 test config supports external markup renderer") return } + const binaryContentPrefix = "any prefix text." + const binaryContent = binaryContentPrefix + "\xfe\xfe\xfe\x00\xff\xff" + detectedEncoding, _ := charset.DetectEncoding([]byte(binaryContent)) + assert.NotEqual(t, binaryContent, strings.ToValidUTF8(binaryContent, "?")) + assert.Equal(t, "ISO-8859-2", detectedEncoding) // even if the binary content can be detected as text encoding, it shouldn't affect the raw rendering + onGiteaRun(t, func(t *testing.T, _ *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - _, err := createFile(user2, repo1, "file.no-sanitizer", "master", `any content`) + _, err := createFileInBranch(user2, repo1, createFileInBranchOptions{}, map[string]string{ + "test.html": `
`, + "html.no-sanitizer": ``, + "bin.no-sanitizer": binaryContent, + }) require.NoError(t, err) t.Run("RenderNoSanitizer", func(t *testing.T) { - req := NewRequest(t, "GET", "/user2/repo1/src/branch/master/file.no-sanitizer") + req := NewRequest(t, "GET", "/user2/repo1/src/branch/master/html.no-sanitizer") resp := MakeRequest(t, req, http.StatusOK) - doc := NewHTMLParser(t, resp.Body) - div := doc.Find("div.file-view") + div := NewHTMLParser(t, resp.Body).Find("div.file-view") data, err := div.Html() assert.NoError(t, err) - assert.Equal(t, ``, strings.TrimSpace(data)) + assert.Equal(t, ``, strings.TrimSpace(data)) + + req = NewRequest(t, "GET", "/user2/repo1/src/branch/master/bin.no-sanitizer") + resp = MakeRequest(t, req, http.StatusOK) + div = NewHTMLParser(t, resp.Body).Find("div.file-view") + data, err = div.Html() + assert.NoError(t, err) + assert.Equal(t, strings.ReplaceAll(binaryContent, "\x00", ""), strings.TrimSpace(data)) // HTML template engine removes the null bytes }) }) t.Run("RenderContentDirectly", func(t *testing.T) { - req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html") + req := NewRequest(t, "GET", "/user2/repo1/src/branch/master/test.html") resp := MakeRequest(t, req, http.StatusOK) assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type")) @@ -55,18 +72,21 @@ func TestExternalMarkupRenderer(t *testing.T) { div := doc.Find("div.file-view") data, err := div.Html() assert.NoError(t, err) - assert.Equal(t, "
\n\ttest external renderer\n
", strings.TrimSpace(data)) + // the content is fully sanitized + assert.Equal(t, `
<script></script>
`, strings.TrimSpace(data)) }) - // above tested "no-sanitizer" mode, then we test iframe mode below + // above tested in-page rendering (no iframe), then we test iframe mode below r := markup.GetRendererByFileName("any-file.html").(*external.Renderer) defer test.MockVariableValue(&r.RenderContentMode, setting.RenderContentModeIframe)() + assert.True(t, r.NeedPostProcess()) r = markup.GetRendererByFileName("any-file.no-sanitizer").(*external.Renderer) defer test.MockVariableValue(&r.RenderContentMode, setting.RenderContentModeIframe)() + assert.False(t, r.NeedPostProcess()) t.Run("RenderContentInIFrame", func(t *testing.T) { t.Run("DefaultSandbox", func(t *testing.T) { - req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html") + req := NewRequest(t, "GET", "/user2/repo1/src/branch/master/test.html") t.Run("ParentPage", func(t *testing.T) { respParent := MakeRequest(t, req, http.StatusOK) @@ -77,31 +97,42 @@ func TestExternalMarkupRenderer(t *testing.T) { // default sandbox on parent page assert.Equal(t, "allow-scripts allow-popups", iframe.AttrOr("sandbox", "")) - assert.Equal(t, "/user30/renderer/render/branch/master/README.html", iframe.AttrOr("data-src", "")) + assert.Equal(t, "/user2/repo1/render/branch/master/test.html", iframe.AttrOr("data-src", "")) }) t.Run("SubPage", func(t *testing.T) { - req = NewRequest(t, "GET", "/user30/renderer/render/branch/master/README.html") + req = NewRequest(t, "GET", "/user2/repo1/render/branch/master/test.html") respSub := MakeRequest(t, req, http.StatusOK) assert.Equal(t, "text/html; charset=utf-8", respSub.Header().Get("Content-Type")) // default sandbox in sub page response assert.Equal(t, "frame-src 'self'; sandbox allow-scripts allow-popups", respSub.Header().Get("Content-Security-Policy")) - assert.Equal(t, "
\n\ttest external renderer\n
\n", respSub.Body.String()) + // FIXME: actually here is a bug (legacy design problem), the "PostProcess" will escape "
<script></script>
`, respSub.Body.String()) }) }) t.Run("NoSanitizerNoSandbox", func(t *testing.T) { - req := NewRequest(t, "GET", "/user2/repo1/src/branch/master/file.no-sanitizer") - respParent := MakeRequest(t, req, http.StatusOK) - iframe := NewHTMLParser(t, respParent.Body).Find("iframe.external-render-iframe") - assert.Equal(t, "/user2/repo1/render/branch/master/file.no-sanitizer", iframe.AttrOr("data-src", "")) + t.Run("BinaryContent", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/repo1/src/branch/master/bin.no-sanitizer") + respParent := MakeRequest(t, req, http.StatusOK) + iframe := NewHTMLParser(t, respParent.Body).Find("iframe.external-render-iframe") + assert.Equal(t, "/user2/repo1/render/branch/master/bin.no-sanitizer", iframe.AttrOr("data-src", "")) - req = NewRequest(t, "GET", "/user2/repo1/render/branch/master/file.no-sanitizer") - respSub := MakeRequest(t, req, http.StatusOK) + req = NewRequest(t, "GET", "/user2/repo1/render/branch/master/bin.no-sanitizer") + respSub := MakeRequest(t, req, http.StatusOK) + assert.Equal(t, binaryContent, respSub.Body.String()) // raw content should keep the raw bytes (including invalid UTF-8 bytes), and no "external-render-iframe" helpers - // no sandbox (disabled by RENDER_CONTENT_SANDBOX) - assert.Empty(t, iframe.AttrOr("sandbox", "")) - assert.Equal(t, "frame-src 'self'", respSub.Header().Get("Content-Security-Policy")) + // no sandbox (disabled by RENDER_CONTENT_SANDBOX) + assert.Empty(t, iframe.AttrOr("sandbox", "")) + assert.Equal(t, "frame-src 'self'", respSub.Header().Get("Content-Security-Policy")) + }) + + t.Run("HTMLContentWithExternalRenderIframeHelper", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/repo1/render/branch/master/html.no-sanitizer") + respSub := MakeRequest(t, req, http.StatusOK) + assert.Equal(t, ``, respSub.Body.String()) + assert.Equal(t, "frame-src 'self'", respSub.Header().Get("Content-Security-Policy")) + }) }) }) } diff --git a/tests/sqlite.ini.tmpl b/tests/sqlite.ini.tmpl index 9d184bce6a..61f7e2a46d 100644 --- a/tests/sqlite.ini.tmpl +++ b/tests/sqlite.ini.tmpl @@ -122,7 +122,7 @@ RENDER_CONTENT_MODE = sanitized [markup.no-sanitizer] ENABLED = true FILE_EXTENSIONS = .no-sanitizer -RENDER_COMMAND = echo '' +RENDER_COMMAND = go run tools/test-echo.go ; This test case is reused, at first it is used to test "no-sanitizer" (sandbox doesn't take effect here) ; Then it will be updated and used to test "iframe + sandbox-disabled" RENDER_CONTENT_MODE = no-sanitizer From 018156079ba62f2b35daa60da9520e206867325a Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 13 Nov 2025 19:19:51 -0800 Subject: [PATCH 4/8] Upgrade deps golang.org/x/crypto (#35952) --- go.mod | 4 ++-- go.sum | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 7d787cf11b..11ab491946 100644 --- a/go.mod +++ b/go.mod @@ -117,9 +117,9 @@ require ( github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-meta v1.1.0 gitlab.com/gitlab-org/api/client-go v0.142.4 - golang.org/x/crypto v0.42.0 + golang.org/x/crypto v0.43.0 golang.org/x/image v0.30.0 - golang.org/x/net v0.44.0 + golang.org/x/net v0.45.0 golang.org/x/oauth2 v0.30.0 golang.org/x/sync v0.17.0 golang.org/x/sys v0.37.0 diff --git a/go.sum b/go.sum index 02a710e7f0..29f5ba745f 100644 --- a/go.sum +++ b/go.sum @@ -840,8 +840,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -908,8 +908,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= -golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= +golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -987,8 +987,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 358de23a5057c9746b9349f73000d213958ae485 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 14 Nov 2025 11:49:57 +0800 Subject: [PATCH 5/8] Fix container push tag overwriting (#35936) Fix #35853 --- routers/api/packages/container/manifest.go | 37 +++++++++---------- .../api_packages_container_test.go | 36 ++++++++++++------ 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/routers/api/packages/container/manifest.go b/routers/api/packages/container/manifest.go index de40215aa7..e408f6ee3b 100644 --- a/routers/api/packages/container/manifest.go +++ b/routers/api/packages/container/manifest.go @@ -10,7 +10,6 @@ import ( "io" "os" "strings" - "time" "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" @@ -260,6 +259,13 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met return nil, err } + // "docker buildx imagetools create" multi-arch operations: + // {"type":"oci","is_tagged":false,"platform":"unknown/unknown"} + // {"type":"oci","is_tagged":false,"platform":"linux/amd64","layer_creation":["ADD file:9233f6f2237d79659a9521f7e390df217cec49f1a8aa3a12147bbca1956acdb9 in /","CMD [\"/bin/sh\"]"]} + // {"type":"oci","is_tagged":false,"platform":"unknown/unknown"} + // {"type":"oci","is_tagged":false,"platform":"linux/arm64","layer_creation":["ADD file:df53811312284306901fdaaff0a357a4bf40d631e662fe9ce6d342442e494b6c in /","CMD [\"/bin/sh\"]"]} + // {"type":"oci","is_tagged":true,"manifests":[{"platform":"linux/amd64","digest":"sha256:72bb73e706c0dec424d00a1febb21deaf1175a70ead009ad8b159729cfcf5769","size":2819478},{"platform":"linux/arm64","digest":"sha256:9e1426dd084a3221663b85ca1ee99d140c50b153917a5c5604c1f9b78229fd24","size":2716499},{"platform":"unknown/unknown","digest":"sha256:b93f03d0ae11b988243e1b2cd8d29accf5b9670547b7bd8c7d96abecc7283e6e","size":1798},{"platform":"unknown/unknown","digest":"sha256:f034b182ba66366c63a5d195c6dfcd3333c027409c0ac98e55ade36aaa3b2963","size":1798}]} + _pv := &packages_model.PackageVersion{ PackageID: p.ID, CreatorID: mci.Creator.ID, @@ -273,25 +279,16 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met log.Error("Error inserting package: %v", err) return nil, err } - - if container_module.IsMediaTypeImageIndex(mci.MediaType) { - if pv.CreatedUnix.AsTime().Before(time.Now().Add(-24 * time.Hour)) { - if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { - return nil, err - } - // keep download count on overwriting - _pv.DownloadCount = pv.DownloadCount - if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil { - if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { - log.Error("Error inserting package: %v", err) - return nil, err - } - } - } else { - err = packages_model.UpdateVersion(ctx, &packages_model.PackageVersion{ID: pv.ID, MetadataJSON: _pv.MetadataJSON}) - if err != nil { - return nil, err - } + if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { + return nil, err + } + // keep download count on overwriting + _pv.DownloadCount = pv.DownloadCount + pv, err = packages_model.GetOrInsertVersion(ctx, _pv) + if err != nil { + if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { + log.Error("Error inserting package: %v", err) + return nil, err } } } diff --git a/tests/integration/api_packages_container_test.go b/tests/integration/api_packages_container_test.go index 7e93cb47a2..3c2d8bac33 100644 --- a/tests/integration/api_packages_container_test.go +++ b/tests/integration/api_packages_container_test.go @@ -28,6 +28,7 @@ import ( oci "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestPackageContainer(t *testing.T) { @@ -70,13 +71,12 @@ func TestPackageContainer(t *testing.T) { manifestDigest := "sha256:4f10484d1c1bb13e3956b4de1cd42db8e0f14a75be1617b60f2de3cd59c803c6" manifestContent := `{"schemaVersion":2,"mediaType":"` + container_module.ContentTypeDockerDistributionManifestV2 + `","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d","size":1069},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4","size":32}]}` - manifestContentType := container_module.ContentTypeDockerDistributionManifestV2 untaggedManifestDigest := "sha256:4305f5f5572b9a426b88909b036e52ee3cf3d7b9c1b01fac840e90747f56623d" untaggedManifestContent := `{"schemaVersion":2,"mediaType":"` + oci.MediaTypeImageManifest + `","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d","size":1069},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4","size":32}]}` - indexManifestDigest := "sha256:bab112d6efb9e7f221995caaaa880352feb5bd8b1faf52fae8d12c113aa123ec" - indexManifestContent := `{"schemaVersion":2,"mediaType":"` + oci.MediaTypeImageIndex + `","manifests":[{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"` + manifestDigest + `","platform":{"os":"linux","architecture":"arm","variant":"v7"}},{"mediaType":"` + oci.MediaTypeImageManifest + `","digest":"` + untaggedManifestDigest + `","platform":{"os":"linux","architecture":"arm64","variant":"v8"}}]}` + indexManifestDigest := "sha256:2c6b5afb967d5de02795ee1d177c3746d005df4b4c2b829385b0d186b3414b6b" + indexManifestContent := `{"schemaVersion":2,"mediaType":"` + oci.MediaTypeImageIndex + `","is_tagged":true,"manifests":[{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"` + manifestDigest + `","platform":{"os":"linux","architecture":"arm","variant":"v7"}},{"mediaType":"` + oci.MediaTypeImageManifest + `","digest":"` + untaggedManifestDigest + `","platform":{"os":"linux","architecture":"arm64","variant":"v8"}}]}` anonymousToken := "" userToken := "" @@ -467,15 +467,16 @@ func TestPackageContainer(t *testing.T) { assert.NoError(t, err) assert.EqualValues(t, 1, pv.DownloadCount) - // Overwrite existing tag should keep the download count - req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, tag), strings.NewReader(manifestContent)). - AddTokenAuth(userToken). - SetHeader("Content-Type", oci.MediaTypeImageManifest) - MakeRequest(t, req, http.StatusCreated) + t.Run("OverwriteTagKeepDownloadCount", func(t *testing.T) { + req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, tag), strings.NewReader(manifestContent)). + AddTokenAuth(userToken). + SetHeader("Content-Type", oci.MediaTypeImageManifest) + MakeRequest(t, req, http.StatusCreated) - pv, err = packages_model.GetVersionByNameAndVersion(t.Context(), user.ID, packages_model.TypeContainer, image, tag) - assert.NoError(t, err) - assert.EqualValues(t, 1, pv.DownloadCount) + pv, err = packages_model.GetVersionByNameAndVersion(t.Context(), user.ID, packages_model.TypeContainer, image, tag) + assert.NoError(t, err) + assert.EqualValues(t, 1, pv.DownloadCount) + }) }) t.Run("HeadManifest", func(t *testing.T) { @@ -505,7 +506,7 @@ func TestPackageContainer(t *testing.T) { resp := MakeRequest(t, req, http.StatusOK) assert.Equal(t, strconv.Itoa(len(manifestContent)), resp.Header().Get("Content-Length")) - assert.Equal(t, manifestContentType, resp.Header().Get("Content-Type")) + assert.Equal(t, oci.MediaTypeImageManifest, resp.Header().Get("Content-Type")) // the manifest is overwritten by above OverwriteTagKeepDownloadCount assert.Equal(t, manifestDigest, resp.Header().Get("Docker-Content-Digest")) assert.Equal(t, manifestContent, resp.Body.String()) }) @@ -599,6 +600,17 @@ func TestPackageContainer(t *testing.T) { assert.True(t, pd.Files[0].File.IsLead) assert.Equal(t, oci.MediaTypeImageIndex, pd.Files[0].Properties.GetByName(container_module.PropertyMediaType)) assert.Equal(t, indexManifestDigest, pd.Files[0].Properties.GetByName(container_module.PropertyDigest)) + + lastPackageVersionID := pv.ID + t.Run("UploadAgain", func(t *testing.T) { + req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, multiTag), strings.NewReader(indexManifestContent)). + AddTokenAuth(userToken). + SetHeader("Content-Type", oci.MediaTypeImageIndex) + MakeRequest(t, req, http.StatusCreated) + pv, err := packages_model.GetVersionByNameAndVersion(t.Context(), user.ID, packages_model.TypeContainer, image, multiTag) + require.NoError(t, err) + assert.NotEqual(t, lastPackageVersionID, pv.ID) + }) }) t.Run("HeadBlob", func(t *testing.T) { From d6dc531d4be4f94dab1ef3b8e92ac9daa6fbb270 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Fri, 14 Nov 2025 05:21:05 +0100 Subject: [PATCH 6/8] Add GITEA_PR_INDEX env variable to githooks (#35938) `GITEA_PR_ID` is already part of the env variables available in the githooks, but it contains a database ID instead of commonly used index that is part of `owner/repo!index` --- modules/repository/env.go | 6 ++++-- services/pull/merge.go | 1 + services/pull/update_rebase.go | 1 + services/wiki/wiki.go | 2 ++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/repository/env.go b/modules/repository/env.go index 78e06f86fb..55a81f006e 100644 --- a/modules/repository/env.go +++ b/modules/repository/env.go @@ -25,6 +25,7 @@ const ( EnvKeyID = "GITEA_KEY_ID" // public key ID EnvDeployKeyID = "GITEA_DEPLOY_KEY_ID" EnvPRID = "GITEA_PR_ID" + EnvPRIndex = "GITEA_PR_INDEX" // not used by Gitea at the moment, it is for custom git hooks EnvPushTrigger = "GITEA_PUSH_TRIGGER" EnvIsInternal = "GITEA_INTERNAL_PUSH" EnvAppURL = "GITEA_ROOT_URL" @@ -50,11 +51,11 @@ func InternalPushingEnvironment(doer *user_model.User, repo *repo_model.Reposito // PushingEnvironment returns an os environment to allow hooks to work on push func PushingEnvironment(doer *user_model.User, repo *repo_model.Repository) []string { - return FullPushingEnvironment(doer, doer, repo, repo.Name, 0) + return FullPushingEnvironment(doer, doer, repo, repo.Name, 0, 0) } // FullPushingEnvironment returns an os environment to allow hooks to work on push -func FullPushingEnvironment(author, committer *user_model.User, repo *repo_model.Repository, repoName string, prID int64) []string { +func FullPushingEnvironment(author, committer *user_model.User, repo *repo_model.Repository, repoName string, prID, prIndex int64) []string { isWiki := "false" if strings.HasSuffix(repoName, ".wiki") { isWiki = "true" @@ -75,6 +76,7 @@ func FullPushingEnvironment(author, committer *user_model.User, repo *repo_model EnvPusherID+"="+strconv.FormatInt(committer.ID, 10), EnvRepoID+"="+strconv.FormatInt(repo.ID, 10), EnvPRID+"="+strconv.FormatInt(prID, 10), + EnvPRIndex+"="+strconv.FormatInt(prIndex, 10), EnvAppURL+"="+setting.AppURL, "SSH_ORIGINAL_COMMAND=gitea-internal", ) diff --git a/services/pull/merge.go b/services/pull/merge.go index 9c7e09a227..f5430546a3 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -403,6 +403,7 @@ func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *use pr.BaseRepo, pr.BaseRepo.Name, pr.ID, + pr.Index, ) mergeCtx.env = append(mergeCtx.env, repo_module.EnvPushTrigger+"="+string(pushTrigger)) diff --git a/services/pull/update_rebase.go b/services/pull/update_rebase.go index e6845f6b14..6a70c03467 100644 --- a/services/pull/update_rebase.go +++ b/services/pull/update_rebase.go @@ -80,6 +80,7 @@ func updateHeadByRebaseOnToBase(ctx context.Context, pr *issues_model.PullReques pr.HeadRepo, pr.HeadRepo.Name, pr.ID, + pr.Index, )). WithDir(mergeCtx.tmpBasePath). WithStdout(mergeCtx.outbuf). diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index 25f836dd5d..a9dc726982 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -223,6 +223,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model repo, repo.Name+".wiki", 0, + 0, ), }); err != nil { log.Error("Push failed: %v", err) @@ -341,6 +342,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model repo, repo.Name+".wiki", 0, + 0, ), }); err != nil { if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) { From 0fb3be7f0e5720915fd7866d29fbf66828aff71a Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 14 Nov 2025 12:50:48 +0800 Subject: [PATCH 7/8] Fix diff blob excerpt expansion (#35922) And add comments and tests --- routers/web/repo/compare.go | 39 +++++----- services/gitdiff/gitdiff.go | 42 ++++++++--- services/gitdiff/gitdiff_test.go | 123 +++++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 33 deletions(-) diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index f3375e4898..7750278a8d 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -9,7 +9,6 @@ import ( "encoding/csv" "errors" "fmt" - "html" "io" "net/http" "net/url" @@ -957,30 +956,26 @@ func ExcerptBlob(ctx *context.Context) { ctx.HTTPError(http.StatusInternalServerError, "getExcerptLines") return } - if idxRight > lastRight { - lineText := " " - if rightHunkSize > 0 || leftHunkSize > 0 { - lineText = fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", idxLeft, leftHunkSize, idxRight, rightHunkSize) - } - lineText = html.EscapeString(lineText) - lineSection := &gitdiff.DiffLine{ - Type: gitdiff.DiffLineSection, - Content: lineText, - SectionInfo: &gitdiff.DiffLineSectionInfo{ - Path: filePath, - LastLeftIdx: lastLeft, - LastRightIdx: lastRight, - LeftIdx: idxLeft, - RightIdx: idxRight, - LeftHunkSize: leftHunkSize, - RightHunkSize: rightHunkSize, - }, - } + + newLineSection := &gitdiff.DiffLine{ + Type: gitdiff.DiffLineSection, + SectionInfo: &gitdiff.DiffLineSectionInfo{ + Path: filePath, + LastLeftIdx: lastLeft, + LastRightIdx: lastRight, + LeftIdx: idxLeft, + RightIdx: idxRight, + LeftHunkSize: leftHunkSize, + RightHunkSize: rightHunkSize, + }, + } + if newLineSection.GetExpandDirection() != "" { + newLineSection.Content = fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", idxLeft, leftHunkSize, idxRight, rightHunkSize) switch direction { case "up": - section.Lines = append([]*gitdiff.DiffLine{lineSection}, section.Lines...) + section.Lines = append([]*gitdiff.DiffLine{newLineSection}, section.Lines...) case "down": - section.Lines = append(section.Lines, lineSection) + section.Lines = append(section.Lines, newLineSection) } } diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 830bb1131b..4ad06bc04f 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -82,14 +82,34 @@ type DiffLine struct { // DiffLineSectionInfo represents diff line section meta data type DiffLineSectionInfo struct { - Path string - LastLeftIdx int - LastRightIdx int - LeftIdx int - RightIdx int + Path string + + // These line "idx" are 1-based line numbers + // Left/Right refer to the left/right side of the diff: + // + // LastLeftIdx | LastRightIdx + // [up/down expander] @@ hunk info @@ + // LeftIdx | RightIdx + + LastLeftIdx int + LastRightIdx int + LeftIdx int + RightIdx int + + // Hunk sizes of the hidden lines LeftHunkSize int RightHunkSize int + // For example: + // 17 | 31 + // [up/down] @@ -40,23 +54,9 @@ .... + // 40 | 54 + // + // In this case: + // LastLeftIdx = 17, LastRightIdx = 31 + // LeftHunkSize = 23, RightHunkSize = 9 + // LeftIdx = 40, RightIdx = 54 + HiddenCommentIDs []int64 // IDs of hidden comments in this section } @@ -158,13 +178,13 @@ func (d *DiffLine) getBlobExcerptQuery() string { return query } -func (d *DiffLine) getExpandDirection() string { +func (d *DiffLine) GetExpandDirection() string { if d.Type != DiffLineSection || d.SectionInfo == nil || d.SectionInfo.LeftIdx-d.SectionInfo.LastLeftIdx <= 1 || d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx <= 1 { return "" } if d.SectionInfo.LastLeftIdx <= 0 && d.SectionInfo.LastRightIdx <= 0 { return "up" - } else if d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx > BlobExcerptChunkSize && d.SectionInfo.RightHunkSize > 0 { + } else if d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx-1 > BlobExcerptChunkSize && d.SectionInfo.RightHunkSize > 0 { return "updown" } else if d.SectionInfo.LeftHunkSize <= 0 && d.SectionInfo.RightHunkSize <= 0 { return "down" @@ -202,13 +222,13 @@ func (d *DiffLine) RenderBlobExcerptButtons(fileNameHash string, data *DiffBlobE content += htmlutil.HTMLFormat(`%d`, tooltip, len(d.SectionInfo.HiddenCommentIDs)) } - expandDirection := d.getExpandDirection() - if expandDirection == "up" || expandDirection == "updown" { - content += makeButton("up", "octicon-fold-up") - } + expandDirection := d.GetExpandDirection() if expandDirection == "updown" || expandDirection == "down" { content += makeButton("down", "octicon-fold-down") } + if expandDirection == "up" || expandDirection == "updown" { + content += makeButton("up", "octicon-fold-up") + } if expandDirection == "single" { content += makeButton("single", "octicon-fold") } diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go index 51fb9b58d6..721ae0dfc7 100644 --- a/services/gitdiff/gitdiff_test.go +++ b/services/gitdiff/gitdiff_test.go @@ -983,3 +983,126 @@ func TestDiffLine_RenderBlobExcerptButtons(t *testing.T) { }) } } + +func TestDiffLine_GetExpandDirection(t *testing.T) { + cases := []struct { + name string + diffLine *DiffLine + direction string + }{ + { + name: "NotSectionLine", + diffLine: &DiffLine{Type: DiffLineAdd, SectionInfo: &DiffLineSectionInfo{}}, + direction: "", + }, + { + name: "NilSectionInfo", + diffLine: &DiffLine{Type: DiffLineSection, SectionInfo: nil}, + direction: "", + }, + { + name: "NoHiddenLines", + // last block stops at line 100, next block starts at line 101, so no hidden lines, no expansion. + diffLine: &DiffLine{ + Type: DiffLineSection, + SectionInfo: &DiffLineSectionInfo{ + LastRightIdx: 100, + LastLeftIdx: 100, + RightIdx: 101, + LeftIdx: 101, + }, + }, + direction: "", + }, + { + name: "FileHead", + diffLine: &DiffLine{ + Type: DiffLineSection, + SectionInfo: &DiffLineSectionInfo{ + LastRightIdx: 0, // LastXxxIdx = 0 means this is the first section in the file. + LastLeftIdx: 0, + RightIdx: 1, + LeftIdx: 1, + }, + }, + direction: "", + }, + { + name: "FileHeadHiddenLines", + diffLine: &DiffLine{ + Type: DiffLineSection, + SectionInfo: &DiffLineSectionInfo{ + LastRightIdx: 0, + LastLeftIdx: 0, + RightIdx: 101, + LeftIdx: 101, + }, + }, + direction: "up", + }, + { + name: "HiddenSingleHunk", + diffLine: &DiffLine{ + Type: DiffLineSection, + SectionInfo: &DiffLineSectionInfo{ + LastRightIdx: 100, + LastLeftIdx: 100, + RightIdx: 102, + LeftIdx: 102, + RightHunkSize: 1234, // non-zero dummy value + LeftHunkSize: 5678, // non-zero dummy value + }, + }, + direction: "single", + }, + { + name: "HiddenSingleFullHunk", + // the hidden lines can exactly fit into one hunk + diffLine: &DiffLine{ + Type: DiffLineSection, + SectionInfo: &DiffLineSectionInfo{ + LastRightIdx: 100, + LastLeftIdx: 100, + RightIdx: 100 + BlobExcerptChunkSize + 1, + LeftIdx: 100 + BlobExcerptChunkSize + 1, + RightHunkSize: 1234, // non-zero dummy value + LeftHunkSize: 5678, // non-zero dummy value + }, + }, + direction: "single", + }, + { + name: "HiddenUpDownHunks", + diffLine: &DiffLine{ + Type: DiffLineSection, + SectionInfo: &DiffLineSectionInfo{ + LastRightIdx: 100, + LastLeftIdx: 100, + RightIdx: 100 + BlobExcerptChunkSize + 2, + LeftIdx: 100 + BlobExcerptChunkSize + 2, + RightHunkSize: 1234, // non-zero dummy value + LeftHunkSize: 5678, // non-zero dummy value + }, + }, + direction: "updown", + }, + { + name: "FileTail", + diffLine: &DiffLine{ + Type: DiffLineSection, + SectionInfo: &DiffLineSectionInfo{ + LastRightIdx: 100, + LastLeftIdx: 100, + RightIdx: 102, + LeftIdx: 102, + RightHunkSize: 0, + LeftHunkSize: 0, + }, + }, + direction: "down", + }, + } + for _, c := range cases { + assert.Equal(t, c.direction, c.diffLine.GetExpandDirection(), "case %s expected direction: %s", c.name, c.direction) + } +} From de69e7f16a9b4158c4a03b5d5ef9654c766f9518 Mon Sep 17 00:00:00 2001 From: DrMaxNix Date: Tue, 18 Nov 2025 08:55:27 +0000 Subject: [PATCH 8/8] Change project default column icon to 'star' (#35967) Consistently use a `star` icon to highlight the default column of a project. The icon is both shown while viewing the project, as well as while changing the default status of this column. image --- options/locale/locale_en-US.ini | 1 + templates/projects/view.tmpl | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index ddc12aefaa..ea69d45fa2 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1482,6 +1482,7 @@ projects.column.new_submit = "Create Column" projects.column.new = "New Column" projects.column.set_default = "Set Default" projects.column.set_default_desc = "Set this column as default for uncategorized issues and pulls" +projects.column.default_column_hint = "New issues added to this project will be added to this column" projects.column.delete = "Delete Column" projects.column.deletion_desc = "Deleting a project column moves all related issues to the default column. Continue?" projects.column.color = "Color" diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 21bc287643..5801396e3c 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -78,7 +78,9 @@
{{.NumIssues}}
-
{{.Title}}
+
+ {{if .Default}}{{svg "octicon-star"}} {{end}}{{.Title}} +
{{if $canWriteProject}}