diff --git a/.drone.yml b/.drone.yml index 7150629028e3..cf0caf7612f6 100644 --- a/.drone.yml +++ b/.drone.yml @@ -12,6 +12,9 @@ trigger: - push - tag - pull_request + paths: + exclude: + - docs/** volumes: - name: deps @@ -112,7 +115,6 @@ steps: image: golang:1.19 # this step is kept as the lowest version of golang that we support pull: always environment: - GO111MODULE: on GOPROXY: https://goproxy.io commands: - go build -o gitea_no_gcc # test if build succeeds without the sqlite tag @@ -124,7 +126,6 @@ steps: - name: build-backend-arm64 image: golang:1.20 environment: - GO111MODULE: on GOPROXY: https://goproxy.io GOOS: linux GOARCH: arm64 @@ -140,7 +141,6 @@ steps: - name: build-backend-windows image: golang:1.20 environment: - GO111MODULE: on GOPROXY: https://goproxy.io GOOS: windows GOARCH: amd64 @@ -155,7 +155,6 @@ steps: - name: build-backend-386 image: golang:1.20 environment: - GO111MODULE: on GOPROXY: https://goproxy.io GOOS: linux GOARCH: 386 @@ -183,6 +182,9 @@ trigger: - push - tag - pull_request + paths: + exclude: + - docs/** volumes: - name: deps @@ -410,6 +412,9 @@ trigger: - push - tag - pull_request + paths: + exclude: + - docs/** volumes: - name: deps @@ -517,6 +522,9 @@ depends_on: trigger: event: - pull_request + paths: + exclude: + - docs/** volumes: - name: deps @@ -696,6 +704,9 @@ trigger: - "release/*" event: - push + paths: + exclude: + - docs/** depends_on: - testing-amd64 @@ -947,6 +958,9 @@ trigger: - push - tag - pull_request + paths: + include: + - docs/** steps: - name: build-docs @@ -1241,6 +1255,9 @@ depends_on: trigger: ref: - "refs/pull/**" + paths: + exclude: + - docs/** steps: - name: dryrun diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3a12bb8f721e..b752abb794de 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,9 +1,9 @@ - Please check the following: - -1. Make sure you are targeting the `main` branch, pull requests on release branches are only allowed for bug fixes. -2. Read contributing guidelines: https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md -3. Describe what your pull request does and which issue you're targeting (if any) - ---> +1. Make sure you are targeting the `main` branch, pull requests on release branches are only allowed for backports. +2. Make sure you have read contributing guidelines: https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md . +3. Describe what your pull request does and which issue you're targeting (if any). +4. It is recommended to enable "Allow edits by maintainers", so maintainers can help more easily. +5. Your input here will be included in the commit message when this PR has been merged. If you don't want some content to be included, please separate them with a line like `---`. +6. Delete all these tips before posting. + diff --git a/CHANGELOG.md b/CHANGELOG.md index 18b52673a026..99a67884af53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,43 @@ This changelog goes through all the changes that have been made in each release without substantial changes to our git log; to see the highlights of what has been added to each release, please refer to the [blog](https://blog.gitea.io). +## [1.18.4](https://github.com/go-gitea/gitea/releases/tag/1.18.4) - 2023-02-20 + +* SECURITY + * Provide the ability to set password hash algorithm parameters (#22942) (#22943) + * Add command to bulk set must-change-password (#22823) (#22928) +* ENHANCEMENTS + * Use import of OCI structs (#22765) (#22805) + * Fix color of tertiary button on dark theme (#22739) (#22744) + * Link issue and pull requests status change in UI notifications directly to their event in the timelined view. (#22627) (#22642) +* BUGFIXES + * Notify on container image create (#22806) (#22965) + * Fix blame view missing lines (#22826) (#22929) + * Fix incorrect role labels for migrated issues and comments (#22914) (#22923) + * Fix PR file tree folders no longer collapsing (#22864) (#22872) + * Escape filename when assemble URL (#22850) (#22871) + * Fix isAllowed of escapeStreamer (#22814) (#22837) + * Load issue before accessing index in merge message (#22822) (#22830) + * Improve trace logging for pulls and processes (#22633) (#22812) + * Fix restore repo bug, clarify the problem of ForeignIndex (#22776) (#22794) + * Add default user visibility to cli command "admin user create" (#22750) (#22760) + * Escape path for the file list (#22741) (#22757) + * Fix bugs with WebAuthn preventing sign in and registration. (#22651) (#22721) + * Add missing close bracket in imagediff (#22710) (#22712) + * Move code comments to a standalone file and fix the bug when adding a reply to an outdated review appears to not post(#20821) (#22707) + * Fix line spacing for plaintext previews (#22699) (#22701) + * Fix wrong hint when deleting a branch successfully from pull request UI (#22673) (#22698) + * Fix README TOC links (#22577) (#22677) + * Fix missing message in git hook when pull requests disabled on fork (#22625) (#22658) + * Improve checkIfPRContentChanged (#22611) (#22644) + * Prevent duplicate labels when importing more than 99 (#22591) (#22598) + * Don't return duplicated users who can create org repo (#22560) (#22562) +* BUILD + * Upgrade golangcilint to v1.51.0 (#22764) +* MISC + * Use proxy for pull mirror (#22771) (#22772) + * Use `--index-url` in PyPi description (#22620) (#22636) + ## [1.18.3](https://github.com/go-gitea/gitea/releases/tag/v1.18.3) - 2023-01-23 * SECURITY diff --git a/MAINTAINERS b/MAINTAINERS index a37d336de3a4..fcd235e52a57 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -47,3 +47,5 @@ Wim (@42wim) Xinyu Zhou (@xin-u) Jason Song (@wolfogre) Yarden Shoham (@yardenshoham) +Yu Tian (@Zettat123) +Eddie Yang <576951401@qq.com> (@yp05327) diff --git a/go.mod b/go.mod index 3dc050b99a17..343a70da25a0 100644 --- a/go.mod +++ b/go.mod @@ -103,10 +103,10 @@ require ( github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87 github.com/yuin/goldmark-meta v1.1.0 golang.org/x/crypto v0.4.0 - golang.org/x/net v0.4.0 + golang.org/x/net v0.7.0 golang.org/x/oauth2 v0.3.0 - golang.org/x/sys v0.3.0 - golang.org/x/text v0.5.0 + golang.org/x/sys v0.5.0 + golang.org/x/text v0.7.0 golang.org/x/tools v0.1.12 google.golang.org/grpc v1.47.0 google.golang.org/protobuf v1.28.1 diff --git a/go.sum b/go.sum index 03c2cc18c66e..7eb420b02b03 100644 --- a/go.sum +++ b/go.sum @@ -1463,8 +1463,8 @@ golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= -golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1609,14 +1609,15 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= +golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1627,8 +1628,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/models/auth/token_scope.go b/models/auth/token_scope.go index c61c306496b4..38733a1c8f30 100644 --- a/models/auth/token_scope.go +++ b/models/auth/token_scope.go @@ -168,10 +168,23 @@ var allAccessTokenScopeBits = map[AccessTokenScope]AccessTokenScopeBitmap{ // Parse parses the scope string into a bitmap, thus removing possible duplicates. func (s AccessTokenScope) Parse() (AccessTokenScopeBitmap, error) { - list := strings.Split(string(s), ",") - var bitmap AccessTokenScopeBitmap - for _, v := range list { + + // The following is the more performant equivalent of 'for _, v := range strings.Split(remainingScope, ",")' as this is hot code + remainingScopes := string(s) + for len(remainingScopes) > 0 { + i := strings.IndexByte(remainingScopes, ',') + var v string + if i < 0 { + v = remainingScopes + remainingScopes = "" + } else if i+1 >= len(remainingScopes) { + v = remainingScopes[:i] + remainingScopes = "" + } else { + v = remainingScopes[:i] + remainingScopes = remainingScopes[i+1:] + } singleScope := AccessTokenScope(v) if singleScope == "" { continue @@ -187,9 +200,15 @@ func (s AccessTokenScope) Parse() (AccessTokenScopeBitmap, error) { } bitmap |= bits } + return bitmap, nil } +// StringSlice returns the AccessTokenScope as a []string +func (s AccessTokenScope) StringSlice() []string { + return strings.Split(string(s), ",") +} + // Normalize returns a normalized scope string without any duplicates. func (s AccessTokenScope) Normalize() (AccessTokenScope, error) { bitmap, err := s.Parse() diff --git a/models/db/iterate_test.go b/models/db/iterate_test.go index 63487afa49b1..a713fe0d8b85 100644 --- a/models/db/iterate_test.go +++ b/models/db/iterate_test.go @@ -25,7 +25,7 @@ func TestIterate(t *testing.T) { return nil }) assert.NoError(t, err) - assert.EqualValues(t, 81, repoCnt) + assert.EqualValues(t, 83, repoCnt) err = db.Iterate(db.DefaultContext, nil, func(ctx context.Context, repoUnit *repo_model.RepoUnit) error { reopUnit2 := repo_model.RepoUnit{ID: repoUnit.ID} diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index 8706717ad46e..503b8c9ddfa4 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -556,3 +556,16 @@ repo_id: 54 type: 1 created_unix: 946684810 + +- + id: 82 + repo_id: 31 + type: 1 + created_unix: 946684810 + +- + id: 83 + repo_id: 31 + type: 3 + config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}" + created_unix: 946684810 diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index b5f8b27215bb..b1c7fc003067 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -8,8 +8,8 @@ email: user1@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user1 @@ -45,8 +45,8 @@ email: user2@example.com keep_email_private: true email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user2 @@ -82,8 +82,8 @@ email: user3@example.com keep_email_private: false email_notifications_preference: onmention - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user3 @@ -119,8 +119,8 @@ email: user4@example.com keep_email_private: false email_notifications_preference: onmention - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user4 @@ -156,8 +156,8 @@ email: user5@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user5 @@ -193,8 +193,8 @@ email: user6@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user6 @@ -230,8 +230,8 @@ email: user7@example.com keep_email_private: false email_notifications_preference: disabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user7 @@ -267,8 +267,8 @@ email: user8@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user8 @@ -304,8 +304,8 @@ email: user9@example.com keep_email_private: false email_notifications_preference: onmention - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user9 @@ -341,8 +341,8 @@ email: user10@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user10 @@ -378,8 +378,8 @@ email: user11@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user11 @@ -415,8 +415,8 @@ email: user12@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user12 @@ -452,8 +452,8 @@ email: user13@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user13 @@ -489,8 +489,8 @@ email: user14@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user14 @@ -526,8 +526,8 @@ email: user15@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user15 @@ -563,8 +563,8 @@ email: user16@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user16 @@ -600,8 +600,8 @@ email: user17@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user17 @@ -637,8 +637,8 @@ email: user18@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user18 @@ -674,8 +674,8 @@ email: user19@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user19 @@ -711,8 +711,8 @@ email: user20@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user20 @@ -748,8 +748,8 @@ email: user21@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user21 @@ -785,8 +785,8 @@ email: limited_org@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: limited_org @@ -822,8 +822,8 @@ email: privated_org@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: privated_org @@ -859,8 +859,8 @@ email: user24@example.com keep_email_private: true email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user24 @@ -896,8 +896,8 @@ email: org25@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: org25 @@ -933,8 +933,8 @@ email: org26@example.com keep_email_private: false email_notifications_preference: onmention - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: org26 @@ -970,8 +970,8 @@ email: user27@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user27 @@ -1007,8 +1007,8 @@ email: user28@example.com keep_email_private: true email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user28 @@ -1044,8 +1044,8 @@ email: user29@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user29 @@ -1081,8 +1081,8 @@ email: user30@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user30 @@ -1118,8 +1118,8 @@ email: user31@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user31 @@ -1155,8 +1155,8 @@ email: user32@example.com keep_email_private: false email_notifications_preference: enabled - passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f47017 - passwd_hash_algo: argon2 + passwd: ZogKvWdyEx:notpassword + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user32 @@ -1192,8 +1192,8 @@ email: user33@example.com keep_email_private: false email_notifications_preference: enabled - passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 - passwd_hash_algo: pbkdf2$50000$50 + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy must_change_password: false login_source: 0 login_name: user33 diff --git a/models/issues/issue.go b/models/issues/issue.go index 9d7dea01772c..c59e9d14e537 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -251,13 +251,15 @@ func (issue *Issue) LoadPoster(ctx context.Context) (err error) { // LoadPullRequest loads pull request info func (issue *Issue) LoadPullRequest(ctx context.Context) (err error) { - if issue.IsPull && issue.PullRequest == nil { - issue.PullRequest, err = GetPullRequestByIssueID(ctx, issue.ID) - if err != nil { - if IsErrPullRequestNotExist(err) { - return err + if issue.IsPull { + if issue.PullRequest == nil { + issue.PullRequest, err = GetPullRequestByIssueID(ctx, issue.ID) + if err != nil { + if IsErrPullRequestNotExist(err) { + return err + } + return fmt.Errorf("getPullRequestByIssueID [%d]: %w", issue.ID, err) } - return fmt.Errorf("getPullRequestByIssueID [%d]: %w", issue.ID, err) } issue.PullRequest.Issue = issue } @@ -347,7 +349,7 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) { return } - if err = issue.loadProject(ctx); err != nil { + if err = issue.LoadProject(ctx); err != nil { return } diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index c9f4c9f5336b..04d12e055cc5 100644 --- a/models/issues/issue_project.go +++ b/models/issues/issue_project.go @@ -13,11 +13,7 @@ import ( ) // LoadProject load the project the issue was assigned to -func (issue *Issue) LoadProject() (err error) { - return issue.loadProject(db.DefaultContext) -} - -func (issue *Issue) loadProject(ctx context.Context) (err error) { +func (issue *Issue) LoadProject(ctx context.Context) (err error) { if issue.Project == nil { var p project_model.Project if _, err = db.GetEngine(ctx).Table("project"). diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index c7497becd113..989a1d6ae1fb 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -461,6 +461,8 @@ var migrations = []Migration{ NewMigration("Alter gpg_key_import content TEXT field to MEDIUMTEXT", v1_19.AlterPublicGPGKeyImportContentFieldToMediumText), // v243 -> v244 NewMigration("Add exclusive label", v1_19.AddExclusiveLabel), + + // Gitea 1.19.0 ends at v244 } // GetCurrentDBVersion returns the current db version diff --git a/models/unittest/fixtures.go b/models/unittest/fixtures.go index 9fba053825b9..545452a159fb 100644 --- a/models/unittest/fixtures.go +++ b/models/unittest/fixtures.go @@ -9,6 +9,8 @@ import ( "time" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/auth/password/hash" + "code.gitea.io/gitea/modules/setting" "github.com/go-testfixtures/testfixtures/v3" "xorm.io/xorm" @@ -64,6 +66,11 @@ func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) { return err } + // register the dummy hash algorithm function used in the test fixtures + _ = hash.Register("dummy", hash.NewDummyHasher) + + setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy") + return err } @@ -115,5 +122,8 @@ func LoadFixtures(engine ...*xorm.Engine) error { } } } + _ = hash.Register("dummy", hash.NewDummyHasher) + setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy") + return err } diff --git a/modules/auth/password/hash/argon2.go b/modules/auth/password/hash/argon2.go index 14c16b53c428..0cd6472fa127 100644 --- a/modules/auth/password/hash/argon2.go +++ b/modules/auth/password/hash/argon2.go @@ -13,7 +13,7 @@ import ( ) func init() { - Register("argon2", NewArgon2Hasher) + MustRegister("argon2", NewArgon2Hasher) } // Argon2Hasher implements PasswordHasher diff --git a/modules/auth/password/hash/bcrypt.go b/modules/auth/password/hash/bcrypt.go index ddf5420408f3..4607c169cd85 100644 --- a/modules/auth/password/hash/bcrypt.go +++ b/modules/auth/password/hash/bcrypt.go @@ -8,7 +8,7 @@ import ( ) func init() { - Register("bcrypt", NewBcryptHasher) + MustRegister("bcrypt", NewBcryptHasher) } // BcryptHasher implements PasswordHasher diff --git a/modules/auth/password/hash/dummy.go b/modules/auth/password/hash/dummy.go new file mode 100644 index 000000000000..22f2e2f648e7 --- /dev/null +++ b/modules/auth/password/hash/dummy.go @@ -0,0 +1,33 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package hash + +import ( + "encoding/hex" +) + +// DummyHasher implements PasswordHasher and is a dummy hasher that simply +// puts the password in place with its salt +// This SHOULD NOT be used in production and is provided to make the integration +// tests faster only +type DummyHasher struct{} + +// HashWithSaltBytes a provided password and salt +func (hasher *DummyHasher) HashWithSaltBytes(password string, salt []byte) string { + if hasher == nil { + return "" + } + + if len(salt) == 10 { + return string(salt) + ":" + password + } + + return hex.EncodeToString(salt) + ":" + password +} + +// NewDummyHasher is a factory method to create a DummyHasher +// Any provided configuration is ignored +func NewDummyHasher(_ string) *DummyHasher { + return &DummyHasher{} +} diff --git a/modules/auth/password/hash/dummy_test.go b/modules/auth/password/hash/dummy_test.go new file mode 100644 index 000000000000..f3b36df62504 --- /dev/null +++ b/modules/auth/password/hash/dummy_test.go @@ -0,0 +1,25 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package hash + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDummyHasher(t *testing.T) { + dummy := &PasswordHashAlgorithm{ + PasswordSaltHasher: NewDummyHasher(""), + Specification: "dummy", + } + + password, salt := "password", "ZogKvWdyEx" + + hash, err := dummy.Hash(password, salt) + assert.Nil(t, err) + assert.Equal(t, hash, salt+":"+password) + + assert.True(t, dummy.VerifyPassword(password, hash, salt)) +} diff --git a/modules/auth/password/hash/hash.go b/modules/auth/password/hash/hash.go index 3572dd46d477..459320e1b022 100644 --- a/modules/auth/password/hash/hash.go +++ b/modules/auth/password/hash/hash.go @@ -83,17 +83,26 @@ var ( availableHasherFactories = map[string]func(string) PasswordSaltHasher{} ) +// MustRegister registers a PasswordSaltHasher with the availableHasherFactories +// Caution: This is not thread safe. +func MustRegister[T PasswordSaltHasher](name string, newFn func(config string) T) { + if err := Register(name, newFn); err != nil { + panic(err) + } +} + // Register registers a PasswordSaltHasher with the availableHasherFactories // Caution: This is not thread safe. -func Register[T PasswordSaltHasher](name string, newFn func(config string) T) { +func Register[T PasswordSaltHasher](name string, newFn func(config string) T) error { if _, has := availableHasherFactories[name]; has { - panic(fmt.Errorf("duplicate registration of password salt hasher: %s", name)) + return fmt.Errorf("duplicate registration of password salt hasher: %s", name) } availableHasherFactories[name] = func(config string) PasswordSaltHasher { n := newFn(config) return n } + return nil } // In early versions of gitea the password hash algorithm field of a user could be diff --git a/modules/auth/password/hash/hash_test.go b/modules/auth/password/hash/hash_test.go index 593c8386a355..7aa051733f82 100644 --- a/modules/auth/password/hash/hash_test.go +++ b/modules/auth/password/hash/hash_test.go @@ -19,16 +19,20 @@ func (t testSaltHasher) HashWithSaltBytes(password string, salt []byte) string { } func Test_registerHasher(t *testing.T) { - Register("Test_registerHasher", func(config string) testSaltHasher { + MustRegister("Test_registerHasher", func(config string) testSaltHasher { return testSaltHasher(config) }) assert.Panics(t, func() { - Register("Test_registerHasher", func(config string) testSaltHasher { + MustRegister("Test_registerHasher", func(config string) testSaltHasher { return testSaltHasher(config) }) }) + assert.Error(t, Register("Test_registerHasher", func(config string) testSaltHasher { + return testSaltHasher(config) + })) + assert.Equal(t, "password$salt$", Parse("Test_registerHasher").PasswordSaltHasher.HashWithSaltBytes("password", []byte("salt"))) diff --git a/modules/auth/password/hash/pbkdf2.go b/modules/auth/password/hash/pbkdf2.go index be3121318e3e..27382fedb8cb 100644 --- a/modules/auth/password/hash/pbkdf2.go +++ b/modules/auth/password/hash/pbkdf2.go @@ -14,7 +14,7 @@ import ( ) func init() { - Register("pbkdf2", NewPBKDF2Hasher) + MustRegister("pbkdf2", NewPBKDF2Hasher) } // PBKDF2Hasher implements PasswordHasher diff --git a/modules/auth/password/hash/scrypt.go b/modules/auth/password/hash/scrypt.go index e77434fc32b2..f3d38f751a3b 100644 --- a/modules/auth/password/hash/scrypt.go +++ b/modules/auth/password/hash/scrypt.go @@ -13,7 +13,7 @@ import ( ) func init() { - Register("scrypt", NewScryptHasher) + MustRegister("scrypt", NewScryptHasher) } // ScryptHasher implements PasswordHasher diff --git a/modules/context/access_log.go b/modules/context/access_log.go index 84663ee8d3b0..1aaba9dc2d5c 100644 --- a/modules/context/access_log.go +++ b/modules/context/access_log.go @@ -6,8 +6,8 @@ package context import ( "bytes" "context" - "html/template" "net/http" + "text/template" "time" "code.gitea.io/gitea/modules/log" diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go index 67a4e4ded1a1..0244a8d06ed6 100644 --- a/modules/setting/config_provider.go +++ b/modules/setting/config_provider.go @@ -25,9 +25,9 @@ func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting interfac } } -func deprecatedSetting(rootCfg ConfigProvider, oldSection, oldKey, newSection, newKey string) { +func deprecatedSetting(rootCfg ConfigProvider, oldSection, oldKey, newSection, newKey, version string) { if rootCfg.Section(oldSection).HasKey(oldKey) { - log.Error("Deprecated fallback `[%s]` `%s` present. Use `[%s]` `%s` instead. This fallback will be removed in v1.19.0", oldSection, oldKey, newSection, newKey) + log.Error("Deprecated fallback `[%s]` `%s` present. Use `[%s]` `%s` instead. This fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version) } } diff --git a/modules/setting/indexer.go b/modules/setting/indexer.go index 528a9eb65561..5b10018eb7b2 100644 --- a/modules/setting/indexer.go +++ b/modules/setting/indexer.go @@ -56,12 +56,13 @@ func loadIndexerFrom(rootCfg ConfigProvider) { Indexer.IssueIndexerName = sec.Key("ISSUE_INDEXER_NAME").MustString(Indexer.IssueIndexerName) // The following settings are deprecated and can be overridden by settings in [queue] or [queue.issue_indexer] - // FIXME: DEPRECATED to be removed in v1.18.0 - deprecatedSetting(rootCfg, "indexer", "ISSUE_INDEXER_QUEUE_TYPE", "queue.issue_indexer", "TYPE") - deprecatedSetting(rootCfg, "indexer", "ISSUE_INDEXER_QUEUE_DIR", "queue.issue_indexer", "DATADIR") - deprecatedSetting(rootCfg, "indexer", "ISSUE_INDEXER_QUEUE_CONN_STR", "queue.issue_indexer", "CONN_STR") - deprecatedSetting(rootCfg, "indexer", "ISSUE_INDEXER_QUEUE_BATCH_NUMBER", "queue.issue_indexer", "BATCH_LENGTH") - deprecatedSetting(rootCfg, "indexer", "UPDATE_BUFFER_LEN", "queue.issue_indexer", "LENGTH") + // DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version + // if these are removed, the warning will not be shown + deprecatedSetting(rootCfg, "indexer", "ISSUE_INDEXER_QUEUE_TYPE", "queue.issue_indexer", "TYPE", "v1.19.0") + deprecatedSetting(rootCfg, "indexer", "ISSUE_INDEXER_QUEUE_DIR", "queue.issue_indexer", "DATADIR", "v1.19.0") + deprecatedSetting(rootCfg, "indexer", "ISSUE_INDEXER_QUEUE_CONN_STR", "queue.issue_indexer", "CONN_STR", "v1.19.0") + deprecatedSetting(rootCfg, "indexer", "ISSUE_INDEXER_QUEUE_BATCH_NUMBER", "queue.issue_indexer", "BATCH_LENGTH", "v1.19.0") + deprecatedSetting(rootCfg, "indexer", "UPDATE_BUFFER_LEN", "queue.issue_indexer", "LENGTH", "v1.19.0") Indexer.RepoIndexerEnabled = sec.Key("REPO_INDEXER_ENABLED").MustBool(false) Indexer.RepoType = sec.Key("REPO_INDEXER_TYPE").MustString("bleve") diff --git a/modules/setting/lfs.go b/modules/setting/lfs.go index e6c9e42f2cfb..e04cde100b68 100644 --- a/modules/setting/lfs.go +++ b/modules/setting/lfs.go @@ -35,8 +35,9 @@ func loadLFSFrom(rootCfg ConfigProvider) { storageType := lfsSec.Key("STORAGE_TYPE").MustString("") // Specifically default PATH to LFS_CONTENT_PATH - // FIXME: DEPRECATED to be removed in v1.18.0 - deprecatedSetting(rootCfg, "server", "LFS_CONTENT_PATH", "lfs", "PATH") + // DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version + // if these are removed, the warning will not be shown + deprecatedSetting(rootCfg, "server", "LFS_CONTENT_PATH", "lfs", "PATH", "v1.19.0") lfsSec.Key("PATH").MustString( sec.Key("LFS_CONTENT_PATH").String()) diff --git a/modules/setting/mailer.go b/modules/setting/mailer.go index 62a73cb2f348..39afce7d4645 100644 --- a/modules/setting/mailer.go +++ b/modules/setting/mailer.go @@ -64,16 +64,16 @@ func loadMailerFrom(rootCfg ConfigProvider) { } // Handle Deprecations and map on to new configuration - // FIXME: DEPRECATED to be removed in v1.19.0 - deprecatedSetting(rootCfg, "mailer", "MAILER_TYPE", "mailer", "PROTOCOL") + // DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version + // if these are removed, the warning will not be shown + deprecatedSetting(rootCfg, "mailer", "MAILER_TYPE", "mailer", "PROTOCOL", "v1.19.0") if sec.HasKey("MAILER_TYPE") && !sec.HasKey("PROTOCOL") { if sec.Key("MAILER_TYPE").String() == "sendmail" { sec.Key("PROTOCOL").MustString("sendmail") } } - // FIXME: DEPRECATED to be removed in v1.19.0 - deprecatedSetting(rootCfg, "mailer", "HOST", "mailer", "SMTP_ADDR") + deprecatedSetting(rootCfg, "mailer", "HOST", "mailer", "SMTP_ADDR", "v1.19.0") if sec.HasKey("HOST") && !sec.HasKey("SMTP_ADDR") { givenHost := sec.Key("HOST").String() addr, port, err := net.SplitHostPort(givenHost) @@ -89,8 +89,7 @@ func loadMailerFrom(rootCfg ConfigProvider) { sec.Key("SMTP_PORT").MustString(port) } - // FIXME: DEPRECATED to be removed in v1.19.0 - deprecatedSetting(rootCfg, "mailer", "IS_TLS_ENABLED", "mailer", "PROTOCOL") + deprecatedSetting(rootCfg, "mailer", "IS_TLS_ENABLED", "mailer", "PROTOCOL", "v1.19.0") if sec.HasKey("IS_TLS_ENABLED") && !sec.HasKey("PROTOCOL") { if sec.Key("IS_TLS_ENABLED").MustBool() { sec.Key("PROTOCOL").MustString("smtps") @@ -99,38 +98,32 @@ func loadMailerFrom(rootCfg ConfigProvider) { } } - // FIXME: DEPRECATED to be removed in v1.19.0 - deprecatedSetting(rootCfg, "mailer", "DISABLE_HELO", "mailer", "ENABLE_HELO") + deprecatedSetting(rootCfg, "mailer", "DISABLE_HELO", "mailer", "ENABLE_HELO", "v1.19.0") if sec.HasKey("DISABLE_HELO") && !sec.HasKey("ENABLE_HELO") { sec.Key("ENABLE_HELO").MustBool(!sec.Key("DISABLE_HELO").MustBool()) } - // FIXME: DEPRECATED to be removed in v1.19.0 - deprecatedSetting(rootCfg, "mailer", "SKIP_VERIFY", "mailer", "FORCE_TRUST_SERVER_CERT") + deprecatedSetting(rootCfg, "mailer", "SKIP_VERIFY", "mailer", "FORCE_TRUST_SERVER_CERT", "v1.19.0") if sec.HasKey("SKIP_VERIFY") && !sec.HasKey("FORCE_TRUST_SERVER_CERT") { sec.Key("FORCE_TRUST_SERVER_CERT").MustBool(sec.Key("SKIP_VERIFY").MustBool()) } - // FIXME: DEPRECATED to be removed in v1.19.0 - deprecatedSetting(rootCfg, "mailer", "USE_CERTIFICATE", "mailer", "USE_CLIENT_CERT") + deprecatedSetting(rootCfg, "mailer", "USE_CERTIFICATE", "mailer", "USE_CLIENT_CERT", "v1.19.0") if sec.HasKey("USE_CERTIFICATE") && !sec.HasKey("USE_CLIENT_CERT") { sec.Key("USE_CLIENT_CERT").MustBool(sec.Key("USE_CERTIFICATE").MustBool()) } - // FIXME: DEPRECATED to be removed in v1.19.0 - deprecatedSetting(rootCfg, "mailer", "CERT_FILE", "mailer", "CLIENT_CERT_FILE") + deprecatedSetting(rootCfg, "mailer", "CERT_FILE", "mailer", "CLIENT_CERT_FILE", "v1.19.0") if sec.HasKey("CERT_FILE") && !sec.HasKey("CLIENT_CERT_FILE") { sec.Key("CERT_FILE").MustString(sec.Key("CERT_FILE").String()) } - // FIXME: DEPRECATED to be removed in v1.19.0 - deprecatedSetting(rootCfg, "mailer", "KEY_FILE", "mailer", "CLIENT_KEY_FILE") + deprecatedSetting(rootCfg, "mailer", "KEY_FILE", "mailer", "CLIENT_KEY_FILE", "v1.19.0") if sec.HasKey("KEY_FILE") && !sec.HasKey("CLIENT_KEY_FILE") { sec.Key("KEY_FILE").MustString(sec.Key("KEY_FILE").String()) } - // FIXME: DEPRECATED to be removed in v1.19.0 - deprecatedSetting(rootCfg, "mailer", "ENABLE_HTML_ALTERNATIVE", "mailer", "SEND_AS_PLAIN_TEXT") + deprecatedSetting(rootCfg, "mailer", "ENABLE_HTML_ALTERNATIVE", "mailer", "SEND_AS_PLAIN_TEXT", "v1.19.0") if sec.HasKey("ENABLE_HTML_ALTERNATIVE") && !sec.HasKey("SEND_AS_PLAIN_TEXT") { sec.Key("SEND_AS_PLAIN_TEXT").MustBool(!sec.Key("ENABLE_HTML_ALTERNATIVE").MustBool(false)) } diff --git a/modules/setting/mirror.go b/modules/setting/mirror.go index 875062f52252..cd6b8d456248 100644 --- a/modules/setting/mirror.go +++ b/modules/setting/mirror.go @@ -27,8 +27,9 @@ var Mirror = struct { func loadMirrorFrom(rootCfg ConfigProvider) { // Handle old configuration through `[repository]` `DISABLE_MIRRORS` // - please note this was badly named and only disabled the creation of new pull mirrors - // FIXME: DEPRECATED to be removed in v1.18.0 - deprecatedSetting(rootCfg, "repository", "DISABLE_MIRRORS", "mirror", "ENABLED") + // DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version + // if these are removed, the warning will not be shown + deprecatedSetting(rootCfg, "repository", "DISABLE_MIRRORS", "mirror", "ENABLED", "v1.19.0") if rootCfg.Section("repository").Key("DISABLE_MIRRORS").MustBool(false) { Mirror.DisableNewPull = true } diff --git a/modules/setting/server.go b/modules/setting/server.go index 6b0f3752e142..183906268576 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -178,38 +178,40 @@ func loadServerFrom(rootCfg ConfigProvider) { switch protocolCfg { case "https": Protocol = HTTPS - // FIXME: DEPRECATED to be removed in v1.18.0 + + // DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version + // if these are removed, the warning will not be shown if sec.HasKey("ENABLE_ACME") { EnableAcme = sec.Key("ENABLE_ACME").MustBool(false) } else { - deprecatedSetting(rootCfg, "server", "ENABLE_LETSENCRYPT", "server", "ENABLE_ACME") + deprecatedSetting(rootCfg, "server", "ENABLE_LETSENCRYPT", "server", "ENABLE_ACME", "v1.19.0") EnableAcme = sec.Key("ENABLE_LETSENCRYPT").MustBool(false) } if EnableAcme { AcmeURL = sec.Key("ACME_URL").MustString("") AcmeCARoot = sec.Key("ACME_CA_ROOT").MustString("") - // FIXME: DEPRECATED to be removed in v1.18.0 + if sec.HasKey("ACME_ACCEPTTOS") { AcmeTOS = sec.Key("ACME_ACCEPTTOS").MustBool(false) } else { - deprecatedSetting(rootCfg, "server", "LETSENCRYPT_ACCEPTTOS", "server", "ACME_ACCEPTTOS") + deprecatedSetting(rootCfg, "server", "LETSENCRYPT_ACCEPTTOS", "server", "ACME_ACCEPTTOS", "v1.19.0") AcmeTOS = sec.Key("LETSENCRYPT_ACCEPTTOS").MustBool(false) } if !AcmeTOS { log.Fatal("ACME TOS is not accepted (ACME_ACCEPTTOS).") } - // FIXME: DEPRECATED to be removed in v1.18.0 + if sec.HasKey("ACME_DIRECTORY") { AcmeLiveDirectory = sec.Key("ACME_DIRECTORY").MustString("https") } else { - deprecatedSetting(rootCfg, "server", "LETSENCRYPT_DIRECTORY", "server", "ACME_DIRECTORY") + deprecatedSetting(rootCfg, "server", "LETSENCRYPT_DIRECTORY", "server", "ACME_DIRECTORY", "v1.19.0") AcmeLiveDirectory = sec.Key("LETSENCRYPT_DIRECTORY").MustString("https") } - // FIXME: DEPRECATED to be removed in v1.18.0 + if sec.HasKey("ACME_EMAIL") { AcmeEmail = sec.Key("ACME_EMAIL").MustString("") } else { - deprecatedSetting(rootCfg, "server", "LETSENCRYPT_EMAIL", "server", "ACME_EMAIL") + deprecatedSetting(rootCfg, "server", "LETSENCRYPT_EMAIL", "server", "ACME_EMAIL", "v1.19.0") AcmeEmail = sec.Key("LETSENCRYPT_EMAIL").MustString("") } } else { diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 83ebde947882..87b1e2797fe7 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -16,6 +16,7 @@ import ( "strings" "time" + "code.gitea.io/gitea/modules/auth/password/hash" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/user" "code.gitea.io/gitea/modules/util" @@ -232,6 +233,10 @@ func InitProviderAndLoadCommonSettingsForTest(extraConfigs ...string) { if err := PrepareAppDataPath(); err != nil { log.Fatal("Can not prepare APP_DATA_PATH: %v", err) } + // register the dummy hash algorithm function used in the test fixtures + _ = hash.Register("dummy", hash.NewDummyHasher) + + PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy") } // newFileProviderFromConf initializes configuration context. diff --git a/modules/setting/task.go b/modules/setting/task.go index 81732deeb64f..f75b4f14813f 100644 --- a/modules/setting/task.go +++ b/modules/setting/task.go @@ -3,15 +3,16 @@ package setting -// FIXME: DEPRECATED to be removed in v1.18.0 +// DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version +// if these are removed, the warning will not be shown // - will need to set default for [queue.task] LENGTH to 1000 though func loadTaskFrom(rootCfg ConfigProvider) { taskSec := rootCfg.Section("task") queueTaskSec := rootCfg.Section("queue.task") - deprecatedSetting(rootCfg, "task", "QUEUE_TYPE", "queue.task", "TYPE") - deprecatedSetting(rootCfg, "task", "QUEUE_CONN_STR", "queue.task", "CONN_STR") - deprecatedSetting(rootCfg, "task", "QUEUE_LENGTH", "queue.task", "LENGTH") + deprecatedSetting(rootCfg, "task", "QUEUE_TYPE", "queue.task", "TYPE", "v1.19.0") + deprecatedSetting(rootCfg, "task", "QUEUE_CONN_STR", "queue.task", "CONN_STR", "v1.19.0") + deprecatedSetting(rootCfg, "task", "QUEUE_LENGTH", "queue.task", "LENGTH", "v1.19.0") switch taskSec.Key("QUEUE_TYPE").MustString("channel") { case "channel": diff --git a/modules/structs/user_app.go b/modules/structs/user_app.go index 3a5ae34df19a..7f78fbd495e2 100644 --- a/modules/structs/user_app.go +++ b/modules/structs/user_app.go @@ -11,10 +11,11 @@ import ( // AccessToken represents an API access token. // swagger:response AccessToken type AccessToken struct { - ID int64 `json:"id"` - Name string `json:"name"` - Token string `json:"sha1"` - TokenLastEight string `json:"token_last_eight"` + ID int64 `json:"id"` + Name string `json:"name"` + Token string `json:"sha1"` + TokenLastEight string `json:"token_last_eight"` + Scopes []string `json:"scopes"` } // AccessTokenList represents a list of API access token. @@ -22,9 +23,10 @@ type AccessToken struct { type AccessTokenList []*AccessToken // CreateAccessTokenOption options when create access token -// swagger:parameters userCreateToken type CreateAccessTokenOption struct { - Name string `json:"name" binding:"Required"` + // required: true + Name string `json:"name" binding:"Required"` + Scopes []string `json:"scopes"` } // CreateOAuth2ApplicationOptions holds options to create an oauth2 application diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 411a585c81d6..df66ce23390f 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -757,6 +757,7 @@ access_token_deletion_confirm_action = Delete access_token_deletion_desc = Deleting a token will revoke access to your account for applications using it. This cannot be undone. Continue? delete_token_success = The token has been deleted. Applications using it no longer have access to your account. select_scopes = Select scopes +scopes_list = Scopes: manage_oauth2_applications = Manage OAuth2 Applications edit_oauth2_application = Edit OAuth2 Application @@ -2152,6 +2153,7 @@ settings.choose_branch = Choose a branch… settings.no_protected_branch = There are no protected branches. settings.edit_protected_branch = Edit settings.protected_branch_required_rule_name = Required rule name +settings.protected_branch_duplicate_rule_name = Duplicate rule name settings.protected_branch_required_approvals_min = Required approvals cannot be negative. settings.tags = Tags settings.tags.protection = Tag Protection diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 7005725cf664..fa8b517ae77c 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -1420,8 +1420,9 @@ func GetPullRequestFiles(ctx *context.APIContext) { startCommitID := prInfo.MergeBase endCommitID := headCommitID - maxLines, maxFiles := setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles + maxLines := setting.Git.MaxGitDiffLines + // FIXME: If there are too many files in the repo, may cause some unpredictable issues. diff, err := gitdiff.GetDiff(baseGitRepo, &gitdiff.DiffOptions{ BeforeCommitID: startCommitID, @@ -1429,7 +1430,7 @@ func GetPullRequestFiles(ctx *context.APIContext) { SkipTo: ctx.FormString("skip-to"), MaxLines: maxLines, MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters, - MaxFiles: maxFiles, + MaxFiles: -1, // GetDiff() will return all files WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.FormString("whitespace")), }) if err != nil { @@ -1452,6 +1453,7 @@ func GetPullRequestFiles(ctx *context.APIContext) { if lenFiles < 0 { lenFiles = 0 } + apiFiles := make([]*api.ChangedFile, 0, lenFiles) for i := start; i < end; i++ { apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.HeadRepo, endCommitID)) diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go index 7b2f0d8c30d2..f89d53945fa0 100644 --- a/routers/api/v1/user/app.go +++ b/routers/api/v1/user/app.go @@ -9,6 +9,7 @@ import ( "fmt" "net/http" "strconv" + "strings" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/modules/context" @@ -62,6 +63,7 @@ func ListAccessTokens(ctx *context.APIContext) { ID: tokens[i].ID, Name: tokens[i].Name, TokenLastEight: tokens[i].TokenLastEight, + Scopes: tokens[i].Scope.StringSlice(), } } @@ -82,9 +84,9 @@ func CreateAccessToken(ctx *context.APIContext) { // - name: username // in: path // description: username of user - // type: string // required: true - // - name: userCreateToken + // type: string + // - name: body // in: body // schema: // "$ref": "#/definitions/CreateAccessTokenOption" @@ -111,6 +113,13 @@ func CreateAccessToken(ctx *context.APIContext) { return } + scope, err := auth_model.AccessTokenScope(strings.Join(form.Scopes, ",")).Normalize() + if err != nil { + ctx.Error(http.StatusBadRequest, "AccessTokenScope.Normalize", fmt.Errorf("invalid access token scope provided: %w", err)) + return + } + t.Scope = scope + if err := auth_model.NewAccessToken(t); err != nil { ctx.Error(http.StatusInternalServerError, "NewAccessToken", err) return diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 4e40867de204..f21611c6348d 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -43,8 +43,8 @@ const ( ) // setCompareContext sets context data. -func setCompareContext(ctx *context.Context, base, head *git.Commit, headOwner, headName string) { - ctx.Data["BaseCommit"] = base +func setCompareContext(ctx *context.Context, before, head *git.Commit, headOwner, headName string) { + ctx.Data["BeforeCommit"] = before ctx.Data["HeadCommit"] = head ctx.Data["GetBlobByPathForCommit"] = func(commit *git.Commit, path string) *git.Blob { @@ -59,7 +59,7 @@ func setCompareContext(ctx *context.Context, base, head *git.Commit, headOwner, return blob } - setPathsCompareContext(ctx, base, head, headOwner, headName) + setPathsCompareContext(ctx, before, head, headOwner, headName) setImageCompareContext(ctx) setCsvCompareContext(ctx) } @@ -629,9 +629,8 @@ func PrepareCompareDiff( } baseGitRepo := ctx.Repo.GitRepo - baseCommitID := ci.CompareInfo.BaseCommitID - baseCommit, err := baseGitRepo.GetCommit(baseCommitID) + beforeCommit, err := baseGitRepo.GetCommit(beforeCommitID) if err != nil { ctx.ServerError("GetCommit", err) return false @@ -668,7 +667,7 @@ func PrepareCompareDiff( ctx.Data["Username"] = ci.HeadUser.Name ctx.Data["Reponame"] = ci.HeadRepo.Name - setCompareContext(ctx, baseCommit, headCommit, ci.HeadUser.Name, repo.Name) + setCompareContext(ctx, beforeCommit, headCommit, ci.HeadUser.Name, repo.Name) return false } diff --git a/routers/web/repo/setting_protected_branch.go b/routers/web/repo/setting_protected_branch.go index a54565c1f151..0a8c39fef073 100644 --- a/routers/web/repo/setting_protected_branch.go +++ b/routers/web/repo/setting_protected_branch.go @@ -166,10 +166,36 @@ func SettingsProtectedBranchPost(ctx *context.Context) { } var err error - protectBranch, err = git_model.GetProtectedBranchRuleByName(ctx, ctx.Repo.Repository.ID, f.RuleName) - if err != nil { - ctx.ServerError("GetProtectBranchOfRepoByName", err) - return + if f.RuleID > 0 { + // If the RuleID isn't 0, it must be an edit operation. So we get rule by id. + protectBranch, err = git_model.GetProtectedBranchRuleByID(ctx, ctx.Repo.Repository.ID, f.RuleID) + if err != nil { + ctx.ServerError("GetProtectBranchOfRepoByID", err) + return + } + if protectBranch != nil && protectBranch.RuleName != f.RuleName { + // RuleName changed. We need to check if there is a rule with the same name. + // If a rule with the same name exists, an error should be returned. + sameNameProtectBranch, err := git_model.GetProtectedBranchRuleByName(ctx, ctx.Repo.Repository.ID, f.RuleName) + if err != nil { + ctx.ServerError("GetProtectBranchOfRepoByName", err) + return + } + if sameNameProtectBranch != nil { + ctx.Flash.Error(ctx.Tr("repo.settings.protected_branch_duplicate_rule_name")) + ctx.Redirect(fmt.Sprintf("%s/settings/branches/edit?rule_name=%s", ctx.Repo.RepoLink, protectBranch.RuleName)) + return + } + } + } else { + // FIXME: If a new ProtectBranch has a duplicate RuleName, an error should be returned. + // Currently, if a new ProtectBranch with a duplicate RuleName is created, the existing ProtectBranch will be updated. + // But we cannot modify this logic now because many unit tests rely on it. + protectBranch, err = git_model.GetProtectedBranchRuleByName(ctx, ctx.Repo.Repository.ID, f.RuleName) + if err != nil { + ctx.ServerError("GetProtectBranchOfRepoByName", err) + return + } } if protectBranch == nil { // No options found, create defaults. diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index ff0916f8e195..374ae0ac5cb5 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -190,6 +190,7 @@ func (f *RepoSettingForm) Validate(req *http.Request, errs binding.Errors) bindi // ProtectBranchForm form for changing protected branch settings type ProtectBranchForm struct { RuleName string `binding:"Required"` + RuleID int64 EnablePush string WhitelistUsers string WhitelistTeams string diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index ba6d968dbd27..b023717cd258 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -150,7 +150,6 @@ func (m *webhookNotifier) NotifyIssueChangeAssignee(ctx context.Context, doer *u log.Error("LoadPullRequest failed: %v", err) return } - issue.PullRequest.Issue = issue apiPullRequest := &api.PullRequestPayload{ Index: issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), @@ -196,7 +195,6 @@ func (m *webhookNotifier) NotifyIssueChangeTitle(ctx context.Context, doer *user log.Error("LoadPullRequest failed: %v", err) return } - issue.PullRequest.Issue = issue err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequest, &api.PullRequestPayload{ Action: api.HookIssueEdited, Index: issue.Index, @@ -328,7 +326,10 @@ func (m *webhookNotifier) NotifyIssueChangeContent(ctx context.Context, doer *us mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) var err error if issue.IsPull { - issue.PullRequest.Issue = issue + if err := issue.LoadPullRequest(ctx); err != nil { + log.Error("LoadPullRequest: %v", err) + return + } err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequest, &api.PullRequestPayload{ Action: api.HookIssueEdited, Index: issue.Index, diff --git a/templates/org/member/members.tmpl b/templates/org/member/members.tmpl index b76cb9778f95..0eae60fbfc49 100644 --- a/templates/org/member/members.tmpl +++ b/templates/org/member/members.tmpl @@ -39,6 +39,7 @@
+ {{if $.IsOrganizationOwner}}
{{$.locale.Tr "admin.users.2fa"}}
@@ -51,6 +52,7 @@ {{end}}
+ {{end}} {{end}}
diff --git a/templates/org/team/sidebar.tmpl b/templates/org/team/sidebar.tmpl index ab118a7e263f..507173e51fcf 100644 --- a/templates/org/team/sidebar.tmpl +++ b/templates/org/team/sidebar.tmpl @@ -58,7 +58,7 @@ {{range $t, $unit := $.Units}} - {{if and (lt $unit.MaxPerm 2) (not $unit.Type.UnitGlobalDisabled)}} + {{if (not $unit.Type.UnitGlobalDisabled)}} {{$.locale.Tr $unit.NameKey}} {{if eq ($.Team.UnitAccessMode $.Context $unit.Type) 0 -}} diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index cfa81a0f5bf1..a093c19deb8c 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -5,7 +5,7 @@ {{template "base/alert" .}} {{template "repo/sub_menu" .}} {{if .DefaultBranchBranch}} -

+

{{.locale.Tr "repo.default_branch"}}

diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index 0ecbf2916199..028fdc7e5148 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -17,7 +17,7 @@ {{$class = (printf "%s%s" $class " isWarning")}} {{end}} {{end}} -
+

{{RenderCommitMessage $.Context .Commit.Message $.RepoLink $.Repository.ComposeMetas}}{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses "root" $}}

{{if not $.PageIsWiki}} diff --git a/templates/repo/commit_statuses.tmpl b/templates/repo/commit_statuses.tmpl index 8250a858173b..8858fb840212 100644 --- a/templates/repo/commit_statuses.tmpl +++ b/templates/repo/commit_statuses.tmpl @@ -1,6 +1,14 @@ -{{if eq (len .Statuses) 1}}{{$status := index .Statuses 0}}{{if $status.TargetURL}}{{template "repo/commit_status" .Status}}{{end}}{{end}} -
-
+{{if .Statuses}} + {{if and (eq (len .Statuses) 1) .Status.TargetURL}} + + {{template "repo/commit_status" .Status}} + + {{else}} + + {{template "repo/commit_status" .Status}} + + {{end}} +
{{range .Statuses}}
{{template "repo/commit_status" .}} @@ -11,4 +19,4 @@
{{end}}
-
+{{end}} diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index 62cc069555fd..1e7982435586 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -71,7 +71,7 @@
{{range $i, $file := .Diff.Files}} {{/*notice: the index of Diff.Files should not be used for element ID, because the index will be restarted from 0 when doing load-more for PRs with a lot of files*/}} - {{$blobBase := call $.GetBlobByPathForCommit $.BaseCommit $file.OldName}} + {{$blobBase := call $.GetBlobByPathForCommit $.BeforeCommit $file.OldName}} {{$blobHead := call $.GetBlobByPathForCommit $.HeadCommit $file.Name}} {{$isImage := or (call $.IsBlobAnImage $blobBase) (call $.IsBlobAnImage $blobHead)}} {{$isCsv := (call $.IsCsvFile $file)}} diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl index b43253f90b5b..f0ac1e021ec4 100644 --- a/templates/repo/issue/view_title.tmpl +++ b/templates/repo/issue/view_title.tmpl @@ -61,7 +61,7 @@ {{$.locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref | Safe}} {{end}} - +