loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[gitea] week 2025-06 cherry pick (gitea/main -> forgejo) (#6763)

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6763
Reviewed-by: Gusted <gusted@noreply.codeberg.org>

+277 -9
+11 -7
models/user/avatar.go
··· 38 38 39 39 u.Avatar = avatars.HashEmail(seed) 40 40 41 - // Don't share the images so that we can delete them easily 42 - if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error { 43 - if err := png.Encode(w, img); err != nil { 44 - log.Error("Encode: %v", err) 41 + _, err = storage.Avatars.Stat(u.CustomAvatarRelativePath()) 42 + if err != nil { 43 + // If unable to Stat the avatar file (usually it means non-existing), then try to save a new one 44 + // Don't share the images so that we can delete them easily 45 + if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error { 46 + if err := png.Encode(w, img); err != nil { 47 + log.Error("Encode: %v", err) 48 + } 49 + return nil 50 + }); err != nil { 51 + return fmt.Errorf("failed to save avatar %s: %w", u.CustomAvatarRelativePath(), err) 45 52 } 46 - return err 47 - }); err != nil { 48 - return fmt.Errorf("Failed to create dir %s: %w", u.CustomAvatarRelativePath(), err) 49 53 } 50 54 51 55 if _, err := db.GetEngine(ctx).ID(u.ID).Cols("avatar").Update(u); err != nil {
+68
models/user/avatar_test.go
··· 1 + // Copyright 2024 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package user 5 + 6 + import ( 7 + "context" 8 + "io" 9 + "strings" 10 + "testing" 11 + 12 + "code.gitea.io/gitea/models/db" 13 + "code.gitea.io/gitea/models/unittest" 14 + "code.gitea.io/gitea/modules/setting" 15 + "code.gitea.io/gitea/modules/storage" 16 + "code.gitea.io/gitea/modules/test" 17 + 18 + "github.com/stretchr/testify/assert" 19 + "github.com/stretchr/testify/require" 20 + ) 21 + 22 + func TestUserAvatarLink(t *testing.T) { 23 + defer test.MockVariableValue(&setting.AppURL, "https://localhost/")() 24 + defer test.MockVariableValue(&setting.AppSubURL, "")() 25 + 26 + u := &User{ID: 1, Avatar: "avatar.png"} 27 + link := u.AvatarLink(db.DefaultContext) 28 + assert.Equal(t, "https://localhost/avatars/avatar.png", link) 29 + 30 + setting.AppURL = "https://localhost/sub-path/" 31 + setting.AppSubURL = "/sub-path" 32 + link = u.AvatarLink(db.DefaultContext) 33 + assert.Equal(t, "https://localhost/sub-path/avatars/avatar.png", link) 34 + } 35 + 36 + func TestUserAvatarGenerate(t *testing.T) { 37 + require.NoError(t, unittest.PrepareTestDatabase()) 38 + var err error 39 + tmpDir := t.TempDir() 40 + storage.Avatars, err = storage.NewLocalStorage(context.Background(), &setting.Storage{Path: tmpDir}) 41 + require.NoError(t, err) 42 + 43 + u := unittest.AssertExistsAndLoadBean(t, &User{ID: 2}) 44 + 45 + // there was no avatar, generate a new one 46 + assert.Empty(t, u.Avatar) 47 + err = GenerateRandomAvatar(db.DefaultContext, u) 48 + require.NoError(t, err) 49 + assert.NotEmpty(t, u.Avatar) 50 + 51 + // make sure the generated one exists 52 + oldAvatarPath := u.CustomAvatarRelativePath() 53 + _, err = storage.Avatars.Stat(u.CustomAvatarRelativePath()) 54 + require.NoError(t, err) 55 + // and try to change its content 56 + _, err = storage.Avatars.Save(u.CustomAvatarRelativePath(), strings.NewReader("abcd"), 4) 57 + require.NoError(t, err) 58 + 59 + // try to generate again 60 + err = GenerateRandomAvatar(db.DefaultContext, u) 61 + require.NoError(t, err) 62 + assert.Equal(t, oldAvatarPath, u.CustomAvatarRelativePath()) 63 + f, err := storage.Avatars.Open(u.CustomAvatarRelativePath()) 64 + require.NoError(t, err) 65 + defer f.Close() 66 + content, _ := io.ReadAll(f) 67 + assert.Equal(t, "abcd", string(content)) 68 + }
+6 -2
modules/lfs/http_client.go
··· 72 72 73 73 url := fmt.Sprintf("%s/objects/batch", c.endpoint) 74 74 75 + // Original: In some lfs server implementations, they require the ref attribute. #32838 75 76 // `ref` is an "optional object describing the server ref that the objects belong to" 76 - // but some (incorrect) lfs servers require it, so maybe adding an empty ref here doesn't break the correct ones. 77 + // but some (incorrect) lfs servers like aliyun require it, so maybe adding an empty ref here doesn't break the correct ones. 77 78 // https://github.com/git-lfs/git-lfs/blob/a32a02b44bf8a511aa14f047627c49e1a7fd5021/docs/api/batch.md?plain=1#L37 78 - request := &BatchRequest{operation, c.transferNames(), &Reference{}, objects} 79 + // 80 + // UPDATE: it can't use "empty ref" here because it breaks others like https://github.com/go-gitea/gitea/issues/33453 81 + request := &BatchRequest{operation, c.transferNames(), nil, objects} 82 + 79 83 payload := new(bytes.Buffer) 80 84 err := json.NewEncoder(payload).Encode(request) 81 85 if err != nil {
+9
modules/structs/org.go
··· 57 57 Visibility string `json:"visibility" binding:"In(,public,limited,private)"` 58 58 RepoAdminChangeTeamAccess *bool `json:"repo_admin_change_team_access"` 59 59 } 60 + 61 + // RenameOrgOption options when renaming an organization 62 + type RenameOrgOption struct { 63 + // New username for this org. This name cannot be in use yet by any other user. 64 + // 65 + // required: true 66 + // unique: true 67 + NewName string `json:"new_name" binding:"Required"` 68 + }
+1
release-notes/6763.md
··· 1 + feat: [commit](https://codeberg.org/forgejo/forgejo/commit/689fb82a7043fdb2fee02195701b0bc728e99709) API endpoint to rename an organization
+1
routers/api/v1/api.go
··· 1505 1505 m.Combo("").Get(org.Get). 1506 1506 Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit). 1507 1507 Delete(reqToken(), reqOrgOwnership(), org.Delete) 1508 + m.Post("/rename", reqToken(), reqOrgOwnership(), bind(api.RenameOrgOption{}), org.Rename) 1508 1509 m.Combo("/repos").Get(user.ListOrgRepos). 1509 1510 Post(reqToken(), bind(api.CreateRepoOption{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetOrg), repo.CreateOrgRepo) 1510 1511 m.Group("/members", func() {
+38
routers/api/v1/org/org.go
··· 320 320 ctx.JSON(http.StatusOK, org) 321 321 } 322 322 323 + func Rename(ctx *context.APIContext) { 324 + // swagger:operation POST /orgs/{org}/rename organization renameOrg 325 + // --- 326 + // summary: Rename an organization 327 + // produces: 328 + // - application/json 329 + // parameters: 330 + // - name: org 331 + // in: path 332 + // description: existing org name 333 + // type: string 334 + // required: true 335 + // - name: body 336 + // in: body 337 + // required: true 338 + // schema: 339 + // "$ref": "#/definitions/RenameOrgOption" 340 + // responses: 341 + // "204": 342 + // "$ref": "#/responses/empty" 343 + // "403": 344 + // "$ref": "#/responses/forbidden" 345 + // "422": 346 + // "$ref": "#/responses/validationError" 347 + 348 + form := web.GetForm(ctx).(*api.RenameOrgOption) 349 + orgUser := ctx.Org.Organization.AsUser() 350 + if err := user_service.RenameUser(ctx, orgUser, form.NewName); err != nil { 351 + if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) { 352 + ctx.Error(http.StatusUnprocessableEntity, "RenameOrg", err) 353 + } else { 354 + ctx.ServerError("RenameOrg", err) 355 + } 356 + return 357 + } 358 + ctx.Status(http.StatusNoContent) 359 + } 360 + 323 361 // Edit change an organization's information 324 362 func Edit(ctx *context.APIContext) { 325 363 // swagger:operation PATCH /orgs/{org} organization orgEdit
+3
routers/api/v1/swagger/options.go
··· 217 217 CreateVariableOption api.CreateVariableOption 218 218 219 219 // in:body 220 + RenameOrgOption api.RenameOrgOption 221 + 222 + // in:body 220 223 UpdateVariableOption api.UpdateVariableOption 221 224 222 225 // in:body
+1
routers/web/feed/branch.go
··· 43 43 }, 44 44 Description: commit.Message(), 45 45 Content: commit.Message(), 46 + Created: commit.Committer.When, 46 47 }) 47 48 } 48 49
+1
routers/web/feed/file.go
··· 55 55 }, 56 56 Description: commit.Message(), 57 57 Content: commit.Message(), 58 + Created: commit.Committer.When, 58 59 }) 59 60 } 60 61
+56
templates/swagger/v1_json.tmpl
··· 3772 3772 } 3773 3773 } 3774 3774 }, 3775 + "/orgs/{org}/rename": { 3776 + "post": { 3777 + "produces": [ 3778 + "application/json" 3779 + ], 3780 + "tags": [ 3781 + "organization" 3782 + ], 3783 + "summary": "Rename an organization", 3784 + "operationId": "renameOrg", 3785 + "parameters": [ 3786 + { 3787 + "type": "string", 3788 + "description": "existing org name", 3789 + "name": "org", 3790 + "in": "path", 3791 + "required": true 3792 + }, 3793 + { 3794 + "name": "body", 3795 + "in": "body", 3796 + "required": true, 3797 + "schema": { 3798 + "$ref": "#/definitions/RenameOrgOption" 3799 + } 3800 + } 3801 + ], 3802 + "responses": { 3803 + "204": { 3804 + "$ref": "#/responses/empty" 3805 + }, 3806 + "403": { 3807 + "$ref": "#/responses/forbidden" 3808 + }, 3809 + "422": { 3810 + "$ref": "#/responses/validationError" 3811 + } 3812 + } 3813 + } 3814 + }, 3775 3815 "/orgs/{org}/repos": { 3776 3816 "get": { 3777 3817 "produces": [ ··· 26630 26670 "zipball_url": { 26631 26671 "type": "string", 26632 26672 "x-go-name": "ZipURL" 26673 + } 26674 + }, 26675 + "x-go-package": "code.gitea.io/gitea/modules/structs" 26676 + }, 26677 + "RenameOrgOption": { 26678 + "description": "RenameOrgOption options when renaming an organization", 26679 + "type": "object", 26680 + "required": [ 26681 + "new_name" 26682 + ], 26683 + "properties": { 26684 + "new_name": { 26685 + "description": "New username for this org. This name cannot be in use yet by any other user.", 26686 + "type": "string", 26687 + "uniqueItems": true, 26688 + "x-go-name": "NewName" 26633 26689 } 26634 26690 }, 26635 26691 "x-go-package": "code.gitea.io/gitea/modules/structs"
+23
tests/integration/api_feed_user_test.go tests/integration/feed_user_test.go
··· 4 4 package integration 5 5 6 6 import ( 7 + "encoding/xml" 7 8 "net/http" 8 9 "testing" 9 10 ··· 15 16 "github.com/stretchr/testify/require" 16 17 ) 17 18 19 + // RSS is a struct to unmarshal RSS feeds test only 20 + type RSS struct { 21 + Channel struct { 22 + Title string `xml:"title"` 23 + Link string `xml:"link"` 24 + Description string `xml:"description"` 25 + PubDate string `xml:"pubDate"` 26 + Items []struct { 27 + Title string `xml:"title"` 28 + Link string `xml:"link"` 29 + Description string `xml:"description"` 30 + PubDate string `xml:"pubDate"` 31 + } `xml:"item"` 32 + } `xml:"channel"` 33 + } 34 + 18 35 func TestFeed(t *testing.T) { 19 36 defer tests.AddFixtures("tests/integration/fixtures/TestFeed/")() 20 37 defer tests.PrepareTestEnv(t)() ··· 38 55 39 56 data := resp.Body.String() 40 57 assert.Contains(t, data, `<rss version="2.0"`) 58 + 59 + var rss RSS 60 + err := xml.Unmarshal(resp.Body.Bytes(), &rss) 61 + require.NoError(t, err) 62 + assert.Contains(t, rss.Channel.Link, "/user2") 63 + assert.NotEmpty(t, rss.Channel.PubDate) 41 64 }) 42 65 }) 43 66
+23
tests/integration/api_org_test.go
··· 99 99 assert.EqualValues(t, "user1", users[0].UserName) 100 100 } 101 101 102 + func TestAPIOrgRename(t *testing.T) { 103 + defer tests.PrepareTestEnv(t)() 104 + token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteOrganization) 105 + 106 + org := api.CreateOrgOption{ 107 + UserName: "user1_org", 108 + FullName: "User1's organization", 109 + Description: "This organization created by user1", 110 + Website: "https://try.gitea.io", 111 + Location: "Shanghai", 112 + Visibility: "limited", 113 + } 114 + req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &org). 115 + AddTokenAuth(token) 116 + MakeRequest(t, req, http.StatusCreated) 117 + 118 + req = NewRequestWithJSON(t, "POST", "/api/v1/orgs/user1_org/rename", &api.RenameOrgOption{ 119 + NewName: "renamed_org", 120 + }).AddTokenAuth(token) 121 + MakeRequest(t, req, http.StatusNoContent) 122 + unittest.AssertExistsAndLoadBean(t, &org_model.Organization{Name: "renamed_org"}) 123 + } 124 + 102 125 func TestAPIOrgEdit(t *testing.T) { 103 126 defer tests.PrepareTestEnv(t)() 104 127 session := loginUser(t, "user1")
+36
tests/integration/feed_repo_test.go
··· 1 + // Copyright 2025 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package integration 5 + 6 + import ( 7 + "encoding/xml" 8 + "net/http" 9 + "testing" 10 + 11 + "code.gitea.io/gitea/tests" 12 + 13 + "github.com/stretchr/testify/assert" 14 + "github.com/stretchr/testify/require" 15 + ) 16 + 17 + func TestFeedRepo(t *testing.T) { 18 + t.Run("RSS", func(t *testing.T) { 19 + defer tests.PrepareTestEnv(t)() 20 + 21 + req := NewRequest(t, "GET", "/user2/repo1.rss") 22 + resp := MakeRequest(t, req, http.StatusOK) 23 + 24 + data := resp.Body.String() 25 + assert.Contains(t, data, `<rss version="2.0"`) 26 + 27 + var rss RSS 28 + err := xml.Unmarshal(resp.Body.Bytes(), &rss) 29 + require.NoError(t, err) 30 + assert.Contains(t, rss.Channel.Link, "/user2/repo1") 31 + assert.NotEmpty(t, rss.Channel.PubDate) 32 + assert.Len(t, rss.Channel.Items, 1) 33 + assert.EqualValues(t, "issue5", rss.Channel.Items[0].Description) 34 + assert.NotEmpty(t, rss.Channel.Items[0].PubDate) 35 + }) 36 + }