mirror of
https://github.com/go-gitea/gitea.git
synced 2025-11-03 08:02:36 +09:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cccd54999a | ||
|
|
3e7cb5b639 | ||
|
|
ec146b4200 | ||
|
|
51fa86f0fa | ||
|
|
e67b004535 | ||
|
|
a4cc867401 | ||
|
|
8e5aa8fb1e | ||
|
|
046fc8684c | ||
|
|
e3e705200a | ||
|
|
a9d5ab8f88 | ||
|
|
5546b4279c | ||
|
|
4f6d09fb68 | ||
|
|
4e5aca62ee | ||
|
|
030ed9462d | ||
|
|
60f175f7ff | ||
|
|
260816d64a | ||
|
|
a01f56a61a | ||
|
|
c01459a504 | ||
|
|
6e4e3ca012 | ||
|
|
ed0e8865f3 | ||
|
|
dc5adce70d | ||
|
|
328ce0485f | ||
|
|
39b6abf955 | ||
|
|
3eda79647b | ||
|
|
97171be1b4 | ||
|
|
9ef2a338d8 | ||
|
|
66ccae8b90 | ||
|
|
0e6489317e | ||
|
|
67dc1ff926 | ||
|
|
4ee4c06b07 | ||
|
|
3063e37802 | ||
|
|
a40e15a116 | ||
|
|
8f75f61b64 | ||
|
|
25e409e025 | ||
|
|
f8f24d83cf | ||
|
|
15e93a751c | ||
|
|
5a9b3bfa50 | ||
|
|
dd901983c0 | ||
|
|
7f962a16c9 | ||
|
|
5d81f6d73f | ||
|
|
eee4a752a5 | ||
|
|
b3516767fb | ||
|
|
8d1be2a9c5 | ||
|
|
e46f9ff534 | ||
|
|
690e810bcc | ||
|
|
35983ac0a8 | ||
|
|
4b3400bd9c |
26
.github/workflows/release-nightly.yml
vendored
26
.github/workflows/release-nightly.yml
vendored
@@ -59,6 +59,8 @@ jobs:
|
||||
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
|
||||
nightly-docker-rootful:
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
permissions:
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
@@ -85,17 +87,27 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: fetch go modules
|
||||
run: make vendor
|
||||
- name: build rootful docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
push: true
|
||||
tags: gitea/gitea:${{ steps.clean_name.outputs.branch }}
|
||||
tags: |-
|
||||
gitea/gitea:${{ steps.clean_name.outputs.branch }}
|
||||
ghcr.io/go-gitea/gitea:${{ steps.clean_name.outputs.branch }}
|
||||
nightly-docker-rootless:
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
permissions:
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
@@ -122,6 +134,12 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: fetch go modules
|
||||
run: make vendor
|
||||
- name: build rootless docker image
|
||||
@@ -131,4 +149,6 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
file: Dockerfile.rootless
|
||||
tags: gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless
|
||||
tags: |-
|
||||
gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless
|
||||
ghcr.io/go-gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless
|
||||
|
||||
28
.github/workflows/release-tag-rc.yml
vendored
28
.github/workflows/release-tag-rc.yml
vendored
@@ -69,6 +69,8 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
docker-rootful:
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
permissions:
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
@@ -79,7 +81,9 @@ jobs:
|
||||
- uses: docker/metadata-action@v5
|
||||
id: meta
|
||||
with:
|
||||
images: gitea/gitea
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
flavor: |
|
||||
latest=false
|
||||
# 1.2.3-rc0
|
||||
@@ -90,16 +94,24 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: build rootful docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
docker-rootless:
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
permissions:
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
@@ -110,7 +122,9 @@ jobs:
|
||||
- uses: docker/metadata-action@v5
|
||||
id: meta
|
||||
with:
|
||||
images: gitea/gitea
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
# each tag below will have the suffix of -rootless
|
||||
flavor: |
|
||||
latest=false
|
||||
@@ -123,11 +137,17 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: build rootless docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
push: true
|
||||
file: Dockerfile.rootless
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
28
.github/workflows/release-tag-version.yml
vendored
28
.github/workflows/release-tag-version.yml
vendored
@@ -14,6 +14,8 @@ concurrency:
|
||||
jobs:
|
||||
binary:
|
||||
runs-on: namespace-profile-gitea-release-binary
|
||||
permissions:
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
@@ -71,6 +73,8 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
docker-rootful:
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
permissions:
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
@@ -81,7 +85,9 @@ jobs:
|
||||
- uses: docker/metadata-action@v5
|
||||
id: meta
|
||||
with:
|
||||
images: gitea/gitea
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
# this will generate tags in the following format:
|
||||
# latest
|
||||
# 1
|
||||
@@ -96,11 +102,17 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: build rootful docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -116,7 +128,9 @@ jobs:
|
||||
- uses: docker/metadata-action@v5
|
||||
id: meta
|
||||
with:
|
||||
images: gitea/gitea
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
# each tag below will have the suffix of -rootless
|
||||
flavor: |
|
||||
suffix=-rootless,onlatest=true
|
||||
@@ -134,11 +148,17 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: build rootless docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
push: true
|
||||
file: Dockerfile.rootless
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
53
CHANGELOG.md
53
CHANGELOG.md
@@ -4,6 +4,59 @@ This changelog goes through 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.com).
|
||||
|
||||
## [1.23.8](https://github.com/go-gitea/gitea/releases/tag/1.23.8) - 2025-05-11
|
||||
|
||||
* SECURITY
|
||||
* Fix a bug when uploading file via lfs ssh command (#34408) (#34411)
|
||||
* Update net package (#34228) (#34232)
|
||||
* BUGFIXES
|
||||
* Fix releases sidebar navigation link (#34436) #34439
|
||||
* Fix bug webhook milestone is not right. (#34419) #34429
|
||||
* Fix two missed null value checks on the wiki page. (#34205) (#34215)
|
||||
* Swift files can be passed either as file or as form value (#34068) (#34236)
|
||||
* Fix bug when API get pull changed files for deleted head repository (#34333) (#34368)
|
||||
* Upgrade github v61 -> v71 to fix migrating bug (#34389)
|
||||
* Fix bug when visiting comparation page (#34334) (#34364)
|
||||
* Fix wrong review requests when updating the pull request (#34286) (#34304)
|
||||
* Fix github migration error when using multiple tokens (#34144) (#34302)
|
||||
* Explicitly not update indexes when sync database schemas (#34281) (#34295)
|
||||
* Fix panic when comment is nil (#34257) (#34277)
|
||||
* Fix project board links to related Pull Requests (#34213) (#34222)
|
||||
* Don't assume the default wiki branch is master in the wiki API (#34244) (#34245)
|
||||
* DOCUMENTATION
|
||||
* Update token creation API swagger documentation (#34288) (#34296)
|
||||
* MISC
|
||||
* Fix CI Build (#34315)
|
||||
* Add riscv64 support (#34199) (#34204)
|
||||
* Bump go version in go.mod (#34160)
|
||||
* remove hardcoded 'code' string in clone_panel.tmpl (#34153) (#34158)
|
||||
|
||||
## [1.23.7](https://github.com/go-gitea/gitea/releases/tag/1.23.7) - 2025-04-07
|
||||
|
||||
* Enhancements
|
||||
* Add a config option to block "expensive" pages for anonymous users (#34024) (#34071)
|
||||
* Also check default ssh-cert location for host (#34099) (#34100) (#34116)
|
||||
* BUGFIXES
|
||||
* Fix discord webhook 400 status code when description limit is exceeded (#34084) (#34124)
|
||||
* Get changed files based on merge base when checking `pull_request` actions trigger (#34106) (#34120)
|
||||
* Fix invalid version in RPM package path (#34112) (#34115)
|
||||
* Return default avatar url when user id is zero rather than updating database (#34094) (#34095)
|
||||
* Add additional ReplaceAll in pathsep to cater for different pathsep (#34061) (#34070)
|
||||
* Try to fix check-attr bug (#34029) (#34033)
|
||||
* Git client will follow 301 but 307 (#34005) (#34010)
|
||||
* Fix block expensive for 1.23 (#34127)
|
||||
* Fix markdown frontmatter rendering (#34102) (#34107)
|
||||
* Add new CLI flags to set name and scopes when creating a user with access token (#34080) (#34103)
|
||||
* Do not show 500 error when default branch doesn't exist (#34096) (#34097)
|
||||
* Hide activity contributors, recent commits and code frequrency left tabs if there is no code permission (#34053) (#34065)
|
||||
* Simplify emoji rendering (#34048) (#34049)
|
||||
* Adjust the layout of the toolbar on the Issues/Projects page (#33667) (#34047)
|
||||
* Pull request updates will also trigger code owners review requests (#33744) (#34045)
|
||||
* Fix org repo creation being limited by user limits (#34030) (#34044)
|
||||
* Fix git client accessing renamed repo (#34034) (#34043)
|
||||
* Fix the issue with error message logging for the `check-attr` command on Windows OS. (#34035) (#34036)
|
||||
* Polyfill WeakRef (#34025) (#34028)
|
||||
|
||||
## [1.23.6](https://github.com/go-gitea/gitea/releases/tag/v1.23.6) - 2025-03-24
|
||||
|
||||
* SECURITY
|
||||
|
||||
2
Makefile
2
Makefile
@@ -109,7 +109,7 @@ endif
|
||||
|
||||
LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)"
|
||||
|
||||
LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64
|
||||
LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/riscv64
|
||||
|
||||
GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/))
|
||||
MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list code.gitea.io/gitea/models/migrations/...)
|
||||
|
||||
4
assets/go-licenses.json
generated
4
assets/go-licenses.json
generated
File diff suppressed because one or more lines are too long
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
@@ -61,6 +62,16 @@ var microcmdUserCreate = &cli.Command{
|
||||
Name: "access-token",
|
||||
Usage: "Generate access token for the user",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "access-token-name",
|
||||
Usage: `Name of the generated access token`,
|
||||
Value: "gitea-admin",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "access-token-scopes",
|
||||
Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`,
|
||||
Value: "all",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "restricted",
|
||||
Usage: "Make a restricted user account",
|
||||
@@ -162,23 +173,39 @@ func runCreateUser(c *cli.Context) error {
|
||||
IsRestricted: restricted,
|
||||
}
|
||||
|
||||
var accessTokenName string
|
||||
var accessTokenScope auth_model.AccessTokenScope
|
||||
if c.IsSet("access-token") {
|
||||
accessTokenName = strings.TrimSpace(c.String("access-token-name"))
|
||||
if accessTokenName == "" {
|
||||
return errors.New("access-token-name cannot be empty")
|
||||
}
|
||||
var err error
|
||||
accessTokenScope, err = auth_model.AccessTokenScope(c.String("access-token-scopes")).Normalize()
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid access token scope provided: %w", err)
|
||||
}
|
||||
if !accessTokenScope.HasPermissionScope() {
|
||||
return errors.New("access token does not have any permission")
|
||||
}
|
||||
} else if c.IsSet("access-token-name") || c.IsSet("access-token-scopes") {
|
||||
return errors.New("access-token-name and access-token-scopes flags are only valid when access-token flag is set")
|
||||
}
|
||||
|
||||
// arguments should be prepared before creating the user & access token, in case there is anything wrong
|
||||
|
||||
// create the user
|
||||
if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
|
||||
return fmt.Errorf("CreateUser: %w", err)
|
||||
}
|
||||
|
||||
if c.Bool("access-token") {
|
||||
t := &auth_model.AccessToken{
|
||||
Name: "gitea-admin",
|
||||
UID: u.ID,
|
||||
}
|
||||
|
||||
// create the access token
|
||||
if accessTokenScope != "" {
|
||||
t := &auth_model.AccessToken{Name: accessTokenName, UID: u.ID, Scope: accessTokenScope}
|
||||
if err := auth_model.NewAccessToken(ctx, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Access token was successfully created... %s\n", t.Token)
|
||||
}
|
||||
|
||||
fmt.Printf("New user '%s' has been successfully created!\n", username)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,37 +8,97 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAdminUserCreate(t *testing.T) {
|
||||
app := NewMainApp(AppVersion{})
|
||||
|
||||
reset := func() {
|
||||
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
|
||||
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
|
||||
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
|
||||
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
|
||||
require.NoError(t, db.TruncateBeans(db.DefaultContext, &auth_model.AccessToken{}))
|
||||
}
|
||||
|
||||
type createCheck struct{ IsAdmin, MustChangePassword bool }
|
||||
createUser := func(name, args string) createCheck {
|
||||
t.Run("MustChangePassword", func(t *testing.T) {
|
||||
type check struct{ IsAdmin, MustChangePassword bool }
|
||||
createCheck := func(name, args string) check {
|
||||
assert.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
|
||||
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
|
||||
return createCheck{u.IsAdmin, u.MustChangePassword}
|
||||
return check{u.IsAdmin, u.MustChangePassword}
|
||||
}
|
||||
reset()
|
||||
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u", ""), "first non-admin user doesn't need to change password")
|
||||
assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u", ""), "first non-admin user doesn't need to change password")
|
||||
|
||||
reset()
|
||||
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u", "--admin"), "first admin user doesn't need to change password")
|
||||
assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u", "--admin"), "first admin user doesn't need to change password")
|
||||
|
||||
reset()
|
||||
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u", "--admin --must-change-password"))
|
||||
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u2", "--admin"))
|
||||
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u3", "--admin --must-change-password=false"))
|
||||
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: true}, createUser("u4", ""))
|
||||
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u5", "--must-change-password=false"))
|
||||
assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u", "--admin --must-change-password"))
|
||||
assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u2", "--admin"))
|
||||
assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u3", "--admin --must-change-password=false"))
|
||||
assert.Equal(t, check{IsAdmin: false, MustChangePassword: true}, createCheck("u4", ""))
|
||||
assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false"))
|
||||
})
|
||||
|
||||
createUser := func(name, args string) error {
|
||||
return app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s", name, name, args)))
|
||||
}
|
||||
|
||||
t.Run("AccessToken", func(t *testing.T) {
|
||||
// no generated access token
|
||||
reset()
|
||||
assert.NoError(t, createUser("u", "--random-password"))
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
|
||||
// using "--access-token" only means "all" access
|
||||
reset()
|
||||
assert.NoError(t, createUser("u", "--random-password --access-token"))
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
accessToken := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "gitea-admin"})
|
||||
hasScopes, err := accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, hasScopes)
|
||||
|
||||
// using "--access-token" with name & scopes
|
||||
reset()
|
||||
assert.NoError(t, createUser("u", "--random-password --access-token --access-token-name new-token-name --access-token-scopes read:issue,read:user"))
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
accessToken = unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "new-token-name"})
|
||||
hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopeReadUser)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, hasScopes)
|
||||
hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, hasScopes)
|
||||
|
||||
// using "--access-token-name" without "--access-token"
|
||||
reset()
|
||||
err = createUser("u", "--random-password --access-token-name new-token-name")
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
|
||||
|
||||
// using "--access-token-scopes" without "--access-token"
|
||||
reset()
|
||||
err = createUser("u", "--random-password --access-token-scopes read:issue")
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
|
||||
|
||||
// empty permission
|
||||
reset()
|
||||
err = createUser("u", "--random-password --access-token --access-token-scopes public-only")
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
assert.ErrorContains(t, err, "access token does not have any permission")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ var microcmdUserGenerateAccessToken = &cli.Command{
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "scopes",
|
||||
Value: "",
|
||||
Usage: "Comma separated list of scopes to apply to access token",
|
||||
Value: "all",
|
||||
Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`,
|
||||
},
|
||||
},
|
||||
Action: runGenerateAccessToken,
|
||||
@@ -43,7 +43,7 @@ var microcmdUserGenerateAccessToken = &cli.Command{
|
||||
|
||||
func runGenerateAccessToken(c *cli.Context) error {
|
||||
if !c.IsSet("username") {
|
||||
return errors.New("You must provide a username to generate a token for")
|
||||
return errors.New("you must provide a username to generate a token for")
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals()
|
||||
@@ -77,6 +77,9 @@ func runGenerateAccessToken(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid access token scope provided: %w", err)
|
||||
}
|
||||
if !accessTokenScope.HasPermissionScope() {
|
||||
return errors.New("access token does not have any permission")
|
||||
}
|
||||
t.Scope = accessTokenScope
|
||||
|
||||
// create the token
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/google/go-github/v61/github"
|
||||
"github.com/google/go-github/v71/github"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -774,6 +774,9 @@ LEVEL = Info
|
||||
;ALLOW_ONLY_EXTERNAL_REGISTRATION = false
|
||||
;;
|
||||
;; User must sign in to view anything.
|
||||
;; After 1.23.7, it could be set to "expensive" to block anonymous users accessing some pages which consume a lot of resources,
|
||||
;; for example: block anonymous AI crawlers from accessing repo code pages.
|
||||
;; The "expensive" mode is experimental and subject to change.
|
||||
;REQUIRE_SIGNIN_VIEW = false
|
||||
;;
|
||||
;; Mail notification
|
||||
|
||||
@@ -22,3 +22,8 @@ manifests:
|
||||
architecture: arm64
|
||||
os: linux
|
||||
variant: v8
|
||||
-
|
||||
image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}nightly{{/if}}-linux-riscv64-rootless
|
||||
platform:
|
||||
architecture: riscv64
|
||||
os: linux
|
||||
|
||||
@@ -22,3 +22,8 @@ manifests:
|
||||
architecture: arm64
|
||||
os: linux
|
||||
variant: v8
|
||||
-
|
||||
image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}nightly{{/if}}-linux-riscv64
|
||||
platform:
|
||||
architecture: riscv64
|
||||
os: linux
|
||||
|
||||
@@ -31,6 +31,21 @@ if [ -e /data/ssh/ssh_host_ecdsa_cert ]; then
|
||||
SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa_cert"}
|
||||
fi
|
||||
|
||||
# In case someone wants to sign the `{keyname}.pub` key by `ssh-keygen -s ca -I identity ...` to
|
||||
# make use of the ssh-key certificate authority feature (see ssh-keygen CERTIFICATES section),
|
||||
# the generated key file name is `{keyname}-cert.pub`
|
||||
if [ -e /data/ssh/ssh_host_ed25519_key-cert.pub ]; then
|
||||
SSH_ED25519_CERT=${SSH_ED25519_CERT:-"/data/ssh/ssh_host_ed25519_key-cert.pub"}
|
||||
fi
|
||||
|
||||
if [ -e /data/ssh/ssh_host_rsa_key-cert.pub ]; then
|
||||
SSH_RSA_CERT=${SSH_RSA_CERT:-"/data/ssh/ssh_host_rsa_key-cert.pub"}
|
||||
fi
|
||||
|
||||
if [ -e /data/ssh/ssh_host_ecdsa_key-cert.pub ]; then
|
||||
SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa_key-cert.pub"}
|
||||
fi
|
||||
|
||||
if [ -d /etc/ssh ]; then
|
||||
SSH_PORT=${SSH_PORT:-"22"} \
|
||||
SSH_LISTEN_PORT=${SSH_LISTEN_PORT:-"${SSH_PORT}"} \
|
||||
|
||||
8
go.mod
8
go.mod
@@ -1,6 +1,6 @@
|
||||
module code.gitea.io/gitea
|
||||
|
||||
go 1.23.6
|
||||
go 1.23.8
|
||||
|
||||
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
|
||||
// But some CAs use negative serial number, just relax the check. related:
|
||||
@@ -65,7 +65,7 @@ require (
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
|
||||
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/go-github/v61 v61.0.0
|
||||
github.com/google/go-github/v71 v71.0.0
|
||||
github.com/google/licenseclassifier/v2 v2.0.0
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db
|
||||
github.com/google/uuid v1.6.0
|
||||
@@ -120,7 +120,7 @@ require (
|
||||
github.com/yuin/goldmark-meta v1.1.0
|
||||
golang.org/x/crypto v0.36.0
|
||||
golang.org/x/image v0.21.0
|
||||
golang.org/x/net v0.37.0
|
||||
golang.org/x/net v0.38.0
|
||||
golang.org/x/oauth2 v0.27.0
|
||||
golang.org/x/sync v0.12.0
|
||||
golang.org/x/sys v0.31.0
|
||||
@@ -325,6 +325,8 @@ replace github.com/charmbracelet/git-lfs-transfer => gitea.com/gitea/git-lfs-tra
|
||||
// TODO: This could be removed after https://github.com/mholt/archiver/pull/396 merged
|
||||
replace github.com/mholt/archiver/v3 => github.com/anchore/archiver/v3 v3.5.2
|
||||
|
||||
replace git.sr.ht/~mariusor/go-xsd-duration => gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078
|
||||
|
||||
exclude github.com/gofrs/uuid v3.2.0+incompatible
|
||||
|
||||
exclude github.com/gofrs/uuid v4.0.0+incompatible
|
||||
|
||||
15
go.sum
15
go.sum
@@ -14,12 +14,12 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg=
|
||||
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
|
||||
gitea.com/gitea/act v0.261.3 h1:BhiYpGJQKGq0XMYYICCYAN4KnsEWHyLbA6dxhZwFcV4=
|
||||
gitea.com/gitea/act v0.261.3/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
|
||||
gitea.com/gitea/git-lfs-transfer v0.2.0 h1:baHaNoBSRaeq/xKayEXwiDQtlIjps4Ac/Ll4KqLMB40=
|
||||
gitea.com/gitea/git-lfs-transfer v0.2.0/go.mod h1:UrXUCm3xLQkq15fu7qlXHUMlrhdlXHoi13KH2Dfiits=
|
||||
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4=
|
||||
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
|
||||
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso=
|
||||
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw=
|
||||
gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g=
|
||||
@@ -410,10 +410,11 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go=
|
||||
github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-github/v71 v71.0.0 h1:Zi16OymGKZZMm8ZliffVVJ/Q9YZreDKONCr+WUd0Z30=
|
||||
github.com/google/go-github/v71 v71.0.0/go.mod h1:URZXObp2BLlMjwu0O8g4y6VBneUj2bCHgnI8FfgZ51M=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM=
|
||||
@@ -870,8 +871,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
||||
@@ -283,6 +283,10 @@ func (s AccessTokenScope) Normalize() (AccessTokenScope, error) {
|
||||
return bitmap.toScope(), nil
|
||||
}
|
||||
|
||||
func (s AccessTokenScope) HasPermissionScope() bool {
|
||||
return s != "" && s != AccessTokenScopePublicOnly
|
||||
}
|
||||
|
||||
// PublicOnly checks if this token scope is limited to public resources
|
||||
func (s AccessTokenScope) PublicOnly() (bool, error) {
|
||||
bitmap, err := s.parse()
|
||||
|
||||
@@ -173,6 +173,18 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e
|
||||
return &branch, nil
|
||||
}
|
||||
|
||||
// IsBranchExist returns true if the branch exists in the repository.
|
||||
func IsBranchExist(ctx context.Context, repoID int64, branchName string) (bool, error) {
|
||||
var branch Branch
|
||||
has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).And("name=?", branchName).Get(&branch)
|
||||
if err != nil {
|
||||
return false, err
|
||||
} else if !has {
|
||||
return false, nil
|
||||
}
|
||||
return !branch.IsDeleted, nil
|
||||
}
|
||||
|
||||
func GetBranches(ctx context.Context, repoID int64, branchNames []string, includeDeleted bool) ([]*Branch, error) {
|
||||
branches := make([]*Branch, 0, len(branchNames))
|
||||
|
||||
|
||||
@@ -663,7 +663,7 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo
|
||||
}
|
||||
|
||||
if review != nil {
|
||||
// skip it when reviewer hase been request to review
|
||||
// skip it when reviewer has been request to review
|
||||
if review.Type == ReviewTypeRequest {
|
||||
return nil, committer.Commit() // still commit the transaction, or committer.Close() will rollback it, even if it's a reused transaction.
|
||||
}
|
||||
|
||||
@@ -14,5 +14,9 @@ func AddContentVersionToIssueAndComment(x *xorm.Engine) error {
|
||||
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
|
||||
return x.Sync(new(Comment), new(Issue))
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreConstrains: true,
|
||||
IgnoreIndices: true,
|
||||
}, new(Comment), new(Issue))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -13,5 +13,9 @@ func AddForcePushBranchProtection(x *xorm.Engine) error {
|
||||
ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||
ForcePushAllowlistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
return x.Sync(new(ProtectedBranch))
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreConstrains: true,
|
||||
IgnoreIndices: true,
|
||||
}, new(ProtectedBranch))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -10,5 +10,9 @@ func AddSkipSecondaryAuthColumnToOAuth2ApplicationTable(x *xorm.Engine) error {
|
||||
type oauth2Application struct {
|
||||
SkipSecondaryAuthorization bool `xorm:"NOT NULL DEFAULT FALSE"`
|
||||
}
|
||||
return x.Sync(new(oauth2Application))
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreConstrains: true,
|
||||
IgnoreIndices: true,
|
||||
}, new(oauth2Application))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -19,5 +19,9 @@ func AddCommentMetaDataColumn(x *xorm.Engine) error {
|
||||
CommentMetaData *CommentMetaData `xorm:"JSON TEXT"` // put all non-index metadata in a single field
|
||||
}
|
||||
|
||||
return x.Sync(new(Comment))
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreConstrains: true,
|
||||
IgnoreIndices: true,
|
||||
}, new(Comment))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -9,5 +9,9 @@ func AddBlockAdminMergeOverrideBranchProtection(x *xorm.Engine) error {
|
||||
type ProtectedBranch struct {
|
||||
BlockAdminMergeOverride bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
return x.Sync(new(ProtectedBranch))
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreConstrains: true,
|
||||
IgnoreIndices: true,
|
||||
}, new(ProtectedBranch))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -12,5 +12,9 @@ func AddPriorityToProtectedBranch(x *xorm.Engine) error {
|
||||
Priority int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
|
||||
return x.Sync(new(ProtectedBranch))
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreConstrains: true,
|
||||
IgnoreIndices: true,
|
||||
}, new(ProtectedBranch))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ func AddTimeEstimateColumnToIssueTable(x *xorm.Engine) error {
|
||||
type Issue struct {
|
||||
TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
|
||||
return x.Sync(new(Issue))
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreConstrains: true,
|
||||
IgnoreIndices: true,
|
||||
}, new(Issue))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -279,9 +279,7 @@ func (opts *PackageSearchOptions) configureOrderBy(e db.Engine) {
|
||||
default:
|
||||
e.Desc("package_version.created_unix")
|
||||
}
|
||||
|
||||
// Sort by id for stable order with duplicates in the other field
|
||||
e.Asc("package_version.id")
|
||||
e.Desc("package_version.id") // Sort by id for stable order with duplicates in the other field
|
||||
}
|
||||
|
||||
// SearchVersions gets all versions of packages matching the search options
|
||||
|
||||
@@ -61,7 +61,9 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error {
|
||||
|
||||
// AvatarLinkWithSize returns a link to the user's avatar with size. size <= 0 means default size
|
||||
func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
|
||||
if u.IsGhost() || u.IsGiteaActions() {
|
||||
// ghost user was deleted, Gitea actions is a bot user, 0 means the user should be a virtual user
|
||||
// which comes from git configure information
|
||||
if u.IsGhost() || u.IsGiteaActions() || u.ID <= 0 {
|
||||
return avatars.DefaultAvatarLink()
|
||||
}
|
||||
|
||||
|
||||
@@ -463,7 +463,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
||||
matchTimes++
|
||||
}
|
||||
case "paths":
|
||||
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref)
|
||||
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
|
||||
if err != nil {
|
||||
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
|
||||
} else {
|
||||
@@ -476,7 +476,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
||||
}
|
||||
}
|
||||
case "paths-ignore":
|
||||
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref)
|
||||
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
|
||||
if err != nil {
|
||||
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
|
||||
} else {
|
||||
|
||||
@@ -350,9 +350,10 @@ func (c *Command) Run(opts *RunOpts) error {
|
||||
// We need to check if the context is canceled by the program on Windows.
|
||||
// This is because Windows does not have signal checking when terminating the process.
|
||||
// It always returns exit code 1, unlike Linux, which has many exit codes for signals.
|
||||
// `err.Error()` returns "exit status 1" when using the `git check-attr` command after the context is canceled.
|
||||
if runtime.GOOS == "windows" &&
|
||||
err != nil &&
|
||||
err.Error() == "" &&
|
||||
(err.Error() == "" || err.Error() == "exit status 1") &&
|
||||
cmd.ProcessState.ExitCode() == 1 &&
|
||||
ctx.Err() == context.Canceled {
|
||||
return ctx.Err()
|
||||
|
||||
@@ -280,7 +280,7 @@ func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
}
|
||||
wr.tmp = append(wr.tmp, p...)
|
||||
return len(p), nil
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strconv"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
@@ -29,9 +30,7 @@ func (n *Details) Kind() ast.NodeKind {
|
||||
|
||||
// NewDetails returns a new Paragraph node.
|
||||
func NewDetails() *Details {
|
||||
return &Details{
|
||||
BaseBlock: ast.BaseBlock{},
|
||||
}
|
||||
return &Details{}
|
||||
}
|
||||
|
||||
// Summary is a block that contains the summary of details block
|
||||
@@ -54,9 +53,7 @@ func (n *Summary) Kind() ast.NodeKind {
|
||||
|
||||
// NewSummary returns a new Summary node.
|
||||
func NewSummary() *Summary {
|
||||
return &Summary{
|
||||
BaseBlock: ast.BaseBlock{},
|
||||
}
|
||||
return &Summary{}
|
||||
}
|
||||
|
||||
// TaskCheckBoxListItem is a block that represents a list item of a markdown block with a checkbox
|
||||
@@ -95,29 +92,6 @@ type Icon struct {
|
||||
Name []byte
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump .
|
||||
func (n *Icon) Dump(source []byte, level int) {
|
||||
m := map[string]string{}
|
||||
m["Name"] = string(n.Name)
|
||||
ast.DumpHelper(n, source, level, m, nil)
|
||||
}
|
||||
|
||||
// KindIcon is the NodeKind for Icon
|
||||
var KindIcon = ast.NewNodeKind("Icon")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *Icon) Kind() ast.NodeKind {
|
||||
return KindIcon
|
||||
}
|
||||
|
||||
// NewIcon returns a new Paragraph node.
|
||||
func NewIcon(name string) *Icon {
|
||||
return &Icon{
|
||||
BaseInline: ast.BaseInline{},
|
||||
Name: []byte(name),
|
||||
}
|
||||
}
|
||||
|
||||
// ColorPreview is an inline for a color preview
|
||||
type ColorPreview struct {
|
||||
ast.BaseInline
|
||||
@@ -175,3 +149,24 @@ func NewAttention(attentionType string) *Attention {
|
||||
AttentionType: attentionType,
|
||||
}
|
||||
}
|
||||
|
||||
var KindRawHTML = ast.NewNodeKind("RawHTML")
|
||||
|
||||
type RawHTML struct {
|
||||
ast.BaseBlock
|
||||
rawHTML template.HTML
|
||||
}
|
||||
|
||||
func (n *RawHTML) Dump(source []byte, level int) {
|
||||
m := map[string]string{}
|
||||
m["RawHTML"] = string(n.rawHTML)
|
||||
ast.DumpHelper(n, source, level, m, nil)
|
||||
}
|
||||
|
||||
func (n *RawHTML) Kind() ast.NodeKind {
|
||||
return KindRawHTML
|
||||
}
|
||||
|
||||
func NewRawHTML(rawHTML template.HTML) *RawHTML {
|
||||
return &RawHTML{rawHTML: rawHTML}
|
||||
}
|
||||
|
||||
@@ -4,25 +4,24 @@
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/svg"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
east "github.com/yuin/goldmark/extension/ast"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func nodeToTable(meta *yaml.Node) ast.Node {
|
||||
for {
|
||||
for meta != nil && meta.Kind == yaml.DocumentNode {
|
||||
meta = meta.Content[0]
|
||||
}
|
||||
if meta == nil {
|
||||
return nil
|
||||
}
|
||||
switch meta.Kind {
|
||||
case yaml.DocumentNode:
|
||||
meta = meta.Content[0]
|
||||
continue
|
||||
default:
|
||||
}
|
||||
break
|
||||
}
|
||||
switch meta.Kind {
|
||||
case yaml.MappingNode:
|
||||
return mappingNodeToTable(meta)
|
||||
case yaml.SequenceNode:
|
||||
@@ -72,12 +71,28 @@ func sequenceNodeToTable(meta *yaml.Node) ast.Node {
|
||||
return table
|
||||
}
|
||||
|
||||
func nodeToDetails(meta *yaml.Node, icon string) ast.Node {
|
||||
func nodeToDetails(g *ASTTransformer, meta *yaml.Node) ast.Node {
|
||||
for meta != nil && meta.Kind == yaml.DocumentNode {
|
||||
meta = meta.Content[0]
|
||||
}
|
||||
if meta == nil {
|
||||
return nil
|
||||
}
|
||||
if meta.Kind != yaml.MappingNode {
|
||||
return nil
|
||||
}
|
||||
var keys []string
|
||||
for i := 0; i < len(meta.Content); i += 2 {
|
||||
if meta.Content[i].Kind == yaml.ScalarNode {
|
||||
keys = append(keys, meta.Content[i].Value)
|
||||
}
|
||||
}
|
||||
details := NewDetails()
|
||||
details.SetAttributeString(g.renderInternal.SafeAttr("class"), g.renderInternal.SafeValue("frontmatter-content"))
|
||||
summary := NewSummary()
|
||||
summary.AppendChild(summary, NewIcon(icon))
|
||||
summaryInnerHTML := htmlutil.HTMLFormat("%s %s", svg.RenderHTML("octicon-table", 12), strings.Join(keys, ", "))
|
||||
summary.AppendChild(summary, NewRawHTML(summaryInnerHTML))
|
||||
details.AppendChild(details, summary)
|
||||
details.AppendChild(details, nodeToTable(meta))
|
||||
|
||||
return details
|
||||
}
|
||||
|
||||
@@ -5,9 +5,6 @@ package markdown
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
@@ -51,7 +48,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
|
||||
|
||||
tocList := make([]Header, 0, 20)
|
||||
if rc.yamlNode != nil {
|
||||
metaNode := rc.toMetaNode()
|
||||
metaNode := rc.toMetaNode(g)
|
||||
if metaNode != nil {
|
||||
node.InsertBefore(node, firstChild, metaNode)
|
||||
}
|
||||
@@ -111,11 +108,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
|
||||
}
|
||||
}
|
||||
|
||||
// it is copied from old code, which is quite doubtful whether it is correct
|
||||
var reValidIconName = sync.OnceValue(func() *regexp.Regexp {
|
||||
return regexp.MustCompile(`^[-\w]+$`) // old: regexp.MustCompile("^[a-z ]+$")
|
||||
})
|
||||
|
||||
// NewHTMLRenderer creates a HTMLRenderer to render in the gitea form.
|
||||
func NewHTMLRenderer(renderInternal *internal.RenderInternal, opts ...html.Option) renderer.NodeRenderer {
|
||||
r := &HTMLRenderer{
|
||||
@@ -140,11 +132,11 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
reg.Register(ast.KindDocument, r.renderDocument)
|
||||
reg.Register(KindDetails, r.renderDetails)
|
||||
reg.Register(KindSummary, r.renderSummary)
|
||||
reg.Register(KindIcon, r.renderIcon)
|
||||
reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
|
||||
reg.Register(KindAttention, r.renderAttention)
|
||||
reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem)
|
||||
reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
|
||||
reg.Register(KindRawHTML, r.renderRawHTML)
|
||||
}
|
||||
|
||||
func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
@@ -206,30 +198,14 @@ func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.N
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
func (r *HTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
n := node.(*Icon)
|
||||
|
||||
name := strings.TrimSpace(strings.ToLower(string(n.Name)))
|
||||
|
||||
if len(name) == 0 {
|
||||
// skip this
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
if !reValidIconName().MatchString(name) {
|
||||
// skip this
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// FIXME: the "icon xxx" is from Fomantic UI, it's really questionable whether it still works correctly
|
||||
err := r.renderInternal.FormatWithSafeAttrs(w, `<i class="icon %s"></i>`, name)
|
||||
n := node.(*RawHTML)
|
||||
_, err := w.WriteString(string(r.renderInternal.ProtectSafeAttrs(n.rawHTML)))
|
||||
if err != nil {
|
||||
return ast.WalkStop, err
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
@@ -184,11 +184,7 @@ func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
|
||||
// Preserve original length.
|
||||
bufWithMetadataLength := len(buf)
|
||||
|
||||
rc := &RenderConfig{
|
||||
Meta: markup.RenderMetaAsDetails,
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}
|
||||
rc := &RenderConfig{Meta: markup.RenderMetaAsDetails}
|
||||
buf, _ = ExtractMetadataBytes(buf, rc)
|
||||
|
||||
metaLength := bufWithMetadataLength - len(buf)
|
||||
|
||||
@@ -383,18 +383,74 @@ func TestColorPreview(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskList(t *testing.T) {
|
||||
func TestMarkdownFrontmatter(t *testing.T) {
|
||||
testcases := []struct {
|
||||
testcase string
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"MapInFrontmatter",
|
||||
`---
|
||||
key1: val1
|
||||
key2: val2
|
||||
---
|
||||
test
|
||||
`,
|
||||
`<details class="frontmatter-content"><summary><span>octicon-table(12/)</span> key1, key2</summary><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>key1</th>
|
||||
<th>key2</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>val1</td>
|
||||
<td>val2</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</details><p>test</p>
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
"ListInFrontmatter",
|
||||
`---
|
||||
- item1
|
||||
- item2
|
||||
---
|
||||
test
|
||||
`,
|
||||
`- item1
|
||||
- item2
|
||||
|
||||
<p>test</p>
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
"StringInFrontmatter",
|
||||
`---
|
||||
anything
|
||||
---
|
||||
test
|
||||
`,
|
||||
`anything
|
||||
|
||||
<p>test</p>
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
// data-source-position should take into account YAML frontmatter.
|
||||
"ListAfterFrontmatter",
|
||||
`---
|
||||
foo: bar
|
||||
---
|
||||
- [ ] task 1`,
|
||||
`<details><summary><i class="icon table"></i></summary><table>
|
||||
`<details class="frontmatter-content"><summary><span>octicon-table(12/)</span> foo</summary><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>foo</th>
|
||||
@@ -414,9 +470,9 @@ foo: bar
|
||||
}
|
||||
|
||||
for _, test := range testcases {
|
||||
res, err := markdown.RenderString(markup.NewTestRenderContext(), test.testcase)
|
||||
assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
|
||||
assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
|
||||
res, err := markdown.RenderString(markup.NewTestRenderContext(), test.input)
|
||||
assert.NoError(t, err, "Unexpected error in testcase: %q", test.name)
|
||||
assert.Equal(t, test.expected, string(res), "Unexpected result in testcase %q", test.name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
// RenderConfig represents rendering configuration for this file
|
||||
type RenderConfig struct {
|
||||
Meta markup.RenderMetaMode
|
||||
Icon string
|
||||
TOC string // "false": hide, "side"/empty: in sidebar, "main"/"true": in main view
|
||||
Lang string
|
||||
yamlNode *yaml.Node
|
||||
@@ -74,7 +73,7 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
|
||||
|
||||
type yamlRenderConfig struct {
|
||||
Meta *string `yaml:"meta"`
|
||||
Icon *string `yaml:"details_icon"`
|
||||
Icon *string `yaml:"details_icon"` // deprecated, because there is no font icon, so no custom icon
|
||||
TOC *string `yaml:"include_toc"`
|
||||
Lang *string `yaml:"lang"`
|
||||
}
|
||||
@@ -96,10 +95,6 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
|
||||
rc.Meta = renderMetaModeFromString(*cfg.Gitea.Meta)
|
||||
}
|
||||
|
||||
if cfg.Gitea.Icon != nil {
|
||||
rc.Icon = strings.TrimSpace(strings.ToLower(*cfg.Gitea.Icon))
|
||||
}
|
||||
|
||||
if cfg.Gitea.Lang != nil && *cfg.Gitea.Lang != "" {
|
||||
rc.Lang = *cfg.Gitea.Lang
|
||||
}
|
||||
@@ -111,7 +106,7 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rc *RenderConfig) toMetaNode() ast.Node {
|
||||
func (rc *RenderConfig) toMetaNode(g *ASTTransformer) ast.Node {
|
||||
if rc.yamlNode == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -119,7 +114,7 @@ func (rc *RenderConfig) toMetaNode() ast.Node {
|
||||
case markup.RenderMetaAsTable:
|
||||
return nodeToTable(rc.yamlNode)
|
||||
case markup.RenderMetaAsDetails:
|
||||
return nodeToDetails(rc.yamlNode, rc.Icon)
|
||||
return nodeToDetails(g, rc.yamlNode)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -19,42 +20,36 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
{
|
||||
"empty", &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}, "",
|
||||
},
|
||||
{
|
||||
"lang", &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "test",
|
||||
}, "lang: test",
|
||||
},
|
||||
{
|
||||
"metatable", &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}, "gitea: table",
|
||||
},
|
||||
{
|
||||
"metanone", &RenderConfig{
|
||||
Meta: "none",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}, "gitea: none",
|
||||
},
|
||||
{
|
||||
"metadetails", &RenderConfig{
|
||||
Meta: "details",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}, "gitea: details",
|
||||
},
|
||||
{
|
||||
"metawrong", &RenderConfig{
|
||||
Meta: "details",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}, "gitea: wrong",
|
||||
},
|
||||
@@ -62,7 +57,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
"toc", &RenderConfig{
|
||||
TOC: "true",
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}, "include_toc: true",
|
||||
},
|
||||
@@ -70,14 +64,12 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
"tocfalse", &RenderConfig{
|
||||
TOC: "false",
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}, "include_toc: false",
|
||||
},
|
||||
{
|
||||
"toclang", &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
TOC: "true",
|
||||
Lang: "testlang",
|
||||
}, `
|
||||
@@ -88,7 +80,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
{
|
||||
"complexlang", &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "testlang",
|
||||
}, `
|
||||
gitea:
|
||||
@@ -98,7 +89,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
{
|
||||
"complexlang2", &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "testlang",
|
||||
}, `
|
||||
lang: notright
|
||||
@@ -109,7 +99,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
{
|
||||
"complexlang", &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "testlang",
|
||||
}, `
|
||||
gitea:
|
||||
@@ -121,7 +110,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
Lang: "two",
|
||||
Meta: "table",
|
||||
TOC: "true",
|
||||
Icon: "smiley",
|
||||
}, `
|
||||
lang: one
|
||||
include_toc: true
|
||||
@@ -137,7 +125,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}
|
||||
if err := yaml.Unmarshal([]byte(strings.ReplaceAll(tt.args, "\t", " ")), got); err != nil {
|
||||
@@ -145,18 +132,9 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
if got.Meta != tt.expected.Meta {
|
||||
t.Errorf("Meta Expected %s Got %s", tt.expected.Meta, got.Meta)
|
||||
}
|
||||
if got.Icon != tt.expected.Icon {
|
||||
t.Errorf("Icon Expected %s Got %s", tt.expected.Icon, got.Icon)
|
||||
}
|
||||
if got.Lang != tt.expected.Lang {
|
||||
t.Errorf("Lang Expected %s Got %s", tt.expected.Lang, got.Lang)
|
||||
}
|
||||
if got.TOC != tt.expected.TOC {
|
||||
t.Errorf("TOC Expected %q Got %q", tt.expected.TOC, got.TOC)
|
||||
}
|
||||
assert.Equal(t, tt.expected.Meta, got.Meta)
|
||||
assert.Equal(t, tt.expected.Lang, got.Lang)
|
||||
assert.Equal(t, tt.expected.TOC, got.TOC)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ type ConfigKey interface {
|
||||
In(defaultVal string, candidates []string) string
|
||||
String() string
|
||||
Strings(delim string) []string
|
||||
Bool() (bool, error)
|
||||
|
||||
MustString(defaultVal string) string
|
||||
MustBool(defaultVal ...bool) bool
|
||||
|
||||
@@ -43,7 +43,8 @@ var Service = struct {
|
||||
ShowRegistrationButton bool
|
||||
EnablePasswordSignInForm bool
|
||||
ShowMilestonesDashboardPage bool
|
||||
RequireSignInView bool
|
||||
RequireSignInViewStrict bool
|
||||
BlockAnonymousAccessExpensive bool
|
||||
EnableNotifyMail bool
|
||||
EnableBasicAuth bool
|
||||
EnablePasskeyAuth bool
|
||||
@@ -159,7 +160,18 @@ func loadServiceFrom(rootCfg ConfigProvider) {
|
||||
Service.EmailDomainBlockList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_BLOCKLIST")
|
||||
Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration))
|
||||
Service.ShowMilestonesDashboardPage = sec.Key("SHOW_MILESTONES_DASHBOARD_PAGE").MustBool(true)
|
||||
Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool()
|
||||
|
||||
// boolean values are considered as "strict"
|
||||
var err error
|
||||
Service.RequireSignInViewStrict, err = sec.Key("REQUIRE_SIGNIN_VIEW").Bool()
|
||||
if s := sec.Key("REQUIRE_SIGNIN_VIEW").String(); err != nil && s != "" {
|
||||
// non-boolean value only supports "expensive" at the moment
|
||||
Service.BlockAnonymousAccessExpensive = s == "expensive"
|
||||
if !Service.BlockAnonymousAccessExpensive {
|
||||
log.Error("Invalid config option: REQUIRE_SIGNIN_VIEW = %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true)
|
||||
Service.EnablePasswordSignInForm = sec.Key("ENABLE_PASSWORD_SIGNIN_FORM").MustBool(true)
|
||||
Service.EnablePasskeyAuth = sec.Key("ENABLE_PASSKEY_AUTHENTICATION").MustBool(true)
|
||||
|
||||
@@ -7,16 +7,14 @@ import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLoadServices(t *testing.T) {
|
||||
oldService := Service
|
||||
defer func() {
|
||||
Service = oldService
|
||||
}()
|
||||
defer test.MockVariableValue(&Service)()
|
||||
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[service]
|
||||
@@ -48,10 +46,7 @@ EMAIL_DOMAIN_BLOCKLIST = d3, *.b
|
||||
}
|
||||
|
||||
func TestLoadServiceVisibilityModes(t *testing.T) {
|
||||
oldService := Service
|
||||
defer func() {
|
||||
Service = oldService
|
||||
}()
|
||||
defer test.MockVariableValue(&Service)()
|
||||
|
||||
kases := map[string]func(){
|
||||
`
|
||||
@@ -130,3 +125,33 @@ ALLOWED_USER_VISIBILITY_MODES = public, limit, privated
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadServiceRequireSignInView(t *testing.T) {
|
||||
defer test.MockVariableValue(&Service)()
|
||||
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[service]
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadServiceFrom(cfg)
|
||||
assert.False(t, Service.RequireSignInViewStrict)
|
||||
assert.False(t, Service.BlockAnonymousAccessExpensive)
|
||||
|
||||
cfg, err = NewConfigProviderFromData(`
|
||||
[service]
|
||||
REQUIRE_SIGNIN_VIEW = true
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadServiceFrom(cfg)
|
||||
assert.True(t, Service.RequireSignInViewStrict)
|
||||
assert.False(t, Service.BlockAnonymousAccessExpensive)
|
||||
|
||||
cfg, err = NewConfigProviderFromData(`
|
||||
[service]
|
||||
REQUIRE_SIGNIN_VIEW = expensive
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadServiceFrom(cfg)
|
||||
assert.False(t, Service.RequireSignInViewStrict)
|
||||
assert.True(t, Service.BlockAnonymousAccessExpensive)
|
||||
}
|
||||
|
||||
@@ -23,9 +23,11 @@ type AccessToken struct {
|
||||
type AccessTokenList []*AccessToken
|
||||
|
||||
// CreateAccessTokenOption options when create access token
|
||||
// swagger:model CreateAccessTokenOption
|
||||
type CreateAccessTokenOption struct {
|
||||
// required: true
|
||||
Name string `json:"name" binding:"Required"`
|
||||
// example: ["all", "read:activitypub","read:issue", "write:misc", "read:notification", "read:organization", "read:package", "read:repository", "read:user"]
|
||||
Scopes []string `json:"scopes"`
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
applet
|
||||
application.linux-arm64
|
||||
application.linux-armv6hf
|
||||
application.linux-riscv64
|
||||
application.linux32
|
||||
application.linux64
|
||||
application.windows32
|
||||
|
||||
@@ -2699,6 +2699,7 @@ branch.restore_success = Branch "%s" has been restored.
|
||||
branch.restore_failed = Failed to restore branch "%s".
|
||||
branch.protected_deletion_failed = Branch "%s" is protected. It cannot be deleted.
|
||||
branch.default_deletion_failed = Branch "%s" is the default branch. It cannot be deleted.
|
||||
branch.default_branch_not_exist = Default branch "%s" does not exist.
|
||||
branch.restore = Restore Branch "%s"
|
||||
branch.download = Download Branch "%s"
|
||||
branch.rename = Rename Branch "%s"
|
||||
|
||||
@@ -51,7 +51,7 @@ func apiError(ctx *context.Context, status int, obj any) {
|
||||
|
||||
// https://rust-lang.github.io/rfcs/2789-sparse-index.html
|
||||
func RepositoryConfig(ctx *context.Context) {
|
||||
ctx.JSON(http.StatusOK, cargo_service.BuildConfig(ctx.Package.Owner, setting.Service.RequireSignInView || ctx.Package.Owner.Visibility != structs.VisibleTypePublic))
|
||||
ctx.JSON(http.StatusOK, cargo_service.BuildConfig(ctx.Package.Owner, setting.Service.RequireSignInViewStrict || ctx.Package.Owner.Visibility != structs.VisibleTypePublic))
|
||||
}
|
||||
|
||||
func EnumeratePackageVersions(ctx *context.Context) {
|
||||
|
||||
@@ -126,7 +126,7 @@ func apiUnauthorizedError(ctx *context.Context) {
|
||||
|
||||
// ReqContainerAccess is a middleware which checks the current user valid (real user or ghost if anonymous access is enabled)
|
||||
func ReqContainerAccess(ctx *context.Context) {
|
||||
if ctx.Doer == nil || (setting.Service.RequireSignInView && ctx.Doer.IsGhost()) {
|
||||
if ctx.Doer == nil || (setting.Service.RequireSignInViewStrict && ctx.Doer.IsGhost()) {
|
||||
apiUnauthorizedError(ctx)
|
||||
}
|
||||
}
|
||||
@@ -152,7 +152,7 @@ func Authenticate(ctx *context.Context) {
|
||||
u := ctx.Doer
|
||||
packageScope := auth_service.GetAccessScope(ctx.Data)
|
||||
if u == nil {
|
||||
if setting.Service.RequireSignInView {
|
||||
if setting.Service.RequireSignInViewStrict {
|
||||
apiUnauthorizedError(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -290,7 +290,24 @@ func DownloadManifest(ctx *context.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-6
|
||||
// formFileOptionalReadCloser returns (nil, nil) if the formKey is not present.
|
||||
func formFileOptionalReadCloser(ctx *context.Context, formKey string) (io.ReadCloser, error) {
|
||||
multipartFile, _, err := ctx.Req.FormFile(formKey)
|
||||
if err != nil && !errors.Is(err, http.ErrMissingFile) {
|
||||
return nil, err
|
||||
}
|
||||
if multipartFile != nil {
|
||||
return multipartFile, nil
|
||||
}
|
||||
|
||||
content := ctx.Req.FormValue(formKey)
|
||||
if content == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return io.NopCloser(strings.NewReader(ctx.Req.FormValue(formKey))), nil
|
||||
}
|
||||
|
||||
// UploadPackageFile refers to https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-6
|
||||
func UploadPackageFile(ctx *context.Context) {
|
||||
packageScope := ctx.PathParam("scope")
|
||||
packageName := ctx.PathParam("name")
|
||||
@@ -304,9 +321,9 @@ func UploadPackageFile(ctx *context.Context) {
|
||||
|
||||
packageVersion := v.Core().String()
|
||||
|
||||
file, _, err := ctx.Req.FormFile("source-archive")
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
file, err := formFileOptionalReadCloser(ctx, "source-archive")
|
||||
if file == nil || err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, "unable to read source-archive file")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
@@ -318,10 +335,13 @@ func UploadPackageFile(ctx *context.Context) {
|
||||
}
|
||||
defer buf.Close()
|
||||
|
||||
var mr io.Reader
|
||||
metadata := ctx.Req.FormValue("metadata")
|
||||
if metadata != "" {
|
||||
mr = strings.NewReader(metadata)
|
||||
mr, err := formFileOptionalReadCloser(ctx, "metadata")
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, "unable to read metadata file")
|
||||
return
|
||||
}
|
||||
if mr != nil {
|
||||
defer mr.Close()
|
||||
}
|
||||
|
||||
pck, err := swift_module.ParsePackage(buf, buf.Size(), mr)
|
||||
|
||||
@@ -356,7 +356,7 @@ func reqToken() func(ctx *context.APIContext) {
|
||||
|
||||
func reqExploreSignIn() func(ctx *context.APIContext) {
|
||||
return func(ctx *context.APIContext) {
|
||||
if (setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView) && !ctx.IsSigned {
|
||||
if (setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView) && !ctx.IsSigned {
|
||||
ctx.Error(http.StatusUnauthorized, "reqExploreSignIn", "you must be signed in to search for users")
|
||||
}
|
||||
}
|
||||
@@ -874,7 +874,7 @@ func Routes() *web.Router {
|
||||
m.Use(apiAuth(buildAuthGroup()))
|
||||
|
||||
m.Use(verifyAuthWithOptions(&common.VerifyOptions{
|
||||
SignInRequired: setting.Service.RequireSignInView,
|
||||
SignInRequired: setting.Service.RequireSignInViewStrict,
|
||||
}))
|
||||
|
||||
addActionsRoutes := func(
|
||||
|
||||
@@ -895,6 +895,15 @@ func EditIssue(ctx *context.APIContext) {
|
||||
issue.MilestoneID != *form.Milestone {
|
||||
oldMilestoneID := issue.MilestoneID
|
||||
issue.MilestoneID = *form.Milestone
|
||||
if issue.MilestoneID > 0 {
|
||||
issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, *form.Milestone)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
issue.Milestone = nil
|
||||
}
|
||||
if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err)
|
||||
return
|
||||
|
||||
@@ -694,6 +694,11 @@ func EditPullRequest(ctx *context.APIContext) {
|
||||
issue.MilestoneID != form.Milestone {
|
||||
oldMilestoneID := issue.MilestoneID
|
||||
issue.MilestoneID = form.Milestone
|
||||
issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, form.Milestone)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err)
|
||||
return
|
||||
}
|
||||
if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err)
|
||||
return
|
||||
@@ -1638,7 +1643,9 @@ func GetPullRequestFiles(ctx *context.APIContext) {
|
||||
|
||||
apiFiles := make([]*api.ChangedFile, 0, limit)
|
||||
for i := start; i < start+limit; i++ {
|
||||
apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.HeadRepo, endCommitID))
|
||||
// refs/pull/1/head stores the HEAD commit ID, allowing all related commits to be found in the base repository.
|
||||
// The head repository might have been deleted, so we should not rely on it here.
|
||||
apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.BaseRepo, endCommitID))
|
||||
}
|
||||
|
||||
ctx.SetLinkHeader(totalNumberOfFiles, listOptions.PageSize)
|
||||
|
||||
@@ -193,7 +193,7 @@ func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.Wi
|
||||
}
|
||||
|
||||
// get commit count - wiki revisions
|
||||
commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
|
||||
commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
|
||||
|
||||
// Get last change information.
|
||||
lastCommit, err := wikiRepo.GetCommitByPath(pageFilename)
|
||||
@@ -432,7 +432,7 @@ func ListPageRevisions(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
// get commit count - wiki revisions
|
||||
commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
|
||||
commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
|
||||
|
||||
page := ctx.FormInt("page")
|
||||
if page <= 1 {
|
||||
@@ -442,7 +442,7 @@ func ListPageRevisions(ctx *context.APIContext) {
|
||||
// get Commit Count
|
||||
commitsHistory, err := wikiRepo.CommitsByFileAndRange(
|
||||
git.CommitsByFileAndRangeOptions{
|
||||
Revision: "master",
|
||||
Revision: ctx.Repo.Repository.DefaultWikiBranch,
|
||||
File: pageFilename,
|
||||
Page: page,
|
||||
})
|
||||
@@ -486,7 +486,7 @@ func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
commit, err := wikiRepo.GetBranchCommit("master")
|
||||
commit, err := wikiRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch)
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
ctx.NotFound(err)
|
||||
|
||||
92
routers/common/blockexpensive.go
Normal file
92
routers/common/blockexpensive.go
Normal file
@@ -0,0 +1,92 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func BlockExpensive() func(next http.Handler) http.Handler {
|
||||
if !setting.Service.BlockAnonymousAccessExpensive {
|
||||
return nil
|
||||
}
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
ret := determineRequestPriority(req.Context())
|
||||
if !ret.SignedIn {
|
||||
if ret.Expensive || ret.LongPolling {
|
||||
http.Redirect(w, req, setting.AppSubURL+"/user/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
}
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func isRoutePathExpensive(routePattern string) bool {
|
||||
if strings.HasPrefix(routePattern, "/user/") || strings.HasPrefix(routePattern, "/login/") {
|
||||
return false
|
||||
}
|
||||
|
||||
expensivePaths := []string{
|
||||
// code related
|
||||
"/{username}/{reponame}/archive/",
|
||||
"/{username}/{reponame}/blame/",
|
||||
"/{username}/{reponame}/commit/",
|
||||
"/{username}/{reponame}/commits/",
|
||||
"/{username}/{reponame}/graph",
|
||||
"/{username}/{reponame}/media/",
|
||||
"/{username}/{reponame}/raw/",
|
||||
"/{username}/{reponame}/src/",
|
||||
|
||||
// issue & PR related (no trailing slash)
|
||||
"/{username}/{reponame}/issues",
|
||||
"/{username}/{reponame}/{type:issues}",
|
||||
"/{username}/{reponame}/pulls",
|
||||
"/{username}/{reponame}/{type:pulls}",
|
||||
"/{username}/{reponame}/{type:issues|pulls}", // for 1.23 only
|
||||
|
||||
// wiki
|
||||
"/{username}/{reponame}/wiki/",
|
||||
|
||||
// activity
|
||||
"/{username}/{reponame}/activity/",
|
||||
}
|
||||
for _, path := range expensivePaths {
|
||||
if strings.HasPrefix(routePattern, path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isRoutePathForLongPolling(routePattern string) bool {
|
||||
return routePattern == "/user/events"
|
||||
}
|
||||
|
||||
func determineRequestPriority(ctx context.Context) (ret struct {
|
||||
SignedIn bool
|
||||
Expensive bool
|
||||
LongPolling bool
|
||||
},
|
||||
) {
|
||||
dataStore := middleware.GetContextData(ctx)
|
||||
chiRoutePath := chi.RouteContext(ctx).RoutePattern()
|
||||
if _, ok := dataStore[middleware.ContextDataKeySignedUser].(*user_model.User); ok {
|
||||
ret.SignedIn = true
|
||||
} else {
|
||||
ret.Expensive = isRoutePathExpensive(chiRoutePath)
|
||||
ret.LongPolling = isRoutePathForLongPolling(chiRoutePath)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
30
routers/common/blockexpensive_test.go
Normal file
30
routers/common/blockexpensive_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBlockExpensive(t *testing.T) {
|
||||
cases := []struct {
|
||||
expensive bool
|
||||
routePath string
|
||||
}{
|
||||
{false, "/user/xxx"},
|
||||
{false, "/login/xxx"},
|
||||
{true, "/{username}/{reponame}/archive/xxx"},
|
||||
{true, "/{username}/{reponame}/graph"},
|
||||
{true, "/{username}/{reponame}/src/xxx"},
|
||||
{true, "/{username}/{reponame}/wiki/xxx"},
|
||||
{true, "/{username}/{reponame}/activity/xxx"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
assert.Equal(t, c.expensive, isRoutePathExpensive(c.routePath), "routePath: %s", c.routePath)
|
||||
}
|
||||
|
||||
assert.True(t, isRoutePathForLongPolling("/user/events"))
|
||||
}
|
||||
@@ -156,7 +156,7 @@ func Install(ctx *context.Context) {
|
||||
form.DisableRegistration = setting.Service.DisableRegistration
|
||||
form.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration
|
||||
form.EnableCaptcha = setting.Service.EnableCaptcha
|
||||
form.RequireSignInView = setting.Service.RequireSignInView
|
||||
form.RequireSignInView = setting.Service.RequireSignInViewStrict
|
||||
form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
|
||||
form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization
|
||||
form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking
|
||||
|
||||
@@ -81,6 +81,7 @@ func ServCommand(ctx *context.PrivateContext) {
|
||||
ownerName := ctx.PathParam(":owner")
|
||||
repoName := ctx.PathParam(":repo")
|
||||
mode := perm.AccessMode(ctx.FormInt("mode"))
|
||||
verb := ctx.FormString("verb")
|
||||
|
||||
// Set the basic parts of the results to return
|
||||
results := private.ServCommandResults{
|
||||
@@ -286,7 +287,7 @@ func ServCommand(ctx *context.PrivateContext) {
|
||||
repo.IsPrivate ||
|
||||
owner.Visibility.IsPrivate() ||
|
||||
(user != nil && user.IsRestricted) || // user will be nil if the key is a deploykey
|
||||
setting.Service.RequireSignInView) {
|
||||
setting.Service.RequireSignInViewStrict) {
|
||||
if key.Type == asymkey_model.KeyTypeDeploy {
|
||||
if deployKey.Mode < mode {
|
||||
ctx.JSON(http.StatusUnauthorized, private.Response{
|
||||
@@ -295,8 +296,11 @@ func ServCommand(ctx *context.PrivateContext) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Because of the special ref "refs/for" we will need to delay write permission check
|
||||
if git.DefaultFeatures().SupportProcReceive && unitType == unit.TypeCode {
|
||||
// Because of the special ref "refs/for" (AGit) we will need to delay write permission check,
|
||||
// AGit flow needs to write its own ref when the doer has "reader" permission (allowing to create PR).
|
||||
// The real permission check is done in HookPreReceive (routers/private/hook_pre_receive.go).
|
||||
// Here it should relax the permission check for "git push (git-receive-pack)", but not for others like LFS operations.
|
||||
if git.DefaultFeatures().SupportProcReceive && unitType == unit.TypeCode && verb == "git-receive-pack" {
|
||||
mode = perm.AccessModeRead
|
||||
}
|
||||
|
||||
|
||||
@@ -4,26 +4,12 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/web/repo"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
func addOwnerRepoGitHTTPRouters(m *web.Router) {
|
||||
reqGitSignIn := func(ctx *context.Context) {
|
||||
if !setting.Service.RequireSignInView {
|
||||
return
|
||||
}
|
||||
// rely on the results of Contexter
|
||||
if !ctx.IsSigned {
|
||||
// TODO: support digit auth - which would be Authorization header with digit
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`)
|
||||
ctx.Error(http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
m.Group("/{username}/{reponame}", func() {
|
||||
m.Methods("POST,OPTIONS", "/git-upload-pack", repo.ServiceUploadPack)
|
||||
m.Methods("POST,OPTIONS", "/git-receive-pack", repo.ServiceReceivePack)
|
||||
@@ -36,5 +22,5 @@ func addOwnerRepoGitHTTPRouters(m *web.Router) {
|
||||
m.Methods("GET,OPTIONS", "/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38,62}}", repo.GetLooseObject)
|
||||
m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.pack", repo.GetPackFile)
|
||||
m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile)
|
||||
}, optSignInIgnoreCsrf, reqGitSignIn, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb())
|
||||
}, optSignInIgnoreCsrf, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb())
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package actions
|
||||
import (
|
||||
"bytes"
|
||||
stdCtx "context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
@@ -77,7 +78,11 @@ func List(ctx *context.Context) {
|
||||
return
|
||||
} else if !empty {
|
||||
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.Data["NotFoundPrompt"] = ctx.Tr("repo.branch.default_branch_not_exist", ctx.Repo.Repository.DefaultBranch)
|
||||
ctx.NotFound("GetBranchCommit", err)
|
||||
return
|
||||
} else if err != nil {
|
||||
ctx.ServerError("GetBranchCommit", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/models/git"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
@@ -52,12 +53,26 @@ func Activity(ctx *context.Context) {
|
||||
ctx.Data["DateUntil"] = timeUntil
|
||||
ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + ctx.Data["Period"].(string))
|
||||
|
||||
canReadCode := ctx.Repo.CanRead(unit.TypeCode)
|
||||
if canReadCode {
|
||||
// GetActivityStats needs to read the default branch to get some information
|
||||
branchExist, _ := git.IsBranchExist(ctx, ctx.Repo.Repository.ID, ctx.Repo.Repository.DefaultBranch)
|
||||
if !branchExist {
|
||||
ctx.Data["NotFoundPrompt"] = ctx.Tr("repo.branch.default_branch_not_exist", ctx.Repo.Repository.DefaultBranch)
|
||||
ctx.NotFound("", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
if ctx.Data["Activity"], err = activities_model.GetActivityStats(ctx, ctx.Repo.Repository, timeFrom,
|
||||
// TODO: refactor these arguments to a struct
|
||||
ctx.Data["Activity"], err = activities_model.GetActivityStats(ctx, ctx.Repo.Repository, timeFrom,
|
||||
ctx.Repo.CanRead(unit.TypeReleases),
|
||||
ctx.Repo.CanRead(unit.TypeIssues),
|
||||
ctx.Repo.CanRead(unit.TypePullRequests),
|
||||
ctx.Repo.CanRead(unit.TypeCode)); err != nil {
|
||||
canReadCode,
|
||||
)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetActivityStats", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func CodeFrequencyData(ctx *context.Context) {
|
||||
ctx.Status(http.StatusAccepted)
|
||||
return
|
||||
}
|
||||
ctx.ServerError("GetCodeFrequencyData", err)
|
||||
ctx.ServerError("GetContributorStats", err)
|
||||
} else {
|
||||
ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
|
||||
}
|
||||
|
||||
@@ -405,7 +405,6 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
|
||||
ctx.ServerError("OpenRepository", err)
|
||||
return nil
|
||||
}
|
||||
defer ci.HeadGitRepo.Close()
|
||||
} else {
|
||||
ctx.NotFound("ParseCompareInfo", nil)
|
||||
return nil
|
||||
@@ -708,7 +707,7 @@ func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repositor
|
||||
func CompareDiff(ctx *context.Context) {
|
||||
ci := ParseCompareInfo(ctx)
|
||||
defer func() {
|
||||
if ci != nil && ci.HeadGitRepo != nil {
|
||||
if !ctx.Repo.PullRequest.SameRepo && ci != nil && ci.HeadGitRepo != nil {
|
||||
ci.HeadGitRepo.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -127,7 +127,7 @@ func httpBase(ctx *context.Context) *serviceHandler {
|
||||
// Only public pull don't need auth.
|
||||
isPublicPull := repoExist && !repo.IsPrivate && isPull
|
||||
var (
|
||||
askAuth = !isPublicPull || setting.Service.RequireSignInView
|
||||
askAuth = !isPublicPull || setting.Service.RequireSignInViewStrict
|
||||
environ []string
|
||||
)
|
||||
|
||||
|
||||
@@ -418,6 +418,16 @@ func UpdateIssueMilestone(ctx *context.Context) {
|
||||
continue
|
||||
}
|
||||
issue.MilestoneID = milestoneID
|
||||
if milestoneID > 0 {
|
||||
var err error
|
||||
issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetMilestoneByRepoID", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
issue.Milestone = nil
|
||||
}
|
||||
if err := issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
|
||||
ctx.ServerError("ChangeMilestoneAssign", err)
|
||||
return
|
||||
|
||||
@@ -1263,7 +1263,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
||||
|
||||
ci := ParseCompareInfo(ctx)
|
||||
defer func() {
|
||||
if ci != nil && ci.HeadGitRepo != nil {
|
||||
if !ctx.Repo.PullRequest.SameRepo && ci != nil && ci.HeadGitRepo != nil {
|
||||
ci.HeadGitRepo.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -4,12 +4,10 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
contributors_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -26,16 +24,3 @@ func RecentCommits(ctx *context.Context) {
|
||||
|
||||
ctx.HTML(http.StatusOK, tplRecentCommits)
|
||||
}
|
||||
|
||||
// RecentCommitsData returns JSON of recent commits data
|
||||
func RecentCommitsData(ctx *context.Context) {
|
||||
if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
|
||||
if errors.Is(err, contributors_service.ErrAwaitGeneration) {
|
||||
ctx.Status(http.StatusAccepted)
|
||||
return
|
||||
}
|
||||
ctx.ServerError("RecentCommitsData", err)
|
||||
} else {
|
||||
ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"image"
|
||||
"io"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
@@ -79,7 +78,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||
if workFlowErr != nil {
|
||||
ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error())
|
||||
}
|
||||
} else if slices.Contains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) {
|
||||
} else if issue_service.IsCodeOwnerFile(ctx.Repo.TreePath) {
|
||||
if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil {
|
||||
_, warnings := issue_model.GetCodeOwnersFromContent(ctx, data)
|
||||
if len(warnings) > 0 {
|
||||
|
||||
@@ -285,23 +285,23 @@ func Routes() *web.Router {
|
||||
mid = append(mid, repo.GetActiveStopwatch)
|
||||
mid = append(mid, goGet)
|
||||
|
||||
others := web.NewRouter()
|
||||
others.Use(mid...)
|
||||
registerRoutes(others)
|
||||
routes.Mount("", others)
|
||||
webRoutes := web.NewRouter()
|
||||
webRoutes.Use(mid...)
|
||||
webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive())
|
||||
routes.Mount("", webRoutes)
|
||||
return routes
|
||||
}
|
||||
|
||||
var optSignInIgnoreCsrf = verifyAuthWithOptions(&common.VerifyOptions{DisableCSRF: true})
|
||||
|
||||
// registerRoutes register routes
|
||||
func registerRoutes(m *web.Router) {
|
||||
// registerWebRoutes register routes
|
||||
func registerWebRoutes(m *web.Router) {
|
||||
// required to be signed in or signed out
|
||||
reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true})
|
||||
reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true})
|
||||
// optional sign in (if signed in, use the user as doer, if not, no doer)
|
||||
optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView})
|
||||
optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView})
|
||||
optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict})
|
||||
optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView})
|
||||
|
||||
validation.AddBindingRules()
|
||||
|
||||
@@ -1454,6 +1454,8 @@ func registerRoutes(m *web.Router) {
|
||||
m.Group("/{username}/{reponame}/activity", func() {
|
||||
m.Get("", repo.Activity)
|
||||
m.Get("/{period}", repo.Activity)
|
||||
|
||||
m.Group("", func() {
|
||||
m.Group("/contributors", func() {
|
||||
m.Get("", repo.Contributors)
|
||||
m.Get("/data", repo.ContributorsData)
|
||||
@@ -1464,10 +1466,11 @@ func registerRoutes(m *web.Router) {
|
||||
})
|
||||
m.Group("/recent-commits", func() {
|
||||
m.Get("", repo.RecentCommits)
|
||||
m.Get("/data", repo.RecentCommitsData)
|
||||
m.Get("/data", repo.CodeFrequencyData) // "recent-commits" also uses the same data as "code-frequency"
|
||||
})
|
||||
}, reqRepoCodeReader)
|
||||
},
|
||||
optSignIn, context.RepoAssignment, context.RequireRepoReaderOr(unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases),
|
||||
optSignIn, context.RepoAssignment, context.RequireRepoReaderOr(unit.TypeCode, unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases),
|
||||
context.RepoRef(), repo.MustBeNotEmpty,
|
||||
)
|
||||
// end "/{username}/{reponame}/activity"
|
||||
|
||||
@@ -151,7 +151,7 @@ func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
|
||||
|
||||
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
|
||||
ctx.Data["Title"] = "Page Not Found"
|
||||
ctx.HTML(http.StatusNotFound, base.TplName("status/404"))
|
||||
ctx.HTML(http.StatusNotFound, "status/404")
|
||||
}
|
||||
|
||||
// ServerError displays a 500 (Internal Server Error) page and prints the given error, if any.
|
||||
|
||||
@@ -93,7 +93,7 @@ func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, string, any))
|
||||
}
|
||||
|
||||
func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.AccessMode, error) {
|
||||
if setting.Service.RequireSignInView && (doer == nil || doer.IsGhost()) {
|
||||
if setting.Service.RequireSignInViewStrict && (doer == nil || doer.IsGhost()) {
|
||||
return perm.AccessModeNone, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -356,7 +356,9 @@ func RedirectToRepo(ctx *Base, redirectRepoID int64) {
|
||||
if ctx.Req.URL.RawQuery != "" {
|
||||
redirectPath += "?" + ctx.Req.URL.RawQuery
|
||||
}
|
||||
ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect)
|
||||
// Git client needs a 301 redirect by default to follow the new location
|
||||
// It's not documentated in git documentation, but it's the behavior of git client
|
||||
ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusMovedPermanently)
|
||||
}
|
||||
|
||||
func repoAssignment(ctx *Context, repo *repo_model.Repository) {
|
||||
|
||||
@@ -121,7 +121,7 @@ func checkStorage(opts *checkStorageOptions) func(ctx context.Context, logger lo
|
||||
storer: storage.LFS,
|
||||
isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) {
|
||||
// The oid of an LFS stored object is the name but with all the path.Separators removed
|
||||
oid := strings.ReplaceAll(path, "/", "")
|
||||
oid := strings.ReplaceAll(strings.ReplaceAll(path, "\\", ""), "/", "")
|
||||
exists, err := git.ExistsLFSObject(ctx, oid)
|
||||
return !exists, err
|
||||
},
|
||||
|
||||
@@ -92,8 +92,12 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
|
||||
|
||||
var reviewNotifiers []*ReviewRequestNotifier
|
||||
if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) {
|
||||
if err := issue.LoadPullRequest(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
reviewNotifiers, err = PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest)
|
||||
reviewNotifiers, err = PullRequestCodeOwnersReview(ctx, issue.PullRequest)
|
||||
if err != nil {
|
||||
log.Error("PullRequestCodeOwnersReview: %v", err)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package issue
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
@@ -40,20 +41,27 @@ type ReviewRequestNotifier struct {
|
||||
ReviewTeam *org_model.Team
|
||||
}
|
||||
|
||||
func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) {
|
||||
files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}
|
||||
var codeOwnerFiles = []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}
|
||||
|
||||
func IsCodeOwnerFile(f string) bool {
|
||||
return slices.Contains(codeOwnerFiles, f)
|
||||
}
|
||||
|
||||
func PullRequestCodeOwnersReview(ctx context.Context, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) {
|
||||
if err := pr.LoadIssue(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
issue := pr.Issue
|
||||
if pr.IsWorkInProgress(ctx) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := pr.LoadHeadRepo(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := pr.LoadBaseRepo(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pr.Issue.Repo = pr.BaseRepo
|
||||
|
||||
if pr.BaseRepo.IsFork {
|
||||
return nil, nil
|
||||
@@ -71,7 +79,7 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue,
|
||||
}
|
||||
|
||||
var data string
|
||||
for _, file := range files {
|
||||
for _, file := range codeOwnerFiles {
|
||||
if blob, err := commit.GetBlobByPath(file); err == nil {
|
||||
data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
|
||||
if err == nil {
|
||||
@@ -79,8 +87,14 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue,
|
||||
}
|
||||
}
|
||||
}
|
||||
if data == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rules, _ := issues_model.GetCodeOwnersFromContent(ctx, data)
|
||||
if len(rules) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// get the mergebase
|
||||
mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName())
|
||||
@@ -116,13 +130,31 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// load all reviews from database
|
||||
latestReivews, _, err := issues_model.GetReviewsByIssueID(ctx, pr.IssueID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contain := func(list issues_model.ReviewList, u *user_model.User) bool {
|
||||
for _, review := range list {
|
||||
if review.ReviewerTeamID == 0 && review.ReviewerID == u.ID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for _, u := range uniqUsers {
|
||||
if u.ID != issue.Poster.ID {
|
||||
if u.ID != issue.Poster.ID && !contain(latestReivews, u) {
|
||||
comment, err := issues_model.AddReviewRequest(ctx, issue, u, issue.Poster)
|
||||
if err != nil {
|
||||
log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err)
|
||||
return nil, err
|
||||
}
|
||||
if comment == nil { // comment maybe nil if review type is ReviewTypeRequest
|
||||
continue
|
||||
}
|
||||
notifiers = append(notifiers, &ReviewRequestNotifier{
|
||||
Comment: comment,
|
||||
IsAdd: true,
|
||||
@@ -130,12 +162,16 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, t := range uniqTeams {
|
||||
comment, err := issues_model.AddTeamReviewRequest(ctx, issue, t, issue.Poster)
|
||||
if err != nil {
|
||||
log.Warn("Failed add assignee team: %s to PR review: %s#%d, error: %s", t.Name, pr.BaseRepo.Name, pr.ID, err)
|
||||
return nil, err
|
||||
}
|
||||
if comment == nil { // comment maybe nil if review type is ReviewTypeRequest
|
||||
continue
|
||||
}
|
||||
notifiers = append(notifiers, &ReviewRequestNotifier{
|
||||
Comment: comment,
|
||||
IsAdd: true,
|
||||
|
||||
@@ -7,7 +7,7 @@ package migrations
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/google/go-github/v61/github"
|
||||
"github.com/google/go-github/v71/github"
|
||||
)
|
||||
|
||||
// ErrRepoNotCreated returns the error that repository not created
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/proxy"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
|
||||
"github.com/google/go-github/v61/github"
|
||||
"github.com/google/go-github/v71/github"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
@@ -135,7 +135,7 @@ func (g *GithubDownloaderV3) LogString() string {
|
||||
func (g *GithubDownloaderV3) addClient(client *http.Client, baseURL string) {
|
||||
githubClient := github.NewClient(client)
|
||||
if baseURL != "https://github.com" {
|
||||
githubClient, _ = github.NewClient(client).WithEnterpriseURLs(baseURL, baseURL)
|
||||
githubClient, _ = githubClient.WithEnterpriseURLs(baseURL, baseURL)
|
||||
}
|
||||
g.clients = append(g.clients, githubClient)
|
||||
g.rates = append(g.rates, nil)
|
||||
@@ -448,9 +448,11 @@ func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool,
|
||||
if !g.SkipReactions {
|
||||
for i := 1; ; i++ {
|
||||
g.waitAndPickClient()
|
||||
res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, issue.GetNumber(), &github.ListOptions{
|
||||
res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, issue.GetNumber(), &github.ListReactionOptions{
|
||||
ListOptions: github.ListOptions{
|
||||
Page: i,
|
||||
PerPage: perPage,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
@@ -534,9 +536,11 @@ func (g *GithubDownloaderV3) getComments(commentable base.Commentable) ([]*base.
|
||||
if !g.SkipReactions {
|
||||
for i := 1; ; i++ {
|
||||
g.waitAndPickClient()
|
||||
res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{
|
||||
res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListReactionOptions{
|
||||
ListOptions: github.ListOptions{
|
||||
Page: i,
|
||||
PerPage: g.maxPerPage,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -609,9 +613,11 @@ func (g *GithubDownloaderV3) GetAllComments(page, perPage int) ([]*base.Comment,
|
||||
if !g.SkipReactions {
|
||||
for i := 1; ; i++ {
|
||||
g.waitAndPickClient()
|
||||
res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{
|
||||
res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListReactionOptions{
|
||||
ListOptions: github.ListOptions{
|
||||
Page: i,
|
||||
PerPage: g.maxPerPage,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
@@ -680,9 +686,11 @@ func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullReq
|
||||
if !g.SkipReactions {
|
||||
for i := 1; ; i++ {
|
||||
g.waitAndPickClient()
|
||||
res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, pr.GetNumber(), &github.ListOptions{
|
||||
res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, pr.GetNumber(), &github.ListReactionOptions{
|
||||
ListOptions: github.ListOptions{
|
||||
Page: i,
|
||||
PerPage: perPage,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
@@ -767,9 +775,11 @@ func (g *GithubDownloaderV3) convertGithubReviewComments(cs []*github.PullReques
|
||||
if !g.SkipReactions {
|
||||
for i := 1; ; i++ {
|
||||
g.waitAndPickClient()
|
||||
res, resp, err := g.getClient().Reactions.ListPullRequestCommentReactions(g.ctx, g.repoOwner, g.repoName, c.GetID(), &github.ListOptions{
|
||||
res, resp, err := g.getClient().Reactions.ListPullRequestCommentReactions(g.ctx, g.repoOwner, g.repoName, c.GetID(), &github.ListReactionOptions{
|
||||
ListOptions: github.ListOptions{
|
||||
Page: i,
|
||||
PerPage: g.maxPerPage,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -879,3 +889,18 @@ func (g *GithubDownloaderV3) GetReviews(reviewable base.Reviewable) ([]*base.Rev
|
||||
}
|
||||
return allReviews, nil
|
||||
}
|
||||
|
||||
// FormatCloneURL add authentication into remote URLs
|
||||
func (g *GithubDownloaderV3) FormatCloneURL(opts MigrateOptions, remoteAddr string) (string, error) {
|
||||
u, err := url.Parse(remoteAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(opts.AuthToken) > 0 {
|
||||
// "multiple tokens" are used to benefit more "API rate limit quota"
|
||||
// git clone doesn't count for rate limits, so only use the first token.
|
||||
// source: https://github.com/orgs/community/discussions/44515
|
||||
u.User = url.UserPassword("oauth2", strings.Split(opts.AuthToken, ",")[0])
|
||||
}
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
base "code.gitea.io/gitea/modules/migration"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGitHubDownloadRepo(t *testing.T) {
|
||||
@@ -429,3 +430,36 @@ func TestGitHubDownloadRepo(t *testing.T) {
|
||||
},
|
||||
}, reviews)
|
||||
}
|
||||
|
||||
func TestGithubMultiToken(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
token string
|
||||
expectedCloneURL string
|
||||
}{
|
||||
{
|
||||
desc: "Single Token",
|
||||
token: "single_token",
|
||||
expectedCloneURL: "https://oauth2:single_token@github.com",
|
||||
},
|
||||
{
|
||||
desc: "Multi Token",
|
||||
token: "token1,token2",
|
||||
expectedCloneURL: "https://oauth2:token1@github.com",
|
||||
},
|
||||
}
|
||||
factory := GithubDownloaderV3Factory{}
|
||||
|
||||
for _, tC := range testCases {
|
||||
t.Run(tC.desc, func(t *testing.T) {
|
||||
opts := base.MigrateOptions{CloneAddr: "https://github.com/go-gitea/gitea", AuthToken: tC.token}
|
||||
client, err := factory.New(context.Background(), opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
cloneURL, err := client.FormatCloneURL(opts, "https://github.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tC.expectedCloneURL, cloneURL)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ func createOrUpdateConfigFile(ctx context.Context, repo *repo_model.Repository,
|
||||
"Initialize Cargo Config",
|
||||
func(t *files_service.TemporaryUploadRepository) error {
|
||||
var b bytes.Buffer
|
||||
err := json.NewEncoder(&b).Encode(BuildConfig(owner, setting.Service.RequireSignInView || owner.Visibility != structs.VisibleTypePublic || repo.IsPrivate))
|
||||
err := json.NewEncoder(&b).Encode(BuildConfig(owner, setting.Service.RequireSignInViewStrict || owner.Visibility != structs.VisibleTypePublic || repo.IsPrivate))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -408,7 +408,6 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []
|
||||
files = append(files, f)
|
||||
}
|
||||
}
|
||||
packageVersion := fmt.Sprintf("%s-%s", pd.FileMetadata.Version, pd.FileMetadata.Release)
|
||||
packages = append(packages, &Package{
|
||||
Type: "rpm",
|
||||
Name: pd.Package.Name,
|
||||
@@ -437,7 +436,7 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []
|
||||
Archive: pd.FileMetadata.ArchiveSize,
|
||||
},
|
||||
Location: Location{
|
||||
Href: fmt.Sprintf("package/%s/%s/%s/%s-%s.%s.rpm", pd.Package.Name, packageVersion, pd.FileMetadata.Architecture, pd.Package.Name, packageVersion, pd.FileMetadata.Architecture),
|
||||
Href: fmt.Sprintf("package/%s/%s/%s/%s-%s.%s.rpm", pd.Package.Name, pd.Version.Version, pd.FileMetadata.Architecture, pd.Package.Name, pd.Version.Version, pd.FileMetadata.Architecture),
|
||||
},
|
||||
Format: Format{
|
||||
License: pd.VersionMetadata.License,
|
||||
|
||||
@@ -189,7 +189,15 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U
|
||||
}
|
||||
defer releaser()
|
||||
defer func() {
|
||||
go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "")
|
||||
go AddTestPullRequestTask(TestPullRequestOptions{
|
||||
RepoID: pr.BaseRepo.ID,
|
||||
Doer: doer,
|
||||
Branch: pr.BaseBranch,
|
||||
IsSync: false,
|
||||
IsForcePush: false,
|
||||
OldCommitID: "",
|
||||
NewCommitID: "",
|
||||
})
|
||||
}()
|
||||
|
||||
_, err = doMergeAndPush(ctx, pr, doer, mergeStyle, expectedHeadCommitID, message, repo_module.PushTriggerPRMergeToBase)
|
||||
|
||||
@@ -176,7 +176,7 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
|
||||
}
|
||||
|
||||
if !pr.IsWorkInProgress(ctx) {
|
||||
reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(ctx, issue, pr)
|
||||
reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(ctx, pr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -349,19 +349,29 @@ func checkForInvalidation(ctx context.Context, requests issues_model.PullRequest
|
||||
return nil
|
||||
}
|
||||
|
||||
type TestPullRequestOptions struct {
|
||||
RepoID int64
|
||||
Doer *user_model.User
|
||||
Branch string
|
||||
IsSync bool // True means it's a pull request synchronization, false means it's triggered for pull request merging or updating
|
||||
IsForcePush bool
|
||||
OldCommitID string
|
||||
NewCommitID string
|
||||
}
|
||||
|
||||
// AddTestPullRequestTask adds new test tasks by given head/base repository and head/base branch,
|
||||
// and generate new patch for testing as needed.
|
||||
func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, isSync bool, oldCommitID, newCommitID string) {
|
||||
log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", repoID, branch)
|
||||
func AddTestPullRequestTask(opts TestPullRequestOptions) {
|
||||
log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", opts.RepoID, opts.Branch)
|
||||
graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) {
|
||||
// There is no sensible way to shut this down ":-("
|
||||
// If you don't let it run all the way then you will lose data
|
||||
// TODO: graceful: AddTestPullRequestTask needs to become a queue!
|
||||
|
||||
// GetUnmergedPullRequestsByHeadInfo() only return open and unmerged PR.
|
||||
prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, repoID, branch)
|
||||
prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, opts.RepoID, opts.Branch)
|
||||
if err != nil {
|
||||
log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", repoID, branch, err)
|
||||
log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", opts.RepoID, opts.Branch, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -377,25 +387,24 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
|
||||
}
|
||||
|
||||
AddToTaskQueue(ctx, pr)
|
||||
comment, err := CreatePushPullComment(ctx, doer, pr, oldCommitID, newCommitID)
|
||||
comment, err := CreatePushPullComment(ctx, opts.Doer, pr, opts.OldCommitID, opts.NewCommitID)
|
||||
if err == nil && comment != nil {
|
||||
notify_service.PullRequestPushCommits(ctx, doer, pr, comment)
|
||||
notify_service.PullRequestPushCommits(ctx, opts.Doer, pr, comment)
|
||||
}
|
||||
}
|
||||
|
||||
if isSync {
|
||||
requests := issues_model.PullRequestList(prs)
|
||||
if err = requests.LoadAttributes(ctx); err != nil {
|
||||
if opts.IsSync {
|
||||
if err = issues_model.PullRequestList(prs).LoadAttributes(ctx); err != nil {
|
||||
log.Error("PullRequestList.LoadAttributes: %v", err)
|
||||
}
|
||||
if invalidationErr := checkForInvalidation(ctx, requests, repoID, doer, branch); invalidationErr != nil {
|
||||
if invalidationErr := checkForInvalidation(ctx, prs, opts.RepoID, opts.Doer, opts.Branch); invalidationErr != nil {
|
||||
log.Error("checkForInvalidation: %v", invalidationErr)
|
||||
}
|
||||
if err == nil {
|
||||
for _, pr := range prs {
|
||||
objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
|
||||
if newCommitID != "" && newCommitID != objectFormat.EmptyObjectID().String() {
|
||||
changed, err := checkIfPRContentChanged(ctx, pr, oldCommitID, newCommitID)
|
||||
if opts.NewCommitID != "" && opts.NewCommitID != objectFormat.EmptyObjectID().String() {
|
||||
changed, err := checkIfPRContentChanged(ctx, pr, opts.OldCommitID, opts.NewCommitID)
|
||||
if err != nil {
|
||||
log.Error("checkIfPRContentChanged: %v", err)
|
||||
}
|
||||
@@ -411,12 +420,12 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
|
||||
log.Error("GetFirstMatchProtectedBranchRule: %v", err)
|
||||
}
|
||||
if pb != nil && pb.DismissStaleApprovals {
|
||||
if err := DismissApprovalReviews(ctx, doer, pr); err != nil {
|
||||
if err := DismissApprovalReviews(ctx, opts.Doer, pr); err != nil {
|
||||
log.Error("DismissApprovalReviews: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, newCommitID); err != nil {
|
||||
if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, opts.NewCommitID); err != nil {
|
||||
log.Error("MarkReviewsAsNotStale: %v", err)
|
||||
}
|
||||
divergence, err := GetDiverging(ctx, pr)
|
||||
@@ -430,15 +439,25 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
|
||||
}
|
||||
}
|
||||
|
||||
notify_service.PullRequestSynchronized(ctx, doer, pr)
|
||||
if !pr.IsWorkInProgress(ctx) {
|
||||
reviewNotifiers, err := issue_service.PullRequestCodeOwnersReview(ctx, pr)
|
||||
if err != nil {
|
||||
log.Error("PullRequestCodeOwnersReview: %v", err)
|
||||
}
|
||||
if len(reviewNotifiers) > 0 {
|
||||
issue_service.ReviewRequestNotify(ctx, pr.Issue, opts.Doer, reviewNotifiers)
|
||||
}
|
||||
}
|
||||
|
||||
notify_service.PullRequestSynchronized(ctx, opts.Doer, pr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", repoID, branch)
|
||||
prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, repoID, branch)
|
||||
log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", opts.RepoID, opts.Branch)
|
||||
prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, opts.RepoID, opts.Branch)
|
||||
if err != nil {
|
||||
log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", repoID, branch, err)
|
||||
log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", opts.RepoID, opts.Branch, err)
|
||||
return
|
||||
}
|
||||
for _, pr := range prs {
|
||||
|
||||
@@ -42,7 +42,15 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.
|
||||
|
||||
if rebase {
|
||||
defer func() {
|
||||
go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "")
|
||||
go AddTestPullRequestTask(TestPullRequestOptions{
|
||||
RepoID: pr.BaseRepo.ID,
|
||||
Doer: doer,
|
||||
Branch: pr.BaseBranch,
|
||||
IsSync: false,
|
||||
IsForcePush: false,
|
||||
OldCommitID: "",
|
||||
NewCommitID: "",
|
||||
})
|
||||
}()
|
||||
|
||||
return updateHeadByRebaseOnToBase(ctx, pr, doer)
|
||||
@@ -83,7 +91,15 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.
|
||||
_, err = doMergeAndPush(ctx, reversePR, doer, repo_model.MergeStyleMerge, "", message, repository.PushTriggerPRUpdateWithBase)
|
||||
|
||||
defer func() {
|
||||
go AddTestPullRequestTask(doer, reversePR.HeadRepo.ID, reversePR.HeadBranch, false, "", "")
|
||||
go AddTestPullRequestTask(TestPullRequestOptions{
|
||||
RepoID: reversePR.HeadRepo.ID,
|
||||
Doer: doer,
|
||||
Branch: reversePR.HeadBranch,
|
||||
IsSync: false,
|
||||
IsForcePush: false,
|
||||
OldCommitID: "",
|
||||
NewCommitID: "",
|
||||
})
|
||||
}()
|
||||
|
||||
return err
|
||||
|
||||
@@ -170,7 +170,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
|
||||
branch := opts.RefFullName.BranchName()
|
||||
if !opts.IsDelRef() {
|
||||
log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name)
|
||||
go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true, opts.OldCommitID, opts.NewCommitID)
|
||||
|
||||
newCommit, err := gitRepo.GetCommit(opts.NewCommitID)
|
||||
if err != nil {
|
||||
@@ -212,6 +211,17 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
|
||||
log.Error("IsForcePush %s:%s failed: %v", repo.FullName(), branch, err)
|
||||
}
|
||||
|
||||
// only update branch can trigger pull request task because the pull request hasn't been created yet when creaing a branch
|
||||
go pull_service.AddTestPullRequestTask(pull_service.TestPullRequestOptions{
|
||||
RepoID: repo.ID,
|
||||
Doer: pusher,
|
||||
Branch: branch,
|
||||
IsSync: true,
|
||||
IsForcePush: isForcePush,
|
||||
OldCommitID: opts.OldCommitID,
|
||||
NewCommitID: opts.NewCommitID,
|
||||
})
|
||||
|
||||
if isForcePush {
|
||||
log.Trace("Push %s is a force push", opts.NewCommitID)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"unicode/utf8"
|
||||
|
||||
webhook_model "code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -101,6 +102,13 @@ var (
|
||||
redColor = color("ff3232")
|
||||
)
|
||||
|
||||
// https://discord.com/developers/docs/resources/message#embed-object-embed-limits
|
||||
// Discord has some limits in place for the embeds.
|
||||
// According to some tests, there is no consistent limit for different character sets.
|
||||
// For example: 4096 ASCII letters are allowed, but only 2490 emoji characters are allowed.
|
||||
// To keep it simple, we currently truncate at 2000.
|
||||
const discordDescriptionCharactersLimit = 2000
|
||||
|
||||
type discordConvertor struct {
|
||||
Username string
|
||||
AvatarURL string
|
||||
@@ -307,7 +315,7 @@ func (d discordConvertor) createPayload(s *api.User, title, text, url string, co
|
||||
Embeds: []DiscordEmbed{
|
||||
{
|
||||
Title: title,
|
||||
Description: text,
|
||||
Description: base.TruncateString(text, discordDescriptionCharactersLimit),
|
||||
URL: url,
|
||||
Color: color,
|
||||
Author: DiscordEmbedAuthor{
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
<dt>{{ctx.Locale.Tr "admin.config.enable_openid_signin"}}</dt>
|
||||
<dd>{{svg (Iif .Service.EnableOpenIDSignIn "octicon-check" "octicon-x")}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.require_sign_in_view"}}</dt>
|
||||
<dd>{{svg (Iif .Service.RequireSignInView "octicon-check" "octicon-x")}}</dd>
|
||||
<dd>{{svg (Iif .Service.RequireSignInViewStrict "octicon-check" "octicon-x")}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.mail_notify"}}</dt>
|
||||
<dd>{{svg (Iif .Service.EnableNotifyMail "octicon-check" "octicon-x")}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.enable_captcha"}}</dt>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}}
|
||||
|
||||
<div class="ui container tw-max-w-full">
|
||||
<div class="tw-flex tw-justify-between tw-items-center tw-mb-4 tw-gap-3">
|
||||
<h2 class="tw-mb-0 tw-flex-1 tw-break-anywhere">{{.Project.Title}}</h2>
|
||||
<div class="project-toolbar-right">
|
||||
<div class="ui secondary filter menu labels">
|
||||
<div class="flex-text-block tw-flex-wrap tw-mb-4">
|
||||
<h2 class="tw-mb-0">{{.Project.Title}}</h2>
|
||||
<div class="tw-flex-1"></div>
|
||||
<div class="ui secondary menu tw-m-0">
|
||||
{{$queryLink := QueryBuild "?" "labels" .SelectLabels "assignee" $.AssigneeID "archived_labels" (Iif $.ShowArchivedLabels "true")}}
|
||||
|
||||
{{template "repo/issue/filter_item_label" dict "Labels" .Labels "QueryLink" $queryLink "SupportArchivedLabel" true}}
|
||||
@@ -19,7 +19,6 @@
|
||||
"TextNegativeOne" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee")
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
{{if $canWriteProject}}
|
||||
<div class="ui compact mini menu">
|
||||
<a class="item" href="{{.Link}}/edit?redirect=project">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<button class="ui primary button js-btn-clone-panel">
|
||||
{{svg "octicon-code" 16}}
|
||||
<span>Code</span>
|
||||
<span>{{ctx.Locale.Tr "repo.code"}}</span>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
</button>
|
||||
<div class="clone-panel-popup tippy-target">
|
||||
|
||||
@@ -10,12 +10,7 @@
|
||||
<div class="ui attached segment">
|
||||
{{template "base/alert" .}}
|
||||
{{template "repo/create_helper" .}}
|
||||
|
||||
{{if not .CanCreateRepo}}
|
||||
<div class="ui negative message">
|
||||
<p>{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
<div id="create-repo-error-message" class="ui negative message tw-text-center tw-hidden"></div>
|
||||
<div class="inline required field {{if .Err_Owner}}error{{end}}">
|
||||
<label>{{ctx.Locale.Tr "repo.owner"}}</label>
|
||||
<div class="ui selection owner dropdown">
|
||||
@@ -26,7 +21,11 @@
|
||||
</span>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="menu">
|
||||
<div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}">
|
||||
<div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}"
|
||||
{{if not .CanCreateRepo}}
|
||||
data-create-repo-disallowed-prompt="{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}"
|
||||
{{end}}
|
||||
>
|
||||
{{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}}
|
||||
<span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span>
|
||||
</div>
|
||||
@@ -209,7 +208,7 @@
|
||||
<br>
|
||||
<div class="inline field">
|
||||
<label></label>
|
||||
<button class="ui primary button{{if not .CanCreateRepo}} disabled{{end}}">
|
||||
<button class="ui primary button">
|
||||
{{ctx.Locale.Tr "repo.create_repo"}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="flex-item">
|
||||
<div class="flex-item-main">
|
||||
<div class="flex-item-title">
|
||||
<a class="item muted" href="{{.Link}}/releases">
|
||||
<a class="item muted" href="{{.RepoLink}}/releases">
|
||||
{{ctx.Locale.Tr "repo.releases"}}
|
||||
<span class="ui small label">{{.NumReleases}}</span>
|
||||
</a>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
{{if $.Page.LinkedPRs}}
|
||||
{{range index $.Page.LinkedPRs .ID}}
|
||||
<div class="meta tw-my-1">
|
||||
<a href="{{$.Issue.Repo.Link}}/pulls/{{.Index}}">
|
||||
<a href="{{.Repo.Link}}/pulls/{{.Index}}">
|
||||
<span class="tw-m-0 text {{if .PullRequest.HasMerged}}purple{{else if .IsClosed}}red{{else}}green{{end}}">{{svg "octicon-git-merge" 16 "tw-mr-1 tw-align-middle"}}</span>
|
||||
<span class="tw-align-middle">{{.Title}} <span class="text light grey">#{{.Index}}</span></span>
|
||||
</a>
|
||||
|
||||
@@ -14,9 +14,10 @@
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="list-header">
|
||||
{{template "repo/issue/navbar" .}}
|
||||
<div class="list-header flex-text-block">
|
||||
{{template "repo/issue/search" .}}
|
||||
<a class="ui small button" href="{{.RepoLink}}/labels">{{ctx.Locale.Tr "repo.labels"}}</a>
|
||||
<a class="ui small button" href="{{.RepoLink}}/milestones">{{ctx.Locale.Tr "repo.milestones"}}</a>
|
||||
{{if not .Repository.IsArchived}}
|
||||
{{if .PageIsIssueList}}
|
||||
<a class="ui small primary button issue-list-new" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
{{$canReadCode := $.Permission.CanRead ctx.Consts.RepoUnitTypeCode}}
|
||||
|
||||
<div class="ui fluid vertical menu">
|
||||
<a class="{{if .PageIsPulse}}active {{end}}item" href="{{.RepoLink}}/activity">
|
||||
{{ctx.Locale.Tr "repo.activity.navbar.pulse"}}
|
||||
</a>
|
||||
{{if $canReadCode}}
|
||||
<a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors">
|
||||
{{ctx.Locale.Tr "repo.activity.navbar.contributors"}}
|
||||
</a>
|
||||
@@ -11,4 +14,5 @@
|
||||
<a class="{{if .PageIsRecentCommits}}active{{end}} item" href="{{.RepoLink}}/activity/recent-commits">
|
||||
{{ctx.Locale.Tr "repo.activity.navbar.recent_commits"}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content repository projects view-project">
|
||||
{{template "repo/header" .}}
|
||||
<div class="ui container padded">
|
||||
<div class="tw-flex tw-justify-between tw-items-center tw-mb-4">
|
||||
{{template "repo/issue/navbar" .}}
|
||||
<div class="flex-text-block tw-justify-end tw-mb-4">
|
||||
<a class="ui small button" href="{{.RepoLink}}/labels">{{ctx.Locale.Tr "repo.labels"}}</a>
|
||||
<a class="ui small button" href="{{.RepoLink}}/milestones">{{ctx.Locale.Tr "repo.milestones"}}</a>
|
||||
<a class="ui small primary button" href="{{.RepoLink}}/issues/new/choose?project={{.Project.ID}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="ui stackable grid">
|
||||
<div class="ui eight wide column">
|
||||
<div class="ui header">
|
||||
<a class="file-revisions-btn ui basic button" title="{{ctx.Locale.Tr "repo.wiki.back_to_wiki"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}"><span>{{.revision}}</span> {{svg "octicon-home"}}</a>
|
||||
<a class="file-revisions-btn ui basic button" title="{{ctx.Locale.Tr "repo.wiki.back_to_wiki"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}">{{if .revision}}<span>{{.revision}}</span> {{end}}{{svg "octicon-home"}}</a>
|
||||
{{$title}}
|
||||
<div class="ui sub header tw-break-anywhere">
|
||||
{{$timeSince := DateUtils.TimeSince .Author.When}}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<div class="ui dividing header">
|
||||
<div class="flex-text-block tw-flex-wrap tw-justify-end">
|
||||
<div class="flex-text-block tw-flex-1 tw-min-w-[300px]">
|
||||
<a class="file-revisions-btn ui basic button" title="{{ctx.Locale.Tr "repo.wiki.file_revision"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_revision" ><span>{{.CommitCount}}</span> {{svg "octicon-history"}}</a>
|
||||
<a class="file-revisions-btn ui basic button" title="{{ctx.Locale.Tr "repo.wiki.file_revision"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_revision" >{{if .CommitCount}}<span>{{.CommitCount}}</span> {{end}}{{svg "octicon-history"}}</a>
|
||||
<div class="tw-flex-1 gt-ellipsis">
|
||||
{{$title}}
|
||||
<div class="ui sub header gt-ellipsis">
|
||||
|
||||
13
templates/swagger/v1_json.tmpl
generated
13
templates/swagger/v1_json.tmpl
generated
@@ -19616,7 +19616,18 @@
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-go-name": "Scopes"
|
||||
"x-go-name": "Scopes",
|
||||
"example": [
|
||||
"all",
|
||||
"read:activitypub",
|
||||
"read:issue",
|
||||
"write:misc",
|
||||
"read:notification",
|
||||
"read:organization",
|
||||
"read:package",
|
||||
"read:repository",
|
||||
"read:user"
|
||||
]
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -24,7 +26,9 @@ import (
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
release_service "code.gitea.io/gitea/services/release"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
@@ -451,3 +455,109 @@ func TestCreateDeleteRefEvent(t *testing.T) {
|
||||
assert.NotNil(t, run)
|
||||
})
|
||||
}
|
||||
|
||||
func TestClosePullRequestWithPath(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
// user2 is the owner of the base repo
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user2Token := getTokenForLoggedInUser(t, loginUser(t, user2.Name), auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
// user4 is the owner of the fork repo
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
user4Token := getTokenForLoggedInUser(t, loginUser(t, user4.Name), auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
// create the base repo
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
|
||||
Name: "close-pull-request-with-path",
|
||||
Private: false,
|
||||
Readme: "Default",
|
||||
AutoInit: true,
|
||||
DefaultBranch: "main",
|
||||
}).AddTokenAuth(user2Token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
var apiBaseRepo api.Repository
|
||||
DecodeJSON(t, resp, &apiBaseRepo)
|
||||
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiBaseRepo.ID})
|
||||
user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
// init the workflow
|
||||
wfTreePath := ".gitea/workflows/pull.yml"
|
||||
wfFileContent := `name: Pull Request
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
paths:
|
||||
- 'app/**'
|
||||
jobs:
|
||||
echo:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'Hello World'
|
||||
`
|
||||
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", baseRepo.OwnerName, baseRepo.Name, wfTreePath), &api.CreateFileOptions{
|
||||
FileOptions: api.FileOptions{
|
||||
BranchName: baseRepo.DefaultBranch,
|
||||
Message: "create " + wfTreePath,
|
||||
Author: api.Identity{
|
||||
Name: user2.Name,
|
||||
Email: user2.Email,
|
||||
},
|
||||
Committer: api.Identity{
|
||||
Name: user2.Name,
|
||||
Email: user2.Email,
|
||||
},
|
||||
Dates: api.CommitDateOptions{
|
||||
Author: time.Now(),
|
||||
Committer: time.Now(),
|
||||
},
|
||||
},
|
||||
ContentBase64: base64.StdEncoding.EncodeToString([]byte(wfFileContent)),
|
||||
}).AddTokenAuth(user2Token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
// user4 forks the repo
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseRepo.OwnerName, baseRepo.Name),
|
||||
&api.CreateForkOption{
|
||||
Name: util.ToPointer("close-pull-request-with-path-fork"),
|
||||
}).AddTokenAuth(user4Token)
|
||||
resp = MakeRequest(t, req, http.StatusAccepted)
|
||||
var apiForkRepo api.Repository
|
||||
DecodeJSON(t, resp, &apiForkRepo)
|
||||
forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiForkRepo.ID})
|
||||
user4APICtx := NewAPITestContext(t, user4.Name, forkRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
// user4 creates a pull request to add file "app/main.go"
|
||||
doAPICreateFile(user4APICtx, "app/main.go", &api.CreateFileOptions{
|
||||
FileOptions: api.FileOptions{
|
||||
NewBranchName: "user4/add-main",
|
||||
Message: "create main.go",
|
||||
Author: api.Identity{
|
||||
Name: user4.Name,
|
||||
Email: user4.Email,
|
||||
},
|
||||
Committer: api.Identity{
|
||||
Name: user4.Name,
|
||||
Email: user4.Email,
|
||||
},
|
||||
Dates: api.CommitDateOptions{
|
||||
Author: time.Now(),
|
||||
Committer: time.Now(),
|
||||
},
|
||||
},
|
||||
ContentBase64: base64.StdEncoding.EncodeToString([]byte("// main.go")),
|
||||
})(t)
|
||||
apiPull, err := doAPICreatePullRequest(user4APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, user4.Name+":user4/add-main")(t)
|
||||
assert.NoError(t, err)
|
||||
|
||||
doAPIMergePullRequest(user2APICtx, baseRepo.OwnerName, baseRepo.Name, apiPull.Index)(t)
|
||||
|
||||
pullRequest := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
|
||||
|
||||
// load and compare ActionRun
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: baseRepo.ID}))
|
||||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID})
|
||||
assert.Equal(t, actions_module.GithubEventPullRequest, actionRun.TriggerEvent)
|
||||
assert.Equal(t, pullRequest.MergedCommitID, actionRun.CommitSHA)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -148,9 +148,9 @@ func TestAPIOrgEditBadVisibility(t *testing.T) {
|
||||
|
||||
func TestAPIOrgDeny(t *testing.T) {
|
||||
onGiteaRun(t, func(*testing.T, *url.URL) {
|
||||
setting.Service.RequireSignInView = true
|
||||
setting.Service.RequireSignInViewStrict = true
|
||||
defer func() {
|
||||
setting.Service.RequireSignInView = false
|
||||
setting.Service.RequireSignInViewStrict = false
|
||||
}()
|
||||
|
||||
orgName := "user1_org"
|
||||
|
||||
@@ -111,7 +111,7 @@ func TestPackageContainer(t *testing.T) {
|
||||
AddTokenAuth(anonymousToken)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
defer test.MockVariableValue(&setting.Service.RequireSignInView, true)()
|
||||
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL))
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -131,11 +132,7 @@ func TestPackageGeneric(t *testing.T) {
|
||||
|
||||
t.Run("RequireSignInView", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
setting.Service.RequireSignInView = true
|
||||
defer func() {
|
||||
setting.Service.RequireSignInView = false
|
||||
}()
|
||||
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
|
||||
|
||||
req = NewRequest(t, "GET", url+"/dummy.bin")
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPackageSwift(t *testing.T) {
|
||||
@@ -34,6 +35,7 @@ func TestPackageSwift(t *testing.T) {
|
||||
packageName := "test_package"
|
||||
packageID := packageScope + "." + packageName
|
||||
packageVersion := "1.0.3"
|
||||
packageVersion2 := "1.0.4"
|
||||
packageAuthor := "KN4CK3R"
|
||||
packageDescription := "Gitea Test Package"
|
||||
packageRepositoryURL := "https://gitea.io/gitea/gitea"
|
||||
@@ -183,6 +185,94 @@ func TestPackageSwift(t *testing.T) {
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("UploadMultipart", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
uploadPackage := func(t *testing.T, url string, expectedStatus int, sr io.Reader, metadata string) {
|
||||
var body bytes.Buffer
|
||||
mpw := multipart.NewWriter(&body)
|
||||
|
||||
// Read the source archive content
|
||||
sourceContent, err := io.ReadAll(sr)
|
||||
assert.NoError(t, err)
|
||||
mpw.WriteField("source-archive", string(sourceContent))
|
||||
|
||||
if metadata != "" {
|
||||
mpw.WriteField("metadata", metadata)
|
||||
}
|
||||
|
||||
mpw.Close()
|
||||
|
||||
req := NewRequestWithBody(t, "PUT", url, &body).
|
||||
SetHeader("Content-Type", mpw.FormDataContentType()).
|
||||
SetHeader("Accept", swift_router.AcceptJSON).
|
||||
AddBasicAuth(user.Name)
|
||||
MakeRequest(t, req, expectedStatus)
|
||||
}
|
||||
|
||||
createArchive := func(files map[string]string) *bytes.Buffer {
|
||||
var buf bytes.Buffer
|
||||
zw := zip.NewWriter(&buf)
|
||||
for filename, content := range files {
|
||||
w, _ := zw.Create(filename)
|
||||
w.Write([]byte(content))
|
||||
}
|
||||
zw.Close()
|
||||
return &buf
|
||||
}
|
||||
|
||||
uploadURL := fmt.Sprintf("%s/%s/%s/%s", url, packageScope, packageName, packageVersion2)
|
||||
|
||||
req := NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{}))
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
|
||||
// Test with metadata as form field
|
||||
uploadPackage(
|
||||
t,
|
||||
uploadURL,
|
||||
http.StatusCreated,
|
||||
createArchive(map[string]string{
|
||||
"Package.swift": contentManifest1,
|
||||
"Package@swift-5.6.swift": contentManifest2,
|
||||
}),
|
||||
`{"name":"`+packageName+`","version":"`+packageVersion2+`","description":"`+packageDescription+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`,
|
||||
)
|
||||
|
||||
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeSwift)
|
||||
assert.NoError(t, err)
|
||||
require.Len(t, pvs, 2) // ATTENTION: many subtests are unable to run separately, they depend on the results of previous tests
|
||||
thisPackageVersion := pvs[0]
|
||||
pd, err := packages.GetPackageDescriptor(db.DefaultContext, thisPackageVersion)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, pd.SemVer)
|
||||
assert.Equal(t, packageID, pd.Package.Name)
|
||||
assert.Equal(t, packageVersion2, pd.Version.Version)
|
||||
assert.IsType(t, &swift_module.Metadata{}, pd.Metadata)
|
||||
metadata := pd.Metadata.(*swift_module.Metadata)
|
||||
assert.Equal(t, packageDescription, metadata.Description)
|
||||
assert.Len(t, metadata.Manifests, 2)
|
||||
assert.Equal(t, contentManifest1, metadata.Manifests[""].Content)
|
||||
assert.Equal(t, contentManifest2, metadata.Manifests["5.6"].Content)
|
||||
assert.Len(t, pd.VersionProperties, 1)
|
||||
assert.Equal(t, packageRepositoryURL, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
|
||||
|
||||
pfs, err := packages.GetFilesByVersionID(db.DefaultContext, thisPackageVersion.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, pfs, 1)
|
||||
assert.Equal(t, fmt.Sprintf("%s-%s.zip", packageName, packageVersion2), pfs[0].Name)
|
||||
assert.True(t, pfs[0].IsLead)
|
||||
|
||||
uploadPackage(
|
||||
t,
|
||||
uploadURL,
|
||||
http.StatusConflict,
|
||||
createArchive(map[string]string{
|
||||
"Package.swift": contentManifest1,
|
||||
}),
|
||||
"",
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("Download", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
@@ -211,7 +301,7 @@ func TestPackageSwift(t *testing.T) {
|
||||
SetHeader("Accept", swift_router.AcceptJSON)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
versionURL := setting.AppURL + url[1:] + fmt.Sprintf("/%s/%s/%s", packageScope, packageName, packageVersion)
|
||||
versionURL := setting.AppURL + url[1:] + fmt.Sprintf("/%s/%s/%s", packageScope, packageName, packageVersion2)
|
||||
|
||||
assert.Equal(t, "1", resp.Header().Get("Content-Version"))
|
||||
assert.Equal(t, fmt.Sprintf(`<%s>; rel="latest-version"`, versionURL), resp.Header().Get("Link"))
|
||||
@@ -221,9 +311,9 @@ func TestPackageSwift(t *testing.T) {
|
||||
var result *swift_router.EnumeratePackageVersionsResponse
|
||||
DecodeJSON(t, resp, &result)
|
||||
|
||||
assert.Len(t, result.Releases, 1)
|
||||
assert.Contains(t, result.Releases, packageVersion)
|
||||
assert.Equal(t, versionURL, result.Releases[packageVersion].URL)
|
||||
assert.Len(t, result.Releases, 2)
|
||||
assert.Contains(t, result.Releases, packageVersion2)
|
||||
assert.Equal(t, versionURL, result.Releases[packageVersion2].URL)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.json", url, packageScope, packageName)).
|
||||
AddBasicAuth(user.Name)
|
||||
|
||||
@@ -9,7 +9,10 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
@@ -18,11 +21,15 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"code.gitea.io/gitea/services/gitdiff"
|
||||
issue_service "code.gitea.io/gitea/services/issue"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
files_service "code.gitea.io/gitea/services/repository/files"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -425,3 +432,94 @@ func TestAPICommitPullRequest(t *testing.T) {
|
||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/commits/%s/pull", owner.Name, repo.Name, invalidCommitSHA).AddTokenAuth(ctx.Token)
|
||||
ctx.Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAPIViewPullFilesWithHeadRepoDeleted(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
|
||||
ctx := NewAPITestContext(t, "user1", baseRepo.Name, auth_model.AccessTokenScopeAll)
|
||||
|
||||
doAPIForkRepository(ctx, "user2")(t)
|
||||
|
||||
forkedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ForkID: baseRepo.ID, OwnerName: "user1"})
|
||||
|
||||
// add a new file to the forked repo
|
||||
addFileToForkedResp, err := files_service.ChangeRepoFiles(git.DefaultContext, forkedRepo, user1, &files_service.ChangeRepoFilesOptions{
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "create",
|
||||
TreePath: "file_1.txt",
|
||||
ContentReader: strings.NewReader("file1"),
|
||||
},
|
||||
},
|
||||
Message: "add file1",
|
||||
OldBranch: "master",
|
||||
NewBranch: "fork-branch-1",
|
||||
Author: &files_service.IdentityOptions{
|
||||
Name: user1.Name,
|
||||
Email: user1.Email,
|
||||
},
|
||||
Committer: &files_service.IdentityOptions{
|
||||
Name: user1.Name,
|
||||
Email: user1.Email,
|
||||
},
|
||||
Dates: &files_service.CommitDateOptions{
|
||||
Author: time.Now(),
|
||||
Committer: time.Now(),
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, addFileToForkedResp)
|
||||
|
||||
// create Pull
|
||||
pullIssue := &issues_model.Issue{
|
||||
RepoID: baseRepo.ID,
|
||||
Title: "Test pull-request-target-event",
|
||||
PosterID: user1.ID,
|
||||
Poster: user1,
|
||||
IsPull: true,
|
||||
}
|
||||
pullRequest := &issues_model.PullRequest{
|
||||
HeadRepoID: forkedRepo.ID,
|
||||
BaseRepoID: baseRepo.ID,
|
||||
HeadBranch: "fork-branch-1",
|
||||
BaseBranch: "master",
|
||||
HeadRepo: forkedRepo,
|
||||
BaseRepo: baseRepo,
|
||||
Type: issues_model.PullRequestGitea,
|
||||
}
|
||||
|
||||
prOpts := &pull_service.NewPullRequestOptions{Repo: baseRepo, Issue: pullIssue, PullRequest: pullRequest}
|
||||
err = pull_service.NewPullRequest(git.DefaultContext, prOpts)
|
||||
assert.NoError(t, err)
|
||||
pr := convert.ToAPIPullRequest(context.Background(), pullRequest, user1)
|
||||
|
||||
ctx = NewAPITestContext(t, "user2", baseRepo.Name, auth_model.AccessTokenScopeAll)
|
||||
doAPIGetPullFiles(ctx, pr, func(t *testing.T, files []*api.ChangedFile) {
|
||||
if assert.Len(t, files, 1) {
|
||||
assert.Equal(t, "file_1.txt", files[0].Filename)
|
||||
assert.Empty(t, files[0].PreviousFilename)
|
||||
assert.Equal(t, 1, files[0].Additions)
|
||||
assert.Equal(t, 1, files[0].Changes)
|
||||
assert.Equal(t, 0, files[0].Deletions)
|
||||
assert.Equal(t, "added", files[0].Status)
|
||||
}
|
||||
})(t)
|
||||
|
||||
// delete the head repository of the pull request
|
||||
forkCtx := NewAPITestContext(t, "user1", forkedRepo.Name, auth_model.AccessTokenScopeAll)
|
||||
doAPIDeleteRepository(forkCtx)(t)
|
||||
|
||||
doAPIGetPullFiles(ctx, pr, func(t *testing.T, files []*api.ChangedFile) {
|
||||
if assert.Len(t, files, 1) {
|
||||
assert.Equal(t, "file_1.txt", files[0].Filename)
|
||||
assert.Empty(t, files[0].PreviousFilename)
|
||||
assert.Equal(t, 1, files[0].Additions)
|
||||
assert.Equal(t, 1, files[0].Changes)
|
||||
assert.Equal(t, 0, files[0].Deletions)
|
||||
assert.Equal(t, "added", files[0].Status)
|
||||
}
|
||||
})(t)
|
||||
})
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user