mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 03:18:24 +00:00 
			
		
		
		
	Fix various bugs (#35684)
1. Fix incorrect column in `applySubscribedCondition`, add a test 2. Fix debian version parsing, add more tests fix #35695 3. Fix log level for HTTP errors, fix #35651 4. Fix abused "panic" handler in API `Migrate` 5. Fix the redirection from PR to issue, add a test 6. Fix Actions variable & secret name validation, add more tests * envNameCIRegexMatch is unnecessary, removed * validating in "delete" function doesn't make sense, removed 7. Fix incorrect link in release email --------- Signed-off-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: delvh <dev.lh@web.de>
This commit is contained in:
		| @@ -476,7 +476,7 @@ func applySubscribedCondition(sess *xorm.Session, subscriberID int64) { | |||||||
| 			), | 			), | ||||||
| 			builder.Eq{"issue.poster_id": subscriberID}, | 			builder.Eq{"issue.poster_id": subscriberID}, | ||||||
| 			builder.In("issue.repo_id", builder. | 			builder.In("issue.repo_id", builder. | ||||||
| 				Select("id"). | 				Select("repo_id"). | ||||||
| 				From("watch"). | 				From("watch"). | ||||||
| 				Where(builder.And(builder.Eq{"user_id": subscriberID}, | 				Where(builder.And(builder.Eq{"user_id": subscriberID}, | ||||||
| 					builder.In("mode", repo_model.WatchModeNormal, repo_model.WatchModeAuto))), | 					builder.In("mode", repo_model.WatchModeNormal, repo_model.WatchModeAuto))), | ||||||
|   | |||||||
| @@ -197,6 +197,12 @@ func TestIssues(t *testing.T) { | |||||||
| 			}, | 			}, | ||||||
| 			[]int64{2}, | 			[]int64{2}, | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			issues_model.IssuesOptions{ | ||||||
|  | 				SubscriberID: 11, | ||||||
|  | 			}, | ||||||
|  | 			[]int64{11, 5, 9, 8, 3, 2, 1}, | ||||||
|  | 		}, | ||||||
| 	} { | 	} { | ||||||
| 		issues, err := issues_model.Issues(t.Context(), &test.Opts) | 		issues, err := issues_model.Issues(t.Context(), &test.Opts) | ||||||
| 		assert.NoError(t, err) | 		assert.NoError(t, err) | ||||||
|   | |||||||
| @@ -46,7 +46,7 @@ var ( | |||||||
| 	// https://www.debian.org/doc/debian-policy/ch-controlfields.html#source | 	// https://www.debian.org/doc/debian-policy/ch-controlfields.html#source | ||||||
| 	namePattern = regexp.MustCompile(`\A[a-z0-9][a-z0-9+-.]+\z`) | 	namePattern = regexp.MustCompile(`\A[a-z0-9][a-z0-9+-.]+\z`) | ||||||
| 	// https://www.debian.org/doc/debian-policy/ch-controlfields.html#version | 	// https://www.debian.org/doc/debian-policy/ch-controlfields.html#version | ||||||
| 	versionPattern = regexp.MustCompile(`\A(?:[0-9]:)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`) | 	versionPattern = regexp.MustCompile(`\A(?:(0|[1-9][0-9]*):)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`) | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type Package struct { | type Package struct { | ||||||
|   | |||||||
| @@ -176,4 +176,12 @@ func TestParseControlFile(t *testing.T) { | |||||||
| 		assert.Equal(t, []string{"a", "b"}, p.Metadata.Dependencies) | 		assert.Equal(t, []string{"a", "b"}, p.Metadata.Dependencies) | ||||||
| 		assert.Equal(t, full, p.Control) | 		assert.Equal(t, full, p.Control) | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
|  | 	t.Run("ValidVersions", func(t *testing.T) { | ||||||
|  | 		for _, version := range []string{"1.0", "0:1.2", "9:1.0", "10:1.0", "900:1a.2b-x-y_z~1+2"} { | ||||||
|  | 			p, err := ParseControlFile(buildContent("testpkg", version, "amd64")) | ||||||
|  | 			assert.NoError(t, err, "ParseControlFile with version %q", version) | ||||||
|  | 			assert.NotNil(t, p) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -104,7 +104,8 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) { | |||||||
| 		} | 		} | ||||||
| 		logf := logInfo | 		logf := logInfo | ||||||
| 		// lower the log level for some specific requests, in most cases these logs are not useful | 		// lower the log level for some specific requests, in most cases these logs are not useful | ||||||
| 		if strings.HasPrefix(req.RequestURI, "/assets/") /* static assets */ || | 		if status > 0 && status < 400 && | ||||||
|  | 			strings.HasPrefix(req.RequestURI, "/assets/") /* static assets */ || | ||||||
| 			req.RequestURI == "/user/events" /* Server-Sent Events (SSE) handler */ || | 			req.RequestURI == "/user/events" /* Server-Sent Events (SSE) handler */ || | ||||||
| 			req.RequestURI == "/api/actions/runner.v1.RunnerService/FetchTask" /* Actions Runner polling */ { | 			req.RequestURI == "/api/actions/runner.v1.RunnerService/FetchTask" /* Actions Runner polling */ { | ||||||
| 			logf = logTrace | 			logf = logTrace | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ | |||||||
| package repo | package repo | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	gocontext "context" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| @@ -173,7 +173,7 @@ func Migrate(ctx *context.APIContext) { | |||||||
| 		opts.AWSSecretAccessKey = form.AWSSecretAccessKey | 		opts.AWSSecretAccessKey = form.AWSSecretAccessKey | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	repo, err := repo_service.CreateRepositoryDirectly(ctx, ctx.Doer, repoOwner, repo_service.CreateRepoOptions{ | 	createdRepo, err := repo_service.CreateRepositoryDirectly(ctx, ctx.Doer, repoOwner, repo_service.CreateRepoOptions{ | ||||||
| 		Name:           opts.RepoName, | 		Name:           opts.RepoName, | ||||||
| 		Description:    opts.Description, | 		Description:    opts.Description, | ||||||
| 		OriginalURL:    form.CloneAddr, | 		OriginalURL:    form.CloneAddr, | ||||||
| @@ -187,35 +187,37 @@ func Migrate(ctx *context.APIContext) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	opts.MigrateToRepoID = repo.ID | 	opts.MigrateToRepoID = createdRepo.ID | ||||||
|  |  | ||||||
|  | 	doLongTimeMigrate := func(ctx gocontext.Context, doer *user_model.User) (migratedRepo *repo_model.Repository, retErr error) { | ||||||
| 		defer func() { | 		defer func() { | ||||||
| 			if e := recover(); e != nil { | 			if e := recover(); e != nil { | ||||||
| 			var buf bytes.Buffer | 				log.Error("MigrateRepository panic: %v\n%s", e, log.Stack(2)) | ||||||
| 			fmt.Fprintf(&buf, "Handler crashed with error: %v", log.Stack(2)) | 				if errDelete := repo_service.DeleteRepositoryDirectly(ctx, createdRepo.ID); errDelete != nil { | ||||||
|  | 					log.Error("Unable to delete repo after MigrateRepository panic: %v", errDelete) | ||||||
| 			err = errors.New(buf.String()) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if err == nil { |  | ||||||
| 			notify_service.MigrateRepository(ctx, ctx.Doer, repoOwner, repo) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if repo != nil { |  | ||||||
| 			if errDelete := repo_service.DeleteRepositoryDirectly(ctx, repo.ID); errDelete != nil { |  | ||||||
| 				log.Error("DeleteRepository: %v", errDelete) |  | ||||||
| 				} | 				} | ||||||
|  | 				retErr = errors.New("MigrateRepository panic") // no idea why it would happen, just legacy code | ||||||
| 			} | 			} | ||||||
| 		}() | 		}() | ||||||
|  |  | ||||||
| 	if repo, err = migrations.MigrateRepository(graceful.GetManager().HammerContext(), ctx.Doer, repoOwner.Name, opts, nil); err != nil { | 		migratedRepo, err := migrations.MigrateRepository(ctx, doer, repoOwner.Name, opts, nil) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		notify_service.MigrateRepository(ctx, doer, repoOwner, migratedRepo) | ||||||
|  | 		return migratedRepo, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// use a background context, don't cancel the migration even if the client goes away | ||||||
|  | 	// HammerContext doesn't seem right (from https://github.com/go-gitea/gitea/pull/9335/files) | ||||||
|  | 	// There are other abuses, maybe most HammerContext abuses should be fixed together in the future. | ||||||
|  | 	migratedRepo, err := doLongTimeMigrate(graceful.GetManager().HammerContext(), ctx.Doer) | ||||||
|  | 	if err != nil { | ||||||
| 		handleMigrateError(ctx, repoOwner, err) | 		handleMigrateError(ctx, repoOwner, err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Trace("Repository migrated: %s/%s", repoOwner.Name, form.RepoName) | 	ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, migratedRepo, access_model.Permission{AccessMode: perm.AccessModeAdmin})) | ||||||
| 	ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeAdmin})) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func handleMigrateError(ctx *context.APIContext, repoOwner *user_model.User, err error) { | func handleMigrateError(ctx *context.APIContext, repoOwner *user_model.User, err error) { | ||||||
|   | |||||||
| @@ -131,7 +131,7 @@ func getPullInfo(ctx *context.Context) (issue *issues_model.Issue, ok bool) { | |||||||
| 	ctx.Data["Issue"] = issue | 	ctx.Data["Issue"] = issue | ||||||
|  |  | ||||||
| 	if !issue.IsPull { | 	if !issue.IsPull { | ||||||
| 		ctx.NotFound(nil) | 		ctx.Redirect(issue.Link()) | ||||||
| 		return nil, false | 		return nil, false | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,10 +5,8 @@ package actions | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"regexp" |  | ||||||
|  |  | ||||||
| 	actions_model "code.gitea.io/gitea/models/actions" | 	actions_model "code.gitea.io/gitea/models/actions" | ||||||
| 	"code.gitea.io/gitea/modules/log" |  | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
| 	secret_service "code.gitea.io/gitea/services/secrets" | 	secret_service "code.gitea.io/gitea/services/secrets" | ||||||
| ) | ) | ||||||
| @@ -18,10 +16,6 @@ func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data, desc | |||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := envNameCIRegexMatch(name); err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	v, err := actions_model.InsertVariable(ctx, ownerID, repoID, name, util.ReserveLineBreakForTextarea(data), description) | 	v, err := actions_model.InsertVariable(ctx, ownerID, repoID, name, util.ReserveLineBreakForTextarea(data), description) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| @@ -35,10 +29,6 @@ func UpdateVariableNameData(ctx context.Context, variable *actions_model.ActionV | |||||||
| 		return false, err | 		return false, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := envNameCIRegexMatch(variable.Name); err != nil { |  | ||||||
| 		return false, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	variable.Data = util.ReserveLineBreakForTextarea(variable.Data) | 	variable.Data = util.ReserveLineBreakForTextarea(variable.Data) | ||||||
|  |  | ||||||
| 	return actions_model.UpdateVariableCols(ctx, variable, "name", "data", "description") | 	return actions_model.UpdateVariableCols(ctx, variable, "name", "data", "description") | ||||||
| @@ -49,14 +39,6 @@ func DeleteVariableByID(ctx context.Context, variableID int64) error { | |||||||
| } | } | ||||||
|  |  | ||||||
| func DeleteVariableByName(ctx context.Context, ownerID, repoID int64, name string) error { | func DeleteVariableByName(ctx context.Context, ownerID, repoID int64, name string) error { | ||||||
| 	if err := secret_service.ValidateName(name); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err := envNameCIRegexMatch(name); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	v, err := GetVariable(ctx, actions_model.FindVariablesOpts{ | 	v, err := GetVariable(ctx, actions_model.FindVariablesOpts{ | ||||||
| 		OwnerID: ownerID, | 		OwnerID: ownerID, | ||||||
| 		RepoID:  repoID, | 		RepoID:  repoID, | ||||||
| @@ -79,19 +61,3 @@ func GetVariable(ctx context.Context, opts actions_model.FindVariablesOpts) (*ac | |||||||
| 	} | 	} | ||||||
| 	return vars[0], nil | 	return vars[0], nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // some regular expression of `variables` and `secrets` |  | ||||||
| // reference to: |  | ||||||
| // https://docs.github.com/en/actions/learn-github-actions/variables#naming-conventions-for-configuration-variables |  | ||||||
| // https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets |  | ||||||
| var ( |  | ||||||
| 	forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI") |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func envNameCIRegexMatch(name string) error { |  | ||||||
| 	if forbiddenEnvNameCIRx.MatchString(name) { |  | ||||||
| 		log.Error("Env Name cannot be ci") |  | ||||||
| 		return util.NewInvalidArgumentErrorf("env name cannot be ci") |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -56,10 +56,6 @@ func DeleteSecretByID(ctx context.Context, ownerID, repoID, secretID int64) erro | |||||||
| } | } | ||||||
|  |  | ||||||
| func DeleteSecretByName(ctx context.Context, ownerID, repoID int64, name string) error { | func DeleteSecretByName(ctx context.Context, ownerID, repoID int64, name string) error { | ||||||
| 	if err := ValidateName(name); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	s, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{ | 	s, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{ | ||||||
| 		OwnerID: ownerID, | 		OwnerID: ownerID, | ||||||
| 		RepoID:  repoID, | 		RepoID:  repoID, | ||||||
|   | |||||||
| @@ -5,21 +5,29 @@ package secrets | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"regexp" | 	"regexp" | ||||||
|  | 	"strings" | ||||||
|  | 	"sync" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | // https://docs.github.com/en/actions/learn-github-actions/variables#naming-conventions-for-configuration-variables | ||||||
| // https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets | // https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets | ||||||
| var ( | var globalVars = sync.OnceValue(func() (ret struct { | ||||||
| 	namePattern            = regexp.MustCompile("(?i)^[A-Z_][A-Z0-9_]*$") | 	namePattern, forbiddenPrefixPattern *regexp.Regexp | ||||||
| 	forbiddenPrefixPattern = regexp.MustCompile("(?i)^GIT(EA|HUB)_") | }, | ||||||
|  | ) { | ||||||
| 	ErrInvalidName = util.NewInvalidArgumentErrorf("invalid secret name") | 	ret.namePattern = regexp.MustCompile("(?i)^[A-Z_][A-Z0-9_]*$") | ||||||
| ) | 	ret.forbiddenPrefixPattern = regexp.MustCompile("(?i)^GIT(EA|HUB)_") | ||||||
|  | 	return ret | ||||||
|  | }) | ||||||
|  |  | ||||||
| func ValidateName(name string) error { | func ValidateName(name string) error { | ||||||
| 	if !namePattern.MatchString(name) || forbiddenPrefixPattern.MatchString(name) { | 	vars := globalVars() | ||||||
| 		return ErrInvalidName | 	if !vars.namePattern.MatchString(name) || | ||||||
|  | 		vars.forbiddenPrefixPattern.MatchString(name) || | ||||||
|  | 		strings.EqualFold(name, "CI") /* CI is always set to true in GitHub Actions*/ { | ||||||
|  | 		return util.NewInvalidArgumentErrorf("invalid variable or secret name") | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										29
									
								
								services/secrets/validation_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								services/secrets/validation_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | // Copyright 2025 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package secrets | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestValidateName(t *testing.T) { | ||||||
|  | 	cases := []struct { | ||||||
|  | 		name  string | ||||||
|  | 		valid bool | ||||||
|  | 	}{ | ||||||
|  | 		{"FOO", true}, | ||||||
|  | 		{"FOO1_BAR2", true}, | ||||||
|  | 		{"_FOO", true}, // really? why support this | ||||||
|  | 		{"1FOO", false}, | ||||||
|  | 		{"giteA_xx", false}, | ||||||
|  | 		{"githuB_xx", false}, | ||||||
|  | 		{"cI", false}, | ||||||
|  | 	} | ||||||
|  | 	for _, c := range cases { | ||||||
|  | 		err := ValidateName(c.name) | ||||||
|  | 		assert.Equal(t, c.valid, err == nil, "ValidateName(%q)", c.name) | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -32,10 +32,10 @@ | |||||||
| 		<ul> | 		<ul> | ||||||
| 			{{if not .DisableDownloadSourceArchives}} | 			{{if not .DisableDownloadSourceArchives}} | ||||||
| 			<li> | 			<li> | ||||||
| 				<a href="{{.Release.Repo.Link}}/archive/{{.Release.TagName | PathEscapeSegments}}.zip" rel="nofollow"><strong>{{.locale.Tr "mail.release.download.zip"}}</strong></a> | 				<a href="{{.Release.Repo.HTMLURL}}/archive/{{.Release.TagName | PathEscapeSegments}}.zip" rel="nofollow"><strong>{{.locale.Tr "mail.release.download.zip"}}</strong></a> | ||||||
| 			</li> | 			</li> | ||||||
| 			<li> | 			<li> | ||||||
| 				<a href="{{.Release.Repo.Link}}/archive/{{.Release.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow"><strong>{{.locale.Tr "mail.release.download.targz"}}</strong></a> | 				<a href="{{.Release.Repo.HTMLURL}}/archive/{{.Release.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow"><strong>{{.locale.Tr "mail.release.download.targz"}}</strong></a> | ||||||
| 			</li> | 			</li> | ||||||
| 			{{end}} | 			{{end}} | ||||||
| 			{{if .Release.Attachments}} | 			{{if .Release.Attachments}} | ||||||
|   | |||||||
| @@ -131,9 +131,5 @@ func TestAPIRepoSecrets(t *testing.T) { | |||||||
| 		req = NewRequest(t, "DELETE", url). | 		req = NewRequest(t, "DELETE", url). | ||||||
| 			AddTokenAuth(token) | 			AddTokenAuth(token) | ||||||
| 		MakeRequest(t, req, http.StatusNotFound) | 		MakeRequest(t, req, http.StatusNotFound) | ||||||
|  |  | ||||||
| 		req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/actions/secrets/000", repo.FullName())). |  | ||||||
| 			AddTokenAuth(token) |  | ||||||
| 		MakeRequest(t, req, http.StatusBadRequest) |  | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -92,9 +92,5 @@ func TestAPIUserSecrets(t *testing.T) { | |||||||
| 		req = NewRequest(t, "DELETE", url). | 		req = NewRequest(t, "DELETE", url). | ||||||
| 			AddTokenAuth(token) | 			AddTokenAuth(token) | ||||||
| 		MakeRequest(t, req, http.StatusNotFound) | 		MakeRequest(t, req, http.StatusNotFound) | ||||||
|  |  | ||||||
| 		req = NewRequest(t, "DELETE", "/api/v1/user/actions/secrets/000"). |  | ||||||
| 			AddTokenAuth(token) |  | ||||||
| 		MakeRequest(t, req, http.StatusBadRequest) |  | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -497,9 +497,13 @@ func TestIssueRedirect(t *testing.T) { | |||||||
| 	resp = session.MakeRequest(t, req, http.StatusSeeOther) | 	resp = session.MakeRequest(t, req, http.StatusSeeOther) | ||||||
| 	assert.Equal(t, "/user2/repo1/pulls/2", test.RedirectURL(resp)) | 	assert.Equal(t, "/user2/repo1/pulls/2", test.RedirectURL(resp)) | ||||||
|  |  | ||||||
| 	repoUnit := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoUnit{RepoID: 1, Type: unit.TypeIssues}) | 	// by the way, test PR redirection | ||||||
|  | 	req = NewRequest(t, "GET", "/user2/repo1/pulls/1") | ||||||
|  | 	resp = session.MakeRequest(t, req, http.StatusSeeOther) | ||||||
|  | 	assert.Equal(t, "/user2/repo1/issues/1", test.RedirectURL(resp)) | ||||||
|  |  | ||||||
| 	// disable issue unit, it will be reset | 	// disable issue unit | ||||||
|  | 	repoUnit := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoUnit{RepoID: 1, Type: unit.TypeIssues}) | ||||||
| 	_, err := db.DeleteByID[repo_model.RepoUnit](t.Context(), repoUnit.ID) | 	_, err := db.DeleteByID[repo_model.RepoUnit](t.Context(), repoUnit.ID) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user