+
{{template "base/footer" .}}
diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl
index 0911d02e1f4..e6b9c557700 100644
--- a/templates/repo/editor/edit.tmpl
+++ b/templates/repo/editor/edit.tmpl
@@ -1,53 +1,59 @@
{{template "base/head" .}}
{{template "base/alert" .}}
-
+
+ {{template "repo/view_file_tree" .}}
+
+
+
+
{{template "repo/header" .}}
-
+
{{template "base/footer" .}}
diff --git a/templates/repo/editor/upload.tmpl b/templates/repo/editor/upload.tmpl
index 3e36c77b3b9..847d6df88d7 100644
--- a/templates/repo/editor/upload.tmpl
+++ b/templates/repo/editor/upload.tmpl
@@ -1,19 +1,25 @@
{{template "base/head" .}}
{{template "base/alert" .}}
-
{{template "repo/header" .}}
-
+
{{template "base/alert" .}}
-
- {{.CsrfTokenHtml}}
- {{template "repo/editor/common_top" .}}
-
{{template "base/footer" .}}
diff --git a/templates/repo/find/files.tmpl b/templates/repo/find/files.tmpl
deleted file mode 100644
index ce242796bec..00000000000
--- a/templates/repo/find/files.tmpl
+++ /dev/null
@@ -1,21 +0,0 @@
-{{template "base/head" .}}
-
- {{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 "repo/header" .}}
-
-
-{{template "base/footer" .}}
diff --git a/templates/repo/view.tmpl b/templates/repo/view.tmpl
index f99fe2f57ab..99f2a7da7ee 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 66e4fffcb9b..b31648fbbe7 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 8aed05f3469..f79fcc22aa3 100644
--- a/templates/repo/view_file_tree.tmpl
+++ b/templates/repo/view_file_tree.tmpl
@@ -1,15 +1,17 @@
-
-
-
- {{ctx.Locale.Tr "files"}}
-
+
+
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 00000000000..3d6ea928edd
--- /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 145494aa1a9..61443ac4652 100644
--- a/templates/repo/view_list.tmpl
+++ b/templates/repo/view_list.tmpl
@@ -47,7 +47,7 @@
{{end}}
{{end}}
+
+ {{ctx.Locale.Tr "files"}}
+
-{{/* TODO: Dynamically move components such as refSelector and createPR here */}}
-
+ {{/* TODO: Dynamically move components such as refSelector and createPR here */}}
+
+
+
{{if $commit}}
{{$commitLink := printf "%s/commit/%s" $.RepoLink (PathEscape $commit.ID.String)}}
{{ctx.RenderUtils.RenderCommitMessageLinkSubject $commit.Message $commitLink $.Repository}}
diff --git a/templates/shared/combomarkdowneditor.tmpl b/templates/shared/combomarkdowneditor.tmpl
index fa3e6c6adee..62a42552aec 100644
--- a/templates/shared/combomarkdowneditor.tmpl
+++ b/templates/shared/combomarkdowneditor.tmpl
@@ -43,6 +43,7 @@
{{svg "octicon-bold"}}
{{svg "octicon-italic"}}
+ {{svg "octicon-strikethrough"}}
{{svg "octicon-quote"}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 0cefa6795f4..b37937dcee9 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -6750,6 +6750,66 @@
}
}
},
+ "put": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "repository"
+ ],
+ "summary": "Update a branch reference to a new commit",
+ "operationId": "repoUpdateBranch",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "owner of the repo",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repo",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the branch",
+ "name": "branch",
+ "in": "path",
+ "required": true
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "schema": {
+ "$ref": "#/definitions/UpdateBranchRepoOption"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "$ref": "#/responses/empty"
+ },
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ },
+ "409": {
+ "$ref": "#/responses/conflict"
+ },
+ "422": {
+ "$ref": "#/responses/validationError"
+ }
+ }
+ },
"delete": {
"produces": [
"application/json"
@@ -28702,6 +28762,31 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
+ "UpdateBranchRepoOption": {
+ "description": "UpdateBranchRepoOption options when updating a branch reference in a repository",
+ "type": "object",
+ "required": [
+ "new_commit_id"
+ ],
+ "properties": {
+ "force": {
+ "description": "Force update even if the change is not a fast-forward",
+ "type": "boolean",
+ "x-go-name": "Force"
+ },
+ "new_commit_id": {
+ "description": "New commit SHA (or any ref) the branch should point to",
+ "type": "string",
+ "x-go-name": "NewCommitID"
+ },
+ "old_commit_id": {
+ "description": "Expected old commit SHA of the branch; if provided it must match the current tip",
+ "type": "string",
+ "x-go-name": "OldCommitID"
+ }
+ },
+ "x-go-package": "code.gitea.io/gitea/modules/structs"
+ },
"UpdateFileOptions": {
"description": "UpdateFileOptions options for updating or creating a file\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)",
"type": "object",
diff --git a/tests/e2e/README.md b/tests/e2e/README.md
index db083793d8b..ea3805ab95c 100644
--- a/tests/e2e/README.md
+++ b/tests/e2e/README.md
@@ -17,13 +17,7 @@ make clean frontend
## Install playwright system dependencies
```
-npx playwright install-deps
-```
-
-
-## Run all tests via local act_runner
-```
-act_runner exec -W ./.github/workflows/pull-e2e-tests.yml --event=pull_request --default-actions-url="https://github.com" -i catthehacker/ubuntu:runner-latest
+pnpm exec playwright install-deps
```
## Run sqlite e2e tests
@@ -85,8 +79,8 @@ TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=test TEST_MSSQL_USERNAME=sa TES
Although the main goal of e2e is assertion testing, we have added a framework for visual regress testing. If you are working on front-end features, please use the following:
- Check out `main`, `make clean frontend`, and run e2e tests with `VISUAL_TEST=1` to generate outputs. This will initially fail, as no screenshots exist. You can run the e2e tests again to assert it passes.
- - Check out your branch, `make clean frontend`, and run e2e tests with `VISUAL_TEST=1`. You should be able to assert you front-end changes don't break any other tests unintentionally.
+ - Check out your branch, `make clean frontend`, and run e2e tests with `VISUAL_TEST=1`. You should be able to assert you front-end changes don't break any other tests unintentionally.
-VISUAL_TEST=1 will create screenshots in tests/e2e/test-snapshots. The test will fail the first time this is enabled (until we get visual test image persistence figured out), because it will be testing against an empty screenshot folder.
+VISUAL_TEST=1 will create screenshots in tests/e2e/test-snapshots. The test will fail the first time this is enabled (until we get visual test image persistence figured out), because it will be testing against an empty screenshot folder.
ACCEPT_VISUAL=1 will overwrite the snapshot images with new images.
diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go
index d5486b6a392..9dc0ddb9df8 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)
+ })
+}
diff --git a/tests/integration/api_actions_permission_test.go b/tests/integration/api_actions_permission_test.go
new file mode 100644
index 00000000000..072e2635a96
--- /dev/null
+++ b/tests/integration/api_actions_permission_test.go
@@ -0,0 +1,54 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/test"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func testActionUserSignIn(t *testing.T) {
+ req := NewRequest(t, "GET", "/api/v1/user").
+ AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var u api.User
+ DecodeJSON(t, resp, &u)
+ assert.Equal(t, "gitea-actions", u.UserName)
+}
+
+func testActionUserAccessPublicRepo(t *testing.T) {
+ req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/raw/README.md").
+ AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
+ resp := MakeRequest(t, req, http.StatusOK)
+ assert.Equal(t, "file", resp.Header().Get("x-gitea-object-type"))
+
+ defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
+
+ req = NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/raw/README.md").
+ AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
+ resp = MakeRequest(t, req, http.StatusOK)
+ assert.Equal(t, "file", resp.Header().Get("x-gitea-object-type"))
+}
+
+func testActionUserNoAccessOtherPrivateRepo(t *testing.T) {
+ req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo2/raw/README.md").
+ AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
+ MakeRequest(t, req, http.StatusNotFound)
+}
+
+func TestActionUserAccessPermission(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ t.Run("ActionUserSignIn", testActionUserSignIn)
+ t.Run("ActionUserAccessPublicRepo", testActionUserAccessPublicRepo)
+ t.Run("ActionUserNoAccessOtherPrivateRepo", testActionUserNoAccessOtherPrivateRepo)
+}
diff --git a/tests/integration/api_admin_test.go b/tests/integration/api_admin_test.go
index dbd62c4078b..763d4d526b3 100644
--- a/tests/integration/api_admin_test.go
+++ b/tests/integration/api_admin_test.go
@@ -382,10 +382,12 @@ func TestAPIEditUser_NotAllowedEmailDomain(t *testing.T) {
SourceID: 0,
Email: &newEmail,
}).AddTokenAuth(token)
- resp := MakeRequest(t, req, http.StatusOK)
- assert.Equal(t, "the domain of user email user2@example1.com conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", resp.Header().Get("X-Gitea-Warning"))
+ resp := MakeRequest(t, req, http.StatusBadRequest)
+ errMap := make(map[string]string)
+ assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), &errMap))
+ assert.Equal(t, "the domain of user email user2@example1.com conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", errMap["message"])
- originalEmail := "user2@example.com"
+ originalEmail := "user2@example.org"
req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{
LoginName: "user2",
SourceID: 0,
diff --git a/tests/integration/api_branch_test.go b/tests/integration/api_branch_test.go
index 2147ef9d0d9..043aa10c7fb 100644
--- a/tests/integration/api_branch_test.go
+++ b/tests/integration/api_branch_test.go
@@ -4,6 +4,8 @@
package integration
import (
+ "encoding/base64"
+ "fmt"
"net/http"
"net/http/httptest"
"net/url"
@@ -243,6 +245,79 @@ func TestAPIRenameBranch(t *testing.T) {
})
}
+func TestAPIUpdateBranchReference(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+ ctx := NewAPITestContext(t, "user2", "update-branch", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
+ giteaURL.Path = ctx.GitPath()
+
+ var defaultBranch string
+ t.Run("CreateRepo", doAPICreateRepository(ctx, false, func(t *testing.T, repo api.Repository) {
+ defaultBranch = repo.DefaultBranch
+ }))
+
+ createBranchReq := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/branches", ctx.Username, ctx.Reponame), &api.CreateBranchRepoOption{
+ BranchName: "feature",
+ OldRefName: defaultBranch,
+ }).AddTokenAuth(ctx.Token)
+ ctx.Session.MakeRequest(t, createBranchReq, http.StatusCreated)
+
+ var featureInitialCommit string
+ t.Run("LoadFeatureBranch", doAPIGetBranch(ctx, "feature", func(t *testing.T, branch api.Branch) {
+ featureInitialCommit = branch.Commit.ID
+ assert.NotEmpty(t, featureInitialCommit)
+ }))
+
+ content := base64.StdEncoding.EncodeToString([]byte("branch update test"))
+ var newCommit string
+ doAPICreateFile(ctx, "docs/update.txt", &api.CreateFileOptions{
+ FileOptions: api.FileOptions{
+ BranchName: defaultBranch,
+ NewBranchName: defaultBranch,
+ Message: "add docs/update.txt",
+ },
+ ContentBase64: content,
+ }, func(t *testing.T, resp api.FileResponse) {
+ newCommit = resp.Commit.SHA
+ assert.NotEmpty(t, newCommit)
+ })(t)
+
+ updateReq := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/branches/%s", ctx.Username, ctx.Reponame, "feature"), &api.UpdateBranchRepoOption{
+ NewCommitID: newCommit,
+ OldCommitID: featureInitialCommit,
+ }).AddTokenAuth(ctx.Token)
+ ctx.Session.MakeRequest(t, updateReq, http.StatusNoContent)
+
+ t.Run("FastForwardApplied", doAPIGetBranch(ctx, "feature", func(t *testing.T, branch api.Branch) {
+ assert.Equal(t, newCommit, branch.Commit.ID)
+ }))
+
+ staleReq := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/branches/%s", ctx.Username, ctx.Reponame, "feature"), &api.UpdateBranchRepoOption{
+ NewCommitID: newCommit,
+ OldCommitID: featureInitialCommit,
+ }).AddTokenAuth(ctx.Token)
+ ctx.Session.MakeRequest(t, staleReq, http.StatusUnprocessableEntity)
+
+ nonFFReq := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/branches/%s", ctx.Username, ctx.Reponame, "feature"), &api.UpdateBranchRepoOption{
+ NewCommitID: featureInitialCommit,
+ OldCommitID: newCommit,
+ }).AddTokenAuth(ctx.Token)
+ ctx.Session.MakeRequest(t, nonFFReq, http.StatusUnprocessableEntity)
+
+ forceReq := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/branches/%s", ctx.Username, ctx.Reponame, "feature"), &api.UpdateBranchRepoOption{
+ NewCommitID: featureInitialCommit,
+ OldCommitID: newCommit,
+ Force: true,
+ }).AddTokenAuth(ctx.Token)
+ ctx.Session.MakeRequest(t, forceReq, http.StatusNoContent)
+
+ t.Run("ForceApplied", doAPIGetBranch(ctx, "feature", func(t *testing.T, branch api.Branch) {
+ assert.Equal(t, featureInitialCommit, branch.Commit.ID)
+ }))
+ })
+}
+
func testAPIRenameBranch(t *testing.T, doerName, ownerName, repoName, from, to string, expectedHTTPStatus int) *httptest.ResponseRecorder {
token := getUserToken(t, doerName, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestWithJSON(t, "PATCH", "api/v1/repos/"+ownerName+"/"+repoName+"/branches/"+from, &api.RenameBranchRepoOption{
diff --git a/tests/integration/api_repo_file_helpers.go b/tests/integration/api_repo_file_helpers.go
index f8d6fc803f1..9a4c4486646 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/attachment_test.go b/tests/integration/attachment_test.go
index 44aaee09f89..18efde72142 100644
--- a/tests/integration/attachment_test.go
+++ b/tests/integration/attachment_test.go
@@ -7,17 +7,23 @@ import (
"bytes"
"image"
"image/png"
+ "io/fs"
"mime/multipart"
"net/http"
+ "os"
"strings"
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/test"
+ "code.gitea.io/gitea/modules/web"
+ route_web "code.gitea.io/gitea/routers/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func testGeneratePngBytes() []byte {
@@ -52,14 +58,38 @@ func testCreateIssueAttachment(t *testing.T, session *TestSession, csrf, repoURL
return obj["uuid"]
}
-func TestCreateAnonymousAttachment(t *testing.T) {
+func TestAttachments(t *testing.T) {
defer tests.PrepareTestEnv(t)()
+ t.Run("CreateAnonymousAttachment", testCreateAnonymousAttachment)
+ t.Run("CreateUser2IssueAttachment", testCreateUser2IssueAttachment)
+ t.Run("UploadAttachmentDeleteTemp", testUploadAttachmentDeleteTemp)
+ t.Run("GetAttachment", testGetAttachment)
+}
+
+func testUploadAttachmentDeleteTemp(t *testing.T) {
+ session := loginUser(t, "user2")
+ countTmpFile := func() int {
+ // TODO: GOLANG-HTTP-TMPDIR: Golang saves the uploaded file to os.TempDir() when it exceeds the max memory limit.
+ files, err := fs.Glob(os.DirFS(os.TempDir()), "multipart-*") //nolint:usetesting // Golang's "http" package's behavior
+ require.NoError(t, err)
+ return len(files)
+ }
+ var tmpFileCountDuringUpload int
+ defer test.MockVariableValue(&context.ParseMultipartFormMaxMemory, 1)()
+ defer web.RouteMock(route_web.RouterMockPointBeforeWebRoutes, func(resp http.ResponseWriter, req *http.Request) {
+ tmpFileCountDuringUpload = countTmpFile()
+ })()
+ _ = testCreateIssueAttachment(t, session, GetUserCSRFToken(t, session), "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusOK)
+ assert.Equal(t, 1, tmpFileCountDuringUpload, "the temp file should exist when uploaded size exceeds the parse form's max memory")
+ assert.Equal(t, 0, countTmpFile(), "the temp file should be deleted after upload")
+}
+
+func testCreateAnonymousAttachment(t *testing.T) {
session := emptyTestSession(t)
testCreateIssueAttachment(t, session, GetAnonymousCSRFToken(t, session), "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusSeeOther)
}
-func TestCreateIssueAttachment(t *testing.T) {
- defer tests.PrepareTestEnv(t)()
+func testCreateUser2IssueAttachment(t *testing.T) {
const repoURL = "user2/repo1"
session := loginUser(t, "user2")
uuid := testCreateIssueAttachment(t, session, GetUserCSRFToken(t, session), repoURL, "image.png", testGeneratePngBytes(), http.StatusOK)
@@ -90,8 +120,7 @@ func TestCreateIssueAttachment(t *testing.T) {
MakeRequest(t, req, http.StatusOK)
}
-func TestGetAttachment(t *testing.T) {
- defer tests.PrepareTestEnv(t)()
+func testGetAttachment(t *testing.T) {
adminSession := loginUser(t, "user1")
user2Session := loginUser(t, "user2")
user8Session := loginUser(t, "user8")
diff --git a/tests/integration/git_ssh_redirect_test.go b/tests/integration/git_ssh_redirect_test.go
index 5e35ed2a744..3ae2652412c 100644
--- a/tests/integration/git_ssh_redirect_test.go
+++ b/tests/integration/git_ssh_redirect_test.go
@@ -6,9 +6,13 @@ package integration
import (
"fmt"
"net/url"
+ "os"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/modules/structs"
+
+ "github.com/stretchr/testify/assert"
)
func TestGitSSHRedirect(t *testing.T) {
@@ -16,7 +20,8 @@ func TestGitSSHRedirect(t *testing.T) {
}
func testGitSSHRedirect(t *testing.T, u *url.URL) {
- apiTestContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
+ apiTestContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteOrganization)
+ session := loginUser(t, "user2")
withKeyFile(t, "my-testing-key", func(keyFile string) {
t.Run("CreateUserKey", doAPICreateUserKey(apiTestContext, "test-key", keyFile))
@@ -38,5 +43,39 @@ func testGitSSHRedirect(t *testing.T, u *url.URL) {
t.Run("Clone", doGitClone(t.TempDir(), cloneURL))
})
}
+
+ doAPICreateOrganization(apiTestContext, &structs.CreateOrgOption{
+ UserName: "olduser2",
+ FullName: "Old User2",
+ })(t)
+
+ cloneURL := createSSHUrl("olduser2/repo1.git", u)
+ t.Run("Clone Should Fail", doGitCloneFail(cloneURL))
+
+ doAPICreateOrganizationRepository(apiTestContext, "olduser2", &structs.CreateRepoOption{
+ Name: "repo1",
+ AutoInit: true,
+ })(t)
+ testEditFile(t, session, "olduser2", "repo1", "master", "README.md", "This is olduser2's repo1\n")
+
+ dstDir := t.TempDir()
+ t.Run("Clone", doGitClone(dstDir, cloneURL))
+ readMEContent, err := os.ReadFile(dstDir + "/README.md")
+ assert.NoError(t, err)
+ assert.Equal(t, "This is olduser2's repo1\n", string(readMEContent))
+
+ apiTestContext2 := NewAPITestContext(t, "user2", "oldrepo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteOrganization)
+ doAPICreateRepository(apiTestContext2, false)(t)
+ testEditFile(t, session, "user2", "oldrepo1", "master", "README.md", "This is user2's oldrepo1\n")
+
+ dstDir = t.TempDir()
+ cloneURL = createSSHUrl("user2/oldrepo1.git", u)
+ t.Run("Clone", doGitClone(dstDir, cloneURL))
+ readMEContent, err = os.ReadFile(dstDir + "/README.md")
+ assert.NoError(t, err)
+ assert.Equal(t, "This is user2's oldrepo1\n", string(readMEContent))
+
+ cloneURL = createSSHUrl("olduser2/oldrepo1.git", u)
+ t.Run("Clone Should Fail", doGitCloneFail(cloneURL))
})
}
diff --git a/tests/integration/repofiles_change_test.go b/tests/integration/repofiles_change_test.go
index 6821f8bf611..6fd42401c52 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/tools/lint-go-gopls.sh b/tools/lint-go-gopls.sh
deleted file mode 100755
index 2cd26ca6fe4..00000000000
--- 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
diff --git a/tsconfig.json b/tsconfig.json
index 1daf4b7233b..2466faf592c 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/css/editor/fileeditor.css b/web_src/css/editor/fileeditor.css
index 698efffc992..12ae97a1094 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 779339c46b3..aedf53569a9 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/modules/svg.css b/web_src/css/modules/svg.css
index 738ec22cd34..e32fa0911f1 100644
--- a/web_src/css/modules/svg.css
+++ b/web_src/css/modules/svg.css
@@ -1,17 +1,24 @@
-.svg {
+/* some material icons have "fill=none" (e.g.: ".txt -> document"), so the CSS styles shouldn't overwrite it,
+ and material icons should have no "fill" set explicitly, otherwise some like ".editorconfig" won't render correctly */
+.svg:not(.git-entry-icon) {
display: inline-block;
vertical-align: text-top;
fill: currentcolor;
}
-.svg.git-entry-icon {
- fill: transparent; /* some material icons have dark background fill, so need to reset */
-}
-
.middle .svg {
vertical-align: middle;
}
+/* some browsers like Chrome have a bug: when a SVG is in a "display: none" container and referenced
+ somewhere else by `