From e5f3c16587483daa18fa1db8bbc111452d9b864a Mon Sep 17 00:00:00 2001 From: Chai-Shi Date: Fri, 10 Jan 2025 13:29:55 +0800 Subject: [PATCH 001/117] Fix sync fork for consistency (#33147) Fixes #33145 An integration test could be added. --------- Co-authored-by: wxiaoguang --- modules/structs/repo_branch.go | 8 ++ routers/api/v1/api.go | 1 + routers/api/v1/repo/branch.go | 45 +++++++ routers/api/v1/swagger/repo.go | 12 ++ services/repository/merge_upstream.go | 49 +++++-- .../repo/code/upstream_diverging_info.tmpl | 6 +- templates/swagger/v1_json.tmpl | 78 +++++++++++ tests/integration/repo_merge_upstream_test.go | 122 ++++++++++++++++++ 8 files changed, 308 insertions(+), 13 deletions(-) create mode 100644 tests/integration/repo_merge_upstream_test.go diff --git a/modules/structs/repo_branch.go b/modules/structs/repo_branch.go index a9aa1d330a..55c98d60b9 100644 --- a/modules/structs/repo_branch.go +++ b/modules/structs/repo_branch.go @@ -133,3 +133,11 @@ type EditBranchProtectionOption struct { type UpdateBranchProtectionPriories struct { IDs []int64 `json:"ids"` } + +type MergeUpstreamRequest struct { + Branch string `json:"branch"` +} + +type MergeUpstreamResponse struct { + MergeStyle string `json:"merge_type"` +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 2f943d306c..b1a42a85e6 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1190,6 +1190,7 @@ func Routes() *web.Router { m.Get("/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive) m.Combo("/forks").Get(repo.ListForks). Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork) + m.Post("/merge-upstream", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeCode), bind(api.MergeUpstreamRequest{}), repo.MergeUpstream) m.Group("/branches", func() { m.Get("", repo.ListBranches) m.Get("/*", repo.GetBranch) diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index aa0ab70ce8..592879235c 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/optional" repo_module "code.gitea.io/gitea/modules/repository" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" @@ -1186,3 +1187,47 @@ func UpdateBranchProtectionPriories(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } + +func MergeUpstream(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/merge-upstream repository repoMergeUpstream + // --- + // summary: Merge a branch from upstream + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/MergeUpstreamRequest" + // responses: + // "200": + // "$ref": "#/responses/MergeUpstreamResponse" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + form := web.GetForm(ctx).(*api.MergeUpstreamRequest) + mergeStyle, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, form.Branch) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "MergeUpstream", err) + return + } else if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "MergeUpstream", err) + return + } + ctx.Error(http.StatusInternalServerError, "MergeUpstream", err) + return + } + ctx.JSON(http.StatusOK, &api.MergeUpstreamResponse{MergeStyle: mergeStyle}) +} diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index b9d2a0217c..f754c80a5b 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -448,3 +448,15 @@ type swaggerCompare struct { // in:body Body api.Compare `json:"body"` } + +// swagger:response MergeUpstreamRequest +type swaggerMergeUpstreamRequest struct { + // in:body + Body api.MergeUpstreamRequest `json:"body"` +} + +// swagger:response MergeUpstreamResponse +type swaggerMergeUpstreamResponse struct { + // in:body + Body api.MergeUpstreamResponse `json:"body"` +} diff --git a/services/repository/merge_upstream.go b/services/repository/merge_upstream.go index 85ca8f7e31..ef161889c0 100644 --- a/services/repository/merge_upstream.go +++ b/services/repository/merge_upstream.go @@ -12,17 +12,20 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/pull" ) type UpstreamDivergingInfo struct { - BaseIsNewer bool - CommitsBehind int - CommitsAhead int + BaseHasNewCommits bool + CommitsBehind int + CommitsAhead int } +// MergeUpstream merges the base repository's default branch into the fork repository's current branch. func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, branch string) (mergeStyle string, err error) { if err = repo.MustNotBeArchived(); err != nil { return "", err @@ -32,7 +35,7 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model. } err = git.Push(ctx, repo.BaseRepo.RepoPath(), git.PushOptions{ Remote: repo.RepoPath(), - Branch: fmt.Sprintf("%s:%s", branch, branch), + Branch: fmt.Sprintf("%s:%s", repo.BaseRepo.DefaultBranch, branch), Env: repo_module.PushingEnvironment(doer, repo), }) if err == nil { @@ -64,7 +67,7 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model. BaseRepoID: repo.BaseRepo.ID, BaseRepo: repo.BaseRepo, HeadBranch: branch, // maybe HeadCommitID is not needed - BaseBranch: branch, + BaseBranch: repo.BaseRepo.DefaultBranch, } fakeIssue.PullRequest = fakePR err = pull.Update(ctx, fakePR, doer, "merge upstream", false) @@ -74,7 +77,8 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model. return "merge", nil } -func GetUpstreamDivergingInfo(ctx context.Context, repo *repo_model.Repository, branch string) (*UpstreamDivergingInfo, error) { +// GetUpstreamDivergingInfo returns the information about the divergence between the fork repository's branch and the base repository's default branch. +func GetUpstreamDivergingInfo(ctx reqctx.RequestContext, repo *repo_model.Repository, branch string) (*UpstreamDivergingInfo, error) { if !repo.IsFork { return nil, util.NewInvalidArgumentErrorf("repo is not a fork") } @@ -92,7 +96,7 @@ func GetUpstreamDivergingInfo(ctx context.Context, repo *repo_model.Repository, return nil, err } - baseBranch, err := git_model.GetBranch(ctx, repo.BaseRepo.ID, branch) + baseBranch, err := git_model.GetBranch(ctx, repo.BaseRepo.ID, repo.BaseRepo.DefaultBranch) if err != nil { return nil, err } @@ -102,14 +106,39 @@ func GetUpstreamDivergingInfo(ctx context.Context, repo *repo_model.Repository, return info, nil } - // TODO: if the fork repo has new commits, this call will fail: + // if the fork repo has new commits, this call will fail because they are not in the base repo // exit status 128 - fatal: Invalid symmetric difference expression aaaaaaaaaaaa...bbbbbbbbbbbb - // so at the moment, we are not able to handle this case, should be improved in the future + // so at the moment, we first check the update time, then check whether the fork branch has base's head diff, err := git.GetDivergingCommits(ctx, repo.BaseRepo.RepoPath(), baseBranch.CommitID, forkBranch.CommitID) if err != nil { - info.BaseIsNewer = baseBranch.UpdatedUnix > forkBranch.UpdatedUnix + info.BaseHasNewCommits = baseBranch.UpdatedUnix > forkBranch.UpdatedUnix + if info.BaseHasNewCommits { + return info, nil + } + + // if the base's update time is before the fork, check whether the base's head is in the fork + baseGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo.BaseRepo) + if err != nil { + return nil, err + } + headGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo) + if err != nil { + return nil, err + } + + baseCommitID, err := baseGitRepo.ConvertToGitID(baseBranch.CommitID) + if err != nil { + return nil, err + } + headCommit, err := headGitRepo.GetCommit(forkBranch.CommitID) + if err != nil { + return nil, err + } + hasPreviousCommit, _ := headCommit.HasPreviousCommit(baseCommitID) + info.BaseHasNewCommits = !hasPreviousCommit return info, nil } + info.CommitsBehind, info.CommitsAhead = diff.Behind, diff.Ahead return info, nil } diff --git a/templates/repo/code/upstream_diverging_info.tmpl b/templates/repo/code/upstream_diverging_info.tmpl index 51402598f9..bdcd99a7f7 100644 --- a/templates/repo/code/upstream_diverging_info.tmpl +++ b/templates/repo/code/upstream_diverging_info.tmpl @@ -1,8 +1,8 @@ -{{if and .UpstreamDivergingInfo (or .UpstreamDivergingInfo.BaseIsNewer .UpstreamDivergingInfo.CommitsBehind)}} +{{if and .UpstreamDivergingInfo (or .UpstreamDivergingInfo.BaseHasNewCommits .UpstreamDivergingInfo.CommitsBehind)}}
- {{$upstreamLink := printf "%s/src/branch/%s" .Repository.BaseRepo.Link (.BranchName|PathEscapeSegments)}} - {{$upstreamHtml := HTMLFormat `%s:%s` $upstreamLink .Repository.BaseRepo.FullName .BranchName}} + {{$upstreamLink := printf "%s/src/branch/%s" .Repository.BaseRepo.Link (.Repository.BaseRepo.DefaultBranch|PathEscapeSegments)}} + {{$upstreamHtml := HTMLFormat `%s:%s` $upstreamLink .Repository.BaseRepo.FullName .Repository.BaseRepo.DefaultBranch}} {{if .UpstreamDivergingInfo.CommitsBehind}} {{ctx.Locale.TrN .UpstreamDivergingInfo.CommitsBehind "repo.pulls.upstream_diverging_prompt_behind_1" "repo.pulls.upstream_diverging_prompt_behind_n" .UpstreamDivergingInfo.CommitsBehind $upstreamHtml}} {{else}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 82a301da2f..fb37d45ce8 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -10867,6 +10867,52 @@ } } }, + "/repos/{owner}/{repo}/merge-upstream": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Merge a branch from upstream", + "operationId": "repoMergeUpstream", + "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 + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/MergeUpstreamRequest" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/MergeUpstreamResponse" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/milestones": { "get": { "produces": [ @@ -22827,6 +22873,26 @@ "x-go-name": "MergePullRequestForm", "x-go-package": "code.gitea.io/gitea/services/forms" }, + "MergeUpstreamRequest": { + "type": "object", + "properties": { + "branch": { + "type": "string", + "x-go-name": "Branch" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "MergeUpstreamResponse": { + "type": "object", + "properties": { + "merge_type": { + "type": "string", + "x-go-name": "MergeStyle" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "MigrateRepoOptions": { "description": "MigrateRepoOptions options for migrating repository's\nthis is used to interact with api v1", "type": "object", @@ -26008,6 +26074,18 @@ "type": "string" } }, + "MergeUpstreamRequest": { + "description": "", + "schema": { + "$ref": "#/definitions/MergeUpstreamRequest" + } + }, + "MergeUpstreamResponse": { + "description": "", + "schema": { + "$ref": "#/definitions/MergeUpstreamResponse" + } + }, "Milestone": { "description": "Milestone", "schema": { diff --git a/tests/integration/repo_merge_upstream_test.go b/tests/integration/repo_merge_upstream_test.go new file mode 100644 index 0000000000..e3e423c51d --- /dev/null +++ b/tests/integration/repo_merge_upstream_test.go @@ -0,0 +1,122 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "net/url" + "strings" + "testing" + "time" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRepoMergeUpstream(t *testing.T) { + onGiteaRun(t, func(*testing.T, *url.URL) { + forkUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + + baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + baseUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: baseRepo.OwnerID}) + + checkFileContent := func(branch, exp string) { + req := NewRequest(t, "GET", fmt.Sprintf("/%s/test-repo-fork/raw/branch/%s/new-file.txt", forkUser.Name, branch)) + resp := MakeRequest(t, req, http.StatusOK) + require.Equal(t, exp, resp.Body.String()) + } + + session := loginUser(t, forkUser.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + // create a fork + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseUser.Name, baseRepo.Name), &api.CreateForkOption{ + Name: util.ToPointer("test-repo-fork"), + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusAccepted) + forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: forkUser.ID, Name: "test-repo-fork"}) + + // create fork-branch + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/test-repo-fork/branches/_new/branch/master", forkUser.Name), map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "new_branch_name": "fork-branch", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + queryMergeUpstreamButtonLink := func(htmlDoc *HTMLDoc) string { + return htmlDoc.Find(`button[data-url*="merge-upstream"]`).AttrOr("data-url", "") + } + + t.Run("HeadBeforeBase", func(t *testing.T) { + // add a file in base repo + require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "new-file.txt", "master", "test-content-1")) + + // the repo shows a prompt to "sync fork" + var mergeUpstreamLink string + require.Eventually(t, func() bool { + resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + mergeUpstreamLink = queryMergeUpstreamButtonLink(htmlDoc) + if mergeUpstreamLink == "" { + return false + } + respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html() + return strings.Contains(respMsg, `This branch is 1 commit behind user2/repo1:master`) + }, 5*time.Second, 100*time.Millisecond) + + // click the "sync fork" button + req = NewRequestWithValues(t, "POST", mergeUpstreamLink, map[string]string{"_csrf": GetUserCSRFToken(t, session)}) + session.MakeRequest(t, req, http.StatusOK) + checkFileContent("fork-branch", "test-content-1") + }) + + t.Run("BaseChangeAfterHeadChange", func(t *testing.T) { + // update the files: base first, head later, and check the prompt + require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "new-file.txt", "master", "test-content-2")) + require.NoError(t, createOrReplaceFileInBranch(forkUser, forkRepo, "new-file-other.txt", "fork-branch", "test-content-other")) + + // make sure the base branch's update time is before the fork, to make it test the complete logic + baseBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: baseRepo.ID, Name: "master"}) + forkBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: forkRepo.ID, Name: "fork-branch"}) + _, err := db.GetEngine(db.DefaultContext).ID(forkBranch.ID).Update(&git_model.Branch{UpdatedUnix: baseBranch.UpdatedUnix + 1}) + require.NoError(t, err) + + // the repo shows a prompt to "sync fork" + require.Eventually(t, func() bool { + resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html() + return strings.Contains(respMsg, `The base branch user2/repo1:master has new changes`) + }, 5*time.Second, 100*time.Millisecond) + + // and do the merge-upstream by API + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/test-repo-fork/merge-upstream", forkUser.Name), &api.MergeUpstreamRequest{ + Branch: "fork-branch", + }).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + checkFileContent("fork-branch", "test-content-2") + + var mergeResp api.MergeUpstreamResponse + DecodeJSON(t, resp, &mergeResp) + assert.Equal(t, "merge", mergeResp.MergeStyle) + + // after merge, there should be no "sync fork" button anymore + require.Eventually(t, func() bool { + resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + return queryMergeUpstreamButtonLink(htmlDoc) == "" + }, 5*time.Second, 100*time.Millisecond) + }) + }) +} From d3083d21981f9445cf7570956a1fdedfc8578b56 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 9 Jan 2025 22:00:06 -0800 Subject: [PATCH 002/117] Some small refactors (#33144) --- models/issues/comment_list.go | 22 ++++++++++++---- models/issues/issue.go | 3 +++ models/issues/issue_list.go | 38 ++------------------------- models/user/user_list.go | 47 ++++++++++++++++++++++++++++++++++ routers/web/repo/issue_view.go | 30 +++++++++++++++++----- 5 files changed, 92 insertions(+), 48 deletions(-) create mode 100644 models/user/user_list.go diff --git a/models/issues/comment_list.go b/models/issues/comment_list.go index 61ac1c8f56..c483ada75a 100644 --- a/models/issues/comment_list.go +++ b/models/issues/comment_list.go @@ -26,14 +26,14 @@ func (comments CommentList) LoadPosters(ctx context.Context) error { return c.PosterID, c.Poster == nil && c.PosterID > 0 }) - posterMaps, err := getPostersByIDs(ctx, posterIDs) + posterMaps, err := user_model.GetUsersMapByIDs(ctx, posterIDs) if err != nil { return err } for _, comment := range comments { if comment.Poster == nil { - comment.Poster = getPoster(comment.PosterID, posterMaps) + comment.Poster = user_model.GetPossibleUserFromMap(comment.PosterID, posterMaps) } } return nil @@ -41,7 +41,7 @@ func (comments CommentList) LoadPosters(ctx context.Context) error { func (comments CommentList) getLabelIDs() []int64 { return container.FilterSlice(comments, func(comment *Comment) (int64, bool) { - return comment.LabelID, comment.LabelID > 0 + return comment.LabelID, comment.LabelID > 0 && comment.Label == nil }) } @@ -51,6 +51,9 @@ func (comments CommentList) loadLabels(ctx context.Context) error { } labelIDs := comments.getLabelIDs() + if len(labelIDs) == 0 { + return nil + } commentLabels := make(map[int64]*Label, len(labelIDs)) left := len(labelIDs) for left > 0 { @@ -118,8 +121,8 @@ func (comments CommentList) loadMilestones(ctx context.Context) error { milestoneIDs = milestoneIDs[limit:] } - for _, issue := range comments { - issue.Milestone = milestoneMaps[issue.MilestoneID] + for _, comment := range comments { + comment.Milestone = milestoneMaps[comment.MilestoneID] } return nil } @@ -175,6 +178,9 @@ func (comments CommentList) loadAssignees(ctx context.Context) error { } assigneeIDs := comments.getAssigneeIDs() + if len(assigneeIDs) == 0 { + return nil + } assignees := make(map[int64]*user_model.User, len(assigneeIDs)) left := len(assigneeIDs) for left > 0 { @@ -301,6 +307,9 @@ func (comments CommentList) loadDependentIssues(ctx context.Context) error { e := db.GetEngine(ctx) issueIDs := comments.getDependentIssueIDs() + if len(issueIDs) == 0 { + return nil + } issues := make(map[int64]*Issue, len(issueIDs)) left := len(issueIDs) for left > 0 { @@ -427,6 +436,9 @@ func (comments CommentList) loadReviews(ctx context.Context) error { } reviewIDs := comments.getReviewIDs() + if len(reviewIDs) == 0 { + return nil + } reviews := make(map[int64]*Review, len(reviewIDs)) if err := db.GetEngine(ctx).In("id", reviewIDs).Find(&reviews); err != nil { return err diff --git a/models/issues/issue.go b/models/issues/issue.go index 1777fbb6a6..564a9fb835 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -238,6 +238,9 @@ func (issue *Issue) loadCommentsByType(ctx context.Context, tp CommentType) (err IssueID: issue.ID, Type: tp, }) + for _, comment := range issue.Comments { + comment.Issue = issue + } return err } diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go index 22a4548adc..02fd330f0a 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -81,53 +81,19 @@ func (issues IssueList) LoadPosters(ctx context.Context) error { return issue.PosterID, issue.Poster == nil && issue.PosterID > 0 }) - posterMaps, err := getPostersByIDs(ctx, posterIDs) + posterMaps, err := user_model.GetUsersMapByIDs(ctx, posterIDs) if err != nil { return err } for _, issue := range issues { if issue.Poster == nil { - issue.Poster = getPoster(issue.PosterID, posterMaps) + issue.Poster = user_model.GetPossibleUserFromMap(issue.PosterID, posterMaps) } } return nil } -func getPostersByIDs(ctx context.Context, posterIDs []int64) (map[int64]*user_model.User, error) { - posterMaps := make(map[int64]*user_model.User, len(posterIDs)) - left := len(posterIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - err := db.GetEngine(ctx). - In("id", posterIDs[:limit]). - Find(&posterMaps) - if err != nil { - return nil, err - } - left -= limit - posterIDs = posterIDs[limit:] - } - return posterMaps, nil -} - -func getPoster(posterID int64, posterMaps map[int64]*user_model.User) *user_model.User { - if posterID == user_model.ActionsUserID { - return user_model.NewActionsUser() - } - if posterID <= 0 { - return nil - } - poster, ok := posterMaps[posterID] - if !ok { - return user_model.NewGhostUser() - } - return poster -} - func (issues IssueList) getIssueIDs() []int64 { ids := make([]int64, 0, len(issues)) for _, issue := range issues { diff --git a/models/user/user_list.go b/models/user/user_list.go new file mode 100644 index 0000000000..c66d59f0d9 --- /dev/null +++ b/models/user/user_list.go @@ -0,0 +1,47 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "context" + + "code.gitea.io/gitea/models/db" +) + +func GetUsersMapByIDs(ctx context.Context, userIDs []int64) (map[int64]*User, error) { + userMaps := make(map[int64]*User, len(userIDs)) + left := len(userIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + err := db.GetEngine(ctx). + In("id", userIDs[:limit]). + Find(&userMaps) + if err != nil { + return nil, err + } + left -= limit + userIDs = userIDs[limit:] + } + return userMaps, nil +} + +func GetPossibleUserFromMap(userID int64, usererMaps map[int64]*User) *User { + switch userID { + case GhostUserID: + return NewGhostUser() + case ActionsUserID: + return NewActionsUser() + case 0: + return nil + default: + user, ok := usererMaps[userID] + if !ok { + return NewGhostUser() + } + return user + } +} diff --git a/routers/web/repo/issue_view.go b/routers/web/repo/issue_view.go index 61e75e211b..aa49d2e1e8 100644 --- a/routers/web/repo/issue_view.go +++ b/routers/web/repo/issue_view.go @@ -40,16 +40,30 @@ import ( ) // roleDescriptor returns the role descriptor for a comment in/with the given repo, poster and issue -func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *user_model.User, issue *issues_model.Issue, hasOriginalAuthor bool) (issues_model.RoleDescriptor, error) { +func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *user_model.User, permsCache map[int64]access_model.Permission, issue *issues_model.Issue, hasOriginalAuthor bool) (issues_model.RoleDescriptor, error) { roleDescriptor := issues_model.RoleDescriptor{} if hasOriginalAuthor { return roleDescriptor, nil } - perm, err := access_model.GetUserRepoPermission(ctx, repo, poster) - if err != nil { - return roleDescriptor, err + var perm access_model.Permission + var err error + if permsCache != nil { + var ok bool + perm, ok = permsCache[poster.ID] + if !ok { + perm, err = access_model.GetUserRepoPermission(ctx, repo, poster) + if err != nil { + return roleDescriptor, err + } + } + permsCache[poster.ID] = perm + } else { + perm, err = access_model.GetUserRepoPermission(ctx, repo, poster) + if err != nil { + return roleDescriptor, err + } } // If the poster is the actual poster of the issue, enable Poster role. @@ -576,6 +590,8 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue return } + permCache := make(map[int64]access_model.Permission) + for _, comment = range issue.Comments { comment.Issue = issue @@ -593,7 +609,7 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue continue } - comment.ShowRole, err = roleDescriptor(ctx, issue.Repo, comment.Poster, issue, comment.HasOriginalAuthor()) + comment.ShowRole, err = roleDescriptor(ctx, issue.Repo, comment.Poster, permCache, issue, comment.HasOriginalAuthor()) if err != nil { ctx.ServerError("roleDescriptor", err) return @@ -691,7 +707,7 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue continue } - c.ShowRole, err = roleDescriptor(ctx, issue.Repo, c.Poster, issue, c.HasOriginalAuthor()) + c.ShowRole, err = roleDescriptor(ctx, issue.Repo, c.Poster, permCache, issue, c.HasOriginalAuthor()) if err != nil { ctx.ServerError("roleDescriptor", err) return @@ -940,7 +956,7 @@ func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) { ctx.ServerError("RenderString", err) return } - if issue.ShowRole, err = roleDescriptor(ctx, issue.Repo, issue.Poster, issue, issue.HasOriginalAuthor()); err != nil { + if issue.ShowRole, err = roleDescriptor(ctx, issue.Repo, issue.Poster, nil, issue, issue.HasOriginalAuthor()); err != nil { ctx.ServerError("roleDescriptor", err) return } From d7ec23febf7c97de9d626797997a5a91ad5c3da8 Mon Sep 17 00:00:00 2001 From: Harry Vince <47283812+harryvince@users.noreply.github.com> Date: Fri, 10 Jan 2025 07:28:38 +0000 Subject: [PATCH 003/117] Fix editor markdown not incrementing in a numbered list (#33187) Amended the logic for newPrefix in the MarkdownEditor to resolve incorrect number ordering. Fixes #33184 Attached screenshot of fixed input similar to issue Screenshot 2025-01-09 at 23 59 24 --------- Co-authored-by: wxiaoguang --- .../js/features/comp/EditorMarkdown.test.ts | 170 +++++++++++++++++- web_src/js/features/comp/EditorMarkdown.ts | 136 +++++++++++--- 2 files changed, 273 insertions(+), 33 deletions(-) diff --git a/web_src/js/features/comp/EditorMarkdown.test.ts b/web_src/js/features/comp/EditorMarkdown.test.ts index 7b4b44e83c..9f34d77348 100644 --- a/web_src/js/features/comp/EditorMarkdown.test.ts +++ b/web_src/js/features/comp/EditorMarkdown.test.ts @@ -1,4 +1,166 @@ -import {initTextareaMarkdown} from './EditorMarkdown.ts'; +import {initTextareaMarkdown, markdownHandleIndention, textareaSplitLines} from './EditorMarkdown.ts'; + +test('textareaSplitLines', () => { + let ret = textareaSplitLines('a\nbc\nd', 0); + expect(ret).toEqual({lines: ['a', 'bc', 'd'], lengthBeforePosLine: 0, posLineIndex: 0, inlinePos: 0}); + + ret = textareaSplitLines('a\nbc\nd', 1); + expect(ret).toEqual({lines: ['a', 'bc', 'd'], lengthBeforePosLine: 0, posLineIndex: 0, inlinePos: 1}); + + ret = textareaSplitLines('a\nbc\nd', 2); + expect(ret).toEqual({lines: ['a', 'bc', 'd'], lengthBeforePosLine: 2, posLineIndex: 1, inlinePos: 0}); + + ret = textareaSplitLines('a\nbc\nd', 3); + expect(ret).toEqual({lines: ['a', 'bc', 'd'], lengthBeforePosLine: 2, posLineIndex: 1, inlinePos: 1}); + + ret = textareaSplitLines('a\nbc\nd', 4); + expect(ret).toEqual({lines: ['a', 'bc', 'd'], lengthBeforePosLine: 2, posLineIndex: 1, inlinePos: 2}); + + ret = textareaSplitLines('a\nbc\nd', 5); + expect(ret).toEqual({lines: ['a', 'bc', 'd'], lengthBeforePosLine: 5, posLineIndex: 2, inlinePos: 0}); + + ret = textareaSplitLines('a\nbc\nd', 6); + expect(ret).toEqual({lines: ['a', 'bc', 'd'], lengthBeforePosLine: 5, posLineIndex: 2, inlinePos: 1}); +}); + +test('markdownHandleIndention', () => { + const testInput = (input: string, expected?: string) => { + const inputPos = input.indexOf('|'); + input = input.replace('|', ''); + const ret = markdownHandleIndention({value: input, selStart: inputPos, selEnd: inputPos}); + if (expected === null) { + expect(ret).toEqual({handled: false}); + } else { + const expectedPos = expected.indexOf('|'); + expected = expected.replace('|', ''); + expect(ret).toEqual({ + handled: true, + valueSelection: {value: expected, selStart: expectedPos, selEnd: expectedPos}, + }); + } + }; + + testInput(` + a|b +`, ` + a + |b +`); + + testInput(` +1. a +2. | +`, ` +1. a +| +`); + + testInput(` +|1. a +`, null); // let browser handle it + + testInput(` +1. a +1. b|c +`, ` +1. a +2. b +3. |c +`); + + testInput(` +2. a +2. b| + +1. x +1. y +`, ` +1. a +2. b +3. | + +1. x +1. y +`); + + testInput(` +2. a +2. b + +1. x| +1. y +`, ` +2. a +2. b + +1. x +2. | +3. y +`); + + testInput(` +1. a +2. b| +3. c +`, ` +1. a +2. b +3. | +4. c +`); + + testInput(` +1. a + 1. b + 2. b + 3. b + 4. b +1. c| +`, ` +1. a + 1. b + 2. b + 3. b + 4. b +2. c +3. | +`); + + testInput(` +1. a +2. a +3. a +4. a +5. a +6. a +7. a +8. a +9. b|c +`, ` +1. a +2. a +3. a +4. a +5. a +6. a +7. a +8. a +9. b +10. |c +`); + + // this is a special case, it's difficult to re-format the parent level at the moment, so leave it to the future + testInput(` +1. a + 2. b| +3. c +`, ` +1. a + 1. b + 2. | +3. c +`); +}); test('EditorMarkdown', () => { const textarea = document.createElement('textarea'); @@ -32,10 +194,10 @@ test('EditorMarkdown', () => { testInput({value: '1. \n2. ', pos: 3}, {value: '\n2. ', pos: 0}); testInput('- x', '- x\n- '); - testInput('1. foo', '1. foo\n1. '); - testInput({value: '1. a\n2. b\n3. c', pos: 4}, {value: '1. a\n1. \n2. b\n3. c', pos: 8}); + testInput('1. foo', '1. foo\n2. '); + testInput({value: '1. a\n2. b\n3. c', pos: 4}, {value: '1. a\n2. \n3. b\n4. c', pos: 8}); testInput('- [ ]', '- [ ]\n- '); testInput('- [ ] foo', '- [ ] foo\n- [ ] '); testInput('* [x] foo', '* [x] foo\n* [ ] '); - testInput('1. [x] foo', '1. [x] foo\n1. [ ] '); + testInput('1. [x] foo', '1. [x] foo\n2. [ ] '); }); diff --git a/web_src/js/features/comp/EditorMarkdown.ts b/web_src/js/features/comp/EditorMarkdown.ts index 5e2ef121f5..d3ed492396 100644 --- a/web_src/js/features/comp/EditorMarkdown.ts +++ b/web_src/js/features/comp/EditorMarkdown.ts @@ -14,7 +14,13 @@ export function textareaInsertText(textarea, value) { triggerEditorContentChanged(textarea); } -function handleIndentSelection(textarea, e) { +type TextareaValueSelection = { + value: string; + selStart: number; + selEnd: number; +} + +function handleIndentSelection(textarea: HTMLTextAreaElement, e) { const selStart = textarea.selectionStart; const selEnd = textarea.selectionEnd; if (selEnd === selStart) return; // do not process when no selection @@ -56,53 +62,125 @@ function handleIndentSelection(textarea, e) { triggerEditorContentChanged(textarea); } -function handleNewline(textarea: HTMLTextAreaElement, e: Event) { - const selStart = textarea.selectionStart; - const selEnd = textarea.selectionEnd; - if (selEnd !== selStart) return; // do not process when there is a selection +type MarkdownHandleIndentionResult = { + handled: boolean; + valueSelection?: TextareaValueSelection; +} - const value = textarea.value; +type TextLinesBuffer = { + lines: string[]; + lengthBeforePosLine: number; + posLineIndex: number; + inlinePos: number +} - // find the current line - // * if selStart is 0, lastIndexOf(..., -1) is the same as lastIndexOf(..., 0) - // * if lastIndexOf reruns -1, lineStart is 0 and it is still correct. - const lineStart = value.lastIndexOf('\n', selStart - 1) + 1; - let lineEnd = value.indexOf('\n', selStart); - lineEnd = lineEnd < 0 ? value.length : lineEnd; - let line = value.slice(lineStart, lineEnd); - if (!line) return; // if the line is empty, do nothing, let the browser handle it +export function textareaSplitLines(value: string, pos: number): TextLinesBuffer { + const lines = value.split('\n'); + let lengthBeforePosLine = 0, inlinePos = 0, posLineIndex = 0; + for (; posLineIndex < lines.length; posLineIndex++) { + const lineLength = lines[posLineIndex].length + 1; + if (lengthBeforePosLine + lineLength > pos) { + inlinePos = pos - lengthBeforePosLine; + break; + } + lengthBeforePosLine += lineLength; + } + return {lines, lengthBeforePosLine, posLineIndex, inlinePos}; +} + +function markdownReformatListNumbers(linesBuf: TextLinesBuffer, indention: string) { + const reDeeperIndention = new RegExp(`^${indention}\\s+`); + const reSameLevel = new RegExp(`^${indention}([0-9]+)\\.`); + let firstLineIdx: number; + for (firstLineIdx = linesBuf.posLineIndex - 1; firstLineIdx >= 0; firstLineIdx--) { + const line = linesBuf.lines[firstLineIdx]; + if (!reDeeperIndention.test(line) && !reSameLevel.test(line)) break; + } + firstLineIdx++; + let num = 1; + for (let i = firstLineIdx; i < linesBuf.lines.length; i++) { + const oldLine = linesBuf.lines[i]; + const sameLevel = reSameLevel.test(oldLine); + if (!sameLevel && !reDeeperIndention.test(oldLine)) break; + if (sameLevel) { + const newLine = `${indention}${num}.${oldLine.replace(reSameLevel, '')}`; + linesBuf.lines[i] = newLine; + num++; + if (linesBuf.posLineIndex === i) { + // need to correct the cursor inline position if the line length changes + linesBuf.inlinePos += newLine.length - oldLine.length; + linesBuf.inlinePos = Math.max(0, linesBuf.inlinePos); + linesBuf.inlinePos = Math.min(newLine.length, linesBuf.inlinePos); + } + } + } + recalculateLengthBeforeLine(linesBuf); +} + +function recalculateLengthBeforeLine(linesBuf: TextLinesBuffer) { + linesBuf.lengthBeforePosLine = 0; + for (let i = 0; i < linesBuf.posLineIndex; i++) { + linesBuf.lengthBeforePosLine += linesBuf.lines[i].length + 1; + } +} + +export function markdownHandleIndention(tvs: TextareaValueSelection): MarkdownHandleIndentionResult { + const unhandled: MarkdownHandleIndentionResult = {handled: false}; + if (tvs.selEnd !== tvs.selStart) return unhandled; // do not process when there is a selection + + const linesBuf = textareaSplitLines(tvs.value, tvs.selStart); + const line = linesBuf.lines[linesBuf.posLineIndex] ?? ''; + if (!line) return unhandled; // if the line is empty, do nothing, let the browser handle it // parse the indention - const indention = /^\s*/.exec(line)[0]; - line = line.slice(indention.length); + let lineContent = line; + const indention = /^\s*/.exec(lineContent)[0]; + lineContent = lineContent.slice(indention.length); + if (linesBuf.inlinePos <= indention.length) return unhandled; // if cursor is at the indention, do nothing, let the browser handle it // parse the prefixes: "1. ", "- ", "* ", there could also be " [ ] " or " [x] " for task lists // there must be a space after the prefix because none of "1.foo" / "-foo" is a list item - const prefixMatch = /^([0-9]+\.|[-*])(\s\[([ x])\])?\s/.exec(line); + const prefixMatch = /^([0-9]+\.|[-*])(\s\[([ x])\])?\s/.exec(lineContent); let prefix = ''; if (prefixMatch) { prefix = prefixMatch[0]; - if (lineStart + prefix.length > selStart) prefix = ''; // do not add new line if cursor is at prefix + if (prefix.length > linesBuf.inlinePos) prefix = ''; // do not add new line if cursor is at prefix } - line = line.slice(prefix.length); - if (!indention && !prefix) return; // if no indention and no prefix, do nothing, let the browser handle it + lineContent = lineContent.slice(prefix.length); + if (!indention && !prefix) return unhandled; // if no indention and no prefix, do nothing, let the browser handle it - e.preventDefault(); - if (!line) { + if (!lineContent) { // clear current line if we only have i.e. '1. ' and the user presses enter again to finish creating a list - textarea.value = value.slice(0, lineStart) + value.slice(lineEnd); - textarea.setSelectionRange(selStart - prefix.length, selStart - prefix.length); + linesBuf.lines[linesBuf.posLineIndex] = ''; + linesBuf.inlinePos = 0; } else { - // start a new line with the same indention and prefix + // start a new line with the same indention let newPrefix = prefix; - // a simple approach, otherwise it needs to parse the lines after the current line if (/^\d+\./.test(prefix)) newPrefix = `1. ${newPrefix.slice(newPrefix.indexOf('.') + 2)}`; newPrefix = newPrefix.replace('[x]', '[ ]'); - const newLine = `\n${indention}${newPrefix}`; - textarea.value = value.slice(0, selStart) + newLine + value.slice(selEnd); - textarea.setSelectionRange(selStart + newLine.length, selStart + newLine.length); + + const inlinePos = linesBuf.inlinePos; + linesBuf.lines[linesBuf.posLineIndex] = line.substring(0, inlinePos); + const newLineLeft = `${indention}${newPrefix}`; + const newLine = `${newLineLeft}${line.substring(inlinePos)}`; + linesBuf.lines.splice(linesBuf.posLineIndex + 1, 0, newLine); + linesBuf.posLineIndex++; + linesBuf.inlinePos = newLineLeft.length; + recalculateLengthBeforeLine(linesBuf); } + + markdownReformatListNumbers(linesBuf, indention); + const newPos = linesBuf.lengthBeforePosLine + linesBuf.inlinePos; + return {handled: true, valueSelection: {value: linesBuf.lines.join('\n'), selStart: newPos, selEnd: newPos}}; +} + +function handleNewline(textarea: HTMLTextAreaElement, e: Event) { + const ret = markdownHandleIndention({value: textarea.value, selStart: textarea.selectionStart, selEnd: textarea.selectionEnd}); + if (!ret.handled) return; + e.preventDefault(); + textarea.value = ret.valueSelection.value; + textarea.setSelectionRange(ret.valueSelection.selStart, ret.valueSelection.selEnd); triggerEditorContentChanged(textarea); } From 5c150ce9b0c9df32ebeebacdc31f44a32dc7fa15 Mon Sep 17 00:00:00 2001 From: yp05327 <576951401@qq.com> Date: Fri, 10 Jan 2025 17:03:07 +0900 Subject: [PATCH 004/117] Update README.md (#33149) ~~Waiting for the upload of screenshots~~ I have a good idea about the screenshots. I will do it later. --------- Co-authored-by: Gary Wang --- .github/ISSUE_TEMPLATE/config.yml | 2 +- CONTRIBUTING.md | 2 +- README.md | 42 +++++++++++++----- README_ZH.md | 73 ++++++++++++++++++++++++------- 4 files changed, 90 insertions(+), 29 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index d37ce219c3..d409f18cd9 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -13,5 +13,5 @@ contact_links: url: https://docs.gitea.com/help/faq about: Please check if your question isn't mentioned here. - name: Crowdin Translations - url: https://crowdin.com/project/gitea + url: https://translate.gitea.com about: Translations are managed here. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 60146276db..11c99d1e3a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -182,7 +182,7 @@ Here's how to run the test suite: ## Translation -All translation work happens on [Crowdin](https://crowdin.com/project/gitea). +All translation work happens on [Crowdin](https://translate.gitea.com). The only translation that is maintained in this repository is [the English translation](https://github.com/go-gitea/gitea/blob/main/options/locale/locale_en-US.ini). It is synced regularly with Crowdin. \ Other locales on main branch **should not** be updated manually as they will be overwritten with each sync. \ diff --git a/README.md b/README.md index c280c832ac..f747d993d7 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea") [![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT") [![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea) -[![](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea "Crowdin") +[![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin") [View this document in Chinese](./README_ZH.md) @@ -31,6 +31,14 @@ For accessing free Gitea service (with a limited number of repositories), you ca To quickly deploy your own dedicated Gitea instance on Gitea Cloud, you can start a free trial at [cloud.gitea.com](https://cloud.gitea.com). +## Documentation + +You can find comprehensive documentation on our official [documentation website](https://docs.gitea.com/). + +It includes installation, administration, usage, development, contributing guides, and more to help you get started and explore all features effectively. + +If you have any suggestions or would like to contribute to it, you can visit the [documentation repository](https://gitea.com/gitea/docs) + ## Building From the root of the source tree, run: @@ -52,6 +60,8 @@ More info: https://docs.gitea.com/installation/install-from-source ## Using +After building, a binary file named `gitea` will be generated in the root of the source tree by default. To run it, use: + ./gitea web > [!NOTE] @@ -68,22 +78,25 @@ Expected workflow is: Fork -> Patch -> Push -> Pull Request ## Translating -Translations are done through Crowdin. If you want to translate to a new language ask one of the managers in the Crowdin project to add a new language there. +[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com) + +Translations are done through [Crowdin](https://translate.gitea.com). If you want to translate to a new language ask one of the managers in the Crowdin project to add a new language there. You can also just create an issue for adding a language or ask on discord on the #translation channel. If you need context or find some translation issues, you can leave a comment on the string or ask on Discord. For general translation questions there is a section in the docs. Currently a bit empty but we hope to fill it as questions pop up. -https://docs.gitea.com/contributing/localization +Get more information from [documentation](https://docs.gitea.com/contributing/localization). -[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea) +## Official and Third-Party Projects -## Further information +We provide an official [go-sdk](https://gitea.com/gitea/go-sdk), a CLI tool called [tea](https://gitea.com/gitea/tea) and an [action runner](https://gitea.com/gitea/act_runner) for Gitea Action. -For more information and instructions about how to install Gitea, please look at our [documentation](https://docs.gitea.com/). -If you have questions that are not covered by the documentation, you can get in contact with us on our [Discord server](https://discord.gg/Gitea) or create a post in the [discourse forum](https://forum.gitea.com/). +We maintain a list of Gitea-related projects at [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea), where you can discover more third-party projects, including SDKs, plugins, themes, and more. -We maintain a list of Gitea-related projects at [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea). +## Communication -The official Gitea CLI is developed at [gitea/tea](https://gitea.com/gitea/tea). +[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea") + +If you have questions that are not covered by the [documentation](https://docs.gitea.com/), you can get in contact with us on our [Discord server](https://discord.gg/Gitea) or create a post in the [discourse forum](https://forum.gitea.com/). ## Authors @@ -122,18 +135,25 @@ Gitea is pronounced [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY) as in "gi-tea" We're [working on it](https://github.com/go-gitea/gitea/issues/1029). +**Where can I find the security patches?** + +In the [release log](https://github.com/go-gitea/gitea/releases) or the [change log](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md), search for the keyword `SECURITY` to find the security patches. + ## License This project is licensed under the MIT License. See the [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) file for the full license text. -## Screenshots +## Further information -Looking for an overview of the interface? Check it out! +
+Looking for an overview of the interface? Check it out! |![Dashboard](https://dl.gitea.com/screenshots/home_timeline.png)|![User Profile](https://dl.gitea.com/screenshots/user_profile.png)|![Global Issues](https://dl.gitea.com/screenshots/global_issues.png)| |:---:|:---:|:---:| |![Branches](https://dl.gitea.com/screenshots/branches.png)|![Web Editor](https://dl.gitea.com/screenshots/web_editor.png)|![Activity](https://dl.gitea.com/screenshots/activity.png)| |![New Migration](https://dl.gitea.com/screenshots/migration.png)|![Migrating](https://dl.gitea.com/screenshots/migration.gif)|![Pull Request View](https://image.ibb.co/e02dSb/6.png)| |![Pull Request Dark](https://dl.gitea.com/screenshots/pull_requests_dark.png)|![Diff Review Dark](https://dl.gitea.com/screenshots/review_dark.png)|![Diff Dark](https://dl.gitea.com/screenshots/diff_dark.png)| + +
diff --git a/README_ZH.md b/README_ZH.md index a2e36dc22f..2dd60fd564 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -9,13 +9,13 @@ [![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea") [![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT") [![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea) -[![](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea "Crowdin") +[![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin") [View this document in English](./README.md) ## 目标 -Gitea 的首要目标是创建一个极易安装,运行非常快速,安装和使用体验良好的自建 Git 服务。我们采用 Go 作为后端语言,这使我们只要生成一个可执行程序即可。并且他还支持跨平台,支持 Linux, macOS 和 Windows 以及各种架构,除了 x86,amd64,还包括 ARM 和 PowerPC。 +Gitea 的首要目标是创建一个极易安装,运行非常快速,安装和使用体验良好的自建 Git 服务。我们采用 Go 作为后端语言,这使我们只要生成一个可执行程序即可。并且他还支持跨平台,支持 Linux、macOS 和 Windows 以及各种架构,除了 x86 和 amd64,还包括 ARM 和 PowerPC。 如果你想试用在线演示和报告问题,请访问 [demo.gitea.com](https://demo.gitea.com/)。 @@ -23,39 +23,80 @@ Gitea 的首要目标是创建一个极易安装,运行非常快速,安装 如果你想在 Gitea Cloud 上快速部署你自己独享的 Gitea 实例,请访问 [cloud.gitea.com](https://cloud.gitea.com) 开始免费试用。 -## 提示 - -1. **开始贡献代码之前请确保你已经看过了 [贡献者向导(英文)](CONTRIBUTING.md)**. -2. 所有的安全问题,请私下发送邮件给 **security@gitea.io**。谢谢! -3. 如果你要使用API,请参见 [API 文档](https://godoc.org/code.gitea.io/sdk/gitea). - ## 文档 关于如何安装请访问我们的 [文档站](https://docs.gitea.com/zh-cn/category/installation),如果没有找到对应的文档,你也可以通过 [Discord - 英文](https://discord.gg/gitea) 和 QQ群 328432459 来和我们交流。 -## 贡献流程 +## 编译 -Fork -> Patch -> Push -> Pull Request +在源代码的根目录下执行: + + TAGS="bindata" make build + +或者如果需要SQLite支持: + + TAGS="bindata sqlite sqlite_unlock_notify" make build + +编译过程会分成2个子任务: + +- `make backend`,需要 [Go Stable](https://go.dev/dl/),最低版本需求可查看 [go.mod](/go.mod)。 +- `make frontend`,需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本。 + +你需要连接网络来下载 go 和 npm modules。当从 tar 格式的源文件编译时,其中包含了预编译的前端文件,因此 `make frontend` 将不会被执行。这允许编译时不需要 Node.js。 + +更多信息: https://docs.gitea.com/installation/install-from-source + +## 使用 + +编译之后,默认会在根目录下生成一个名为 `gitea` 的文件。你可以这样执行它: + + ./gitea web + +> [!注意] +> 如果你要使用API,请参见 [API 文档](https://godoc.org/code.gitea.io/sdk/gitea)。 + +## 贡献 + +贡献流程:Fork -> Patch -> Push -> Pull Request + +> [!注意] +> +> 1. **开始贡献代码之前请确保你已经看过了 [贡献者向导(英文)](CONTRIBUTING.md)**。 +> 2. 所有的安全问题,请私下发送邮件给 **security@gitea.io**。 谢谢! ## 翻译 -多语言翻译是基于Crowdin进行的. -[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea) +[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com) + +多语言翻译是基于Crowdin进行的。 + +从 [文档](https://docs.gitea.com/contributing/localization) 中获取更多信息。 + +## 官方和第三方项目 + +Gitea 提供官方的 [go-sdk](https://gitea.com/gitea/go-sdk),以及名为 [tea](https://gitea.com/gitea/tea) 的 CLI 工具 和 用于 Gitea Action 的 [action runner](https://gitea.com/gitea/act_runner)。 + +[gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea) 是一个 Gitea 相关项目的列表,你可以在这里找到更多的第三方项目,包括 SDK、插件、主题等等。 ## 作者 -* [Maintainers](https://github.com/orgs/go-gitea/people) -* [Contributors](https://github.com/go-gitea/gitea/graphs/contributors) -* [Translators](options/locale/TRANSLATORS) +- [Maintainers](https://github.com/orgs/go-gitea/people) +- [Contributors](https://github.com/go-gitea/gitea/graphs/contributors) +- [Translators](options/locale/TRANSLATORS) ## 授权许可 本项目采用 MIT 开源授权许可证,完整的授权说明已放置在 [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) 文件中。 -## 截图 +## 更多信息 + +
+截图 |![Dashboard](https://dl.gitea.com/screenshots/home_timeline.png)|![User Profile](https://dl.gitea.com/screenshots/user_profile.png)|![Global Issues](https://dl.gitea.com/screenshots/global_issues.png)| |:---:|:---:|:---:| |![Branches](https://dl.gitea.com/screenshots/branches.png)|![Web Editor](https://dl.gitea.com/screenshots/web_editor.png)|![Activity](https://dl.gitea.com/screenshots/activity.png)| |![New Migration](https://dl.gitea.com/screenshots/migration.png)|![Migrating](https://dl.gitea.com/screenshots/migration.gif)|![Pull Request View](https://image.ibb.co/e02dSb/6.png)| |![Pull Request Dark](https://dl.gitea.com/screenshots/pull_requests_dark.png)|![Diff Review Dark](https://dl.gitea.com/screenshots/review_dark.png)|![Diff Dark](https://dl.gitea.com/screenshots/diff_dark.png)| + +
From 8c6d7076b700a27c9c220f86a6e1e0e0b53b7fed Mon Sep 17 00:00:00 2001 From: TheFox0x7 Date: Sat, 11 Jan 2025 21:33:43 +0100 Subject: [PATCH 005/117] fix(cache): cache test triggered by non memory cache (#33220) Change SlowCacheThreshold to 30 milliseconds so it doesn't trigger on non memory cache Closes: https://github.com/go-gitea/gitea/issues/33190 Closes: https://github.com/go-gitea/gitea/issues/32657 --- modules/cache/cache.go | 9 +++++++-- modules/cache/cache_test.go | 3 ++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/modules/cache/cache.go b/modules/cache/cache.go index b5400b0bd6..f7828e3cae 100644 --- a/modules/cache/cache.go +++ b/modules/cache/cache.go @@ -37,10 +37,15 @@ func Init() error { } const ( - testCacheKey = "DefaultCache.TestKey" - SlowCacheThreshold = 100 * time.Microsecond + testCacheKey = "DefaultCache.TestKey" + // SlowCacheThreshold marks cache tests as slow + // set to 30ms per discussion: https://github.com/go-gitea/gitea/issues/33190 + // TODO: Replace with metrics histogram + SlowCacheThreshold = 30 * time.Millisecond ) +// Test performs delete, put and get operations on a predefined key +// returns func Test() (time.Duration, error) { if defaultCache == nil { return 0, fmt.Errorf("default cache not initialized") diff --git a/modules/cache/cache_test.go b/modules/cache/cache_test.go index d0352947a8..5408020306 100644 --- a/modules/cache/cache_test.go +++ b/modules/cache/cache_test.go @@ -43,7 +43,8 @@ func TestTest(t *testing.T) { elapsed, err := Test() assert.NoError(t, err) // mem cache should take from 300ns up to 1ms on modern hardware ... - assert.Less(t, elapsed, time.Millisecond) + assert.Positive(t, elapsed) + assert.Less(t, elapsed, SlowCacheThreshold) } func TestGetCache(t *testing.T) { From fd7d393c67b69abee9e2934edbe7c571bab76a81 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 11 Jan 2025 16:05:33 -0800 Subject: [PATCH 006/117] Fix unpin hint on the pinned pull requests (#33207) --- models/issues/comment.go | 4 ++-- options/locale/locale_en-US.ini | 2 +- templates/repo/issue/card.tmpl | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/models/issues/comment.go b/models/issues/comment.go index a7ec8f57fc..a248708820 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -112,8 +112,8 @@ const ( CommentTypePRScheduledToAutoMerge // 34 pr was scheduled to auto merge when checks succeed CommentTypePRUnScheduledToAutoMerge // 35 pr was un scheduled to auto merge when checks succeed - CommentTypePin // 36 pin Issue - CommentTypeUnpin // 37 unpin Issue + CommentTypePin // 36 pin Issue/PullRequest + CommentTypeUnpin // 37 unpin Issue/PullRequest CommentTypeChangeTimeEstimate // 38 Change time estimate ) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 140e2efe57..9d240ac897 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1652,7 +1652,7 @@ issues.attachment.open_tab = `Click to see "%s" in a new tab` issues.attachment.download = `Click to download "%s"` issues.subscribe = Subscribe issues.unsubscribe = Unsubscribe -issues.unpin_issue = Unpin Issue +issues.unpin = Unpin issues.max_pinned = "You can't pin more issues" issues.pin_comment = "pinned this %s" issues.unpin_comment = "unpinned this %s" diff --git a/templates/repo/issue/card.tmpl b/templates/repo/issue/card.tmpl index 2e19e86d7a..c7bbe91885 100644 --- a/templates/repo/issue/card.tmpl +++ b/templates/repo/issue/card.tmpl @@ -16,7 +16,7 @@
{{.Title | ctx.RenderUtils.RenderIssueSimpleTitle}} {{if and $.isPinnedIssueCard $.Page.IsRepoAdmin}} - + {{svg "octicon-x" 16}} {{end}} From a7e750414c41f41fb7783ab2e7266c5366d42424 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sun, 12 Jan 2025 00:35:53 +0000 Subject: [PATCH 007/117] [skip ci] Updated translations via Crowdin --- options/locale/locale_cs-CZ.ini | 2 +- options/locale/locale_de-DE.ini | 2 +- options/locale/locale_el-GR.ini | 2 +- options/locale/locale_es-ES.ini | 2 +- options/locale/locale_fr-FR.ini | 2 +- options/locale/locale_ga-IE.ini | 15 ++++++++++++++- options/locale/locale_id-ID.ini | 1 + options/locale/locale_ja-JP.ini | 2 +- options/locale/locale_lv-LV.ini | 2 +- options/locale/locale_pt-BR.ini | 2 +- options/locale/locale_pt-PT.ini | 2 +- options/locale/locale_ru-RU.ini | 2 +- options/locale/locale_sk-SK.ini | 1 + options/locale/locale_tr-TR.ini | 2 +- options/locale/locale_zh-CN.ini | 2 +- options/locale/locale_zh-TW.ini | 2 +- 16 files changed, 29 insertions(+), 14 deletions(-) diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 1d3bc7d577..ad15d22dd2 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -1651,7 +1651,7 @@ issues.attachment.open_tab=`Klikněte pro zobrazení „%s“ v nové záložce` issues.attachment.download=`Klikněte pro stažení „%s“` issues.subscribe=Odebírat issues.unsubscribe=Zrušit odběr -issues.unpin_issue=Odepnout úkol +issues.unpin=Odepnout issues.max_pinned=Nemůžete připnout další úkoly issues.pin_comment=připnuto %s issues.unpin_comment=odepnul/a tento %s diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index ce2c43cb56..e805f7fe0a 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -1646,7 +1646,7 @@ issues.attachment.open_tab=`Klicken, um „%s“ in einem neuen Tab zu öffnen` issues.attachment.download=`Klicken, um „%s“ herunterzuladen` issues.subscribe=Abonnieren issues.unsubscribe=Abbestellen -issues.unpin_issue=Issue abheften +issues.unpin=Loslösen issues.max_pinned=Du kannst keine weiteren Issues anheften issues.pin_comment=hat das %s angeheftet issues.unpin_comment=hat das %s abgeheftet diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index 31e57bbf97..454e57eb12 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -1495,7 +1495,7 @@ issues.attachment.open_tab=`Κάντε κλικ για να δείτε το "%s" issues.attachment.download=`Κάντε κλικ για να λάβετε το "%s"` issues.subscribe=Εγγραφή issues.unsubscribe=Διαγραφή -issues.unpin_issue=Άφεση Ζητήματος +issues.unpin=Άφεση issues.max_pinned=Δεν μπορείτε να διατηρήσετε περισσότερα ζητήματα issues.pin_comment=διατήρησε αυτό %s issues.unpin_comment=άφησε αυτό %s diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index cdfe1fb2e5..7dd8030d2b 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -1485,7 +1485,7 @@ issues.attachment.open_tab='Haga clic para ver "%s" en una pestaña nueva' issues.attachment.download=`Haga clic para descargar "%s"` issues.subscribe=Suscribir issues.unsubscribe=Desuscribirse -issues.unpin_issue=Desanclar incidencia +issues.unpin=Desanclar issues.max_pinned=No puedes anclar más incidencias issues.pin_comment=anclado este %s issues.unpin_comment=desanclado este %s diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index becd8829fc..3615c6b24d 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -1651,7 +1651,7 @@ issues.attachment.open_tab=`Cliquez ici pour voir « %s » dans un nouvel ongl issues.attachment.download=`Cliquez pour télécharger « %s ».` issues.subscribe=S’abonner issues.unsubscribe=Se désabonner -issues.unpin_issue=Désépingler le ticket +issues.unpin=Désépingler issues.max_pinned=Vous ne pouvez pas épingler plus de tickets issues.pin_comment=a épinglé ça %s. issues.unpin_comment=a désépinglé ça %s. diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index cf6c76a9db..ef486b9e7a 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -244,6 +244,7 @@ license_desc=Téigh go bhfaighidh doiciméadúchán roimh aon socruithe a athrú. require_db_desc=Éilíonn Gitea MySQL, PostgreSQL, MSSQL, SQLite3 nó TiDB (prótacal MySQL). @@ -1015,6 +1016,9 @@ new_repo_helper=Tá gach comhad tionscadail i stór, lena n-áirítear stair ath owner=Úinéir owner_helper=B'fhéidir nach dtaispeánfar roinnt eagraíochtaí sa anuas mar gheall ar theorainn uasta comhaireamh stórais. repo_name=Ainm Stórais +repo_name_profile_public_hint=Is stóras speisialta é .profile is féidir leat a úsáid chun README.md a chur le do phróifíl eagraíochta poiblí, le feiceáil ag aon duine. Cinntigh go bhfuil sé poiblí agus tosaigh é le README san eolaire próifíle le tosú. +repo_name_profile_private_hint=Is stóras speisialta é .profile-private is féidir leat a úsáid chun README.md a chur le do phróifíl bhall eagraíochta, nach féidir a fheiceáil ach ag baill eagraíochta. Cinntigh go bhfuil sé príobháideach agus tosaigh le README sa eolaire próifíle chun tús a chur leis. +repo_name_helper=Úsáideann ainmneacha maith stóras focail eochair gairide, áithnid agus uathúla. D'fhéadfaí stóras darbh ainm '.profile' nó '.profile-private' a úsáid chun README.md a chur leis an bpróifíl úsáideora/eagraíochta. repo_size=Méid an Stóras template=Teimpléad template_select=Roghnaigh teimpléad. @@ -1231,6 +1235,7 @@ create_new_repo_command=Stóras nua a chruthú ar an líne ordaithe push_exist_repo=Stóras atá ann cheana a bhrú ón líne ordaithe empty_message=Níl aon ábhar sa stóras seo. broken_message=Ní féidir na sonraí Git atá mar bhunús leis an stóras seo a léamh. Déan teagmháil le riarthóir an chás seo nó scrios an stóras seo. +no_branch=Níl aon bhrainsí ag an stóras seo. code=Cód code.desc=Rochtain ar chód foinse, comhaid, gealltanais agus brainsí. @@ -1646,7 +1651,7 @@ issues.attachment.open_tab=`Cliceáil chun "%s" a fheiceáil i gcluaisín nua` issues.attachment.download=`Cliceáil chun "%s" a íoslódáil issues.subscribe=Liostáil issues.unsubscribe=Díliostáil -issues.unpin_issue=Bain pionna an t-eagrán +issues.unpin=Díphoráil issues.max_pinned=Ní féidir leat níos mó saincheisteanna a phionadh issues.pin_comment=phionnáil an %s seo issues.unpin_comment=bain pionna an %s seo @@ -2622,6 +2627,9 @@ diff.image.overlay=Forleagan diff.has_escaped=Tá carachtair Unicode i bhfolach ag an líne seo diff.show_file_tree=Taispeáin crann comhad diff.hide_file_tree=Folaigh crann comhad +diff.submodule_added=Fomhodúl %[1]s curtha leis ag %[2]s +diff.submodule_deleted=Scriosadh fomhodúl %[1]s ó %[2]s +diff.submodule_updated=Nuashonraíodh fomhodúl %[1]s: %[2]s releases.desc=Rian leaganacha tionscadal agus íoslódálacha. release.releases=Eisiúintí @@ -2860,6 +2868,9 @@ teams.invite.title=Tugadh cuireadh duit dul isteach i bhfoireann %s/etc/apk/repositories: alpine.registry.key=Íoslódáil eochair RSA poiblí na clárlainne isteach san fhillteán /etc/apk/keys/ chun an síniú innéacs a fhíorú: alpine.registry.info=Roghnaigh $branch agus $repository ón liosta thíos. @@ -3755,6 +3767,7 @@ workflow.not_found=Níor aimsíodh sreabhadh oibre '%s'. workflow.run_success=Ritheann sreabhadh oibre '%s' go rathúil. workflow.from_ref=Úsáid sreabhadh oibre ó workflow.has_workflow_dispatch=Tá comhoibriú ag an gcur i bhfeidhm seo le himeacht workflow_dispatch. +workflow.has_no_workflow_dispatch=Níl aon truicear teagmhais workflow_dispatch ag sreabhadh oibre '%s'. need_approval_desc=Teastaíonn faomhadh chun sreafaí oibre a rith le haghaidh iarratas tarraingt forc. diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index 1fcf6d59b6..391691ebf5 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -818,6 +818,7 @@ issues.attachment.open_tab=`Klik untuk melihat "%s" di tab baru` issues.attachment.download=`Klik untuk mengunduh "%s"` issues.subscribe=Berlangganan issues.unsubscribe=Berhenti berlangganan +issues.unpin=Lepas sematan issues.delete=Hapus diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 8ee9112c34..4fa61eb494 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -1641,7 +1641,7 @@ issues.attachment.open_tab=`クリックして新しいタブで "%s" を見る` issues.attachment.download=`クリックして "%s" をダウンロード` issues.subscribe=購読する issues.unsubscribe=購読を解除 -issues.unpin_issue=イシューのピン留めを解除 +issues.unpin=ピン留め解除 issues.max_pinned=これ以上イシューをピン留めできません issues.pin_comment=がピン留め %s issues.unpin_comment=がピン留めを解除 %s diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index 0dfc5683ec..c90de9578b 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -1501,7 +1501,7 @@ issues.attachment.open_tab=`Noklikšķiniet, lai apskatītos "%s" jaunā logā` issues.attachment.download=`Noklikšķiniet, lai lejupielādētu "%s"` issues.subscribe=Abonēt issues.unsubscribe=Atrakstīties -issues.unpin_issue=Atspraust problēmu +issues.unpin=Atspraust issues.max_pinned=Nevar piespraust vairāk problēmas issues.pin_comment=piesprauda šo %s issues.unpin_comment=atsprauda šo %s diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index f0c034a133..f42de9287e 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -1491,7 +1491,7 @@ issues.attachment.open_tab=`Clique para ver "%s" em uma nova aba` issues.attachment.download=`Clique para baixar "%s"` issues.subscribe=Inscrever-se issues.unsubscribe=Desinscrever -issues.unpin_issue=Desfixar issue +issues.unpin=Desfixar issues.max_pinned=Você não pode fixar mais issues issues.pin_comment=fixou isto %s issues.unpin_comment=desafixou isto %s diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 824da09dda..7b57e776d1 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -1651,7 +1651,7 @@ issues.attachment.open_tab=`Clique para ver "%s" num separador novo` issues.attachment.download=`Clique para descarregar "%s"` issues.subscribe=Subscrever issues.unsubscribe=Anular subscrição -issues.unpin_issue=Desafixar questão +issues.unpin=Desafixar issues.max_pinned=Já não pode fixar mais questões issues.pin_comment=fixou isto %s issues.unpin_comment=desafixou isto %s diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index 027a2cb19d..9c6c706288 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -1471,7 +1471,7 @@ issues.attachment.open_tab=`Нажмите, чтобы увидеть «%s» в issues.attachment.download=`Нажмите, чтобы скачать «%s»` issues.subscribe=Подписаться issues.unsubscribe=Отказаться от подписки -issues.unpin_issue=Открепить задачу +issues.unpin=Открепить issues.max_pinned=Нельзя закрепить больше задач issues.pin_comment=закрепил(а) эту задачу %s issues.unpin_comment=открепил(а) эту задачу %s diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini index 43b190098f..cd2f915755 100644 --- a/options/locale/locale_sk-SK.ini +++ b/options/locale/locale_sk-SK.ini @@ -1068,6 +1068,7 @@ issues.dismiss_review=Zamietnuť revíziu issues.dismiss_review_warning=Naozaj chcete zrušiť túto revíziu? issues.cancel=Zrušiť issues.save=Uložiť +issues.unpin=Odopnúť diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index fb30ab3b67..e939d9f04e 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -1605,7 +1605,7 @@ issues.attachment.open_tab=`Yeni bir sekmede "%s" görmek için tıkla` issues.attachment.download=`"%s" indirmek için tıkla` issues.subscribe=Abone Ol issues.unsubscribe=Abonelikten Çık -issues.unpin_issue=Konuyu Sabitlemeden Kaldır +issues.unpin=Sabitlemeyi kaldır issues.max_pinned=Daha fazla konuyu sabitleyemezsiniz issues.pin_comment=%s sabitlendi issues.unpin_comment=%s sabitlenmesi kaldırıldı diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 5e4723a4cd..564e9f9d8d 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -1646,7 +1646,7 @@ issues.attachment.open_tab=`在新的标签页中查看 '%s'` issues.attachment.download=`点击下载 '%s'` issues.subscribe=订阅 issues.unsubscribe=取消订阅 -issues.unpin_issue=取消置顶 +issues.unpin=取消置顶 issues.max_pinned=您不能置顶更多工单 issues.pin_comment=于 %s 被置顶 issues.unpin_comment=于 %s 取消置顶 diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index 948b47bc9c..b4183aa70a 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -1640,7 +1640,7 @@ issues.attachment.open_tab=`在新分頁中查看「%s」` issues.attachment.download=`點擊下載「%s」` issues.subscribe=訂閱 issues.unsubscribe=取消訂閱 -issues.unpin_issue=取消固定問題 +issues.unpin=取消固定 issues.max_pinned=您不能固定更多問題 issues.pin_comment=固定於 %s issues.unpin_comment=取消固定於 %s From a068462ac08e3937ba829722d9a417763cb3763e Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 12 Jan 2025 11:39:46 +0800 Subject: [PATCH 008/117] Refactor context repository (#33202) --- modules/markup/asciicast/asciicast.go | 2 +- modules/markup/render.go | 4 +- routers/web/feed/branch.go | 2 +- routers/web/feed/file.go | 2 +- routers/web/repo/blame.go | 4 +- routers/web/repo/branch.go | 12 +-- routers/web/repo/cherry_pick.go | 4 +- routers/web/repo/commit.go | 2 +- routers/web/repo/editor.go | 8 +- routers/web/repo/middlewares.go | 12 +-- routers/web/repo/packages.go | 4 +- routers/web/repo/patch.go | 4 +- routers/web/repo/release.go | 6 +- routers/web/repo/render.go | 2 +- routers/web/repo/view.go | 4 +- routers/web/repo/view_file.go | 10 +-- routers/web/repo/view_home.go | 2 +- routers/web/repo/view_readme.go | 2 +- services/context/context_model.go | 20 ----- services/context/repo.go | 65 +++++++--------- templates/repo/blame.tmpl | 6 +- templates/repo/commits_table.tmpl | 2 +- templates/repo/editor/edit.tmpl | 2 +- templates/repo/header.tmpl | 2 +- templates/repo/home.tmpl | 6 +- templates/repo/settings/collaboration.tmpl | 90 +++++++++++----------- templates/repo/sub_menu.tmpl | 2 +- templates/repo/view_file.tmpl | 6 +- templates/repo/wiki/pages.tmpl | 2 +- 29 files changed, 125 insertions(+), 164 deletions(-) diff --git a/modules/markup/asciicast/asciicast.go b/modules/markup/asciicast/asciicast.go index 1d0d631650..d86d61d7c4 100644 --- a/modules/markup/asciicast/asciicast.go +++ b/modules/markup/asciicast/asciicast.go @@ -46,7 +46,7 @@ func (Renderer) Render(ctx *markup.RenderContext, _ io.Reader, output io.Writer) setting.AppSubURL, url.PathEscape(ctx.RenderOptions.Metas["user"]), url.PathEscape(ctx.RenderOptions.Metas["repo"]), - ctx.RenderOptions.Metas["BranchNameSubURL"], + ctx.RenderOptions.Metas["RefTypeNameSubURL"], url.PathEscape(ctx.RenderOptions.RelativePath), ) return ctx.RenderInternal.FormatWithSafeAttrs(output, `
`, playerClassName, playerSrcAttr, rawURL) diff --git a/modules/markup/render.go b/modules/markup/render.go index b239e59687..37a2a86687 100644 --- a/modules/markup/render.go +++ b/modules/markup/render.go @@ -44,7 +44,7 @@ type RenderOptions struct { MarkupType string // user&repo, format&style®exp (for external issue pattern), teams&org (for mention) - // BranchNameSubURL (for iframe&asciicast) + // RefTypeNameSubURL (for iframe&asciicast) // markupAllowShortIssuePattern // markdownLineBreakStyle (comment, document) Metas map[string]string @@ -170,7 +170,7 @@ sandbox="allow-scripts" setting.AppSubURL, url.PathEscape(ctx.RenderOptions.Metas["user"]), url.PathEscape(ctx.RenderOptions.Metas["repo"]), - ctx.RenderOptions.Metas["BranchNameSubURL"], + ctx.RenderOptions.Metas["RefTypeNameSubURL"], url.PathEscape(ctx.RenderOptions.RelativePath), )) return err diff --git a/routers/web/feed/branch.go b/routers/web/feed/branch.go index 80ce2ad198..6c4cc11ca0 100644 --- a/routers/web/feed/branch.go +++ b/routers/web/feed/branch.go @@ -23,7 +23,7 @@ func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType stri } title := fmt.Sprintf("Latest commits for branch %s", ctx.Repo.BranchName) - link := &feeds.Link{Href: repo.HTMLURL() + "/" + ctx.Repo.BranchNameSubURL()} + link := &feeds.Link{Href: repo.HTMLURL() + "/" + ctx.Repo.RefTypeNameSubURL()} feed := &feeds.Feed{ Title: title, diff --git a/routers/web/feed/file.go b/routers/web/feed/file.go index 1ab768ff27..9732264351 100644 --- a/routers/web/feed/file.go +++ b/routers/web/feed/file.go @@ -35,7 +35,7 @@ func ShowFileFeed(ctx *context.Context, repo *repo.Repository, formatType string title := fmt.Sprintf("Latest commits for file %s", ctx.Repo.TreePath) - link := &feeds.Link{Href: repo.HTMLURL() + "/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)} + link := &feeds.Link{Href: repo.HTMLURL() + "/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)} feed := &feeds.Feed{ Title: title, diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go index ad79087513..1022c5e6d6 100644 --- a/routers/web/repo/blame.go +++ b/routers/web/repo/blame.go @@ -46,9 +46,9 @@ func RefBlame(ctx *context.Context) { return } - branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() treeLink := branchLink - rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() if len(ctx.Repo.TreePath) > 0 { treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath) diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index 5d58c64ec8..51d91b7d6a 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -185,7 +185,7 @@ func CreateBranch(ctx *context.Context) { if ctx.HasError() { ctx.Flash.Error(ctx.GetErrMsg()) - ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) + ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()) return } @@ -205,25 +205,25 @@ func CreateBranch(ctx *context.Context) { if err != nil { if release_service.IsErrProtectedTagName(err) { ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected")) - ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) + ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()) return } if release_service.IsErrTagAlreadyExists(err) { e := err.(release_service.ErrTagAlreadyExists) ctx.Flash.Error(ctx.Tr("repo.branch.tag_collision", e.TagName)) - ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) + ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()) return } if git_model.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) { ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.NewBranchName)) - ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) + ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()) return } if git_model.IsErrBranchNameConflict(err) { e := err.(git_model.ErrBranchNameConflict) ctx.Flash.Error(ctx.Tr("repo.branch.branch_name_conflict", form.NewBranchName, e.BranchName)) - ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) + ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()) return } if git.IsErrPushRejected(err) { @@ -242,7 +242,7 @@ func CreateBranch(ctx *context.Context) { } ctx.Flash.Error(flashError) } - ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) + ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()) return } diff --git a/routers/web/repo/cherry_pick.go b/routers/web/repo/cherry_pick.go index 35f158df52..33d941c9d8 100644 --- a/routers/web/repo/cherry_pick.go +++ b/routers/web/repo/cherry_pick.go @@ -57,7 +57,7 @@ func CherryPick(ctx *context.Context) { ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) ctx.Data["last_commit"] = ctx.Repo.CommitID ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() ctx.HTML(200, tplCherryPick) } @@ -85,7 +85,7 @@ func CherryPickPost(ctx *context.Context) { ctx.Data["new_branch_name"] = form.NewBranchName ctx.Data["last_commit"] = ctx.Repo.CommitID ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() if ctx.HasError() { ctx.HTML(200, tplCherryPick) diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 3655233312..638b5e680a 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -191,7 +191,7 @@ func SearchCommits(ctx *context.Context) { query := ctx.FormTrim("q") if len(query) == 0 { - ctx.Redirect(ctx.Repo.RepoLink + "/commits/" + ctx.Repo.BranchNameSubURL()) + ctx.Redirect(ctx.Repo.RepoLink + "/commits/" + ctx.Repo.RefTypeNameSubURL()) return } diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 5fbdeee27e..85f407ab8d 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -180,7 +180,7 @@ func editFile(ctx *context.Context, isNewFile bool) { ctx.Data["TreeNames"] = treeNames ctx.Data["TreePaths"] = treePaths - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() ctx.Data["commit_summary"] = "" ctx.Data["commit_message"] = "" if canCommit { @@ -428,7 +428,7 @@ func DiffPreviewPost(ctx *context.Context) { // DeleteFile render delete file page func DeleteFile(ctx *context.Context) { ctx.Data["PageIsDelete"] = true - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() treePath := cleanUploadFileName(ctx.Repo.TreePath) if treePath != ctx.Repo.TreePath { @@ -462,7 +462,7 @@ func DeleteFilePost(ctx *context.Context) { } ctx.Data["PageIsDelete"] = true - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() ctx.Data["TreePath"] = ctx.Repo.TreePath ctx.Data["commit_summary"] = form.CommitSummary ctx.Data["commit_message"] = form.CommitMessage @@ -604,7 +604,7 @@ func UploadFile(ctx *context.Context) { ctx.Data["TreeNames"] = treeNames ctx.Data["TreePaths"] = treePaths - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() ctx.Data["commit_summary"] = "" ctx.Data["commit_message"] = "" if canCommit { diff --git a/routers/web/repo/middlewares.go b/routers/web/repo/middlewares.go index 420931c5fb..7518e6feae 100644 --- a/routers/web/repo/middlewares.go +++ b/routers/web/repo/middlewares.go @@ -4,12 +4,9 @@ package repo import ( - "fmt" "strconv" - system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/services/context" user_service "code.gitea.io/gitea/services/user" @@ -22,12 +19,9 @@ func SetEditorconfigIfExists(ctx *context.Context) { } ec, _, err := ctx.Repo.GetEditorconfig() - - if err != nil && !git.IsErrNotExist(err) { - description := fmt.Sprintf("Error while getting .editorconfig file: %v", err) - if err := system_model.CreateRepositoryNotice(description); err != nil { - ctx.ServerError("ErrCreatingReporitoryNotice", err) - } + if err != nil { + // it used to check `!git.IsErrNotExist(err)` and create a system notice, but it is quite annoying and useless + // because network errors also happen frequently, so we just ignore it return } diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index 5dcfd0454c..65a340a799 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -62,9 +62,7 @@ func Packages(ctx *context.Context) { ctx.Data["PackageType"] = packageType ctx.Data["AvailableTypes"] = packages.TypeList ctx.Data["HasPackages"] = hasPackages - if ctx.Repo != nil { - ctx.Data["CanWritePackages"] = ctx.IsUserRepoWriter([]unit.Type{unit.TypePackages}) || ctx.IsUserSiteAdmin() - } + ctx.Data["CanWritePackages"] = ctx.Repo.CanWrite(unit.TypePackages) || ctx.IsUserSiteAdmin() ctx.Data["PackageDescriptors"] = pds ctx.Data["Total"] = total ctx.Data["RepositoryAccessMap"] = map[int64]bool{ctx.Repo.Repository.ID: true} // There is only the current repository diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go index 1807cf31a1..4d47a705d6 100644 --- a/routers/web/repo/patch.go +++ b/routers/web/repo/patch.go @@ -37,7 +37,7 @@ func NewDiffPatch(ctx *context.Context) { ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) ctx.Data["last_commit"] = ctx.Repo.CommitID ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() ctx.HTML(200, tplPatchFile) } @@ -52,7 +52,7 @@ func NewDiffPatchPost(ctx *context.Context) { branchName = form.NewBranchName } ctx.Data["PageIsPatch"] = true - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() ctx.Data["FileContent"] = form.Content ctx.Data["commit_summary"] = form.CommitSummary ctx.Data["commit_message"] = form.CommitMessage diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index 5b099fe8d4..53655703fc 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -435,19 +435,19 @@ func NewReleasePost(ctx *context.Context) { if release_service.IsErrTagAlreadyExists(err) { e := err.(release_service.ErrTagAlreadyExists) ctx.Flash.Error(ctx.Tr("repo.branch.tag_collision", e.TagName)) - ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) + ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()) return } if release_service.IsErrInvalidTagName(err) { ctx.Flash.Error(ctx.Tr("repo.release.tag_name_invalid")) - ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) + ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()) return } if release_service.IsErrProtectedTagName(err) { ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected")) - ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) + ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()) return } diff --git a/routers/web/repo/render.go b/routers/web/repo/render.go index 856425ae35..069bf6f6bf 100644 --- a/routers/web/repo/render.go +++ b/routers/web/repo/render.go @@ -58,7 +58,7 @@ func RenderFile(ctx *context.Context) { } rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ - CurrentRefPath: ctx.Repo.BranchNameSubURL(), + CurrentRefPath: ctx.Repo.RefTypeNameSubURL(), CurrentTreePath: path.Dir(ctx.Repo.TreePath), }).WithRelativePath(ctx.Repo.TreePath).WithInStandalonePage(true) diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 9fe2b58ebc..05ecf2ab79 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -243,7 +243,7 @@ func LastCommit(ctx *context.Context) { ctx.Data["ParentPath"] = "/" + paths[len(paths)-2] } } - branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() ctx.Data["BranchLink"] = branchLink ctx.HTML(http.StatusOK, tplRepoViewList) @@ -301,7 +301,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri return nil } - branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() treeLink := branchLink if len(ctx.Repo.TreePath) > 0 { diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index 17c2821824..f4be4783fb 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -45,7 +45,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName) ctx.Data["FileIsSymlink"] = entry.IsLink() ctx.Data["FileName"] = blob.Name() - ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) commit, err := ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath) if err != nil { @@ -92,7 +92,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { isDisplayingRendered := !isDisplayingSource if fInfo.isLFSFile { - ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/media/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/media/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) } isRepresentableAsText := fInfo.st.IsRepresentableAsText() @@ -170,9 +170,9 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["IsMarkup"] = true ctx.Data["MarkupType"] = markupType metas := ctx.Repo.Repository.ComposeDocumentMetas(ctx) - metas["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL() + metas["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL() rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ - CurrentRefPath: ctx.Repo.BranchNameSubURL(), + CurrentRefPath: ctx.Repo.RefTypeNameSubURL(), CurrentTreePath: path.Dir(ctx.Repo.TreePath), }). WithMarkupType(markupType). @@ -262,7 +262,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["MarkupType"] = markupType rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ - CurrentRefPath: ctx.Repo.BranchNameSubURL(), + CurrentRefPath: ctx.Repo.RefTypeNameSubURL(), CurrentTreePath: path.Dir(ctx.Repo.TreePath), }). WithMarkupType(markupType). diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go index 8c9f54656b..7aa8a72430 100644 --- a/routers/web/repo/view_home.go +++ b/routers/web/repo/view_home.go @@ -346,7 +346,7 @@ func Home(ctx *context.Context) { // prepare the tree path var treeNames, paths []string - branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() treeLink := branchLink if ctx.Repo.TreePath != "" { treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath) diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go index 5bd39de963..48befe47f8 100644 --- a/routers/web/repo/view_readme.go +++ b/routers/web/repo/view_readme.go @@ -189,7 +189,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil ctx.Data["MarkupType"] = markupType rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ - CurrentRefPath: ctx.Repo.BranchNameSubURL(), + CurrentRefPath: ctx.Repo.RefTypeNameSubURL(), CurrentTreePath: path.Join(ctx.Repo.TreePath, subfolder), }). WithMarkupType(markupType). diff --git a/services/context/context_model.go b/services/context/context_model.go index 4f70aac516..3a1776102f 100644 --- a/services/context/context_model.go +++ b/services/context/context_model.go @@ -3,27 +3,7 @@ package context -import ( - "code.gitea.io/gitea/models/unit" -) - // IsUserSiteAdmin returns true if current user is a site admin func (ctx *Context) IsUserSiteAdmin() bool { return ctx.IsSigned && ctx.Doer.IsAdmin } - -// IsUserRepoAdmin returns true if current user is admin in current repo -func (ctx *Context) IsUserRepoAdmin() bool { - return ctx.Repo.IsAdmin() -} - -// IsUserRepoWriter returns true if current user has write privilege in current repo -func (ctx *Context) IsUserRepoWriter(unitTypes []unit.Type) bool { - for _, unitType := range unitTypes { - if ctx.Repo.CanWrite(unitType) { - return true - } - } - - return false -} diff --git a/services/context/repo.go b/services/context/repo.go index 94b2972c2b..121910235f 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -46,22 +46,27 @@ type PullRequest struct { // Repository contains information to operate a repository type Repository struct { access_model.Permission - IsWatching bool + + Repository *repo_model.Repository + Owner *user_model.User + + RepoLink string + GitRepo *git.Repository + + // these fields indicate the current ref type, for example: ".../src/branch/master" means IsViewBranch=true IsViewBranch bool IsViewTag bool IsViewCommit bool - Repository *repo_model.Repository - Owner *user_model.User - Commit *git.Commit - Tag *git.Tag - GitRepo *git.Repository - RefName string - BranchName string - TagName string - TreePath string - CommitID string - RepoLink string - CloneLink repo_model.CloneLink + + RefName string + BranchName string + TagName string + TreePath string + + // Commit it is always set to the commit for the branch or tag + Commit *git.Commit + CommitID string + CommitsCount int64 PullRequest *PullRequest @@ -149,7 +154,7 @@ func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.Use }, err } -// CanUseTimetracker returns whether or not a user can use the timetracker. +// CanUseTimetracker returns whether a user can use the timetracker. func (r *Repository) CanUseTimetracker(ctx context.Context, issue *issues_model.Issue, user *user_model.User) bool { // Checking for following: // 1. Is timetracker enabled @@ -199,8 +204,12 @@ func (r *Repository) GetCommitGraphsCount(ctx context.Context, hidePRRefs bool, }) } -// BranchNameSubURL sub-URL for the BranchName field -func (r *Repository) BranchNameSubURL() string { +// RefTypeNameSubURL makes a sub-url for the current ref (branch/tag/commit) field, for example: +// * "branch/master" +// * "tag/v1.0.0" +// * "commit/123456" +// It is usually used to construct a link like ".../src/{{RefTypeNameSubURL}}/{{PathEscapeSegments TreePath}}" +func (r *Repository) RefTypeNameSubURL() string { switch { case r.IsViewBranch: return "branch/" + util.PathEscapeSegments(r.BranchName) @@ -213,21 +222,6 @@ func (r *Repository) BranchNameSubURL() string { return "" } -// FileExists returns true if a file exists in the given repo branch -func (r *Repository) FileExists(path, branch string) (bool, error) { - if branch == "" { - branch = r.Repository.DefaultBranch - } - commit, err := r.GitRepo.GetBranchCommit(branch) - if err != nil { - return false, err - } - if _, err := commit.GetTreeEntryByPath(path); err != nil { - return false, err - } - return true, nil -} - // GetEditorconfig returns the .editorconfig definition if found in the // HEAD of the default repo branch. func (r *Repository) GetEditorconfig(optCommit ...*git.Commit) (cfg *editorconfig.Editorconfig, warning, err error) { @@ -450,7 +444,6 @@ func RepoAssignment(ctx *Context) { ctx.Repo.Owner = owner ctx.ContextUser = owner ctx.Data["ContextUser"] = ctx.ContextUser - ctx.Data["Username"] = ctx.Repo.Owner.Name // redirect link to wiki if strings.HasSuffix(repoName, ".wiki") { @@ -502,7 +495,6 @@ func RepoAssignment(ctx *Context) { ctx.Repo.RepoLink = repo.Link() ctx.Data["RepoLink"] = ctx.Repo.RepoLink - ctx.Data["RepoRelPath"] = ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name if setting.Other.EnableFeed { ctx.Data["EnableFeed"] = true @@ -537,9 +529,6 @@ func RepoAssignment(ctx *Context) { ctx.Data["Title"] = owner.Name + "/" + repo.Name ctx.Data["Repository"] = repo ctx.Data["Owner"] = ctx.Repo.Repository.Owner - ctx.Data["IsRepositoryOwner"] = ctx.Repo.IsOwner() - ctx.Data["IsRepositoryAdmin"] = ctx.Repo.IsAdmin() - ctx.Data["RepoOwnerIsOrganization"] = repo.Owner.IsOrganization() ctx.Data["CanWriteCode"] = ctx.Repo.CanWrite(unit_model.TypeCode) ctx.Data["CanWriteIssues"] = ctx.Repo.CanWrite(unit_model.TypeIssues) ctx.Data["CanWritePulls"] = ctx.Repo.CanWrite(unit_model.TypePullRequests) @@ -979,7 +968,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func redirect := path.Join( ctx.Repo.RepoLink, util.PathEscapeSegments(prefix), - ctx.Repo.BranchNameSubURL(), + ctx.Repo.RefTypeNameSubURL(), util.PathEscapeSegments(ctx.Repo.TreePath)) ctx.Redirect(redirect) return @@ -988,7 +977,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ctx.Data["BranchName"] = ctx.Repo.BranchName ctx.Data["RefName"] = ctx.Repo.RefName - ctx.Data["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL() + ctx.Data["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL() ctx.Data["TagName"] = ctx.Repo.TagName ctx.Data["CommitID"] = ctx.Repo.CommitID ctx.Data["TreePath"] = ctx.Repo.TreePath diff --git a/templates/repo/blame.tmpl b/templates/repo/blame.tmpl index 62d1bbf2ba..f8bd22074f 100644 --- a/templates/repo/blame.tmpl +++ b/templates/repo/blame.tmpl @@ -1,5 +1,5 @@ {{if or .UsesIgnoreRevs .FaultyIgnoreRevsFile}} - {{$revsFileLink := URLJoin .RepoLink "src" .BranchNameSubURL "/.git-blame-ignore-revs"}} + {{$revsFileLink := URLJoin .RepoLink "src" .RefTypeNameSubURL "/.git-blame-ignore-revs"}} {{if .UsesIgnoreRevs}}

{{ctx.Locale.Tr "repo.blame.ignore_revs" $revsFileLink "?bypass-blame-ignore=true"}}

@@ -21,8 +21,8 @@ {{if not .IsViewCommit}} {{ctx.Locale.Tr "repo.file_permalink"}} {{end}} - {{ctx.Locale.Tr "repo.normal_view"}} - {{ctx.Locale.Tr "repo.file_history"}} + {{ctx.Locale.Tr "repo.normal_view"}} + {{ctx.Locale.Tr "repo.file_history"}}
diff --git a/templates/repo/commits_table.tmpl b/templates/repo/commits_table.tmpl index 91fc1c2fae..a0c5eacdd4 100644 --- a/templates/repo/commits_table.tmpl +++ b/templates/repo/commits_table.tmpl @@ -19,7 +19,7 @@ {{if .PageIsCommits}}
-
+
{{template "shared/search/input" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.commit_kind")}} {{template "repo/commits_search_dropdown" .}} diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl index 204a426970..577a2be9ad 100644 --- a/templates/repo/editor/edit.tmpl +++ b/templates/repo/editor/edit.tmpl @@ -32,7 +32,7 @@