diff --git a/modules/testlogger/testlogger.go b/modules/testlogger/testlogger.go
index c5ca90622..7300b8066 100644
--- a/modules/testlogger/testlogger.go
+++ b/modules/testlogger/testlogger.go
@@ -43,9 +43,7 @@ func (w *testLoggerWriterCloser) pushT(t testing.TB) {
 }
 
 func (w *testLoggerWriterCloser) Log(level log.Level, msg string) {
-	if len(msg) > 0 && msg[len(msg)-1] == '\n' {
-		msg = msg[:len(msg)-1]
-	}
+	msg = strings.TrimSpace(msg)
 
 	w.printMsg(msg)
 	if level >= log.ERROR {
@@ -111,6 +109,279 @@ var ignoredErrorMessageSuffixes = []string{
 
 	// TestRebuildCargo
 	`RebuildCargoIndex() [E] RebuildIndex failed: GetRepositoryByOwnerAndName: repository does not exist [id: 0, uid: 0, owner_name: user2, name: _cargo-index]`,
+
+	// TestCommitMail/Delete/Not_activated
+	`/context_response.go:87:HTML() [E] Render failed: failed to render template: repo/editor/edit, error: template error: builtin(static):repo/editor/edit:13:13 : executing "repo/editor/edit" at <len .TreeNames>: error calling len: reflect: call of reflect.Value.Type on zero Value
+----------------------------------------------------------------------
+					{{$n := len .TreeNames}}
+					        ^
+----------------------------------------------------------------------`,
+	// TestCommitMail/Delete/Not_belong_to_user
+	`/context_response.go:87:HTML() [E] Render failed: failed to render template: repo/editor/edit, error: template error: builtin(static):repo/editor/edit:13:13 : executing "repo/editor/edit" at <len .TreeNames>: error calling len: reflect: call of reflect.Value.Type on zero Value
+----------------------------------------------------------------------
+					{{$n := len .TreeNames}}
+					        ^
+----------------------------------------------------------------------`,
+	// TestCommitMail/Apply_patch/Not_activated
+	`/context_response.go:87:HTML() [E] Render failed: failed to render template: repo/editor/edit, error: template error: builtin(static):repo/editor/edit:13:13 : executing "repo/editor/edit" at <len .TreeNames>: error calling len: reflect: call of reflect.Value.Type on zero Value
+----------------------------------------------------------------------
+					{{$n := len .TreeNames}}
+					        ^
+----------------------------------------------------------------------`,
+	// TestCommitMail/Apply_patch/Not_belong_to_user
+	`/context_response.go:87:HTML() [E] Render failed: failed to render template: repo/editor/edit, error: template error: builtin(static):repo/editor/edit:13:13 : executing "repo/editor/edit" at <len .TreeNames>: error calling len: reflect: call of reflect.Value.Type on zero Value
+----------------------------------------------------------------------
+					{{$n := len .TreeNames}}
+					        ^
+----------------------------------------------------------------------`,
+	// TestCommitMail/Cherry_pick/Not_activated
+	`/context_response.go:87:HTML() [E] Render failed: failed to render template: repo/editor/edit, error: template error: builtin(static):repo/editor/edit:13:13 : executing "repo/editor/edit" at <len .TreeNames>: error calling len: reflect: call of reflect.Value.Type on zero Value
+----------------------------------------------------------------------
+					{{$n := len .TreeNames}}
+					        ^
+----------------------------------------------------------------------`,
+	// TestCommitMail/Cherry_pick/Not_belong_to_user
+	`/context_response.go:87:HTML() [E] Render failed: failed to render template: repo/editor/edit, error: template error: builtin(static):repo/editor/edit:13:13 : executing "repo/editor/edit" at <len .TreeNames>: error calling len: reflect: call of reflect.Value.Type on zero Value
+----------------------------------------------------------------------
+					{{$n := len .TreeNames}}
+					        ^
+----------------------------------------------------------------------`,
+	// TestDangerZoneConfirmation/Convert_fork/Fail
+	`/gitea-repositories/user20/big_test_public_fork_7.git Error: no such file or directory`,
+	// TestGitSmartHTTP
+	`/web/repo/githttp.go:384:sendFile() [E] request file path contains invalid path: objects/info/..\..\..\..\custom\conf\app.ini`,
+	// TestGit/HTTP/BranchProtectMerge
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Not allowed to push to protected branch protected. HookPreReceive(last) failed: internal API error response, status=403`,
+	// TestGit/HTTP/BranchProtectMerge
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Not allowed to push to protected branch protected. HookPreReceive(last) failed: internal API error response, status=403`,
+	// TestGit/HTTP/BranchProtectMerge
+	`/gitea-repositories/user2/repo-tmp-17.git'`,
+	// TestGit/HTTP/BranchProtectMerge
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: branch protected is protected from force push. HookPreReceive(last) failed: internal API error response, status=403`,
+	// TestGit/HTTP/MergeFork/CreatePRAndMerge
+	`s/web/repo/branch.go:108:DeleteBranchPost() [E] DeleteBranch: GetBranch: branch does not exist [repo_id: 1099 name: user2:master]`,  // sqlite
+	"s/web/repo/branch.go:108:DeleteBranchPost() [E] DeleteBranch: GetBranch: branch does not exist [repo_id: 10000 name: user2:master]", // mysql
+	"s/web/repo/branch.go:108:DeleteBranchPost() [E] DeleteBranch: GetBranch: branch does not exist [repo_id: 1060 name: user2:master]",  // pgsql
+	// TestGit/SSH/LFS/PushCommit/Little
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/LFS/PushCommit/Little
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/LFS/PushCommit/Big
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/LFS/PushCommit/Big
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/LFS/Locks
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/LFS/Locks
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/LFS/Locks
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/LFS/Locks
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/LFS/Locks
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/CreateAgitFlowPull/PushParams/NoParams
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/CreateAgitFlowPull/PushParams/NoParams
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/CreateAgitFlowPull/PushParams/TitleOverride
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/CreateAgitFlowPull/PushParams/TitleOverride
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/CreateAgitFlowPull/PushParams/DescriptionOverride
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/CreateAgitFlowPull/PushParams/DescriptionOverride
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/CreateAgitFlowPull/Force_push/Fails
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/CreateAgitFlowPull/Force_push/Fails
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/CreateAgitFlowPull/Force_push/Succeeds
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/CreateAgitFlowPull/Force_push/Succeeds
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/CreateAgitFlowPull/Force_push
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/CreateAgitFlowPull/Force_push
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/CreateAgitFlowPull/Branch_already_contains_commit
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/CreateAgitFlowPull/Branch_already_contains_commit
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/CreateAgitFlowPull
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/CreateAgitFlowPull
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/CreateAgitFlowPull
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/CreateAgitFlowPull
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/CreateAgitFlowPull
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/CreateAgitFlowPull
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/CreateAgitFlowPull
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/CreateAgitFlowPull
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/BranchProtectMerge
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/BranchProtectMerge
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/BranchProtectMerge
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/BranchProtectMerge
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/BranchProtectMerge
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Not allowed to push to protected branch protected. HookPreReceive(last) failed: internal API error response, status=403`,
+	// TestGit/SSH/BranchProtectMerge
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/BranchProtectMerge
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/BranchProtectMerge
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/BranchProtectMerge
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/BranchProtectMerge
+	`/gitea-repositories/user2/repo-tmp-18.git'`,
+	// TestGit/SSH/BranchProtectMerge
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/BranchProtectMerge
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/BranchProtectMerge
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/BranchProtectMerge
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/BranchProtectMerge
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: branch protected is protected from force push. HookPreReceive(last) failed: internal API error response, status=403`,
+	// TestGit/SSH/BranchProtectMerge
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/BranchProtectMerge
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
+	// TestGit/SSH/MergeFork/CreatePRAndMerge
+	`s/web/repo/branch.go:108:DeleteBranchPost() [E] DeleteBranch: GetBranch: branch does not exist [repo_id: 1102 name: user2:master]`,  // sqlite
+	"s/web/repo/branch.go:108:DeleteBranchPost() [E] DeleteBranch: GetBranch: branch does not exist [repo_id: 10003 name: user2:master]", // mysql
+	"s/web/repo/branch.go:108:DeleteBranchPost() [E] DeleteBranch: GetBranch: branch does not exist [repo_id: 1063 name: user2:master]",  // pgsql
+	// TestGit/SSH/PushCreate
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Push to create is not enabled for users. ServCommand failed: internal API error response, status=403`,
+	// TestGit/SSH/PushCreate
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Cannot find repository: user2/repo-tmp-push-create-ssh. ServCommand failed: internal API error response, status=404`,
+	// TestGit/SSH/PushCreate
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Invalid repo name. Invalid repo name: invalid/repo-tmp-push-create-ssh`,
+	// TestIssueReaction
+	`rs/web/repo/issue.go:3282:ChangeIssueReaction() [E] ChangeIssueReaction: '8ball' is not an allowed reaction`,
+	// TestIssuePinMove
+	`eb/repo/issue_pin.go:95:IssuePinMove() [E] Issue does not belong to this repository`,
+	// TestLinksLogin
+	`ervices/pull/pull.go:880:GetIssuesAllCommitStatus() [E] getAllCommitStatus: cant get commit statuses of pull [6]: object does not exist [id: refs/pull/2/head, rel_path: ]`,
+	// TestLinksLogin
+	`ervices/pull/pull.go:880:GetIssuesAllCommitStatus() [E] getAllCommitStatus: cant get commit statuses of pull [6]: object does not exist [id: refs/pull/2/head, rel_path: ]`,
+	// TestLinksLogin
+	`ervices/pull/pull.go:880:GetIssuesAllCommitStatus() [E] getAllCommitStatus: cant get commit statuses of pull [6]: object does not exist [id: refs/pull/2/head, rel_path: ]`,
+	// TestLinksLogin
+	`ervices/pull/pull.go:872:GetIssuesAllCommitStatus() [E] Cannot open git repository <Repository 23:org17/big_test_public_4> for issue #1[20]. Error: no such file or directory`,
+	// TestMigrate
+	`] for OwnerID[2] failed: error while listing issues: token does not have at least one of required scope(s): [read:issue]`,
+	// TestMigrate
+	`ervices/task/task.go:53:handler() [E] Run task failed: error while listing issues: token does not have at least one of required scope(s): [read:issue]`,
+	// TestMigrate
+	`] for OwnerID[2] failed: error while listing issues: token does not have at least one of required scope(s): [read:issue]`,
+	// TestMigrate
+	`ervices/task/task.go:53:handler() [E] Run task failed: error while listing issues: token does not have at least one of required scope(s): [read:issue]`,
+	// TestMirrorPush
+	`/web/repo/githttp.go:533:GetInfoRefs() [E] fork/exec /usr/bin/git: no such file or directory -`,
+
+	// TestOrgMembers
+	`nization/org_user.go:122:loadOrganizationOwners() [E] Organization does not have owner team: 25`,
+	// TestOrgMembers
+	`nization/org_user.go:122:loadOrganizationOwners() [E] Organization does not have owner team: 25`,
+	// TestOrgMembers
+	`nization/org_user.go:122:loadOrganizationOwners() [E] Organization does not have owner team: 25`,
+	// TestRecentlyPushed/unrelated_branches_are_not_shown
+	`repository/branch.go:30:SyncRepoBranches() [E] OpenRepository[user30/repo50]: %!w(*errors.errorString=&{no such file or directory})`,
+	// TestRecentlyPushed/unrelated_branches_are_not_shown
+	`repository/branch.go:488:handlerBranchSync() [E] syncRepoBranches [50] failed: no such file or directory`,
+	// TestRecentlyPushed/unrelated_branches_are_not_shown
+	`repository/branch.go:30:SyncRepoBranches() [E] OpenRepository[user30/repo51]: %!w(*errors.errorString=&{no such file or directory})`,
+	// TestRecentlyPushed/unrelated_branches_are_not_shown
+	`repository/branch.go:488:handlerBranchSync() [E] syncRepoBranches [51] failed: no such file or directory`,
+	// TestRecentlyPushed/unrelated_branches_are_not_shown
+	`repository/branch.go:30:SyncRepoBranches() [E] OpenRepository[user2/scoped_label]: %!w(*errors.errorString=&{no such file or directory})`,
+	// TestRecentlyPushed/unrelated_branches_are_not_shown
+	`repository/branch.go:488:handlerBranchSync() [E] syncRepoBranches [55] failed: no such file or directory`,
+	// TestCantMergeConflict
+	"]user1/repo1#1[base...conflict]> Unable to merge tracking into base: Merge Conflict Error: exit status 1: \nAuto-merging README.md\nCONFLICT (content): Merge conflict in README.md\nAutomatic merge failed; fix conflicts and then commit the result.",
+
+	// TestCantMergeUnrelated
+	`]user1/repo1#1[base...unrelated]> Unable to merge tracking into base: Merge UnrelatedHistories Error: exit status 128: fatal: refusing to merge unrelated histories`,
+	// TestCantFastForwardOnlyMergeDiverging
+	"]user1/repo1#1[master...diverging]> Unable to merge tracking into base: Merge DivergingFastForwardOnly Error: exit status 128: hint: Diverging branches can't be fast-forwarded, you need to either:\nhint: \nhint: \tgit merge --no-ff\nhint: \nhint: or:\nhint: \nhint: \tgit rebase\nhint: \nhint: Disable this message with \"git config advice.diverging false\"\nfatal: Not possible to fast-forward, aborting.",
+	// TestPullrequestReopen/Base_branch_deleted
+	`fatal: couldn't find remote ref base-branch`,
+	// TestPullrequestReopen/Head_branch_deleted
+	`]user2/reopen-base#1[base-branch...org26/reopen-head:head-branch]>]: branch does not exist [repo_id: 0 name: head-branch]`,
+	// TestDatabaseMissingABranch
+	`repository/branch.go:30:SyncRepoBranches() [E] OpenRepository[user30/repo50]: %!w(*errors.errorString=&{no such file or directory})`,
+	// TestDatabaseMissingABranch
+	`repository/branch.go:488:handlerBranchSync() [E] syncRepoBranches [50] failed: no such file or directory`,
+	// TestDatabaseMissingABranch
+	`repository/branch.go:30:SyncRepoBranches() [E] OpenRepository[user30/repo51]: %!w(*errors.errorString=&{no such file or directory})`,
+	// TestDatabaseMissingABranch
+	`repository/branch.go:488:handlerBranchSync() [E] syncRepoBranches [51] failed: no such file or directory`,
+	// TestDatabaseMissingABranch
+	`repository/branch.go:30:SyncRepoBranches() [E] OpenRepository[user2/scoped_label]: %!w(*errors.errorString=&{no such file or directory})`,
+	// TestDatabaseMissingABranch
+	`repository/branch.go:488:handlerBranchSync() [E] syncRepoBranches [55] failed: no such file or directory`,
+	// TestDatabaseMissingABranch
+	`repository/branch.go:100:LoadBranches() [E] loadOneBranch() on repo #1, branch 'will-be-missing' failed: CountDivergingCommits: exit status 128 - fatal: bad revision 'master...refs/heads/will-be-missing'
+		- fatal: bad revision 'master...refs/heads/will-be-missing'`,
+	// TestDatabaseMissingABranch
+	`repository/branch.go:30:SyncRepoBranches() [E] OpenRepository[user30/repo50]: %!w(*errors.errorString=&{no such file or directory})`,
+	// TestDatabaseMissingABranch
+	`repository/branch.go:488:handlerBranchSync() [E] syncRepoBranches [50] failed: no such file or directory`,
+	// TestDatabaseMissingABranch
+	`repository/branch.go:30:SyncRepoBranches() [E] OpenRepository[user30/repo51]: %!w(*errors.errorString=&{no such file or directory})`,
+	// TestDatabaseMissingABranch
+	`repository/branch.go:488:handlerBranchSync() [E] syncRepoBranches [51] failed: no such file or directory`,
+	// TestDatabaseMissingABranch
+	`repository/branch.go:30:SyncRepoBranches() [E] OpenRepository[user2/scoped_label]: %!w(*errors.errorString=&{no such file or directory})`,
+	// TestDatabaseMissingABranch
+	`repository/branch.go:488:handlerBranchSync() [E] syncRepoBranches [55] failed: no such file or directory`,
+	// TestDatabaseMissingABranch
+	"LoadBranches() [E] loadOneBranch() on repo #1, branch 'will-be-missing' failed: CountDivergingCommits: exit status 128 - fatal: bad revision 'master...refs/heads/will-be-missing'\n - fatal: bad revision 'master...refs/heads/will-be-missing'",
+
+	// TestCreateNewTagProtected/Git
+	`s/private/ssh_log.go:26:SSHLog() [E] ssh: Tag v-2 is protected. HookPreReceive(last) failed: internal API error response, status=403`,
+	// TestMarkDownReadmeImage
+	`ers/web/repo/view.go:1167:checkOutdatedBranch() [E] GetBranch: branch does not exist [repo_id: 1 name: home-md-img-check]`,
+	// TestMarkDownReadmeImage
+	`ers/web/repo/view.go:1167:checkOutdatedBranch() [E] GetBranch: branch does not exist [repo_id: 1 name: home-md-img-check]`,
+	// TestMarkDownReadmeImageSubfolder
+	`ers/web/repo/view.go:1167:checkOutdatedBranch() [E] GetBranch: branch does not exist [repo_id: 1 name: sub-home-md-img-check]`,
+	// TestMarkDownReadmeImageSubfolder
+	`ers/web/repo/view.go:1167:checkOutdatedBranch() [E] GetBranch: branch does not exist [repo_id: 1 name: sub-home-md-img-check]`,
+
+	// TestKeyOnlyOneType
+	`:ssh-key-test-repo-push is not authorized to write to user2/ssh-key-test-repo. ServCommand failed: internal API error response, status=401`,
+
+	// TestPullRebase
+	"/gitea-repositories/user2/repo1.git' does not appear to be a git repository\nfatal: Could not read from remote repository.\n\nPlease make sure you have the correct access rights\nand the repository exists.",
+
+	// TestPullRebaseMerge
+	"]user2/repo1#3[master...branch2]>]: branch does not exist [repo_id: 0 name: branch2]",
+
+	// TestAuthorizeNoClientID
+	`TrString() [E] Missing translation "form.ResponseType"`,
+
+	// TestWebhookForms
+	`TrString() [E] Missing translation "form.AuthorizationHeader"`,
+	`TrString() [E] Missing translation "form.Channel"`,
+	`TrString() [E] Missing translation "form.ContentType"`,
+	`TrString() [E] Missing translation "form.HTTPMethod"`,
+	`TrString() [E] Missing translation "form.PayloadURL"`,
+
+	// TestRenameInvalidUsername
+	`TrString() [E] Missing translation "form.Name"`,
 }
 
 func (w *testLoggerWriterCloser) recordError(msg string) {
@@ -128,6 +399,11 @@ func (w *testLoggerWriterCloser) recordError(msg string) {
 		err = w.errs[len(w.errs)-1]
 	}
 
+	if len(w.t) > 0 {
+		// format error message to easily add it to the ignore list
+		msg = fmt.Sprintf("// %s\n\t%q,", w.t[len(w.t)-1].Name(), msg)
+	}
+
 	err = errors.Join(err, errors.New(msg))
 
 	if len(w.errs) > 0 {
diff --git a/tests/integration/cmd_forgejo_actions_test.go b/tests/integration/cmd_forgejo_actions_test.go
index 44211007f..e45526ac7 100644
--- a/tests/integration/cmd_forgejo_actions_test.go
+++ b/tests/integration/cmd_forgejo_actions_test.go
@@ -4,8 +4,11 @@ package integration
 
 import (
 	gocontext "context"
+	"errors"
+	"io"
 	"net/url"
 	"os"
+	"os/exec"
 	"strings"
 	"testing"
 
@@ -19,16 +22,18 @@ import (
 
 func Test_CmdForgejo_Actions(t *testing.T) {
 	onGiteaRun(t, func(*testing.T, *url.URL) {
-		token, err := cmdForgejoCaptureOutput(t, []string{"forgejo", "forgejo-cli", "actions", "generate-runner-token"})
+		token, err := runMainApp("forgejo-cli", "actions", "generate-runner-token")
 		assert.NoError(t, err)
 		assert.EqualValues(t, 40, len(token))
 
-		secret, err := cmdForgejoCaptureOutput(t, []string{"forgejo", "forgejo-cli", "actions", "generate-secret"})
+		secret, err := runMainApp("forgejo-cli", "actions", "generate-secret")
 		assert.NoError(t, err)
 		assert.EqualValues(t, 40, len(secret))
 
-		_, err = cmdForgejoCaptureOutput(t, []string{"forgejo", "forgejo-cli", "actions", "register"})
-		assert.ErrorContains(t, err, "at least one of the --secret")
+		_, err = runMainApp("forgejo-cli", "actions", "register")
+		var exitErr *exec.ExitError
+		assert.True(t, errors.As(err, &exitErr))
+		assert.Contains(t, string(exitErr.Stderr), "at least one of the --secret")
 
 		for _, testCase := range []struct {
 			testName     string
@@ -62,10 +67,12 @@ func Test_CmdForgejo_Actions(t *testing.T) {
 			},
 		} {
 			t.Run(testCase.testName, func(t *testing.T) {
-				cmd := []string{"forgejo", "forgejo-cli", "actions", "register", "--secret", testCase.secret, "--scope", testCase.scope}
-				output, err := cmdForgejoCaptureOutput(t, cmd)
-				assert.ErrorContains(t, err, testCase.errorMessage)
+				output, err := runMainApp("forgejo-cli", "actions", "register", "--secret", testCase.secret, "--scope", testCase.scope)
 				assert.EqualValues(t, "", output)
+
+				var exitErr *exec.ExitError
+				assert.True(t, errors.As(err, &exitErr))
+				assert.Contains(t, string(exitErr.Stderr), testCase.errorMessage)
 			})
 		}
 
@@ -75,7 +82,7 @@ func Test_CmdForgejo_Actions(t *testing.T) {
 		for _, testCase := range []struct {
 			testName     string
 			secretOption func() string
-			stdin        []string
+			stdin        io.Reader
 		}{
 			{
 				testName: "secret from argument",
@@ -88,7 +95,7 @@ func Test_CmdForgejo_Actions(t *testing.T) {
 				secretOption: func() string {
 					return "--secret-stdin"
 				},
-				stdin: []string{secret},
+				stdin: strings.NewReader(secret),
 			},
 			{
 				testName: "secret from file",
@@ -100,8 +107,7 @@ func Test_CmdForgejo_Actions(t *testing.T) {
 			},
 		} {
 			t.Run(testCase.testName, func(t *testing.T) {
-				cmd := []string{"forgejo", "forgejo-cli", "actions", "register", testCase.secretOption(), "--scope=org26"}
-				uuid, err := cmdForgejoCaptureOutput(t, cmd, testCase.stdin...)
+				uuid, err := runMainAppWithStdin(testCase.stdin, "forgejo-cli", "actions", "register", testCase.secretOption(), "--scope=org26")
 				assert.NoError(t, err)
 				assert.EqualValues(t, expecteduuid, uuid)
 			})
@@ -161,7 +167,7 @@ func Test_CmdForgejo_Actions(t *testing.T) {
 		} {
 			t.Run(testCase.testName, func(t *testing.T) {
 				cmd := []string{
-					"forgejo", "forgejo-cli", "actions", "register",
+					"actions", "register",
 					"--secret", testCase.secret, "--scope", testCase.scope,
 				}
 				if testCase.name != "" {
@@ -177,7 +183,7 @@ func Test_CmdForgejo_Actions(t *testing.T) {
 				// Run twice to verify it is idempotent
 				//
 				for i := 0; i < 2; i++ {
-					uuid, err := cmdForgejoCaptureOutput(t, cmd)
+					uuid, err := runMainApp("forgejo-cli", cmd...)
 					assert.NoError(t, err)
 					if assert.EqualValues(t, testCase.uuid, uuid) {
 						ownerName, repoName, found := strings.Cut(testCase.scope, "/")
diff --git a/tests/integration/cmd_forgejo_test.go b/tests/integration/cmd_forgejo_test.go
deleted file mode 100644
index 76f5a6fc0..000000000
--- a/tests/integration/cmd_forgejo_test.go
+++ /dev/null
@@ -1,36 +0,0 @@
-// SPDX-License-Identifier: MIT
-
-package integration
-
-import (
-	"bytes"
-	"context"
-	"strings"
-	"testing"
-
-	"code.gitea.io/gitea/cmd/forgejo"
-
-	"github.com/urfave/cli/v2"
-)
-
-func cmdForgejoCaptureOutput(t *testing.T, args []string, stdin ...string) (string, error) {
-	buf := new(bytes.Buffer)
-
-	app := cli.NewApp()
-	app.Writer = buf
-	app.ErrWriter = buf
-	ctx := context.Background()
-	ctx = forgejo.ContextSetNoInit(ctx, true)
-	ctx = forgejo.ContextSetNoExit(ctx, true)
-	ctx = forgejo.ContextSetStdout(ctx, buf)
-	ctx = forgejo.ContextSetStderr(ctx, buf)
-	if len(stdin) > 0 {
-		ctx = forgejo.ContextSetStdin(ctx, strings.NewReader(strings.Join(stdin, "")))
-	}
-	app.Commands = []*cli.Command{
-		forgejo.CmdForgejo(ctx),
-	}
-	err := app.Run(args)
-
-	return buf.String(), err
-}
diff --git a/tests/integration/cmd_keys_test.go b/tests/integration/cmd_keys_test.go
index 61f11c58b..a3220c13c 100644
--- a/tests/integration/cmd_keys_test.go
+++ b/tests/integration/cmd_keys_test.go
@@ -4,16 +4,15 @@
 package integration
 
 import (
-	"bytes"
+	"errors"
 	"net/url"
+	"os/exec"
 	"testing"
 
-	"code.gitea.io/gitea/cmd"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 
 	"github.com/stretchr/testify/assert"
-	"github.com/urfave/cli/v2"
 )
 
 func Test_CmdKeys(t *testing.T) {
@@ -24,30 +23,30 @@ func Test_CmdKeys(t *testing.T) {
 			wantErr        bool
 			expectedOutput string
 		}{
-			{"test_empty_1", []string{"keys", "--username=git", "--type=test", "--content=test"}, true, ""},
-			{"test_empty_2", []string{"keys", "-e", "git", "-u", "git", "-t", "test", "-k", "test"}, true, ""},
+			{"test_empty_1", []string{"--username=git", "--type=test", "--content=test"}, true, ""},
+			{"test_empty_2", []string{"-e", "git", "-u", "git", "-t", "test", "-k", "test"}, true, ""},
 			{
 				"with_key",
-				[]string{"keys", "-e", "git", "-u", "git", "-t", "ssh-rsa", "-k", "AAAAB3NzaC1yc2EAAAADAQABAAABgQDWVj0fQ5N8wNc0LVNA41wDLYJ89ZIbejrPfg/avyj3u/ZohAKsQclxG4Ju0VirduBFF9EOiuxoiFBRr3xRpqzpsZtnMPkWVWb+akZwBFAx8p+jKdy4QXR/SZqbVobrGwip2UjSrri1CtBxpJikojRIZfCnDaMOyd9Jp6KkujvniFzUWdLmCPxUE9zhTaPu0JsEP7MW0m6yx7ZUhHyfss+NtqmFTaDO+QlMR7L2QkDliN2Jl3Xa3PhuWnKJfWhdAq1Cw4oraKUOmIgXLkuiuxVQ6mD3AiFupkmfqdHq6h+uHHmyQqv3gU+/sD8GbGAhf6ftqhTsXjnv1Aj4R8NoDf9BS6KRkzkeun5UisSzgtfQzjOMEiJtmrep2ZQrMGahrXa+q4VKr0aKJfm+KlLfwm/JztfsBcqQWNcTURiCFqz+fgZw0Ey/de0eyMzldYTdXXNRYCKjs9bvBK+6SSXRM7AhftfQ0ZuoW5+gtinPrnmoOaSCEJbAiEiTO/BzOHgowiM="},
+				[]string{"-e", "git", "-u", "git", "-t", "ssh-rsa", "-k", "AAAAB3NzaC1yc2EAAAADAQABAAABgQDWVj0fQ5N8wNc0LVNA41wDLYJ89ZIbejrPfg/avyj3u/ZohAKsQclxG4Ju0VirduBFF9EOiuxoiFBRr3xRpqzpsZtnMPkWVWb+akZwBFAx8p+jKdy4QXR/SZqbVobrGwip2UjSrri1CtBxpJikojRIZfCnDaMOyd9Jp6KkujvniFzUWdLmCPxUE9zhTaPu0JsEP7MW0m6yx7ZUhHyfss+NtqmFTaDO+QlMR7L2QkDliN2Jl3Xa3PhuWnKJfWhdAq1Cw4oraKUOmIgXLkuiuxVQ6mD3AiFupkmfqdHq6h+uHHmyQqv3gU+/sD8GbGAhf6ftqhTsXjnv1Aj4R8NoDf9BS6KRkzkeun5UisSzgtfQzjOMEiJtmrep2ZQrMGahrXa+q4VKr0aKJfm+KlLfwm/JztfsBcqQWNcTURiCFqz+fgZw0Ey/de0eyMzldYTdXXNRYCKjs9bvBK+6SSXRM7AhftfQ0ZuoW5+gtinPrnmoOaSCEJbAiEiTO/BzOHgowiM="},
 				false,
 				"# gitea public key\ncommand=\"" + setting.AppPath + " --config=" + util.ShellEscape(setting.CustomConf) + " serv key-1\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDWVj0fQ5N8wNc0LVNA41wDLYJ89ZIbejrPfg/avyj3u/ZohAKsQclxG4Ju0VirduBFF9EOiuxoiFBRr3xRpqzpsZtnMPkWVWb+akZwBFAx8p+jKdy4QXR/SZqbVobrGwip2UjSrri1CtBxpJikojRIZfCnDaMOyd9Jp6KkujvniFzUWdLmCPxUE9zhTaPu0JsEP7MW0m6yx7ZUhHyfss+NtqmFTaDO+QlMR7L2QkDliN2Jl3Xa3PhuWnKJfWhdAq1Cw4oraKUOmIgXLkuiuxVQ6mD3AiFupkmfqdHq6h+uHHmyQqv3gU+/sD8GbGAhf6ftqhTsXjnv1Aj4R8NoDf9BS6KRkzkeun5UisSzgtfQzjOMEiJtmrep2ZQrMGahrXa+q4VKr0aKJfm+KlLfwm/JztfsBcqQWNcTURiCFqz+fgZw0Ey/de0eyMzldYTdXXNRYCKjs9bvBK+6SSXRM7AhftfQ0ZuoW5+gtinPrnmoOaSCEJbAiEiTO/BzOHgowiM= user2@localhost\n",
 			},
-			{"invalid", []string{"keys", "--not-a-flag=git"}, true, "Incorrect Usage: flag provided but not defined: -not-a-flag\n\n"},
+			{"invalid", []string{"--not-a-flag=git"}, true, "Incorrect Usage: flag provided but not defined: -not-a-flag\n\n"},
 		}
 		for _, tt := range tests {
 			t.Run(tt.name, func(t *testing.T) {
-				out := new(bytes.Buffer)
-				app := cli.NewApp()
-				app.Writer = out
-				app.Commands = []*cli.Command{cmd.CmdKeys}
-				cmd.CmdKeys.HideHelp = true
-				err := app.Run(append([]string{"prog"}, tt.args...))
+				out, err := runMainApp("keys", tt.args...)
+
+				var exitErr *exec.ExitError
+				if errors.As(err, &exitErr) {
+					t.Log(string(exitErr.Stderr))
+				}
 				if tt.wantErr {
 					assert.Error(t, err)
 				} else {
 					assert.NoError(t, err)
 				}
-				assert.Equal(t, tt.expectedOutput, out.String())
+				assert.Equal(t, tt.expectedOutput, out)
 			})
 		}
 	})
diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go
index e8f28105c..b087281ff 100644
--- a/tests/integration/integration_test.go
+++ b/tests/integration/integration_test.go
@@ -17,6 +17,7 @@ import (
 	"net/http/httptest"
 	"net/url"
 	"os"
+	"os/exec"
 	"path/filepath"
 	"strconv"
 	"strings"
@@ -24,6 +25,7 @@ import (
 	"testing"
 	"time"
 
+	"code.gitea.io/gitea/cmd"
 	"code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -93,7 +95,43 @@ func NewNilResponseHashSumRecorder() *NilResponseHashSumRecorder {
 	}
 }
 
+// runMainApp runs the subcommand and returns its standard output. Any returned error will usually be of type *ExitError. If c.Stderr was nil, Output populates ExitError.Stderr.
+func runMainApp(subcommand string, args ...string) (string, error) {
+	return runMainAppWithStdin(nil, subcommand, args...)
+}
+
+// runMainAppWithStdin runs the subcommand and returns its standard output. Any returned error will usually be of type *ExitError. If c.Stderr was nil, Output populates ExitError.Stderr.
+func runMainAppWithStdin(stdin io.Reader, subcommand string, args ...string) (string, error) {
+	// running the main app directly will very likely mess with the testing setup (logger & co.)
+	// hence we run it as a subprocess and capture its output
+	args = append([]string{subcommand}, args...)
+	cmd := exec.Command(os.Args[0], args...)
+	cmd.Env = append(os.Environ(),
+		"GITEA_TEST_CLI=true",
+		"GITEA_CONF="+setting.CustomConf,
+		"GITEA_WORK_DIR="+setting.AppWorkPath)
+	cmd.Stdin = stdin
+	out, err := cmd.Output()
+	return string(out), err
+}
+
 func TestMain(m *testing.M) {
+	// GITEA_TEST_CLI is set by runMainAppWithStdin
+	// inspired by https://abhinavg.net/2022/05/15/hijack-testmain/
+	if testCLI := os.Getenv("GITEA_TEST_CLI"); testCLI == "true" {
+		app := cmd.NewMainApp("test-version", "integration-test")
+		args := append([]string{
+			"executable-name", // unused, but expected at position 1
+			"--config", os.Getenv("GITEA_CONF"),
+		},
+			os.Args[1:]..., // skip the executable name
+		)
+		if err := cmd.RunMainApp(app, args...); err != nil {
+			panic(err) // should never happen since RunMainApp exits on error
+		}
+		return
+	}
+
 	defer log.GetManager().Close()
 
 	managerCtx, cancel := context.WithCancel(context.Background())
diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go
index c9ecadca9..e613538d0 100644
--- a/tests/integration/pull_merge_test.go
+++ b/tests/integration/pull_merge_test.go
@@ -26,11 +26,14 @@ import (
 	"code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
+	"code.gitea.io/gitea/modules/hostmatcher"
+	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/modules/translation"
 	"code.gitea.io/gitea/services/pull"
 	files_service "code.gitea.io/gitea/services/repository/files"
+	webhook_service "code.gitea.io/gitea/services/webhook"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -83,7 +86,19 @@ func testPullCleanUp(t *testing.T, session *TestSession, user, repo, pullnum str
 func retrieveHookTasks(t *testing.T, hookID int64, activateWebhook bool) []*webhook.HookTask {
 	t.Helper()
 	if activateWebhook {
-		updated, err := db.GetEngine(db.DefaultContext).ID(hookID).Cols("is_active").Update(webhook.Webhook{IsActive: true})
+		s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			w.WriteHeader(http.StatusNoContent)
+		}))
+		t.Cleanup(s.Close)
+		updated, err := db.GetEngine(db.DefaultContext).ID(hookID).Cols("is_active", "url").Update(webhook.Webhook{
+			IsActive: true,
+			URL:      s.URL,
+		})
+
+		// allow webhook deliveries on localhost
+		t.Cleanup(test.MockVariableValue(&setting.Webhook.AllowedHostList, hostmatcher.MatchBuiltinLoopback))
+		webhook_service.Init()
+
 		assert.Equal(t, int64(1), updated)
 		assert.NoError(t, err)
 	}