From 577ed107ddb7c054c0d55e5c5e2027d015116ee0 Mon Sep 17 00:00:00 2001 From: Viktor Suprun Date: Thu, 26 Feb 2026 01:54:02 +1100 Subject: [PATCH 1/5] Fix SVG height calculation in diff viewer (#36748) Fixes #36742 --- web_src/js/features/imagediff.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/features/imagediff.ts b/web_src/js/features/imagediff.ts index 23f05fbdc7..1e89aa8b6e 100644 --- a/web_src/js/features/imagediff.ts +++ b/web_src/js/features/imagediff.ts @@ -53,7 +53,7 @@ function getDefaultSvgBoundsIfUndefined(text: string, src: string): Bounds | nul const viewBox = svg.viewBox.baseVal; return { width: defaultSize, - height: defaultSize * viewBox.width / viewBox.height, + height: defaultSize * viewBox.height / viewBox.width, }; } return { From 569c49debe06f30a2bbb50b3812e705c556b8adf Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 25 Feb 2026 08:28:39 -0800 Subject: [PATCH 2/5] Add validation constraints for repository creation fields (#36671) Adds validation constraints to repository creation inputs, enforcing max-length limits for labels/license/readme and enum validation for trust model and object format. Updates both the API option struct and the web form struct to keep validation consistent. --- modules/structs/repo.go | 8 ++++---- services/forms/repo_form.go | 8 ++++---- services/repository/create.go | 3 +++ templates/swagger/v1_json.tmpl | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 765546a5aa..a08cf36037 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -135,7 +135,7 @@ type CreateRepoOption struct { // Whether the repository is private Private bool `json:"private"` // Label-Set to use - IssueLabels string `json:"issue_labels"` + IssueLabels string `json:"issue_labels" binding:"MaxSize(255)"` // Whether the repository should be auto-initialized? AutoInit bool `json:"auto_init"` // Whether the repository is template @@ -143,15 +143,15 @@ type CreateRepoOption struct { // Gitignores to use Gitignores string `json:"gitignores"` // License to use - License string `json:"license"` + License string `json:"license" binding:"MaxSize(100)"` // Readme of the repository to create - Readme string `json:"readme"` + Readme string `json:"readme" binding:"MaxSize(255)"` // DefaultBranch of the repository (used when initializes and in template) DefaultBranch string `json:"default_branch" binding:"GitRefName;MaxSize(100)"` // TrustModel of the repository // enum: default,collaborator,committer,collaboratorcommitter TrustModel string `json:"trust_model"` - // ObjectFormatName of the underlying git repository + // ObjectFormatName of the underlying git repository, empty string for default (sha1) // enum: sha1,sha256 ObjectFormatName string `json:"object_format_name" binding:"MaxSize(6)"` } diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 765a723968..8b69c6bcc6 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -27,9 +27,9 @@ type CreateRepoForm struct { DefaultBranch string `binding:"GitRefName;MaxSize(100)"` AutoInit bool Gitignores string - IssueLabels string - License string - Readme string + IssueLabels string `binding:"MaxSize(255)"` + License string `binding:"MaxSize(100)"` + Readme string `binding:"MaxSize(255)"` Template bool RepoTemplate int64 @@ -41,7 +41,7 @@ type CreateRepoForm struct { Labels bool ProtectedBranch bool - ForkSingleBranch string + ForkSingleBranch string `binding:"MaxSize(255)"` ObjectFormatName string } diff --git a/services/repository/create.go b/services/repository/create.go index cbdc9cca76..e027d3b979 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -230,6 +230,9 @@ func CreateRepositoryDirectly(ctx context.Context, doer, owner *user_model.User, if opts.ObjectFormatName == "" { opts.ObjectFormatName = git.Sha1ObjectFormat.Name() } + if opts.ObjectFormatName != git.Sha1ObjectFormat.Name() && opts.ObjectFormatName != git.Sha256ObjectFormat.Name() { + return nil, fmt.Errorf("unsupported object format: %s", opts.ObjectFormatName) + } repo := &repo_model.Repository{ OwnerID: owner.ID, diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 570747ca57..a1ecc7fb4f 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -23780,7 +23780,7 @@ "x-go-name": "Name" }, "object_format_name": { - "description": "ObjectFormatName of the underlying git repository", + "description": "ObjectFormatName of the underlying git repository, empty string for default (sha1)", "type": "string", "enum": [ "sha1", From 0de8a3d3d8346d04c32934de554cb2ab66fbc294 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 25 Feb 2026 21:08:08 +0100 Subject: [PATCH 3/5] Avoid opening new tab when downloading actions logs (#36740) `target="_blank"` causes the browser to flash a new tab when actions logs are downloaded. Using the [`download`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/a#download) attribute fixes this. --- web_src/js/components/RepoActionView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index efa3472e8c..4a8da7b11d 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -626,7 +626,7 @@ export default defineComponent({
- + {{ locale.downloadLogs }} From 9ae28b6f3985992f4e761e96c215fc21cfa2773a Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 25 Feb 2026 21:20:28 +0100 Subject: [PATCH 4/5] Change image transparency grid to CSS (#36711) These new colors work much better on dark theme than before (where it was far too bright). image image --------- Co-authored-by: Giteabot Co-authored-by: Claude Opus 4.6 --- web_src/css/base.css | 2 +- web_src/css/themes/theme-gitea-dark.css | 2 ++ web_src/css/themes/theme-gitea-light.css | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/web_src/css/base.css b/web_src/css/base.css index 5a75aaaee6..3fa5c1246c 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -42,7 +42,7 @@ --gap-inline: 0.25rem; /* gap for inline texts and elements, for example: the spaces for sentence with labels, button text, etc */ --gap-block: 0.5rem; /* gap for element blocks, for example: spaces between buttons, menu image & title, header icon & title etc */ - --background-view-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAG0lEQVQYlWN4+vTpf3SMDTAMBYXYBLFpHgoKAeiOf0SGE9kbAAAAAElFTkSuQmCC") right bottom var(--color-primary-light-7); + --background-view-image: repeating-conic-gradient(var(--color-transparency-grid-dark) 0 25%, var(--color-transparency-grid-light) 0 50%) 0 0 / 18px 18px; } @media (min-width: 768px) and (max-width: 1200px) { diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css index f58c222c9a..ad5eec9e82 100644 --- a/web_src/css/themes/theme-gitea-dark.css +++ b/web_src/css/themes/theme-gitea-dark.css @@ -247,6 +247,8 @@ gitea-theme-meta-info { --color-highlight-bg: #352c1c; --color-overlay-backdrop: #080808c0; --color-danger: var(--color-red); + --color-transparency-grid-light: #2a2a2a; + --color-transparency-grid-dark: #1a1a1a; accent-color: var(--color-accent); color-scheme: dark; } diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css index 8766bf7abc..049b64f73f 100644 --- a/web_src/css/themes/theme-gitea-light.css +++ b/web_src/css/themes/theme-gitea-light.css @@ -247,6 +247,8 @@ gitea-theme-meta-info { --color-highlight-bg: #fffbdd; --color-overlay-backdrop: #080808c0; --color-danger: var(--color-red); + --color-transparency-grid-light: #fafafa; + --color-transparency-grid-dark: #e2e2e2; accent-color: var(--color-accent); color-scheme: light; } From 840cf68c3e7d8cb7dcd5e2038935139a3d16241d Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 26 Feb 2026 04:59:29 +0800 Subject: [PATCH 5/5] Fix release draft access check logic (#36720) 1. remove hasRepoWriteScope to avoid abuse 2. clarify "ctx.Written" behavior 3. merge "read-only" tests to slightly improve performance --- routers/api/v1/repo/release.go | 42 +++++++------------ routers/api/v1/repo/release_attachment.go | 20 +++------ .../api_releases_attachment_test.go | 9 +--- tests/integration/api_releases_test.go | 32 +++++++------- 4 files changed, 38 insertions(+), 65 deletions(-) diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go index 4f17590abd..ff43628fa5 100644 --- a/routers/api/v1/repo/release.go +++ b/routers/api/v1/repo/release.go @@ -10,7 +10,6 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/git" @@ -22,26 +21,19 @@ import ( release_service "code.gitea.io/gitea/services/release" ) -func hasRepoWriteScope(ctx *context.APIContext) bool { - scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) - if ctx.Data["IsApiToken"] != true || !ok { - return true - } - - requiredScopes := auth_model.GetRequiredScopes(auth_model.Write, auth_model.AccessTokenScopeCategoryRepository) - allow, err := scope.HasScope(requiredScopes...) - if err != nil { - ctx.APIError(http.StatusForbidden, "checking scope failed: "+err.Error()) - return false - } - return allow -} - -func canAccessDraftRelease(ctx *context.APIContext) bool { +func canAccessReleaseDraft(ctx *context.APIContext) bool { if !ctx.IsSigned || !ctx.Repo.CanWrite(unit.TypeReleases) { return false } - return hasRepoWriteScope(ctx) + if ctx.Data["IsApiToken"] != true { + // not API token request, the request is from a user session with write access + return true + } + // the request is from an access token with scope + scope := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) + requiredScopes := auth_model.GetRequiredScopes(auth_model.Write, auth_model.AccessTokenScopeCategoryRepository) + allow, _ := scope.HasScope(requiredScopes...) // err (invalid token) can be safely ignored + return allow } // GetRelease get a single release of a repository @@ -85,13 +77,9 @@ func GetRelease(ctx *context.APIContext) { return } - if release.IsDraft { // only the users with write access can see draft releases - if !canAccessDraftRelease(ctx) { - if !ctx.Written() { - ctx.APIErrorNotFound() - } - return - } + if release.IsDraft && !canAccessReleaseDraft(ctx) { // only the users with write access can see draft releases + ctx.APIErrorNotFound() + return } if err := release.LoadAttributes(ctx); err != nil { @@ -182,14 +170,12 @@ func ListReleases(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" listOptions := utils.GetListOptions(ctx) - - includeDrafts := (ctx.Repo.AccessMode >= perm.AccessModeWrite || ctx.Repo.UnitAccessMode(unit.TypeReleases) >= perm.AccessModeWrite) && hasRepoWriteScope(ctx) if ctx.Written() { return } opts := repo_model.FindReleasesOptions{ ListOptions: listOptions, - IncludeDrafts: includeDrafts, + IncludeDrafts: canAccessReleaseDraft(ctx), IncludeTags: false, IsDraft: ctx.FormOptionalBool("draft"), IsPreRelease: ctx.FormOptionalBool("pre-release"), diff --git a/routers/api/v1/repo/release_attachment.go b/routers/api/v1/repo/release_attachment.go index 6b30070db8..19075961f3 100644 --- a/routers/api/v1/repo/release_attachment.go +++ b/routers/api/v1/repo/release_attachment.go @@ -34,13 +34,9 @@ func checkReleaseMatchRepo(ctx *context.APIContext, releaseID int64) bool { ctx.APIErrorNotFound() return false } - if release.IsDraft { - if !canAccessDraftRelease(ctx) { - if !ctx.Written() { - ctx.APIErrorNotFound() - } - return false - } + if release.IsDraft && !canAccessReleaseDraft(ctx) { + ctx.APIErrorNotFound() + return false } return true } @@ -149,13 +145,9 @@ func ListReleaseAttachments(ctx *context.APIContext) { ctx.APIErrorNotFound() return } - if release.IsDraft { - if !canAccessDraftRelease(ctx) { - if !ctx.Written() { - ctx.APIErrorNotFound() - } - return - } + if release.IsDraft && !canAccessReleaseDraft(ctx) { + ctx.APIErrorNotFound() + return } if err := release.LoadAttributes(ctx); err != nil { ctx.APIErrorInternal(err) diff --git a/tests/integration/api_releases_attachment_test.go b/tests/integration/api_releases_attachment_test.go index e859b23c72..3f2592e331 100644 --- a/tests/integration/api_releases_attachment_test.go +++ b/tests/integration/api_releases_attachment_test.go @@ -15,16 +15,13 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" - "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" ) -func TestAPIEditReleaseAttachmentWithUnallowedFile(t *testing.T) { +func testAPIEditReleaseAttachmentWithUnallowedFile(t *testing.T) { // Limit the allowed release types (since by default there is no restriction) defer test.MockVariableValue(&setting.Repository.Release.AllowedTypes, ".exe")() - defer tests.PrepareTestEnv(t)() - attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 9}) release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: attachment.ReleaseID}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID}) @@ -42,9 +39,7 @@ func TestAPIEditReleaseAttachmentWithUnallowedFile(t *testing.T) { session.MakeRequest(t, req, http.StatusUnprocessableEntity) } -func TestAPIDraftReleaseAttachmentAccess(t *testing.T) { - defer tests.PrepareTestEnv(t)() - +func testAPIDraftReleaseAttachmentAccess(t *testing.T) { attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 13}) release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: attachment.ReleaseID}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID}) diff --git a/tests/integration/api_releases_test.go b/tests/integration/api_releases_test.go index c7200129dd..c7f1343dde 100644 --- a/tests/integration/api_releases_test.go +++ b/tests/integration/api_releases_test.go @@ -29,9 +29,19 @@ import ( "github.com/stretchr/testify/assert" ) -func TestAPIListReleasesWithWriteToken(t *testing.T) { +func TestAPIReleaseRead(t *testing.T) { defer tests.PrepareTestEnv(t)() + t.Run("DraftReleaseAttachmentAccess", testAPIDraftReleaseAttachmentAccess) + t.Run("ListReleasesWithWriteToken", testAPIListReleasesWithWriteToken) + t.Run("ListReleasesWithReadToken", testAPIListReleasesWithReadToken) + t.Run("GetDraftRelease", testAPIGetDraftRelease) + t.Run("GetLatestRelease", testAPIGetLatestRelease) + t.Run("GetReleaseByTag", testAPIGetReleaseByTag) + t.Run("GetDraftReleaseByTag", testAPIGetDraftReleaseByTag) + t.Run("EditReleaseAttachmentWithUnallowedFile", testAPIEditReleaseAttachmentWithUnallowedFile) // failed attempt, so it is also a read test +} +func testAPIListReleasesWithWriteToken(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) token := getUserToken(t, user2.LowerName, auth_model.AccessTokenScopeWriteRepository) @@ -81,9 +91,7 @@ func TestAPIListReleasesWithWriteToken(t *testing.T) { testFilterByLen(true, url.Values{"draft": {"true"}, "pre-release": {"true"}}, 0, "there is no pre-release draft") } -func TestAPIListReleasesWithReadToken(t *testing.T) { - defer tests.PrepareTestEnv(t)() - +func testAPIListReleasesWithReadToken(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) token := getUserToken(t, user2.LowerName, auth_model.AccessTokenScopeReadRepository) @@ -129,9 +137,7 @@ func TestAPIListReleasesWithReadToken(t *testing.T) { testFilterByLen(true, url.Values{"draft": {"true"}, "pre-release": {"true"}}, 0, "there is no pre-release draft") } -func TestAPIGetDraftRelease(t *testing.T) { - defer tests.PrepareTestEnv(t)() - +func testAPIGetDraftRelease(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: 4}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) @@ -300,9 +306,7 @@ func TestAPICreateReleaseGivenInvalidTarget(t *testing.T) { MakeRequest(t, req, http.StatusNotFound) } -func TestAPIGetLatestRelease(t *testing.T) { - defer tests.PrepareTestEnv(t)() - +func testAPIGetLatestRelease(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) @@ -315,9 +319,7 @@ func TestAPIGetLatestRelease(t *testing.T) { assert.Equal(t, "testing-release", release.Title) } -func TestAPIGetReleaseByTag(t *testing.T) { - defer tests.PrepareTestEnv(t)() - +func testAPIGetReleaseByTag(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) @@ -341,9 +343,7 @@ func TestAPIGetReleaseByTag(t *testing.T) { assert.NotEmpty(t, err.Message) } -func TestAPIGetDraftReleaseByTag(t *testing.T) { - defer tests.PrepareTestEnv(t)() - +func testAPIGetDraftReleaseByTag(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})