diff --git a/.github/workflows/cron-licenses.yml b/.github/workflows/cron-licenses.yml index c5dd70a1f8b..ee1c3e0c750 100644 --- a/.github/workflows/cron-licenses.yml +++ b/.github/workflows/cron-licenses.yml @@ -20,7 +20,7 @@ jobs: - run: make generate-gitignore timeout-minutes: 40 - name: push translations to repo - uses: appleboy/git-push-action@v1.0.0 + uses: appleboy/git-push-action@v1.2.0 with: author_email: "teabot@gitea.io" author_name: GiteaBot diff --git a/.github/workflows/cron-translations.yml b/.github/workflows/cron-translations.yml index d87ba8b20d0..56a30fb5ba6 100644 --- a/.github/workflows/cron-translations.yml +++ b/.github/workflows/cron-translations.yml @@ -29,7 +29,7 @@ jobs: - name: update locales run: ./build/update-locales.sh - name: push translations to repo - uses: appleboy/git-push-action@v1.0.0 + uses: appleboy/git-push-action@v1.2.0 with: author_email: "teabot@gitea.io" author_name: GiteaBot diff --git a/models/git/lfs_lock.go b/models/git/lfs_lock.go index 184e616915d..aabed6b7fae 100644 --- a/models/git/lfs_lock.go +++ b/models/git/lfs_lock.go @@ -101,10 +101,10 @@ func GetLFSLock(ctx context.Context, repo *repo_model.Repository, path string) ( return rel, nil } -// GetLFSLockByID returns release by given id. -func GetLFSLockByID(ctx context.Context, id int64) (*LFSLock, error) { +// GetLFSLockByIDAndRepo returns lfs lock by given id and repository id. +func GetLFSLockByIDAndRepo(ctx context.Context, id, repoID int64) (*LFSLock, error) { lock := new(LFSLock) - has, err := db.GetEngine(ctx).ID(id).Get(lock) + has, err := db.GetEngine(ctx).ID(id).And("repo_id = ?", repoID).Get(lock) if err != nil { return nil, err } else if !has { @@ -153,7 +153,7 @@ func CountLFSLockByRepoID(ctx context.Context, repoID int64) (int64, error) { // DeleteLFSLockByID deletes a lock by given ID. func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repository, u *user_model.User, force bool) (*LFSLock, error) { return db.WithTx2(ctx, func(ctx context.Context) (*LFSLock, error) { - lock, err := GetLFSLockByID(ctx, id) + lock, err := GetLFSLockByIDAndRepo(ctx, id, repo.ID) if err != nil { return nil, err } diff --git a/models/git/lfs_lock_test.go b/models/git/lfs_lock_test.go new file mode 100644 index 00000000000..c88e89be473 --- /dev/null +++ b/models/git/lfs_lock_test.go @@ -0,0 +1,82 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "fmt" + "testing" + "time" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func createTestLock(t *testing.T, repo *repo_model.Repository, owner *user_model.User) *LFSLock { + t.Helper() + + path := fmt.Sprintf("%s-%d-%d", t.Name(), repo.ID, time.Now().UnixNano()) + lock, err := CreateLFSLock(t.Context(), repo, &LFSLock{ + OwnerID: owner.ID, + Path: path, + }) + require.NoError(t, err) + return lock +} + +func TestGetLFSLockByIDAndRepo(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + + lockRepo1 := createTestLock(t, repo1, user2) + lockRepo3 := createTestLock(t, repo3, user4) + + fetched, err := GetLFSLockByIDAndRepo(t.Context(), lockRepo1.ID, repo1.ID) + require.NoError(t, err) + assert.Equal(t, lockRepo1.ID, fetched.ID) + assert.Equal(t, repo1.ID, fetched.RepoID) + + _, err = GetLFSLockByIDAndRepo(t.Context(), lockRepo1.ID, repo3.ID) + assert.Error(t, err) + assert.True(t, IsErrLFSLockNotExist(err)) + + _, err = GetLFSLockByIDAndRepo(t.Context(), lockRepo3.ID, repo1.ID) + assert.Error(t, err) + assert.True(t, IsErrLFSLockNotExist(err)) +} + +func TestDeleteLFSLockByIDRequiresRepoMatch(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + + lockRepo1 := createTestLock(t, repo1, user2) + lockRepo3 := createTestLock(t, repo3, user4) + + _, err := DeleteLFSLockByID(t.Context(), lockRepo3.ID, repo1, user2, true) + assert.Error(t, err) + assert.True(t, IsErrLFSLockNotExist(err)) + + existing, err := GetLFSLockByIDAndRepo(t.Context(), lockRepo3.ID, repo3.ID) + require.NoError(t, err) + assert.Equal(t, lockRepo3.ID, existing.ID) + + deleted, err := DeleteLFSLockByID(t.Context(), lockRepo3.ID, repo3, user4, true) + require.NoError(t, err) + assert.Equal(t, lockRepo3.ID, deleted.ID) + + deleted, err = DeleteLFSLockByID(t.Context(), lockRepo1.ID, repo1, user2, false) + require.NoError(t, err) + assert.Equal(t, lockRepo1.ID, deleted.ID) +} diff --git a/modules/git/parse.go b/modules/git/parse.go index d4ff0ecb23e..94020e690dd 100644 --- a/modules/git/parse.go +++ b/modules/git/parse.go @@ -46,8 +46,8 @@ func parseLsTreeLine(line []byte) (*LsTreeEntry, error) { entry.Size = optional.Some(size) } - entry.EntryMode, err = ParseEntryMode(string(entryMode)) - if err != nil || entry.EntryMode == EntryModeNoEntry { + entry.EntryMode = ParseEntryMode(string(entryMode)) + if entry.EntryMode == EntryModeNoEntry { return nil, fmt.Errorf("invalid ls-tree output (invalid mode): %q, err: %w", line, err) } diff --git a/modules/git/tree_entry_mode.go b/modules/git/tree_entry_mode.go index f36c07bc2a0..2ceba113740 100644 --- a/modules/git/tree_entry_mode.go +++ b/modules/git/tree_entry_mode.go @@ -4,7 +4,6 @@ package git import ( - "fmt" "strconv" ) @@ -55,21 +54,38 @@ func (e EntryMode) IsExecutable() bool { return e == EntryModeExec } -func ParseEntryMode(mode string) (EntryMode, error) { +func ParseEntryMode(mode string) EntryMode { switch mode { case "000000": - return EntryModeNoEntry, nil + return EntryModeNoEntry case "100644": - return EntryModeBlob, nil + return EntryModeBlob case "100755": - return EntryModeExec, nil + return EntryModeExec case "120000": - return EntryModeSymlink, nil + return EntryModeSymlink case "160000": - return EntryModeCommit, nil - case "040000", "040755": // git uses 040000 for tree object, but some users may get 040755 for unknown reasons - return EntryModeTree, nil + return EntryModeCommit + case "040000": + return EntryModeTree default: - return 0, fmt.Errorf("unparsable entry mode: %s", mode) + // git uses 040000 for tree object, but some users may get 040755 from non-standard git implementations + m, _ := strconv.ParseInt(mode, 8, 32) + modeInt := EntryMode(m) + switch modeInt & 0o770000 { + case 0o040000: + return EntryModeTree + case 0o160000: + return EntryModeCommit + case 0o120000: + return EntryModeSymlink + case 0o100000: + if modeInt&0o777 == 0o755 { + return EntryModeExec + } + return EntryModeBlob + default: + return EntryModeNoEntry + } } } diff --git a/modules/git/tree_entry_test.go b/modules/git/tree_entry_test.go index b28abfb5451..8e3fb5ff993 100644 --- a/modules/git/tree_entry_test.go +++ b/modules/git/tree_entry_test.go @@ -27,3 +27,30 @@ func TestEntriesCustomSort(t *testing.T) { entries.CustomSort(strings.Compare) assert.Equal(t, expected, entries) } + +func TestParseEntryMode(t *testing.T) { + tests := []struct { + modeStr string + expectMod EntryMode + }{ + {"000000", EntryModeNoEntry}, + {"000755", EntryModeNoEntry}, + + {"100644", EntryModeBlob}, + {"100755", EntryModeExec}, + + {"120000", EntryModeSymlink}, + {"120755", EntryModeSymlink}, + {"160000", EntryModeCommit}, + {"160755", EntryModeCommit}, + + {"040000", EntryModeTree}, + {"040755", EntryModeTree}, + + {"777777", EntryModeNoEntry}, // invalid mode + } + for _, test := range tests { + mod := ParseEntryMode(test.modeStr) + assert.Equal(t, test.expectMod, mod, "modeStr: %s", test.modeStr) + } +} diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 480aafe8795..1dbb0975a3d 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -2542,8 +2542,8 @@ "repo.diff.too_many_files": "Some files were not shown because too many files have changed in this diff", "repo.diff.show_more": "Show More", "repo.diff.load": "Load Diff", - "repo.diff.generated": "generated", - "repo.diff.vendored": "vendored", + "repo.diff.generated": "Generated", + "repo.diff.vendored": "Vendored", "repo.diff.comment.add_line_comment": "Add line comment", "repo.diff.comment.placeholder": "Leave a comment", "repo.diff.comment.add_single_comment": "Add single comment", @@ -3724,8 +3724,8 @@ "projects.exit_fullscreen": "Exit Fullscreen", "git.filemode.changed_filemode": "%[1]s → %[2]s", "git.filemode.directory": "Directory", - "git.filemode.normal_file": "Normal file", - "git.filemode.executable_file": "Executable file", - "git.filemode.symbolic_link": "Symbolic link", + "git.filemode.normal_file": "Regular", + "git.filemode.executable_file": "Executable", + "git.filemode.symbolic_link": "Symlink", "git.filemode.submodule": "Submodule" } diff --git a/options/locale/locale_ga-IE.json b/options/locale/locale_ga-IE.json index 679963630a7..18bef7188cd 100644 --- a/options/locale/locale_ga-IE.json +++ b/options/locale/locale_ga-IE.json @@ -32,6 +32,7 @@ "password": "Pasfhocal", "access_token": "Comhartha Rochtana", "re_type": "Deimhnigh Pasfhocal", + "captcha": "CAPTCHA", "twofa": "Fíordheimhniú Dhá-Fhachtóir", "twofa_scratch": "Cód Scratch Dhá-Fhachtóra", "passcode": "Paschód", @@ -132,6 +133,7 @@ "confirm_delete_selected": "Deimhnigh chun gach earra roghnaithe a scriosadh?", "name": "Ainm", "value": "Luach", + "readme": "Léigh-mé", "filter_title": "Scagaire", "filter.clear": "Scagaire Soiléir", "filter.is_archived": "Cartlannaithe", @@ -229,6 +231,7 @@ "install.db_name": "Ainm Bunachar Sonraí", "install.db_schema": "Scéim", "install.db_schema_helper": "Fág bán le haghaidh réamhshocraithe bunachar sonraí (\"poiblí\").", + "install.ssl_mode": "SSL", "install.path": "Cosán", "install.sqlite_helper": "Conair comhad don bhunachar sonraí SQLite3. Cuir
isteach cosán iomlán má reáchtáil tú Gitea mar sheirbhís.", "install.reinstall_error": "Tá tú ag iarraidh a shuiteáil i mbunachar sonraí Gitea atá ann cheana", @@ -406,6 +409,7 @@ "auth.twofa_scratch_token_incorrect": "Tá do chód scratch mícheart.", "auth.twofa_required": "Ní mór duit fíordheimhniú dhá fhachtóir a shocrú chun rochtain a fháil ar stórtha, nó iarracht a dhéanamh logáil isteach arís.", "auth.login_userpass": "Sínigh isteach", + "auth.login_openid": "OpenID", "auth.oauth_signup_tab": "Cláraigh Cuntas Nua", "auth.oauth_signup_title": "Comhlánaigh Cuntas Nua", "auth.oauth_signup_submit": "Cuntas Comhlánaigh", @@ -654,6 +658,7 @@ "settings.twofa": "Fíordheimhniú Dhá Fachtóir (TOTP)", "settings.account_link": "Cuntais Nasctha", "settings.organization": "Eagraíochtaí", + "settings.uid": "UID", "settings.webauthn": "Fíordheimhniú Dhá-Fachtóir (Eochracha Slándála)", "settings.public_profile": "Próifíl Phoiblí", "settings.biography_placeholder": "Inis dúinn beagán fút féin! (Is féidir leat Markdown a úsáid)", @@ -991,6 +996,7 @@ "repo.multiple_licenses": "Ceadúnais Iolracha", "repo.object_format": "Formáid Oibiacht", "repo.object_format_helper": "Formáid oibiacht an stór. Ní féidir é a athrú níos déanaí. Is é SHA1 an comhoiriúnacht is fearr.", + "repo.readme": "LÉIGHMÉ", "repo.readme_helper": "Roghnaigh comhad teimpléad README.", "repo.readme_helper_desc": "Seo an áit inar féidir leat cur síos iomlán a scríobh do thionscadal.", "repo.auto_init": "Taisce a thionscnamh (Cuireann sé .gitignore, Ceadúnas agus README)", @@ -1055,6 +1061,7 @@ "repo.desc.template": "Teimpléad", "repo.desc.internal": "Inmheánach", "repo.desc.archived": "Cartlannaithe", + "repo.desc.sha256": "SHA256", "repo.template.items": "Míreanna Teimpléad", "repo.template.git_content": "Ábhar Git (Brainse Réamhshocraithe)", "repo.template.git_hooks": "Crúcanna Git", @@ -1083,6 +1090,7 @@ "repo.migrate_options_lfs_endpoint.description.local": "Tacaítear le cosán freastalaí áitiúil freisin.", "repo.migrate_options_lfs_endpoint.placeholder": "Mura bhfágtar bán é, díorthófar an críochphointe ón URL clónála.", "repo.migrate_items": "Míreanna Imirce", + "repo.migrate_items_wiki": "Vicí", "repo.migrate_items_milestones": "Clocha míle", "repo.migrate_items_labels": "Lipéid", "repo.migrate_items_issues": "Saincheisteanna", @@ -1728,8 +1736,11 @@ "repo.issues.reference_link": "Tagairt: %s", "repo.compare.compare_base": "bonn", "repo.compare.compare_head": "déan comparáid", + "repo.compare.title": "Athruithe a chur i gcomparáid", + "repo.compare.description": "Roghnaigh dhá bhrainse nó clib chun a fheiceáil cad atá athraithe nó chun iarratas tarraingthe nua a thosú.", "repo.pulls.desc": "Cumasaigh iarratais tarraingthe agus athbhreithnithe cód.", "repo.pulls.new": "Iarratas Tarraingthe Nua", + "repo.pulls.new.description": "Pléigh agus athbhreithnigh na hathruithe sa chomparáid seo le daoine eile.", "repo.pulls.new.blocked_user": "Ní féidir iarratas tarraingthe a chruthú toisc go bhfuil úinéir an stórais bac ort.", "repo.pulls.new.must_collaborator": "Caithfidh tú a bheith ina chomhoibritheoir chun iarratas tarraingthe a chruthú.", "repo.pulls.new.already_existed": "Tá iarratas tarraingthe idir na brainsí seo ann cheana féin", @@ -1739,7 +1750,6 @@ "repo.pulls.allow_edits_from_maintainers": "Ceadaigh eagarthóirí ó chothabhálaí", "repo.pulls.allow_edits_from_maintainers_desc": "Is féidir le húsáideoirí a bhfuil rochtain scríofa acu ar an mbunbhrainse brú chuig an bhrainse", "repo.pulls.allow_edits_from_maintainers_err": "Theip ar nuashonrú", - "repo.pulls.compare_changes_desc": "Roghnaigh an brainse le cumasc isteach agus an brainse le tarraingt uaidh.", "repo.pulls.has_viewed_file": "Breathnaithe", "repo.pulls.has_changed_since_last_review": "Athraithe ó d'athbhreithniú deire", "repo.pulls.viewed_files_label": "Breathnaíodh ar %[1]d / %[2]d comhaid", @@ -2313,8 +2323,19 @@ "repo.settings.slack_domain": "Fearann", "repo.settings.slack_channel": "Cainéal", "repo.settings.add_web_hook_desc": "Comhtháthaigh %s isteach i do stóras.", + "repo.settings.web_hook_name_gitea": "Gitea", + "repo.settings.web_hook_name_gogs": "Gogs", + "repo.settings.web_hook_name_slack": "Slack", + "repo.settings.web_hook_name_discord": "Discord", + "repo.settings.web_hook_name_dingtalk": "DingTalk", "repo.settings.web_hook_name_telegram": "Teileagram", "repo.settings.web_hook_name_matrix": "Maitrís", + "repo.settings.web_hook_name_msteams": "Microsoft Teams", + "repo.settings.web_hook_name_feishu_or_larksuite": "Feishu / Lark Suite", + "repo.settings.web_hook_name_feishu": "Feishu", + "repo.settings.web_hook_name_larksuite": "Lark Suite", + "repo.settings.web_hook_name_wechatwork": "WeCom (Wechat Work)", + "repo.settings.web_hook_name_packagist": "Packagist", "repo.settings.packagist_username": "Ainm úsáideora Pacagist", "repo.settings.packagist_api_token": "Comhartha API", "repo.settings.packagist_package_url": "URL pacáiste Packagist", @@ -2460,6 +2481,7 @@ "repo.settings.unarchive.success": "Rinneadh an stóras a dhíchartlann go rathúil.", "repo.settings.unarchive.error": "Tharla earráid agus tú ag iarraidh an stóras a dhíchartlannú. Féach an logáil le haghaidh tuilleadh sonraí.", "repo.settings.update_avatar_success": "Nuashonraíodh avatar an stóras.", + "repo.settings.lfs": "LFS", "repo.settings.lfs_filelist": "Comhaid LFS a stóráiltear sa stóras seo", "repo.settings.lfs_no_lfs_files": "Níl aon chomhaid LFS stóráilte sa stóras seo", "repo.settings.lfs_findcommits": "Aimsigh gealltanais", @@ -2479,6 +2501,7 @@ "repo.settings.lfs_force_unlock": "Díghlasáil Fórsa", "repo.settings.lfs_pointers.found": "Fuarthas %d pointeoir(í) bloba — %d gaolmhar, %d neamhghaolmhar (%d ar iarraidh ón stóras)", "repo.settings.lfs_pointers.sha": "SHA Blob", + "repo.settings.lfs_pointers.oid": "OID", "repo.settings.lfs_pointers.inRepo": "I Stóras", "repo.settings.lfs_pointers.exists": "Ann sa siopa", "repo.settings.lfs_pointers.accessible": "Inrochtana don Úsáideoir", @@ -2844,6 +2867,7 @@ "admin.dashboard.task.finished": "Tasc: Tá %[1]s tosaithe ag %[2]s críochnaithe", "admin.dashboard.task.unknown": "Tasc anaithnid: %[1]s", "admin.dashboard.cron.started": "Cron tosaithe: %[1]s", + "admin.dashboard.cron.process": "Cron: %[1]s", "admin.dashboard.cron.cancelled": "Cron: %[1]s cealaithe: %[3]s", "admin.dashboard.cron.error": "Earráid i gCron: %s: %[3]s", "admin.dashboard.cron.finished": "Cron: %[1]s críochnaithe", @@ -2923,6 +2947,7 @@ "admin.users.reserved": "In áirithe", "admin.users.bot": "Bota", "admin.users.remote": "Iargúlta", + "admin.users.2fa": "2FA", "admin.users.repos": "Stórais", "admin.users.created": "Cruthaithe", "admin.users.last_login": "Sínigh Isteach Deiridh", @@ -3044,6 +3069,7 @@ "admin.auths.attribute_mail": "Tréith ríomhphoist", "admin.auths.attribute_ssh_public_key": "Tréith Eochair SSH Phoiblí", "admin.auths.attribute_avatar": "Tréith Avatar", + "admin.auths.ssh_keys_are_verified": "Meastar gur fíoraithe iad eochracha SSH in LDAP", "admin.auths.attributes_in_bind": "Faigh tréithe i gComhthéacs Bind DN", "admin.auths.allow_deactivate_all": "Lig do thoradh cuardaigh folamh gach úsáideoir a dhíghníomhachtú", "admin.auths.use_paged_search": "Úsáid Cuardach Leathanaigh", @@ -3177,6 +3203,7 @@ "admin.config.db_name": "Ainm", "admin.config.db_user": "Ainm úsáideora", "admin.config.db_schema": "Scéim", + "admin.config.db_ssl_mode": "SSL", "admin.config.db_path": "Cosán", "admin.config.service_config": "Cumraíocht Seirbhíse", "admin.config.register_email_confirm": "Deimhniú Ríomhphost a éileamh chun Clárú", @@ -3430,6 +3457,7 @@ "packages.assets": "Sócmhainní", "packages.versions": "Leaganacha", "packages.versions.view_all": "Féach ar gach", + "packages.dependency.id": "ID", "packages.dependency.version": "Leagan", "packages.search_in_external_registry": "Cuardaigh i %s", "packages.alpine.registry": "Socraigh an clárlann seo tríd an URL a chur i do chomhad /etc/apk/repositories:", @@ -3594,6 +3622,7 @@ "actions.runners.new": "Cruthaigh reathaí nua", "actions.runners.new_notice": "Conas reathaí a thosú", "actions.runners.status": "Stádas", + "actions.runners.id": "ID", "actions.runners.name": "Ainm", "actions.runners.owner_type": "Cineál", "actions.runners.description": "Cur síos", @@ -3693,6 +3722,7 @@ "projects.type-3.display_name": "Tionscadal Eagrúcháin", "projects.enter_fullscreen": "Lánscáileán", "projects.exit_fullscreen": "Scoir Lánscáileáin", + "git.filemode.changed_filemode": "%[1]s → %[2]s", "git.filemode.directory": "Eolaire", "git.filemode.normal_file": "Comhad gnáth", "git.filemode.executable_file": "Comhad infheidhmithe", diff --git a/routers/api/v1/repo/release_attachment.go b/routers/api/v1/repo/release_attachment.go index 43e97beb276..5f5423fafed 100644 --- a/routers/api/v1/repo/release_attachment.go +++ b/routers/api/v1/repo/release_attachment.go @@ -398,7 +398,6 @@ func DeleteReleaseAttachment(ctx *context.APIContext) { ctx.APIErrorNotFound() return } - // FIXME Should prove the existence of the given repo, but results in unnecessary database requests if err := repo_model.DeleteAttachment(ctx, attach, true); err != nil { ctx.APIErrorInternal(err) diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go index 54200d8de8d..bff91b51a7b 100644 --- a/routers/web/repo/attachment.go +++ b/routers/web/repo/attachment.go @@ -4,11 +4,12 @@ package repo import ( - "fmt" "net/http" + issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -40,7 +41,7 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) { file, header, err := ctx.Req.FormFile("file") if err != nil { - ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err)) + ctx.ServerError("FormFile", err) return } defer file.Close() @@ -56,7 +57,7 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) { ctx.HTTPError(http.StatusBadRequest, err.Error()) return } - ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("NewAttachment: %v", err)) + ctx.ServerError("UploadAttachmentGeneralSizeLimit", err) return } @@ -74,13 +75,44 @@ func DeleteAttachment(ctx *context.Context) { ctx.HTTPError(http.StatusBadRequest, err.Error()) return } - if !ctx.IsSigned || (ctx.Doer.ID != attach.UploaderID) { + + if !ctx.IsSigned { ctx.HTTPError(http.StatusForbidden) return } + + if attach.RepoID != ctx.Repo.Repository.ID { + ctx.HTTPError(http.StatusBadRequest, "attachment does not belong to this repository") + return + } + + if ctx.Doer.ID != attach.UploaderID { + if attach.IssueID > 0 { + issue, err := issues_model.GetIssueByID(ctx, attach.IssueID) + if err != nil { + ctx.ServerError("GetIssueByID", err) + return + } + if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) { + ctx.HTTPError(http.StatusForbidden) + return + } + } else if attach.ReleaseID > 0 { + if !ctx.Repo.Permission.CanWrite(unit.TypeReleases) { + ctx.HTTPError(http.StatusForbidden) + return + } + } else { + if !ctx.Repo.Permission.IsAdmin() && !ctx.Repo.Permission.IsOwner() { + ctx.HTTPError(http.StatusForbidden) + return + } + } + } + err = repo_model.DeleteAttachment(ctx, attach, true) if err != nil { - ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("DeleteAttachment: %v", err)) + ctx.ServerError("DeleteAttachment", err) return } ctx.JSON(http.StatusOK, map[string]string{ @@ -114,7 +146,7 @@ func ServeAttachment(ctx *context.Context, uuid string) { } else { // If we have the repository we check access perm, err := access_model.GetUserRepoPermission(ctx, repository, ctx.Doer) if err != nil { - ctx.HTTPError(http.StatusInternalServerError, "GetUserRepoPermission", err.Error()) + ctx.ServerError("GetUserRepoPermission", err) return } if !perm.CanRead(unitType) { diff --git a/services/gitdiff/git_diff_tree.go b/services/gitdiff/git_diff_tree.go index 2a3c7c94450..b4f26210be7 100644 --- a/services/gitdiff/git_diff_tree.go +++ b/services/gitdiff/git_diff_tree.go @@ -166,16 +166,6 @@ func parseGitDiffTreeLine(line string) (*DiffTreeRecord, error) { return nil, fmt.Errorf("unparsable output for diff-tree --raw: `%s`, expected 5 space delimited values got %d)", line, len(fields)) } - baseMode, err := git.ParseEntryMode(fields[0]) - if err != nil { - return nil, err - } - - headMode, err := git.ParseEntryMode(fields[1]) - if err != nil { - return nil, err - } - baseBlobID := fields[2] headBlobID := fields[3] @@ -201,8 +191,8 @@ func parseGitDiffTreeLine(line string) (*DiffTreeRecord, error) { return &DiffTreeRecord{ Status: status, Score: score, - BaseMode: baseMode, - HeadMode: headMode, + BaseMode: git.ParseEntryMode(fields[0]), + HeadMode: git.ParseEntryMode(fields[1]), BaseBlobID: baseBlobID, HeadBlobID: headBlobID, BasePath: basePath, diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index be5c1dbece1..f00c90d7378 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -399,20 +399,20 @@ type DiffFile struct { isAmbiguous bool // basic fields (parsed from diff result) - Name string - NameHash string - OldName string - Addition int - Deletion int - Type DiffFileType - Mode string - OldMode string - IsCreated bool - IsDeleted bool - IsBin bool - IsLFSFile bool - IsRenamed bool - IsSubmodule bool + Name string + NameHash string + OldName string + Addition int + Deletion int + Type DiffFileType + EntryMode string + OldEntryMode string + IsCreated bool + IsDeleted bool + IsBin bool + IsLFSFile bool + IsRenamed bool + IsSubmodule bool // basic fields but for render purpose only Sections []*DiffSection IsIncomplete bool @@ -501,21 +501,36 @@ func (diffFile *DiffFile) ShouldBeHidden() bool { return diffFile.IsGenerated || diffFile.IsViewed } -func (diffFile *DiffFile) ModeTranslationKey(mode string) string { - switch mode { - case "040000": - return "git.filemode.directory" - case "100644": - return "git.filemode.normal_file" - case "100755": - return "git.filemode.executable_file" - case "120000": - return "git.filemode.symbolic_link" - case "160000": - return "git.filemode.submodule" - default: - return mode +func (diffFile *DiffFile) TranslateDiffEntryMode(locale translation.Locale) string { + entryModeTr := func(mode string) string { + entryMode := git.ParseEntryMode(mode) + switch { + case entryMode.IsDir(): + return locale.TrString("git.filemode.directory") + case entryMode.IsRegular(): + return locale.TrString("git.filemode.normal_file") + case entryMode.IsExecutable(): + return locale.TrString("git.filemode.executable_file") + case entryMode.IsLink(): + return locale.TrString("git.filemode.symbolic_link") + case entryMode.IsSubModule(): + return locale.TrString("git.filemode.submodule") + default: + return mode + } } + + if diffFile.EntryMode != "" && diffFile.OldEntryMode != "" { + oldMode := entryModeTr(diffFile.OldEntryMode) + newMode := entryModeTr(diffFile.EntryMode) + return locale.TrString("git.filemode.changed_filemode", oldMode, newMode) + } + if diffFile.EntryMode != "" { + if entryMode := git.ParseEntryMode(diffFile.EntryMode); !entryMode.IsRegular() { + return entryModeTr(diffFile.EntryMode) + } + } + return "" } type limitByteWriter struct { @@ -695,10 +710,10 @@ parsingLoop: strings.HasPrefix(line, "new mode "): if strings.HasPrefix(line, "old mode ") { - curFile.OldMode = prepareValue(line, "old mode ") + curFile.OldEntryMode = prepareValue(line, "old mode ") } if strings.HasPrefix(line, "new mode ") { - curFile.Mode = prepareValue(line, "new mode ") + curFile.EntryMode = prepareValue(line, "new mode ") } if strings.HasSuffix(line, " 160000\n") { curFile.IsSubmodule, curFile.SubmoduleDiffInfo = true, &SubmoduleDiffInfo{} @@ -733,7 +748,7 @@ parsingLoop: curFile.Type = DiffFileAdd curFile.IsCreated = true if strings.HasPrefix(line, "new file mode ") { - curFile.Mode = prepareValue(line, "new file mode ") + curFile.EntryMode = prepareValue(line, "new file mode ") } if strings.HasSuffix(line, " 160000\n") { curFile.IsSubmodule, curFile.SubmoduleDiffInfo = true, &SubmoduleDiffInfo{} diff --git a/services/lfs/locks.go b/services/lfs/locks.go index 5bc3f6b95a4..c2279edaf0a 100644 --- a/services/lfs/locks.go +++ b/services/lfs/locks.go @@ -90,7 +90,7 @@ func GetListLockHandler(ctx *context.Context) { }) return } - lock, err := git_model.GetLFSLockByID(ctx, v) + lock, err := git_model.GetLFSLockByIDAndRepo(ctx, v, repository.ID) if err != nil && !git_model.IsErrLFSLockNotExist(err) { log.Error("Unable to get lock with ID[%s]: Error: %v", v, err) } diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index ff9bd2e792e..2a3330d890b 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -82,43 +82,42 @@ {{$isExpandable := or (gt $file.Addition 0) (gt $file.Deletion 0) $file.IsBin}} {{$isReviewFile := and $.IsSigned $.PageIsPullFiles (not $.Repository.IsArchived) $.IsShowingAllCommits}}
-

+
- -
- {{if $file.IsBin}} - - {{ctx.Locale.Tr "repo.diff.bin"}} - - {{else}} - {{template "repo/diff/stats" dict "file" . "root" $}} - {{end}} +
+ + {{$entryModeText := $file.TranslateDiffEntryMode ctx.Locale}} + + {{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}} +
- {{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}} - - {{if .IsLFSFile}}LFS{{end}} - {{if $file.IsGenerated}} - {{ctx.Locale.Tr "repo.diff.generated"}} - {{end}} - {{if $file.IsVendored}} - {{ctx.Locale.Tr "repo.diff.vendored"}} - {{end}} - {{if and $file.Mode $file.OldMode}} - {{$old := ctx.Locale.Tr ($file.ModeTranslationKey $file.OldMode)}} - {{$new := ctx.Locale.Tr ($file.ModeTranslationKey $file.Mode)}} - {{ctx.Locale.Tr "git.filemode.changed_filemode" $old $new}} - {{else if $file.Mode}} - {{ctx.Locale.Tr ($file.ModeTranslationKey $file.Mode)}} - {{end}} - + + {{if $file.IsLFSFile}} + LFS + {{end}} + {{if $file.IsGenerated}} + {{ctx.Locale.Tr "repo.diff.generated"}} + {{end}} + {{if $file.IsVendored}} + {{ctx.Locale.Tr "repo.diff.vendored"}} + {{end}} + {{if $entryModeText}} + {{$entryModeText}} + {{end}}
-
+
+ {{if $file.IsBin}} + {{ctx.Locale.Tr "repo.diff.bin"}} + {{else}} + {{template "repo/diff/stats" dict "Addition" .Addition "Deletion" .Deletion}} + {{end}} + {{if $showFileViewToggle}}
@@ -157,7 +156,7 @@
{{end}}
-

+
{{if or $file.IsIncomplete $file.IsBin}} diff --git a/templates/repo/diff/stats.tmpl b/templates/repo/diff/stats.tmpl index d0dff1bd094..31797cd9704 100644 --- a/templates/repo/diff/stats.tmpl +++ b/templates/repo/diff/stats.tmpl @@ -1,5 +1,17 @@ -{{Eval .file.Addition "+" .file.Deletion}} - - {{/* if the denominator is zero, then the float result is "width: NaNpx", as before, it just works */}} -
-
+{{/* Template Attributes: +* Addition: Number of additions +* Deletion: Number of deletions +* Classes: Additional classes for the root element +*/}} +{{if or .Addition .Deletion}} +
+ + {{if .Addition}}+{{.Addition}}{{end}} + {{if .Deletion}}-{{.Deletion}}{{end}} + + + {{/* if the denominator is zero, then the float result is "width: NaNpx", as before, it just works */}} +
+
+
+{{end}} diff --git a/templates/repo/pulls/tab_menu.tmpl b/templates/repo/pulls/tab_menu.tmpl index a0ecdf96cdd..70ce8271bd6 100644 --- a/templates/repo/pulls/tab_menu.tmpl +++ b/templates/repo/pulls/tab_menu.tmpl @@ -16,12 +16,7 @@ {{if .NumFiles}}{{.NumFiles}}{{else}}-{{end}} {{if or .DiffShortStat.TotalAddition .DiffShortStat.TotalDeletion}} - - {{if .DiffShortStat.TotalAddition}}+{{.DiffShortStat.TotalAddition}}{{end}} {{if .DiffShortStat.TotalDeletion}}-{{.DiffShortStat.TotalDeletion}}{{end}} - -
-
-
+ {{template "repo/diff/stats" dict "Addition" .DiffShortStat.TotalAddition "Deletion" .DiffShortStat.TotalDeletion "Classes" "tw-ml-auto tw-pl-3 tw-font-semibold"}} {{end}}
diff --git a/tests/integration/attachment_test.go b/tests/integration/attachment_test.go index 0459371e3d5..1e4e7c8e1cb 100644 --- a/tests/integration/attachment_test.go +++ b/tests/integration/attachment_test.go @@ -34,6 +34,14 @@ func testGeneratePngBytes() []byte { } func testCreateIssueAttachment(t *testing.T, session *TestSession, repoURL, filename string, content []byte, expectedStatus int) string { + return testCreateAttachment(t, session, repoURL, "issues", filename, content, expectedStatus) +} + +func testCreateReleaseAttachment(t *testing.T, session *TestSession, repoURL, filename string, content []byte, expectedStatus int) string { + return testCreateAttachment(t, session, repoURL, "releases", filename, content, expectedStatus) +} + +func testCreateAttachment(t *testing.T, session *TestSession, repoURL, issueOrRelease, filename string, content []byte, expectedStatus int) string { body := &bytes.Buffer{} // Setup multi-part @@ -45,7 +53,7 @@ func testCreateIssueAttachment(t *testing.T, session *TestSession, repoURL, file err = writer.Close() assert.NoError(t, err) - req := NewRequestWithBody(t, "POST", repoURL+"/issues/attachments", body) + req := NewRequestWithBody(t, "POST", repoURL+"/"+issueOrRelease+"/attachments", body) req.Header.Add("Content-Type", writer.FormDataContentType()) resp := session.MakeRequest(t, req, expectedStatus) @@ -57,12 +65,23 @@ func testCreateIssueAttachment(t *testing.T, session *TestSession, repoURL, file return obj["uuid"] } +func testDeleteIssueAttachment(t *testing.T, session *TestSession, repoURL, uuid string, expectedStatus int) { + req := NewRequestWithValues(t, "POST", repoURL+"/issues/attachments/remove", map[string]string{"file": uuid}) + session.MakeRequest(t, req, expectedStatus) +} + +func testDeleteReleaseAttachment(t *testing.T, session *TestSession, repoURL, uuid string, expectedStatus int) { + req := NewRequestWithValues(t, "POST", repoURL+"/releases/attachments/remove", map[string]string{"file": uuid}) + session.MakeRequest(t, req, expectedStatus) +} + func TestAttachments(t *testing.T) { defer tests.PrepareTestEnv(t)() t.Run("CreateAnonymousAttachment", testCreateAnonymousAttachment) t.Run("CreateUser2IssueAttachment", testCreateUser2IssueAttachment) t.Run("UploadAttachmentDeleteTemp", testUploadAttachmentDeleteTemp) t.Run("GetAttachment", testGetAttachment) + t.Run("DeleteAttachmentPermissions", testDeleteAttachmentPermissions) } func testUploadAttachmentDeleteTemp(t *testing.T) { @@ -157,3 +176,28 @@ func testGetAttachment(t *testing.T) { }) } } + +func testDeleteAttachmentPermissions(t *testing.T) { + const repoURL = "user2/repo1" + + ownerSession := loginUser(t, "user2") + readonlySession := loginUser(t, "user5") + + issueFromOwner := testCreateIssueAttachment(t, ownerSession, repoURL, "owner-issue.png", testGeneratePngBytes(), http.StatusOK) + testDeleteIssueAttachment(t, readonlySession, repoURL, issueFromOwner, http.StatusForbidden) + + issueFromReader := testCreateIssueAttachment(t, readonlySession, repoURL, "reader-issue.png", testGeneratePngBytes(), http.StatusOK) + testDeleteIssueAttachment(t, ownerSession, repoURL, issueFromReader, http.StatusOK) + + testCreateReleaseAttachment(t, readonlySession, repoURL, "reader-release.png", testGeneratePngBytes(), http.StatusNotFound) + + crossRepoUUID := testCreateIssueAttachment(t, ownerSession, repoURL, "cross-repo.png", testGeneratePngBytes(), http.StatusOK) + testDeleteIssueAttachment(t, ownerSession, "user2/repo2", crossRepoUUID, http.StatusBadRequest) + testDeleteIssueAttachment(t, ownerSession, repoURL, crossRepoUUID, http.StatusOK) + + releaseUUID := testCreateReleaseAttachment(t, ownerSession, repoURL, "reader-release.png", testGeneratePngBytes(), http.StatusOK) + testDeleteReleaseAttachment(t, ownerSession, repoURL, releaseUUID, http.StatusOK) + + // test deleting release attachment from another repo + testDeleteReleaseAttachment(t, ownerSession, "user2/repo2", crossRepoUUID, http.StatusBadRequest) +} diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 0bf37ca0838..4de1f3ccdf6 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -1293,8 +1293,7 @@ td .commit-summary { filter: drop-shadow(-4px 0 0 var(--color-primary-alpha-30)) !important; } -.code-comment:target, -.diff-file-box:target { +.code-comment:target { border-color: var(--color-primary) !important; border-radius: var(--border-radius) !important; box-shadow: 0 0 0 3px var(--color-primary-alpha-30) !important; @@ -1681,18 +1680,27 @@ tbody.commit-list { margin-top: 1em; } +.diff-file-box:target { + border-color: var(--color-primary) !important; + border-radius: var(--border-radius) !important; + box-shadow: 0 0 0 3px var(--color-primary-alpha-30) !important; +} + .diff-file-header { padding: 5px 8px !important; - box-shadow: 0 -1px 0 1px var(--color-body); /* prevent borders being visible behind top corners when sticky and scrolled */ - font-weight: var(--font-weight-normal); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; + gap: 0.5em; + + /* prevent borders from being visible behind top corners when sticky and scrolled, + this "shadow" is used to use body's color to cover the scrolled-up left and right borders at corners */ + box-shadow: 0 -1px 0 1px var(--color-body); } -.diff-file-header .file { - min-width: 0; +.diff-file-box:target .diff-file-header { + box-shadow: unset; /* when targeted, still use the parent's box-shadow, remove the patched above */ } .diff-file-header .file-link { @@ -1715,6 +1723,7 @@ tbody.commit-list { .diff-file-header { flex-direction: column; align-items: stretch; + gap: 0; } } @@ -1743,13 +1752,13 @@ tbody.commit-list { .diff-stats-bar { display: inline-block; - background-color: var(--color-red); + background-color: var(--color-diff-prompt-del-fg); /* the background is used as "text foreground color" */ height: 12px; width: 44px; } .diff-stats-bar .diff-stats-add-bar { - background-color: var(--color-green); + background-color: var(--color-diff-prompt-add-fg); height: 100%; } diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css index f89752dc791..7998f549905 100644 --- a/web_src/css/themes/theme-gitea-dark.css +++ b/web_src/css/themes/theme-gitea-dark.css @@ -158,6 +158,8 @@ gitea-theme-meta-info { --color-diff-removed-row-bg: #301e1e; --color-diff-removed-row-border: #634343; --color-diff-removed-word-bg: #6f3333; + --color-diff-prompt-add-fg: #87ab63; + --color-diff-prompt-del-fg: #cc4848; --color-diff-inactive: #22282d; --color-error-border: #a04141; --color-error-bg: #522; diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css index 1261ef8be03..d1694920411 100644 --- a/web_src/css/themes/theme-gitea-light.css +++ b/web_src/css/themes/theme-gitea-light.css @@ -158,6 +158,8 @@ gitea-theme-meta-info { --color-diff-removed-row-bg: #ffeef0; --color-diff-removed-row-border: #f1c0c0; --color-diff-removed-word-bg: #fdb8c0; + --color-diff-prompt-add-fg: #21ba45; + --color-diff-prompt-del-fg: #db2828; --color-diff-inactive: #f0f2f4; --color-error-border: #e0b4b4; --color-error-bg: #fff6f6;