Merge branch 'main' into lunny/refactor_getpatch

This commit is contained in:
Lunny Xiao 2025-02-27 11:09:33 -08:00
commit e264a3e047
58 changed files with 532 additions and 261 deletions

View File

@ -17,6 +17,7 @@ insert_final_newline = false
[templates/swagger/v1_json.tmpl] [templates/swagger/v1_json.tmpl]
indent_style = space indent_style = space
insert_final_newline = false
[templates/user/auth/oidc_wellknown.tmpl] [templates/user/auth/oidc_wellknown.tmpl]
indent_style = space indent_style = space

View File

@ -336,7 +336,7 @@ module.exports = {
'@typescript-eslint/no-unsafe-unary-minus': [2], '@typescript-eslint/no-unsafe-unary-minus': [2],
'@typescript-eslint/no-unused-expressions': [0], '@typescript-eslint/no-unused-expressions': [0],
'@typescript-eslint/no-unused-vars': [2, {vars: 'all', args: 'all', caughtErrors: 'all', ignoreRestSiblings: false, argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_'}], '@typescript-eslint/no-unused-vars': [2, {vars: 'all', args: 'all', caughtErrors: 'all', ignoreRestSiblings: false, argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_'}],
'@typescript-eslint/no-use-before-define': [0], '@typescript-eslint/no-use-before-define': [2, {functions: false, classes: true, variables: true, allowNamedExports: true, typedefs: false, enums: false, ignoreTypeReferences: true}],
'@typescript-eslint/no-useless-constructor': [0], '@typescript-eslint/no-useless-constructor': [0],
'@typescript-eslint/no-useless-empty-export': [0], '@typescript-eslint/no-useless-empty-export': [0],
'@typescript-eslint/no-wrapper-object-types': [2], '@typescript-eslint/no-wrapper-object-types': [2],
@ -693,7 +693,7 @@ module.exports = {
'no-unused-labels': [2], 'no-unused-labels': [2],
'no-unused-private-class-members': [2], 'no-unused-private-class-members': [2],
'no-unused-vars': [0], // handled by @typescript-eslint/no-unused-vars 'no-unused-vars': [0], // handled by @typescript-eslint/no-unused-vars
'no-use-before-define': [2, {functions: false, classes: true, variables: true, allowNamedExports: true}], 'no-use-before-define': [0], // handled by @typescript-eslint/no-use-before-define
'no-use-extend-native/no-use-extend-native': [2], 'no-use-extend-native/no-use-extend-native': [2],
'no-useless-backreference': [2], 'no-useless-backreference': [2],
'no-useless-call': [2], 'no-useless-call': [2],

10
.github/labeler.yml vendored
View File

@ -41,7 +41,7 @@ modifies/internal:
- ".dockerignore" - ".dockerignore"
- "docker/**" - "docker/**"
- ".editorconfig" - ".editorconfig"
- ".eslintrc.yaml" - ".eslintrc.cjs"
- ".golangci.yml" - ".golangci.yml"
- ".gitpod.yml" - ".gitpod.yml"
- ".markdownlint.yaml" - ".markdownlint.yaml"
@ -49,7 +49,7 @@ modifies/internal:
- "stylelint.config.js" - "stylelint.config.js"
- ".yamllint.yaml" - ".yamllint.yaml"
- ".github/**" - ".github/**"
- ".gitea/" - ".gitea/**"
- ".devcontainer/**" - ".devcontainer/**"
- "build.go" - "build.go"
- "build/**" - "build/**"
@ -73,9 +73,9 @@ modifies/go:
modifies/frontend: modifies/frontend:
- changed-files: - changed-files:
- any-glob-to-any-file: - any-glob-to-any-file:
- "**/*.js" - "*.js"
- "**/*.ts" - "*.ts"
- "**/*.vue" - "web_src/**"
docs-update-needed: docs-update-needed:
- changed-files: - changed-files:

View File

@ -51,14 +51,16 @@ jobs:
- "options/locale/locale_en-US.ini" - "options/locale/locale_en-US.ini"
frontend: frontend:
- "**/*.js" - "*.js"
- "*.ts"
- "web_src/**" - "web_src/**"
- "tools/*.js"
- "tools/*.ts"
- "assets/emoji.json" - "assets/emoji.json"
- "package.json" - "package.json"
- "package-lock.json" - "package-lock.json"
- "Makefile" - "Makefile"
- ".eslintrc.yaml" - ".eslintrc.cjs"
- "stylelint.config.js"
- ".npmrc" - ".npmrc"
docs: docs:
@ -85,6 +87,7 @@ jobs:
swagger: swagger:
- "templates/swagger/v1_json.tmpl" - "templates/swagger/v1_json.tmpl"
- "templates/swagger/v1_input.json"
- "Makefile" - "Makefile"
- "package.json" - "package.json"
- "package-lock.json" - "package-lock.json"

View File

@ -88,9 +88,9 @@ jobs:
# 1.2 # 1.2
# 1.2.3 # 1.2.3
tags: | tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}} type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{version}}
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
@ -126,9 +126,9 @@ jobs:
# 1.2 # 1.2
# 1.2.3 # 1.2.3
tags: | tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}} type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{version}}
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:

View File

@ -165,10 +165,8 @@ ifdef DEPS_PLAYWRIGHT
endif endif
SWAGGER_SPEC := templates/swagger/v1_json.tmpl SWAGGER_SPEC := templates/swagger/v1_json.tmpl
SWAGGER_SPEC_S_TMPL := s|"basePath": *"/api/v1"|"basePath": "{{AppSubUrl \| JSEscape}}/api/v1"|g SWAGGER_SPEC_INPUT := templates/swagger/v1_input.json
SWAGGER_SPEC_S_JSON := s|"basePath": *"{{AppSubUrl \| JSEscape}}/api/v1"|"basePath": "/api/v1"|g
SWAGGER_EXCLUDE := code.gitea.io/sdk SWAGGER_EXCLUDE := code.gitea.io/sdk
SWAGGER_NEWLINE_COMMAND := -e '$$a\'
TEST_MYSQL_HOST ?= mysql:3306 TEST_MYSQL_HOST ?= mysql:3306
TEST_MYSQL_DBNAME ?= testgitea TEST_MYSQL_DBNAME ?= testgitea
@ -271,10 +269,8 @@ endif
.PHONY: generate-swagger .PHONY: generate-swagger
generate-swagger: $(SWAGGER_SPEC) ## generate the swagger spec from code comments generate-swagger: $(SWAGGER_SPEC) ## generate the swagger spec from code comments
$(SWAGGER_SPEC): $(GO_SOURCES_NO_BINDATA) $(SWAGGER_SPEC): $(GO_SOURCES_NO_BINDATA) $(SWAGGER_SPEC_INPUT)
$(GO) run $(SWAGGER_PACKAGE) generate spec -x "$(SWAGGER_EXCLUDE)" -o './$(SWAGGER_SPEC)' $(GO) run $(SWAGGER_PACKAGE) generate spec --exclude "$(SWAGGER_EXCLUDE)" --input "$(SWAGGER_SPEC_INPUT)" --output './$(SWAGGER_SPEC)'
$(SED_INPLACE) '$(SWAGGER_SPEC_S_TMPL)' './$(SWAGGER_SPEC)'
$(SED_INPLACE) $(SWAGGER_NEWLINE_COMMAND) './$(SWAGGER_SPEC)'
.PHONY: swagger-check .PHONY: swagger-check
swagger-check: generate-swagger swagger-check: generate-swagger
@ -287,9 +283,11 @@ swagger-check: generate-swagger
.PHONY: swagger-validate .PHONY: swagger-validate
swagger-validate: ## check if the swagger spec is valid swagger-validate: ## check if the swagger spec is valid
$(SED_INPLACE) '$(SWAGGER_SPEC_S_JSON)' './$(SWAGGER_SPEC)' @# swagger "validate" requires that the "basePath" must start with a slash, but we are using Golang template "{{...}}"
@$(SED_INPLACE) -E -e 's|"basePath":( *)"(.*)"|"basePath":\1"/\2"|g' './$(SWAGGER_SPEC)' # add a prefix slash to basePath
@# FIXME: there are some warnings
$(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)' $(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)'
$(SED_INPLACE) '$(SWAGGER_SPEC_S_TMPL)' './$(SWAGGER_SPEC)' @$(SED_INPLACE) -E -e 's|"basePath":( *)"/(.*)"|"basePath":\1"\2"|g' './$(SWAGGER_SPEC)' # remove the prefix slash from basePath
.PHONY: checks .PHONY: checks
checks: checks-frontend checks-backend ## run various consistency checks checks: checks-frontend checks-backend ## run various consistency checks
@ -380,6 +378,7 @@ lint-go-gopls: ## lint go files with gopls
.PHONY: lint-editorconfig .PHONY: lint-editorconfig
lint-editorconfig: lint-editorconfig:
@echo "Running editorconfig check..."
@$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) $(EDITORCONFIG_FILES) @$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) $(EDITORCONFIG_FILES)
.PHONY: lint-actions .PHONY: lint-actions

View File

@ -54,10 +54,6 @@ func runACME(listenAddr string, m http.Handler) error {
altTLSALPNPort = p altTLSALPNPort = p
} }
// FIXME: this path is not right, it uses "AppWorkPath" incorrectly, and writes the data into "AppWorkPath/https"
// Ideally it should migrate to AppDataPath write to "AppDataPath/https"
certmagic.Default.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory}
magic := certmagic.NewDefault()
// Try to use private CA root if provided, otherwise defaults to system's trust // Try to use private CA root if provided, otherwise defaults to system's trust
var certPool *x509.CertPool var certPool *x509.CertPool
if setting.AcmeCARoot != "" { if setting.AcmeCARoot != "" {
@ -67,7 +63,13 @@ func runACME(listenAddr string, m http.Handler) error {
log.Warn("Failed to parse CA Root certificate, using default CA trust: %v", err) log.Warn("Failed to parse CA Root certificate, using default CA trust: %v", err)
} }
} }
myACME := certmagic.NewACMEIssuer(magic, certmagic.ACMEIssuer{ // FIXME: this path is not right, it uses "AppWorkPath" incorrectly, and writes the data into "AppWorkPath/https"
// Ideally it should migrate to AppDataPath write to "AppDataPath/https"
// And one more thing, no idea why we should set the global default variables here
// But it seems that the current ACME code needs these global variables to make renew work.
// Otherwise, "renew" will use incorrect storage path
certmagic.Default.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory}
certmagic.DefaultACME = certmagic.ACMEIssuer{
CA: setting.AcmeURL, CA: setting.AcmeURL,
TrustedRoots: certPool, TrustedRoots: certPool,
Email: setting.AcmeEmail, Email: setting.AcmeEmail,
@ -77,8 +79,10 @@ func runACME(listenAddr string, m http.Handler) error {
ListenHost: setting.HTTPAddr, ListenHost: setting.HTTPAddr,
AltTLSALPNPort: altTLSALPNPort, AltTLSALPNPort: altTLSALPNPort,
AltHTTPPort: altHTTPPort, AltHTTPPort: altHTTPPort,
}) }
magic := certmagic.NewDefault()
myACME := certmagic.NewACMEIssuer(magic, certmagic.DefaultACME)
magic.Issuers = []certmagic.Issuer{myACME} magic.Issuers = []certmagic.Issuer{myACME}
// this obtains certificates or renews them if necessary // this obtains certificates or renews them if necessary

4
go.mod
View File

@ -117,10 +117,10 @@ require (
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
github.com/yuin/goldmark-meta v1.1.0 github.com/yuin/goldmark-meta v1.1.0
gitlab.com/gitlab-org/api/client-go v0.123.0 gitlab.com/gitlab-org/api/client-go v0.123.0
golang.org/x/crypto v0.33.0 golang.org/x/crypto v0.35.0
golang.org/x/image v0.24.0 golang.org/x/image v0.24.0
golang.org/x/net v0.35.0 golang.org/x/net v0.35.0
golang.org/x/oauth2 v0.26.0 golang.org/x/oauth2 v0.27.0
golang.org/x/sync v0.11.0 golang.org/x/sync v0.11.0
golang.org/x/sys v0.30.0 golang.org/x/sys v0.30.0
golang.org/x/text v0.22.0 golang.org/x/text v0.22.0

7
go.sum
View File

@ -831,8 +831,9 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
@ -868,8 +869,8 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 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= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@ -454,6 +454,24 @@ func ActivityReadable(user, doer *user_model.User) bool {
doer != nil && (doer.IsAdmin || user.ID == doer.ID) doer != nil && (doer.IsAdmin || user.ID == doer.ID)
} }
func FeedDateCond(opts GetFeedsOptions) builder.Cond {
cond := builder.NewCond()
if opts.Date == "" {
return cond
}
dateLow, err := time.ParseInLocation("2006-01-02", opts.Date, setting.DefaultUILocation)
if err != nil {
log.Warn("Unable to parse %s, filter not applied: %v", opts.Date, err)
} else {
dateHigh := dateLow.Add(86399000000000) // 23h59m59s
cond = cond.And(builder.Gte{"`action`.created_unix": dateLow.Unix()})
cond = cond.And(builder.Lte{"`action`.created_unix": dateHigh.Unix()})
}
return cond
}
func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.Cond, error) { func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.Cond, error) {
cond := builder.NewCond() cond := builder.NewCond()
@ -534,17 +552,7 @@ func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.
cond = cond.And(builder.Eq{"is_deleted": false}) cond = cond.And(builder.Eq{"is_deleted": false})
} }
if opts.Date != "" { cond = cond.And(FeedDateCond(opts))
dateLow, err := time.ParseInLocation("2006-01-02", opts.Date, setting.DefaultUILocation)
if err != nil {
log.Warn("Unable to parse %s, filter not applied: %v", opts.Date, err)
} else {
dateHigh := dateLow.Add(86399000000000) // 23h59m59s
cond = cond.And(builder.Gte{"`action`.created_unix": dateLow.Unix()})
cond = cond.And(builder.Lte{"`action`.created_unix": dateHigh.Unix()})
}
}
return cond, nil return cond, nil
} }

View File

@ -208,10 +208,32 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err
return nil, 0, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo") return nil, 0, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo")
} }
cond, err := ActivityQueryCondition(ctx, opts) var err error
var cond builder.Cond
// if the actor is the requested user or is an administrator, we can skip the ActivityQueryCondition
if opts.Actor != nil && opts.RequestedUser != nil && (opts.Actor.IsAdmin || opts.Actor.ID == opts.RequestedUser.ID) {
cond = builder.Eq{
"user_id": opts.RequestedUser.ID,
}.And(
FeedDateCond(opts),
)
if !opts.IncludeDeleted {
cond = cond.And(builder.Eq{"is_deleted": false})
}
if !opts.IncludePrivate {
cond = cond.And(builder.Eq{"is_private": false})
}
if opts.OnlyPerformedBy {
cond = cond.And(builder.Eq{"act_user_id": opts.RequestedUser.ID})
}
} else {
cond, err = ActivityQueryCondition(ctx, opts)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
}
actions := make([]*Action, 0, opts.PageSize) actions := make([]*Action, 0, opts.PageSize)
var count int64 var count int64

View File

@ -44,7 +44,7 @@ func init() {
// TranslatableMessage represents JSON struct that can be translated with a Locale // TranslatableMessage represents JSON struct that can be translated with a Locale
type TranslatableMessage struct { type TranslatableMessage struct {
Format string Format string
Args []any `json:"omitempty"` Args []any `json:",omitempty"`
} }
// LoadRepo loads repository of the task // LoadRepo loads repository of the task

View File

@ -28,11 +28,16 @@ type PullRequestsOptions struct {
Labels []int64 Labels []int64
MilestoneID int64 MilestoneID int64
PosterID int64 PosterID int64
BaseBranch string
} }
func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) *xorm.Session { func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) *xorm.Session {
sess := db.GetEngine(ctx).Where("pull_request.base_repo_id=?", baseRepoID) sess := db.GetEngine(ctx).Where("pull_request.base_repo_id=?", baseRepoID)
if opts.BaseBranch != "" {
sess.And("pull_request.base_branch=?", opts.BaseBranch)
}
sess.Join("INNER", "issue", "pull_request.issue_id = issue.id") sess.Join("INNER", "issue", "pull_request.issue_id = issue.id")
switch opts.State { switch opts.State {
case "closed", "open": case "closed", "open":

View File

@ -124,6 +124,7 @@ func GetUserOrgsList(ctx context.Context, user *user_model.User) ([]*MinimalOrg,
if err := db.GetEngine(ctx).Select(columnsStr). if err := db.GetEngine(ctx).Select(columnsStr).
Table("user"). Table("user").
Where(builder.In("`user`.`id`", queryUserOrgIDs(user.ID, true))). Where(builder.In("`user`.`id`", queryUserOrgIDs(user.ID, true))).
OrderBy("`user`.lower_name ASC").
Find(&orgs); err != nil { Find(&orgs); err != nil {
return nil, err return nil, err
} }

View File

@ -646,13 +646,15 @@ func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML {
type CloneLink struct { type CloneLink struct {
SSH string SSH string
HTTPS string HTTPS string
Tea string
} }
// ComposeHTTPSCloneURL returns HTTPS clone URL based on given owner and repository name. // ComposeHTTPSCloneURL returns HTTPS clone URL based on the given owner and repository name.
func ComposeHTTPSCloneURL(ctx context.Context, owner, repo string) string { func ComposeHTTPSCloneURL(ctx context.Context, owner, repo string) string {
return fmt.Sprintf("%s%s/%s.git", httplib.GuessCurrentAppURL(ctx), url.PathEscape(owner), url.PathEscape(repo)) return fmt.Sprintf("%s%s/%s.git", httplib.GuessCurrentAppURL(ctx), url.PathEscape(owner), url.PathEscape(repo))
} }
// ComposeSSHCloneURL returns SSH clone URL based on the given owner and repository name.
func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) string { func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) string {
sshUser := setting.SSH.User sshUser := setting.SSH.User
sshDomain := setting.SSH.Domain sshDomain := setting.SSH.Domain
@ -686,11 +688,17 @@ func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) strin
return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName)) return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
} }
// ComposeTeaCloneCommand returns Tea CLI clone command based on the given owner and repository name.
func ComposeTeaCloneCommand(ctx context.Context, owner, repo string) string {
return fmt.Sprintf("tea clone %s/%s", url.PathEscape(owner), url.PathEscape(repo))
}
func (repo *Repository) cloneLink(ctx context.Context, doer *user_model.User, repoPathName string) *CloneLink { func (repo *Repository) cloneLink(ctx context.Context, doer *user_model.User, repoPathName string) *CloneLink {
cl := new(CloneLink) return &CloneLink{
cl.SSH = ComposeSSHCloneURL(doer, repo.OwnerName, repoPathName) SSH: ComposeSSHCloneURL(doer, repo.OwnerName, repoPathName),
cl.HTTPS = ComposeHTTPSCloneURL(ctx, repo.OwnerName, repoPathName) HTTPS: ComposeHTTPSCloneURL(ctx, repo.OwnerName, repoPathName),
return cl Tea: ComposeTeaCloneCommand(ctx, repo.OwnerName, repoPathName),
}
} }
// CloneLink returns clone URLs of repository. // CloneLink returns clone URLs of repository.

View File

@ -5,6 +5,7 @@ package repo
import ( import (
"context" "context"
"strings"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
@ -149,9 +150,9 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us
// If isShowFullName is set to true, also include full name prefix search // If isShowFullName is set to true, also include full name prefix search
func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull bool, search string, isShowFullName bool) ([]*user_model.User, error) { func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull bool, search string, isShowFullName bool) ([]*user_model.User, error) {
users := make([]*user_model.User, 0, 30) users := make([]*user_model.User, 0, 30)
var prefixCond builder.Cond = builder.Like{"name", search + "%"} var prefixCond builder.Cond = builder.Like{"lower_name", strings.ToLower(search) + "%"}
if isShowFullName { if isShowFullName {
prefixCond = prefixCond.Or(builder.Like{"full_name", "%" + search + "%"}) prefixCond = prefixCond.Or(db.BuildCaseInsensitiveLike("full_name", "%"+search+"%"))
} }
cond := builder.In("`user`.id", cond := builder.In("`user`.id",

View File

@ -12,6 +12,7 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestRepoAssignees(t *testing.T) { func TestRepoAssignees(t *testing.T) {
@ -38,3 +39,19 @@ func TestRepoAssignees(t *testing.T) {
assert.NotContains(t, []int64{users[0].ID, users[1].ID, users[2].ID}, 15) assert.NotContains(t, []int64{users[0].ID, users[1].ID, users[2].ID}, 15)
} }
} }
func TestGetIssuePostersWithSearch(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
users, err := repo_model.GetIssuePostersWithSearch(db.DefaultContext, repo2, false, "USER", false /* full name */)
require.NoError(t, err)
require.Len(t, users, 1)
assert.Equal(t, "user2", users[0].Name)
users, err = repo_model.GetIssuePostersWithSearch(db.DefaultContext, repo2, false, "TW%O", true /* full name */)
require.NoError(t, err)
require.Len(t, users, 1)
assert.Equal(t, "user2", users[0].Name)
}

View File

@ -169,12 +169,6 @@ func loadServerFrom(rootCfg ConfigProvider) {
HTTPAddr = sec.Key("HTTP_ADDR").MustString("0.0.0.0") HTTPAddr = sec.Key("HTTP_ADDR").MustString("0.0.0.0")
HTTPPort = sec.Key("HTTP_PORT").MustString("3000") HTTPPort = sec.Key("HTTP_PORT").MustString("3000")
Protocol = HTTP
protocolCfg := sec.Key("PROTOCOL").String()
switch protocolCfg {
case "https":
Protocol = HTTPS
// DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version // DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version
// if these are removed, the warning will not be shown // if these are removed, the warning will not be shown
if sec.HasKey("ENABLE_ACME") { if sec.HasKey("ENABLE_ACME") {
@ -183,6 +177,16 @@ func loadServerFrom(rootCfg ConfigProvider) {
deprecatedSetting(rootCfg, "server", "ENABLE_LETSENCRYPT", "server", "ENABLE_ACME", "v1.19.0") deprecatedSetting(rootCfg, "server", "ENABLE_LETSENCRYPT", "server", "ENABLE_ACME", "v1.19.0")
EnableAcme = sec.Key("ENABLE_LETSENCRYPT").MustBool(false) EnableAcme = sec.Key("ENABLE_LETSENCRYPT").MustBool(false)
} }
Protocol = HTTP
protocolCfg := sec.Key("PROTOCOL").String()
if protocolCfg != "https" && EnableAcme {
log.Fatal("ACME could only be used with HTTPS protocol")
}
switch protocolCfg {
case "https":
Protocol = HTTPS
if EnableAcme { if EnableAcme {
AcmeURL = sec.Key("ACME_URL").MustString("") AcmeURL = sec.Key("ACME_URL").MustString("")
AcmeCARoot = sec.Key("ACME_CA_ROOT").MustString("") AcmeCARoot = sec.Key("ACME_CA_ROOT").MustString("")
@ -210,6 +214,9 @@ func loadServerFrom(rootCfg ConfigProvider) {
deprecatedSetting(rootCfg, "server", "LETSENCRYPT_EMAIL", "server", "ACME_EMAIL", "v1.19.0") deprecatedSetting(rootCfg, "server", "LETSENCRYPT_EMAIL", "server", "ACME_EMAIL", "v1.19.0")
AcmeEmail = sec.Key("LETSENCRYPT_EMAIL").MustString("") AcmeEmail = sec.Key("LETSENCRYPT_EMAIL").MustString("")
} }
if AcmeEmail == "" {
log.Fatal("ACME Email is not set (ACME_EMAIL).")
}
} else { } else {
CertFile = sec.Key("CERT_FILE").String() CertFile = sec.Key("CERT_FILE").String()
KeyFile = sec.Key("KEY_FILE").String() KeyFile = sec.Key("KEY_FILE").String()

View File

@ -1465,6 +1465,8 @@ issues.filter_milestones = Filter Milestone
issues.filter_projects = Filter Project issues.filter_projects = Filter Project
issues.filter_labels = Filter Label issues.filter_labels = Filter Label
issues.filter_reviewers = Filter Reviewer issues.filter_reviewers = Filter Reviewer
issues.filter_no_results = No results
issues.filter_no_results_placeholder = Try adjusting your search filters.
issues.new = New Issue issues.new = New Issue
issues.new.title_empty = Title cannot be empty issues.new.title_empty = Title cannot be empty
issues.new.labels = Labels issues.new.labels = Labels

View File

@ -1701,7 +1701,9 @@ issues.time_estimate_invalid=Le format du temps estimé est invalide
issues.start_tracking_history=`a commencé son travail %s.` issues.start_tracking_history=`a commencé son travail %s.`
issues.tracker_auto_close=Le minuteur sera automatiquement arrêté quand le ticket sera fermé. issues.tracker_auto_close=Le minuteur sera automatiquement arrêté quand le ticket sera fermé.
issues.tracking_already_started=`Vous avez déjà un minuteur en cours sur <a href="%s">un autre ticket</a> !` issues.tracking_already_started=`Vous avez déjà un minuteur en cours sur <a href="%s">un autre ticket</a> !`
issues.stop_tracking=Arrêter le minuteur
issues.stop_tracking_history=a travaillé sur <b>%[1]s</b> %[2]s issues.stop_tracking_history=a travaillé sur <b>%[1]s</b> %[2]s
issues.cancel_tracking=Abandonner
issues.cancel_tracking_history=`a abandonné son minuteur %s.` issues.cancel_tracking_history=`a abandonné son minuteur %s.`
issues.del_time=Supprimer ce minuteur du journal issues.del_time=Supprimer ce minuteur du journal
issues.add_time_history=a pointé du temps de travail sur <b>%[1]s</b>, %[2]s issues.add_time_history=a pointé du temps de travail sur <b>%[1]s</b>, %[2]s

View File

@ -1464,6 +1464,8 @@ issues.filter_milestones=Cloch Mhíle Scagaire
issues.filter_projects=Tionscadal Scagaire issues.filter_projects=Tionscadal Scagaire
issues.filter_labels=Lipéad Scagaire issues.filter_labels=Lipéad Scagaire
issues.filter_reviewers=Athbhreithneoir Scagaire issues.filter_reviewers=Athbhreithneoir Scagaire
issues.filter_no_results=Gan torthaí
issues.filter_no_results_placeholder=Bain triail as do scagairí cuardaigh a choigeartú.
issues.new=Eagrán Nua issues.new=Eagrán Nua
issues.new.title_empty=Ní féidir leis an teideal a bheith folamh issues.new.title_empty=Ní féidir leis an teideal a bheith folamh
issues.new.labels=Lipéid issues.new.labels=Lipéid
@ -1701,7 +1703,9 @@ issues.time_estimate_invalid=Tá formáid meastachán ama neamhbhailí
issues.start_tracking_history=thosaigh ag obair %s issues.start_tracking_history=thosaigh ag obair %s
issues.tracker_auto_close=Stopfar ama go huathoibríoch nuair a dhúnfar an tsaincheist seo issues.tracker_auto_close=Stopfar ama go huathoibríoch nuair a dhúnfar an tsaincheist seo
issues.tracking_already_started=`Tá tús curtha agat cheana féin ag rianú ama ar <a href="%s">eagrán eile</a>!` issues.tracking_already_started=`Tá tús curtha agat cheana féin ag rianú ama ar <a href="%s">eagrán eile</a>!`
issues.stop_tracking=Stad uaineadóir
issues.stop_tracking_history=d'oibrigh do <b>%[1]s</b> %[2]s issues.stop_tracking_history=d'oibrigh do <b>%[1]s</b> %[2]s
issues.cancel_tracking=Caith amach
issues.cancel_tracking_history=`rianú ama curtha ar ceal %s` issues.cancel_tracking_history=`rianú ama curtha ar ceal %s`
issues.del_time=Scrios an log ama seo issues.del_time=Scrios an log ama seo
issues.add_time_history=cuireadh am caite <b>%[1]s</b> %[2]s leis issues.add_time_history=cuireadh am caite <b>%[1]s</b> %[2]s leis

View File

@ -385,6 +385,12 @@ show_only_public=公開のみ表示
issues.in_your_repos=あなたのリポジトリ issues.in_your_repos=あなたのリポジトリ
guide_title=アクティビティはありません
guide_desc=現在フォロー中のリポジトリやユーザーがないため、表示するコンテンツがありません。 以下のリンクから、興味のあるリポジトリやユーザーを探すことができます。
explore_repos=リポジトリを探す
explore_users=ユーザーを探す
empty_org=組織はまだありません。
empty_repo=リポジトリはまだありません。
[explore] [explore]
repos=リポジトリ repos=リポジトリ
@ -1348,6 +1354,8 @@ editor.new_branch_name_desc=新しいブランチ名…
editor.cancel=キャンセル editor.cancel=キャンセル
editor.filename_cannot_be_empty=ファイル名は空にできません。 editor.filename_cannot_be_empty=ファイル名は空にできません。
editor.filename_is_invalid=`ファイル名が不正です: "%s"` editor.filename_is_invalid=`ファイル名が不正です: "%s"`
editor.commit_email=コミット メールアドレス
editor.invalid_commit_email=コミットに使うメールアドレスが正しくありません。
editor.branch_does_not_exist=このリポジトリにブランチ "%s" は存在しません。 editor.branch_does_not_exist=このリポジトリにブランチ "%s" は存在しません。
editor.branch_already_exists=ブランチ "%s" は、このリポジトリに既に存在します。 editor.branch_already_exists=ブランチ "%s" は、このリポジトリに既に存在します。
editor.directory_is_a_file=ディレクトリ名 "%s" はすでにリポジトリ内のファイルで使用されています。 editor.directory_is_a_file=ディレクトリ名 "%s" はすでにリポジトリ内のファイルで使用されています。
@ -1693,7 +1701,9 @@ issues.time_estimate_invalid=見積時間のフォーマットが不正です
issues.start_tracking_history=が作業を開始 %s issues.start_tracking_history=が作業を開始 %s
issues.tracker_auto_close=タイマーは、このイシューがクローズされると自動的に終了します issues.tracker_auto_close=タイマーは、このイシューがクローズされると自動的に終了します
issues.tracking_already_started=`<a href="%s">別のイシュー</a>で既にタイムトラッキングを開始しています!` issues.tracking_already_started=`<a href="%s">別のイシュー</a>で既にタイムトラッキングを開始しています!`
issues.stop_tracking=タイマー終了
issues.stop_tracking_history=が <b>%[1]s</b> の作業を終了 %[2]s issues.stop_tracking_history=が <b>%[1]s</b> の作業を終了 %[2]s
issues.cancel_tracking=破棄
issues.cancel_tracking_history=`がタイムトラッキングを中止 %s` issues.cancel_tracking_history=`がタイムトラッキングを中止 %s`
issues.del_time=このタイムログを削除 issues.del_time=このタイムログを削除
issues.add_time_history=が作業時間 <b>%[1]s</b> を追加 %[2]s issues.add_time_history=が作業時間 <b>%[1]s</b> を追加 %[2]s
@ -2329,6 +2339,8 @@ settings.event_fork=フォーク
settings.event_fork_desc=リポジトリがフォークされたとき。 settings.event_fork_desc=リポジトリがフォークされたとき。
settings.event_wiki=Wiki settings.event_wiki=Wiki
settings.event_wiki_desc=Wikiページが作成・名前変更・編集・削除されたとき。 settings.event_wiki_desc=Wikiページが作成・名前変更・編集・削除されたとき。
settings.event_statuses=ステータス
settings.event_statuses_desc=APIによってコミットのステータスが更新されたとき。
settings.event_release=リリース settings.event_release=リリース
settings.event_release_desc=リポジトリでリリースが作成・更新・削除されたとき。 settings.event_release_desc=リポジトリでリリースが作成・更新・削除されたとき。
settings.event_push=プッシュ settings.event_push=プッシュ
@ -2876,6 +2888,14 @@ view_as_role=表示: %s
view_as_public_hint=READMEを公開ユーザーとして見ています。 view_as_public_hint=READMEを公開ユーザーとして見ています。
view_as_member_hint=READMEをこの組織のメンバーとして見ています。 view_as_member_hint=READMEをこの組織のメンバーとして見ています。
worktime=作業時間
worktime.date_range_start=期間 (自)
worktime.date_range_end=期間 (至)
worktime.query=集計
worktime.time=時間
worktime.by_repositories=リポジトリ別
worktime.by_milestones=マイルストーン別
worktime.by_members=メンバー別
[admin] [admin]
maintenance=メンテナンス maintenance=メンテナンス

View File

@ -1464,6 +1464,8 @@ issues.filter_milestones=Filtrar etapa
issues.filter_projects=Filtrar planeamento issues.filter_projects=Filtrar planeamento
issues.filter_labels=Filtrar rótulo issues.filter_labels=Filtrar rótulo
issues.filter_reviewers=Filtrar revisor issues.filter_reviewers=Filtrar revisor
issues.filter_no_results=Sem resultados
issues.filter_no_results_placeholder=Tente ajustar os seus filtros de pesquisa.
issues.new=Questão nova issues.new=Questão nova
issues.new.title_empty=O título não pode estar vazio issues.new.title_empty=O título não pode estar vazio
issues.new.labels=Rótulos issues.new.labels=Rótulos

View File

@ -8,7 +8,6 @@ import (
"strings" "strings"
packages_model "code.gitea.io/gitea/models/packages" packages_model "code.gitea.io/gitea/models/packages"
maven_module "code.gitea.io/gitea/modules/packages/maven"
) )
// MetadataResponse https://maven.apache.org/ref/3.2.5/maven-repository-metadata/repository-metadata.html // MetadataResponse https://maven.apache.org/ref/3.2.5/maven-repository-metadata/repository-metadata.html
@ -22,7 +21,7 @@ type MetadataResponse struct {
} }
// pds is expected to be sorted ascending by CreatedUnix // pds is expected to be sorted ascending by CreatedUnix
func createMetadataResponse(pds []*packages_model.PackageDescriptor) *MetadataResponse { func createMetadataResponse(pds []*packages_model.PackageDescriptor, groupID, artifactID string) *MetadataResponse {
var release *packages_model.PackageDescriptor var release *packages_model.PackageDescriptor
versions := make([]string, 0, len(pds)) versions := make([]string, 0, len(pds))
@ -35,11 +34,9 @@ func createMetadataResponse(pds []*packages_model.PackageDescriptor) *MetadataRe
latest := pds[len(pds)-1] latest := pds[len(pds)-1]
metadata := latest.Metadata.(*maven_module.Metadata)
resp := &MetadataResponse{ resp := &MetadataResponse{
GroupID: metadata.GroupID, GroupID: groupID,
ArtifactID: metadata.ArtifactID, ArtifactID: artifactID,
Latest: latest.Version.Version, Latest: latest.Version.Version,
Version: versions, Version: versions,
} }

View File

@ -84,20 +84,19 @@ func handlePackageFile(ctx *context.Context, serveContent bool) {
} }
func serveMavenMetadata(ctx *context.Context, params parameters) { func serveMavenMetadata(ctx *context.Context, params parameters) {
// /com/foo/project/maven-metadata.xml[.md5/.sha1/.sha256/.sha512] // path pattern: /com/foo/project/maven-metadata.xml[.md5/.sha1/.sha256/.sha512]
// in case there are legacy package names ("GroupID-ArtifactID") we need to check both, new packages always use ":" as separator("GroupID:ArtifactID")
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageName()) pvsLegacy, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy())
if errors.Is(err, util.ErrNotExist) {
pvs, err = packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy())
}
if err != nil { if err != nil {
apiError(ctx, http.StatusInternalServerError, err) apiError(ctx, http.StatusInternalServerError, err)
return return
} }
if len(pvs) == 0 { pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageName())
apiError(ctx, http.StatusNotFound, packages_model.ErrPackageNotExist) if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return return
} }
pvs = append(pvsLegacy, pvs...)
pds, err := packages_model.GetPackageDescriptors(ctx, pvs) pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
if err != nil { if err != nil {
@ -110,7 +109,7 @@ func serveMavenMetadata(ctx *context.Context, params parameters) {
return pds[i].Version.CreatedUnix < pds[j].Version.CreatedUnix return pds[i].Version.CreatedUnix < pds[j].Version.CreatedUnix
}) })
xmlMetadata, err := xml.Marshal(createMetadataResponse(pds)) xmlMetadata, err := xml.Marshal(createMetadataResponse(pds, params.GroupID, params.ArtifactID))
if err != nil { if err != nil {
apiError(ctx, http.StatusInternalServerError, err) apiError(ctx, http.StatusInternalServerError, err)
return return

View File

@ -7,8 +7,6 @@
// This documentation describes the Gitea API. // This documentation describes the Gitea API.
// //
// Schemes: https, http // Schemes: https, http
// BasePath: /api/v1
// Version: {{AppVer | JSEscape}}
// License: MIT http://opensource.org/licenses/MIT // License: MIT http://opensource.org/licenses/MIT
// //
// Consumes: // Consumes:

View File

@ -7,6 +7,7 @@ package repo
import ( import (
"errors" "errors"
"net/http" "net/http"
"strings"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
@ -274,12 +275,13 @@ func GetRepoPermissions(ctx *context.APIContext) {
// "403": // "403":
// "$ref": "#/responses/forbidden" // "$ref": "#/responses/forbidden"
if !ctx.Doer.IsAdmin && ctx.Doer.LoginName != ctx.PathParam("collaborator") && !ctx.IsUserRepoAdmin() { collaboratorUsername := ctx.PathParam("collaborator")
if !ctx.Doer.IsAdmin && ctx.Doer.LowerName != strings.ToLower(collaboratorUsername) && !ctx.IsUserRepoAdmin() {
ctx.APIError(http.StatusForbidden, "Only admins can query all permissions, repo admins can query all repo permissions, collaborators can query only their own") ctx.APIError(http.StatusForbidden, "Only admins can query all permissions, repo admins can query all repo permissions, collaborators can query only their own")
return return
} }
collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator")) collaborator, err := user_model.GetUserByName(ctx, collaboratorUsername)
if err != nil { if err != nil {
if user_model.IsErrUserNotExist(err) { if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusNotFound, err) ctx.APIError(http.StatusNotFound, err)

View File

@ -61,6 +61,10 @@ func ListPullRequests(ctx *context.APIContext) {
// description: Name of the repo // description: Name of the repo
// type: string // type: string
// required: true // required: true
// - name: base_branch
// in: query
// description: Filter by target base branch of the pull request
// type: string
// - name: state // - name: state
// in: query // in: query
// description: State of pull request // description: State of pull request
@ -134,6 +138,7 @@ func ListPullRequests(ctx *context.APIContext) {
Labels: labelIDs, Labels: labelIDs,
MilestoneID: ctx.FormInt64("milestone"), MilestoneID: ctx.FormInt64("milestone"),
PosterID: posterID, PosterID: posterID,
BaseBranch: ctx.FormTrim("base_branch"),
}) })
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)

View File

@ -52,13 +52,22 @@ func generateMockStepsLog(logCur actions.LogCursor) (stepsLog []*actions.ViewSte
return stepsLog return stepsLog
} }
func MockActionsRunsJobs(ctx *context.Context) { func MockActionsView(ctx *context.Context) {
req := web.GetForm(ctx).(*actions.ViewRequest) ctx.Data["RunID"] = ctx.PathParam("run")
ctx.Data["JobID"] = ctx.PathParam("job")
ctx.HTML(http.StatusOK, "devtest/repo-action-view")
}
func MockActionsRunsJobs(ctx *context.Context) {
runID := ctx.PathParamInt64("run")
req := web.GetForm(ctx).(*actions.ViewRequest)
resp := &actions.ViewResponse{} resp := &actions.ViewResponse{}
resp.State.Run.TitleHTML = `mock run title <a href="/">link</a>` resp.State.Run.TitleHTML = `mock run title <a href="/">link</a>`
resp.State.Run.Status = actions_model.StatusRunning.String() resp.State.Run.Status = actions_model.StatusRunning.String()
resp.State.Run.CanCancel = true resp.State.Run.CanCancel = runID == 10
resp.State.Run.CanApprove = runID == 20
resp.State.Run.CanRerun = runID == 30
resp.State.Run.CanDeleteArtifact = true resp.State.Run.CanDeleteArtifact = true
resp.State.Run.WorkflowID = "workflow-id" resp.State.Run.WorkflowID = "workflow-id"
resp.State.Run.WorkflowLink = "./workflow-link" resp.State.Run.WorkflowLink = "./workflow-link"
@ -85,6 +94,29 @@ func MockActionsRunsJobs(ctx *context.Context) {
Size: 1024 * 1024, Size: 1024 * 1024,
Status: "completed", Status: "completed",
}) })
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
ID: runID * 10,
Name: "job 100",
Status: actions_model.StatusRunning.String(),
CanRerun: true,
Duration: "1h",
})
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
ID: runID*10 + 1,
Name: "job 101",
Status: actions_model.StatusWaiting.String(),
CanRerun: false,
Duration: "2h",
})
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
ID: runID*10 + 2,
Name: "job 102",
Status: actions_model.StatusFailure.String(),
CanRerun: false,
Duration: "3h",
})
resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &actions.ViewJobStep{ resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &actions.ViewJobStep{
Summary: "step 0 (mock slow)", Summary: "step 0 (mock slow)",
Duration: time.Hour.String(), Duration: time.Hour.String(),

View File

@ -78,7 +78,7 @@ func httpBase(ctx *context.Context) *serviceHandler {
strings.HasSuffix(ctx.Req.URL.Path, "git-upload-archive") { strings.HasSuffix(ctx.Req.URL.Path, "git-upload-archive") {
isPull = true isPull = true
} else { } else {
isPull = ctx.Req.Method == "GET" isPull = ctx.Req.Method == "HEAD" || ctx.Req.Method == "GET"
} }
var accessMode perm.AccessMode var accessMode perm.AccessMode

View File

@ -1634,6 +1634,7 @@ func registerRoutes(m *web.Router) {
m.Any("", devtest.List) m.Any("", devtest.List)
m.Any("/fetch-action-test", devtest.FetchActionTest) m.Any("/fetch-action-test", devtest.FetchActionTest)
m.Any("/{sub}", devtest.Tmpl) m.Any("/{sub}", devtest.Tmpl)
m.Get("/repo-action-view/{run}/{job}", devtest.MockActionsView)
m.Post("/actions-mock/runs/{run}/jobs/{job}", web.Bind(actions.ViewRequest{}), devtest.MockActionsRunsJobs) m.Post("/actions-mock/runs/{run}/jobs/{job}", web.Bind(actions.ViewRequest{}), devtest.MockActionsRunsJobs)
}) })
} }

View File

@ -291,6 +291,11 @@ func RepoRefForAPI(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ctx := GetAPIContext(req) ctx := GetAPIContext(req)
if ctx.Repo.Repository.IsEmpty {
ctx.APIErrorNotFound("repository is empty")
return
}
if ctx.Repo.GitRepo == nil { if ctx.Repo.GitRepo == nil {
ctx.APIErrorInternal(fmt.Errorf("no open git repo")) ctx.APIErrorInternal(fmt.Errorf("no open git repo"))
return return

View File

@ -14,6 +14,7 @@ import (
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
packages_model "code.gitea.io/gitea/models/packages"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
project_model "code.gitea.io/gitea/models/project" project_model "code.gitea.io/gitea/models/project"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
@ -267,6 +268,11 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
return err return err
} }
// unlink packages linked to this repository
if err = packages_model.UnlinkRepositoryFromAllPackages(ctx, repoID); err != nil {
return err
}
if err = committer.Commit(); err != nil { if err = committer.Commit(); err != nil {
return err return err
} }

View File

@ -11,7 +11,6 @@ import (
"code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
packages_model "code.gitea.io/gitea/models/packages"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
system_model "code.gitea.io/gitea/models/system" system_model "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
@ -63,11 +62,7 @@ func DeleteRepository(ctx context.Context, doer *user_model.User, repo *repo_mod
notify_service.DeleteRepository(ctx, doer, repo) notify_service.DeleteRepository(ctx, doer, repo)
} }
if err := DeleteRepositoryDirectly(ctx, doer, repo.ID); err != nil { return DeleteRepositoryDirectly(ctx, doer, repo.ID)
return err
}
return packages_model.UnlinkRepositoryFromAllPackages(ctx, repo.ID)
} }
// PushCreateRepo creates a repository when a new repository is pushed to an appropriate namespace // PushCreateRepo creates a repository when a new repository is pushed to an appropriate namespace

View File

@ -9,30 +9,25 @@
{{if .User.IsAdmin}} {{if .User.IsAdmin}}
<span class="ui basic label">{{ctx.Locale.Tr "admin.users.admin"}}</span> <span class="ui basic label">{{ctx.Locale.Tr "admin.users.admin"}}</span>
{{end}} {{end}}
{{if .User.IsTypeBot}}
<span class="ui basic label">{{ctx.Locale.Tr "admin.users.bot"}}</span>
{{end}}
</div> </div>
<div class="flex-item-body"> <div class="flex-item-body">
<b>{{ctx.Locale.Tr "admin.users.auth_source"}}:</b> <b>{{ctx.Locale.Tr "admin.users.auth_source"}}:</b>
{{if eq .LoginSource.ID 0}} {{Iif (eq .LoginSource.ID 0) (ctx.Locale.Tr "admin.users.local") .LoginSource.Name}}
{{ctx.Locale.Tr "admin.users.local"}}
{{else}}
{{.LoginSource.Name}}
{{end}}
</div> </div>
<div class="flex-item-body"> <div class="flex-item-body">
<b>{{ctx.Locale.Tr "admin.users.activated"}}:</b> <b>{{ctx.Locale.Tr "admin.users.activated"}}:</b>
{{if .User.IsActive}} {{svg (Iif .User.IsActive "octicon-check" "octicon-x")}}
{{svg "octicon-check"}} </div>
{{else}} <div class="flex-item-body">
{{svg "octicon-x"}} <b>{{ctx.Locale.Tr "admin.users.prohibit_login"}}:</b>
{{end}} {{svg (Iif .User.ProhibitLogin "octicon-check" "octicon-x")}}
</div> </div>
<div class="flex-item-body"> <div class="flex-item-body">
<b>{{ctx.Locale.Tr "admin.users.restricted"}}:</b> <b>{{ctx.Locale.Tr "admin.users.restricted"}}:</b>
{{if .User.IsRestricted}} {{svg (Iif .User.IsRestricted "octicon-check" "octicon-x")}}
{{svg "octicon-check"}}
{{else}}
{{svg "octicon-x"}}
{{end}}
</div> </div>
<div class="flex-item-body"> <div class="flex-item-body">
<b>{{ctx.Locale.Tr "settings.visibility"}}:</b> <b>{{ctx.Locale.Tr "settings.visibility"}}:</b>
@ -42,11 +37,7 @@
</div> </div>
<div class="flex-item-body"> <div class="flex-item-body">
<b>{{ctx.Locale.Tr "admin.users.2fa"}}:</b> <b>{{ctx.Locale.Tr "admin.users.2fa"}}:</b>
{{if .TwoFactorEnabled}} {{svg (Iif .TwoFactorEnabled "octicon-check" "octicon-x")}}
<span class="text green">{{svg "octicon-check"}}</span>
{{else}}
{{svg "octicon-x"}}
{{end}}
</div> </div>
{{if .User.Language}} {{if .User.Language}}
<div class="flex-item-body"> <div class="flex-item-body">

View File

@ -3,7 +3,7 @@
<div class="flex-item"> <div class="flex-item">
<div class="flex-item-main"> <div class="flex-item-main">
<div class="flex-text-block"> <div class="flex-text-block">
{{.Email}} <a href="mailto:{{.Email}}">{{.Email}}</a>
{{if .IsPrimary}} {{if .IsPrimary}}
<div class="ui primary label">{{ctx.Locale.Tr "settings.primary"}}</div> <div class="ui primary label">{{ctx.Locale.Tr "settings.primary"}}</div>
{{end}} {{end}}

View File

@ -1,8 +1,13 @@
{{template "base/head" .}} {{template "base/head" .}}
<div class="page-content"> <div class="page-content">
<div class="tw-flex tw-justify-center tw-items-center tw-gap-5">
<a href="/devtest/repo-action-view/10/100">Run:CanCancel</a>
<a href="/devtest/repo-action-view/20/200">Run:CanApprove</a>
<a href="/devtest/repo-action-view/30/300">Run:CanRerun</a>
</div>
{{template "repo/actions/view_component" (dict {{template "repo/actions/view_component" (dict
"RunIndex" 1 "RunIndex" (or .RunID 10)
"JobIndex" 2 "JobIndex" (or .JobID 100)
"ActionsURL" (print AppSubUrl "/devtest/actions-mock") "ActionsURL" (print AppSubUrl "/devtest/actions-mock")
)}} )}}
</div> </div>

View File

@ -14,6 +14,7 @@
{{if $.CloneButtonShowSSH}} {{if $.CloneButtonShowSSH}}
<button class="item repo-clone-ssh" data-link="{{$.CloneButtonOriginLink.SSH}}">SSH</button> <button class="item repo-clone-ssh" data-link="{{$.CloneButtonOriginLink.SSH}}">SSH</button>
{{end}} {{end}}
<button class="item repo-clone-tea" data-link="{{$.CloneButtonOriginLink.Tea}}">Tea CLI</button>
</div> </div>
<div class="divider"></div> <div class="divider"></div>

View File

@ -17,7 +17,7 @@
{{ctx.Locale.PrettyNumber .OpenCount}} {{ctx.Locale.Tr "repo.issues.open_title"}} {{ctx.Locale.PrettyNumber .OpenCount}} {{ctx.Locale.Tr "repo.issues.open_title"}}
</a> </a>
<a class="{{if eq .State "closed"}}active {{end}}item flex-text-inline" href="{{if eq .State "closed"}}{{$allStatesLink}}{{else}}{{$closedLink}}{{end}}"> <a class="{{if eq .State "closed"}}active {{end}}item flex-text-inline" href="{{if eq .State "closed"}}{{$allStatesLink}}{{else}}{{$closedLink}}{{end}}">
{{svg "octicon-check"}} {{svg "octicon-issue-closed"}}
{{ctx.Locale.PrettyNumber .ClosedCount}} {{ctx.Locale.Tr "repo.issues.closed_title"}} {{ctx.Locale.PrettyNumber .ClosedCount}} {{ctx.Locale.Tr "repo.issues.closed_title"}}
</a> </a>
</div> </div>

View File

@ -1,13 +1,18 @@
{{if and .Issue.IsPull .IsIssuePoster (not .Issue.IsClosed) .Issue.PullRequest.HeadRepo}} {{- $isHeadForkedRepo := and .Issue.PullRequest .Issue.PullRequest.HeadRepo (ne .Issue.PullRequest.HeadRepo.FullName .Issue.PullRequest.BaseRepo.FullName) -}}
{{if and (not (eq .Issue.PullRequest.HeadRepo.FullName .Issue.PullRequest.BaseRepo.FullName)) .CanWriteToHeadRepo}} {{if $isHeadForkedRepo}}
{{- $isPullPoster := and .Issue.IsPull .IsIssuePoster -}}
{{- $isPullEditable := and .Issue.PullRequest (not .Issue.IsClosed) (not .Repository.IsArchived) -}}
{{- $allowToChange := and $isPullPoster $isPullEditable -}}
<div class="divider"></div> <div class="divider"></div>
<div class="ui checkbox loading-icon-2px" id="allow-edits-from-maintainers" <div class="ui checkbox {{if not $allowToChange}}disabled{{end}} loading-icon-2px"
{{if $allowToChange}}
id="allow-edits-from-maintainers"
data-url="{{.Issue.Link}}" data-url="{{.Issue.Link}}"
data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_desc"}}" data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_desc"}}"
data-prompt-error="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_err"}}" data-prompt-error="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_err"}}"
{{end}}
> >
<label><strong>{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers"}}</strong></label> <label><strong>{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers"}}</strong></label>
<input type="checkbox" {{if .Issue.PullRequest.AllowMaintainerEdit}}checked{{end}}> <input type="checkbox" {{if .Issue.PullRequest.AllowMaintainerEdit}}checked{{end}} {{if not $allowToChange}}disabled{{end}}>
</div> </div>
{{end}}
{{end}} {{end}}

View File

@ -6,7 +6,7 @@
{{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/assignee?issue_ids={{$pageMeta.Issue.ID}}"{{end}} {{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/assignee?issue_ids={{$pageMeta.Issue.ID}}"{{end}}
> >
<input class="combo-value" name="assignee_ids" type="hidden" value="{{$data.SelectedAssigneeIDs}}"> <input class="combo-value" name="assignee_ids" type="hidden" value="{{$data.SelectedAssigneeIDs}}">
<div class="ui dropdown {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}"> <div class="ui dropdown text-flex-grow {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}">
<a class="text muted"> <a class="text muted">
<strong>{{ctx.Locale.Tr "repo.issues.new.assignees"}}</strong> {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}} <strong>{{ctx.Locale.Tr "repo.issues.new.assignees"}}</strong> {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}}
</a> </a>

View File

@ -4,7 +4,7 @@
{{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/labels?issue_ids={{$pageMeta.Issue.ID}}"{{end}} {{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/labels?issue_ids={{$pageMeta.Issue.ID}}"{{end}}
> >
<input class="combo-value" name="label_ids" type="hidden" value="{{$data.SelectedLabelIDs}}"> <input class="combo-value" name="label_ids" type="hidden" value="{{$data.SelectedLabelIDs}}">
<div class="ui dropdown {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}"> <div class="ui dropdown text-flex-grow {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}">
<a class="text muted"> <a class="text muted">
<strong>{{ctx.Locale.Tr "repo.issues.new.labels"}}</strong> {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}} <strong>{{ctx.Locale.Tr "repo.issues.new.labels"}}</strong> {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}}
</a> </a>

View File

@ -6,7 +6,7 @@
{{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/milestone?issue_ids={{$pageMeta.Issue.ID}}"{{end}} {{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/milestone?issue_ids={{$pageMeta.Issue.ID}}"{{end}}
> >
<input class="combo-value" name="milestone_id" type="hidden" value="{{$data.SelectedMilestoneID}}"> <input class="combo-value" name="milestone_id" type="hidden" value="{{$data.SelectedMilestoneID}}">
<div class="ui dropdown {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}"> <div class="ui dropdown text-flex-grow {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}">
<a class="text muted"> <a class="text muted">
<strong>{{ctx.Locale.Tr "repo.issues.new.milestone"}}</strong> {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}} <strong>{{ctx.Locale.Tr "repo.issues.new.milestone"}}</strong> {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}}
</a> </a>

View File

@ -6,7 +6,7 @@
{{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/projects?issue_ids={{$pageMeta.Issue.ID}}"{{end}} {{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/projects?issue_ids={{$pageMeta.Issue.ID}}"{{end}}
> >
<input class="combo-value" name="project_id" type="hidden" value="{{$data.SelectedProjectID}}"> <input class="combo-value" name="project_id" type="hidden" value="{{$data.SelectedProjectID}}">
<div class="ui dropdown {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}"> <div class="ui dropdown text-flex-grow {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}">
<a class="text muted"> <a class="text muted">
<strong>{{ctx.Locale.Tr "repo.issues.new.projects"}}</strong> {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}} <strong>{{ctx.Locale.Tr "repo.issues.new.projects"}}</strong> {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}}
</a> </a>

View File

@ -6,7 +6,7 @@
{{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/request_review?issue_ids={{$pageMeta.Issue.ID}}"{{end}} {{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/request_review?issue_ids={{$pageMeta.Issue.ID}}"{{end}}
> >
<input type="hidden" class="combo-value" name="reviewer_ids">{{/* match CreateIssueForm */}} <input type="hidden" class="combo-value" name="reviewer_ids">{{/* match CreateIssueForm */}}
<div class="ui dropdown {{if or (not $hasCandidates) (not $data.CanChooseReviewer)}}disabled{{end}}"> <div class="ui dropdown text-flex-grow {{if or (not $hasCandidates) (not $data.CanChooseReviewer)}}disabled{{end}}">
<a class="text muted"> <a class="text muted">
<strong>{{ctx.Locale.Tr "repo.issues.review.reviewers"}}</strong> {{if $data.CanChooseReviewer}}{{svg "octicon-gear"}}{{end}} <strong>{{ctx.Locale.Tr "repo.issues.review.reviewers"}}</strong> {{if $data.CanChooseReviewer}}{{svg "octicon-gear"}}{{end}}
</a> </a>

View File

@ -2,10 +2,12 @@
{{if and .CanUseTimetracker (not .Repository.IsArchived)}} {{if and .CanUseTimetracker (not .Repository.IsArchived)}}
<div class="divider"></div> <div class="divider"></div>
<div> <div>
<div class="ui dropdown jump"> <div class="ui dropdown text-flex-grow jump">
<a class="text muted"> <a class="text muted">
<strong>{{ctx.Locale.Tr "repo.issues.tracker"}}</strong> {{svg "octicon-gear"}} <div>
{{if $.IsStopwatchRunning}}{{svg "octicon-stopwatch"}}{{end}} <strong>{{ctx.Locale.Tr "repo.issues.tracker"}}</strong> {{if $.IsStopwatchRunning}}{{svg "octicon-stopwatch"}}{{end}}
</div>
{{svg "octicon-gear"}}
</a> </a>
<div class="menu"> <div class="menu">
<a class="item issue-set-time-estimate show-modal" data-modal="#issue-time-set-estimate-modal"> <a class="item issue-set-time-estimate show-modal" data-modal="#issue-time-set-estimate-modal">

View File

@ -153,6 +153,11 @@
</div> </div>
{{end}} {{end}}
</div> </div>
{{else}}
<div class="tw-text-center tw-p-8">
<h3 class="tw-my-4">{{ctx.Locale.Tr "repo.issues.filter_no_results"}}</h3>
<p class="tw-text-placeholder-text">{{ctx.Locale.Tr "repo.issues.filter_no_results_placeholder"}}</p>
</div>
{{end}} {{end}}
{{if .IssueIndexerUnavailable}} {{if .IssueIndexerUnavailable}}
<div class="ui error message"> <div class="ui error message">

View File

@ -0,0 +1,6 @@
{
"info": {
"version": "{{AppVer | JSEscape}}"
},
"basePath": "{{AppSubUrl | JSEscape}}/api/v1"
}

View File

@ -12047,6 +12047,12 @@
"in": "path", "in": "path",
"required": true "required": true
}, },
{
"type": "string",
"description": "Filter by target base branch of the pull request",
"name": "base_branch",
"in": "query"
},
{ {
"enum": [ "enum": [
"open", "open",

View File

@ -80,6 +80,7 @@ func TestPackageMaven(t *testing.T) {
t.Run("UploadLegacy", func(t *testing.T) { t.Run("UploadLegacy", func(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()
// try to upload a package with legacy package name (will be saved as "GroupID-ArtifactID")
legacyRootLink := "/api/packages/user2/maven/com/gitea/legacy-project" legacyRootLink := "/api/packages/user2/maven/com/gitea/legacy-project"
req := NewRequestWithBody(t, "PUT", legacyRootLink+"/1.0.2/any-file-name?use_legacy_package_name=1", strings.NewReader("test-content")).AddBasicAuth(user.Name) req := NewRequestWithBody(t, "PUT", legacyRootLink+"/1.0.2/any-file-name?use_legacy_package_name=1", strings.NewReader("test-content")).AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated) MakeRequest(t, req, http.StatusCreated)
@ -97,6 +98,13 @@ func TestPackageMaven(t *testing.T) {
req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea%3Alegacy-project/1.0.2") req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea%3Alegacy-project/1.0.2")
MakeRequest(t, req, http.StatusNotFound) MakeRequest(t, req, http.StatusNotFound)
// legacy package names should also be able to be listed
req = NewRequest(t, "GET", legacyRootLink+"/maven-metadata.xml").AddBasicAuth(user.Name)
resp := MakeRequest(t, req, http.StatusOK)
respBody := resp.Body.String()
assert.Contains(t, respBody, "<version>1.0.2</version>")
// then upload a package with correct package name (will be saved as "GroupID:ArtifactID")
req = NewRequestWithBody(t, "PUT", legacyRootLink+"/1.0.3/any-file-name", strings.NewReader("test-content")).AddBasicAuth(user.Name) req = NewRequestWithBody(t, "PUT", legacyRootLink+"/1.0.3/any-file-name", strings.NewReader("test-content")).AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated) MakeRequest(t, req, http.StatusCreated)
_, err = packages.GetPackageByName(db.DefaultContext, user.ID, packages.TypeMaven, "com.gitea-legacy-project") _, err = packages.GetPackageByName(db.DefaultContext, user.ID, packages.TypeMaven, "com.gitea-legacy-project")
@ -114,6 +122,12 @@ func TestPackageMaven(t *testing.T) {
req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea%3Alegacy-project/1.0.2") req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea%3Alegacy-project/1.0.2")
MakeRequest(t, req, http.StatusOK) MakeRequest(t, req, http.StatusOK)
// now 2 packages should be listed
req = NewRequest(t, "GET", legacyRootLink+"/maven-metadata.xml").AddBasicAuth(user.Name)
resp = MakeRequest(t, req, http.StatusOK)
respBody = resp.Body.String()
assert.Contains(t, respBody, "<version>1.0.2</version>")
assert.Contains(t, respBody, "<version>1.0.3</version>")
require.NoError(t, packages.DeletePackageByID(db.DefaultContext, p.ID)) require.NoError(t, packages.DeletePackageByID(db.DefaultContext, p.ID))
}) })

View File

@ -5,7 +5,6 @@ package integration
import ( import (
"net/http" "net/http"
"net/url"
"testing" "testing"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
@ -14,12 +13,13 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestAPIRepoCollaboratorPermission(t *testing.T) { func TestAPIRepoCollaboratorPermission(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) { defer tests.PrepareTestEnv(t)()
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
repo2Owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo2.OwnerID}) repo2Owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo2.OwnerID})
@ -107,6 +107,19 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
DecodeJSON(t, resp, &repoPermission) DecodeJSON(t, resp, &repoPermission)
assert.Equal(t, "read", repoPermission.Permission) assert.Equal(t, "read", repoPermission.Permission)
t.Run("CollaboratorCanReadOwnPermission", func(t *testing.T) {
session := loginUser(t, user5.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission", repo2Owner.Name, repo2.Name, user5.Name).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
repoCollPerm := api.RepoCollaboratorPermission{}
DecodeJSON(t, resp, &repoCollPerm)
assert.Equal(t, "read", repoCollPerm.Permission)
})
}) })
t.Run("CollaboratorCanQueryItsPermissions", func(t *testing.T) { t.Run("CollaboratorCanQueryItsPermissions", func(t *testing.T) {
@ -141,5 +154,4 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
assert.Equal(t, "read", repoPermission.Permission) assert.Equal(t, "read", repoPermission.Permission)
}) })
})
} }

View File

@ -60,12 +60,20 @@ func TestEmptyRepoAddFile(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user30") session := loginUser(t, "user30")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
// test web page
req := NewRequest(t, "GET", "/user30/empty") req := NewRequest(t, "GET", "/user30/empty")
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
bodyString := resp.Body.String() bodyString := resp.Body.String()
assert.Contains(t, bodyString, "empty-repo-guide") assert.Contains(t, bodyString, "empty-repo-guide")
assert.True(t, test.IsNormalPageCompleted(bodyString)) assert.True(t, test.IsNormalPageCompleted(bodyString))
// test api
req = NewRequest(t, "GET", "/api/v1/repos/user30/empty/raw/main/README.md").AddTokenAuth(token)
session.MakeRequest(t, req, http.StatusNotFound)
// create a new file
req = NewRequest(t, "GET", "/user30/empty/_new/"+setting.Repository.DefaultBranch) req = NewRequest(t, "GET", "/user30/empty/_new/"+setting.Repository.DefaultBranch)
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body).Find(`input[name="commit_choice"]`) doc := NewHTMLParser(t, resp.Body).Find(`input[name="commit_choice"]`)

View File

@ -9,7 +9,10 @@ import (
"net/url" "net/url"
"testing" "testing"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestGitSmartHTTP(t *testing.T) { func TestGitSmartHTTP(t *testing.T) {
@ -18,51 +21,55 @@ func TestGitSmartHTTP(t *testing.T) {
func testGitSmartHTTP(t *testing.T, u *url.URL) { func testGitSmartHTTP(t *testing.T, u *url.URL) {
kases := []struct { kases := []struct {
p string method, path string
code int code int
}{ }{
{ {
p: "user2/repo1/info/refs", path: "user2/repo1/info/refs",
code: http.StatusOK, code: http.StatusOK,
}, },
{ {
p: "user2/repo1/HEAD", method: "HEAD",
path: "user2/repo1/info/refs",
code: http.StatusOK, code: http.StatusOK,
}, },
{ {
p: "user2/repo1/objects/info/alternates", path: "user2/repo1/HEAD",
code: http.StatusOK,
},
{
path: "user2/repo1/objects/info/alternates",
code: http.StatusNotFound, code: http.StatusNotFound,
}, },
{ {
p: "user2/repo1/objects/info/http-alternates", path: "user2/repo1/objects/info/http-alternates",
code: http.StatusNotFound, code: http.StatusNotFound,
}, },
{ {
p: "user2/repo1/../../custom/conf/app.ini", path: "user2/repo1/../../custom/conf/app.ini",
code: http.StatusNotFound, code: http.StatusNotFound,
}, },
{ {
p: "user2/repo1/objects/info/../../../../custom/conf/app.ini", path: "user2/repo1/objects/info/../../../../custom/conf/app.ini",
code: http.StatusNotFound, code: http.StatusNotFound,
}, },
{ {
p: `user2/repo1/objects/info/..\..\..\..\custom\conf\app.ini`, path: `user2/repo1/objects/info/..\..\..\..\custom\conf\app.ini`,
code: http.StatusBadRequest, code: http.StatusBadRequest,
}, },
} }
for _, kase := range kases { for _, kase := range kases {
t.Run(kase.p, func(t *testing.T) { t.Run(kase.path, func(t *testing.T) {
p := u.String() + kase.p req, err := http.NewRequest(util.IfZero(kase.method, "GET"), u.String()+kase.path, nil)
req, err := http.NewRequest("GET", p, nil) require.NoError(t, err)
assert.NoError(t, err)
req.SetBasicAuth("user2", userPassword) req.SetBasicAuth("user2", userPassword)
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err) require.NoError(t, err)
defer resp.Body.Close() defer resp.Body.Close()
assert.EqualValues(t, kase.code, resp.StatusCode) assert.EqualValues(t, kase.code, resp.StatusCode)
_, err = io.ReadAll(resp.Body) _, err = io.ReadAll(resp.Body)
assert.NoError(t, err) require.NoError(t, err)
}) })
} }
} }

View File

@ -130,8 +130,13 @@ func TestViewRepo1CloneLinkAnonymous(t *testing.T) {
link, exists := htmlDoc.doc.Find(".repo-clone-https").Attr("data-link") link, exists := htmlDoc.doc.Find(".repo-clone-https").Attr("data-link")
assert.True(t, exists, "The template has changed") assert.True(t, exists, "The template has changed")
assert.Equal(t, setting.AppURL+"user2/repo1.git", link) assert.Equal(t, setting.AppURL+"user2/repo1.git", link)
_, exists = htmlDoc.doc.Find(".repo-clone-ssh").Attr("data-link") _, exists = htmlDoc.doc.Find(".repo-clone-ssh").Attr("data-link")
assert.False(t, exists) assert.False(t, exists)
link, exists = htmlDoc.doc.Find(".repo-clone-tea").Attr("data-link")
assert.True(t, exists, "The template has changed")
assert.Equal(t, "tea clone user2/repo1", link)
} }
func TestViewRepo1CloneLinkAuthorized(t *testing.T) { func TestViewRepo1CloneLinkAuthorized(t *testing.T) {
@ -146,10 +151,15 @@ func TestViewRepo1CloneLinkAuthorized(t *testing.T) {
link, exists := htmlDoc.doc.Find(".repo-clone-https").Attr("data-link") link, exists := htmlDoc.doc.Find(".repo-clone-https").Attr("data-link")
assert.True(t, exists, "The template has changed") assert.True(t, exists, "The template has changed")
assert.Equal(t, setting.AppURL+"user2/repo1.git", link) assert.Equal(t, setting.AppURL+"user2/repo1.git", link)
link, exists = htmlDoc.doc.Find(".repo-clone-ssh").Attr("data-link") link, exists = htmlDoc.doc.Find(".repo-clone-ssh").Attr("data-link")
assert.True(t, exists, "The template has changed") assert.True(t, exists, "The template has changed")
sshURL := fmt.Sprintf("ssh://%s@%s:%d/user2/repo1.git", setting.SSH.User, setting.SSH.Domain, setting.SSH.Port) sshURL := fmt.Sprintf("ssh://%s@%s:%d/user2/repo1.git", setting.SSH.User, setting.SSH.Domain, setting.SSH.Port)
assert.Equal(t, sshURL, link) assert.Equal(t, sshURL, link)
link, exists = htmlDoc.doc.Find(".repo-clone-tea").Attr("data-link")
assert.True(t, exists, "The template has changed")
assert.Equal(t, "tea clone user2/repo1", link)
} }
func TestViewRepoWithSymlinks(t *testing.T) { func TestViewRepoWithSymlinks(t *testing.T) {

View File

@ -1188,3 +1188,13 @@ the "!important" is necessary to override Fomantic UI menu item styles, meanwhil
overflow: hidden !important; overflow: hidden !important;
text-overflow: ellipsis !important; text-overflow: ellipsis !important;
} }
.ui.dropdown.text-flex-grow {
display: flex;
}
.ui.dropdown.text-flex-grow > .text {
display: flex;
flex-grow: 1;
justify-content: space-between;
}

View File

@ -105,7 +105,6 @@ export default defineComponent({
intervalID: null as IntervalId | null, intervalID: null as IntervalId | null,
currentJobStepsStates: [] as Array<Record<string, any>>, currentJobStepsStates: [] as Array<Record<string, any>>,
artifacts: [] as Array<Record<string, any>>, artifacts: [] as Array<Record<string, any>>,
onHoverRerunIndex: -1,
menuVisible: false, menuVisible: false,
isFullScreen: false, isFullScreen: false,
timeVisible: { timeVisible: {
@ -120,7 +119,7 @@ export default defineComponent({
link: '', link: '',
title: '', title: '',
titleHTML: '', titleHTML: '',
status: 'unknown' as RunStatus, status: '' as RunStatus, // do not show the status before initialized, otherwise it would show an incorrect "error" icon
canCancel: false, canCancel: false,
canApprove: false, canApprove: false,
canRerun: false, canRerun: false,
@ -492,13 +491,13 @@ export default defineComponent({
<div class="action-view-left"> <div class="action-view-left">
<div class="job-group-section"> <div class="job-group-section">
<div class="job-brief-list"> <div class="job-brief-list">
<a class="job-brief-item" :href="run.link+'/jobs/'+index" :class="parseInt(jobIndex) === index ? 'selected' : ''" v-for="(job, index) in run.jobs" :key="job.id" @mouseenter="onHoverRerunIndex = job.id" @mouseleave="onHoverRerunIndex = -1"> <a class="job-brief-item" :href="run.link+'/jobs/'+index" :class="parseInt(jobIndex) === index ? 'selected' : ''" v-for="(job, index) in run.jobs" :key="job.id">
<div class="job-brief-item-left"> <div class="job-brief-item-left">
<ActionRunStatus :locale-status="locale.status[job.status]" :status="job.status"/> <ActionRunStatus :locale-status="locale.status[job.status]" :status="job.status"/>
<span class="job-brief-name tw-mx-2 gt-ellipsis">{{ job.name }}</span> <span class="job-brief-name tw-mx-2 gt-ellipsis">{{ job.name }}</span>
</div> </div>
<span class="job-brief-item-right"> <span class="job-brief-item-right">
<SvgIcon name="octicon-sync" role="button" :data-tooltip-content="locale.rerun" class="job-brief-rerun tw-mx-2 link-action" :data-url="`${run.link}/jobs/${index}/rerun`" v-if="job.canRerun && onHoverRerunIndex === job.id"/> <SvgIcon name="octicon-sync" role="button" :data-tooltip-content="locale.rerun" class="job-brief-rerun tw-mx-2 link-action" :data-url="`${run.link}/jobs/${index}/rerun`" v-if="job.canRerun"/>
<span class="step-summary-duration">{{ job.duration }}</span> <span class="step-summary-duration">{{ job.duration }}</span>
</span> </span>
</a> </a>
@ -721,11 +720,6 @@ export default defineComponent({
.job-brief-item .job-brief-rerun { .job-brief-item .job-brief-rerun {
cursor: pointer; cursor: pointer;
transition: transform 0.2s;
}
.job-brief-item .job-brief-rerun:hover {
transform: scale(130%);
} }
.job-brief-item .job-brief-item-left { .job-brief-item .job-brief-item-left {

View File

@ -0,0 +1,7 @@
import {substituteRepoOpenWithUrl} from './repo-common.ts';
test('substituteRepoOpenWithUrl', () => {
// For example: "x-github-client://openRepo/https://github.com/go-gitea/gitea"
expect(substituteRepoOpenWithUrl('proto://a/{url}', 'https://gitea')).toEqual('proto://a/https://gitea');
expect(substituteRepoOpenWithUrl('proto://a?link={url}', 'https://gitea')).toEqual('proto://a?link=https%3A%2F%2Fgitea');
});

View File

@ -42,23 +42,60 @@ export function initRepoActivityTopAuthorsChart() {
} }
} }
export function substituteRepoOpenWithUrl(tmpl: string, url: string): string {
const pos = tmpl.indexOf('{url}');
if (pos === -1) return tmpl;
const posQuestionMark = tmpl.indexOf('?');
const needEncode = posQuestionMark >= 0 && posQuestionMark < pos;
return tmpl.replace('{url}', needEncode ? encodeURIComponent(url) : url);
}
function initCloneSchemeUrlSelection(parent: Element) { function initCloneSchemeUrlSelection(parent: Element) {
const elCloneUrlInput = parent.querySelector<HTMLInputElement>('.repo-clone-url'); const elCloneUrlInput = parent.querySelector<HTMLInputElement>('.repo-clone-url');
const tabSsh = parent.querySelector('.repo-clone-ssh');
const tabHttps = parent.querySelector('.repo-clone-https'); const tabHttps = parent.querySelector('.repo-clone-https');
const tabSsh = parent.querySelector('.repo-clone-ssh');
const tabTea = parent.querySelector('.repo-clone-tea');
const updateClonePanelUi = function() { const updateClonePanelUi = function() {
const scheme = localStorage.getItem('repo-clone-protocol') || 'https'; let scheme = localStorage.getItem('repo-clone-protocol');
const isSSH = scheme === 'ssh' && Boolean(tabSsh) || scheme !== 'ssh' && !tabHttps; if (!['https', 'ssh', 'tea'].includes(scheme)) {
if (tabHttps) { scheme = 'https';
tabHttps.textContent = window.origin.split(':')[0].toUpperCase(); // show "HTTP" or "HTTPS" }
tabHttps.classList.toggle('active', !isSSH);
} // Fallbacks if the scheme preference is not available in the tabs, for example: empty repo page, there are only HTTPS and SSH
if (tabSsh) { if (scheme === 'tea' && !tabTea) {
tabSsh.classList.toggle('active', isSSH); scheme = 'https';
}
if (scheme === 'https' && !tabHttps) {
scheme = 'ssh';
} else if (scheme === 'ssh' && !tabSsh) {
scheme = 'https';
}
const isHttps = scheme === 'https';
const isSsh = scheme === 'ssh';
const isTea = scheme === 'tea';
if (tabHttps) {
tabHttps.textContent = window.origin.split(':')[0].toUpperCase(); // show "HTTP" or "HTTPS"
tabHttps.classList.toggle('active', isHttps);
}
if (tabSsh) {
tabSsh.classList.toggle('active', isSsh);
}
if (tabTea) {
tabTea.classList.toggle('active', isTea);
}
let tab: Element;
if (isHttps) {
tab = tabHttps;
} else if (isSsh) {
tab = tabSsh;
} else if (isTea) {
tab = tabTea;
} }
const tab = isSSH ? tabSsh : tabHttps;
if (!tab) return; if (!tab) return;
const link = toOriginUrl(tab.getAttribute('data-link')); const link = toOriginUrl(tab.getAttribute('data-link'));
@ -70,18 +107,22 @@ function initCloneSchemeUrlSelection(parent: Element) {
} }
} }
for (const el of parent.querySelectorAll<HTMLAnchorElement>('.js-clone-url-editor')) { for (const el of parent.querySelectorAll<HTMLAnchorElement>('.js-clone-url-editor')) {
el.href = el.getAttribute('data-href-template').replace('{url}', encodeURIComponent(link)); el.href = substituteRepoOpenWithUrl(el.getAttribute('data-href-template'), link);
} }
}; };
updateClonePanelUi(); updateClonePanelUi();
// tabSsh or tabHttps might not both exist, eg: guest view, or one is disabled by the server // tabSsh or tabHttps might not both exist, eg: guest view, or one is disabled by the server
tabHttps?.addEventListener('click', () => {
localStorage.setItem('repo-clone-protocol', 'https');
updateClonePanelUi();
});
tabSsh?.addEventListener('click', () => { tabSsh?.addEventListener('click', () => {
localStorage.setItem('repo-clone-protocol', 'ssh'); localStorage.setItem('repo-clone-protocol', 'ssh');
updateClonePanelUi(); updateClonePanelUi();
}); });
tabHttps?.addEventListener('click', () => { tabTea?.addEventListener('click', () => {
localStorage.setItem('repo-clone-protocol', 'https'); localStorage.setItem('repo-clone-protocol', 'tea');
updateClonePanelUi(); updateClonePanelUi();
}); });
elCloneUrlInput.addEventListener('focus', () => { elCloneUrlInput.addEventListener('focus', () => {