diff --git a/models/issues/comment.go b/models/issues/comment.go index f15618bf500..25e74c01eab 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -699,7 +699,7 @@ func (c *Comment) LoadTime(ctx context.Context) error { return nil } var err error - c.Time, err = GetTrackedTimeByID(ctx, c.TimeID) + c.Time, err = GetTrackedTimeByID(ctx, c.IssueID, c.TimeID) return err } diff --git a/models/issues/tracked_time.go b/models/issues/tracked_time.go index 9c11881e442..0b5c341f1f2 100644 --- a/models/issues/tracked_time.go +++ b/models/issues/tracked_time.go @@ -311,13 +311,13 @@ func deleteTime(ctx context.Context, t *TrackedTime) error { } // GetTrackedTimeByID returns raw TrackedTime without loading attributes by id -func GetTrackedTimeByID(ctx context.Context, id int64) (*TrackedTime, error) { +func GetTrackedTimeByID(ctx context.Context, issueID, trackedTimeID int64) (*TrackedTime, error) { time := new(TrackedTime) - has, err := db.GetEngine(ctx).ID(id).Get(time) + has, err := db.GetEngine(ctx).ID(trackedTimeID).Where("issue_id = ?", issueID).Get(time) if err != nil { return nil, err } else if !has { - return nil, db.ErrNotExist{Resource: "tracked_time", ID: id} + return nil, db.ErrNotExist{Resource: "tracked_time", ID: trackedTimeID} } return time, nil } diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go index 57dc23b17f7..a734f3bf8f5 100644 --- a/modules/setting/config_provider.go +++ b/modules/setting/config_provider.go @@ -348,23 +348,6 @@ func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) { } } -// NewConfigProviderForLocale loads locale configuration from source and others. "string" if for a local file path, "[]byte" is for INI content -func NewConfigProviderForLocale(source any, others ...any) (ConfigProvider, error) { - iniFile, err := ini.LoadSources(ini.LoadOptions{ - IgnoreInlineComment: true, - UnescapeValueCommentSymbols: true, - IgnoreContinuation: true, - }, source, others...) - if err != nil { - return nil, fmt.Errorf("unable to load locale ini: %w", err) - } - iniFile.BlockMode = false - return &iniConfigProvider{ - ini: iniFile, - loadedFromEmpty: true, - }, nil -} - func init() { ini.PrettyFormat = false } diff --git a/modules/setting/config_provider_test.go b/modules/setting/config_provider_test.go index 63121f00740..dcbe280e824 100644 --- a/modules/setting/config_provider_test.go +++ b/modules/setting/config_provider_test.go @@ -113,24 +113,6 @@ func TestNewConfigProviderFromFile(t *testing.T) { assert.Equal(t, "[foo]\nk1 = a\n\n[bar]\nk1 = b\n", string(bs)) } -func TestNewConfigProviderForLocale(t *testing.T) { - // load locale from file - localeFile := t.TempDir() + "/locale.ini" - _ = os.WriteFile(localeFile, []byte(`k1=a`), 0o644) - cfg, err := NewConfigProviderForLocale(localeFile) - assert.NoError(t, err) - assert.Equal(t, "a", cfg.Section("").Key("k1").String()) - - // load locale from bytes - cfg, err = NewConfigProviderForLocale([]byte("k1=foo\nk2=bar")) - assert.NoError(t, err) - assert.Equal(t, "foo", cfg.Section("").Key("k1").String()) - cfg, err = NewConfigProviderForLocale([]byte("k1=foo\nk2=bar"), []byte("k2=xxx")) - assert.NoError(t, err) - assert.Equal(t, "foo", cfg.Section("").Key("k1").String()) - assert.Equal(t, "xxx", cfg.Section("").Key("k2").String()) -} - func TestDisableSaving(t *testing.T) { testFile := t.TempDir() + "/test.ini" _ = os.WriteFile(testFile, []byte("k1=a\nk2=b"), 0o644) diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go index 171da272ccc..7c1e77ccf5c 100644 --- a/routers/api/v1/repo/issue_tracked_time.go +++ b/routers/api/v1/repo/issue_tracked_time.go @@ -356,7 +356,7 @@ func DeleteTime(ctx *context.APIContext) { return } - time, err := issues_model.GetTrackedTimeByID(ctx, ctx.PathParamInt64("id")) + time, err := issues_model.GetTrackedTimeByID(ctx, issue.ID, ctx.PathParamInt64("id")) if err != nil { if db.IsErrNotExist(err) { ctx.APIErrorNotFound(err) diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go index 985bfd6698f..b9ed059fde9 100644 --- a/routers/web/repo/issue_timetrack.go +++ b/routers/web/repo/issue_timetrack.go @@ -60,7 +60,7 @@ func DeleteTime(c *context.Context) { return } - t, err := issues_model.GetTrackedTimeByID(c, c.PathParamInt64("timeid")) + t, err := issues_model.GetTrackedTimeByID(c, issue.ID, c.PathParamInt64("timeid")) if err != nil { if db.IsErrNotExist(err) { c.NotFound(err) diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index 576dcb95f6b..9ce35f9eab6 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -25,6 +25,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/migrations" notify_service "code.gitea.io/gitea/services/notify" repo_service "code.gitea.io/gitea/services/repository" ) @@ -172,7 +173,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*repo_module.SyncResu if m.LFS && setting.LFS.StartServer { log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo) endpoint := lfs.DetermineEndpoint(remoteURL.String(), m.LFSEndpoint) - lfsClient := lfs.NewClient(endpoint, nil) + lfsClient := lfs.NewClient(endpoint, migrations.NewMigrationHTTPTransport()) if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, m.Repo, gitRepo, lfsClient); err != nil { log.Error("SyncMirrors [repo: %-v]: failed to synchronize LFS objects for repository: %v", m.Repo.FullName(), err) } diff --git a/services/mirror/mirror_push.go b/services/mirror/mirror_push.go index 1acb227ac0a..844e18684b9 100644 --- a/services/mirror/mirror_push.go +++ b/services/mirror/mirror_push.go @@ -23,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/migrations" repo_service "code.gitea.io/gitea/services/repository" ) @@ -144,7 +145,7 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error { defer gitRepo.Close() endpoint := lfs.DetermineEndpoint(remoteURL.String(), "") - lfsClient := lfs.NewClient(endpoint, nil) + lfsClient := lfs.NewClient(endpoint, migrations.NewMigrationHTTPTransport()) if err := pushAllLFSObjects(ctx, gitRepo, lfsClient); err != nil { return util.SanitizeErrorCredentialURLs(err) } diff --git a/tests/integration/api_issue_tracked_time_test.go b/tests/integration/api_issue_tracked_time_test.go index 7d6992c3279..12f4def9e1a 100644 --- a/tests/integration/api_issue_tracked_time_test.go +++ b/tests/integration/api_issue_tracked_time_test.go @@ -79,6 +79,12 @@ func TestAPIDeleteTrackedTime(t *testing.T) { AddTokenAuth(token) MakeRequest(t, req, http.StatusForbidden) + // Deletion should be scoped to the issue in the URL + time5 := unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{ID: 5}) + req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/%d/times/%d", user2.Name, issue2.Repo.Name, issue2.Index, time5.ID). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusNotFound) + time3 := unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{ID: 3}) req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/%d/times/%d", user2.Name, issue2.Repo.Name, issue2.Index, time3.ID). AddTokenAuth(token) diff --git a/tests/integration/issue_timetrack_test.go b/tests/integration/issue_timetrack_test.go new file mode 100644 index 00000000000..0a3188fca01 --- /dev/null +++ b/tests/integration/issue_timetrack_test.go @@ -0,0 +1,32 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "testing" + + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestIssueTimeDeleteScoped(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + issue1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) + assert.NoError(t, issue1.LoadRepo(t.Context())) + tracked := unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{ID: 5}) + + session := loginUser(t, issue1.Repo.OwnerName) + url := fmt.Sprintf("/%s/%s/issues/%d/times/%d/delete", issue1.Repo.OwnerName, issue1.Repo.Name, issue1.Index, tracked.ID) + req := NewRequestWithValues(t, "POST", url, map[string]string{}) + session.MakeRequest(t, req, http.StatusNotFound) + + tracked = unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{ID: tracked.ID}) + assert.False(t, tracked.Deleted) +} diff --git a/web_src/js/features/codeeditor.ts b/web_src/js/features/codeeditor.ts index b2aa9ea1c5e..dc3f2fad81b 100644 --- a/web_src/js/features/codeeditor.ts +++ b/web_src/js/features/codeeditor.ts @@ -1,6 +1,6 @@ import {colord} from 'colord'; import {basename, extname, isObject, isDarkTheme} from '../utils.ts'; -import {onInputDebounce} from '../utils/dom.ts'; +import {onInputDebounce, toggleElem} from '../utils/dom.ts'; import type MonacoNamespace from 'monaco-editor'; type Monaco = typeof MonacoNamespace; @@ -197,19 +197,19 @@ function getFileBasedOptions(filename: string, lineWrapExts: string[]): MonacoOp } function togglePreviewDisplay(previewable: boolean): void { + // FIXME: here and below, the selector is too broad, it should only query in the editor related scope const previewTab = document.querySelector('a[data-tab="preview"]'); + // the "preview tab" exists for "file code editor", but doesn't exist for "git hook editor" if (!previewTab) return; - if (previewable) { - previewTab.style.display = ''; - } else { - previewTab.style.display = 'none'; - // If the "preview" tab was active, user changes the filename to a non-previewable one, - // then the "preview" tab becomes inactive (hidden), so the "write" tab should become active - if (previewTab.classList.contains('active')) { - const writeTab = document.querySelector('a[data-tab="write"]'); - writeTab?.click(); - } + toggleElem(previewTab, previewable); + if (previewable) return; + + // If not previewable but the "preview" tab was active (user changes the filename to a non-previewable one), + // then the "preview" tab becomes inactive (hidden), so the "write" tab should become active + if (previewTab.classList.contains('active')) { + const writeTab = document.querySelector('a[data-tab="write"]'); + writeTab?.click(); // TODO: it shouldn't need null-safe operator, writeTab must exist } } diff --git a/web_src/js/features/repo-actions.ts b/web_src/js/features/repo-actions.ts index 8d93fce53f4..671eef7f3ed 100644 --- a/web_src/js/features/repo-actions.ts +++ b/web_src/js/features/repo-actions.ts @@ -8,7 +8,7 @@ export function initRepositoryActionView() { // TODO: the parent element's full height doesn't work well now, // but we can not pollute the global style at the moment, only fix the height problem for pages with this component const parentFullHeight = document.querySelector('body > div.full.height'); - if (parentFullHeight) parentFullHeight.style.paddingBottom = '0'; + if (parentFullHeight) parentFullHeight.classList.add('tw-pb-0'); const view = createApp(RepoActionView, { runIndex: el.getAttribute('data-run-index'), diff --git a/web_src/js/features/repo-editor.ts b/web_src/js/features/repo-editor.ts index 93595c385e0..b100cd7c914 100644 --- a/web_src/js/features/repo-editor.ts +++ b/web_src/js/features/repo-editor.ts @@ -113,8 +113,8 @@ export function initRepoEditor() { warningDiv = document.createElement('div'); warningDiv.classList.add('ui', 'warning', 'message', 'flash-message', 'flash-warning', 'space-related'); warningDiv.innerHTML = html`

File path contains leading or trailing whitespace.

`; - // Add display 'block' because display is set to 'none' in formantic\build\semantic.css - warningDiv.style.display = 'block'; + // Change to `block` display because it is set to 'none' in fomantic/build/semantic.css + warningDiv.classList.add('tw-block'); const inputContainer = document.querySelector('.repo-editor-header')!; inputContainer.insertAdjacentElement('beforebegin', warningDiv); }