From 01156f9cc48d67e00657a788249ac02a6ed733cd Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 28 Feb 2025 00:47:00 +0800 Subject: [PATCH 01/62] Show info about maintainers are allowed to edit a PR (#33738) A simple and clear approach. The existing test TestPullCompare_EnableAllowEditsFromMaintainer should be good enough. Fix #21280 ![image](https://github.com/user-attachments/assets/b685950b-4095-45ad-b913-425f840f2876) --- .../repo/issue/sidebar/allow_maintainer_edit.tmpl | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/templates/repo/issue/sidebar/allow_maintainer_edit.tmpl b/templates/repo/issue/sidebar/allow_maintainer_edit.tmpl index ad4ce96a47..f584f9076c 100644 --- a/templates/repo/issue/sidebar/allow_maintainer_edit.tmpl +++ b/templates/repo/issue/sidebar/allow_maintainer_edit.tmpl @@ -1,13 +1,18 @@ -{{if and .Issue.IsPull .IsIssuePoster (not .Issue.IsClosed) .Issue.PullRequest.HeadRepo}} - {{if and (not (eq .Issue.PullRequest.HeadRepo.FullName .Issue.PullRequest.BaseRepo.FullName)) .CanWriteToHeadRepo}} +{{- $isHeadForkedRepo := and .Issue.PullRequest .Issue.PullRequest.HeadRepo (ne .Issue.PullRequest.HeadRepo.FullName .Issue.PullRequest.BaseRepo.FullName) -}} +{{if $isHeadForkedRepo}} + {{- $isPullPoster := and .Issue.IsPull .IsIssuePoster -}} + {{- $isPullEditable := and .Issue.PullRequest (not .Issue.IsClosed) (not .Repository.IsArchived) -}} + {{- $allowToChange := and $isPullPoster $isPullEditable -}}
-
- +
- {{end}} {{end}} From b7aac5ef9a8bb86a5b62cb4e0d15d6034308cc5c Mon Sep 17 00:00:00 2001 From: Royce Remer Date: Thu, 27 Feb 2025 09:50:44 -0800 Subject: [PATCH 02/62] allow filtering /repos/{owner}/{repo}/pulls by target base branch queryparam (#33684) Co-authored-by: Royce Remer Co-authored-by: delvh --- models/issues/pull_list.go | 5 +++++ routers/api/v1/repo/pull.go | 5 +++++ templates/swagger/v1_json.tmpl | 6 ++++++ 3 files changed, 16 insertions(+) diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go index 1ddb94e566..b105fab97c 100644 --- a/models/issues/pull_list.go +++ b/models/issues/pull_list.go @@ -28,11 +28,16 @@ type PullRequestsOptions struct { Labels []int64 MilestoneID int64 PosterID int64 + BaseBranch string } func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) *xorm.Session { sess := db.GetEngine(ctx).Where("pull_request.base_repo_id=?", baseRepoID) + if opts.BaseBranch != "" { + sess.And("pull_request.base_branch=?", opts.BaseBranch) + } + sess.Join("INNER", "issue", "pull_request.issue_id = issue.id") switch opts.State { case "closed", "open": diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 1f61ac031a..412f2cfb58 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -59,6 +59,10 @@ func ListPullRequests(ctx *context.APIContext) { // description: Name of the repo // type: string // required: true + // - name: base_branch + // in: query + // description: Filter by target base branch of the pull request + // type: string // - name: state // in: query // description: State of pull request @@ -132,6 +136,7 @@ func ListPullRequests(ctx *context.APIContext) { Labels: labelIDs, MilestoneID: ctx.FormInt64("milestone"), PosterID: posterID, + BaseBranch: ctx.FormTrim("base_branch"), }) if err != nil { ctx.APIErrorInternal(err) diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index fd3e2a70f1..48ed958ca2 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -12047,6 +12047,12 @@ "in": "path", "required": true }, + { + "type": "string", + "description": "Filter by target base branch of the pull request", + "name": "base_branch", + "in": "query" + }, { "enum": [ "open", From f52e31f5ce88796f7a75c92dd721de4020784e7c Mon Sep 17 00:00:00 2001 From: Quentin Date: Thu, 27 Feb 2025 19:18:02 +0100 Subject: [PATCH 03/62] Clone repository with Tea CLI (#33725) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds "Tea CLI" as a clone method. Capture d’écran 2025-02-25 à 23 38 47 --------- Signed-off-by: Quentin Guidée Co-authored-by: wxiaoguang --- models/repo/repo.go | 18 +++++++--- templates/repo/clone_panel.tmpl | 1 + tests/integration/repo_test.go | 10 ++++++ web_src/js/features/repo-common.ts | 57 +++++++++++++++++++++++------- 4 files changed, 69 insertions(+), 17 deletions(-) diff --git a/models/repo/repo.go b/models/repo/repo.go index 4e27dbaf14..d42792faa2 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -646,13 +646,15 @@ func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML { type CloneLink struct { SSH string HTTPS string + Tea string } -// ComposeHTTPSCloneURL returns HTTPS clone URL based on given owner and repository name. +// ComposeHTTPSCloneURL returns HTTPS clone URL based on the given owner and repository name. func ComposeHTTPSCloneURL(ctx context.Context, owner, repo string) string { return fmt.Sprintf("%s%s/%s.git", httplib.GuessCurrentAppURL(ctx), url.PathEscape(owner), url.PathEscape(repo)) } +// ComposeSSHCloneURL returns SSH clone URL based on the given owner and repository name. func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) string { sshUser := setting.SSH.User sshDomain := setting.SSH.Domain @@ -686,11 +688,17 @@ func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) strin return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName)) } +// ComposeTeaCloneCommand returns Tea CLI clone command based on the given owner and repository name. +func ComposeTeaCloneCommand(ctx context.Context, owner, repo string) string { + return fmt.Sprintf("tea clone %s/%s", url.PathEscape(owner), url.PathEscape(repo)) +} + func (repo *Repository) cloneLink(ctx context.Context, doer *user_model.User, repoPathName string) *CloneLink { - cl := new(CloneLink) - cl.SSH = ComposeSSHCloneURL(doer, repo.OwnerName, repoPathName) - cl.HTTPS = ComposeHTTPSCloneURL(ctx, repo.OwnerName, repoPathName) - return cl + return &CloneLink{ + SSH: ComposeSSHCloneURL(doer, repo.OwnerName, repoPathName), + HTTPS: ComposeHTTPSCloneURL(ctx, repo.OwnerName, repoPathName), + Tea: ComposeTeaCloneCommand(ctx, repo.OwnerName, repoPathName), + } } // CloneLink returns clone URLs of repository. diff --git a/templates/repo/clone_panel.tmpl b/templates/repo/clone_panel.tmpl index b813860150..2ed8f52fbe 100644 --- a/templates/repo/clone_panel.tmpl +++ b/templates/repo/clone_panel.tmpl @@ -14,6 +14,7 @@ {{if $.CloneButtonShowSSH}} {{end}} +
diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go index 8c568a1272..38d0e7fe1f 100644 --- a/tests/integration/repo_test.go +++ b/tests/integration/repo_test.go @@ -130,8 +130,13 @@ func TestViewRepo1CloneLinkAnonymous(t *testing.T) { link, exists := htmlDoc.doc.Find(".repo-clone-https").Attr("data-link") assert.True(t, exists, "The template has changed") assert.Equal(t, setting.AppURL+"user2/repo1.git", link) + _, exists = htmlDoc.doc.Find(".repo-clone-ssh").Attr("data-link") assert.False(t, exists) + + link, exists = htmlDoc.doc.Find(".repo-clone-tea").Attr("data-link") + assert.True(t, exists, "The template has changed") + assert.Equal(t, "tea clone user2/repo1", link) } func TestViewRepo1CloneLinkAuthorized(t *testing.T) { @@ -146,10 +151,15 @@ func TestViewRepo1CloneLinkAuthorized(t *testing.T) { link, exists := htmlDoc.doc.Find(".repo-clone-https").Attr("data-link") assert.True(t, exists, "The template has changed") assert.Equal(t, setting.AppURL+"user2/repo1.git", link) + link, exists = htmlDoc.doc.Find(".repo-clone-ssh").Attr("data-link") assert.True(t, exists, "The template has changed") sshURL := fmt.Sprintf("ssh://%s@%s:%d/user2/repo1.git", setting.SSH.User, setting.SSH.Domain, setting.SSH.Port) assert.Equal(t, sshURL, link) + + link, exists = htmlDoc.doc.Find(".repo-clone-tea").Attr("data-link") + assert.True(t, exists, "The template has changed") + assert.Equal(t, "tea clone user2/repo1", link) } func TestViewRepoWithSymlinks(t *testing.T) { diff --git a/web_src/js/features/repo-common.ts b/web_src/js/features/repo-common.ts index 444cb163bf..4362a2c713 100644 --- a/web_src/js/features/repo-common.ts +++ b/web_src/js/features/repo-common.ts @@ -53,20 +53,49 @@ export function substituteRepoOpenWithUrl(tmpl: string, url: string): string { function initCloneSchemeUrlSelection(parent: Element) { const elCloneUrlInput = parent.querySelector('.repo-clone-url'); - const tabSsh = parent.querySelector('.repo-clone-ssh'); const tabHttps = parent.querySelector('.repo-clone-https'); + const tabSsh = parent.querySelector('.repo-clone-ssh'); + const tabTea = parent.querySelector('.repo-clone-tea'); const updateClonePanelUi = function() { - const scheme = localStorage.getItem('repo-clone-protocol') || 'https'; - const isSSH = scheme === 'ssh' && Boolean(tabSsh) || scheme !== 'ssh' && !tabHttps; - if (tabHttps) { - tabHttps.textContent = window.origin.split(':')[0].toUpperCase(); // show "HTTP" or "HTTPS" - tabHttps.classList.toggle('active', !isSSH); - } - if (tabSsh) { - tabSsh.classList.toggle('active', isSSH); + let scheme = localStorage.getItem('repo-clone-protocol'); + if (!['https', 'ssh', 'tea'].includes(scheme)) { + scheme = 'https'; + } + + // Fallbacks if the scheme preference is not available in the tabs, for example: empty repo page, there are only HTTPS and SSH + if (scheme === 'tea' && !tabTea) { + scheme = 'https'; + } + if (scheme === 'https' && !tabHttps) { + scheme = 'ssh'; + } else if (scheme === 'ssh' && !tabSsh) { + scheme = 'https'; + } + + const isHttps = scheme === 'https'; + const isSsh = scheme === 'ssh'; + const isTea = scheme === 'tea'; + + if (tabHttps) { + tabHttps.textContent = window.origin.split(':')[0].toUpperCase(); // show "HTTP" or "HTTPS" + tabHttps.classList.toggle('active', isHttps); + } + if (tabSsh) { + tabSsh.classList.toggle('active', isSsh); + } + if (tabTea) { + tabTea.classList.toggle('active', isTea); + } + + let tab: Element; + if (isHttps) { + tab = tabHttps; + } else if (isSsh) { + tab = tabSsh; + } else if (isTea) { + tab = tabTea; } - const tab = isSSH ? tabSsh : tabHttps; if (!tab) return; const link = toOriginUrl(tab.getAttribute('data-link')); @@ -84,12 +113,16 @@ function initCloneSchemeUrlSelection(parent: Element) { updateClonePanelUi(); // tabSsh or tabHttps might not both exist, eg: guest view, or one is disabled by the server + tabHttps?.addEventListener('click', () => { + localStorage.setItem('repo-clone-protocol', 'https'); + updateClonePanelUi(); + }); tabSsh?.addEventListener('click', () => { localStorage.setItem('repo-clone-protocol', 'ssh'); updateClonePanelUi(); }); - tabHttps?.addEventListener('click', () => { - localStorage.setItem('repo-clone-protocol', 'https'); + tabTea?.addEventListener('click', () => { + localStorage.setItem('repo-clone-protocol', 'tea'); updateClonePanelUi(); }); elCloneUrlInput.addEventListener('focus', () => { From 8362a4155929007b8d02b63a2e657557edb08fb9 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 27 Feb 2025 20:05:28 +0100 Subject: [PATCH 04/62] Remove superflous tw-content-center (#33741) --- templates/base/paginate.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/base/paginate.tmpl b/templates/base/paginate.tmpl index 9a7a6322f7..253892c009 100644 --- a/templates/base/paginate.tmpl +++ b/templates/base/paginate.tmpl @@ -17,7 +17,7 @@ {{if eq .Num -1}} ... {{else}} - {{.Num}} + {{.Num}} {{end}} {{end}} From 303af554c9bc7b22d5182a6fe9e22192a546cd41 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Thu, 27 Feb 2025 14:40:12 -0500 Subject: [PATCH 05/62] Improve "generate new access token" form (#33730) Fix: https://github.com/go-gitea/gitea/issues/33519 As discussed in [PR #33614](https://github.com/go-gitea/gitea/pull/33614), the ScopedAccessTokenSelector Vue component is not particularly useful. This PR removes the component and reverts to using HTML templates. It also introduces some (hopefully) useful refactoring. The Vue component was causing the UX bug reported in the linked issue. Required form fields are now properly working, as expected (see screenshot). ![Screenshot from 2025-02-25 22-00-28](https://github.com/user-attachments/assets/41167854-0718-48b0-a3ee-75ca3a7b8b20) --------- Co-authored-by: wxiaoguang --- models/auth/access_token_scope.go | 14 +++- models/auth/access_token_scope_test.go | 5 +- options/locale/locale_en-US.ini | 1 - routers/web/user/setting/applications.go | 34 ++++++-- services/context/context.go | 11 ++- services/forms/user_form.go | 11 +-- services/forms/user_form_test.go | 27 ------- templates/user/settings/applications.tmpl | 76 ++++++++--------- .../settings/applications_oauth2_list.tmpl | 54 ++++++------- tests/integration/api_admin_org_test.go | 2 +- tests/integration/api_admin_test.go | 4 +- .../api_helper_for_declarative_test.go | 4 + tests/integration/api_repo_git_blobs_test.go | 2 +- tests/integration/api_repo_git_trees_test.go | 2 +- tests/integration/integration_test.go | 50 ++---------- web_src/css/index.css | 1 - web_src/css/modules/checkbox.css | 10 +++ web_src/css/modules/select.css | 25 ------ .../components/ScopedAccessTokenSelector.vue | 81 ------------------- web_src/js/features/scoped-access-token.ts | 20 ----- web_src/js/index.ts | 2 - 21 files changed, 138 insertions(+), 298 deletions(-) delete mode 100644 web_src/css/modules/select.css delete mode 100644 web_src/js/components/ScopedAccessTokenSelector.vue delete mode 100644 web_src/js/features/scoped-access-token.ts diff --git a/models/auth/access_token_scope.go b/models/auth/access_token_scope.go index 897ff3fc9e..0e5b2e96e6 100644 --- a/models/auth/access_token_scope.go +++ b/models/auth/access_token_scope.go @@ -5,6 +5,7 @@ package auth import ( "fmt" + "slices" "strings" "code.gitea.io/gitea/models/perm" @@ -14,7 +15,7 @@ import ( type AccessTokenScopeCategory int const ( - AccessTokenScopeCategoryActivityPub = iota + AccessTokenScopeCategoryActivityPub AccessTokenScopeCategory = iota AccessTokenScopeCategoryAdmin AccessTokenScopeCategoryMisc // WARN: this is now just a placeholder, don't remove it which will change the following values AccessTokenScopeCategoryNotification @@ -193,6 +194,14 @@ var accessTokenScopes = map[AccessTokenScopeLevel]map[AccessTokenScopeCategory]A }, } +func GetAccessTokenCategories() (res []string) { + for _, cat := range accessTokenScopes[Read] { + res = append(res, strings.TrimPrefix(string(cat), "read:")) + } + slices.Sort(res) + return res +} + // GetRequiredScopes gets the specific scopes for a given level and categories func GetRequiredScopes(level AccessTokenScopeLevel, scopeCategories ...AccessTokenScopeCategory) []AccessTokenScope { scopes := make([]AccessTokenScope, 0, len(scopeCategories)) @@ -270,6 +279,9 @@ func (s AccessTokenScope) parse() (accessTokenScopeBitmap, error) { // StringSlice returns the AccessTokenScope as a []string func (s AccessTokenScope) StringSlice() []string { + if s == "" { + return nil + } return strings.Split(string(s), ",") } diff --git a/models/auth/access_token_scope_test.go b/models/auth/access_token_scope_test.go index a6097e45d7..9e4aa83633 100644 --- a/models/auth/access_token_scope_test.go +++ b/models/auth/access_token_scope_test.go @@ -17,6 +17,7 @@ type scopeTestNormalize struct { } func TestAccessTokenScope_Normalize(t *testing.T) { + assert.Equal(t, []string{"activitypub", "admin", "issue", "misc", "notification", "organization", "package", "repository", "user"}, GetAccessTokenCategories()) tests := []scopeTestNormalize{ {"", "", nil}, {"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", nil}, @@ -25,7 +26,7 @@ func TestAccessTokenScope_Normalize(t *testing.T) { {"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,public-only", "public-only,all", nil}, } - for _, scope := range []string{"activitypub", "admin", "misc", "notification", "organization", "package", "issue", "repository", "user"} { + for _, scope := range GetAccessTokenCategories() { tests = append(tests, scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%s", scope)), AccessTokenScope(fmt.Sprintf("read:%s", scope)), nil}, scopeTestNormalize{AccessTokenScope(fmt.Sprintf("write:%s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil}, @@ -59,7 +60,7 @@ func TestAccessTokenScope_HasScope(t *testing.T) { {"public-only", "read:issue", false, nil}, } - for _, scope := range []string{"activitypub", "admin", "misc", "notification", "organization", "package", "issue", "repository", "user"} { + for _, scope := range GetAccessTokenCategories() { tests = append(tests, scopeTestHasScope{ AccessTokenScope(fmt.Sprintf("read:%s", scope)), diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 4c25ba3205..5aa6d19dae 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -917,7 +917,6 @@ delete_token_success = The token has been deleted. Applications using it no long repo_and_org_access = Repository and Organization Access permissions_public_only = Public only permissions_access_all = All (public, private, and limited) -select_permissions = Select permissions permission_not_set = Not set permission_no_access = No Access permission_read = Read diff --git a/routers/web/user/setting/applications.go b/routers/web/user/setting/applications.go index cf71d01dc1..1f6c97a5cc 100644 --- a/routers/web/user/setting/applications.go +++ b/routers/web/user/setting/applications.go @@ -6,12 +6,14 @@ package setting import ( "net/http" + "strings" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" @@ -39,18 +41,29 @@ func ApplicationsPost(ctx *context.Context) { ctx.Data["PageIsSettingsApplications"] = true ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) - if ctx.HasError() { - loadApplicationsData(ctx) - - ctx.HTML(http.StatusOK, tplSettingsApplications) - return + _ = ctx.Req.ParseForm() + var scopeNames []string + for k, v := range ctx.Req.Form { + if strings.HasPrefix(k, "scope-") { + scopeNames = append(scopeNames, v...) + } } - scope, err := form.GetScope() + scope, err := auth_model.AccessTokenScope(strings.Join(scopeNames, ",")).Normalize() if err != nil { ctx.ServerError("GetScope", err) return } + if scope == "" || scope == auth_model.AccessTokenScopePublicOnly { + ctx.Flash.Error(ctx.Tr("settings.at_least_one_permission"), true) + } + + if ctx.HasError() { + loadApplicationsData(ctx) + ctx.HTML(http.StatusOK, tplSettingsApplications) + return + } + t := &auth_model.AccessToken{ UID: ctx.Doer.ID, Name: form.Name, @@ -99,7 +112,14 @@ func loadApplicationsData(ctx *context.Context) { } ctx.Data["Tokens"] = tokens ctx.Data["EnableOAuth2"] = setting.OAuth2.Enabled - ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin + + // Handle specific ordered token categories for admin or non-admin users + tokenCategoryNames := auth_model.GetAccessTokenCategories() + if !ctx.Doer.IsAdmin { + tokenCategoryNames = util.SliceRemoveAll(tokenCategoryNames, "admin") + } + ctx.Data["TokenCategories"] = tokenCategoryNames + if setting.OAuth2.Enabled { ctx.Data["Applications"], err = db.Find[auth_model.OAuth2Application](ctx, auth_model.FindOAuth2ApplicationsOptions{ OwnerID: ctx.Doer.ID, diff --git a/services/context/context.go b/services/context/context.go index 5e08fba442..f3a0f0bb5f 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -213,13 +213,16 @@ func Contexter() func(next http.Handler) http.Handler { // Attention: this function changes ctx.Data and ctx.Flash // If HasError is called, then before Redirect, the error message should be stored by ctx.Flash.Error(ctx.GetErrMsg()) again. func (ctx *Context) HasError() bool { - hasErr, ok := ctx.Data["HasError"] - if !ok { + hasErr, _ := ctx.Data["HasError"].(bool) + hasErr = hasErr || ctx.Flash.ErrorMsg != "" + if !hasErr { return false } - ctx.Flash.ErrorMsg = ctx.GetErrMsg() + if ctx.Flash.ErrorMsg == "" { + ctx.Flash.ErrorMsg = ctx.GetErrMsg() + } ctx.Data["Flash"] = ctx.Flash - return hasErr.(bool) + return hasErr } // GetErrMsg returns error message in form validation. diff --git a/services/forms/user_form.go b/services/forms/user_form.go index ed79936add..c9ce71e886 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -7,9 +7,7 @@ package forms import ( "mime/multipart" "net/http" - "strings" - auth_model "code.gitea.io/gitea/models/auth" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web/middleware" @@ -347,8 +345,7 @@ func (f *EditVariableForm) Validate(req *http.Request, errs binding.Errors) bind // NewAccessTokenForm form for creating access token type NewAccessTokenForm struct { - Name string `binding:"Required;MaxSize(255)" locale:"settings.token_name"` - Scope []string + Name string `binding:"Required;MaxSize(255)" locale:"settings.token_name"` } // Validate validates the fields @@ -357,12 +354,6 @@ func (f *NewAccessTokenForm) Validate(req *http.Request, errs binding.Errors) bi return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } -func (f *NewAccessTokenForm) GetScope() (auth_model.AccessTokenScope, error) { - scope := strings.Join(f.Scope, ",") - s, err := auth_model.AccessTokenScope(scope).Normalize() - return s, err -} - // EditOAuth2ApplicationForm form for editing oauth2 applications type EditOAuth2ApplicationForm struct { Name string `binding:"Required;MaxSize(255)" form:"application_name"` diff --git a/services/forms/user_form_test.go b/services/forms/user_form_test.go index 66050187c9..b4120f20ed 100644 --- a/services/forms/user_form_test.go +++ b/services/forms/user_form_test.go @@ -4,10 +4,8 @@ package forms import ( - "strconv" "testing" - auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/modules/setting" "github.com/gobwas/glob" @@ -104,28 +102,3 @@ func TestRegisterForm_IsDomainAllowed_BlockedEmail(t *testing.T) { assert.Equal(t, v.valid, form.IsEmailDomainAllowed()) } } - -func TestNewAccessTokenForm_GetScope(t *testing.T) { - tests := []struct { - form NewAccessTokenForm - scope auth_model.AccessTokenScope - expectedErr error - }{ - { - form: NewAccessTokenForm{Name: "test", Scope: []string{"read:repository"}}, - scope: "read:repository", - }, - { - form: NewAccessTokenForm{Name: "test", Scope: []string{"read:repository", "write:user"}}, - scope: "read:repository,write:user", - }, - } - - for i, test := range tests { - t.Run(strconv.Itoa(i), func(t *testing.T) { - scope, err := test.form.GetScope() - assert.Equal(t, test.expectedErr, err) - assert.Equal(t, test.scope, scope) - }) - } -} diff --git a/templates/user/settings/applications.tmpl b/templates/user/settings/applications.tmpl index 31d1a2ac5b..501f238c7a 100644 --- a/templates/user/settings/applications.tmpl +++ b/templates/user/settings/applications.tmpl @@ -50,49 +50,41 @@
-
- {{ctx.Locale.Tr "settings.generate_new_token"}} -
-
- {{.CsrfTokenHtml}} -
- - -
-
- - - -
-
- - {{ctx.Locale.Tr "settings.select_permissions"}} - -

- {{ctx.Locale.Tr "settings.access_token_desc" (HTMLFormat `href="%s/api/swagger" target="_blank"` AppSubUrl) (`href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`|SafeHTML)}} -

-
+
+

{{ctx.Locale.Tr "settings.generate_new_token"}}

+ + {{.CsrfTokenHtml}} +
+ +
-
- - {{/* Fomantic ".ui.form .warning.message" is hidden by default, so put the warning message out of the form*/}} -
- {{ctx.Locale.Tr "settings.at_least_one_permission"}} -
+
+
{{ctx.Locale.Tr "settings.repo_and_org_access"}}
+ + +
+
+
{{ctx.Locale.Tr "settings.access_token_desc" (HTMLFormat `href="%s/api/swagger" target="_blank"` AppSubUrl) (`href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`|SafeHTML)}}
+ + {{range $category := .TokenCategories}} + + + + + + + {{end}} +
{{$category}}
+
+ + +
{{if .EnableOAuth2}} diff --git a/templates/user/settings/applications_oauth2_list.tmpl b/templates/user/settings/applications_oauth2_list.tmpl index 61098e118b..418d8e9cfc 100644 --- a/templates/user/settings/applications_oauth2_list.tmpl +++ b/templates/user/settings/applications_oauth2_list.tmpl @@ -48,33 +48,33 @@
-
- {{ctx.Locale.Tr "settings.create_oauth2_application"}} -
-
- {{.CsrfTokenHtml}} -
- - -
-
- - -
-
-
- - +
+

{{ctx.Locale.Tr "settings.create_oauth2_application"}}

+ + {{.CsrfTokenHtml}} +
+ +
-
-
-
- - +
+ +
-
- - +
+
+ + +
+
+
+
+ + +
+
+ + +
diff --git a/tests/integration/api_admin_org_test.go b/tests/integration/api_admin_org_test.go index a29d0ba1d7..b243856127 100644 --- a/tests/integration/api_admin_org_test.go +++ b/tests/integration/api_admin_org_test.go @@ -76,7 +76,7 @@ func TestAPIAdminOrgCreateNotAdmin(t *testing.T) { defer tests.PrepareTestEnv(t)() nonAdminUsername := "user2" session := loginUser(t, nonAdminUsername) - token := getTokenForLoggedInUser(t, session) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll) org := api.CreateOrgOption{ UserName: "user2_org", FullName: "User2's organization", diff --git a/tests/integration/api_admin_test.go b/tests/integration/api_admin_test.go index 66209ee4e0..b42f05fc55 100644 --- a/tests/integration/api_admin_test.go +++ b/tests/integration/api_admin_test.go @@ -76,7 +76,7 @@ func TestAPIAdminDeleteUnauthorizedKey(t *testing.T) { var newPublicKey api.PublicKey DecodeJSON(t, resp, &newPublicKey) - token = getUserToken(t, normalUsername) + token = getUserToken(t, normalUsername, auth_model.AccessTokenScopeAll) req = NewRequestf(t, "DELETE", "/api/v1/admin/users/%s/keys/%d", adminUsername, newPublicKey.ID). AddTokenAuth(token) MakeRequest(t, req, http.StatusForbidden) @@ -139,7 +139,7 @@ func TestAPIListUsersNotLoggedIn(t *testing.T) { func TestAPIListUsersNonAdmin(t *testing.T) { defer tests.PrepareTestEnv(t)() nonAdminUsername := "user2" - token := getUserToken(t, nonAdminUsername) + token := getUserToken(t, nonAdminUsername, auth_model.AccessTokenScopeAll) req := NewRequest(t, "GET", "/api/v1/admin/users"). AddTokenAuth(token) MakeRequest(t, req, http.StatusForbidden) diff --git a/tests/integration/api_helper_for_declarative_test.go b/tests/integration/api_helper_for_declarative_test.go index 96669b46f0..f3a595540f 100644 --- a/tests/integration/api_helper_for_declarative_test.go +++ b/tests/integration/api_helper_for_declarative_test.go @@ -33,6 +33,10 @@ type APITestContext struct { func NewAPITestContext(t *testing.T, username, reponame string, scope ...auth.AccessTokenScope) APITestContext { session := loginUser(t, username) + if len(scope) == 0 { + // FIXME: legacy logic: no scope means all + scope = []auth.AccessTokenScope{auth.AccessTokenScopeAll} + } token := getTokenForLoggedInUser(t, session, scope...) return APITestContext{ Session: session, diff --git a/tests/integration/api_repo_git_blobs_test.go b/tests/integration/api_repo_git_blobs_test.go index 184362e7e3..9c4be31396 100644 --- a/tests/integration/api_repo_git_blobs_test.go +++ b/tests/integration/api_repo_git_blobs_test.go @@ -72,7 +72,7 @@ func TestAPIReposGitBlobs(t *testing.T) { // Login as User4. session = loginUser(t, user4.Name) - token4 := getTokenForLoggedInUser(t, session) + token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll) // Test using org repo "org3/repo3" where user4 is a NOT collaborator req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/d56a3073c1dbb7b15963110a049d50cdb5db99fc?access=%s", org3.Name, repo3.Name, token4) diff --git a/tests/integration/api_repo_git_trees_test.go b/tests/integration/api_repo_git_trees_test.go index 8eec6d8d22..47063d9091 100644 --- a/tests/integration/api_repo_git_trees_test.go +++ b/tests/integration/api_repo_git_trees_test.go @@ -69,7 +69,7 @@ func TestAPIReposGitTrees(t *testing.T) { // Login as User4. session = loginUser(t, user4.Name) - token4 := getTokenForLoggedInUser(t, session) + token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll) // Test using org repo "org3/repo3" where user4 is a NOT collaborator req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/d56a3073c1dbb7b15963110a049d50cdb5db99fc?access=%s", org3.Name, repo3.Name, token4) diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 9e487924d1..2f6b7eae31 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -249,55 +249,19 @@ func loginUserWithPassword(t testing.TB, userName, password string) *TestSession // token has to be unique this counter take care of var tokenCounter int64 -// getTokenForLoggedInUser returns a token for a logged in user. -// The scope is an optional list of snake_case strings like the frontend form fields, -// but without the "scope_" prefix. +// getTokenForLoggedInUser returns a token for a logged-in user. func getTokenForLoggedInUser(t testing.TB, session *TestSession, scopes ...auth.AccessTokenScope) string { t.Helper() - var token string - req := NewRequest(t, "GET", "/user/settings/applications") - resp := session.MakeRequest(t, req, http.StatusOK) - var csrf string - for _, cookie := range resp.Result().Cookies() { - if cookie.Name != "_csrf" { - continue - } - csrf = cookie.Value - break - } - if csrf == "" { - doc := NewHTMLParser(t, resp.Body) - csrf = doc.GetCSRF() - } - assert.NotEmpty(t, csrf) urlValues := url.Values{} - urlValues.Add("_csrf", csrf) + urlValues.Add("_csrf", GetUserCSRFToken(t, session)) urlValues.Add("name", fmt.Sprintf("api-testing-token-%d", atomic.AddInt64(&tokenCounter, 1))) for _, scope := range scopes { - urlValues.Add("scope", string(scope)) + urlValues.Add("scope-dummy", string(scope)) // it only needs to start with "scope-" to be accepted } - req = NewRequestWithURLValues(t, "POST", "/user/settings/applications", urlValues) - resp = session.MakeRequest(t, req, http.StatusSeeOther) - - // Log the flash values on failure - if !assert.Equal(t, []string{"/user/settings/applications"}, resp.Result().Header["Location"]) { - for _, cookie := range resp.Result().Cookies() { - if cookie.Name != gitea_context.CookieNameFlash { - continue - } - flash, _ := url.ParseQuery(cookie.Value) - for key, value := range flash { - t.Logf("Flash %q: %q", key, value) - } - } - } - - req = NewRequest(t, "GET", "/user/settings/applications") - resp = session.MakeRequest(t, req, http.StatusOK) - htmlDoc := NewHTMLParser(t, resp.Body) - token = htmlDoc.doc.Find(".ui.info p").Text() - assert.NotEmpty(t, token) - return token + req := NewRequestWithURLValues(t, "POST", "/user/settings/applications", urlValues) + session.MakeRequest(t, req, http.StatusSeeOther) + flashes := session.GetCookieFlashMessage() + return flashes.InfoMsg } type RequestWrapper struct { diff --git a/web_src/css/index.css b/web_src/css/index.css index ce1a23b245..630aa3c2ef 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -19,7 +19,6 @@ @import "./modules/dimmer.css"; @import "./modules/modal.css"; -@import "./modules/select.css"; @import "./modules/tippy.css"; @import "./modules/breadcrumb.css"; @import "./modules/comment.css"; diff --git a/web_src/css/modules/checkbox.css b/web_src/css/modules/checkbox.css index 0a3a71acaa..f7e61ba360 100644 --- a/web_src/css/modules/checkbox.css +++ b/web_src/css/modules/checkbox.css @@ -119,3 +119,13 @@ input[type="radio"] { .ui.toggle.checkbox input:focus:checked ~ label::before { background: var(--color-primary) !important; } + +label.gt-checkbox { + display: inline-flex; + align-items: center; + gap: 0.25em; +} + +.ui.form .field > label.gt-checkbox { + display: flex; +} diff --git a/web_src/css/modules/select.css b/web_src/css/modules/select.css deleted file mode 100644 index 1d7d749d4a..0000000000 --- a/web_src/css/modules/select.css +++ /dev/null @@ -1,25 +0,0 @@ -.gitea-select { - position: relative; -} - -.gitea-select select { - appearance: none; /* hide default triangle */ -} - -/* ::before and ::after pseudo elements don't work on select elements, - so we need to put it on the parent. */ -.gitea-select::after { - position: absolute; - top: 12px; - right: 8px; - pointer-events: none; - content: ""; - width: 14px; - height: 14px; - mask-size: cover; - -webkit-mask-size: cover; - mask-image: var(--octicon-chevron-right); - -webkit-mask-image: var(--octicon-chevron-right); - transform: rotate(90deg); /* point the chevron down */ - background: currentcolor; -} diff --git a/web_src/js/components/ScopedAccessTokenSelector.vue b/web_src/js/components/ScopedAccessTokenSelector.vue deleted file mode 100644 index 9eaf824035..0000000000 --- a/web_src/js/components/ScopedAccessTokenSelector.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - diff --git a/web_src/js/features/scoped-access-token.ts b/web_src/js/features/scoped-access-token.ts deleted file mode 100644 index c498d4c011..0000000000 --- a/web_src/js/features/scoped-access-token.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {createApp} from 'vue'; - -export async function initScopedAccessTokenCategories() { - const el = document.querySelector('#scoped-access-token-selector'); - if (!el) return; - - const {default: ScopedAccessTokenSelector} = await import(/* webpackChunkName: "scoped-access-token-selector" */'../components/ScopedAccessTokenSelector.vue'); - try { - const View = createApp(ScopedAccessTokenSelector, { - isAdmin: JSON.parse(el.getAttribute('data-is-admin')), - noAccessLabel: el.getAttribute('data-no-access-label'), - readLabel: el.getAttribute('data-read-label'), - writeLabel: el.getAttribute('data-write-label'), - }); - View.mount(el); - } catch (err) { - console.error('ScopedAccessTokenSelector failed to load', err); - el.textContent = el.getAttribute('data-locale-component-failed-to-load'); - } -} diff --git a/web_src/js/index.ts b/web_src/js/index.ts index b89e596047..032e6ffe1b 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -68,7 +68,6 @@ import {initColorPickers} from './features/colorpicker.ts'; import {initAdminSelfCheck} from './features/admin/selfcheck.ts'; import {initOAuth2SettingsDisableCheckbox} from './features/oauth2-settings.ts'; import {initGlobalFetchAction} from './features/common-fetch-action.ts'; -import {initScopedAccessTokenCategories} from './features/scoped-access-token.ts'; import { initFootLanguageMenu, initGlobalDropdown, @@ -209,7 +208,6 @@ onDomReady(() => { initUserSettings, initRepoDiffView, initPdfViewer, - initScopedAccessTokenCategories, initColorPickers, initOAuth2SettingsDisableCheckbox, From 7a8eed13b9113850cbb7845b44c193594c7ad6d1 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Fri, 28 Feb 2025 00:32:56 +0000 Subject: [PATCH 06/62] [skip ci] Updated translations via Crowdin --- options/locale/locale_cs-CZ.ini | 1 - options/locale/locale_de-DE.ini | 1 - options/locale/locale_el-GR.ini | 1 - options/locale/locale_es-ES.ini | 1 - options/locale/locale_fr-FR.ini | 1 - options/locale/locale_ga-IE.ini | 1 - options/locale/locale_ja-JP.ini | 1 - options/locale/locale_lv-LV.ini | 1 - options/locale/locale_pt-BR.ini | 1 - options/locale/locale_pt-PT.ini | 1 - options/locale/locale_ru-RU.ini | 1 - options/locale/locale_tr-TR.ini | 1 - options/locale/locale_zh-CN.ini | 1 - options/locale/locale_zh-TW.ini | 1 - 14 files changed, 14 deletions(-) diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 3f2ac68802..dbe0ebfab2 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -911,7 +911,6 @@ delete_token_success=Token byl odstraněn. Aplikace, které jej používají ji repo_and_org_access=Repozitář a přístup organizace permissions_public_only=Pouze veřejnost permissions_access_all=Vše (veřejné, soukromé a omezené) -select_permissions=Vyberte oprávnění permission_not_set=Není nastaveno permission_no_access=Bez přístupu permission_read=Přečtené diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index f1eada3990..d58523c71e 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -910,7 +910,6 @@ delete_token_success=Der Zugriffstoken wurde gelöscht. Anwendungen die diesen T repo_and_org_access=Repository- und Organisationszugriff permissions_public_only=Nur öffentlich permissions_access_all=Alle (öffentlich, privat und begrenzt) -select_permissions=Berechtigungen auswählen permission_not_set=Nicht festgelegt permission_no_access=Kein Zugriff permission_read=Lesen diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index 7fb4151f17..490df97937 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -810,7 +810,6 @@ delete_token_success=Το διακριτικό έχει διαγραφεί. Οι repo_and_org_access=Πρόσβαση στο Αποθετήριο και Οργανισμό permissions_public_only=Δημόσια μόνο permissions_access_all=Όλα (δημόσια, ιδιωτικά, και περιορισμένα) -select_permissions=Επιλέξτε δικαιώματα permission_no_access=Καμία Πρόσβαση permission_read=Αναγνωσμένες permission_write=Ανάγνωση και Εγγραφή diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index c399b1209c..431c626ea7 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -806,7 +806,6 @@ delete_token_success=El token ha sido eliminado. Las aplicaciones que lo usen ya repo_and_org_access=Acceso al Repositorio y a la Organización permissions_public_only=Sólo público permissions_access_all=Todo (público, privado y limitado) -select_permissions=Seleccionar permisos permission_no_access=Sin acceso permission_read=Leídas permission_write=Lectura y Escritura diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index 87e878ebef..f64fae3982 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -917,7 +917,6 @@ delete_token_success=Ce jeton a été supprimé. Les applications l'utilisant n' repo_and_org_access=Accès aux Organisations et Dépôts permissions_public_only=Publique uniquement permissions_access_all=Tout (public, privé et limité) -select_permissions=Sélectionner les autorisations permission_not_set=Non défini permission_no_access=Aucun accès permission_read=Lecture diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index 6c519c6340..f3a1d5722d 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -917,7 +917,6 @@ delete_token_success=Tá an comhartha scriosta. Níl rochtain ag iarratais a ús repo_and_org_access=Rochtain Stórála agus Eagraíochta permissions_public_only=Poiblí amháin permissions_access_all=Gach (poiblí, príobháideach agus teoranta) -select_permissions=Roghnaigh ceadanna permission_not_set=Níl leagtha permission_no_access=Gan rochtain permission_read=Léigh diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index ed2a16cf90..5aa27247b5 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -917,7 +917,6 @@ delete_token_success=トークンを削除しました。 削除したトーク repo_and_org_access=リポジトリと組織へのアクセス permissions_public_only=公開のみ permissions_access_all=すべて (公開、プライベート、限定) -select_permissions=許可の選択 permission_not_set=設定なし permission_no_access=アクセス不可 permission_read=読み取り diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index d2df0813ae..56246c016e 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -814,7 +814,6 @@ delete_token_success=Pilnvara tika izdzēsta. Lietojumprogrammām, kas izmantoja repo_and_org_access=Repozitorija un organizācijas piekļuve permissions_public_only=Tikai publiskie permissions_access_all=Visi (publiskie, privātie un ierobežotie) -select_permissions=Norādiet tiesības permission_no_access=Nav piekļuves permission_read=Skatīšanās permission_write=Skatīšanās un raksīšanas diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 33aad76023..3df70ab7c6 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -812,7 +812,6 @@ delete_token_success=O token foi excluído. Os aplicativos que o utilizam já n repo_and_org_access=Acesso ao Repositório e Organização permissions_public_only=Apenas público permissions_access_all=Todos (público, privado e limitado) -select_permissions=Selecionar permissões permission_no_access=Sem acesso permission_read=Ler permission_write=Ler e escrever diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 9f5be0a4ef..82c7705dcf 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -917,7 +917,6 @@ delete_token_success=O código foi eliminado. Aplicações que o usavam deixaram repo_and_org_access=Acesso aos repositórios e às organizações permissions_public_only=Apenas público permissions_access_all=Tudo (público, privado e limitado) -select_permissions=Escolher permissões permission_not_set=Não definido permission_no_access=Sem acesso permission_read=Lidas diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index 0aa776b78a..b5784dc9d5 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -807,7 +807,6 @@ delete_token_success=Токен удалён. Приложения, исполь repo_and_org_access=Доступ к репозиторию и организации permissions_public_only=Только публичные permissions_access_all=Все (публичные, приватные и ограниченные) -select_permissions=Выбрать разрешения permission_no_access=Нет доступа permission_read=Прочитанные permission_write=Чтение и запись diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index 0454512402..5064d40864 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -895,7 +895,6 @@ delete_token_success=Jeton silindi. Onu kullanan uygulamalar artık hesabınıza repo_and_org_access=Depo ve Organizasyon Erişimi permissions_public_only=Yalnızca herkese açık permissions_access_all=Tümü (herkese açık, özel ve sınırlı) -select_permissions=İzinleri seçin permission_not_set=Ayarlanmadı permission_no_access=Erişim Yok permission_read=Okunmuş diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 3b6aca4e92..72170189ec 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -910,7 +910,6 @@ delete_token_success=令牌已经被删除。使用该令牌的应用将不再 repo_and_org_access=仓库和组织访问权限 permissions_public_only=仅公开 permissions_access_all=全部(公开、私有和受限) -select_permissions=选择权限 permission_not_set=未设置 permission_no_access=无访问权限 permission_read=可读 diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index 737f183f73..67d3a0fcf6 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -907,7 +907,6 @@ delete_token_success=已刪除 Token。使用此 Token 的應用程式無法再 repo_and_org_access=儲存庫和組織存取 permissions_public_only=僅公開 permissions_access_all=全部 (公開、私有與受限) -select_permissions=選擇權限 permission_not_set=尚未設定 permission_no_access=沒有權限 permission_read=讀取 From aba96f65cd687868c6eff5a079cfc7b25740642d Mon Sep 17 00:00:00 2001 From: Alexander McRae Date: Thu, 27 Feb 2025 16:58:25 -0800 Subject: [PATCH 07/62] Use `git diff-tree` for `DiffFileTree` on diff pages (#33514) Modify Diff View FileTree to show all files ## Changes * removes Show Status button on diff * uses `git diff-tree` to generate the file tree for the diff * doesn't reload the diff tree each time we load more files in the preview * selecting and unloaded file will keep loading until that file is loaded * removes `DiffFileList.vue` and "Show Stats" in diff options ## Open Questions * selecting and unloaded file will keep loading until that file is loaded. Is this behaviour okay? It matches what github does. ### Demo In this demo I set `git.MAX_GIT_DIFF_FILES=1` in my `app.ini` to demonstrate a worst case example. In most cases the behaviour isn't nearly as jarring as we load a bunch of files at a time. https://github.com/user-attachments/assets/72f29663-d6fc-472d-94fa-7fb5950c2836 --------- Co-authored-by: silverwind Co-authored-by: wxiaoguang --- options/locale/locale_en-US.ini | 1 - routers/web/repo/commit.go | 12 +++ routers/web/repo/compare.go | 10 +++ routers/web/repo/pull.go | 23 ++++++ routers/web/repo/treelist.go | 32 ++++++++ templates/repo/diff/box.tmpl | 26 ------- templates/repo/diff/options_dropdown.tmpl | 1 - web_src/js/components/DiffFileList.vue | 60 --------------- web_src/js/components/DiffFileTree.vue | 72 +----------------- web_src/js/components/DiffFileTreeItem.vue | 67 ++++++++--------- web_src/js/features/repo-diff-filetree.ts | 9 --- web_src/js/features/repo-diff.ts | 69 +++++++++++------ web_src/js/modules/stores.ts | 9 ++- web_src/js/utils.test.ts | 8 +- web_src/js/utils.ts | 6 ++ web_src/js/utils/filetree.test.ts | 86 ++++++++++++++++++++++ web_src/js/utils/filetree.ts | 85 +++++++++++++++++++++ 17 files changed, 352 insertions(+), 224 deletions(-) delete mode 100644 web_src/js/components/DiffFileList.vue create mode 100644 web_src/js/utils/filetree.test.ts create mode 100644 web_src/js/utils/filetree.ts diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 5aa6d19dae..1da2fb61ad 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2594,7 +2594,6 @@ diff.commit = commit diff.git-notes = Notes diff.data_not_available = Diff Content Not Available diff.options_button = Diff Options -diff.show_diff_stats = Show Stats diff.download_patch = Download Patch File diff.download_diff = Download Diff File diff.show_split_view = Split View diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 9c12ef9297..2728eda8b6 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -344,18 +344,30 @@ func Diff(ctx *context.Context) { ctx.Data["Reponame"] = repoName var parentCommit *git.Commit + var parentCommitID string if commit.ParentCount() > 0 { parentCommit, err = gitRepo.GetCommit(parents[0]) if err != nil { ctx.NotFound(err) return } + parentCommitID = parentCommit.ID.String() } setCompareContext(ctx, parentCommit, commit, userName, repoName) ctx.Data["Title"] = commit.Summary() + " · " + base.ShortSha(commitID) ctx.Data["Commit"] = commit ctx.Data["Diff"] = diff + if !fileOnly { + diffTree, err := gitdiff.GetDiffTree(ctx, gitRepo, false, parentCommitID, commitID) + if err != nil { + ctx.ServerError("GetDiffTree", err) + return + } + + ctx.PageData["DiffFiles"] = transformDiffTreeForUI(diffTree, nil) + } + statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll) if err != nil { log.Error("GetLatestCommitStatus: %v", err) diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 71bce759a9..5165636a52 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -633,6 +633,16 @@ func PrepareCompareDiff( ctx.Data["Diff"] = diff ctx.Data["DiffNotAvailable"] = diff.NumFiles == 0 + if !fileOnly { + diffTree, err := gitdiff.GetDiffTree(ctx, ci.HeadGitRepo, false, beforeCommitID, headCommitID) + if err != nil { + ctx.ServerError("GetDiffTree", err) + return false + } + + ctx.PageData["DiffFiles"] = transformDiffTreeForUI(diffTree, nil) + } + headCommit, err := ci.HeadGitRepo.GetCommit(headCommitID) if err != nil { ctx.ServerError("GetCommit", err) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 223f8d017e..0769f456ec 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -761,6 +761,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi var methodWithError string var diff *gitdiff.Diff + shouldGetUserSpecificDiff := false // if we're not logged in or only a single commit (or commit range) is shown we // have to load only the diff and not get the viewed information @@ -772,6 +773,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi } else { diff, err = gitdiff.SyncAndGetUserSpecificDiff(ctx, ctx.Doer.ID, pull, gitRepo, diffOptions, files...) methodWithError = "SyncAndGetUserSpecificDiff" + shouldGetUserSpecificDiff = true } if err != nil { ctx.ServerError(methodWithError, err) @@ -816,6 +818,27 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi } } + if !fileOnly { + // note: use mergeBase is set to false because we already have the merge base from the pull request info + diffTree, err := gitdiff.GetDiffTree(ctx, gitRepo, false, pull.MergeBase, headCommitID) + if err != nil { + ctx.ServerError("GetDiffTree", err) + return + } + + filesViewedState := make(map[string]pull_model.ViewedState) + if shouldGetUserSpecificDiff { + // This sort of sucks because we already fetch this when getting the diff + review, err := pull_model.GetNewestReviewState(ctx, ctx.Doer.ID, issue.ID) + if err == nil && review != nil && review.UpdatedFiles != nil { + // If there wasn't an error and we have a review with updated files, use that + filesViewedState = review.UpdatedFiles + } + } + + ctx.PageData["DiffFiles"] = transformDiffTreeForUI(diffTree, filesViewedState) + } + ctx.Data["Diff"] = diff ctx.Data["DiffNotAvailable"] = diff.NumFiles == 0 diff --git a/routers/web/repo/treelist.go b/routers/web/repo/treelist.go index d11af4669f..9ce9f8424d 100644 --- a/routers/web/repo/treelist.go +++ b/routers/web/repo/treelist.go @@ -6,9 +6,11 @@ package repo import ( "net/http" + pull_model "code.gitea.io/gitea/models/pull" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/gitdiff" "github.com/go-enry/go-enry/v2" ) @@ -52,3 +54,33 @@ func isExcludedEntry(entry *git.TreeEntry) bool { return false } + +type FileDiffFile struct { + Name string + NameHash string + IsSubmodule bool + IsViewed bool + Status string +} + +// transformDiffTreeForUI transforms a DiffTree into a slice of FileDiffFile for UI rendering +// it also takes a map of file names to their viewed state, which is used to mark files as viewed +func transformDiffTreeForUI(diffTree *gitdiff.DiffTree, filesViewedState map[string]pull_model.ViewedState) []FileDiffFile { + files := make([]FileDiffFile, 0, len(diffTree.Files)) + + for _, file := range diffTree.Files { + nameHash := git.HashFilePathForWebUI(file.HeadPath) + isSubmodule := file.HeadMode == git.EntryModeCommit + isViewed := filesViewedState[file.HeadPath] == pull_model.Viewed + + files = append(files, FileDiffFile{ + Name: file.HeadPath, + NameHash: nameHash, + IsSubmodule: isSubmodule, + IsViewed: isViewed, + Status: file.Status, + }) + } + + return files +} diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index a3b64b8a11..8de54b932a 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -57,32 +57,6 @@
{{end}} - -
{{end}}
{{if $showFileTree}} diff --git a/templates/repo/diff/options_dropdown.tmpl b/templates/repo/diff/options_dropdown.tmpl index 09b7b80e41..8d08e7ad46 100644 --- a/templates/repo/diff/options_dropdown.tmpl +++ b/templates/repo/diff/options_dropdown.tmpl @@ -1,7 +1,6 @@