From 98ef79d73a6a546241dd02959ae17f136369b604 Mon Sep 17 00:00:00 2001 From: a1012112796 <1012112796@qq.com> Date: Mon, 8 Dec 2025 02:07:04 +0800 Subject: [PATCH 1/4] allow action user have read permission in public repo like other user (#36095) related #28187 --------- Signed-off-by: a1012112796 <1012112796@qq.com> --- models/perm/access/repo_permission.go | 8 ++- .../api_actions_permission_test.go | 54 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 tests/integration/api_actions_permission_test.go diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 15526cb1e6..d343ae6e35 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -276,8 +276,14 @@ func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Reposito if !actionsCfg.IsCollaborativeOwner(taskRepo.OwnerID) || !taskRepo.IsPrivate { // The task repo can access the current repo only if the task repo is private and // the owner of the task repo is a collaborative owner of the current repo. - // FIXME allow public repo read access if tokenless pull is enabled // FIXME should owner's visibility also be considered here? + + // check permission like simple user but limit to read-only + perm, err = GetUserRepoPermission(ctx, repo, user_model.NewActionsUser()) + if err != nil { + return perm, err + } + perm.AccessMode = min(perm.AccessMode, perm_model.AccessModeRead) return perm, nil } accessMode = perm_model.AccessModeRead diff --git a/tests/integration/api_actions_permission_test.go b/tests/integration/api_actions_permission_test.go new file mode 100644 index 0000000000..072e2635a9 --- /dev/null +++ b/tests/integration/api_actions_permission_test.go @@ -0,0 +1,54 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "testing" + + "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 testActionUserSignIn(t *testing.T) { + req := NewRequest(t, "GET", "/api/v1/user"). + AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a") + resp := MakeRequest(t, req, http.StatusOK) + + var u api.User + DecodeJSON(t, resp, &u) + assert.Equal(t, "gitea-actions", u.UserName) +} + +func testActionUserAccessPublicRepo(t *testing.T) { + req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/raw/README.md"). + AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a") + resp := MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "file", resp.Header().Get("x-gitea-object-type")) + + defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)() + + req = NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/raw/README.md"). + AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a") + resp = MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "file", resp.Header().Get("x-gitea-object-type")) +} + +func testActionUserNoAccessOtherPrivateRepo(t *testing.T) { + req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo2/raw/README.md"). + AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a") + MakeRequest(t, req, http.StatusNotFound) +} + +func TestActionUserAccessPermission(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + t.Run("ActionUserSignIn", testActionUserSignIn) + t.Run("ActionUserAccessPublicRepo", testActionUserAccessPublicRepo) + t.Run("ActionUserNoAccessOtherPrivateRepo", testActionUserNoAccessOtherPrivateRepo) +} From 69700f9cddddc8a62641e42f0c2dd6d5db794223 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 7 Dec 2025 23:09:10 -0800 Subject: [PATCH 2/4] Fix possible bug when migrating issues/pull requests (#33487) When migrating issues or pull requests from a big repository, some issue/pull request maybe deleted when migrating. So that there will be duplicated issues/pull requests because we are get information with pagination. This PR introduced a map to record all migrated issue pull request index when migrating to avoid the failure because of duplicated records. --- services/migrations/migrate.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/services/migrations/migrate.go b/services/migrations/migrate.go index eba9c79df5..bd7e52cc3d 100644 --- a/services/migrations/migrate.go +++ b/services/migrations/migrate.go @@ -16,6 +16,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/hostmatcher" "code.gitea.io/gitea/modules/log" @@ -327,6 +328,9 @@ func migrateRepository(ctx context.Context, doer *user_model.User, downloader ba messenger("repo.migrate.migrating_issues") issueBatchSize := uploader.MaxBatchInsertSize("issue") + // because when the migrating is running, some issues maybe removed, so after the next page + // some of issue maybe duplicated, so we need to record the inserted issue indexes + mapInsertedIssueIndexes := container.Set[int64]{} for i := 1; ; i++ { issues, isEnd, err := downloader.GetIssues(ctx, i, issueBatchSize) if err != nil { @@ -336,6 +340,14 @@ func migrateRepository(ctx context.Context, doer *user_model.User, downloader ba log.Warn("migrating issues is not supported, ignored") break } + for i := 0; i < len(issues); i++ { + if mapInsertedIssueIndexes.Contains(issues[i].Number) { + issues = append(issues[:i], issues[i+1:]...) + i-- + continue + } + mapInsertedIssueIndexes.Add(issues[i].Number) + } if err := uploader.CreateIssues(ctx, issues...); err != nil { return err @@ -381,6 +393,7 @@ func migrateRepository(ctx context.Context, doer *user_model.User, downloader ba log.Trace("migrating pull requests and comments") messenger("repo.migrate.migrating_pulls") prBatchSize := uploader.MaxBatchInsertSize("pullrequest") + mapInsertedPRIndexes := container.Set[int64]{} for i := 1; ; i++ { prs, isEnd, err := downloader.GetPullRequests(ctx, i, prBatchSize) if err != nil { @@ -390,6 +403,14 @@ func migrateRepository(ctx context.Context, doer *user_model.User, downloader ba log.Warn("migrating pull requests is not supported, ignored") break } + for i := 0; i < len(prs); i++ { + if mapInsertedPRIndexes.Contains(prs[i].Number) { + prs = append(prs[:i], prs[i+1:]...) + i-- + continue + } + mapInsertedPRIndexes.Add(prs[i].Number) + } if err := uploader.CreatePullRequests(ctx, prs...); err != nil { return err From d83a071db999f120973d852e79afeafd440de964 Mon Sep 17 00:00:00 2001 From: Ger Schinkel Date: Tue, 9 Dec 2025 16:14:05 +0100 Subject: [PATCH 3/4] Changed a small typo in an error message and code comments. (#36117) --- modules/setting/config_provider.go | 4 ++-- services/context/csrf.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go index b6f9f07f98..57dc23b17f 100644 --- a/modules/setting/config_provider.go +++ b/modules/setting/config_provider.go @@ -337,14 +337,14 @@ func LogStartupProblem(skip int, level log.Level, format string, args ...any) { func deprecatedSetting(rootCfg ConfigProvider, oldSection, oldKey, newSection, newKey, version string) { if rootCfg.Section(oldSection).HasKey(oldKey) { - LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents, please use `[%s].%s` instead because this fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version) + LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` present, please use `[%s].%s` instead because this fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version) } } // deprecatedSettingDB add a hint that the configuration has been moved to database but still kept in app.ini func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) { if rootCfg.Section(oldSection).HasKey(oldKey) { - LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents but it won't take effect because it has been moved to admin panel -> config setting", oldSection, oldKey) + LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` present but it won't take effect because it has been moved to admin panel -> config setting", oldSection, oldKey) } } diff --git a/services/context/csrf.go b/services/context/csrf.go index f190465bdb..aa99f34b03 100644 --- a/services/context/csrf.go +++ b/services/context/csrf.go @@ -118,7 +118,7 @@ func (c *csrfProtector) PrepareForSessionUser(ctx *Context) { if uidChanged { _ = ctx.Session.Set(c.opt.oldSessionKey, c.id) } else if cookieToken != "" { - // If cookie token presents, re-use existing unexpired token, else generate a new one. + // If cookie token present, re-use existing unexpired token, else generate a new one. if issueTime, ok := ParseCsrfToken(cookieToken); ok { dur := time.Since(issueTime) // issueTime is not a monotonic-clock, the server time may change a lot to an early time. if dur >= -CsrfTokenRegenerationInterval && dur <= CsrfTokenRegenerationInterval { From ed698d1a6130e2555eb0a7123084d406aa73430a Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 10 Dec 2025 08:30:50 +0100 Subject: [PATCH 4/4] Add matching pair insertion to markdown textarea (#36121) 1. Our textarea already has some editor-like feature like tab indentation, so I thought why not also add insertion of matching closing quotes/brackets over selected text. This does that. 2. `textareaInsertText` is replaced with `replaceTextareaSelection` which does the same but create a new edit history entry in the textarea so CTRL-Z works. The button that inserts tables into the textarea can now also be reverted via CTRL-Z, which was not possible before. --- .../js/features/comp/ComboMarkdownEditor.ts | 4 +- web_src/js/features/comp/EditorMarkdown.ts | 49 ++++++++++++++++--- web_src/js/features/comp/EditorUpload.ts | 4 +- 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/web_src/js/features/comp/ComboMarkdownEditor.ts b/web_src/js/features/comp/ComboMarkdownEditor.ts index 9ceb087005..86b1a037a0 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.ts +++ b/web_src/js/features/comp/ComboMarkdownEditor.ts @@ -17,7 +17,7 @@ import {POST} from '../../modules/fetch.ts'; import { EventEditorContentChanged, initTextareaMarkdown, - textareaInsertText, + replaceTextareaSelection, triggerEditorContentChanged, } from './EditorMarkdown.ts'; import {DropzoneCustomEventReloadFiles, initDropzone} from '../dropzone.ts'; @@ -273,7 +273,7 @@ export class ComboMarkdownEditor { let cols = parseInt(addTablePanel.querySelector('[name=cols]')!.value); rows = Math.max(1, Math.min(100, rows)); cols = Math.max(1, Math.min(100, cols)); - textareaInsertText(this.textarea, `\n${this.generateMarkdownTable(rows, cols)}\n\n`); + replaceTextareaSelection(this.textarea, `\n${this.generateMarkdownTable(rows, cols)}\n\n`); addTablePanelTippy.hide(); }); } diff --git a/web_src/js/features/comp/EditorMarkdown.ts b/web_src/js/features/comp/EditorMarkdown.ts index 2240e2f41b..da7bbcfef7 100644 --- a/web_src/js/features/comp/EditorMarkdown.ts +++ b/web_src/js/features/comp/EditorMarkdown.ts @@ -4,14 +4,23 @@ export function triggerEditorContentChanged(target: HTMLElement) { target.dispatchEvent(new CustomEvent(EventEditorContentChanged, {bubbles: true})); } -export function textareaInsertText(textarea: HTMLTextAreaElement, value: string) { - const startPos = textarea.selectionStart; - const endPos = textarea.selectionEnd; - textarea.value = textarea.value.substring(0, startPos) + value + textarea.value.substring(endPos); - textarea.selectionStart = startPos; - textarea.selectionEnd = startPos + value.length; +/** replace selected text or insert text by creating a new edit history entry, + * e.g. CTRL-Z works after this */ +export function replaceTextareaSelection(textarea: HTMLTextAreaElement, text: string) { + const before = textarea.value.slice(0, textarea.selectionStart); + const after = textarea.value.slice(textarea.selectionEnd); + textarea.focus(); - triggerEditorContentChanged(textarea); + let success = false; + try { + success = document.execCommand('insertText', false, text); // eslint-disable-line @typescript-eslint/no-deprecated + } catch {} + + // fall back to regular replacement + if (!success) { + textarea.value = `${before}${text}${after}`; + triggerEditorContentChanged(textarea); + } } type TextareaValueSelection = { @@ -176,7 +185,7 @@ export function markdownHandleIndention(tvs: TextareaValueSelection): MarkdownHa return {handled: true, valueSelection: {value: linesBuf.lines.join('\n'), selStart: newPos, selEnd: newPos}}; } -function handleNewline(textarea: HTMLTextAreaElement, e: Event) { +function handleNewline(textarea: HTMLTextAreaElement, e: KeyboardEvent) { const ret = markdownHandleIndention({value: textarea.value, selStart: textarea.selectionStart, selEnd: textarea.selectionEnd}); if (!ret.handled || !ret.valueSelection) return; // FIXME: the "handled" seems redundant, only valueSelection is enough (null for unhandled) e.preventDefault(); @@ -185,6 +194,28 @@ function handleNewline(textarea: HTMLTextAreaElement, e: Event) { triggerEditorContentChanged(textarea); } +// Keys that act as dead keys will not work because the spec dictates that such keys are +// emitted as `Dead` in e.key instead of the actual key. +const pairs = new Map([ + ["'", "'"], + ['"', '"'], + ['`', '`'], + ['(', ')'], + ['[', ']'], + ['{', '}'], + ['<', '>'], +]); + +function handlePairCharacter(textarea: HTMLTextAreaElement, e: KeyboardEvent): void { + const selStart = textarea.selectionStart; + const selEnd = textarea.selectionEnd; + if (selEnd === selStart) return; // do not process when no selection + e.preventDefault(); + const inner = textarea.value.substring(selStart, selEnd); + replaceTextareaSelection(textarea, `${e.key}${inner}${pairs.get(e.key)}`); + textarea.setSelectionRange(selStart + 1, selEnd + 1); +} + function isTextExpanderShown(textarea: HTMLElement): boolean { return Boolean(textarea.closest('text-expander')?.querySelector('.suggestions')); } @@ -198,6 +229,8 @@ export function initTextareaMarkdown(textarea: HTMLTextAreaElement) { } else if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey && !e.altKey) { // use Enter to insert a new line with the same indention and prefix handleNewline(textarea, e); + } else if (pairs.has(e.key)) { + handlePairCharacter(textarea, e); } }); } diff --git a/web_src/js/features/comp/EditorUpload.ts b/web_src/js/features/comp/EditorUpload.ts index 92593e7092..6aff4242ba 100644 --- a/web_src/js/features/comp/EditorUpload.ts +++ b/web_src/js/features/comp/EditorUpload.ts @@ -1,5 +1,5 @@ import {imageInfo} from '../../utils/image.ts'; -import {textareaInsertText, triggerEditorContentChanged} from './EditorMarkdown.ts'; +import {replaceTextareaSelection, triggerEditorContentChanged} from './EditorMarkdown.ts'; import { DropzoneCustomEventRemovedFile, DropzoneCustomEventUploadDone, @@ -43,7 +43,7 @@ class TextareaEditor { } insertPlaceholder(value: string) { - textareaInsertText(this.editor, value); + replaceTextareaSelection(this.editor, value); } replacePlaceholder(oldVal: string, newVal: string) {