From f4e38e6367326276c9493d950cdb85718d244fdf Mon Sep 17 00:00:00 2001 From: Zettat123 Date: Fri, 28 Nov 2025 12:33:52 -0700 Subject: [PATCH 01/15] Fix Actions `pull_request.paths` being triggered incorrectly by rebase (#36045) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Partially fix #34710 The bug described in #34710 can be divided into two parts: `push.paths` and `pull_request.paths`. This PR fixes the issue related to `pull_request.paths`. The root cause is that the check for whether the workflow can be triggered happens **before** updating the PR’s merge base. This causes the file-change detection to use the old merge base. Therefore, we need to update the merge base first and then check whether the workflow can be triggered. --- services/pull/pull.go | 30 ++++++++----- services/pull/review.go | 2 +- tests/integration/actions_trigger_test.go | 54 +++++++++++++++++++++++ 3 files changed, 73 insertions(+), 13 deletions(-) diff --git a/services/pull/pull.go b/services/pull/pull.go index 6f0318ea49..04f48f0565 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -427,10 +427,16 @@ func AddTestPullRequestTask(opts TestPullRequestOptions) { for _, pr := range headBranchPRs { objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName) if opts.NewCommitID != "" && opts.NewCommitID != objectFormat.EmptyObjectID().String() { - changed, err := checkIfPRContentChanged(ctx, pr, opts.OldCommitID, opts.NewCommitID) + changed, newMergeBase, err := checkIfPRContentChanged(ctx, pr, opts.OldCommitID, opts.NewCommitID) if err != nil { log.Error("checkIfPRContentChanged: %v", err) } + if newMergeBase != "" && pr.MergeBase != newMergeBase { + pr.MergeBase = newMergeBase + if _, err := pr.UpdateColsIfNotMerged(ctx, "merge_base"); err != nil { + log.Error("Update merge base for %-v: %v", pr, err) + } + } if changed { // Mark old reviews as stale if diff to mergebase has changed if err := issues_model.MarkReviewsAsStale(ctx, pr.IssueID); err != nil { @@ -496,30 +502,30 @@ func AddTestPullRequestTask(opts TestPullRequestOptions) { // checkIfPRContentChanged checks if diff to target branch has changed by push // A commit can be considered to leave the PR untouched if the patch/diff with its merge base is unchanged -func checkIfPRContentChanged(ctx context.Context, pr *issues_model.PullRequest, oldCommitID, newCommitID string) (hasChanged bool, err error) { +func checkIfPRContentChanged(ctx context.Context, pr *issues_model.PullRequest, oldCommitID, newCommitID string) (hasChanged bool, mergeBase string, err error) { prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr) // FIXME: why it still needs to create a temp repo, since the alongside calls like GetDiverging doesn't do so anymore if err != nil { log.Error("CreateTemporaryRepoForPR %-v: %v", pr, err) - return false, err + return false, "", err } defer cancel() tmpRepo, err := git.OpenRepository(ctx, prCtx.tmpBasePath) if err != nil { - return false, fmt.Errorf("OpenRepository: %w", err) + return false, "", fmt.Errorf("OpenRepository: %w", err) } defer tmpRepo.Close() // Find the merge-base - _, base, err := tmpRepo.GetMergeBase("", "base", "tracking") + mergeBase, _, err = tmpRepo.GetMergeBase("", "base", "tracking") if err != nil { - return false, fmt.Errorf("GetMergeBase: %w", err) + return false, "", fmt.Errorf("GetMergeBase: %w", err) } - cmd := gitcmd.NewCommand("diff", "--name-only", "-z").AddDynamicArguments(newCommitID, oldCommitID, base) + cmd := gitcmd.NewCommand("diff", "--name-only", "-z").AddDynamicArguments(newCommitID, oldCommitID, mergeBase) stdoutReader, stdoutWriter, err := os.Pipe() if err != nil { - return false, fmt.Errorf("unable to open pipe for to run diff: %w", err) + return false, mergeBase, fmt.Errorf("unable to open pipe for to run diff: %w", err) } stderr := new(bytes.Buffer) @@ -535,19 +541,19 @@ func checkIfPRContentChanged(ctx context.Context, pr *issues_model.PullRequest, }). Run(ctx); err != nil { if err == util.ErrNotEmpty { - return true, nil + return true, mergeBase, nil } err = gitcmd.ConcatenateError(err, stderr.String()) log.Error("Unable to run diff on %s %s %s in tempRepo for PR[%d]%s/%s...%s/%s: Error: %v", - newCommitID, oldCommitID, base, + newCommitID, oldCommitID, mergeBase, pr.ID, pr.BaseRepo.FullName(), pr.BaseBranch, pr.HeadRepo.FullName(), pr.HeadBranch, err) - return false, fmt.Errorf("Unable to run git diff --name-only -z %s %s %s: %w", newCommitID, oldCommitID, base, err) + return false, mergeBase, fmt.Errorf("Unable to run git diff --name-only -z %s %s %s: %w", newCommitID, oldCommitID, mergeBase, err) } - return false, nil + return false, mergeBase, nil } // PushToBaseRepo pushes commits from branches of head repository to diff --git a/services/pull/review.go b/services/pull/review.go index 3977e351ca..9aeeb4c31d 100644 --- a/services/pull/review.go +++ b/services/pull/review.go @@ -333,7 +333,7 @@ func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repos if headCommitID == commitID { stale = false } else { - stale, err = checkIfPRContentChanged(ctx, pr, commitID, headCommitID) + stale, _, err = checkIfPRContentChanged(ctx, pr, commitID, headCommitID) if err != nil { return nil, nil, err } diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index d5486b6a39..9dc0ddb9df 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -30,6 +30,7 @@ import ( "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" + webhook_module "code.gitea.io/gitea/modules/webhook" issue_service "code.gitea.io/gitea/services/issue" pull_service "code.gitea.io/gitea/services/pull" release_service "code.gitea.io/gitea/services/release" @@ -1595,3 +1596,56 @@ jobs: assert.NotNil(t, run) }) } + +func TestPullRequestWithPathsRebase(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + repoName := "actions-pr-paths-rebase" + apiRepo := createActionsTestRepo(t, token, repoName, false) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) + apiCtx := NewAPITestContext(t, "user2", repoName, auth_model.AccessTokenScopeWriteRepository) + runner := newMockRunner() + runner.registerAsRepoRunner(t, "user2", repoName, "mock-runner", []string{"ubuntu-latest"}, false) + + // init files and dirs + testCreateFile(t, session, "user2", repoName, repo.DefaultBranch, "", "dir1/dir1.txt", "1") + testCreateFile(t, session, "user2", repoName, repo.DefaultBranch, "", "dir2/dir2.txt", "2") + wfFileContent := `name: ci +on: + pull_request: + paths: + - 'dir1/**' +jobs: + ci-job: + runs-on: ubuntu-latest + steps: + - run: echo 'ci' +` + testCreateFile(t, session, "user2", repoName, repo.DefaultBranch, "", ".gitea/workflows/ci.yml", wfFileContent) + + // create a PR to modify "dir1/dir1.txt", the workflow will be triggered + testEditFileToNewBranch(t, session, "user2", repoName, repo.DefaultBranch, "update-dir1", "dir1/dir1.txt", "11") + _, err := doAPICreatePullRequest(apiCtx, "user2", repoName, repo.DefaultBranch, "update-dir1")(t) + assert.NoError(t, err) + pr1Task := runner.fetchTask(t) + _, _, pr1Run := getTaskAndJobAndRunByTaskID(t, pr1Task.Id) + assert.Equal(t, webhook_module.HookEventPullRequest, pr1Run.Event) + + // create a PR to modify "dir2/dir2.txt" then update main branch and rebase, the workflow will not be triggered + testEditFileToNewBranch(t, session, "user2", repoName, repo.DefaultBranch, "update-dir2", "dir2/dir2.txt", "22") + apiPull, err := doAPICreatePullRequest(apiCtx, "user2", repoName, repo.DefaultBranch, "update-dir2")(t) + runner.fetchNoTask(t) + assert.NoError(t, err) + testEditFile(t, session, "user2", repoName, repo.DefaultBranch, "dir1/dir1.txt", "11") // change the file in "dir1" + req := NewRequestWithValues(t, "POST", + fmt.Sprintf("/%s/%s/pulls/%d/update?style=rebase", "user2", repoName, apiPull.Index), // update by rebase + map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + }) + session.MakeRequest(t, req, http.StatusSeeOther) + runner.fetchNoTask(t) + }) +} From b54af8811efb8e37b9c2e2d56dbc67f689132f83 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 29 Nov 2025 15:13:22 +0100 Subject: [PATCH 02/15] Replace `lint-go-gopls` with additional `govet` linters (#36028) Many (but not all) analyzers ran by `gopls check` are available in `golangci-lint` as part of default-disabled `govet` linters, so I think it's best we remove this manual linting step and let `golangci-lint` handle it. I hand-picked two available linters that were previously linted using gopls and this list is not exhaustive. This will reduce CI time by about 3 minutes. --- .golangci.yml | 4 ++++ Makefile | 9 +-------- tools/lint-go-gopls.sh | 23 ----------------------- 3 files changed, 5 insertions(+), 31 deletions(-) delete mode 100755 tools/lint-go-gopls.sh diff --git a/.golangci.yml b/.golangci.yml index 60482c415f..2f1587a1e6 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -114,6 +114,10 @@ linters: - stringsbuilder perfsprint: concat-loop: false + govet: + enable: + - nilness + - unusedwrite exclusions: generated: lax presets: diff --git a/Makefile b/Makefile index 4a7e73e582..647ab38e14 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,6 @@ XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1 GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.9 -GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.20.0 DOCKER_IMAGE ?= gitea/gitea DOCKER_TAG ?= latest @@ -333,7 +332,7 @@ lint-frontend: lint-js lint-css ## lint frontend files lint-frontend-fix: lint-js-fix lint-css-fix ## lint frontend files and fix issues .PHONY: lint-backend -lint-backend: lint-go lint-go-gitea-vet lint-go-gopls lint-editorconfig ## lint backend files +lint-backend: lint-go lint-go-gitea-vet lint-editorconfig ## lint backend files .PHONY: lint-backend-fix lint-backend-fix: lint-go-fix lint-go-gitea-vet lint-editorconfig ## lint backend files and fix issues @@ -396,11 +395,6 @@ lint-go-gitea-vet: ## lint go files with gitea-vet @echo "Running gitea-vet..." @$(GO) vet -vettool="$(shell GOOS= GOARCH= go tool -n gitea-vet)" ./... -.PHONY: lint-go-gopls -lint-go-gopls: ## lint go files with gopls - @echo "Running gopls check..." - @GO=$(GO) GOPLS_PACKAGE=$(GOPLS_PACKAGE) tools/lint-go-gopls.sh $(GO_SOURCES) - .PHONY: lint-editorconfig lint-editorconfig: @echo "Running editorconfig check..." @@ -844,7 +838,6 @@ deps-tools: ## install tool dependencies $(GO) install $(GO_LICENSES_PACKAGE) & \ $(GO) install $(GOVULNCHECK_PACKAGE) & \ $(GO) install $(ACTIONLINT_PACKAGE) & \ - $(GO) install $(GOPLS_PACKAGE) & \ wait node_modules: pnpm-lock.yaml diff --git a/tools/lint-go-gopls.sh b/tools/lint-go-gopls.sh deleted file mode 100755 index 2cd26ca6fe..0000000000 --- a/tools/lint-go-gopls.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -set -uo pipefail - -cd "$(dirname -- "${BASH_SOURCE[0]}")" && cd .. - -IGNORE_PATTERNS=( - "is deprecated" # TODO: fix these -) - -# lint all go files with 'gopls check' and look for lines starting with the -# current absolute path, indicating a error was found. This is necessary -# because the tool does not set non-zero exit code when errors are found. -# ref: https://github.com/golang/go/issues/67078 -ERROR_LINES=$("$GO" run "$GOPLS_PACKAGE" check -severity=warning "$@" 2>/dev/null | grep -E "^$PWD" | grep -vFf <(printf '%s\n' "${IGNORE_PATTERNS[@]}")); -NUM_ERRORS=$(echo -n "$ERROR_LINES" | wc -l) - -if [ "$NUM_ERRORS" -eq "0" ]; then - exit 0; -else - echo "$ERROR_LINES" - echo "Found $NUM_ERRORS 'gopls check' errors" - exit 1; -fi From 7d6861ac54f6040b73ae3c929be6fb523a392660 Mon Sep 17 00:00:00 2001 From: Bryan Mutai Date: Sun, 30 Nov 2025 06:58:15 +0300 Subject: [PATCH 03/15] Add "Go to file", "Delete Directory" to repo file list page (#35911) /claim #35898 Resolves #35898 ### Summary of key changes: 1. Add file name search/Go to file functionality to repo button row. 2. Add backend functionality to delete directory 3. Add context menu for directories with functionality to copy path & delete a directory 4. Move Add/Upload file dropdown to right for parity with Github UI 5. Add tree view to the edit/upload UI --------- Co-authored-by: wxiaoguang --- options/locale/locale_en-US.ini | 3 + routers/api/v1/repo/file.go | 4 - routers/web/repo/blame.go | 14 +- routers/web/repo/editor.go | 50 +++- routers/web/repo/editor_apply_patch.go | 2 +- routers/web/repo/editor_cherry_pick.go | 2 +- routers/web/repo/find.go | 24 -- routers/web/repo/view.go | 51 ++-- routers/web/repo/view_home.go | 51 ++-- routers/web/web.go | 1 - services/repository/files/temp_repo.go | 8 + services/repository/files/update.go | 53 +--- templates/repo/editor/delete.tmpl | 29 ++- templates/repo/editor/edit.tmpl | 92 +++---- templates/repo/editor/upload.tmpl | 28 ++- templates/repo/find/files.tmpl | 21 -- templates/repo/view.tmpl | 4 +- templates/repo/view_content.tmpl | 86 ++++--- templates/repo/view_file_tree.tmpl | 30 +-- .../repo/view_file_tree_toggle_button.tmpl | 6 + templates/repo/view_list.tmpl | 2 +- tests/integration/api_repo_file_helpers.go | 3 +- tests/integration/repofiles_change_test.go | 197 ++++++--------- web_src/css/editor/fileeditor.css | 20 -- web_src/css/modules/animations.css | 2 +- web_src/css/repo.css | 58 +++-- web_src/css/repo/home.css | 10 +- web_src/css/themes/theme-gitea-dark.css | 1 + web_src/css/themes/theme-gitea-light.css | 1 + web_src/js/components/RepoFileSearch.vue | 230 ++++++++++++++++++ web_src/js/features/repo-findfile.ts | 65 +---- web_src/js/features/repo-view-file-tree.ts | 8 +- web_src/js/index-domready.ts | 4 +- 33 files changed, 671 insertions(+), 489 deletions(-) delete mode 100644 routers/web/repo/find.go delete mode 100644 templates/repo/find/files.tmpl create mode 100644 templates/repo/view_file_tree_toggle_button.tmpl create mode 100644 web_src/js/components/RepoFileSearch.vue diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index b5b90b31a5..6712250924 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1354,8 +1354,11 @@ editor.this_file_locked = File is locked editor.must_be_on_a_branch = You must be on a branch to make or propose changes to this file. editor.fork_before_edit = You must fork this repository to make or propose changes to this file. editor.delete_this_file = Delete File +editor.delete_this_directory = Delete Directory editor.must_have_write_access = You must have write access to make or propose changes to this file. editor.file_delete_success = File "%s" has been deleted. +editor.directory_delete_success = Directory "%s" has been deleted. +editor.delete_directory = Delete directory '%s' editor.name_your_file = Name your file… editor.filename_help = Add a directory by typing its name followed by a slash ('/'). Remove a directory by typing backspace at the beginning of the input field. editor.or = or diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index ec34d54d22..27a0827a10 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -610,10 +610,6 @@ func handleChangeRepoFilesError(ctx *context.APIContext, err error) { ctx.APIError(http.StatusUnprocessableEntity, err) return } - if git.IsErrBranchNotExist(err) || files_service.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) { - ctx.APIError(http.StatusNotFound, err) - return - } if errors.Is(err, util.ErrNotExist) { ctx.APIError(http.StatusNotFound, err) return diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go index e304633f95..0eebff6aa8 100644 --- a/routers/web/repo/blame.go +++ b/routers/web/repo/blame.go @@ -10,7 +10,6 @@ import ( "net/url" "path" "strconv" - "strings" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -42,8 +41,8 @@ type blameRow struct { // RefBlame render blame page func RefBlame(ctx *context.Context) { - ctx.Data["PageIsViewCode"] = true ctx.Data["IsBlame"] = true + prepareRepoViewContent(ctx, ctx.Repo.RefTypeNameSubURL()) // Get current entry user currently looking at. if ctx.Repo.TreePath == "" { @@ -56,17 +55,6 @@ func RefBlame(ctx *context.Context) { return } - treeNames := strings.Split(ctx.Repo.TreePath, "/") - var paths []string - for i := range treeNames { - paths = append(paths, strings.Join(treeNames[:i+1], "/")) - } - - ctx.Data["Paths"] = paths - ctx.Data["TreeNames"] = treeNames - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) - blob := entry.Blob() fileSize := blob.Size() ctx.Data["FileSize"] = fileSize diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 8c630cb35f..983249a6d2 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -41,7 +41,12 @@ const ( editorCommitChoiceNewBranch string = "commit-to-new-branch" ) -func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) *context.CommitFormOptions { +func prepareEditorPage(ctx *context.Context, editorAction string) *context.CommitFormOptions { + prepareHomeTreeSideBarSwitch(ctx) + return prepareEditorPageFormOptions(ctx, editorAction) +} + +func prepareEditorPageFormOptions(ctx *context.Context, editorAction string) *context.CommitFormOptions { cleanedTreePath := files_service.CleanGitTreePath(ctx.Repo.TreePath) if cleanedTreePath != ctx.Repo.TreePath { redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(cleanedTreePath)) @@ -283,7 +288,7 @@ func EditFile(ctx *context.Context) { // on the "New File" page, we should add an empty path field to make end users could input a new name prepareTreePathFieldsAndPaths(ctx, util.Iif(isNewFile, ctx.Repo.TreePath+"/", ctx.Repo.TreePath)) - prepareEditorCommitFormOptions(ctx, editorAction) + prepareEditorPage(ctx, editorAction) if ctx.Written() { return } @@ -376,15 +381,16 @@ func EditFilePost(ctx *context.Context) { // DeleteFile render delete file page func DeleteFile(ctx *context.Context) { - prepareEditorCommitFormOptions(ctx, "_delete") + prepareEditorPage(ctx, "_delete") if ctx.Written() { return } ctx.Data["PageIsDelete"] = true + prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath) ctx.HTML(http.StatusOK, tplDeleteFile) } -// DeleteFilePost response for deleting file +// DeleteFilePost response for deleting file or directory func DeleteFilePost(ctx *context.Context) { parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx) if ctx.Written() { @@ -392,17 +398,37 @@ func DeleteFilePost(ctx *context.Context) { } treePath := ctx.Repo.TreePath - _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ + if treePath == "" { + ctx.JSONError("cannot delete root directory") // it should not happen unless someone is trying to be malicious + return + } + + // Check if the path is a directory + entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath) + if err != nil { + ctx.NotFoundOrServerError("GetTreeEntryByPath", git.IsErrNotExist, err) + return + } + + var commitMessage string + if entry.IsDir() { + commitMessage = parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete_directory", treePath)) + } else { + commitMessage = parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath)) + } + + _, err = files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ LastCommitID: parsed.form.LastCommit, OldBranch: parsed.OldBranchName, NewBranch: parsed.NewBranchName, Files: []*files_service.ChangeRepoFile{ { - Operation: "delete", - TreePath: treePath, + Operation: "delete", + TreePath: treePath, + DeleteRecursively: true, }, }, - Message: parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath)), + Message: commitMessage, Signoff: parsed.form.Signoff, Author: parsed.GitCommitter, Committer: parsed.GitCommitter, @@ -412,7 +438,11 @@ func DeleteFilePost(ctx *context.Context) { return } - ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath)) + if entry.IsDir() { + ctx.Flash.Success(ctx.Tr("repo.editor.directory_delete_success", treePath)) + } else { + ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath)) + } redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath) redirectForCommitChoice(ctx, parsed, redirectTreePath) } @@ -420,7 +450,7 @@ func DeleteFilePost(ctx *context.Context) { func UploadFile(ctx *context.Context) { ctx.Data["PageIsUpload"] = true prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath) - opts := prepareEditorCommitFormOptions(ctx, "_upload") + opts := prepareEditorPage(ctx, "_upload") if ctx.Written() { return } diff --git a/routers/web/repo/editor_apply_patch.go b/routers/web/repo/editor_apply_patch.go index aad7b4129c..357c6f3a21 100644 --- a/routers/web/repo/editor_apply_patch.go +++ b/routers/web/repo/editor_apply_patch.go @@ -14,7 +14,7 @@ import ( ) func NewDiffPatch(ctx *context.Context) { - prepareEditorCommitFormOptions(ctx, "_diffpatch") + prepareEditorPage(ctx, "_diffpatch") if ctx.Written() { return } diff --git a/routers/web/repo/editor_cherry_pick.go b/routers/web/repo/editor_cherry_pick.go index 099814a9fa..32e3c58e87 100644 --- a/routers/web/repo/editor_cherry_pick.go +++ b/routers/web/repo/editor_cherry_pick.go @@ -16,7 +16,7 @@ import ( ) func CherryPick(ctx *context.Context) { - prepareEditorCommitFormOptions(ctx, "_cherrypick") + prepareEditorPage(ctx, "_cherrypick") if ctx.Written() { return } diff --git a/routers/web/repo/find.go b/routers/web/repo/find.go deleted file mode 100644 index 3a3a7610e7..0000000000 --- a/routers/web/repo/find.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package repo - -import ( - "net/http" - - "code.gitea.io/gitea/modules/templates" - "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/services/context" -) - -const ( - tplFindFiles templates.TplName = "repo/find/files" -) - -// FindFiles render the page to find repository files -func FindFiles(ctx *context.Context) { - path := ctx.PathParam("*") - ctx.Data["TreeLink"] = ctx.Repo.RepoLink + "/src/" + util.PathEscapeSegments(path) - ctx.Data["DataLink"] = ctx.Repo.RepoLink + "/tree-list/" + util.PathEscapeSegments(path) - ctx.HTML(http.StatusOK, tplFindFiles) -} diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 09ac33cff4..8e85cc3278 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -245,27 +245,17 @@ func LastCommit(ctx *context.Context) { return } + // The "/lastcommit/" endpoint is used to render the embedded HTML content for the directory file listing with latest commit info + // It needs to construct correct links to the file items, but the route only accepts a commit ID, not a full ref name (branch or tag). + // So we need to get the ref name from the query parameter "refSubUrl". + // TODO: LAST-COMMIT-ASYNC-LOADING: it needs more tests to cover this + refSubURL := path.Clean(ctx.FormString("refSubUrl")) + prepareRepoViewContent(ctx, util.IfZero(refSubURL, ctx.Repo.RefTypeNameSubURL())) renderDirectoryFiles(ctx, 0) if ctx.Written() { return } - var treeNames []string - paths := make([]string, 0, 5) - if len(ctx.Repo.TreePath) > 0 { - treeNames = strings.Split(ctx.Repo.TreePath, "/") - for i := range treeNames { - paths = append(paths, strings.Join(treeNames[:i+1], "/")) - } - - ctx.Data["HasParentPath"] = true - if len(paths)-2 >= 0 { - ctx.Data["ParentPath"] = "/" + paths[len(paths)-2] - } - } - branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - ctx.Data["BranchLink"] = branchLink - ctx.HTML(http.StatusOK, tplRepoViewList) } @@ -289,7 +279,9 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri return nil } - ctx.Data["LastCommitLoaderURL"] = ctx.Repo.RepoLink + "/lastcommit/" + url.PathEscape(ctx.Repo.CommitID) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + // TODO: LAST-COMMIT-ASYNC-LOADING: search this keyword to see more details + lastCommitLoaderURL := ctx.Repo.RepoLink + "/lastcommit/" + url.PathEscape(ctx.Repo.CommitID) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + ctx.Data["LastCommitLoaderURL"] = lastCommitLoaderURL + "?refSubUrl=" + url.QueryEscape(ctx.Repo.RefTypeNameSubURL()) // Get current entry user currently looking at. entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) @@ -322,6 +314,21 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri ctx.ServerError("GetCommitsInfo", err) return nil } + + { + if timeout != 0 && !setting.IsProd && !setting.IsInTesting { + log.Debug("first call to get directory file commit info") + clearFilesCommitInfo := func() { + log.Warn("clear directory file commit info to force async loading on frontend") + for i := range files { + files[i].Commit = nil + } + } + _ = clearFilesCommitInfo + // clearFilesCommitInfo() // TODO: LAST-COMMIT-ASYNC-LOADING: debug the frontend async latest commit info loading, uncomment this line, and it needs more tests + } + } + ctx.Data["Files"] = files prepareDirectoryFileIcons(ctx, files) for _, f := range files { @@ -334,16 +341,6 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri if !loadLatestCommitData(ctx, latestCommit) { return nil } - - branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - treeLink := branchLink - - if len(ctx.Repo.TreePath) > 0 { - treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath) - } - - ctx.Data["TreeLink"] = treeLink - return allEntries } diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go index 17043055e5..00d30bedef 100644 --- a/routers/web/repo/view_home.go +++ b/routers/web/repo/view_home.go @@ -362,6 +362,32 @@ func redirectFollowSymlink(ctx *context.Context, treePathEntry *git.TreeEntry) b return false } +func prepareRepoViewContent(ctx *context.Context, refTypeNameSubURL string) { + // for: home, file list, file view, blame + ctx.Data["PageIsViewCode"] = true + ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled // show Upload File button or menu item + + // prepare the tree path navigation + var treeNames, paths []string + branchLink := ctx.Repo.RepoLink + "/src/" + refTypeNameSubURL + treeLink := branchLink + if ctx.Repo.TreePath != "" { + treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + treeNames = strings.Split(ctx.Repo.TreePath, "/") + for i := range treeNames { + paths = append(paths, strings.Join(treeNames[:i+1], "/")) + } + ctx.Data["HasParentPath"] = true + if len(paths)-2 >= 0 { + ctx.Data["ParentPath"] = "/" + paths[len(paths)-2] + } + } + ctx.Data["Paths"] = paths + ctx.Data["TreeLink"] = treeLink + ctx.Data["TreeNames"] = treeNames + ctx.Data["BranchLink"] = branchLink +} + // Home render repository home page func Home(ctx *context.Context) { if handleRepoHomeFeed(ctx) { @@ -383,8 +409,7 @@ func Home(ctx *context.Context) { title += ": " + ctx.Repo.Repository.Description } ctx.Data["Title"] = title - ctx.Data["PageIsViewCode"] = true - ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled // show New File / Upload File buttons + prepareRepoViewContent(ctx, ctx.Repo.RefTypeNameSubURL()) if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() { // empty or broken repositories need to be handled differently @@ -405,26 +430,6 @@ func Home(ctx *context.Context) { return } - // prepare the tree path - var treeNames, paths []string - branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - treeLink := branchLink - if ctx.Repo.TreePath != "" { - treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath) - treeNames = strings.Split(ctx.Repo.TreePath, "/") - for i := range treeNames { - paths = append(paths, strings.Join(treeNames[:i+1], "/")) - } - ctx.Data["HasParentPath"] = true - if len(paths)-2 >= 0 { - ctx.Data["ParentPath"] = "/" + paths[len(paths)-2] - } - } - ctx.Data["Paths"] = paths - ctx.Data["TreeLink"] = treeLink - ctx.Data["TreeNames"] = treeNames - ctx.Data["BranchLink"] = branchLink - // some UI components are only shown when the tree path is root isTreePathRoot := ctx.Repo.TreePath == "" @@ -455,7 +460,7 @@ func Home(ctx *context.Context) { if isViewHomeOnlyContent(ctx) { ctx.HTML(http.StatusOK, tplRepoViewContent) - } else if len(treeNames) != 0 { + } else if ctx.Repo.TreePath != "" { ctx.HTML(http.StatusOK, tplRepoView) } else { ctx.HTML(http.StatusOK, tplRepoHome) diff --git a/routers/web/web.go b/routers/web/web.go index dd1b391c68..89a570dce0 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1184,7 +1184,6 @@ func registerWebRoutes(m *web.Router) { m.Post("/{username}/{reponame}/markup", optSignIn, context.RepoAssignment, reqUnitsWithMarkdown, web.Bind(structs.MarkupOption{}), misc.Markup) m.Group("/{username}/{reponame}", func() { - m.Get("/find/*", repo.FindFiles) m.Group("/tree-list", func() { m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.TreeList) m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.TreeList) diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go index feb4811bb0..731f23855d 100644 --- a/services/repository/files/temp_repo.go +++ b/services/repository/files/temp_repo.go @@ -135,6 +135,14 @@ func (t *TemporaryUploadRepository) LsFiles(ctx context.Context, filenames ...st return fileList, nil } +func (t *TemporaryUploadRepository) RemoveRecursivelyFromIndex(ctx context.Context, path string) error { + _, _, err := gitcmd.NewCommand("rm", "--cached", "-r"). + AddDynamicArguments(path). + WithDir(t.basePath). + RunStdBytes(ctx) + return err +} + // RemoveFilesFromIndex removes the given files from the index func (t *TemporaryUploadRepository) RemoveFilesFromIndex(ctx context.Context, filenames ...string) error { objFmt, err := t.gitRepo.GetObjectFormat() diff --git a/services/repository/files/update.go b/services/repository/files/update.go index b07055d57a..4830f711fc 100644 --- a/services/repository/files/update.go +++ b/services/repository/files/update.go @@ -46,7 +46,10 @@ type ChangeRepoFile struct { FromTreePath string ContentReader io.ReadSeeker SHA string - Options *RepoFileOptions + + DeleteRecursively bool // when deleting, work as `git rm -r ...` + + Options *RepoFileOptions // FIXME: need to refactor, internal usage only } // ChangeRepoFilesOptions holds the repository files update options @@ -69,26 +72,6 @@ type RepoFileOptions struct { executable bool } -// ErrRepoFileDoesNotExist represents a "RepoFileDoesNotExist" kind of error. -type ErrRepoFileDoesNotExist struct { - Path string - Name string -} - -// IsErrRepoFileDoesNotExist checks if an error is a ErrRepoDoesNotExist. -func IsErrRepoFileDoesNotExist(err error) bool { - _, ok := err.(ErrRepoFileDoesNotExist) - return ok -} - -func (err ErrRepoFileDoesNotExist) Error() string { - return fmt.Sprintf("repository file does not exist [path: %s]", err.Path) -} - -func (err ErrRepoFileDoesNotExist) Unwrap() error { - return util.ErrNotExist -} - type LazyReadSeeker interface { io.ReadSeeker io.Closer @@ -217,24 +200,6 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use } } - for _, file := range opts.Files { - if file.Operation == "delete" { - // Get the files in the index - filesInIndex, err := t.LsFiles(ctx, file.TreePath) - if err != nil { - return nil, fmt.Errorf("DeleteRepoFile: %w", err) - } - - // Find the file we want to delete in the index - inFilelist := slices.Contains(filesInIndex, file.TreePath) - if !inFilelist { - return nil, ErrRepoFileDoesNotExist{ - Path: file.TreePath, - } - } - } - } - if hasOldBranch { // Get the commit of the original branch commit, err := t.GetBranchCommit(opts.OldBranch) @@ -272,8 +237,14 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use addedLfsPointers = append(addedLfsPointers, *addedLfsPointer) } case "delete": - if err = t.RemoveFilesFromIndex(ctx, file.TreePath); err != nil { - return nil, err + if file.DeleteRecursively { + if err = t.RemoveRecursivelyFromIndex(ctx, file.TreePath); err != nil { + return nil, err + } + } else { + if err = t.RemoveFilesFromIndex(ctx, file.TreePath); err != nil { + return nil, err + } } default: return nil, fmt.Errorf("invalid file operation: %s %s, supported operations are create, update, delete", file.Operation, file.Options.treePath) diff --git a/templates/repo/editor/delete.tmpl b/templates/repo/editor/delete.tmpl index bf6143f1cb..70769326a7 100644 --- a/templates/repo/editor/delete.tmpl +++ b/templates/repo/editor/delete.tmpl @@ -1,13 +1,30 @@ {{template "base/head" .}}
{{template "repo/header" .}} -
+
{{template "base/alert" .}} -
- {{.CsrfTokenHtml}} - {{template "repo/editor/common_top" .}} - {{template "repo/editor/commit_form" .}} -
+
+ {{template "repo/view_file_tree" .}} +
+
+ {{.CsrfTokenHtml}} + {{template "repo/editor/common_top" .}} +
+ {{/* although the UI isn't good enough, this header is necessary for the "left file tree view" toggle button, this button must exist */}} + {{template "repo/view_file_tree_toggle_button" .}} + {{/* then, to make the page looks overall good, add the breadcrumb here to make the toggle button can be shown in a text row, but not a single button*/}} + +
+ {{template "repo/editor/commit_form" .}} +
+
+
{{template "base/footer" .}} diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl index 0911d02e1f..e6b9c55770 100644 --- a/templates/repo/editor/edit.tmpl +++ b/templates/repo/editor/edit.tmpl @@ -1,53 +1,59 @@ {{template "base/head" .}}
{{template "repo/header" .}} -
+
{{template "base/alert" .}} -
+ {{template "repo/view_file_tree" .}} +
+ - {{.CsrfTokenHtml}} - {{template "repo/editor/common_top" .}} -
- {{template "repo/editor/common_breadcrumb" .}} + > + {{.CsrfTokenHtml}} + {{template "repo/editor/common_top" .}} +
+ {{template "repo/view_file_tree_toggle_button" .}} + {{template "repo/editor/common_breadcrumb" .}} +
+ {{if not .NotEditableReason}} + + {{else}} +
+
+

{{.NotEditableReason}}

+

{{ctx.Locale.Tr "repo.editor.file_not_editable_hint"}}

+
+
+ {{end}} + {{template "repo/editor/commit_form" .}} +
- {{if not .NotEditableReason}} - - {{else}} -
-
-

{{.NotEditableReason}}

-

{{ctx.Locale.Tr "repo.editor.file_not_editable_hint"}}

-
-
- {{end}} - {{template "repo/editor/commit_form" .}} - +
{{template "base/footer" .}} diff --git a/templates/repo/editor/upload.tmpl b/templates/repo/editor/upload.tmpl index 3e36c77b3b..847d6df88d 100644 --- a/templates/repo/editor/upload.tmpl +++ b/templates/repo/editor/upload.tmpl @@ -1,19 +1,25 @@ {{template "base/head" .}}
{{template "repo/header" .}} -
+
{{template "base/alert" .}} -
- {{.CsrfTokenHtml}} - {{template "repo/editor/common_top" .}} -
- {{template "repo/editor/common_breadcrumb" .}} +
+ {{template "repo/view_file_tree" .}} +
+ + {{.CsrfTokenHtml}} + {{template "repo/editor/common_top" .}} +
+ {{template "repo/view_file_tree_toggle_button" .}} + {{template "repo/editor/common_breadcrumb" .}} +
+
+ {{template "repo/upload" .}} +
+ {{template "repo/editor/commit_form" .}} +
-
- {{template "repo/upload" .}} -
- {{template "repo/editor/commit_form" .}} - +
{{template "base/footer" .}} diff --git a/templates/repo/find/files.tmpl b/templates/repo/find/files.tmpl deleted file mode 100644 index ce242796be..0000000000 --- a/templates/repo/find/files.tmpl +++ /dev/null @@ -1,21 +0,0 @@ -{{template "base/head" .}} -
- {{template "repo/header" .}} -
-
- {{.RepoName}} - / -
- -
-
- - - -
-
-

{{ctx.Locale.Tr "repo.find_file.no_matching"}}

-
-
-
-{{template "base/footer" .}} diff --git a/templates/repo/view.tmpl b/templates/repo/view.tmpl index f99fe2f57a..99f2a7da7e 100644 --- a/templates/repo/view.tmpl +++ b/templates/repo/view.tmpl @@ -17,9 +17,7 @@ {{template "repo/code/recently_pushed_new_branches" dict "RecentBranchesPromptData" .RecentBranchesPromptData}}
-
- {{template "repo/view_file_tree" .}} -
+ {{template "repo/view_file_tree" .}}
{{template "repo/view_content" .}}
diff --git a/templates/repo/view_content.tmpl b/templates/repo/view_content.tmpl index 66e4fffcb9..b31648fbbe 100644 --- a/templates/repo/view_content.tmpl +++ b/templates/repo/view_content.tmpl @@ -5,11 +5,7 @@
{{if not $isTreePathRoot}} - + {{template "repo/view_file_tree_toggle_button" .}} {{end}} {{template "repo/branch_dropdown" dict @@ -37,31 +33,6 @@ {{end}} - - {{if $isTreePathRoot}} - {{ctx.Locale.Tr "repo.find_file.go_to_file"}} - {{end}} - - {{if and .RefFullName.IsBranch (not .IsViewFile)}} - - {{end}} - {{if and $isTreePathRoot .Repository.IsTemplate}} {{ctx.Locale.Tr "repo.use_template"}} @@ -86,12 +57,65 @@
+
+ + {{if .RefFullName.IsBranch}} + {{$addFilePath := .TreePath}} + {{if .IsViewFile}} + {{if gt (len .TreeNames) 1}} + {{$addFilePath = StringUtils.Join (slice .TreeNames 0 (Eval (len .TreeNames) "-" 1)) "/"}} + {{else}} + {{$addFilePath = ""}} + {{end}} + {{end}} +
+ + {{if and (not .IsViewFile) (not $isTreePathRoot)}} + + {{end}} + {{end}} {{if $isTreePathRoot}} {{template "repo/clone_panel" .}} {{end}} {{if and (not $isTreePathRoot) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}} - + {{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}} {{end}} diff --git a/templates/repo/view_file_tree.tmpl b/templates/repo/view_file_tree.tmpl index 8aed05f346..f79fcc22aa 100644 --- a/templates/repo/view_file_tree.tmpl +++ b/templates/repo/view_file_tree.tmpl @@ -1,15 +1,17 @@ -
- - {{ctx.Locale.Tr "files"}} -
+
+
+ + {{ctx.Locale.Tr "files"}} +
-{{/* TODO: Dynamically move components such as refSelector and createPR here */}} -
+ {{/* TODO: Dynamically move components such as refSelector and createPR here */}} +
+
diff --git a/templates/repo/view_file_tree_toggle_button.tmpl b/templates/repo/view_file_tree_toggle_button.tmpl new file mode 100644 index 0000000000..3d6ea928ed --- /dev/null +++ b/templates/repo/view_file_tree_toggle_button.tmpl @@ -0,0 +1,6 @@ + diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl index 145494aa1a..61443ac465 100644 --- a/templates/repo/view_list.tmpl +++ b/templates/repo/view_list.tmpl @@ -47,7 +47,7 @@ {{end}} {{end}}
-
+
{{if $commit}} {{$commitLink := printf "%s/commit/%s" $.RepoLink (PathEscape $commit.ID.String)}} {{ctx.RenderUtils.RenderCommitMessageLinkSubject $commit.Message $commitLink $.Repository}} diff --git a/tests/integration/api_repo_file_helpers.go b/tests/integration/api_repo_file_helpers.go index f8d6fc803f..9a4c448664 100644 --- a/tests/integration/api_repo_file_helpers.go +++ b/tests/integration/api_repo_file_helpers.go @@ -5,6 +5,7 @@ package integration import ( "context" + "errors" "strings" "testing" @@ -72,7 +73,7 @@ func deleteFileInBranch(user *user_model.User, repo *repo_model.Repository, tree func createOrReplaceFileInBranch(user *user_model.User, repo *repo_model.Repository, treePath, branchName, content string) error { _, err := deleteFileInBranch(user, repo, treePath, branchName) - if err != nil && !files_service.IsErrRepoFileDoesNotExist(err) { + if err != nil && !errors.Is(err, util.ErrNotExist) { return err } diff --git a/tests/integration/repofiles_change_test.go b/tests/integration/repofiles_change_test.go index 6821f8bf61..6fd42401c5 100644 --- a/tests/integration/repofiles_change_test.go +++ b/tests/integration/repofiles_change_test.go @@ -5,6 +5,7 @@ package integration import ( "fmt" + "net/http" "net/url" "path" "strings" @@ -12,7 +13,6 @@ import ( "time" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/setting" @@ -22,6 +22,7 @@ import ( files_service "code.gitea.io/gitea/services/repository/files" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func getCreateRepoFilesOptions(repo *repo_model.Repository) *files_service.ChangeRepoFilesOptions { @@ -93,55 +94,6 @@ func getUpdateRepoFilesRenameOptions(repo *repo_model.Repository) *files_service } } -func getDeleteRepoFilesOptions(repo *repo_model.Repository) *files_service.ChangeRepoFilesOptions { - return &files_service.ChangeRepoFilesOptions{ - Files: []*files_service.ChangeRepoFile{ - { - Operation: "delete", - TreePath: "README.md", - SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f", - }, - }, - LastCommitID: "", - OldBranch: repo.DefaultBranch, - NewBranch: repo.DefaultBranch, - Message: "Deletes README.md", - Author: &files_service.IdentityOptions{ - GitUserName: "Bob Smith", - GitUserEmail: "bob@smith.com", - }, - Committer: nil, - } -} - -func getExpectedFileResponseForRepoFilesDelete() *api.FileResponse { - // Just returns fields that don't change, i.e. fields with commit SHAs and dates can't be determined - return &api.FileResponse{ - Content: nil, - Commit: &api.FileCommitResponse{ - Author: &api.CommitUser{ - Identity: api.Identity{ - Name: "Bob Smith", - Email: "bob@smith.com", - }, - }, - Committer: &api.CommitUser{ - Identity: api.Identity{ - Name: "Bob Smith", - Email: "bob@smith.com", - }, - }, - Message: "Deletes README.md\n", - }, - Verification: &api.PayloadCommitVerification{ - Verified: false, - Reason: "gpg.error.not_signed_commit", - Signature: "", - Payload: "", - }, - } -} - func getExpectedFileResponseForRepoFilesCreate(commitID string, lastCommit *git.Commit) *api.FileResponse { treePath := "new/file.txt" encoding := "base64" @@ -578,75 +530,88 @@ func TestChangeRepoFilesWithoutBranchNames(t *testing.T) { } func TestChangeRepoFilesForDelete(t *testing.T) { - onGiteaRun(t, testDeleteRepoFiles) -} + onGiteaRun(t, func(t *testing.T, u *url.URL) { + ctx, _ := contexttest.MockContext(t, "user2/repo1") + ctx.SetPathParam("id", "1") + contexttest.LoadRepo(t, ctx, 1) + contexttest.LoadRepoCommit(t, ctx) + contexttest.LoadUser(t, ctx, 2) + contexttest.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + repo := ctx.Repo.Repository + doer := ctx.Doer -func testDeleteRepoFiles(t *testing.T, u *url.URL) { - // setup - unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") - contexttest.LoadRepo(t, ctx, 1) - contexttest.LoadRepoCommit(t, ctx) - contexttest.LoadUser(t, ctx, 2) - contexttest.LoadGitRepo(t, ctx) - defer ctx.Repo.GitRepo.Close() - repo := ctx.Repo.Repository - doer := ctx.Doer - opts := getDeleteRepoFilesOptions(repo) + t.Run("Delete README.md by commit", func(t *testing.T) { + urlRaw := "/user2/repo1/raw/branch/branch2/README.md" + MakeRequest(t, NewRequest(t, "GET", urlRaw), http.StatusOK) + opts := &files_service.ChangeRepoFilesOptions{ + OldBranch: "branch2", + LastCommitID: "985f0301dba5e7b34be866819cd15ad3d8f508ee", + Files: []*files_service.ChangeRepoFile{ + { + Operation: "delete", + TreePath: "README.md", + }, + }, + Message: "test message", + } + filesResponse, err := files_service.ChangeRepoFiles(t.Context(), repo, doer, opts) + require.NoError(t, err) + assert.NotNil(t, filesResponse) + assert.Nil(t, filesResponse.Files[0]) + assert.Equal(t, "test message\n", filesResponse.Commit.Message) + MakeRequest(t, NewRequest(t, "GET", urlRaw), http.StatusNotFound) + }) - t.Run("Delete README.md file", func(t *testing.T) { - filesResponse, err := files_service.ChangeRepoFiles(t.Context(), repo, doer, opts) - assert.NoError(t, err) - expectedFileResponse := getExpectedFileResponseForRepoFilesDelete() - assert.NotNil(t, filesResponse) - assert.Nil(t, filesResponse.Files[0]) - assert.Equal(t, expectedFileResponse.Commit.Message, filesResponse.Commit.Message) - assert.Equal(t, expectedFileResponse.Commit.Author.Identity, filesResponse.Commit.Author.Identity) - assert.Equal(t, expectedFileResponse.Commit.Committer.Identity, filesResponse.Commit.Committer.Identity) - assert.Equal(t, expectedFileResponse.Verification, filesResponse.Verification) - }) + t.Run("Delete README.md with options", func(t *testing.T) { + urlRaw := "/user2/repo1/raw/branch/master/README.md" + MakeRequest(t, NewRequest(t, "GET", urlRaw), http.StatusOK) + opts := &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "delete", + TreePath: "README.md", + SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f", + }, + }, + OldBranch: repo.DefaultBranch, + NewBranch: repo.DefaultBranch, + Message: "Message for deleting README.md", + Author: &files_service.IdentityOptions{GitUserName: "Bob Smith", GitUserEmail: "bob@smith.com"}, + } + filesResponse, err := files_service.ChangeRepoFiles(t.Context(), repo, doer, opts) + require.NoError(t, err) + require.NotNil(t, filesResponse) + assert.Nil(t, filesResponse.Files[0]) + assert.Equal(t, "Message for deleting README.md\n", filesResponse.Commit.Message) + assert.Equal(t, api.Identity{Name: "Bob Smith", Email: "bob@smith.com"}, filesResponse.Commit.Author.Identity) + assert.Equal(t, api.Identity{Name: "Bob Smith", Email: "bob@smith.com"}, filesResponse.Commit.Committer.Identity) + assert.Equal(t, &api.PayloadCommitVerification{Reason: "gpg.error.not_signed_commit"}, filesResponse.Verification) + MakeRequest(t, NewRequest(t, "GET", urlRaw), http.StatusNotFound) + }) - t.Run("Verify README.md has been deleted", func(t *testing.T) { - filesResponse, err := files_service.ChangeRepoFiles(t.Context(), repo, doer, opts) - assert.Nil(t, filesResponse) - expectedError := "repository file does not exist [path: " + opts.Files[0].TreePath + "]" - assert.EqualError(t, err, expectedError) - }) -} - -// Test opts with branch names removed, same results -func TestChangeRepoFilesForDeleteWithoutBranchNames(t *testing.T) { - onGiteaRun(t, testDeleteRepoFilesWithoutBranchNames) -} - -func testDeleteRepoFilesWithoutBranchNames(t *testing.T, u *url.URL) { - // setup - unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") - contexttest.LoadRepo(t, ctx, 1) - contexttest.LoadRepoCommit(t, ctx) - contexttest.LoadUser(t, ctx, 2) - contexttest.LoadGitRepo(t, ctx) - defer ctx.Repo.GitRepo.Close() - - repo := ctx.Repo.Repository - doer := ctx.Doer - opts := getDeleteRepoFilesOptions(repo) - opts.OldBranch = "" - opts.NewBranch = "" - - t.Run("Delete README.md without Branch Name", func(t *testing.T) { - filesResponse, err := files_service.ChangeRepoFiles(t.Context(), repo, doer, opts) - assert.NoError(t, err) - expectedFileResponse := getExpectedFileResponseForRepoFilesDelete() - assert.NotNil(t, filesResponse) - assert.Nil(t, filesResponse.Files[0]) - assert.Equal(t, expectedFileResponse.Commit.Message, filesResponse.Commit.Message) - assert.Equal(t, expectedFileResponse.Commit.Author.Identity, filesResponse.Commit.Author.Identity) - assert.Equal(t, expectedFileResponse.Commit.Committer.Identity, filesResponse.Commit.Committer.Identity) - assert.Equal(t, expectedFileResponse.Verification, filesResponse.Verification) + t.Run("Delete directory", func(t *testing.T) { + urlRaw := "/user2/repo1/raw/branch/sub-home-md-img-check/docs/README.md" + MakeRequest(t, NewRequest(t, "GET", urlRaw), http.StatusOK) + opts := &files_service.ChangeRepoFilesOptions{ + OldBranch: "sub-home-md-img-check", + LastCommitID: "4649299398e4d39a5c09eb4f534df6f1e1eb87cc", + Files: []*files_service.ChangeRepoFile{ + { + Operation: "delete", + TreePath: "docs", + DeleteRecursively: true, + }, + }, + Message: "test message", + } + filesResponse, err := files_service.ChangeRepoFiles(t.Context(), repo, doer, opts) + require.NoError(t, err) + assert.NotNil(t, filesResponse) + assert.Nil(t, filesResponse.Files[0]) + assert.Equal(t, "test message\n", filesResponse.Commit.Message) + MakeRequest(t, NewRequest(t, "GET", urlRaw), http.StatusNotFound) + }) }) } diff --git a/web_src/css/editor/fileeditor.css b/web_src/css/editor/fileeditor.css index 698efffc99..12ae97a109 100644 --- a/web_src/css/editor/fileeditor.css +++ b/web_src/css/editor/fileeditor.css @@ -1,23 +1,3 @@ -.repository.file.editor .tab[data-tab="write"] { - padding: 0 !important; -} - -.repository.file.editor .tab[data-tab="write"] .editor-toolbar { - border: 0 !important; -} - -.repository.file.editor .tab[data-tab="write"] .CodeMirror { - border-left: 0; - border-right: 0; - border-bottom: 0; -} - -.repo-editor-header { - display: flex; - margin: 1rem 0; - padding: 3px 0; -} - .editor-toolbar { border-color: var(--color-secondary); } diff --git a/web_src/css/modules/animations.css b/web_src/css/modules/animations.css index 779339c46b..aedf53569a 100644 --- a/web_src/css/modules/animations.css +++ b/web_src/css/modules/animations.css @@ -28,7 +28,7 @@ aspect-ratio: 1; transform: translate(-50%, -50%); animation: isloadingspin 1000ms infinite linear; - border-width: 4px; + border-width: 3px; border-style: solid; border-color: var(--color-secondary) var(--color-secondary) var(--color-secondary-dark-8) var(--color-secondary-dark-8); border-radius: var(--border-radius-full); diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 9b70e0e6db..0bf37ca083 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -150,63 +150,68 @@ td .commit-summary { } } -.repository.file.list .non-diff-file-content .header .icon { +.non-diff-file-content .header .icon { font-size: 1em; } -.repository.file.list .non-diff-file-content .header .small.icon { +.non-diff-file-content .header .small.icon { font-size: 0.75em; } -.repository.file.list .non-diff-file-content .header .tiny.icon { +.non-diff-file-content .header .tiny.icon { font-size: 0.5em; } -.repository.file.list .non-diff-file-content .header .file-actions .btn-octicon { +.non-diff-file-content .header .file-actions .btn-octicon { line-height: var(--line-height-default); padding: 8px; vertical-align: middle; color: var(--color-text); } -.repository.file.list .non-diff-file-content .header .file-actions .btn-octicon:hover { +.non-diff-file-content .header .file-actions .btn-octicon:hover { color: var(--color-primary); } -.repository.file.list .non-diff-file-content .header .file-actions .btn-octicon-danger:hover { +.non-diff-file-content .header .file-actions .btn-octicon-danger:hover { color: var(--color-red); } -.repository.file.list .non-diff-file-content .header .file-actions .btn-octicon.disabled { +.non-diff-file-content .header .file-actions .btn-octicon.disabled { color: inherit; opacity: var(--opacity-disabled); cursor: default; } -.repository.file.list .non-diff-file-content .plain-text { +.non-diff-file-content .plain-text { padding: 1em 2em; } -.repository.file.list .non-diff-file-content .plain-text pre { +.non-diff-file-content .plain-text pre { overflow-wrap: anywhere; white-space: pre-wrap; } -.repository.file.list .non-diff-file-content .csv { +.non-diff-file-content .csv { overflow-x: auto; padding: 0 !important; } -.repository.file.list .non-diff-file-content pre { +.non-diff-file-content pre { overflow: auto; } -.repository.file.list .non-diff-file-content .asciicast { +.non-diff-file-content .asciicast { padding: 0 !important; } .repo-editor-header { + display: flex; + margin: 1rem 0; + padding: 3px 0; width: 100%; + gap: 0.5em; + align-items: center; } .repo-editor-header input { @@ -216,17 +221,13 @@ td .commit-summary { margin-right: 5px !important; } -.repository.file.editor .tabular.menu .svg { - margin-right: 5px; -} - .repository.file.editor .commit-form-wrapper { - padding-left: 48px; + padding-left: 58px; } .repository.file.editor .commit-form-wrapper .commit-avatar { float: left; - margin-left: -48px; + margin-left: -58px; } .repository.file.editor .commit-form-wrapper .commit-form { @@ -1409,12 +1410,25 @@ td .commit-summary { flex-grow: 1; } -.repo-button-row .ui.button { +.repo-button-row .ui.button, +.repo-view-container .ui.button.repo-view-file-tree-toggle { flex-shrink: 0; margin: 0; min-height: 30px; } +.repo-view-container .ui.button.repo-view-file-tree-toggle { + padding: 0 6px; +} + +.repo-button-row .repo-file-search-container .ui.input { + height: 30px; +} + +.repo-button-row .ui.dropdown > .menu { + margin-top: 4px; +} + tbody.commit-list { vertical-align: baseline; } @@ -1483,6 +1497,12 @@ tbody.commit-list { line-height: initial; } +.commit-body a.commit code, +.commit-summary a.commit code { + /* these links are generated by the render: ... */ + background: inherit; +} + .git-notes.top { text-align: left; } diff --git a/web_src/css/repo/home.css b/web_src/css/repo/home.css index ee371f1b1c..60bf1f17f9 100644 --- a/web_src/css/repo/home.css +++ b/web_src/css/repo/home.css @@ -54,7 +54,9 @@ gap: var(--page-spacing); } -.repo-view-container .repo-view-file-tree-container { +.repo-view-file-tree-container { + display: flex; + flex-direction: column; flex: 0 0 15%; min-width: 0; max-height: 100vh; @@ -65,6 +67,12 @@ overflow-y: hidden; } +@media (max-width: 767.98px) { + .repo-view-file-tree-container { + display: none; + } +} + .repo-view-content { flex: 1; min-width: 0; diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css index 1718a1f06b..f89752dc79 100644 --- a/web_src/css/themes/theme-gitea-dark.css +++ b/web_src/css/themes/theme-gitea-dark.css @@ -244,6 +244,7 @@ gitea-theme-meta-info { --color-highlight-fg: #87651e; --color-highlight-bg: #352c1c; --color-overlay-backdrop: #080808c0; + --color-danger: var(--color-red); 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 db54f5e5fb..1261ef8be0 100644 --- a/web_src/css/themes/theme-gitea-light.css +++ b/web_src/css/themes/theme-gitea-light.css @@ -244,6 +244,7 @@ gitea-theme-meta-info { --color-highlight-fg: #eed200; --color-highlight-bg: #fffbdd; --color-overlay-backdrop: #080808c0; + --color-danger: var(--color-red); accent-color: var(--color-accent); color-scheme: light; } diff --git a/web_src/js/components/RepoFileSearch.vue b/web_src/js/components/RepoFileSearch.vue new file mode 100644 index 0000000000..cbc1d50656 --- /dev/null +++ b/web_src/js/components/RepoFileSearch.vue @@ -0,0 +1,230 @@ + + + + + diff --git a/web_src/js/features/repo-findfile.ts b/web_src/js/features/repo-findfile.ts index 59c827126f..7a35a3c7ff 100644 --- a/web_src/js/features/repo-findfile.ts +++ b/web_src/js/features/repo-findfile.ts @@ -1,13 +1,8 @@ -import {svg} from '../svg.ts'; -import {toggleElem} from '../utils/dom.ts'; -import {pathEscapeSegments} from '../utils/url.ts'; -import {GET} from '../modules/fetch.ts'; +import {createApp} from 'vue'; +import RepoFileSearch from '../components/RepoFileSearch.vue'; +import {registerGlobalInitFunc} from '../modules/observer.ts'; const threshold = 50; -let files: Array = []; -let repoFindFileInput: HTMLInputElement; -let repoFindFileTableBody: HTMLElement; -let repoFindFileNoResult: HTMLElement; // return the case-insensitive sub-match result as an array: [unmatched, matched, unmatched, matched, ...] // res[even] is unmatched, res[odd] is matched, see unit tests for examples @@ -73,48 +68,14 @@ export function filterRepoFilesWeighted(files: Array, filter: string) { return filterResult; } -function filterRepoFiles(filter: string) { - const treeLink = repoFindFileInput.getAttribute('data-url-tree-link'); - repoFindFileTableBody.innerHTML = ''; - - const filterResult = filterRepoFilesWeighted(files, filter); - - toggleElem(repoFindFileNoResult, !filterResult.length); - for (const r of filterResult) { - const row = document.createElement('tr'); - const cell = document.createElement('td'); - const a = document.createElement('a'); - a.setAttribute('href', `${treeLink}/${pathEscapeSegments(r.matchResult.join(''))}`); - a.innerHTML = svg('octicon-file', 16, 'tw-mr-2'); - row.append(cell); - cell.append(a); - for (const [index, part] of r.matchResult.entries()) { - const span = document.createElement('span'); - // safely escape by using textContent - span.textContent = part; - span.title = span.textContent; - // if the target file path is "abc/xyz", to search "bx", then the matchResult is ['a', 'b', 'c/', 'x', 'yz'] - // the matchResult[odd] is matched and highlighted to red. - if (index % 2 === 1) span.classList.add('ui', 'text', 'red'); - a.append(span); - } - repoFindFileTableBody.append(row); - } -} - -async function loadRepoFiles() { - const response = await GET(repoFindFileInput.getAttribute('data-url-data-link')); - files = await response.json(); - filterRepoFiles(repoFindFileInput.value); -} - -export function initFindFileInRepo() { - repoFindFileInput = document.querySelector('#repo-file-find-input'); - if (!repoFindFileInput) return; - - repoFindFileTableBody = document.querySelector('#repo-find-file-table tbody'); - repoFindFileNoResult = document.querySelector('#repo-find-file-no-result'); - repoFindFileInput.addEventListener('input', () => filterRepoFiles(repoFindFileInput.value)); - - loadRepoFiles(); +export function initRepoFileSearch() { + registerGlobalInitFunc('initRepoFileSearch', (el) => { + createApp(RepoFileSearch, { + repoLink: el.getAttribute('data-repo-link'), + currentRefNameSubURL: el.getAttribute('data-current-ref-name-sub-url'), + treeListUrl: el.getAttribute('data-tree-list-url'), + noResultsText: el.getAttribute('data-no-results-text'), + placeholder: el.getAttribute('data-placeholder'), + }).mount(el); + }); } diff --git a/web_src/js/features/repo-view-file-tree.ts b/web_src/js/features/repo-view-file-tree.ts index f52b64cc51..98ffdb8a86 100644 --- a/web_src/js/features/repo-view-file-tree.ts +++ b/web_src/js/features/repo-view-file-tree.ts @@ -6,8 +6,12 @@ import {registerGlobalEventFunc} from '../modules/observer.ts'; const {appSubUrl} = window.config; +function isUserSignedIn() { + return Boolean(document.querySelector('#navbar .user-menu')); +} + async function toggleSidebar(btn: HTMLElement) { - const elToggleShow = document.querySelector('.repo-view-file-tree-toggle-show'); + const elToggleShow = document.querySelector('.repo-view-file-tree-toggle[data-toggle-action="show"]'); const elFileTreeContainer = document.querySelector('.repo-view-file-tree-container'); const shouldShow = btn.getAttribute('data-toggle-action') === 'show'; toggleElem(elFileTreeContainer, shouldShow); @@ -15,7 +19,7 @@ async function toggleSidebar(btn: HTMLElement) { // FIXME: need to remove "full height" style from parent element - if (!elFileTreeContainer.hasAttribute('data-user-is-signed-in')) return; + if (!isUserSignedIn()) return; await POST(`${appSubUrl}/user/settings/update_preferences`, { data: {codeViewShowFileTree: shouldShow}, }); diff --git a/web_src/js/index-domready.ts b/web_src/js/index-domready.ts index df56c85c86..8f5d4ecb15 100644 --- a/web_src/js/index-domready.ts +++ b/web_src/js/index-domready.ts @@ -16,7 +16,7 @@ import {initMarkupAnchors} from './markup/anchors.ts'; import {initNotificationCount} from './features/notification.ts'; import {initRepoIssueContentHistory} from './features/repo-issue-content.ts'; import {initStopwatch} from './features/stopwatch.ts'; -import {initFindFileInRepo} from './features/repo-findfile.ts'; +import {initRepoFileSearch} from './features/repo-findfile.ts'; import {initMarkupContent} from './markup/content.ts'; import {initRepoFileView} from './features/file-view.ts'; import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts'; @@ -101,7 +101,7 @@ const initPerformanceTracer = callInitFunctions([ initSshKeyFormParser, initStopwatch, initTableSort, - initFindFileInRepo, + initRepoFileSearch, initCopyContent, initAdminCommon, From 5340db4dbe400c7d03bddbe0bcfc7eddc4a768b5 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 1 Dec 2025 15:50:10 -0800 Subject: [PATCH 04/15] Fix bug when updating user email (#36058) Fix #20390 We should use `ReplacePrimaryEmailAddress` instead of `AdminAddOrSetPrimaryEmailAddress` when modify user's email from admin panel. And also we need a database transaction to keep deletion and insertion succeed at the same time. --- routers/web/admin/users.go | 2 +- services/user/email.go | 67 +++++++++++++++++++------------------- 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index a338936151..ed0eecf90a 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -409,7 +409,7 @@ func EditUserPost(ctx *context.Context) { } if form.Email != "" { - if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, u, form.Email); err != nil { + if err := user_service.ReplacePrimaryEmailAddress(ctx, u, form.Email); err != nil { switch { case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err): ctx.Data["Err_Email"] = true diff --git a/services/user/email.go b/services/user/email.go index 5c0de708e9..de1e024bd1 100644 --- a/services/user/email.go +++ b/services/user/email.go @@ -77,43 +77,44 @@ func ReplacePrimaryEmailAddress(ctx context.Context, u *user_model.User, emailSt return err } - if !u.IsOrganization() { - // Check if address exists already - email, err := user_model.GetEmailAddressByEmail(ctx, emailStr) - if err != nil && !errors.Is(err, util.ErrNotExist) { - return err - } - if email != nil { - if email.IsPrimary && email.UID == u.ID { - return nil + return db.WithTx(ctx, func(ctx context.Context) error { + if !u.IsOrganization() { + // Check if address exists already + email, err := user_model.GetEmailAddressByEmail(ctx, emailStr) + if err != nil && !errors.Is(err, util.ErrNotExist) { + return err + } + if email != nil { + if email.IsPrimary && email.UID == u.ID { + return nil + } + return user_model.ErrEmailAlreadyUsed{Email: emailStr} + } + + // Remove old primary address + primary, err := user_model.GetPrimaryEmailAddressOfUser(ctx, u.ID) + if err != nil { + return err + } + if _, err := db.DeleteByID[user_model.EmailAddress](ctx, primary.ID); err != nil { + return err + } + + // Insert new primary address + if _, err := user_model.InsertEmailAddress(ctx, &user_model.EmailAddress{ + UID: u.ID, + Email: emailStr, + IsActivated: true, + IsPrimary: true, + }); err != nil { + return err } - return user_model.ErrEmailAlreadyUsed{Email: emailStr} } - // Remove old primary address - primary, err := user_model.GetPrimaryEmailAddressOfUser(ctx, u.ID) - if err != nil { - return err - } - if _, err := db.DeleteByID[user_model.EmailAddress](ctx, primary.ID); err != nil { - return err - } + u.Email = emailStr - // Insert new primary address - email = &user_model.EmailAddress{ - UID: u.ID, - Email: emailStr, - IsActivated: true, - IsPrimary: true, - } - if _, err := user_model.InsertEmailAddress(ctx, email); err != nil { - return err - } - } - - u.Email = emailStr - - return user_model.UpdateUserCols(ctx, u, "email") + return user_model.UpdateUserCols(ctx, u, "email") + }) } func AddEmailAddresses(ctx context.Context, u *user_model.User, emails []string) error { From 1e777f92c79d4a5c96aa0183b0bdd62bf6150b80 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Tue, 2 Dec 2025 00:38:36 +0000 Subject: [PATCH 05/15] [skip ci] Updated translations via Crowdin --- options/locale/locale_ga-IE.ini | 5 +++++ options/locale/locale_pt-PT.ini | 3 +++ 2 files changed, 8 insertions(+) diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index abda334ff5..6b9ae41e9b 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -1354,8 +1354,11 @@ editor.this_file_locked=Tá an comhad faoi ghlas editor.must_be_on_a_branch=Caithfidh tú a bheith ar bhrainse chun athruithe a dhéanamh nó a mholadh ar an gcomhad seo. editor.fork_before_edit=Ní mór duit an stór seo a fhorcáil chun athruithe a dhéanamh nó a mholadh ar an gcomhad seo. editor.delete_this_file=Scrios Comhad +editor.delete_this_directory=Scrios Eolaire editor.must_have_write_access=Caithfidh rochtain scríofa a bheith agat chun athruithe a dhéanamh nó a mholadh ar an gcomhad seo. editor.file_delete_success=Tá an comhad "%s" scriosta. +editor.directory_delete_success=Scriosadh an eolaire "%s". +editor.delete_directory=Scrios an eolaire '%s' editor.name_your_file=Ainmnigh do chomhad… editor.filename_help=Cuir eolaire leis trína ainm a chlóscríobh ina dhiaidh sin le slash ('/'). Bain eolaire trí backspace a chlóscríobh ag tús an réimse ionchuir. editor.or=nó @@ -1482,6 +1485,7 @@ projects.column.new_submit=Cruthaigh Colún projects.column.new=Colún Nua projects.column.set_default=Socraigh Réamhshocrú projects.column.set_default_desc=Socraigh an colún seo mar réamhshocrú le haghaidh saincheisteanna agus tarraingtí gan chatagóir +projects.column.default_column_hint=Cuirfear saincheisteanna nua a chuirtear leis an tionscadal seo leis an gcolún seo projects.column.delete=Scrios Colún projects.column.deletion_desc=Ag scriosadh colún tionscadail aistríonn gach saincheist ghaolmhar chuig an gcolún. Lean ar aghaidh? projects.column.color=Dath @@ -3038,6 +3042,7 @@ dashboard.update_migration_poster_id=Nuashonraigh ID póstaer imir dashboard.git_gc_repos=Bailitheoir bruscair gach stórais dashboard.resync_all_sshkeys=Nuashonraigh an comhad '.ssh/authorized_keys' le heochracha SSH Gitea dashboard.resync_all_sshprincipals=Nuashonraigh an comhad '.ssh/authorized_principals' le príomhoidí SSH Gitea +dashboard.resync_all_hooks=Athshioncrónaigh crúcaí git na stórtha uile (réamhghlacadh, nuashonrú, iarghlacadh, próiseasghlacadh, ...) dashboard.reinit_missing_repos=Aththosaigh gach stórais Git atá in easnamh a bhfuil taifid ann dóibh dashboard.sync_external_users=Sioncrónaigh sonraí úsáideoirí seachtracha dashboard.cleanup_hook_task_table=Glan suas an tábla hook_task diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 22e16de93f..0b2e57ea00 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -1354,8 +1354,11 @@ editor.this_file_locked=Ficheiro bloqueado editor.must_be_on_a_branch=Tem que estar num ramo para fazer ou propor modificações neste ficheiro. editor.fork_before_edit=Tem que fazer uma derivação deste repositório para fazer ou propor modificações neste ficheiro. editor.delete_this_file=Eliminar ficheiro +editor.delete_this_directory=Eliminar pasta editor.must_have_write_access=Tem que ter permissões de escrita para fazer ou propor modificações neste ficheiro. editor.file_delete_success=O ficheiro "%s" foi eliminado. +editor.directory_delete_success=A pasta "%s" foi eliminada. +editor.delete_directory=Eliminar a pasta '%s' editor.name_your_file=Nomeie o seu ficheiro… editor.filename_help=Adicione uma pasta escrevendo o nome dessa pasta seguido de uma barra('/'). Remova uma pasta carregando na tecla de apagar ('←') no início do campo. editor.or=ou From a04a16dc2b3d4ebc7c56fa3cf40c6321516c22b4 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Tue, 2 Dec 2025 21:37:14 +0100 Subject: [PATCH 06/15] adopt changes --- models/issues/review_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/issues/review_test.go b/models/issues/review_test.go index 6795ea8e66..54adbef8c5 100644 --- a/models/issues/review_test.go +++ b/models/issues/review_test.go @@ -161,7 +161,7 @@ func TestGetReviewersByIssueID(t *testing.T) { UpdatedUnix: 946684815, }, &issues_model.Review{ - ID: 22, + ID: 23, Reviewer: user5, Type: issues_model.ReviewTypeRequest, UpdatedUnix: 946684817, From ca4b21c3054d9e510f2b07d8f224a999d188b9d3 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Tue, 2 Dec 2025 21:51:00 +0100 Subject: [PATCH 07/15] Revert "adopt changes" (was intendet for #33356) This reverts commit a04a16dc2b3d4ebc7c56fa3cf40c6321516c22b4. --- models/issues/review_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/issues/review_test.go b/models/issues/review_test.go index 54adbef8c5..6795ea8e66 100644 --- a/models/issues/review_test.go +++ b/models/issues/review_test.go @@ -161,7 +161,7 @@ func TestGetReviewersByIssueID(t *testing.T) { UpdatedUnix: 946684815, }, &issues_model.Review{ - ID: 23, + ID: 22, Reviewer: user5, Type: issues_model.ReviewTypeRequest, UpdatedUnix: 946684817, From 9f268edd2fbe43c8f97cdac607383dc86d05f9af Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 3 Dec 2025 00:26:07 +0100 Subject: [PATCH 08/15] Update go toolchain to 1.25.5 (#36074) Fixes: https://pkg.go.dev/vuln/GO-2025-4155 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 51cf47b2d3..6806e76ffc 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module code.gitea.io/gitea go 1.25.0 -toolchain go1.25.4 +toolchain go1.25.5 // rfc5280 said: "The serial number is an integer assigned by the CA to each certificate." // But some CAs use negative serial number, just relax the check. related: From 46d7adefe08e5fde1400261278449dccd082d0f4 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 3 Dec 2025 03:13:16 +0100 Subject: [PATCH 09/15] Enable TypeScript `strictNullChecks` (#35843) A big step towards enabling strict mode in Typescript. There was definitely a good share of potential bugs while refactoring this. When in doubt, I opted to keep the potentially broken behaviour. Notably, the `DOMEvent` type is gone, it was broken and we're better of with type assertions on `e.target`. --------- Signed-off-by: silverwind Signed-off-by: wxiaoguang Co-authored-by: delvh Co-authored-by: wxiaoguang --- eslint.config.ts | 2 +- tsconfig.json | 2 +- web_src/js/bootstrap.ts | 2 +- web_src/js/components/ActivityHeatmap.vue | 4 +- web_src/js/components/ContextPopup.vue | 4 +- web_src/js/components/DashboardRepoList.vue | 4 +- web_src/js/components/DiffCommitSelector.vue | 10 +- web_src/js/components/DiffFileTree.vue | 12 +- web_src/js/components/RepoActionView.vue | 4 +- .../js/components/RepoActivityTopAuthors.vue | 6 +- .../js/components/RepoBranchTagSelector.vue | 45 +++--- web_src/js/components/RepoContributors.vue | 14 +- web_src/js/components/RepoFileSearch.vue | 14 +- web_src/js/components/ViewFileTree.vue | 6 +- web_src/js/components/ViewFileTreeItem.vue | 2 +- web_src/js/components/ViewFileTreeStore.ts | 7 +- web_src/js/features/admin/common.ts | 38 ++--- web_src/js/features/admin/config.ts | 2 +- web_src/js/features/admin/selfcheck.ts | 4 +- web_src/js/features/captcha.ts | 4 +- web_src/js/features/citation.ts | 6 +- web_src/js/features/clipboard.ts | 7 +- web_src/js/features/colorpicker.ts | 15 +- web_src/js/features/common-button.ts | 18 +-- web_src/js/features/common-fetch-action.ts | 2 +- web_src/js/features/common-form.ts | 8 +- web_src/js/features/common-issue-list.ts | 11 +- web_src/js/features/common-organization.ts | 2 +- web_src/js/features/common-page.ts | 4 +- .../js/features/comp/ComboMarkdownEditor.ts | 53 +++---- web_src/js/features/comp/Cropper.ts | 11 +- .../js/features/comp/EditorMarkdown.test.ts | 2 +- web_src/js/features/comp/EditorMarkdown.ts | 7 +- web_src/js/features/comp/EditorUpload.ts | 11 +- web_src/js/features/comp/LabelEdit.ts | 28 ++-- web_src/js/features/comp/ReactionSelector.ts | 9 +- web_src/js/features/comp/SearchUserBox.ts | 2 +- web_src/js/features/comp/TextExpander.ts | 3 +- web_src/js/features/comp/WebHookEditor.ts | 9 +- web_src/js/features/copycontent.ts | 2 +- web_src/js/features/dropzone.ts | 17 +-- .../js/features/eventsource.sharedworker.ts | 15 +- web_src/js/features/file-view.ts | 6 +- web_src/js/features/heatmap.ts | 2 +- web_src/js/features/imagediff.ts | 16 +-- web_src/js/features/install.ts | 56 ++++---- web_src/js/features/notification.ts | 6 +- web_src/js/features/oauth2-settings.ts | 8 +- web_src/js/features/pull-view-file.ts | 20 +-- web_src/js/features/repo-branch.ts | 18 +-- web_src/js/features/repo-code.ts | 22 +-- web_src/js/features/repo-commit.ts | 4 +- web_src/js/features/repo-common.ts | 18 +-- web_src/js/features/repo-diff-commit.ts | 14 +- web_src/js/features/repo-diff.ts | 41 +++--- web_src/js/features/repo-editor.ts | 36 ++--- web_src/js/features/repo-graph.ts | 6 +- web_src/js/features/repo-home.ts | 20 +-- web_src/js/features/repo-issue-content.ts | 8 +- web_src/js/features/repo-issue-edit.ts | 42 +++--- web_src/js/features/repo-issue-list.ts | 30 ++-- web_src/js/features/repo-issue-pull.ts | 16 +-- .../features/repo-issue-sidebar-combolist.ts | 18 +-- web_src/js/features/repo-issue-sidebar.ts | 12 +- web_src/js/features/repo-issue.ts | 133 +++++++++--------- web_src/js/features/repo-legacy.ts | 4 +- web_src/js/features/repo-migrate.ts | 10 +- web_src/js/features/repo-migration.ts | 4 +- web_src/js/features/repo-milestone.ts | 4 +- web_src/js/features/repo-new.ts | 28 ++-- web_src/js/features/repo-projects.ts | 40 +++--- web_src/js/features/repo-release.ts | 18 +-- web_src/js/features/repo-search.ts | 6 +- .../features/repo-settings-branches.test.ts | 6 +- web_src/js/features/repo-settings-branches.ts | 4 +- web_src/js/features/repo-settings.ts | 32 ++--- web_src/js/features/repo-unicode-escape.ts | 4 +- web_src/js/features/repo-view-file-tree.ts | 6 +- web_src/js/features/repo-wiki.ts | 4 +- web_src/js/features/sshkey-helper.ts | 2 +- web_src/js/features/tablesort.ts | 6 +- web_src/js/features/user-auth-webauthn.ts | 10 +- web_src/js/features/user-auth.ts | 2 +- web_src/js/features/user-settings.ts | 6 +- web_src/js/htmx.ts | 4 +- web_src/js/markup/anchors.ts | 4 +- web_src/js/markup/codecopy.ts | 2 +- web_src/js/markup/mermaid.ts | 2 +- web_src/js/markup/refissue.ts | 2 +- web_src/js/markup/render-iframe.ts | 2 +- web_src/js/markup/tasklist.ts | 14 +- web_src/js/modules/diff-file.ts | 4 +- web_src/js/modules/fetch.ts | 4 +- web_src/js/modules/fomantic/tab.ts | 2 +- web_src/js/modules/observer.ts | 4 +- web_src/js/modules/sortable.ts | 4 +- web_src/js/modules/tippy.ts | 6 +- web_src/js/modules/toast.ts | 9 +- web_src/js/standalone/devtest.ts | 10 +- .../js/standalone/external-render-iframe.ts | 2 +- web_src/js/standalone/swagger.ts | 2 +- web_src/js/types.ts | 2 +- web_src/js/utils.ts | 36 ++--- web_src/js/utils/dom.test.ts | 6 +- web_src/js/utils/dom.ts | 25 ++-- web_src/js/webcomponents/absolute-date.ts | 8 +- web_src/js/webcomponents/origin-url.ts | 2 +- web_src/js/webcomponents/overflow-menu.ts | 12 +- 108 files changed, 686 insertions(+), 658 deletions(-) diff --git a/eslint.config.ts b/eslint.config.ts index c849cdbc62..c2fddc856c 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -205,7 +205,7 @@ export default defineConfig([ '@typescript-eslint/no-non-null-asserted-optional-chain': [2], '@typescript-eslint/no-non-null-assertion': [0], '@typescript-eslint/no-redeclare': [0], - '@typescript-eslint/no-redundant-type-constituents': [0], // rule does not properly work without strickNullChecks + '@typescript-eslint/no-redundant-type-constituents': [2], '@typescript-eslint/no-require-imports': [2], '@typescript-eslint/no-restricted-imports': [0], '@typescript-eslint/no-restricted-types': [0], diff --git a/tsconfig.json b/tsconfig.json index 1daf4b7233..2466faf592 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -40,7 +40,7 @@ "strictBindCallApply": true, "strictBuiltinIteratorReturn": true, "strictFunctionTypes": true, - "strictNullChecks": false, + "strictNullChecks": true, "stripInternal": true, "verbatimModuleSyntax": true, "types": [ diff --git a/web_src/js/bootstrap.ts b/web_src/js/bootstrap.ts index 4d3f39f5bf..a94e1d66b0 100644 --- a/web_src/js/bootstrap.ts +++ b/web_src/js/bootstrap.ts @@ -35,7 +35,7 @@ export function showGlobalErrorMessage(msg: string, msgType: Intent = 'error') { const msgCount = Number(msgDiv.getAttribute(`data-global-error-msg-count`)) + 1; msgDiv.setAttribute(`data-global-error-msg-compact`, msgCompact); msgDiv.setAttribute(`data-global-error-msg-count`, msgCount.toString()); - msgDiv.querySelector('.ui.message').textContent = msg + (msgCount > 1 ? ` (${msgCount})` : ''); + msgDiv.querySelector('.ui.message')!.textContent = msg + (msgCount > 1 ? ` (${msgCount})` : ''); msgContainer.prepend(msgDiv); } diff --git a/web_src/js/components/ActivityHeatmap.vue b/web_src/js/components/ActivityHeatmap.vue index d805817630..7c7e0cd94c 100644 --- a/web_src/js/components/ActivityHeatmap.vue +++ b/web_src/js/components/ActivityHeatmap.vue @@ -5,7 +5,7 @@ import {onMounted, shallowRef} from 'vue'; import type {Value as HeatmapValue, Locale as HeatmapLocale} from '@silverwind/vue3-calendar-heatmap'; defineProps<{ - values?: HeatmapValue[]; + values: HeatmapValue[]; locale: { textTotalContributions: string; heatMapLocale: Partial; @@ -28,7 +28,7 @@ const endDate = shallowRef(new Date()); onMounted(() => { // work around issue with first legend color being rendered twice and legend cut off - const legend = document.querySelector('.vch__external-legend-wrapper'); + const legend = document.querySelector('.vch__external-legend-wrapper')!; legend.setAttribute('viewBox', '12 0 80 10'); legend.style.marginRight = '-12px'; }); diff --git a/web_src/js/components/ContextPopup.vue b/web_src/js/components/ContextPopup.vue index 31db902adc..733144aae1 100644 --- a/web_src/js/components/ContextPopup.vue +++ b/web_src/js/components/ContextPopup.vue @@ -11,15 +11,17 @@ const props = defineProps<{ }>(); const loading = shallowRef(false); -const issue = shallowRef(null); +const issue = shallowRef(null); const renderedLabels = shallowRef(''); const errorMessage = shallowRef(''); const createdAt = computed(() => { + if (!issue?.value) return ''; return new Date(issue.value.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'}); }); const body = computed(() => { + if (!issue?.value) return ''; const body = issue.value.body.replace(/\n+/g, ' '); return body.length > 85 ? `${body.substring(0, 85)}…` : body; }); diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue index e938814ec6..e1f8475ea8 100644 --- a/web_src/js/components/DashboardRepoList.vue +++ b/web_src/js/components/DashboardRepoList.vue @@ -110,9 +110,9 @@ export default defineComponent({ }, mounted() { - const el = document.querySelector('#dashboard-repo-list'); + const el = document.querySelector('#dashboard-repo-list')!; this.changeReposFilter(this.reposFilter); - fomanticQuery(el.querySelector('.ui.dropdown')).dropdown(); + fomanticQuery(el.querySelector('.ui.dropdown')!).dropdown(); this.textArchivedFilterTitles = { 'archived': this.textShowOnlyArchived, diff --git a/web_src/js/components/DiffCommitSelector.vue b/web_src/js/components/DiffCommitSelector.vue index e9aa3c6744..fcc7af1fa0 100644 --- a/web_src/js/components/DiffCommitSelector.vue +++ b/web_src/js/components/DiffCommitSelector.vue @@ -23,7 +23,7 @@ type CommitListResult = { export default defineComponent({ components: {SvgIcon}, data: () => { - const el = document.querySelector('#diff-commit-select'); + const el = document.querySelector('#diff-commit-select')!; return { menuVisible: false, isLoading: false, @@ -35,7 +35,7 @@ export default defineComponent({ mergeBase: el.getAttribute('data-merge-base'), commits: [] as Array, hoverActivated: false, - lastReviewCommitSha: '', + lastReviewCommitSha: '' as string | null, uniqueIdMenu: generateElemId('diff-commit-selector-menu-'), uniqueIdShowAll: generateElemId('diff-commit-selector-show-all-'), }; @@ -165,7 +165,7 @@ export default defineComponent({ }, /** Called when user clicks on since last review */ changesSinceLastReviewClick() { - window.location.assign(`${this.issueLink}/files/${this.lastReviewCommitSha}..${this.commits.at(-1).id}${this.queryParams}`); + window.location.assign(`${this.issueLink}/files/${this.lastReviewCommitSha}..${this.commits.at(-1)!.id}${this.queryParams}`); }, /** Clicking on a single commit opens this specific commit */ commitClicked(commitId: string, newWindow = false) { @@ -193,7 +193,7 @@ export default defineComponent({ // find all selected commits and generate a link const firstSelected = this.commits.findIndex((x) => x.selected); const lastSelected = this.commits.findLastIndex((x) => x.selected); - let beforeCommitID: string; + let beforeCommitID: string | null = null; if (firstSelected === 0) { beforeCommitID = this.mergeBase; } else { @@ -204,7 +204,7 @@ export default defineComponent({ if (firstSelected === lastSelected) { // if the start and end are the same, we show this single commit window.location.assign(`${this.issueLink}/commits/${afterCommitID}${this.queryParams}`); - } else if (beforeCommitID === this.mergeBase && afterCommitID === this.commits.at(-1).id) { + } else if (beforeCommitID === this.mergeBase && afterCommitID === this.commits.at(-1)!.id) { // if the first commit is selected and the last commit is selected, we show all commits window.location.assign(`${this.issueLink}/files${this.queryParams}`); } else { diff --git a/web_src/js/components/DiffFileTree.vue b/web_src/js/components/DiffFileTree.vue index 981d10c1c1..e2934b967e 100644 --- a/web_src/js/components/DiffFileTree.vue +++ b/web_src/js/components/DiffFileTree.vue @@ -12,14 +12,14 @@ const store = diffTreeStore(); onMounted(() => { // Default to true if unset store.fileTreeIsVisible = localStorage.getItem(LOCAL_STORAGE_KEY) !== 'false'; - document.querySelector('.diff-toggle-file-tree-button').addEventListener('click', toggleVisibility); + document.querySelector('.diff-toggle-file-tree-button')!.addEventListener('click', toggleVisibility); hashChangeListener(); window.addEventListener('hashchange', hashChangeListener); }); onUnmounted(() => { - document.querySelector('.diff-toggle-file-tree-button').removeEventListener('click', toggleVisibility); + document.querySelector('.diff-toggle-file-tree-button')!.removeEventListener('click', toggleVisibility); window.removeEventListener('hashchange', hashChangeListener); }); @@ -33,7 +33,7 @@ function expandSelectedFile() { if (store.selectedItem) { const box = document.querySelector(store.selectedItem); const folded = box?.getAttribute('data-folded') === 'true'; - if (folded) setFileFolding(box, box.querySelector('.fold-file'), false); + if (folded) setFileFolding(box, box.querySelector('.fold-file')!, false); } } @@ -48,10 +48,10 @@ function updateVisibility(visible: boolean) { } function updateState(visible: boolean) { - const btn = document.querySelector('.diff-toggle-file-tree-button'); + const btn = document.querySelector('.diff-toggle-file-tree-button')!; const [toShow, toHide] = btn.querySelectorAll('.icon'); - const tree = document.querySelector('#diff-file-tree'); - const newTooltip = btn.getAttribute(visible ? 'data-hide-text' : 'data-show-text'); + const tree = document.querySelector('#diff-file-tree')!; + const newTooltip = btn.getAttribute(visible ? 'data-hide-text' : 'data-show-text')!; btn.setAttribute('data-tooltip-content', newTooltip); toggleElem(tree, visible); toggleElem(toShow, !visible); diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index 00748ee9bb..357a2ba10e 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -402,7 +402,7 @@ export default defineComponent({ } // auto-scroll to the last log line of the last step - let autoScrollJobStepElement: HTMLElement; + let autoScrollJobStepElement: HTMLElement | undefined; for (let stepIndex = 0; stepIndex < this.currentJob.steps.length; stepIndex++) { if (!autoScrollStepIndexes.get(stepIndex)) continue; autoScrollJobStepElement = this.getJobStepLogsContainer(stepIndex); @@ -468,7 +468,7 @@ export default defineComponent({ } const logLine = this.elStepsContainer().querySelector(selectedLogStep); if (!logLine) return; - logLine.querySelector('.line-num').click(); + logLine.querySelector('.line-num')!.click(); }, }, }); diff --git a/web_src/js/components/RepoActivityTopAuthors.vue b/web_src/js/components/RepoActivityTopAuthors.vue index 5a925f9943..1d04fa5239 100644 --- a/web_src/js/components/RepoActivityTopAuthors.vue +++ b/web_src/js/components/RepoActivityTopAuthors.vue @@ -1,7 +1,7 @@