From 5f136783d114b302eb2a42219e403624ef2abd93 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 13 Jun 2022 00:10:09 +0000
Subject: [PATCH 1/7] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_bg-BG.ini |  4 ++++
 options/locale/locale_cs-CZ.ini |  4 ++++
 options/locale/locale_de-DE.ini |  4 ++++
 options/locale/locale_el-GR.ini | 20 ++++----------------
 options/locale/locale_es-ES.ini |  4 ++++
 options/locale/locale_fa-IR.ini |  4 ++++
 options/locale/locale_fi-FI.ini |  4 ++++
 options/locale/locale_fr-FR.ini |  4 ++++
 options/locale/locale_hu-HU.ini |  4 ++++
 options/locale/locale_id-ID.ini |  4 ++++
 options/locale/locale_is-IS.ini |  4 ++++
 options/locale/locale_it-IT.ini |  4 ++++
 options/locale/locale_ja-JP.ini | 20 ++++----------------
 options/locale/locale_ko-KR.ini |  4 ++++
 options/locale/locale_lv-LV.ini |  4 ++++
 options/locale/locale_ml-IN.ini |  4 ++++
 options/locale/locale_nl-NL.ini |  4 ++++
 options/locale/locale_pl-PL.ini |  4 ++++
 options/locale/locale_pt-BR.ini |  9 ++++-----
 options/locale/locale_pt-PT.ini | 29 +++++++++++++----------------
 options/locale/locale_ru-RU.ini |  4 ++++
 options/locale/locale_si-LK.ini |  4 ++++
 options/locale/locale_sv-SE.ini |  4 ++++
 options/locale/locale_tr-TR.ini |  4 ++++
 options/locale/locale_uk-UA.ini |  4 ++++
 options/locale/locale_zh-CN.ini | 29 +++++++++++++----------------
 options/locale/locale_zh-HK.ini |  4 ++++
 options/locale/locale_zh-TW.ini | 18 +++++++++++++++++-
 28 files changed, 143 insertions(+), 70 deletions(-)

diff --git a/options/locale/locale_bg-BG.ini b/options/locale/locale_bg-BG.ini
index ea4e2a105d..ee19a6ad23 100644
--- a/options/locale/locale_bg-BG.ini
+++ b/options/locale/locale_bg-BG.ini
@@ -768,9 +768,13 @@ pulls.num_conflicting_files_n=%d конфликтни файлове
 pulls.no_merge_desc=Тази заявка за сливане не може да бъде обединена, защото всички опции за обединяване на хранилището са изключени.
 pulls.no_merge_helper=Включете опции за сливане в настройките на хранилището или обединете заявката за сливане ръчно.
 pulls.no_merge_wip=Тази заявка за сливане не може да бъде обединена, защото е отбелязана като работа в прогрес.
+
 ; </summary><code>%[2]s<br>%[3]s</code></details>
 pulls.status_checks_success=Всички проверявания бяха успешни
 
+
+
+
 milestones.new=Нов етап
 milestones.open_tab=%d отворени
 milestones.close_tab=%d затворени
diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini
index 896b4ee970..6ad2bd679b 100644
--- a/options/locale/locale_cs-CZ.ini
+++ b/options/locale/locale_cs-CZ.ini
@@ -1500,6 +1500,7 @@ pulls.squash_merge_pull_request=Vytvořit squash commit
 pulls.merge_manually=Sloučeno ručně
 pulls.merge_commit_id=ID slučovacího commitu
 pulls.require_signed_wont_sign=Větev vyžaduje podepsané commity, ale toto sloučení nebude podepsáno
+
 pulls.invalid_merge_option=Nemůžete použít tuto možnost sloučení pro tento požadavek na natažení.
 pulls.merge_conflict=Sloučení selhalo: Došlo ke konfliktu při sloučení. Tip: Zkuste jinou strategii
 pulls.merge_conflict_summary=Chybové hlášení
@@ -1528,6 +1529,9 @@ pulls.merge_instruction_hint=`Můžete také zobrazit <a class="show-instruction
 pulls.merge_instruction_step1_desc=Z vašeho repositáře projektu se podívejte na novou větev a vyzkoušejte změny.
 pulls.merge_instruction_step2_desc=Slučte změny a aktualizujte je na Gitea.
 
+
+
+
 milestones.new=Nový milník
 milestones.open_tab=%d otevřených
 milestones.close_tab=%d zavřených
diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini
index c6cd975238..4e9a33674a 100644
--- a/options/locale/locale_de-DE.ini
+++ b/options/locale/locale_de-DE.ini
@@ -1526,6 +1526,7 @@ pulls.squash_merge_pull_request=Squash Commit erstellen
 pulls.merge_manually=Manuell mergen
 pulls.merge_commit_id=Der Mergecommit ID
 pulls.require_signed_wont_sign=Der Branch erfordert einen signierten Commit, aber dieser Merge wird nicht signiert
+
 pulls.invalid_merge_option=Du kannst diese Mergeoption auf diesen Pull-Request nicht anwenden.
 pulls.merge_conflict=Zusammenführen fehlgeschlagen: Beim Zusammenführen gab es einen Konflikt. Hinweis: Probiere eine andere Strategie
 pulls.merge_conflict_summary=Fehlermeldung
@@ -1557,6 +1558,9 @@ pulls.merge_instruction_hint=`Siehe auch die <a class="show-instruction">Anleitu
 pulls.merge_instruction_step1_desc=Wechsle auf einen neuen Branch in deinem lokalen Repository und teste die Änderungen.
 pulls.merge_instruction_step2_desc=Führe die Änderungen zusammen und aktualisiere den Stand online auf Gitea.
 
+
+
+
 milestones.new=Neuer Meilenstein
 milestones.open_tab=%d offen
 milestones.close_tab=%d geschlossen
diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini
index 11d8855f96..99b90edc4b 100644
--- a/options/locale/locale_el-GR.ini
+++ b/options/locale/locale_el-GR.ini
@@ -1568,14 +1568,7 @@ pulls.squash_merge_pull_request=Δημιουργία υποβολής squash
 pulls.merge_manually=Συγχωνεύτηκαν χειροκίνητα
 pulls.merge_commit_id=Το ID της υποβολής συγχώνευσης
 pulls.require_signed_wont_sign=Ο κλάδος απαιτεί υπογεγραμμένες υποβολές αλλά αυτή η συγχώνευση δεν θα υπογραφεί
-pulls.merge_pull_request_now=Συγχώνευση του Pull Request Τώρα
-pulls.rebase_merge_pull_request_now=Αλλαγή Βάσης και Συγχώνευση Τώρα
-pulls.rebase_merge_commit_pull_request_now=Αλλαγή Βάσης και Συγχώνευση Τώρα (--no-ff)
-pulls.squash_merge_pull_request_now=Στρίμωγμα και Συγχώνευση τώρα
-pulls.merge_pull_request_on_status_success=Συγχώνευση Του Pull Request Όταν Όλοι Οι Έλεγχοι Επιτύχουν
-pulls.rebase_merge_pull_request_on_status_success=Αλλαγή Βάσης και Συγχώνευση Όταν Όλοι Οι Έλεγχοι Πετύχουν
-pulls.rebase_merge_commit_pull_request_on_status_success=Αλλαγή Βάσης και Συγχώνευση (--no-ff) Όταν Όλοι Οι Έλεγχοι Πετύχουν
-pulls.squash_merge_pull_request_on_status_success=Στρίμωγμα και Συγχώνευση Όταν Όλοι Οι Έλεγχοι Επιτύχουν
+
 pulls.invalid_merge_option=Δεν μπορείτε να χρησιμοποιήσετε αυτήν την επιλογή συγχώνευσης για αυτό το pull request.
 pulls.merge_conflict=Η Συγχώνευση Απέτυχε: Υπήρξε μια διένεξη κατά τη συγχώνευση. Υπόδειξη: Δοκιμάστε μια διαφορετική στρατηγική
 pulls.merge_conflict_summary=Μήνυμα Σφάλματος
@@ -1606,14 +1599,9 @@ pulls.reopened_at=`άνοιξε ξανά αυτό το pull request <a id="%[1]s
 pulls.merge_instruction_hint=`Μπορείτε επίσης να δείτε <a class="show-instruction">τις οδηγίες της γραμμής εντολών</a>.`
 pulls.merge_instruction_step1_desc=Από το αποθετήριο του έργου σας, ελέγξτε έναν νέο κλάδο και τεστάρετε τις αλλαγές.
 pulls.merge_instruction_step2_desc=Συγχώνευσε τις αλλαγές και ενημέρωσε στο Gitea.
-pulls.merge_on_status_success=Το pull request προγραμματίστηκε για συγχώνευση όταν πετύχουν όλοι οι έλεγχοι.
-pulls.merge_on_status_success_already_scheduled=Αυτό το αίτημα έλξης έχει ήδη προγραμματιστεί για συγχώνευση όταν πετύχουν όλοι οι έλεγχοι.
-pulls.pr_has_pending_merge_on_success=%[1]s έχει προγραμματίσει αυτό το pull request για αυτόματη συγχώνευση όταν όλοι οι έλεγχοι επιτύχουν %[2]s.
-pulls.merge_pull_on_success_cancel=Ακύρωση αυτόματης συγχώνευσης
-pulls.pull_request_not_scheduled=Αυτό το pull request δεν είναι προγραμματισμένο να συγχωνευτεί αυτόματα.
-pulls.pull_request_schedule_canceled=Η αυτόματη συγχώνευση ακυρώθηκε για αυτό το pull request.
-pulls.pull_request_scheduled_auto_merge=`έχει προγραμματισεί αυτό το pull request για αυτόματη συγχώνευση όταν όλοι οι έλεγχοι πετύχουν %[1]s`
-pulls.pull_request_canceled_scheduled_auto_merge=`ακύρωσε την αυτόματη συγχώνευση αυτού του pull request όταν όλοι οι έλεγχοι πετύχουν %[1]s`
+
+
+
 
 milestones.new=Νέο Ορόσημο
 milestones.open_tab=%d Ανοιχτά
diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini
index a35cffe495..175895e61f 100644
--- a/options/locale/locale_es-ES.ini
+++ b/options/locale/locale_es-ES.ini
@@ -1523,6 +1523,7 @@ pulls.squash_merge_pull_request=Crear commit squash
 pulls.merge_manually=Fusionado manualmente
 pulls.merge_commit_id=La identificación del commit fusionado
 pulls.require_signed_wont_sign=Esta rama requiere commits firmados pero esta fusión no será firmada
+
 pulls.invalid_merge_option=No puede utilizar esta opción de combinación para esta solicitud de extracción.
 pulls.merge_conflict=Fusión fallida: Hubo un conflicto mientras se fusionaba. Pista: Pruebe una estrategia diferente
 pulls.merge_conflict_summary=Mensaje de error
@@ -1554,6 +1555,9 @@ pulls.merge_instruction_hint=`También puede ver <a class="show-instruction">ins
 pulls.merge_instruction_step1_desc=Desde el repositorio de su proyecto, revisa una nueva rama y prueba los cambios.
 pulls.merge_instruction_step2_desc=Combine los cambios y actualice en Gitea.
 
+
+
+
 milestones.new=Nuevo hito
 milestones.open_tab=%d abiertas
 milestones.close_tab=%d cerradas
diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini
index 9cf067a010..0da69185f1 100644
--- a/options/locale/locale_fa-IR.ini
+++ b/options/locale/locale_fa-IR.ini
@@ -1449,6 +1449,7 @@ pulls.squash_merge_pull_request=ایجاد commit اسکواش
 pulls.merge_manually=بصورت دستی ادغام شد
 pulls.merge_commit_id=شماره کامیت ادغام
 pulls.require_signed_wont_sign=انشعاب نیازمند تغییرات امضا شده است اما این ادغام امضا نخواهد شد
+
 pulls.invalid_merge_option=شما نمی‌توانید از این گزینه برای تقاضای واکشی استفاده کنید.
 pulls.merge_conflict=ادغام ناموفق بود: در حین ادغام مغایرت وجود داشت. نکته: استراتژی متفاوتی را امتحان کنید
 pulls.merge_conflict_summary=پیغام خطا
@@ -1477,6 +1478,9 @@ pulls.merge_instruction_hint=`همچنین می‌توانید <a class="show-in
 pulls.merge_instruction_step1_desc=از انبار پروژه خود، یک شاخه جدید را بگیرید و تغییرات را آزمایش کنید.
 pulls.merge_instruction_step2_desc=تغییرات را ادغام کنید و در Gitea به روز کنید.
 
+
+
+
 milestones.new=نقطه عطف جدید
 milestones.open_tab=%d باز شد
 milestones.close_tab=%d بسته
diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini
index bad4b505f7..63422ad518 100644
--- a/options/locale/locale_fi-FI.ini
+++ b/options/locale/locale_fi-FI.ini
@@ -754,8 +754,12 @@ pulls.merged=Yhdistetty
 pulls.has_merged=Vetopyyntö on yhdistetty.
 pulls.can_auto_merge_desc=Tämä pull-pyyntö voidaan yhdistää automaattisesti.
 
+
 ; </summary><code>%[2]s<br>%[3]s</code></details>
 
+
+
+
 milestones.new=Uusi merkkipaalu
 milestones.open_tab=%d avoinna
 milestones.close_tab=%d suljettu
diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index 667205e06b..3b95c10451 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -1364,6 +1364,7 @@ pulls.no_merge_access=Vous n'êtes pas autorisé⋅e à fusionner cette demande
 pulls.merge_manually=Fusionné manuellement
 pulls.merge_commit_id=L'ID de la révision de fusion
 pulls.require_signed_wont_sign=La branche nécessite des révisions signées mais cette fusion ne sera pas signée
+
 pulls.invalid_merge_option=Vous ne pouvez pas utiliser cette option de fusion pour cette demande.
 pulls.merge_conflict=Échec de la fusion : il y a eu un conflit lors de la fusion. Indice : Essayez une autre stratégie
 pulls.merge_conflict_summary=Message d'erreur
@@ -1390,6 +1391,9 @@ pulls.merge_instruction_hint=`Vous pouvez également voir <a class="show-instruc
 pulls.merge_instruction_step1_desc=Depuis le dépôt de votre projet, sélectionnez une nouvelle branche et testez les modifications.
 pulls.merge_instruction_step2_desc=Fusionner les modifications et mettre à jour sur Gitea.
 
+
+
+
 milestones.new=Nouveau jalon
 milestones.open_tab=%d Ouvert
 milestones.close_tab=%d Fermé
diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini
index 3c58383cc9..70d4e83fd8 100644
--- a/options/locale/locale_hu-HU.ini
+++ b/options/locale/locale_hu-HU.ini
@@ -971,10 +971,14 @@ pulls.cant_reopen_deleted_branch=Ez a pull request nem nyitható meg újra, mive
 pulls.merged=Egyesítve
 pulls.can_auto_merge_desc=Ez az egyesítési kérés automatikusan végrehajtható.
 
+
 ; </summary><code>%[2]s<br>%[3]s</code></details>
 pulls.status_checking=Néhány ellenőrzés függőben van
 pulls.status_checks_success=Minden ellenőrzés sikeres volt
 
+
+
+
 milestones.new=Új mérföldkő
 milestones.open_tab=%d Nyitott
 milestones.close_tab=%d Zárt
diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini
index 41a9e84ad7..18f3e20e6f 100644
--- a/options/locale/locale_id-ID.ini
+++ b/options/locale/locale_id-ID.ini
@@ -838,8 +838,12 @@ pulls.reopen_to_merge=Tolong buka kembali permintaan tarik ini untuk melaksanaka
 pulls.merged=Menggabungkan
 pulls.can_auto_merge_desc=Permintaan tarik ini dapat digabung secara otomatis.
 
+
 ; </summary><code>%[2]s<br>%[3]s</code></details>
 
+
+
+
 milestones.new=Milestone Baru
 milestones.open_tab=%d Terbuka
 milestones.close_tab=%d Tertutup
diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini
index b28196e80c..dfc4827603 100644
--- a/options/locale/locale_is-IS.ini
+++ b/options/locale/locale_is-IS.ini
@@ -925,10 +925,14 @@ pulls.waiting_count_1=%d bíður endurskoðunar
 pulls.waiting_count_n=%d bíða endurskoðunar
 
 pulls.merge_manually=Sameinað handvirkt
+
 ; </summary><code>%[2]s<br>%[3]s</code></details>
 pulls.status_checks_requested=Nauðsynlegt
 pulls.status_checks_details=Nánar
 
+
+
+
 milestones.new=Nýtt tímamót
 milestones.open_tab=%d Opin
 milestones.close_tab=%d Lokuð
diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini
index a6bd6ad1bb..a8f4b534df 100644
--- a/options/locale/locale_it-IT.ini
+++ b/options/locale/locale_it-IT.ini
@@ -1230,6 +1230,7 @@ pulls.no_merge_wip=Questa pull request non può essere unita perché è contrass
 pulls.no_merge_not_ready=Questa pull request non è pronta per il merge, controlla lo stato della revisione e i controlli di stato.
 pulls.no_merge_access=Non sei autorizzato ad effettuare il merge su questa pull request.
 pulls.require_signed_wont_sign=Il branch richiede commit firmati ma questo merge non verrà firmato
+
 pulls.invalid_merge_option=Non puoi utilizzare questa opzione di merge per questa pull request.
 pulls.merge_conflict_summary=Messaggio d'errore
 ; </summary><code>%[2]s<br>%[3]s</code></details>
@@ -1247,6 +1248,9 @@ pulls.update_branch_success=Brench aggiornato con successo
 pulls.update_not_allowed=Non sei abilitato ad aggiornare il branch
 pulls.outdated_with_base_branch=Questo brench non è aggiornato con il branch di base
 
+
+
+
 milestones.new=Nuova Milestone
 milestones.open_tab=%d Aperti
 milestones.close_tab=%d Chiusi
diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini
index 887f675810..3ac3554888 100644
--- a/options/locale/locale_ja-JP.ini
+++ b/options/locale/locale_ja-JP.ini
@@ -1568,14 +1568,7 @@ pulls.squash_merge_pull_request=スカッシュコミットを作成
 pulls.merge_manually=手動マージ済みにする
 pulls.merge_commit_id=マージコミットID
 pulls.require_signed_wont_sign=ブランチでは署名されたコミットが必須ですが、このマージでは署名がされません
-pulls.merge_pull_request_now=今すぐプルリクエストをマージ
-pulls.rebase_merge_pull_request_now=今すぐリベースしてマージ
-pulls.rebase_merge_commit_pull_request_now=今すぐリベースしてマージ(--no-ff)
-pulls.squash_merge_pull_request_now=今すぐスカッシュしてマージ
-pulls.merge_pull_request_on_status_success=すべてのチェックが成功したときにプルリクエストをマージ
-pulls.rebase_merge_pull_request_on_status_success=すべてのチェックが成功したときにリベースしてマージ
-pulls.rebase_merge_commit_pull_request_on_status_success=すべてのチェックが成功したときにリベースしてマージ(--no-ff)
-pulls.squash_merge_pull_request_on_status_success=すべてのチェックが成功したときにスカッシュしてマージ
+
 pulls.invalid_merge_option=このプルリクエストでは、指定したマージ方法は使えません。
 pulls.merge_conflict=マージ失敗: マージ中にコンフリクトがありました。 ヒント: 別のストラテジーを試してみてください
 pulls.merge_conflict_summary=エラーメッセージ
@@ -1606,14 +1599,9 @@ pulls.reopened_at=`がプルリクエストを再オープン <a id="%[1]s" href
 pulls.merge_instruction_hint=`<a class="show-instruction">コマンドラインの手順</a>も確認できます。`
 pulls.merge_instruction_step1_desc=あなたのプロジェクトリポジトリで新しいブランチをチェックアウトし、変更内容をテストします。
 pulls.merge_instruction_step2_desc=変更内容をマージして、Giteaに反映します。
-pulls.merge_on_status_success=このプルリクエストは、すべてのチェックが成功したときにマージされるようにスケジュールされました。
-pulls.merge_on_status_success_already_scheduled=このプルリクエストは、すべてのチェックが成功したときにマージされるようにスケジュールされています。
-pulls.pr_has_pending_merge_on_success=%[1]s が、すべてのチェックが成功すると自動マージを行うよう、このプルリクエストをスケジュール %[2]s。
-pulls.merge_pull_on_success_cancel=自動マージをキャンセル
-pulls.pull_request_not_scheduled=このプルリクエストは自動マージとしてスケジュールされていません。
-pulls.pull_request_schedule_canceled=このプルリクエストの自動マージはキャンセルされました。
-pulls.pull_request_scheduled_auto_merge=`が、すべてのチェックが成功すると自動マージを行うよう、このプルリクエストをスケジュール %[1]s`
-pulls.pull_request_canceled_scheduled_auto_merge=`が、すべてのチェックが成功したときのプルリクエストの自動マージをキャンセル %[1]s`
+
+
+
 
 milestones.new=新しいマイルストーン
 milestones.open_tab=%d件 オープン中
diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini
index ed48ada226..80b598c5be 100644
--- a/options/locale/locale_ko-KR.ini
+++ b/options/locale/locale_ko-KR.ini
@@ -859,9 +859,13 @@ pulls.can_auto_merge_desc=이 풀리퀘스트는 자동적으로 머지될 수 
 pulls.cannot_auto_merge_helper=충돌을 해결하려면 수동으로 머지하십시오.
 
 pulls.no_merge_desc=모든 저장소 머지 옵션이 비활성화 되어있기 때문에 이 풀 리퀘스트를 머지할 수 없습니다.
+
 pulls.invalid_merge_option=이 풀 리퀘스트에서 설정한 머지 옵션을 사용하실 수 없습니다.
 ; </summary><code>%[2]s<br>%[3]s</code></details>
 
+
+
+
 milestones.new=새로운 마일스톤
 milestones.open_tab=%d개 열림
 milestones.close_tab=%d개 닫힘
diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini
index 13aea77c79..ccc6bd280f 100644
--- a/options/locale/locale_lv-LV.ini
+++ b/options/locale/locale_lv-LV.ini
@@ -1511,6 +1511,7 @@ pulls.squash_merge_pull_request=Izveidot saspiešanas revīziju
 pulls.merge_manually=Manuāli sapludināts
 pulls.merge_commit_id=Sapludināšanas revīzijas ID
 pulls.require_signed_wont_sign=Atzarā var iesūtīt tikai parakstītas revīzijas, bet sapludināšanas revīzijas netiks parakstīta
+
 pulls.invalid_merge_option=Nav iespējams izmantot šādu sapludināšanas veidu šim izmaiņu pieprasījumam.
 pulls.merge_conflict=Sapludināšana neizdevās: Veicot sapludināšanu, radās konflikts. Mēģiniet izmantot citu sapludināšanas stratēģiju
 pulls.merge_conflict_summary=Kļūdas paziņojums
@@ -1542,6 +1543,9 @@ pulls.merge_instruction_hint=`Varat aplūkot arī <a class="show-instruction">ko
 pulls.merge_instruction_step1_desc=Projekta repozitorijā izveidojiet jaunu jaunu atzaru un pārbaudiet savas izmaiņas.
 pulls.merge_instruction_step2_desc=Sapludināt izmaiņas un atjaunot tās Gitea.
 
+
+
+
 milestones.new=Jauns atskaites punkts
 milestones.open_tab=%d atvērti
 milestones.close_tab=%d aizvērti
diff --git a/options/locale/locale_ml-IN.ini b/options/locale/locale_ml-IN.ini
index 285c7e8e4b..be06680441 100644
--- a/options/locale/locale_ml-IN.ini
+++ b/options/locale/locale_ml-IN.ini
@@ -723,8 +723,12 @@ issues.dependency.add_error_dep_not_same_repo=രണ്ട് പ്രശ്ന
 
 
 
+
 ; </summary><code>%[2]s<br>%[3]s</code></details>
 
+
+
+
 milestones.filter_sort.most_issues=മിക്ക ഇഷ്യൂകളും
 milestones.filter_sort.least_issues=കുറഞ്ഞ ഇഷ്യൂകളെങ്കിലും
 
diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini
index 66c3fee81b..a18f6f6981 100644
--- a/options/locale/locale_nl-NL.ini
+++ b/options/locale/locale_nl-NL.ini
@@ -1253,6 +1253,7 @@ pulls.no_merge_wip=Deze pull-aanvraag kan niet worden samengevoegd omdat hij als
 pulls.no_merge_not_ready=Deze pull-aanvraag is niet klaar om samen te voegen, controleer de status en status controles.
 pulls.no_merge_access=Je bent niet gemachtigd om deze pull-aanvraag samen te voegen.
 pulls.require_signed_wont_sign=De branch heeft ondertekende commits nodig, maar deze merge zal niet worden ondertekend
+
 pulls.invalid_merge_option=Je kan de samenvoegingsoptie niet gebruiken voor deze pull-aanvraag.
 pulls.merge_conflict=Samenvoegen mislukt: Er was een conflict tijdens het samenvoegen. Hint: Probeer een andere strategie
 pulls.merge_conflict_summary=Foutmelding
@@ -1276,6 +1277,9 @@ pulls.outdated_with_base_branch=Deze branch is verouderd met de basis branch
 pulls.closed_at=`heeft deze pull request gesloten <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 pulls.reopened_at=`heropende deze pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 
+
+
+
 milestones.new=Nieuwe mijlpaal
 milestones.open_tab=%d geopend
 milestones.close_tab=%d gesloten
diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini
index 2bbf821664..06a306fe30 100644
--- a/options/locale/locale_pl-PL.ini
+++ b/options/locale/locale_pl-PL.ini
@@ -1421,6 +1421,7 @@ pulls.no_merge_not_ready=Ten Pull Request nie jest gotowy do scalenia, sprawdź
 pulls.no_merge_access=Nie masz uprawnień, aby scalić ten Pull Request.
 pulls.merge_manually=Ręcznie scalone
 pulls.require_signed_wont_sign=Ta gałąź wymaga podpisanych commitów, ale to scalenie nie będzie podpisane
+
 pulls.invalid_merge_option=Nie możesz użyć tej opcji scalania dla tego pull request'a.
 pulls.merge_conflict=Scalenie nie powiodło się: Wystąpił konflikt przy scalaniu. Porada: Wypróbuj innej strategii
 pulls.merge_conflict_summary=Komunikat o błędzie
@@ -1449,6 +1450,9 @@ pulls.merge_instruction_hint=`Możesz także zobaczyć <a class="show-instructio
 pulls.merge_instruction_step1_desc=Z repozytorium twojego projektu, sprawdź nową gałąź i przetestuj zmiany.
 pulls.merge_instruction_step2_desc=Połącz zmiany i zaktualizuj na Gitea.
 
+
+
+
 milestones.new=Nowy kamień milowy
 milestones.open_tab=Otwarte %d
 milestones.close_tab=Zamknięte %d
diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini
index 658d1f4355..975393dc8d 100644
--- a/options/locale/locale_pt-BR.ini
+++ b/options/locale/locale_pt-BR.ini
@@ -1561,10 +1561,7 @@ pulls.squash_merge_pull_request=Criar commit de squash
 pulls.merge_manually=Merge feito manualmente
 pulls.merge_commit_id=A ID de merge commit
 pulls.require_signed_wont_sign=O branch requer commits assinados, mas este merge não será assinado
-pulls.merge_pull_request_now=Aplicar Pull Request Agora
-pulls.rebase_merge_pull_request_now=Aplicar Rebase e Merge Agora
-pulls.rebase_merge_commit_pull_request_now=Aplicar Rebase e Merge Agora (--no-ff)
-pulls.squash_merge_pull_request_now=Aplicar Squash e Merge Agora
+
 pulls.invalid_merge_option=Você não pode usar esta opção de merge neste pull request.
 pulls.merge_conflict=O merge falhou: Houve um conflito ao fazer merge. Dica: Tente uma estratégia diferente
 pulls.merge_conflict_summary=Mensagem de erro
@@ -1595,7 +1592,9 @@ pulls.reopened_at=`reabriu este pull request <a id="%[1]s" href="#%[1]s">%[2]s</
 pulls.merge_instruction_hint=`Você também pode ver as <a class="show-instruction">instruções para a linha de comandos</a>.`
 pulls.merge_instruction_step1_desc=No repositório do seu projeto, crie um novo branch e teste as alterações.
 pulls.merge_instruction_step2_desc=Faça merge das alterações e atualize no Gitea.
-pulls.merge_pull_on_success_cancel=Cancelar merge automático
+
+
+
 
 milestones.new=Novo marco
 milestones.open_tab=%d Aberto
diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini
index 14a8d11dc3..611b71bce7 100644
--- a/options/locale/locale_pt-PT.ini
+++ b/options/locale/locale_pt-PT.ini
@@ -1568,14 +1568,7 @@ pulls.squash_merge_pull_request=Criar cometimento de compactação
 pulls.merge_manually=Integrado manualmente
 pulls.merge_commit_id=O ID de cometimento da integração
 pulls.require_signed_wont_sign=O ramo requer que os cometimentos sejam assinados mas esta integração não vai ser assinada
-pulls.merge_pull_request_now=Executar agora a integração constante no pedido
-pulls.rebase_merge_pull_request_now=Mudar a base e integrar agora
-pulls.rebase_merge_commit_pull_request_now=Mudar a base e integrar agora (--no-ff)
-pulls.squash_merge_pull_request_now=Compactar e integrar agora
-pulls.merge_pull_request_on_status_success=Executar a integração constante no pedido quando todas as verificações forem bem sucedidas
-pulls.rebase_merge_pull_request_on_status_success=Mudar a base e integrar quando todas as verificações forem bem sucedidas
-pulls.rebase_merge_commit_pull_request_on_status_success=Mudar a base e integrar (--no-ff) quando todas as verificações forem bem sucedidas
-pulls.squash_merge_pull_request_on_status_success=Compactar e integrar quando todas as verificações forem bem sucedidas
+
 pulls.invalid_merge_option=Não pode usar esta opção de integração neste pedido de integração.
 pulls.merge_conflict=A integração falhou: Houve um conflito durante a integração. Dica: tente uma estratégia diferente
 pulls.merge_conflict_summary=Mensagem de erro
@@ -1606,14 +1599,18 @@ pulls.reopened_at=`reabriu este pedido de integração <a id="%[1]s" href="#%[1]
 pulls.merge_instruction_hint=`Também pode ver as <a class="show-instruction">instruções para a linha de comandos</a>.`
 pulls.merge_instruction_step1_desc=No seu repositório, crie um novo ramo e teste as modificações.
 pulls.merge_instruction_step2_desc=Integre as modificações e envie para o Gitea.
-pulls.merge_on_status_success=O pedido de integração foi agendado para ser executado quando todas as verificações forem bem sucedidas.
-pulls.merge_on_status_success_already_scheduled=Este pedido de integração já está agendado para ser executado quando todas as verificações forem bem sucedidas.
-pulls.pr_has_pending_merge_on_success=%[1]s agendou este pedido de integração para ser executado automaticamente quando todas as verificações forem bem sucedidas %[2]s.
-pulls.merge_pull_on_success_cancel=Cancelar a integração automática
-pulls.pull_request_not_scheduled=Este pedido de integração não está agendado para ser executado automaticamente.
-pulls.pull_request_schedule_canceled=A integração automática foi cancelada para este pedido de integração.
-pulls.pull_request_scheduled_auto_merge=`agendou este pedido de integração para ser executado automaticamente quando todas as verificações forem bem sucedidas %[1]s`
-pulls.pull_request_canceled_scheduled_auto_merge=`cancelou a execução automática deste pedido de integração que iria ser executando quando todas as verificações fossem bem sucedidas %[1]s`
+
+pulls.auto_merge_button_when_succeed=(quando as verificações forem bem-sucedidas)
+pulls.auto_merge_when_succeed=Integrar automaticamente quando todas as verificações forem bem-sucedidas
+pulls.auto_merge_newly_scheduled=O pedido de integração foi agendado para ser executado quando todas as verificações forem bem-sucedidas.
+pulls.auto_merge_has_pending_schedule=%[1]s agendou este pedido de integração para ser executado automaticamente quando todas as verificações forem bem-sucedidas %[2]s.
+
+pulls.auto_merge_cancel_schedule=Cancelar a integração automática
+pulls.auto_merge_not_scheduled=Este pedido de integração não está agendado para ser executado automaticamente.
+pulls.auto_merge_canceled_schedule=A integração automática foi cancelada para este pedido de integração.
+
+pulls.auto_merge_newly_scheduled_comment=`agendou este pedido de integração para ser executado automaticamente quando todas as verificações forem bem-sucedidas %[1]s`
+pulls.auto_merge_canceled_schedule_comment=`cancelou a execução automática deste pedido de integração que iria ocorrer quando todas as verificações fossem bem-sucedidas %[1]s`
 
 milestones.new=Nova etapa
 milestones.open_tab=%d abertas
diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini
index 377ef1cb31..a3c1c09acb 100644
--- a/options/locale/locale_ru-RU.ini
+++ b/options/locale/locale_ru-RU.ini
@@ -1519,6 +1519,7 @@ pulls.squash_merge_pull_request=Создать объединенный (squash)
 pulls.merge_manually=Слито вручную
 pulls.merge_commit_id=ID коммита слияния
 pulls.require_signed_wont_sign=Данная ветка ожидает подписанные коммиты, однако слияние не будет подписано
+
 pulls.invalid_merge_option=Этот параметр слияния нельзя использовать для этого запроса на слияние.
 pulls.merge_conflict=Слияние не удалось: Произошел конфликт во время слияния. Совет: попробуйте другую стратегию
 pulls.merge_conflict_summary=Сообщение об ошибке
@@ -1549,6 +1550,9 @@ pulls.merge_instruction_hint=`Вы также можете просмотрет
 pulls.merge_instruction_step1_desc=В репозитории вашего проекта посмотрите новую ветку и протестируйте изменения.
 pulls.merge_instruction_step2_desc=Объединить изменения и обновить на Gitea.
 
+
+
+
 milestones.new=Новый этап
 milestones.open_tab=%d открыто(ы)
 milestones.close_tab=%d закрыто(ы)
diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini
index 397a851821..b6c9bc04af 100644
--- a/options/locale/locale_si-LK.ini
+++ b/options/locale/locale_si-LK.ini
@@ -1393,6 +1393,7 @@ pulls.squash_merge_pull_request=ස්කොෂ් කැපීමට සාද
 pulls.merge_manually=අතින් සංයුක්ත කර ඇත
 pulls.merge_commit_id=ඒකාබද්ධ කිරීමේ හැඳුනුම්පත
 pulls.require_signed_wont_sign=ශාඛාවට අත්සන් කළ කොපියක් අවශ්ය වුවද මෙම ඒකාබද්ධ කිරීම අත්සන් නොකෙරේ
+
 pulls.invalid_merge_option=ඔබ මෙම අදින්න ඉල්ලීම සඳහා මෙම ඒකාබද්ධ විකල්පය භාවිතා කළ නොහැක.
 pulls.merge_conflict=ඒකාබද්ධ කිරීම අසමත් විය: ඒකාබද්ධ වන අතර ගැටුමක් ඇති විය. ඉඟිය: වෙනත් උපාය මාර්ගයක් උත්සාහ කරන්න
 pulls.merge_conflict_summary=දෝෂ පණිවිඩය
@@ -1421,6 +1422,9 @@ pulls.merge_instruction_hint=`ඔබට <a class="show-instruction">විධා
 pulls.merge_instruction_step1_desc=ඔබගේ ව්යාපෘති ගබඩාවෙන්, නව ශාඛාවක් පරීක්ෂා කර වෙනස්කම් පරීක්ෂා කරන්න.
 pulls.merge_instruction_step2_desc=Gitea හි වෙනස්කම් සහ යාවත්කාලීන කිරීම ඒකාබද්ධ කරන්න.
 
+
+
+
 milestones.new=නව සන්ධිස්ථානයක්
 milestones.open_tab=%d විවෘත
 milestones.close_tab=%d වසා
diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini
index 25bb0aeff2..182d4ca782 100644
--- a/options/locale/locale_sv-SE.ini
+++ b/options/locale/locale_sv-SE.ini
@@ -1158,12 +1158,16 @@ pulls.cannot_auto_merge_helper=Merga manuellt för att lösa konlifterna.
 
 pulls.no_merge_desc=Pull-requesten kan inte mergas för alla alternativ för merging är inaktiverade för denna utvecklingskatalog.
 pulls.no_merge_helper=Aktivera mergealternativ i utvecklingskatalogsinställningarna, eller merga manuellt.
+
 pulls.invalid_merge_option=Du kan inte använda detta mergealternativet för denna pull-request.
 ; </summary><code>%[2]s<br>%[3]s</code></details>
 pulls.open_unmerged_pull_exists=`Du kan inte återuppliva denna pull-request då det redan finns en identisk pull-request öppen (#%d).`
 pulls.update_branch_success=Uppdatering av branchen lyckades
 pulls.outdated_with_base_branch=Denna branch är föråldrad gentemot bas-branchen
 
+
+
+
 milestones.new=Ny milstolpe
 milestones.open_tab=%d Öppna
 milestones.close_tab=%d Stängda
diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini
index c7bdb9fc65..93ee7c75db 100644
--- a/options/locale/locale_tr-TR.ini
+++ b/options/locale/locale_tr-TR.ini
@@ -1408,6 +1408,7 @@ pulls.squash_merge_pull_request=Ezme işlemi oluştur
 pulls.merge_manually=Elle birleştirildi
 pulls.merge_commit_id=Birleştirme işlemesi kimliği
 pulls.require_signed_wont_sign=Dal imzalı işlemeler gerektiriyor, ancak bu birleştirme imzalanmayacak
+
 pulls.invalid_merge_option=Bu değişiklik isteği için bu birleştirme seçeneğini kullanamazsınız.
 pulls.merge_conflict=Birleştirme Başarısız Oldu: Birleştirme sırasında bir çakışma oldu. İpucu: Farklı bir strateji deneyin
 pulls.merge_conflict_summary=Hata Mesajı
@@ -1436,6 +1437,9 @@ pulls.merge_instruction_hint=`<a class="show-instruction">komut satırı talimat
 pulls.merge_instruction_step1_desc=Proje deponuzdan yeni bir dala göz atın ve değişiklikleri test edin.
 pulls.merge_instruction_step2_desc=Gitea'daki değişiklikleri ve güncellemeleri birleştirin.
 
+
+
+
 milestones.new=Yeni Kilometre Taşı
 milestones.open_tab=%d Açık
 milestones.close_tab=%d Kapalı
diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini
index 3e83932a2d..c58a5d461f 100644
--- a/options/locale/locale_uk-UA.ini
+++ b/options/locale/locale_uk-UA.ini
@@ -1457,6 +1457,7 @@ pulls.squash_merge_pull_request=Створити зварений (squash) ко
 pulls.merge_manually=Об’єднано вручну
 pulls.merge_commit_id=ID коміту злиття
 pulls.require_signed_wont_sign=Гілка вимагає підписаних комітів, але це злиття не буде підписано
+
 pulls.invalid_merge_option=Цей параметр злиття не можна використовувати для цього Pull Request'а.
 pulls.merge_conflict=Злиття не вдалося: Був конфлікт при злиття. Підказка: спробуйте іншу стратегію
 pulls.merge_conflict_summary=Помилка
@@ -1485,6 +1486,9 @@ pulls.merge_instruction_hint=`Також можна переглянути <a cl
 pulls.merge_instruction_step1_desc=У репозиторії вашого проєкту перевірте нову гілку і протестуйте зміни.
 pulls.merge_instruction_step2_desc=Об'єднати зміни і оновити на Gitea.
 
+
+
+
 milestones.new=Новий етап
 milestones.open_tab=%d відкрито
 milestones.close_tab=%d закрито
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index 1b00a2bb47..01322bbc77 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -1568,14 +1568,7 @@ pulls.squash_merge_pull_request=创建压缩提交
 pulls.merge_manually=手动合并
 pulls.merge_commit_id=合并提交 ID
 pulls.require_signed_wont_sign=分支需要签名的提交,但这个合并将不会被签名
-pulls.merge_pull_request_now=立即合并拉取请求
-pulls.rebase_merge_pull_request_now=立即变基并合并
-pulls.rebase_merge_commit_pull_request_now=立即变基并合并 (--no-ff)
-pulls.squash_merge_pull_request_now=立即压缩提交并合并
-pulls.merge_pull_request_on_status_success=所有检查成功时自动合并拉取请求
-pulls.rebase_merge_pull_request_on_status_success=所有检查成功时自动变基并合并
-pulls.rebase_merge_commit_pull_request_on_status_success=所有检查成功时自动变基并合并 (--no-ff)
-pulls.squash_merge_pull_request_on_status_success=所有检查成功时自动压缩提交并合并
+
 pulls.invalid_merge_option=你可以在此合并请求中使用合并选项。
 pulls.merge_conflict=合并失败:合并时有冲突发生。提示:采用其它合并策略
 pulls.merge_conflict_summary=错误信息
@@ -1606,14 +1599,18 @@ pulls.reopened_at=`重新打开此合并请求 <a id="%[1]s" href="#%[1]s">%[2]s
 pulls.merge_instruction_hint=`你也可以查看 <a class="show-instruction">命令行指令</a>`
 pulls.merge_instruction_step1_desc=从你的仓库中签出一个新的分支并测试变更。
 pulls.merge_instruction_step2_desc=合并变更并更新到 Gitea 上
-pulls.merge_on_status_success=合并请求计划在所有检查成功时合并。
-pulls.merge_on_status_success_already_scheduled=此拉取请求已安排在所有检查成功时合并。
-pulls.pr_has_pending_merge_on_success=%[1]s 安排此拉取请求在所有检查成功时自动合并 %[2]s。
-pulls.merge_pull_on_success_cancel=取消自动合并
-pulls.pull_request_not_scheduled=此拉取请求没有计划自动合并。
-pulls.pull_request_schedule_canceled=此拉取请求的自动合并已取消。
-pulls.pull_request_scheduled_auto_merge=`已安排此拉取请求在所有检查成功时自动合并 %[1]s`
-pulls.pull_request_canceled_scheduled_auto_merge=`已取消当所有检查成功时自动合并此拉取请求 %[1]s`
+
+pulls.auto_merge_button_when_succeed=(当检查成功时)
+pulls.auto_merge_when_succeed=在所有检查成功后自动合并
+pulls.auto_merge_newly_scheduled=合并请求计划在所有检查成功后合并。
+pulls.auto_merge_has_pending_schedule=%[1]s 安排此拉取请求在所有检查成功时自动合并 %[2]s。
+
+pulls.auto_merge_cancel_schedule=取消自动合并
+pulls.auto_merge_not_scheduled=此拉取请求没有计划自动合并。
+pulls.auto_merge_canceled_schedule=此拉取请求的自动合并已取消。
+
+pulls.auto_merge_newly_scheduled_comment=`已安排此拉取请求在所有检查成功后自动合并 %[1]s`
+pulls.auto_merge_canceled_schedule_comment=`已取消当所有检查成功后自动合并此拉取请求 %[1]s`
 
 milestones.new=新的里程碑
 milestones.open_tab=%d 开启中
diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini
index 97d3b8984c..48f04d0744 100644
--- a/options/locale/locale_zh-HK.ini
+++ b/options/locale/locale_zh-HK.ini
@@ -431,8 +431,12 @@ pulls.reopen_to_merge=請重新開啟合併請求來完成合併操作。
 pulls.merged=已合併
 pulls.can_auto_merge_desc=這個拉請求可以自動合併。
 
+
 ; </summary><code>%[2]s<br>%[3]s</code></details>
 
+
+
+
 milestones.new=新的里程碑
 milestones.open_tab=%d 開啟中
 milestones.close_tab=%d 已關閉
diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini
index 2cf099fda4..fc87cb315c 100644
--- a/options/locale/locale_zh-TW.ini
+++ b/options/locale/locale_zh-TW.ini
@@ -1495,6 +1495,9 @@ pulls.allow_edits_from_maintainers=允許維護者編輯
 pulls.allow_edits_from_maintainers_desc=對基礎分支有寫入權限的使用者也可以推送到此分支
 pulls.allow_edits_from_maintainers_err=更新失敗
 pulls.compare_changes_desc=選擇合併的目標分支和來源分支。
+pulls.has_viewed_file=已檢視
+pulls.has_changed_since_last_review=您上次審核後有變動
+pulls.viewed_files_label=%[1]d / %[2]d 個檔案已檢視
 pulls.compare_base=合併到
 pulls.compare_compare=拉取自
 pulls.switch_comparison_type=切換比較類型
@@ -1562,6 +1565,7 @@ pulls.squash_merge_pull_request=建立 Squash 提交
 pulls.merge_manually=手動合併
 pulls.merge_commit_id=合併提交 ID
 pulls.require_signed_wont_sign=該分支需要經簽署的提交,但此合併將不會被簽署。
+
 pulls.invalid_merge_option=您無法對此合併請求使用這個合併選項。
 pulls.merge_conflict=合併失敗:合併時發生衝突。 提示:請嘗試不同的策略
 pulls.merge_conflict_summary=錯誤訊息
@@ -1576,7 +1580,7 @@ pulls.push_rejected_summary=完整的拒絕訊息
 pulls.push_rejected_no_message=合併失敗:此推送被拒絕但未提供其他資訊。<br>請檢查此儲存庫的 Git Hook。
 pulls.open_unmerged_pull_exists=`您不能重新開放,因為目前有相同的合併請求 (#%d) 正在進行中。`
 pulls.status_checking=還在進行一些檢查
-pulls.status_checks_success=所有檢查都通過
+pulls.status_checks_success=已通過所有檢查
 pulls.status_checks_warning=一些檢查回報了警告
 pulls.status_checks_failure=一些檢查失敗了
 pulls.status_checks_error=一些檢查回報了錯誤
@@ -1593,6 +1597,18 @@ pulls.merge_instruction_hint=`您也可以查看<a class="show-instruction">命
 pulls.merge_instruction_step1_desc=在您的儲存庫中切換到新分支並測試變更。
 pulls.merge_instruction_step2_desc=合併變更並更新到 Gitea。
 
+pulls.auto_merge_button_when_succeed=(當通過檢查後)
+pulls.auto_merge_when_succeed=通過所有檢查後自動合併
+pulls.auto_merge_newly_scheduled=合併請求排定於通過所有檢查後合併。
+pulls.auto_merge_has_pending_schedule=%[1]s 排定了在通過所有檢查後自動合併此合併請求 %[2]s。
+
+pulls.auto_merge_cancel_schedule=取消自動合併
+pulls.auto_merge_not_scheduled=此合併請求未排定自動合併。
+pulls.auto_merge_canceled_schedule=此合併請求的自動合併已被取消。
+
+pulls.auto_merge_newly_scheduled_comment=`排定了在通過所有檢查後自動合併此合併請求 %[1]s`
+pulls.auto_merge_canceled_schedule_comment=`取消了在通過所有檢查後自動合併此合併請求 %[1]s`
+
 milestones.new=新增里程碑
 milestones.open_tab=%d 個開放中
 milestones.close_tab=%d 個已關閉

From 3708ca8e2849ca7e36e6bd15ec6935a2a2d81e55 Mon Sep 17 00:00:00 2001
From: yutotnh <57719497+yutotnh@users.noreply.github.com>
Date: Mon, 13 Jun 2022 16:34:46 +0900
Subject: [PATCH 2/7] fix: some typos (#19956)

---
 cmd/cmd.go                                               | 2 +-
 docs/content/doc/advanced/logging-documentation.en-us.md | 2 +-
 models/migrations/migrations.go                          | 4 ++--
 modules/indexer/issues/bleve_test.go                     | 2 +-
 routers/api/v1/repo/milestone.go                         | 2 +-
 templates/swagger/v1_json.tmpl                           | 2 +-
 6 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/cmd/cmd.go b/cmd/cmd.go
index 3846a86900..36d54bb19f 100644
--- a/cmd/cmd.go
+++ b/cmd/cmd.go
@@ -68,7 +68,7 @@ Ensure you are running in the correct environment or set the correct configurati
 If this is the intended configuration file complete the [database] section.`, setting.CustomConf)
 	}
 	if err := db.InitEngine(ctx); err != nil {
-		return fmt.Errorf("unable to initialise the database using the configuration in %q. Error: %v", setting.CustomConf, err)
+		return fmt.Errorf("unable to initialize the database using the configuration in %q. Error: %v", setting.CustomConf, err)
 	}
 	return nil
 }
diff --git a/docs/content/doc/advanced/logging-documentation.en-us.md b/docs/content/doc/advanced/logging-documentation.en-us.md
index bdde5bd8c4..b519641591 100644
--- a/docs/content/doc/advanced/logging-documentation.en-us.md
+++ b/docs/content/doc/advanced/logging-documentation.en-us.md
@@ -349,7 +349,7 @@ recommended that pausing only done for a very short period of time.
 
 It is possible to add and remove logging whilst Gitea is running using the `gitea manager logging add` and `remove` subcommands.
 This functionality can only adjust running log systems and cannot be used to start the access or router loggers if they
-were not already initialised. If you wish to start these systems you are advised to adjust the app.ini and (gracefully) restart
+were not already initialized. If you wish to start these systems you are advised to adjust the app.ini and (gracefully) restart
 the Gitea service.
 
 The main intention of these commands is to easily add a temporary logger to investigate problems on running systems where a restart
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 5a2297ac0d..1bed4e2bc0 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -419,7 +419,7 @@ func EnsureUpToDate(x *xorm.Engine) error {
 	}
 
 	if currentDB < 0 {
-		return fmt.Errorf("Database has not been initialised")
+		return fmt.Errorf("Database has not been initialized")
 	}
 
 	if minDBVersion > currentDB {
@@ -953,7 +953,7 @@ func dropTableColumns(sess *xorm.Session, tableName string, columnNames ...strin
 	return nil
 }
 
-// modifyColumn will modify column's type or other propertity. SQLITE is not supported
+// modifyColumn will modify column's type or other property. SQLITE is not supported
 func modifyColumn(x *xorm.Engine, tableName string, col *schemas.Column) error {
 	var indexes map[string]*schemas.Index
 	var err error
diff --git a/modules/indexer/issues/bleve_test.go b/modules/indexer/issues/bleve_test.go
index 926c32e242..68a7831a3f 100644
--- a/modules/indexer/issues/bleve_test.go
+++ b/modules/indexer/issues/bleve_test.go
@@ -26,7 +26,7 @@ func TestBleveIndexAndSearch(t *testing.T) {
 	defer indexer.Close()
 
 	if _, err := indexer.Init(); err != nil {
-		assert.Fail(t, "Unable to initialise bleve indexer: %v", err)
+		assert.Fail(t, "Unable to initialize bleve indexer: %v", err)
 		return
 	}
 
diff --git a/routers/api/v1/repo/milestone.go b/routers/api/v1/repo/milestone.go
index ce6aa7f46f..0e8800510b 100644
--- a/routers/api/v1/repo/milestone.go
+++ b/routers/api/v1/repo/milestone.go
@@ -39,7 +39,7 @@ func ListMilestones(ctx *context.APIContext) {
 	//   required: true
 	// - name: state
 	//   in: query
-	//   description: Milestone state, Recognised values are open, closed and all. Defaults to "open"
+	//   description: Milestone state, Recognized values are open, closed and all. Defaults to "open"
 	//   type: string
 	// - name: name
 	//   in: query
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index c23bcb2e9a..ecc17b51c8 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -7223,7 +7223,7 @@
           },
           {
             "type": "string",
-            "description": "Milestone state, Recognised values are open, closed and all. Defaults to \"open\"",
+            "description": "Milestone state, Recognized values are open, closed and all. Defaults to \"open\"",
             "name": "state",
             "in": "query"
           },

From 1a9821f57a0293db3adc0eab8aff08ca5fa1026c Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 13 Jun 2022 17:37:59 +0800
Subject: [PATCH 3/7] Move issues related files into models/issues (#19931)

* Move access and repo permission to models/perm/access

* fix test

* fix git test

* Move functions sequence

* Some improvements per @KN4CK3R and @delvh

* Move issues related code to models/issues

* Move some issues related sub package

* Merge

* Fix test

* Fix test

* Fix test

* Fix test

* Rename some files
---
 integrations/api_comment_test.go              |  50 +--
 integrations/api_issue_label_test.go          |  22 +-
 integrations/api_issue_reaction_test.go       |   6 +-
 integrations/api_issue_stopwatch_test.go      |  12 +-
 integrations/api_issue_subscription_test.go   |  14 +-
 integrations/api_issue_test.go                |  17 +-
 integrations/api_issue_tracked_time_test.go   |  18 +-
 integrations/api_pull_commits_test.go         |   4 +-
 integrations/api_pull_review_test.go          |  21 +-
 integrations/api_pull_test.go                 |   6 +-
 integrations/delete_user_test.go              |   4 +-
 integrations/git_test.go                      |  26 +-
 integrations/issue_test.go                    |  39 +-
 integrations/pull_merge_test.go               |  21 +-
 integrations/pull_update_test.go              |  13 +-
 integrations/user_test.go                     |   6 +-
 models/action.go                              |  28 +-
 models/action_test.go                         |  43 ++
 models/branch.go                              |  80 ----
 models/consistency.go                         | 214 ----------
 models/consistency_test.go                    | 149 -------
 models/db/consistency.go                      |  27 ++
 models/db/index.go                            |  27 +-
 models/db/list_options.go                     |   5 +
 models/error.go                               | 376 -----------------
 models/git/branches_test.go                   |   6 +-
 models/git/main_test.go                       |   1 +
 models/issue_label_test.go                    | 394 -----------------
 models/issue_stopwatch_test.go                |  78 ----
 models/issue_user_test.go                     |  61 ---
 .../assignees.go}                             |   2 +-
 .../assignees_test.go}                        |  27 +-
 .../{issue_comment.go => issues/comment.go}   | 106 +++--
 .../comment_list.go}                          |  29 +-
 .../comment_test.go}                          |  19 +-
 models/issues/content_history.go              |   6 +-
 models/issues/content_history_test.go         |  43 +-
 .../dependency.go}                            |  81 +++-
 .../dependency_test.go}                       |  27 +-
 models/{ => issues}/issue.go                  | 397 ++++++++----------
 models/issues/issue_index.go                  |  32 ++
 models/{ => issues}/issue_list.go             |  28 +-
 models/{ => issues}/issue_list_test.go        |  19 +-
 models/{ => issues}/issue_lock.go             |   2 +-
 models/{ => issues}/issue_project.go          |   2 +-
 models/{ => issues}/issue_test.go             | 206 ++++-----
 models/{ => issues}/issue_user.go             |   5 +-
 models/issues/issue_user_test.go              |  62 +++
 models/{ => issues}/issue_watch.go            |   5 +-
 models/{ => issues}/issue_watch_test.go       |  25 +-
 models/{ => issues}/issue_xref.go             |  26 +-
 models/{ => issues}/issue_xref_test.go        |  69 +--
 models/{issue_label.go => issues/label.go}    | 156 ++++++-
 models/issues/label_test.go                   | 395 +++++++++++++++++
 models/issues/main_test.go                    |  24 +-
 models/issues/milestone.go                    |  33 +-
 models/issues/milestone_test.go               | 151 +++++--
 models/{ => issues}/pull.go                   | 155 ++++++-
 models/{ => issues}/pull_list.go              |   2 +-
 models/{ => issues}/pull_test.go              |  94 +++--
 models/issues/reaction_test.go                |  31 +-
 models/{ => issues}/review.go                 |  53 ++-
 models/{ => issues}/review_test.go            | 111 ++---
 .../stopwatch.go}                             |   4 +-
 models/issues/stopwatch_test.go               |  79 ++++
 .../tracked_time.go}                          |   4 +-
 .../tracked_time_test.go}                     |  35 +-
 models/main_test.go                           |   5 -
 models/migrate.go                             |  49 +--
 models/migrate_test.go                        |  28 +-
 models/migrations/v111.go                     |   2 +-
 models/notification.go                        |  58 +--
 models/notification_test.go                   |   5 +-
 models/org_team.go                            |   5 +-
 models/repo.go                                |  20 +-
 models/repo/repo.go                           |  47 +++
 models/repo_activity.go                       |  21 +-
 models/repo_collaboration.go                  |   5 +-
 models/repo_transfer.go                       |   3 +-
 models/statistic.go                           |   4 +-
 models/user.go                                |  18 +-
 modules/context/repo.go                       |   7 +-
 modules/convert/convert.go                    |   4 +-
 modules/convert/issue.go                      |  21 +-
 modules/convert/issue_comment.go              |  14 +-
 modules/convert/issue_test.go                 |   3 +-
 modules/convert/pull.go                       |   8 +-
 modules/convert/pull_review.go                |  20 +-
 modules/convert/pull_test.go                  |   6 +-
 modules/doctor/dbconsistency.go               |  29 +-
 modules/doctor/mergebase.go                   |  10 +-
 modules/eventsource/manager_run.go            |   3 +-
 modules/indexer/issues/db.go                  |   4 +-
 modules/indexer/issues/indexer.go             |  12 +-
 modules/notification/action/action.go         |  21 +-
 modules/notification/base/notifier.go         |  47 ++-
 modules/notification/base/null.go             |  47 ++-
 modules/notification/indexer/indexer.go       |  24 +-
 modules/notification/mail/mail.go             |  43 +-
 modules/notification/notification.go          |  47 ++-
 modules/notification/ui/ui.go                 |  29 +-
 modules/notification/webhook/webhook.go       |  49 +--
 modules/repository/init.go                    |   9 +-
 modules/templates/helper.go                   |   3 +-
 routers/api/v1/misc/nodeinfo.go               |   6 +-
 routers/api/v1/notify/threads.go              |   5 +-
 routers/api/v1/org/label.go                   |  30 +-
 routers/api/v1/repo/file.go                   |   6 +-
 routers/api/v1/repo/issue.go                  |  59 ++-
 routers/api/v1/repo/issue_comment.go          |  66 +--
 routers/api/v1/repo/issue_label.go            |  32 +-
 routers/api/v1/repo/issue_reaction.go         |  17 +-
 routers/api/v1/repo/issue_stopwatch.go        |  20 +-
 routers/api/v1/repo/issue_subscription.go     |  24 +-
 routers/api/v1/repo/issue_tracked_time.go     |  48 +--
 routers/api/v1/repo/label.go                  |  30 +-
 routers/api/v1/repo/patch.go                  |   3 +-
 routers/api/v1/repo/pull.go                   |  82 ++--
 routers/api/v1/repo/pull_review.go            |  64 +--
 routers/private/hook_post_receive.go          |  10 +-
 routers/private/hook_pre_receive.go           |   5 +-
 routers/web/org/org_labels.go                 |  16 +-
 routers/web/repo/branch.go                    |   5 +-
 routers/web/repo/compare.go                   |   5 +-
 routers/web/repo/issue.go                     | 276 ++++++------
 routers/web/repo/issue_content_history.go     |  27 +-
 routers/web/repo/issue_dependency.go          |  26 +-
 routers/web/repo/issue_label.go               |  24 +-
 routers/web/repo/issue_label_test.go          |  24 +-
 routers/web/repo/issue_lock.go                |   6 +-
 routers/web/repo/issue_stopwatch.go           |  14 +-
 routers/web/repo/issue_test.go                | 158 +++----
 routers/web/repo/issue_timetrack.go           |   8 +-
 routers/web/repo/issue_watch.go               |   4 +-
 routers/web/repo/projects.go                  |  14 +-
 routers/web/repo/pull.go                      |  65 +--
 routers/web/repo/pull_review.go               |  24 +-
 routers/web/user/home.go                      |  52 +--
 routers/web/user/stop_watch.go                |   6 +-
 services/agit/agit.go                         |  21 +-
 services/asymkey/sign.go                      |   6 +-
 services/automerge/automerge.go               |  24 +-
 services/comments/comments.go                 |  36 +-
 services/forms/repo_form.go                   |  15 +-
 services/forms/user_form_hidden_comments.go   |  58 +--
 services/gitdiff/gitdiff.go                   |  14 +-
 services/gitdiff/gitdiff_test.go              |  10 +-
 services/gitdiff/main_test.go                 |   1 +
 services/issue/assignee.go                    |  56 +--
 services/issue/assignee_test.go               |   6 +-
 services/issue/commit.go                      |  15 +-
 services/issue/commit_test.go                 |  57 +--
 services/issue/content.go                     |   6 +-
 services/issue/issue.go                       | 108 ++++-
 services/issue/issue_test.go                  |  61 ++-
 services/issue/label.go                       |  32 +-
 services/issue/label_test.go                  |  16 +-
 services/issue/milestone.go                   |  13 +-
 services/issue/milestone_test.go              |   9 +-
 services/issue/status.go                      |  14 +-
 services/mailer/mail.go                       |  29 +-
 services/mailer/mail_comment.go               |   7 +-
 services/mailer/mail_issue.go                 |  17 +-
 services/mailer/mail_test.go                  |  17 +-
 services/migrations/gitea_uploader.go         |  82 ++--
 services/migrations/gitea_uploader_test.go    |   6 +-
 services/pull/check.go                        |  31 +-
 services/pull/check_test.go                   |  12 +-
 services/pull/commit_status.go                |  12 +-
 services/pull/edits.go                        |   6 +-
 services/pull/lfs.go                          |   6 +-
 services/pull/merge.go                        |  29 +-
 services/pull/patch.go                        |  21 +-
 services/pull/pull.go                         |  89 ++--
 services/pull/pull_test.go                    |   6 +-
 services/pull/review.go                       |  60 +--
 services/pull/temp_repo.go                    |   5 +-
 services/pull/update.go                       |  17 +-
 services/repository/repository.go             |   3 +-
 services/repository/template.go               |   7 +-
 180 files changed, 3667 insertions(+), 3677 deletions(-)
 delete mode 100644 models/branch.go
 delete mode 100644 models/consistency_test.go
 create mode 100644 models/db/consistency.go
 delete mode 100644 models/issue_label_test.go
 delete mode 100644 models/issue_stopwatch_test.go
 delete mode 100644 models/issue_user_test.go
 rename models/{issue_assignees.go => issues/assignees.go} (99%)
 rename models/{issue_assignees_test.go => issues/assignees_test.go} (65%)
 rename models/{issue_comment.go => issues/comment.go} (92%)
 rename models/{issue_comment_list.go => issues/comment_list.go} (95%)
 rename models/{issue_comment_test.go => issues/comment_test.go} (71%)
 rename models/{issue_dependency.go => issues/dependency.go} (59%)
 rename models/{issue_dependency_test.go => issues/dependency_test.go} (51%)
 rename models/{ => issues}/issue.go (89%)
 create mode 100644 models/issues/issue_index.go
 rename models/{ => issues}/issue_list.go (96%)
 rename models/{ => issues}/issue_list_test.go (71%)
 rename models/{ => issues}/issue_lock.go (98%)
 rename models/{ => issues}/issue_project.go (99%)
 rename models/{ => issues}/issue_test.go (73%)
 rename models/{ => issues}/issue_user.go (94%)
 create mode 100644 models/issues/issue_user_test.go
 rename models/{ => issues}/issue_watch.go (96%)
 rename models/{ => issues}/issue_watch_test.go (53%)
 rename models/{ => issues}/issue_xref.go (93%)
 rename models/{ => issues}/issue_xref_test.go (61%)
 rename models/{issue_label.go => issues/label.go} (79%)
 create mode 100644 models/issues/label_test.go
 rename models/{ => issues}/pull.go (80%)
 rename models/{ => issues}/pull_list.go (99%)
 rename models/{ => issues}/pull_test.go (58%)
 rename models/{ => issues}/review.go (94%)
 rename models/{ => issues}/review_test.go (50%)
 rename models/{issue_stopwatch.go => issues/stopwatch.go} (99%)
 create mode 100644 models/issues/stopwatch_test.go
 rename models/{issue_tracked_time.go => issues/tracked_time.go} (99%)
 rename models/{issue_tracked_time_test.go => issues/tracked_time_test.go} (56%)

diff --git a/integrations/api_comment_test.go b/integrations/api_comment_test.go
index dde51b2d53..7dcc0279fc 100644
--- a/integrations/api_comment_test.go
+++ b/integrations/api_comment_test.go
@@ -10,7 +10,7 @@ import (
 	"net/url"
 	"testing"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
@@ -23,9 +23,9 @@ import (
 func TestAPIListRepoComments(t *testing.T) {
 	defer prepareTestEnv(t)()
 
-	comment := unittest.AssertExistsAndLoadBean(t, &models.Comment{},
-		unittest.Cond("type = ?", models.CommentTypeComment)).(*models.Comment)
-	issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: comment.IssueID}).(*models.Issue)
+	comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{},
+		unittest.Cond("type = ?", issues_model.CommentTypeComment)).(*issues_model.Comment)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}).(*issues_model.Issue)
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository)
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
 
@@ -38,10 +38,10 @@ func TestAPIListRepoComments(t *testing.T) {
 	DecodeJSON(t, resp, &apiComments)
 	assert.Len(t, apiComments, 2)
 	for _, apiComment := range apiComments {
-		c := &models.Comment{ID: apiComment.ID}
+		c := &issues_model.Comment{ID: apiComment.ID}
 		unittest.AssertExistsAndLoadBean(t, c,
-			unittest.Cond("type = ?", models.CommentTypeComment))
-		unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: c.IssueID, RepoID: repo.ID})
+			unittest.Cond("type = ?", issues_model.CommentTypeComment))
+		unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: c.IssueID, RepoID: repo.ID})
 	}
 
 	// test before and since filters
@@ -69,9 +69,9 @@ func TestAPIListRepoComments(t *testing.T) {
 func TestAPIListIssueComments(t *testing.T) {
 	defer prepareTestEnv(t)()
 
-	comment := unittest.AssertExistsAndLoadBean(t, &models.Comment{},
-		unittest.Cond("type = ?", models.CommentTypeComment)).(*models.Comment)
-	issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: comment.IssueID}).(*models.Issue)
+	comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{},
+		unittest.Cond("type = ?", issues_model.CommentTypeComment)).(*issues_model.Comment)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}).(*issues_model.Issue)
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository)
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
 
@@ -82,8 +82,8 @@ func TestAPIListIssueComments(t *testing.T) {
 
 	var comments []*api.Comment
 	DecodeJSON(t, resp, &comments)
-	expectedCount := unittest.GetCount(t, &models.Comment{IssueID: issue.ID},
-		unittest.Cond("type = ?", models.CommentTypeComment))
+	expectedCount := unittest.GetCount(t, &issues_model.Comment{IssueID: issue.ID},
+		unittest.Cond("type = ?", issues_model.CommentTypeComment))
 	assert.EqualValues(t, expectedCount, len(comments))
 }
 
@@ -91,7 +91,7 @@ func TestAPICreateComment(t *testing.T) {
 	defer prepareTestEnv(t)()
 	const commentBody = "Comment body"
 
-	issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{}).(*models.Issue)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{}).(*issues_model.Issue)
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository)
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
 
@@ -107,13 +107,13 @@ func TestAPICreateComment(t *testing.T) {
 	var updatedComment api.Comment
 	DecodeJSON(t, resp, &updatedComment)
 	assert.EqualValues(t, commentBody, updatedComment.Body)
-	unittest.AssertExistsAndLoadBean(t, &models.Comment{ID: updatedComment.ID, IssueID: issue.ID, Content: commentBody})
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: updatedComment.ID, IssueID: issue.ID, Content: commentBody})
 }
 
 func TestAPIGetComment(t *testing.T) {
 	defer prepareTestEnv(t)()
 
-	comment := unittest.AssertExistsAndLoadBean(t, &models.Comment{ID: 2}).(*models.Comment)
+	comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2}).(*issues_model.Comment)
 	assert.NoError(t, comment.LoadIssue())
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: comment.Issue.RepoID}).(*repo_model.Repository)
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
@@ -141,9 +141,9 @@ func TestAPIEditComment(t *testing.T) {
 	defer prepareTestEnv(t)()
 	const newCommentBody = "This is the new comment body"
 
-	comment := unittest.AssertExistsAndLoadBean(t, &models.Comment{},
-		unittest.Cond("type = ?", models.CommentTypeComment)).(*models.Comment)
-	issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: comment.IssueID}).(*models.Issue)
+	comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{},
+		unittest.Cond("type = ?", issues_model.CommentTypeComment)).(*issues_model.Comment)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}).(*issues_model.Issue)
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository)
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
 
@@ -160,15 +160,15 @@ func TestAPIEditComment(t *testing.T) {
 	DecodeJSON(t, resp, &updatedComment)
 	assert.EqualValues(t, comment.ID, updatedComment.ID)
 	assert.EqualValues(t, newCommentBody, updatedComment.Body)
-	unittest.AssertExistsAndLoadBean(t, &models.Comment{ID: comment.ID, IssueID: issue.ID, Content: newCommentBody})
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID, IssueID: issue.ID, Content: newCommentBody})
 }
 
 func TestAPIDeleteComment(t *testing.T) {
 	defer prepareTestEnv(t)()
 
-	comment := unittest.AssertExistsAndLoadBean(t, &models.Comment{},
-		unittest.Cond("type = ?", models.CommentTypeComment)).(*models.Comment)
-	issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: comment.IssueID}).(*models.Issue)
+	comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{},
+		unittest.Cond("type = ?", issues_model.CommentTypeComment)).(*issues_model.Comment)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}).(*issues_model.Issue)
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository)
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
 
@@ -178,14 +178,14 @@ func TestAPIDeleteComment(t *testing.T) {
 		repoOwner.Name, repo.Name, comment.ID, token)
 	session.MakeRequest(t, req, http.StatusNoContent)
 
-	unittest.AssertNotExistsBean(t, &models.Comment{ID: comment.ID})
+	unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: comment.ID})
 }
 
 func TestAPIListIssueTimeline(t *testing.T) {
 	defer prepareTestEnv(t)()
 
 	// load comment
-	issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 1}).(*models.Issue)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue)
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository)
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
 
@@ -199,6 +199,6 @@ func TestAPIListIssueTimeline(t *testing.T) {
 	// lists extracted directly from DB are the same
 	var comments []*api.TimelineComment
 	DecodeJSON(t, resp, &comments)
-	expectedCount := unittest.GetCount(t, &models.Comment{IssueID: issue.ID})
+	expectedCount := unittest.GetCount(t, &issues_model.Comment{IssueID: issue.ID})
 	assert.EqualValues(t, expectedCount, len(comments))
 }
diff --git a/integrations/api_issue_label_test.go b/integrations/api_issue_label_test.go
index 94b487377e..9b6333b2a2 100644
--- a/integrations/api_issue_label_test.go
+++ b/integrations/api_issue_label_test.go
@@ -10,7 +10,7 @@ import (
 	"strings"
 	"testing"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
@@ -37,7 +37,7 @@ func TestAPIModifyLabels(t *testing.T) {
 	resp := session.MakeRequest(t, req, http.StatusCreated)
 	apiLabel := new(api.Label)
 	DecodeJSON(t, resp, &apiLabel)
-	dbLabel := unittest.AssertExistsAndLoadBean(t, &models.Label{ID: apiLabel.ID, RepoID: repo.ID}).(*models.Label)
+	dbLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: apiLabel.ID, RepoID: repo.ID}).(*issues_model.Label)
 	assert.EqualValues(t, dbLabel.Name, apiLabel.Name)
 	assert.EqualValues(t, strings.TrimLeft(dbLabel.Color, "#"), apiLabel.Color)
 
@@ -92,8 +92,8 @@ func TestAPIAddIssueLabels(t *testing.T) {
 	assert.NoError(t, unittest.LoadFixtures())
 
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
-	issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{RepoID: repo.ID}).(*models.Issue)
-	_ = unittest.AssertExistsAndLoadBean(t, &models.Label{RepoID: repo.ID, ID: 2}).(*models.Label)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID}).(*issues_model.Issue)
+	_ = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{RepoID: repo.ID, ID: 2}).(*issues_model.Label)
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
 
 	session := loginUser(t, owner.Name)
@@ -106,17 +106,17 @@ func TestAPIAddIssueLabels(t *testing.T) {
 	resp := session.MakeRequest(t, req, http.StatusOK)
 	var apiLabels []*api.Label
 	DecodeJSON(t, resp, &apiLabels)
-	assert.Len(t, apiLabels, unittest.GetCount(t, &models.IssueLabel{IssueID: issue.ID}))
+	assert.Len(t, apiLabels, unittest.GetCount(t, &issues_model.IssueLabel{IssueID: issue.ID}))
 
-	unittest.AssertExistsAndLoadBean(t, &models.IssueLabel{IssueID: issue.ID, LabelID: 2})
+	unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: 2})
 }
 
 func TestAPIReplaceIssueLabels(t *testing.T) {
 	assert.NoError(t, unittest.LoadFixtures())
 
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
-	issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{RepoID: repo.ID}).(*models.Issue)
-	label := unittest.AssertExistsAndLoadBean(t, &models.Label{RepoID: repo.ID}).(*models.Label)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID}).(*issues_model.Issue)
+	label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{RepoID: repo.ID}).(*issues_model.Label)
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
 
 	session := loginUser(t, owner.Name)
@@ -133,8 +133,8 @@ func TestAPIReplaceIssueLabels(t *testing.T) {
 		assert.EqualValues(t, label.ID, apiLabels[0].ID)
 	}
 
-	unittest.AssertCount(t, &models.IssueLabel{IssueID: issue.ID}, 1)
-	unittest.AssertExistsAndLoadBean(t, &models.IssueLabel{IssueID: issue.ID, LabelID: label.ID})
+	unittest.AssertCount(t, &issues_model.IssueLabel{IssueID: issue.ID}, 1)
+	unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label.ID})
 }
 
 func TestAPIModifyOrgLabels(t *testing.T) {
@@ -156,7 +156,7 @@ func TestAPIModifyOrgLabels(t *testing.T) {
 	resp := session.MakeRequest(t, req, http.StatusCreated)
 	apiLabel := new(api.Label)
 	DecodeJSON(t, resp, &apiLabel)
-	dbLabel := unittest.AssertExistsAndLoadBean(t, &models.Label{ID: apiLabel.ID, OrgID: owner.ID}).(*models.Label)
+	dbLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: apiLabel.ID, OrgID: owner.ID}).(*issues_model.Label)
 	assert.EqualValues(t, dbLabel.Name, apiLabel.Name)
 	assert.EqualValues(t, strings.TrimLeft(dbLabel.Color, "#"), apiLabel.Color)
 
diff --git a/integrations/api_issue_reaction_test.go b/integrations/api_issue_reaction_test.go
index 4a063c8c68..3834af2130 100644
--- a/integrations/api_issue_reaction_test.go
+++ b/integrations/api_issue_reaction_test.go
@@ -10,8 +10,8 @@ import (
 	"testing"
 	"time"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/convert"
@@ -23,7 +23,7 @@ import (
 func TestAPIIssuesReactions(t *testing.T) {
 	defer prepareTestEnv(t)()
 
-	issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 1}).(*models.Issue)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue)
 	_ = issue.LoadRepo(db.DefaultContext)
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID}).(*user_model.User)
 
@@ -80,7 +80,7 @@ func TestAPIIssuesReactions(t *testing.T) {
 func TestAPICommentReactions(t *testing.T) {
 	defer prepareTestEnv(t)()
 
-	comment := unittest.AssertExistsAndLoadBean(t, &models.Comment{ID: 2}).(*models.Comment)
+	comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2}).(*issues_model.Comment)
 	_ = comment.LoadIssue()
 	issue := comment.Issue
 	_ = issue.LoadRepo(db.DefaultContext)
diff --git a/integrations/api_issue_stopwatch_test.go b/integrations/api_issue_stopwatch_test.go
index 90098b9236..0d06447181 100644
--- a/integrations/api_issue_stopwatch_test.go
+++ b/integrations/api_issue_stopwatch_test.go
@@ -8,8 +8,8 @@ import (
 	"net/http"
 	"testing"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
@@ -30,8 +30,8 @@ func TestAPIListStopWatches(t *testing.T) {
 	resp := session.MakeRequest(t, req, http.StatusOK)
 	var apiWatches []*api.StopWatch
 	DecodeJSON(t, resp, &apiWatches)
-	stopwatch := unittest.AssertExistsAndLoadBean(t, &models.Stopwatch{UserID: owner.ID}).(*models.Stopwatch)
-	issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: stopwatch.IssueID}).(*models.Issue)
+	stopwatch := unittest.AssertExistsAndLoadBean(t, &issues_model.Stopwatch{UserID: owner.ID}).(*issues_model.Stopwatch)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: stopwatch.IssueID}).(*issues_model.Issue)
 	if assert.Len(t, apiWatches, 1) {
 		assert.EqualValues(t, stopwatch.CreatedUnix.AsTime().Unix(), apiWatches[0].Created.Unix())
 		assert.EqualValues(t, issue.Index, apiWatches[0].IssueIndex)
@@ -45,7 +45,7 @@ func TestAPIListStopWatches(t *testing.T) {
 func TestAPIStopStopWatches(t *testing.T) {
 	defer prepareTestEnv(t)()
 
-	issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 2}).(*models.Issue)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}).(*issues_model.Issue)
 	_ = issue.LoadRepo(db.DefaultContext)
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID}).(*user_model.User)
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
@@ -61,7 +61,7 @@ func TestAPIStopStopWatches(t *testing.T) {
 func TestAPICancelStopWatches(t *testing.T) {
 	defer prepareTestEnv(t)()
 
-	issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 1}).(*models.Issue)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue)
 	_ = issue.LoadRepo(db.DefaultContext)
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID}).(*user_model.User)
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
@@ -77,7 +77,7 @@ func TestAPICancelStopWatches(t *testing.T) {
 func TestAPIStartStopWatches(t *testing.T) {
 	defer prepareTestEnv(t)()
 
-	issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 3}).(*models.Issue)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}).(*issues_model.Issue)
 	_ = issue.LoadRepo(db.DefaultContext)
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID}).(*user_model.User)
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
diff --git a/integrations/api_issue_subscription_test.go b/integrations/api_issue_subscription_test.go
index e0bb388365..2c6cddcab9 100644
--- a/integrations/api_issue_subscription_test.go
+++ b/integrations/api_issue_subscription_test.go
@@ -9,7 +9,7 @@ import (
 	"net/http"
 	"testing"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
@@ -21,18 +21,18 @@ import (
 func TestAPIIssueSubscriptions(t *testing.T) {
 	defer prepareTestEnv(t)()
 
-	issue1 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 1}).(*models.Issue)
-	issue2 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 2}).(*models.Issue)
-	issue3 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 3}).(*models.Issue)
-	issue4 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 4}).(*models.Issue)
-	issue5 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 8}).(*models.Issue)
+	issue1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue)
+	issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}).(*issues_model.Issue)
+	issue3 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}).(*issues_model.Issue)
+	issue4 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 4}).(*issues_model.Issue)
+	issue5 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 8}).(*issues_model.Issue)
 
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue1.PosterID}).(*user_model.User)
 
 	session := loginUser(t, owner.Name)
 	token := getTokenForLoggedInUser(t, session)
 
-	testSubscription := func(issue *models.Issue, isWatching bool) {
+	testSubscription := func(issue *issues_model.Issue, isWatching bool) {
 		issueRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository)
 
 		urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/subscriptions/check?token=%s", issueRepo.OwnerName, issueRepo.Name, issue.Index, token)
diff --git a/integrations/api_issue_test.go b/integrations/api_issue_test.go
index cc7d8d6bd5..5c802e8d20 100644
--- a/integrations/api_issue_test.go
+++ b/integrations/api_issue_test.go
@@ -11,7 +11,8 @@ import (
 	"testing"
 	"time"
 
-	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
@@ -34,9 +35,9 @@ func TestAPIListIssues(t *testing.T) {
 	resp := session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
 	var apiIssues []*api.Issue
 	DecodeJSON(t, resp, &apiIssues)
-	assert.Len(t, apiIssues, unittest.GetCount(t, &models.Issue{RepoID: repo.ID}))
+	assert.Len(t, apiIssues, unittest.GetCount(t, &issues_model.Issue{RepoID: repo.ID}))
 	for _, apiIssue := range apiIssues {
-		unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: apiIssue.ID, RepoID: repo.ID})
+		unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: apiIssue.ID, RepoID: repo.ID})
 	}
 
 	// test milestone filter
@@ -91,7 +92,7 @@ func TestAPICreateIssue(t *testing.T) {
 	assert.Equal(t, body, apiIssue.Body)
 	assert.Equal(t, title, apiIssue.Title)
 
-	unittest.AssertExistsAndLoadBean(t, &models.Issue{
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{
 		RepoID:     repoBefore.ID,
 		AssigneeID: owner.ID,
 		Content:    body,
@@ -106,10 +107,10 @@ func TestAPICreateIssue(t *testing.T) {
 func TestAPIEditIssue(t *testing.T) {
 	defer prepareTestEnv(t)()
 
-	issueBefore := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 10}).(*models.Issue)
+	issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10}).(*issues_model.Issue)
 	repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID}).(*repo_model.Repository)
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID}).(*user_model.User)
-	assert.NoError(t, issueBefore.LoadAttributes())
+	assert.NoError(t, issueBefore.LoadAttributes(db.DefaultContext))
 	assert.Equal(t, int64(1019307200), int64(issueBefore.DeadlineUnix))
 	assert.Equal(t, api.StateOpen, issueBefore.State())
 
@@ -137,12 +138,12 @@ func TestAPIEditIssue(t *testing.T) {
 	var apiIssue api.Issue
 	DecodeJSON(t, resp, &apiIssue)
 
-	issueAfter := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 10}).(*models.Issue)
+	issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10}).(*issues_model.Issue)
 	repoAfter := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID}).(*repo_model.Repository)
 
 	// check deleted user
 	assert.Equal(t, int64(500), issueAfter.PosterID)
-	assert.NoError(t, issueAfter.LoadAttributes())
+	assert.NoError(t, issueAfter.LoadAttributes(db.DefaultContext))
 	assert.Equal(t, int64(-1), issueAfter.PosterID)
 	assert.Equal(t, int64(-1), issueBefore.PosterID)
 	assert.Equal(t, int64(-1), apiIssue.Poster.ID)
diff --git a/integrations/api_issue_tracked_time_test.go b/integrations/api_issue_tracked_time_test.go
index 7c69d4eb9e..a6846cb786 100644
--- a/integrations/api_issue_tracked_time_test.go
+++ b/integrations/api_issue_tracked_time_test.go
@@ -10,8 +10,8 @@ import (
 	"testing"
 	"time"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	api "code.gitea.io/gitea/modules/structs"
@@ -23,7 +23,7 @@ func TestAPIGetTrackedTimes(t *testing.T) {
 	defer prepareTestEnv(t)()
 
 	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
-	issue2 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 2}).(*models.Issue)
+	issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}).(*issues_model.Issue)
 	assert.NoError(t, issue2.LoadRepo(db.DefaultContext))
 
 	session := loginUser(t, user2.Name)
@@ -33,7 +33,7 @@ func TestAPIGetTrackedTimes(t *testing.T) {
 	resp := session.MakeRequest(t, req, http.StatusOK)
 	var apiTimes api.TrackedTimeList
 	DecodeJSON(t, resp, &apiTimes)
-	expect, err := models.GetTrackedTimes(db.DefaultContext, &models.FindTrackedTimesOptions{IssueID: issue2.ID})
+	expect, err := issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{IssueID: issue2.ID})
 	assert.NoError(t, err)
 	assert.Len(t, apiTimes, 3)
 
@@ -64,8 +64,8 @@ func TestAPIGetTrackedTimes(t *testing.T) {
 func TestAPIDeleteTrackedTime(t *testing.T) {
 	defer prepareTestEnv(t)()
 
-	time6 := unittest.AssertExistsAndLoadBean(t, &models.TrackedTime{ID: 6}).(*models.TrackedTime)
-	issue2 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 2}).(*models.Issue)
+	time6 := unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{ID: 6}).(*issues_model.TrackedTime)
+	issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}).(*issues_model.Issue)
 	assert.NoError(t, issue2.LoadRepo(db.DefaultContext))
 	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
 
@@ -76,14 +76,14 @@ func TestAPIDeleteTrackedTime(t *testing.T) {
 	req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/%d/times/%d?token=%s", user2.Name, issue2.Repo.Name, issue2.Index, time6.ID, token)
 	session.MakeRequest(t, req, http.StatusForbidden)
 
-	time3 := unittest.AssertExistsAndLoadBean(t, &models.TrackedTime{ID: 3}).(*models.TrackedTime)
+	time3 := unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{ID: 3}).(*issues_model.TrackedTime)
 	req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/%d/times/%d?token=%s", user2.Name, issue2.Repo.Name, issue2.Index, time3.ID, token)
 	session.MakeRequest(t, req, http.StatusNoContent)
 	// Delete non existing time
 	session.MakeRequest(t, req, http.StatusNotFound)
 
 	// Reset time of user 2 on issue 2
-	trackedSeconds, err := models.GetTrackedSeconds(db.DefaultContext, models.FindTrackedTimesOptions{IssueID: 2, UserID: 2})
+	trackedSeconds, err := issues_model.GetTrackedSeconds(db.DefaultContext, issues_model.FindTrackedTimesOptions{IssueID: 2, UserID: 2})
 	assert.NoError(t, err)
 	assert.Equal(t, int64(3661), trackedSeconds)
 
@@ -91,7 +91,7 @@ func TestAPIDeleteTrackedTime(t *testing.T) {
 	session.MakeRequest(t, req, http.StatusNoContent)
 	session.MakeRequest(t, req, http.StatusNotFound)
 
-	trackedSeconds, err = models.GetTrackedSeconds(db.DefaultContext, models.FindTrackedTimesOptions{IssueID: 2, UserID: 2})
+	trackedSeconds, err = issues_model.GetTrackedSeconds(db.DefaultContext, issues_model.FindTrackedTimesOptions{IssueID: 2, UserID: 2})
 	assert.NoError(t, err)
 	assert.Equal(t, int64(0), trackedSeconds)
 }
@@ -99,7 +99,7 @@ func TestAPIDeleteTrackedTime(t *testing.T) {
 func TestAPIAddTrackedTimes(t *testing.T) {
 	defer prepareTestEnv(t)()
 
-	issue2 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 2}).(*models.Issue)
+	issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}).(*issues_model.Issue)
 	assert.NoError(t, issue2.LoadRepo(db.DefaultContext))
 	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
 	admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
diff --git a/integrations/api_pull_commits_test.go b/integrations/api_pull_commits_test.go
index 5e057b05a1..3b75fbcb4a 100644
--- a/integrations/api_pull_commits_test.go
+++ b/integrations/api_pull_commits_test.go
@@ -8,7 +8,7 @@ import (
 	"net/http"
 	"testing"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	api "code.gitea.io/gitea/modules/structs"
@@ -18,7 +18,7 @@ import (
 
 func TestAPIPullCommits(t *testing.T) {
 	defer prepareTestEnv(t)()
-	pullIssue := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2}).(*models.PullRequest)
+	pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}).(*issues_model.PullRequest)
 	assert.NoError(t, pullIssue.LoadIssue())
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.HeadRepoID}).(*repo_model.Repository)
 
diff --git a/integrations/api_pull_review_test.go b/integrations/api_pull_review_test.go
index 3f80dbdf9b..b601ca1d41 100644
--- a/integrations/api_pull_review_test.go
+++ b/integrations/api_pull_review_test.go
@@ -9,7 +9,8 @@ import (
 	"net/http"
 	"testing"
 
-	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/json"
@@ -20,8 +21,8 @@ import (
 
 func TestAPIPullReview(t *testing.T) {
 	defer prepareTestEnv(t)()
-	pullIssue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 3}).(*models.Issue)
-	assert.NoError(t, pullIssue.LoadAttributes())
+	pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}).(*issues_model.Issue)
+	assert.NoError(t, pullIssue.LoadAttributes(db.DefaultContext))
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.RepoID}).(*repo_model.Repository)
 
 	// test ListPullReviews
@@ -64,7 +65,7 @@ func TestAPIPullReview(t *testing.T) {
 	assert.EqualValues(t, *reviews[5], review)
 
 	// test GetPullReviewComments
-	comment := unittest.AssertExistsAndLoadBean(t, &models.Comment{ID: 7}).(*models.Comment)
+	comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 7}).(*issues_model.Comment)
 	req = NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews/%d/comments?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, 10, token)
 	resp = session.MakeRequest(t, req, http.StatusOK)
 	var reviewComments []*api.PullReviewComment
@@ -199,8 +200,8 @@ func TestAPIPullReview(t *testing.T) {
 
 	// test get review requests
 	// to make it simple, use same api with get review
-	pullIssue12 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 12}).(*models.Issue)
-	assert.NoError(t, pullIssue12.LoadAttributes())
+	pullIssue12 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 12}).(*issues_model.Issue)
+	assert.NoError(t, pullIssue12.LoadAttributes(db.DefaultContext))
 	repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue12.RepoID}).(*repo_model.Repository)
 
 	req = NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo3.OwnerName, repo3.Name, pullIssue12.Index, token)
@@ -223,8 +224,8 @@ func TestAPIPullReview(t *testing.T) {
 
 func TestAPIPullReviewRequest(t *testing.T) {
 	defer prepareTestEnv(t)()
-	pullIssue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 3}).(*models.Issue)
-	assert.NoError(t, pullIssue.LoadAttributes())
+	pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}).(*issues_model.Issue)
+	assert.NoError(t, pullIssue.LoadAttributes(db.DefaultContext))
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.RepoID}).(*repo_model.Repository)
 
 	// Test add Review Request
@@ -268,8 +269,8 @@ func TestAPIPullReviewRequest(t *testing.T) {
 	session.MakeRequest(t, req, http.StatusNoContent)
 
 	// Test team review request
-	pullIssue12 := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 12}).(*models.Issue)
-	assert.NoError(t, pullIssue12.LoadAttributes())
+	pullIssue12 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 12}).(*issues_model.Issue)
+	assert.NoError(t, pullIssue12.LoadAttributes(db.DefaultContext))
 	repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue12.RepoID}).(*repo_model.Repository)
 
 	// Test add Team Review Request
diff --git a/integrations/api_pull_test.go b/integrations/api_pull_test.go
index a1c2a4c3e6..0c63ec2c00 100644
--- a/integrations/api_pull_test.go
+++ b/integrations/api_pull_test.go
@@ -9,7 +9,7 @@ import (
 	"net/http"
 	"testing"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
@@ -33,7 +33,7 @@ func TestAPIViewPulls(t *testing.T) {
 
 	var pulls []*api.PullRequest
 	DecodeJSON(t, resp, &pulls)
-	expectedLen := unittest.GetCount(t, &models.Issue{RepoID: repo.ID}, unittest.Cond("is_pull = ?", true))
+	expectedLen := unittest.GetCount(t, &issues_model.Issue{RepoID: repo.ID}, unittest.Cond("is_pull = ?", true))
 	assert.Len(t, pulls, expectedLen)
 }
 
@@ -42,7 +42,7 @@ func TestAPIMergePullWIP(t *testing.T) {
 	defer prepareTestEnv(t)()
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
-	pr := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{Status: models.PullRequestStatusMergeable}, unittest.Cond("has_merged = ?", false)).(*models.PullRequest)
+	pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{Status: issues_model.PullRequestStatusMergeable}, unittest.Cond("has_merged = ?", false)).(*issues_model.PullRequest)
 	pr.LoadIssue()
 	issue_service.ChangeTitle(pr.Issue, owner, setting.Repository.PullRequest.WorkInProgressPrefixes[0]+" "+pr.Issue.Title)
 
diff --git a/integrations/delete_user_test.go b/integrations/delete_user_test.go
index cf376f6fcc..8b86780224 100644
--- a/integrations/delete_user_test.go
+++ b/integrations/delete_user_test.go
@@ -9,7 +9,7 @@ import (
 	"net/http"
 	"testing"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -24,7 +24,7 @@ func assertUserDeleted(t *testing.T, userID int64) {
 	unittest.AssertNotExistsBean(t, &repo_model.Repository{OwnerID: userID})
 	unittest.AssertNotExistsBean(t, &access_model.Access{UserID: userID})
 	unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: userID})
-	unittest.AssertNotExistsBean(t, &models.IssueUser{UID: userID})
+	unittest.AssertNotExistsBean(t, &issues_model.IssueUser{UID: userID})
 	unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: userID})
 	unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID})
 }
diff --git a/integrations/git_test.go b/integrations/git_test.go
index 63afc7913b..a3ba7b7aa9 100644
--- a/integrations/git_test.go
+++ b/integrations/git_test.go
@@ -17,8 +17,8 @@ import (
 	"testing"
 	"time"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/perm"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
@@ -715,7 +715,7 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headB
 		defer gitRepo.Close()
 
 		var (
-			pr1, pr2 *models.PullRequest
+			pr1, pr2 *issues_model.PullRequest
 			commit   string
 		)
 		repo, err := repo_model.GetRepositoryByOwnerAndName(ctx.Username, ctx.Reponame)
@@ -723,7 +723,7 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headB
 			return
 		}
 
-		pullNum := unittest.GetCount(t, &models.PullRequest{})
+		pullNum := unittest.GetCount(t, &issues_model.PullRequest{})
 
 		t.Run("CreateHeadBranch", doGitCreateBranch(dstPath, headBranch))
 
@@ -759,11 +759,11 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headB
 			if !assert.NoError(t, err) {
 				return
 			}
-			unittest.AssertCount(t, &models.PullRequest{}, pullNum+1)
-			pr1 = unittest.AssertExistsAndLoadBean(t, &models.PullRequest{
+			unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+1)
+			pr1 = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
 				HeadRepoID: repo.ID,
-				Flow:       models.PullRequestFlowAGit,
-			}).(*models.PullRequest)
+				Flow:       issues_model.PullRequestFlowAGit,
+			}).(*issues_model.PullRequest)
 			if !assert.NotEmpty(t, pr1) {
 				return
 			}
@@ -780,12 +780,12 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headB
 			if !assert.NoError(t, err) {
 				return
 			}
-			unittest.AssertCount(t, &models.PullRequest{}, pullNum+2)
-			pr2 = unittest.AssertExistsAndLoadBean(t, &models.PullRequest{
+			unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2)
+			pr2 = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
 				HeadRepoID: repo.ID,
 				Index:      pr1.Index + 1,
-				Flow:       models.PullRequestFlowAGit,
-			}).(*models.PullRequest)
+				Flow:       issues_model.PullRequestFlowAGit,
+			}).(*issues_model.PullRequest)
 			if !assert.NotEmpty(t, pr2) {
 				return
 			}
@@ -833,7 +833,7 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headB
 			if !assert.NoError(t, err) {
 				return
 			}
-			unittest.AssertCount(t, &models.PullRequest{}, pullNum+2)
+			unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2)
 			prMsg, err := doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index)(t)
 			if !assert.NoError(t, err) {
 				return
@@ -845,7 +845,7 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headB
 			if !assert.NoError(t, err) {
 				return
 			}
-			unittest.AssertCount(t, &models.PullRequest{}, pullNum+2)
+			unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2)
 			prMsg, err = doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr2.Index)(t)
 			if !assert.NoError(t, err) {
 				return
diff --git a/integrations/issue_test.go b/integrations/issue_test.go
index 8e04b99d5e..7d30d657f5 100644
--- a/integrations/issue_test.go
+++ b/integrations/issue_test.go
@@ -14,7 +14,8 @@ import (
 	"testing"
 	"time"
 
-	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
@@ -34,16 +35,16 @@ func getIssuesSelection(t testing.TB, htmlDoc *HTMLDoc) *goquery.Selection {
 	return issueList.Find("li").Find(".title")
 }
 
-func getIssue(t *testing.T, repoID int64, issueSelection *goquery.Selection) *models.Issue {
+func getIssue(t *testing.T, repoID int64, issueSelection *goquery.Selection) *issues_model.Issue {
 	href, exists := issueSelection.Attr("href")
 	assert.True(t, exists)
 	indexStr := href[strings.LastIndexByte(href, '/')+1:]
 	index, err := strconv.Atoi(indexStr)
 	assert.NoError(t, err, "Invalid issue href: %s", href)
-	return unittest.AssertExistsAndLoadBean(t, &models.Issue{RepoID: repoID, Index: int64(index)}).(*models.Issue)
+	return unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repoID, Index: int64(index)}).(*issues_model.Issue)
 }
 
-func assertMatch(t testing.TB, issue *models.Issue, keyword string) {
+func assertMatch(t testing.TB, issue *issues_model.Issue, keyword string) {
 	matches := strings.Contains(strings.ToLower(issue.Title), keyword) ||
 		strings.Contains(strings.ToLower(issue.Content), keyword)
 	for _, comment := range issue.Comments {
@@ -75,7 +76,7 @@ func TestViewIssuesSortByType(t *testing.T) {
 	htmlDoc := NewHTMLParser(t, resp.Body)
 	issuesSelection := getIssuesSelection(t, htmlDoc)
 	expectedNumIssues := unittest.GetCount(t,
-		&models.Issue{RepoID: repo.ID, PosterID: user.ID},
+		&issues_model.Issue{RepoID: repo.ID, PosterID: user.ID},
 		unittest.Cond("is_closed=?", false),
 		unittest.Cond("is_pull=?", false),
 	)
@@ -94,10 +95,10 @@ func TestViewIssuesKeyword(t *testing.T) {
 	defer prepareTestEnv(t)()
 
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
-	issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{
 		RepoID: repo.ID,
 		Index:  1,
-	}).(*models.Issue)
+	}).(*issues_model.Issue)
 	issues.UpdateIssueIndexer(issue)
 	time.Sleep(time.Second * 1)
 	const keyword = "first"
@@ -238,7 +239,7 @@ func TestIssueCrossReference(t *testing.T) {
 
 	// Ref from issue title
 	issueRefURL, issueRef := testIssueWithBean(t, "user2", 1, fmt.Sprintf("Title ref #%d", issueBase.Index), "Description")
-	unittest.AssertExistsAndLoadBean(t, &models.Comment{
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
 		IssueID:      issueBase.ID,
 		RefRepoID:    1,
 		RefIssueID:   issueRef.ID,
@@ -249,7 +250,7 @@ func TestIssueCrossReference(t *testing.T) {
 
 	// Edit title, neuter ref
 	testIssueChangeInfo(t, "user2", issueRefURL, "title", "Title no ref")
-	unittest.AssertExistsAndLoadBean(t, &models.Comment{
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
 		IssueID:      issueBase.ID,
 		RefRepoID:    1,
 		RefIssueID:   issueRef.ID,
@@ -260,7 +261,7 @@ func TestIssueCrossReference(t *testing.T) {
 
 	// Ref from issue content
 	issueRefURL, issueRef = testIssueWithBean(t, "user2", 1, "TitleXRef", fmt.Sprintf("Description ref #%d", issueBase.Index))
-	unittest.AssertExistsAndLoadBean(t, &models.Comment{
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
 		IssueID:      issueBase.ID,
 		RefRepoID:    1,
 		RefIssueID:   issueRef.ID,
@@ -271,7 +272,7 @@ func TestIssueCrossReference(t *testing.T) {
 
 	// Edit content, neuter ref
 	testIssueChangeInfo(t, "user2", issueRefURL, "content", "Description no ref")
-	unittest.AssertExistsAndLoadBean(t, &models.Comment{
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
 		IssueID:      issueBase.ID,
 		RefRepoID:    1,
 		RefIssueID:   issueRef.ID,
@@ -283,7 +284,7 @@ func TestIssueCrossReference(t *testing.T) {
 	// Ref from a comment
 	session := loginUser(t, "user2")
 	commentID := testIssueAddComment(t, session, issueRefURL, fmt.Sprintf("Adding ref from comment #%d", issueBase.Index), "")
-	comment := &models.Comment{
+	comment := &issues_model.Comment{
 		IssueID:      issueBase.ID,
 		RefRepoID:    1,
 		RefIssueID:   issueRef.ID,
@@ -295,7 +296,7 @@ func TestIssueCrossReference(t *testing.T) {
 
 	// Ref from a different repository
 	_, issueRef = testIssueWithBean(t, "user12", 10, "TitleXRef", fmt.Sprintf("Description ref user2/repo1#%d", issueBase.Index))
-	unittest.AssertExistsAndLoadBean(t, &models.Comment{
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
 		IssueID:      issueBase.ID,
 		RefRepoID:    10,
 		RefIssueID:   issueRef.ID,
@@ -305,13 +306,13 @@ func TestIssueCrossReference(t *testing.T) {
 	})
 }
 
-func testIssueWithBean(t *testing.T, user string, repoID int64, title, content string) (string, *models.Issue) {
+func testIssueWithBean(t *testing.T, user string, repoID int64, title, content string) (string, *issues_model.Issue) {
 	session := loginUser(t, user)
 	issueURL := testNewIssue(t, session, user, fmt.Sprintf("repo%d", repoID), title, content)
 	indexStr := issueURL[strings.LastIndexByte(issueURL, '/')+1:]
 	index, err := strconv.Atoi(indexStr)
 	assert.NoError(t, err, "Invalid issue href: %s", issueURL)
-	issue := &models.Issue{RepoID: repoID, Index: int64(index)}
+	issue := &issues_model.Issue{RepoID: repoID, Index: int64(index)}
 	unittest.AssertExistsAndLoadBean(t, issue)
 	return issueURL, issue
 }
@@ -511,10 +512,10 @@ func TestSearchIssuesWithLabels(t *testing.T) {
 func TestGetIssueInfo(t *testing.T) {
 	defer prepareTestEnv(t)()
 
-	issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 10}).(*models.Issue)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10}).(*issues_model.Issue)
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository)
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
-	assert.NoError(t, issue.LoadAttributes())
+	assert.NoError(t, issue.LoadAttributes(db.DefaultContext))
 	assert.Equal(t, int64(1019307200), int64(issue.DeadlineUnix))
 	assert.Equal(t, api.StateOpen, issue.State())
 
@@ -532,10 +533,10 @@ func TestGetIssueInfo(t *testing.T) {
 func TestUpdateIssueDeadline(t *testing.T) {
 	defer prepareTestEnv(t)()
 
-	issueBefore := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 10}).(*models.Issue)
+	issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10}).(*issues_model.Issue)
 	repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID}).(*repo_model.Repository)
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID}).(*user_model.User)
-	assert.NoError(t, issueBefore.LoadAttributes())
+	assert.NoError(t, issueBefore.LoadAttributes(db.DefaultContext))
 	assert.Equal(t, int64(1019307200), int64(issueBefore.DeadlineUnix))
 	assert.Equal(t, api.StateOpen, issueBefore.State())
 
diff --git a/integrations/pull_merge_test.go b/integrations/pull_merge_test.go
index 6c5b67caa6..de519094d4 100644
--- a/integrations/pull_merge_test.go
+++ b/integrations/pull_merge_test.go
@@ -19,6 +19,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
@@ -233,12 +234,12 @@ func TestCantMergeConflict(t *testing.T) {
 			Name:    "repo1",
 		}).(*repo_model.Repository)
 
-		pr := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{
+		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
 			HeadRepoID: repo1.ID,
 			BaseRepoID: repo1.ID,
 			HeadBranch: "conflict",
 			BaseBranch: "base",
-		}).(*models.PullRequest)
+		}).(*issues_model.PullRequest)
 
 		gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name))
 		assert.NoError(t, err)
@@ -335,12 +336,12 @@ func TestCantMergeUnrelated(t *testing.T) {
 		// Now this PR could be marked conflict - or at least a race may occur - so drop down to pure code at this point...
 		gitRepo, err := git.OpenRepository(git.DefaultContext, path)
 		assert.NoError(t, err)
-		pr := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{
+		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
 			HeadRepoID: repo1.ID,
 			BaseRepoID: repo1.ID,
 			HeadBranch: "unrelated",
 			BaseBranch: "base",
-		}).(*models.PullRequest)
+		}).(*issues_model.PullRequest)
 
 		err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "UNRELATED")
 		assert.Error(t, err, "Merge should return an error due to unrelated")
@@ -387,7 +388,7 @@ func TestConflictChecking(t *testing.T) {
 		assert.NoError(t, err)
 
 		// create Pull to merge the important-secrets branch into main branch.
-		pullIssue := &models.Issue{
+		pullIssue := &issues_model.Issue{
 			RepoID:   baseRepo.ID,
 			Title:    "PR with conflict!",
 			PosterID: user.ID,
@@ -395,26 +396,26 @@ func TestConflictChecking(t *testing.T) {
 			IsPull:   true,
 		}
 
-		pullRequest := &models.PullRequest{
+		pullRequest := &issues_model.PullRequest{
 			HeadRepoID: baseRepo.ID,
 			BaseRepoID: baseRepo.ID,
 			HeadBranch: "important-secrets",
 			BaseBranch: "main",
 			HeadRepo:   baseRepo,
 			BaseRepo:   baseRepo,
-			Type:       models.PullRequestGitea,
+			Type:       issues_model.PullRequestGitea,
 		}
 		err = pull.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil)
 		assert.NoError(t, err)
 
-		issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{Title: "PR with conflict!"}).(*models.Issue)
-		conflictingPR, err := models.GetPullRequestByIssueID(db.DefaultContext, issue.ID)
+		issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"}).(*issues_model.Issue)
+		conflictingPR, err := issues_model.GetPullRequestByIssueID(db.DefaultContext, issue.ID)
 		assert.NoError(t, err)
 
 		// Ensure conflictedFiles is populated.
 		assert.Equal(t, 1, len(conflictingPR.ConflictedFiles))
 		// Check if status is correct.
-		assert.Equal(t, models.PullRequestStatusConflict, conflictingPR.Status)
+		assert.Equal(t, issues_model.PullRequestStatusConflict, conflictingPR.Status)
 		// Ensure that mergeable returns false
 		assert.False(t, conflictingPR.Mergeable())
 	})
diff --git a/integrations/pull_update_test.go b/integrations/pull_update_test.go
index f11eacf144..47ada91e1a 100644
--- a/integrations/pull_update_test.go
+++ b/integrations/pull_update_test.go
@@ -12,6 +12,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
@@ -78,7 +79,7 @@ func TestAPIPullUpdateByRebase(t *testing.T) {
 	})
 }
 
-func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *models.PullRequest {
+func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_model.PullRequest {
 	baseRepo, err := repo_service.CreateRepository(actor, actor, models.CreateRepoOptions{
 		Name:        "repo-pr-update",
 		Description: "repo-tmp-pr-update description",
@@ -146,27 +147,27 @@ func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *models.Pul
 	assert.NoError(t, err)
 
 	// create Pull
-	pullIssue := &models.Issue{
+	pullIssue := &issues_model.Issue{
 		RepoID:   baseRepo.ID,
 		Title:    "Test Pull -to-update-",
 		PosterID: actor.ID,
 		Poster:   actor,
 		IsPull:   true,
 	}
-	pullRequest := &models.PullRequest{
+	pullRequest := &issues_model.PullRequest{
 		HeadRepoID: headRepo.ID,
 		BaseRepoID: baseRepo.ID,
 		HeadBranch: "newBranch",
 		BaseBranch: "master",
 		HeadRepo:   headRepo,
 		BaseRepo:   baseRepo,
-		Type:       models.PullRequestGitea,
+		Type:       issues_model.PullRequestGitea,
 	}
 	err = pull_service.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil)
 	assert.NoError(t, err)
 
-	issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{Title: "Test Pull -to-update-"}).(*models.Issue)
-	pr, err := models.GetPullRequestByIssueID(db.DefaultContext, issue.ID)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "Test Pull -to-update-"}).(*issues_model.Issue)
+	pr, err := issues_model.GetPullRequestByIssueID(db.DefaultContext, issue.ID)
 	assert.NoError(t, err)
 
 	return pr
diff --git a/integrations/user_test.go b/integrations/user_test.go
index d0523d8b3a..6a3d30472d 100644
--- a/integrations/user_test.go
+++ b/integrations/user_test.go
@@ -8,7 +8,7 @@ import (
 	"net/http"
 	"testing"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
@@ -237,8 +237,8 @@ func TestListStopWatches(t *testing.T) {
 	resp := session.MakeRequest(t, req, http.StatusOK)
 	var apiWatches []*api.StopWatch
 	DecodeJSON(t, resp, &apiWatches)
-	stopwatch := unittest.AssertExistsAndLoadBean(t, &models.Stopwatch{UserID: owner.ID}).(*models.Stopwatch)
-	issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: stopwatch.IssueID}).(*models.Issue)
+	stopwatch := unittest.AssertExistsAndLoadBean(t, &issues_model.Stopwatch{UserID: owner.ID}).(*issues_model.Stopwatch)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: stopwatch.IssueID}).(*issues_model.Issue)
 	if assert.Len(t, apiWatches, 1) {
 		assert.EqualValues(t, stopwatch.CreatedUnix.AsTime().Unix(), apiWatches[0].Created.Unix())
 		assert.EqualValues(t, issue.Index, apiWatches[0].IssueIndex)
diff --git a/models/action.go b/models/action.go
index 882bc59d8f..951328070d 100644
--- a/models/action.go
+++ b/models/action.go
@@ -15,6 +15,7 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -76,7 +77,7 @@ type Action struct {
 	RepoID      int64                  `xorm:"INDEX"`
 	Repo        *repo_model.Repository `xorm:"-"`
 	CommentID   int64                  `xorm:"INDEX"`
-	Comment     *Comment               `xorm:"-"`
+	Comment     *issues_model.Comment  `xorm:"-"`
 	IsDeleted   bool                   `xorm:"INDEX NOT NULL DEFAULT false"`
 	RefName     string
 	IsPrivate   bool               `xorm:"INDEX NOT NULL DEFAULT false"`
@@ -223,7 +224,7 @@ func (a *Action) getCommentLink(ctx context.Context) string {
 		return "#"
 	}
 	if a.Comment == nil && a.CommentID != 0 {
-		a.Comment, _ = GetCommentByID(ctx, a.CommentID)
+		a.Comment, _ = issues_model.GetCommentByID(ctx, a.CommentID)
 	}
 	if a.Comment != nil {
 		return a.Comment.HTMLURL()
@@ -238,7 +239,7 @@ func (a *Action) getCommentLink(ctx context.Context) string {
 		return "#"
 	}
 
-	issue, err := getIssueByID(ctx, issueID)
+	issue, err := issues_model.GetIssueByID(ctx, issueID)
 	if err != nil {
 		return "#"
 	}
@@ -295,7 +296,7 @@ func (a *Action) GetIssueInfos() []string {
 // with the action.
 func (a *Action) GetIssueTitle() string {
 	index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64)
-	issue, err := GetIssueByIndex(a.RepoID, index)
+	issue, err := issues_model.GetIssueByIndex(a.RepoID, index)
 	if err != nil {
 		log.Error("GetIssueByIndex: %v", err)
 		return "500 when get issue"
@@ -307,7 +308,7 @@ func (a *Action) GetIssueTitle() string {
 // this action.
 func (a *Action) GetIssueContent() string {
 	index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64)
-	issue, err := GetIssueByIndex(a.RepoID, index)
+	issue, err := issues_model.GetIssueByIndex(a.RepoID, index)
 	if err != nil {
 		log.Error("GetIssueByIndex: %v", err)
 		return "500 when get issue"
@@ -572,3 +573,20 @@ func NotifyWatchersActions(acts []*Action) error {
 	}
 	return committer.Commit()
 }
+
+// DeleteIssueActions delete all actions related with issueID
+func DeleteIssueActions(ctx context.Context, repoID, issueID int64) error {
+	// delete actions assigned to this issue
+	subQuery := builder.Select("`id`").
+		From("`comment`").
+		Where(builder.Eq{"`issue_id`": issueID})
+	if _, err := db.GetEngine(ctx).In("comment_id", subQuery).Delete(&Action{}); err != nil {
+		return err
+	}
+
+	_, err := db.GetEngine(ctx).Table("action").Where("repo_id = ?", repoID).
+		In("op_type", ActionCreateIssue, ActionCreatePullRequest).
+		Where("content LIKE ?", strconv.FormatInt(issueID, 10)+"|%").
+		Delete(&Action{})
+	return err
+}
diff --git a/models/action_test.go b/models/action_test.go
index fb8a6c2686..2d46bd3e80 100644
--- a/models/action_test.go
+++ b/models/action_test.go
@@ -228,3 +228,46 @@ func TestGetFeedsCorrupted(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Len(t, actions, 0)
 }
+
+func TestConsistencyUpdateAction(t *testing.T) {
+	if !setting.Database.UseSQLite3 {
+		t.Skip("Test is only for SQLite database.")
+	}
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	id := 8
+	unittest.AssertExistsAndLoadBean(t, &Action{
+		ID: int64(id),
+	})
+	_, err := db.GetEngine(db.DefaultContext).Exec(`UPDATE action SET created_unix = "" WHERE id = ?`, id)
+	assert.NoError(t, err)
+	actions := make([]*Action, 0, 1)
+	//
+	// XORM returns an error when created_unix is a string
+	//
+	err = db.GetEngine(db.DefaultContext).Where("id = ?", id).Find(&actions)
+	if assert.Error(t, err) {
+		assert.Contains(t, err.Error(), "type string to a int64: invalid syntax")
+	}
+	//
+	// Get rid of incorrectly set created_unix
+	//
+	count, err := CountActionCreatedUnixString()
+	assert.NoError(t, err)
+	assert.EqualValues(t, 1, count)
+	count, err = FixActionCreatedUnixString()
+	assert.NoError(t, err)
+	assert.EqualValues(t, 1, count)
+
+	count, err = CountActionCreatedUnixString()
+	assert.NoError(t, err)
+	assert.EqualValues(t, 0, count)
+	count, err = FixActionCreatedUnixString()
+	assert.NoError(t, err)
+	assert.EqualValues(t, 0, count)
+
+	//
+	// XORM must be happy now
+	//
+	assert.NoError(t, db.GetEngine(db.DefaultContext).Where("id = ?", id).Find(&actions))
+	unittest.CheckConsistencyFor(t, &Action{})
+}
diff --git a/models/branch.go b/models/branch.go
deleted file mode 100644
index 3d6e7d82e2..0000000000
--- a/models/branch.go
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright 2022 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-package models
-
-import (
-	"context"
-
-	"code.gitea.io/gitea/models/db"
-	git_model "code.gitea.io/gitea/models/git"
-	"code.gitea.io/gitea/modules/log"
-)
-
-// HasEnoughApprovals returns true if pr has enough granted approvals.
-func HasEnoughApprovals(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool {
-	if protectBranch.RequiredApprovals == 0 {
-		return true
-	}
-	return GetGrantedApprovalsCount(ctx, protectBranch, pr) >= protectBranch.RequiredApprovals
-}
-
-// GetGrantedApprovalsCount returns the number of granted approvals for pr. A granted approval must be authored by a user in an approval whitelist.
-func GetGrantedApprovalsCount(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) int64 {
-	sess := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID).
-		And("type = ?", ReviewTypeApprove).
-		And("official = ?", true).
-		And("dismissed = ?", false)
-	if protectBranch.DismissStaleApprovals {
-		sess = sess.And("stale = ?", false)
-	}
-	approvals, err := sess.Count(new(Review))
-	if err != nil {
-		log.Error("GetGrantedApprovalsCount: %v", err)
-		return 0
-	}
-
-	return approvals
-}
-
-// MergeBlockedByRejectedReview returns true if merge is blocked by rejected reviews
-func MergeBlockedByRejectedReview(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool {
-	if !protectBranch.BlockOnRejectedReviews {
-		return false
-	}
-	rejectExist, err := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID).
-		And("type = ?", ReviewTypeReject).
-		And("official = ?", true).
-		And("dismissed = ?", false).
-		Exist(new(Review))
-	if err != nil {
-		log.Error("MergeBlockedByRejectedReview: %v", err)
-		return true
-	}
-
-	return rejectExist
-}
-
-// MergeBlockedByOfficialReviewRequests block merge because of some review request to official reviewer
-// of from official review
-func MergeBlockedByOfficialReviewRequests(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool {
-	if !protectBranch.BlockOnOfficialReviewRequests {
-		return false
-	}
-	has, err := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID).
-		And("type = ?", ReviewTypeRequest).
-		And("official = ?", true).
-		Exist(new(Review))
-	if err != nil {
-		log.Error("MergeBlockedByOfficialReviewRequests: %v", err)
-		return true
-	}
-
-	return has
-}
-
-// MergeBlockedByOutdatedBranch returns true if merge is blocked by an outdated head branch
-func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool {
-	return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0
-}
diff --git a/models/consistency.go b/models/consistency.go
index e817b69176..18ed9195fc 100644
--- a/models/consistency.go
+++ b/models/consistency.go
@@ -5,7 +5,6 @@
 package models
 
 import (
-	admin_model "code.gitea.io/gitea/models/admin"
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
@@ -14,151 +13,6 @@ import (
 	"xorm.io/builder"
 )
 
-// CountOrphanedLabels return count of labels witch are broken and not accessible via ui anymore
-func CountOrphanedLabels() (int64, error) {
-	noref, err := db.GetEngine(db.DefaultContext).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Count("label.id")
-	if err != nil {
-		return 0, err
-	}
-
-	norepo, err := db.GetEngine(db.DefaultContext).Table("label").
-		Where(builder.And(
-			builder.Gt{"repo_id": 0},
-			builder.NotIn("repo_id", builder.Select("id").From("repository")),
-		)).
-		Count()
-	if err != nil {
-		return 0, err
-	}
-
-	noorg, err := db.GetEngine(db.DefaultContext).Table("label").
-		Where(builder.And(
-			builder.Gt{"org_id": 0},
-			builder.NotIn("org_id", builder.Select("id").From("user")),
-		)).
-		Count()
-	if err != nil {
-		return 0, err
-	}
-
-	return noref + norepo + noorg, nil
-}
-
-// DeleteOrphanedLabels delete labels witch are broken and not accessible via ui anymore
-func DeleteOrphanedLabels() error {
-	// delete labels with no reference
-	if _, err := db.GetEngine(db.DefaultContext).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Delete(new(Label)); err != nil {
-		return err
-	}
-
-	// delete labels with none existing repos
-	if _, err := db.GetEngine(db.DefaultContext).
-		Where(builder.And(
-			builder.Gt{"repo_id": 0},
-			builder.NotIn("repo_id", builder.Select("id").From("repository")),
-		)).
-		Delete(Label{}); err != nil {
-		return err
-	}
-
-	// delete labels with none existing orgs
-	if _, err := db.GetEngine(db.DefaultContext).
-		Where(builder.And(
-			builder.Gt{"org_id": 0},
-			builder.NotIn("org_id", builder.Select("id").From("user")),
-		)).
-		Delete(Label{}); err != nil {
-		return err
-	}
-
-	return nil
-}
-
-// CountOrphanedIssueLabels return count of IssueLabels witch have no label behind anymore
-func CountOrphanedIssueLabels() (int64, error) {
-	return db.GetEngine(db.DefaultContext).Table("issue_label").
-		NotIn("label_id", builder.Select("id").From("label")).
-		Count()
-}
-
-// DeleteOrphanedIssueLabels delete IssueLabels witch have no label behind anymore
-func DeleteOrphanedIssueLabels() error {
-	_, err := db.GetEngine(db.DefaultContext).
-		NotIn("label_id", builder.Select("id").From("label")).
-		Delete(IssueLabel{})
-	return err
-}
-
-// CountOrphanedIssues count issues without a repo
-func CountOrphanedIssues() (int64, error) {
-	return db.GetEngine(db.DefaultContext).Table("issue").
-		Join("LEFT", "repository", "issue.repo_id=repository.id").
-		Where(builder.IsNull{"repository.id"}).
-		Select("COUNT(`issue`.`id`)").
-		Count()
-}
-
-// DeleteOrphanedIssues delete issues without a repo
-func DeleteOrphanedIssues() error {
-	ctx, committer, err := db.TxContext()
-	if err != nil {
-		return err
-	}
-	defer committer.Close()
-
-	var ids []int64
-
-	if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id").
-		Join("LEFT", "repository", "issue.repo_id=repository.id").
-		Where(builder.IsNull{"repository.id"}).GroupBy("issue.repo_id").
-		Find(&ids); err != nil {
-		return err
-	}
-
-	var attachmentPaths []string
-	for i := range ids {
-		paths, err := deleteIssuesByRepoID(ctx, ids[i])
-		if err != nil {
-			return err
-		}
-		attachmentPaths = append(attachmentPaths, paths...)
-	}
-
-	if err := committer.Commit(); err != nil {
-		return err
-	}
-	committer.Close()
-
-	// Remove issue attachment files.
-	for i := range attachmentPaths {
-		admin_model.RemoveAllWithNotice(db.DefaultContext, "Delete issue attachment", attachmentPaths[i])
-	}
-	return nil
-}
-
-// CountOrphanedObjects count subjects with have no existing refobject anymore
-func CountOrphanedObjects(subject, refobject, joinCond string) (int64, error) {
-	return db.GetEngine(db.DefaultContext).Table("`"+subject+"`").
-		Join("LEFT", "`"+refobject+"`", joinCond).
-		Where(builder.IsNull{"`" + refobject + "`.id"}).
-		Select("COUNT(`" + subject + "`.`id`)").
-		Count()
-}
-
-// DeleteOrphanedObjects delete subjects with have no existing refobject anymore
-func DeleteOrphanedObjects(subject, refobject, joinCond string) error {
-	subQuery := builder.Select("`"+subject+"`.id").
-		From("`"+subject+"`").
-		Join("LEFT", "`"+refobject+"`", joinCond).
-		Where(builder.IsNull{"`" + refobject + "`.id"})
-	sql, args, err := builder.Delete(builder.In("id", subQuery)).From("`" + subject + "`").ToSQL()
-	if err != nil {
-		return err
-	}
-	_, err = db.GetEngine(db.DefaultContext).Exec(append([]interface{}{sql}, args...)...)
-	return err
-}
-
 // CountNullArchivedRepository counts the number of repositories with is_archived is null
 func CountNullArchivedRepository() (int64, error) {
 	return db.GetEngine(db.DefaultContext).Where(builder.IsNull{"is_archived"}).Count(new(repo_model.Repository))
@@ -181,74 +35,6 @@ func FixWrongUserType() (int64, error) {
 	return db.GetEngine(db.DefaultContext).Where(builder.Eq{"type": 0}.And(builder.Neq{"num_teams": 0})).Cols("type").NoAutoTime().Update(&user_model.User{Type: 1})
 }
 
-// CountCommentTypeLabelWithEmptyLabel count label comments with empty label
-func CountCommentTypeLabelWithEmptyLabel() (int64, error) {
-	return db.GetEngine(db.DefaultContext).Where(builder.Eq{"type": CommentTypeLabel, "label_id": 0}).Count(new(Comment))
-}
-
-// FixCommentTypeLabelWithEmptyLabel count label comments with empty label
-func FixCommentTypeLabelWithEmptyLabel() (int64, error) {
-	return db.GetEngine(db.DefaultContext).Where(builder.Eq{"type": CommentTypeLabel, "label_id": 0}).Delete(new(Comment))
-}
-
-// CountCommentTypeLabelWithOutsideLabels count label comments with outside label
-func CountCommentTypeLabelWithOutsideLabels() (int64, error) {
-	return db.GetEngine(db.DefaultContext).Where("comment.type = ? AND ((label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id))", CommentTypeLabel).
-		Table("comment").
-		Join("inner", "label", "label.id = comment.label_id").
-		Join("inner", "issue", "issue.id = comment.issue_id ").
-		Join("inner", "repository", "issue.repo_id = repository.id").
-		Count(new(Comment))
-}
-
-// FixCommentTypeLabelWithOutsideLabels count label comments with outside label
-func FixCommentTypeLabelWithOutsideLabels() (int64, error) {
-	res, err := db.GetEngine(db.DefaultContext).Exec(`DELETE FROM comment WHERE comment.id IN (
-		SELECT il_too.id FROM (
-			SELECT com.id
-				FROM comment AS com
-					INNER JOIN label ON com.label_id = label.id
-					INNER JOIN issue on issue.id = com.issue_id
-					INNER JOIN repository ON issue.repo_id = repository.id
-				WHERE
-					com.type = ? AND ((label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id))
-	) AS il_too)`, CommentTypeLabel)
-	if err != nil {
-		return 0, err
-	}
-
-	return res.RowsAffected()
-}
-
-// CountIssueLabelWithOutsideLabels count label comments with outside label
-func CountIssueLabelWithOutsideLabels() (int64, error) {
-	return db.GetEngine(db.DefaultContext).Where(builder.Expr("(label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id)")).
-		Table("issue_label").
-		Join("inner", "label", "issue_label.label_id = label.id ").
-		Join("inner", "issue", "issue.id = issue_label.issue_id ").
-		Join("inner", "repository", "issue.repo_id = repository.id").
-		Count(new(IssueLabel))
-}
-
-// FixIssueLabelWithOutsideLabels fix label comments with outside label
-func FixIssueLabelWithOutsideLabels() (int64, error) {
-	res, err := db.GetEngine(db.DefaultContext).Exec(`DELETE FROM issue_label WHERE issue_label.id IN (
-		SELECT il_too.id FROM (
-			SELECT il_too_too.id
-				FROM issue_label AS il_too_too
-					INNER JOIN label ON il_too_too.label_id = label.id
-					INNER JOIN issue on issue.id = il_too_too.issue_id
-					INNER JOIN repository on repository.id = issue.repo_id
-				WHERE
-					(label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id)
-	) AS il_too )`)
-	if err != nil {
-		return 0, err
-	}
-
-	return res.RowsAffected()
-}
-
 // CountActionCreatedUnixString count actions where created_unix is an empty string
 func CountActionCreatedUnixString() (int64, error) {
 	if setting.Database.UseSQLite3 {
diff --git a/models/consistency_test.go b/models/consistency_test.go
deleted file mode 100644
index fb946b2fb7..0000000000
--- a/models/consistency_test.go
+++ /dev/null
@@ -1,149 +0,0 @@
-// Copyright 2021 Gitea. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-package models
-
-import (
-	"testing"
-
-	"code.gitea.io/gitea/models/db"
-	issues_model "code.gitea.io/gitea/models/issues"
-	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/models/unittest"
-	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/timeutil"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func TestDeleteOrphanedObjects(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-
-	countBefore, err := db.GetEngine(db.DefaultContext).Count(&PullRequest{})
-	assert.NoError(t, err)
-
-	_, err = db.GetEngine(db.DefaultContext).Insert(&PullRequest{IssueID: 1000}, &PullRequest{IssueID: 1001}, &PullRequest{IssueID: 1003})
-	assert.NoError(t, err)
-
-	orphaned, err := CountOrphanedObjects("pull_request", "issue", "pull_request.issue_id=issue.id")
-	assert.NoError(t, err)
-	assert.EqualValues(t, 3, orphaned)
-
-	err = DeleteOrphanedObjects("pull_request", "issue", "pull_request.issue_id=issue.id")
-	assert.NoError(t, err)
-
-	countAfter, err := db.GetEngine(db.DefaultContext).Count(&PullRequest{})
-	assert.NoError(t, err)
-	assert.EqualValues(t, countBefore, countAfter)
-}
-
-func TestNewMilestone(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	milestone := &issues_model.Milestone{
-		RepoID:  1,
-		Name:    "milestoneName",
-		Content: "milestoneContent",
-	}
-
-	assert.NoError(t, issues_model.NewMilestone(milestone))
-	unittest.AssertExistsAndLoadBean(t, milestone)
-	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &issues_model.Milestone{})
-}
-
-func TestChangeMilestoneStatus(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone)
-
-	assert.NoError(t, issues_model.ChangeMilestoneStatus(milestone, true))
-	unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}, "is_closed=1")
-	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &issues_model.Milestone{})
-
-	assert.NoError(t, issues_model.ChangeMilestoneStatus(milestone, false))
-	unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}, "is_closed=0")
-	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &issues_model.Milestone{})
-}
-
-func TestDeleteMilestoneByRepoID(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	assert.NoError(t, issues_model.DeleteMilestoneByRepoID(1, 1))
-	unittest.AssertNotExistsBean(t, &issues_model.Milestone{ID: 1})
-	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: 1})
-
-	assert.NoError(t, issues_model.DeleteMilestoneByRepoID(unittest.NonexistentID, unittest.NonexistentID))
-}
-
-func TestUpdateMilestone(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-
-	milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone)
-	milestone.Name = " newMilestoneName  "
-	milestone.Content = "newMilestoneContent"
-	assert.NoError(t, issues_model.UpdateMilestone(milestone, milestone.IsClosed))
-	milestone = unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone)
-	assert.EqualValues(t, "newMilestoneName", milestone.Name)
-	unittest.CheckConsistencyFor(t, &issues_model.Milestone{})
-}
-
-func TestUpdateMilestoneCounters(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	issue := unittest.AssertExistsAndLoadBean(t, &Issue{MilestoneID: 1},
-		"is_closed=0").(*Issue)
-
-	issue.IsClosed = true
-	issue.ClosedUnix = timeutil.TimeStampNow()
-	_, err := db.GetEngine(db.DefaultContext).ID(issue.ID).Cols("is_closed", "closed_unix").Update(issue)
-	assert.NoError(t, err)
-	assert.NoError(t, issues_model.UpdateMilestoneCounters(db.DefaultContext, issue.MilestoneID))
-	unittest.CheckConsistencyFor(t, &issues_model.Milestone{})
-
-	issue.IsClosed = false
-	issue.ClosedUnix = 0
-	_, err = db.GetEngine(db.DefaultContext).ID(issue.ID).Cols("is_closed", "closed_unix").Update(issue)
-	assert.NoError(t, err)
-	assert.NoError(t, issues_model.UpdateMilestoneCounters(db.DefaultContext, issue.MilestoneID))
-	unittest.CheckConsistencyFor(t, &issues_model.Milestone{})
-}
-
-func TestConsistencyUpdateAction(t *testing.T) {
-	if !setting.Database.UseSQLite3 {
-		t.Skip("Test is only for SQLite database.")
-	}
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	id := 8
-	unittest.AssertExistsAndLoadBean(t, &Action{
-		ID: int64(id),
-	})
-	_, err := db.GetEngine(db.DefaultContext).Exec(`UPDATE action SET created_unix = "" WHERE id = ?`, id)
-	assert.NoError(t, err)
-	actions := make([]*Action, 0, 1)
-	//
-	// XORM returns an error when created_unix is a string
-	//
-	err = db.GetEngine(db.DefaultContext).Where("id = ?", id).Find(&actions)
-	if assert.Error(t, err) {
-		assert.Contains(t, err.Error(), "type string to a int64: invalid syntax")
-	}
-	//
-	// Get rid of incorrectly set created_unix
-	//
-	count, err := CountActionCreatedUnixString()
-	assert.NoError(t, err)
-	assert.EqualValues(t, 1, count)
-	count, err = FixActionCreatedUnixString()
-	assert.NoError(t, err)
-	assert.EqualValues(t, 1, count)
-
-	count, err = CountActionCreatedUnixString()
-	assert.NoError(t, err)
-	assert.EqualValues(t, 0, count)
-	count, err = FixActionCreatedUnixString()
-	assert.NoError(t, err)
-	assert.EqualValues(t, 0, count)
-
-	//
-	// XORM must be happy now
-	//
-	assert.NoError(t, db.GetEngine(db.DefaultContext).Where("id = ?", id).Find(&actions))
-	unittest.CheckConsistencyFor(t, &Action{})
-}
diff --git a/models/db/consistency.go b/models/db/consistency.go
new file mode 100644
index 0000000000..7addb174c4
--- /dev/null
+++ b/models/db/consistency.go
@@ -0,0 +1,27 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package db
+
+import "xorm.io/builder"
+
+// CountOrphanedObjects count subjects with have no existing refobject anymore
+func CountOrphanedObjects(subject, refobject, joinCond string) (int64, error) {
+	return GetEngine(DefaultContext).Table("`"+subject+"`").
+		Join("LEFT", "`"+refobject+"`", joinCond).
+		Where(builder.IsNull{"`" + refobject + "`.id"}).
+		Select("COUNT(`" + subject + "`.`id`)").
+		Count()
+}
+
+// DeleteOrphanedObjects delete subjects with have no existing refobject anymore
+func DeleteOrphanedObjects(subject, refobject, joinCond string) error {
+	subQuery := builder.Select("`"+subject+"`.id").
+		From("`"+subject+"`").
+		Join("LEFT", "`"+refobject+"`", joinCond).
+		Where(builder.IsNull{"`" + refobject + "`.id"})
+	b := builder.Delete(builder.In("id", subQuery)).From("`" + subject + "`")
+	_, err := GetEngine(DefaultContext).Exec(b)
+	return err
+}
diff --git a/models/db/index.go b/models/db/index.go
index 8598de9498..9b164db1fa 100644
--- a/models/db/index.go
+++ b/models/db/index.go
@@ -20,21 +20,21 @@ type ResourceIndex struct {
 }
 
 // UpsertResourceIndex the function will not return until it acquires the lock or receives an error.
-func UpsertResourceIndex(e Engine, tableName string, groupID int64) (err error) {
+func UpsertResourceIndex(ctx context.Context, tableName string, groupID int64) (err error) {
 	// An atomic UPSERT operation (INSERT/UPDATE) is the only operation
 	// that ensures that the key is actually locked.
 	switch {
 	case setting.Database.UseSQLite3 || setting.Database.UsePostgreSQL:
-		_, err = e.Exec(fmt.Sprintf("INSERT INTO %s (group_id, max_index) "+
+		_, err = Exec(ctx, fmt.Sprintf("INSERT INTO %s (group_id, max_index) "+
 			"VALUES (?,1) ON CONFLICT (group_id) DO UPDATE SET max_index = %s.max_index+1",
 			tableName, tableName), groupID)
 	case setting.Database.UseMySQL:
-		_, err = e.Exec(fmt.Sprintf("INSERT INTO %s (group_id, max_index) "+
+		_, err = Exec(ctx, fmt.Sprintf("INSERT INTO %s (group_id, max_index) "+
 			"VALUES (?,1) ON DUPLICATE KEY UPDATE max_index = max_index+1", tableName),
 			groupID)
 	case setting.Database.UseMSSQL:
 		// https://weblogs.sqlteam.com/dang/2009/01/31/upsert-race-condition-with-merge/
-		_, err = e.Exec(fmt.Sprintf("MERGE %s WITH (HOLDLOCK) as target "+
+		_, err = Exec(ctx, fmt.Sprintf("MERGE %s WITH (HOLDLOCK) as target "+
 			"USING (SELECT ? AS group_id) AS src "+
 			"ON src.group_id = target.group_id "+
 			"WHEN MATCHED THEN UPDATE SET target.max_index = target.max_index+1 "+
@@ -82,30 +82,29 @@ func DeleteResouceIndex(ctx context.Context, tableName string, groupID int64) er
 
 // getNextResourceIndex return the next index
 func getNextResourceIndex(tableName string, groupID int64) (int64, error) {
-	sess := x.NewSession()
-	defer sess.Close()
-	if err := sess.Begin(); err != nil {
-		return 0, err
-	}
-	var preIdx int64
-	_, err := sess.SQL(fmt.Sprintf("SELECT max_index FROM %s WHERE group_id = ?", tableName), groupID).Get(&preIdx)
+	ctx, commiter, err := TxContext()
 	if err != nil {
 		return 0, err
 	}
+	defer commiter.Close()
+	var preIdx int64
+	if _, err := GetEngine(ctx).SQL(fmt.Sprintf("SELECT max_index FROM %s WHERE group_id = ?", tableName), groupID).Get(&preIdx); err != nil {
+		return 0, err
+	}
 
-	if err := UpsertResourceIndex(sess, tableName, groupID); err != nil {
+	if err := UpsertResourceIndex(ctx, tableName, groupID); err != nil {
 		return 0, err
 	}
 
 	var curIdx int64
-	has, err := sess.SQL(fmt.Sprintf("SELECT max_index FROM %s WHERE group_id = ? AND max_index=?", tableName), groupID, preIdx+1).Get(&curIdx)
+	has, err := GetEngine(ctx).SQL(fmt.Sprintf("SELECT max_index FROM %s WHERE group_id = ? AND max_index=?", tableName), groupID, preIdx+1).Get(&curIdx)
 	if err != nil {
 		return 0, err
 	}
 	if !has {
 		return 0, ErrResouceOutdated
 	}
-	if err := sess.Commit(); err != nil {
+	if err := commiter.Commit(); err != nil {
 		return 0, err
 	}
 	return curIdx, nil
diff --git a/models/db/list_options.go b/models/db/list_options.go
index 843e73c8ae..d1d52b6667 100644
--- a/models/db/list_options.go
+++ b/models/db/list_options.go
@@ -10,6 +10,11 @@ import (
 	"xorm.io/xorm"
 )
 
+const (
+	// DefaultMaxInSize represents default variables number on IN () in SQL
+	DefaultMaxInSize = 50
+)
+
 // Paginator is the base for different ListOptions types
 type Paginator interface {
 	GetSkipTake() (skip, take int)
diff --git a/models/error.go b/models/error.go
index 16ae52fc43..3c617904f8 100644
--- a/models/error.go
+++ b/models/error.go
@@ -405,22 +405,6 @@ func (err ErrFilePathProtected) Error() string {
 	return fmt.Sprintf("path is protected and can not be changed [path: %s]", err.Path)
 }
 
-// ErrUserDoesNotHaveAccessToRepo represets an error where the user doesn't has access to a given repo.
-type ErrUserDoesNotHaveAccessToRepo struct {
-	UserID   int64
-	RepoName string
-}
-
-// IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrRepoFileAlreadyExists.
-func IsErrUserDoesNotHaveAccessToRepo(err error) bool {
-	_, ok := err.(ErrUserDoesNotHaveAccessToRepo)
-	return ok
-}
-
-func (err ErrUserDoesNotHaveAccessToRepo) Error() string {
-	return fmt.Sprintf("user doesn't have access to repo [user_id: %d, repo_name: %s]", err.UserID, err.RepoName)
-}
-
 // __________                             .__
 // \______   \____________    ____   ____ |  |__
 //  |    |  _/\_  __ \__  \  /    \_/ ___\|  |  \
@@ -580,162 +564,6 @@ func (err ErrSHAOrCommitIDNotProvided) Error() string {
 	return "a SHA or commit ID must be proved when updating a file"
 }
 
-// .___
-// |   | ______ ________ __   ____
-// |   |/  ___//  ___/  |  \_/ __ \
-// |   |\___ \ \___ \|  |  /\  ___/
-// |___/____  >____  >____/  \___  >
-//          \/     \/            \/
-
-// ErrIssueNotExist represents a "IssueNotExist" kind of error.
-type ErrIssueNotExist struct {
-	ID     int64
-	RepoID int64
-	Index  int64
-}
-
-// IsErrIssueNotExist checks if an error is a ErrIssueNotExist.
-func IsErrIssueNotExist(err error) bool {
-	_, ok := err.(ErrIssueNotExist)
-	return ok
-}
-
-func (err ErrIssueNotExist) Error() string {
-	return fmt.Sprintf("issue does not exist [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index)
-}
-
-// ErrIssueIsClosed represents a "IssueIsClosed" kind of error.
-type ErrIssueIsClosed struct {
-	ID     int64
-	RepoID int64
-	Index  int64
-}
-
-// IsErrIssueIsClosed checks if an error is a ErrIssueNotExist.
-func IsErrIssueIsClosed(err error) bool {
-	_, ok := err.(ErrIssueIsClosed)
-	return ok
-}
-
-func (err ErrIssueIsClosed) Error() string {
-	return fmt.Sprintf("issue is closed [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index)
-}
-
-// ErrNewIssueInsert is used when the INSERT statement in newIssue fails
-type ErrNewIssueInsert struct {
-	OriginalError error
-}
-
-// IsErrNewIssueInsert checks if an error is a ErrNewIssueInsert.
-func IsErrNewIssueInsert(err error) bool {
-	_, ok := err.(ErrNewIssueInsert)
-	return ok
-}
-
-func (err ErrNewIssueInsert) Error() string {
-	return err.OriginalError.Error()
-}
-
-// ErrIssueWasClosed is used when close a closed issue
-type ErrIssueWasClosed struct {
-	ID    int64
-	Index int64
-}
-
-// IsErrIssueWasClosed checks if an error is a ErrIssueWasClosed.
-func IsErrIssueWasClosed(err error) bool {
-	_, ok := err.(ErrIssueWasClosed)
-	return ok
-}
-
-func (err ErrIssueWasClosed) Error() string {
-	return fmt.Sprintf("Issue [%d] %d was already closed", err.ID, err.Index)
-}
-
-// ErrPullWasClosed is used close a closed pull request
-type ErrPullWasClosed struct {
-	ID    int64
-	Index int64
-}
-
-// IsErrPullWasClosed checks if an error is a ErrErrPullWasClosed.
-func IsErrPullWasClosed(err error) bool {
-	_, ok := err.(ErrPullWasClosed)
-	return ok
-}
-
-func (err ErrPullWasClosed) Error() string {
-	return fmt.Sprintf("Pull request [%d] %d was already closed", err.ID, err.Index)
-}
-
-// __________      .__  .__ __________                                     __
-// \______   \__ __|  | |  |\______   \ ____  ________ __   ____   _______/  |_
-//  |     ___/  |  \  | |  | |       _// __ \/ ____/  |  \_/ __ \ /  ___/\   __\
-//  |    |   |  |  /  |_|  |_|    |   \  ___< <_|  |  |  /\  ___/ \___ \  |  |
-//  |____|   |____/|____/____/____|_  /\___  >__   |____/  \___  >____  > |__|
-//                                  \/     \/   |__|           \/     \/
-
-// ErrPullRequestNotExist represents a "PullRequestNotExist" kind of error.
-type ErrPullRequestNotExist struct {
-	ID         int64
-	IssueID    int64
-	HeadRepoID int64
-	BaseRepoID int64
-	HeadBranch string
-	BaseBranch string
-}
-
-// IsErrPullRequestNotExist checks if an error is a ErrPullRequestNotExist.
-func IsErrPullRequestNotExist(err error) bool {
-	_, ok := err.(ErrPullRequestNotExist)
-	return ok
-}
-
-func (err ErrPullRequestNotExist) Error() string {
-	return fmt.Sprintf("pull request does not exist [id: %d, issue_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]",
-		err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch)
-}
-
-// ErrPullRequestAlreadyExists represents a "PullRequestAlreadyExists"-error
-type ErrPullRequestAlreadyExists struct {
-	ID         int64
-	IssueID    int64
-	HeadRepoID int64
-	BaseRepoID int64
-	HeadBranch string
-	BaseBranch string
-}
-
-// IsErrPullRequestAlreadyExists checks if an error is a ErrPullRequestAlreadyExists.
-func IsErrPullRequestAlreadyExists(err error) bool {
-	_, ok := err.(ErrPullRequestAlreadyExists)
-	return ok
-}
-
-// Error does pretty-printing :D
-func (err ErrPullRequestAlreadyExists) Error() string {
-	return fmt.Sprintf("pull request already exists for these targets [id: %d, issue_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]",
-		err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch)
-}
-
-// ErrPullRequestHeadRepoMissing represents a "ErrPullRequestHeadRepoMissing" error
-type ErrPullRequestHeadRepoMissing struct {
-	ID         int64
-	HeadRepoID int64
-}
-
-// IsErrErrPullRequestHeadRepoMissing checks if an error is a ErrPullRequestHeadRepoMissing.
-func IsErrErrPullRequestHeadRepoMissing(err error) bool {
-	_, ok := err.(ErrPullRequestHeadRepoMissing)
-	return ok
-}
-
-// Error does pretty-printing :D
-func (err ErrPullRequestHeadRepoMissing) Error() string {
-	return fmt.Sprintf("pull request head repo missing [id: %d, head_repo_id: %d]",
-		err.ID, err.HeadRepoID)
-}
-
 // ErrInvalidMergeStyle represents an error if merging with disabled merge strategy
 type ErrInvalidMergeStyle struct {
 	ID    int64
@@ -830,29 +658,6 @@ func (err ErrPullRequestHasMerged) Error() string {
 		err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch)
 }
 
-// _________                                       __
-// \_   ___ \  ____   _____   _____   ____   _____/  |_
-// /    \  \/ /  _ \ /     \ /     \_/ __ \ /    \   __\
-// \     \___(  <_> )  Y Y  \  Y Y  \  ___/|   |  \  |
-//  \______  /\____/|__|_|  /__|_|  /\___  >___|  /__|
-//         \/             \/      \/     \/     \/
-
-// ErrCommentNotExist represents a "CommentNotExist" kind of error.
-type ErrCommentNotExist struct {
-	ID      int64
-	IssueID int64
-}
-
-// IsErrCommentNotExist checks if an error is a ErrCommentNotExist.
-func IsErrCommentNotExist(err error) bool {
-	_, ok := err.(ErrCommentNotExist)
-	return ok
-}
-
-func (err ErrCommentNotExist) Error() string {
-	return fmt.Sprintf("comment does not exist [id: %d, issue_id: %d]", err.ID, err.IssueID)
-}
-
 //  _________ __                                __         .__
 //  /   _____//  |_  ____ ________  _  _______ _/  |_  ____ |  |__
 //  \_____  \\   __\/  _ \\____ \ \/ \/ /\__  \\   __\/ ___\|  |  \
@@ -897,60 +702,6 @@ func (err ErrTrackedTimeNotExist) Error() string {
 	return fmt.Sprintf("tracked time does not exist [id: %d]", err.ID)
 }
 
-// .____          ___.          .__
-// |    |   _____ \_ |__   ____ |  |
-// |    |   \__  \ | __ \_/ __ \|  |
-// |    |___ / __ \| \_\ \  ___/|  |__
-// |_______ (____  /___  /\___  >____/
-//         \/    \/    \/     \/
-
-// ErrRepoLabelNotExist represents a "RepoLabelNotExist" kind of error.
-type ErrRepoLabelNotExist struct {
-	LabelID int64
-	RepoID  int64
-}
-
-// IsErrRepoLabelNotExist checks if an error is a RepoErrLabelNotExist.
-func IsErrRepoLabelNotExist(err error) bool {
-	_, ok := err.(ErrRepoLabelNotExist)
-	return ok
-}
-
-func (err ErrRepoLabelNotExist) Error() string {
-	return fmt.Sprintf("label does not exist [label_id: %d, repo_id: %d]", err.LabelID, err.RepoID)
-}
-
-// ErrOrgLabelNotExist represents a "OrgLabelNotExist" kind of error.
-type ErrOrgLabelNotExist struct {
-	LabelID int64
-	OrgID   int64
-}
-
-// IsErrOrgLabelNotExist checks if an error is a OrgErrLabelNotExist.
-func IsErrOrgLabelNotExist(err error) bool {
-	_, ok := err.(ErrOrgLabelNotExist)
-	return ok
-}
-
-func (err ErrOrgLabelNotExist) Error() string {
-	return fmt.Sprintf("label does not exist [label_id: %d, org_id: %d]", err.LabelID, err.OrgID)
-}
-
-// ErrLabelNotExist represents a "LabelNotExist" kind of error.
-type ErrLabelNotExist struct {
-	LabelID int64
-}
-
-// IsErrLabelNotExist checks if an error is a ErrLabelNotExist.
-func IsErrLabelNotExist(err error) bool {
-	_, ok := err.(ErrLabelNotExist)
-	return ok
-}
-
-func (err ErrLabelNotExist) Error() string {
-	return fmt.Sprintf("label does not exist [label_id: %d]", err.LabelID)
-}
-
 //  ____ ___        .__                    .___
 // |    |   \______ |  |   _________     __| _/
 // |    |   /\____ \|  |  /  _ \__  \   / __ |
@@ -974,130 +725,3 @@ func IsErrUploadNotExist(err error) bool {
 func (err ErrUploadNotExist) Error() string {
 	return fmt.Sprintf("attachment does not exist [id: %d, uuid: %s]", err.ID, err.UUID)
 }
-
-// .___                            ________                                   .___                   .__
-// |   | ______ ________ __   ____ \______ \   ____ ______   ____   ____    __| _/____   ____   ____ |__| ____   ______
-// |   |/  ___//  ___/  |  \_/ __ \ |    |  \_/ __ \\____ \_/ __ \ /    \  / __ |/ __ \ /    \_/ ___\|  |/ __ \ /  ___/
-// |   |\___ \ \___ \|  |  /\  ___/ |    `   \  ___/|  |_> >  ___/|   |  \/ /_/ \  ___/|   |  \  \___|  \  ___/ \___ \
-// |___/____  >____  >____/  \___  >_______  /\___  >   __/ \___  >___|  /\____ |\___  >___|  /\___  >__|\___  >____  >
-//          \/     \/            \/        \/     \/|__|        \/     \/      \/    \/     \/     \/        \/     \/
-
-// ErrDependencyExists represents a "DependencyAlreadyExists" kind of error.
-type ErrDependencyExists struct {
-	IssueID      int64
-	DependencyID int64
-}
-
-// IsErrDependencyExists checks if an error is a ErrDependencyExists.
-func IsErrDependencyExists(err error) bool {
-	_, ok := err.(ErrDependencyExists)
-	return ok
-}
-
-func (err ErrDependencyExists) Error() string {
-	return fmt.Sprintf("issue dependency does already exist [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID)
-}
-
-// ErrDependencyNotExists represents a "DependencyAlreadyExists" kind of error.
-type ErrDependencyNotExists struct {
-	IssueID      int64
-	DependencyID int64
-}
-
-// IsErrDependencyNotExists checks if an error is a ErrDependencyExists.
-func IsErrDependencyNotExists(err error) bool {
-	_, ok := err.(ErrDependencyNotExists)
-	return ok
-}
-
-func (err ErrDependencyNotExists) Error() string {
-	return fmt.Sprintf("issue dependency does not exist [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID)
-}
-
-// ErrCircularDependency represents a "DependencyCircular" kind of error.
-type ErrCircularDependency struct {
-	IssueID      int64
-	DependencyID int64
-}
-
-// IsErrCircularDependency checks if an error is a ErrCircularDependency.
-func IsErrCircularDependency(err error) bool {
-	_, ok := err.(ErrCircularDependency)
-	return ok
-}
-
-func (err ErrCircularDependency) Error() string {
-	return fmt.Sprintf("circular dependencies exists (two issues blocking each other) [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID)
-}
-
-// ErrDependenciesLeft represents an error where the issue you're trying to close still has dependencies left.
-type ErrDependenciesLeft struct {
-	IssueID int64
-}
-
-// IsErrDependenciesLeft checks if an error is a ErrDependenciesLeft.
-func IsErrDependenciesLeft(err error) bool {
-	_, ok := err.(ErrDependenciesLeft)
-	return ok
-}
-
-func (err ErrDependenciesLeft) Error() string {
-	return fmt.Sprintf("issue has open dependencies [issue id: %d]", err.IssueID)
-}
-
-// ErrUnknownDependencyType represents an error where an unknown dependency type was passed
-type ErrUnknownDependencyType struct {
-	Type DependencyType
-}
-
-// IsErrUnknownDependencyType checks if an error is ErrUnknownDependencyType
-func IsErrUnknownDependencyType(err error) bool {
-	_, ok := err.(ErrUnknownDependencyType)
-	return ok
-}
-
-func (err ErrUnknownDependencyType) Error() string {
-	return fmt.Sprintf("unknown dependency type [type: %d]", err.Type)
-}
-
-//  __________            .__
-//  \______   \ _______  _|__| ______  _  __
-//  |       _// __ \  \/ /  |/ __ \ \/ \/ /
-//  |    |   \  ___/\   /|  \  ___/\     /
-//  |____|_  /\___  >\_/ |__|\___  >\/\_/
-//  \/     \/             \/
-
-// ErrReviewNotExist represents a "ReviewNotExist" kind of error.
-type ErrReviewNotExist struct {
-	ID int64
-}
-
-// IsErrReviewNotExist checks if an error is a ErrReviewNotExist.
-func IsErrReviewNotExist(err error) bool {
-	_, ok := err.(ErrReviewNotExist)
-	return ok
-}
-
-func (err ErrReviewNotExist) Error() string {
-	return fmt.Sprintf("review does not exist [id: %d]", err.ID)
-}
-
-// ErrNotValidReviewRequest an not allowed review request modify
-type ErrNotValidReviewRequest struct {
-	Reason string
-	UserID int64
-	RepoID int64
-}
-
-// IsErrNotValidReviewRequest checks if an error is a ErrNotValidReviewRequest.
-func IsErrNotValidReviewRequest(err error) bool {
-	_, ok := err.(ErrNotValidReviewRequest)
-	return ok
-}
-
-func (err ErrNotValidReviewRequest) Error() string {
-	return fmt.Sprintf("%s [user_id: %d, repo_id: %d]",
-		err.Reason,
-		err.UserID,
-		err.RepoID)
-}
diff --git a/models/git/branches_test.go b/models/git/branches_test.go
index 1e0b1a98b6..8102d28d48 100644
--- a/models/git/branches_test.go
+++ b/models/git/branches_test.go
@@ -7,9 +7,9 @@ package git_test
 import (
 	"testing"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 
@@ -120,10 +120,10 @@ func TestRenameBranch(t *testing.T) {
 	repo1 = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
 	assert.Equal(t, "main", repo1.DefaultBranch)
 
-	pull := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 1}).(*models.PullRequest) // merged
+	pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest) // merged
 	assert.Equal(t, "master", pull.BaseBranch)
 
-	pull = unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2}).(*models.PullRequest) // open
+	pull = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}).(*issues_model.PullRequest) // open
 	assert.Equal(t, "main", pull.BaseBranch)
 
 	renamedBranch := unittest.AssertExistsAndLoadBean(t, &git_model.RenamedBranch{ID: 2}).(*git_model.RenamedBranch)
diff --git a/models/git/main_test.go b/models/git/main_test.go
index 02401e5204..dc30dfaad7 100644
--- a/models/git/main_test.go
+++ b/models/git/main_test.go
@@ -8,6 +8,7 @@ import (
 	"path/filepath"
 	"testing"
 
+	_ "code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/unittest"
 )
 
diff --git a/models/issue_label_test.go b/models/issue_label_test.go
deleted file mode 100644
index 67a09151d8..0000000000
--- a/models/issue_label_test.go
+++ /dev/null
@@ -1,394 +0,0 @@
-// Copyright 2017 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-package models
-
-import (
-	"html/template"
-	"testing"
-
-	"code.gitea.io/gitea/models/db"
-	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/models/unittest"
-	user_model "code.gitea.io/gitea/models/user"
-
-	"github.com/stretchr/testify/assert"
-)
-
-// TODO TestGetLabelTemplateFile
-
-func TestLabel_CalOpenIssues(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	label := unittest.AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label)
-	label.CalOpenIssues()
-	assert.EqualValues(t, 2, label.NumOpenIssues)
-}
-
-func TestLabel_ForegroundColor(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	label := unittest.AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label)
-	assert.Equal(t, template.CSS("#000"), label.ForegroundColor())
-
-	label = unittest.AssertExistsAndLoadBean(t, &Label{ID: 2}).(*Label)
-	assert.Equal(t, template.CSS("#fff"), label.ForegroundColor())
-}
-
-func TestNewLabels(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	labels := []*Label{
-		{RepoID: 2, Name: "labelName2", Color: "#123456"},
-		{RepoID: 3, Name: "labelName3", Color: "#123"},
-		{RepoID: 4, Name: "labelName4", Color: "ABCDEF"},
-		{RepoID: 5, Name: "labelName5", Color: "DEF"},
-	}
-	assert.Error(t, NewLabel(db.DefaultContext, &Label{RepoID: 3, Name: "invalid Color", Color: ""}))
-	assert.Error(t, NewLabel(db.DefaultContext, &Label{RepoID: 3, Name: "invalid Color", Color: "#45G"}))
-	assert.Error(t, NewLabel(db.DefaultContext, &Label{RepoID: 3, Name: "invalid Color", Color: "#12345G"}))
-	assert.Error(t, NewLabel(db.DefaultContext, &Label{RepoID: 3, Name: "invalid Color", Color: "45G"}))
-	assert.Error(t, NewLabel(db.DefaultContext, &Label{RepoID: 3, Name: "invalid Color", Color: "12345G"}))
-	for _, label := range labels {
-		unittest.AssertNotExistsBean(t, label)
-	}
-	assert.NoError(t, NewLabels(labels...))
-	for _, label := range labels {
-		unittest.AssertExistsAndLoadBean(t, label, unittest.Cond("id = ?", label.ID))
-	}
-	unittest.CheckConsistencyFor(t, &Label{}, &repo_model.Repository{})
-}
-
-func TestGetLabelByID(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	label, err := GetLabelByID(db.DefaultContext, 1)
-	assert.NoError(t, err)
-	assert.EqualValues(t, 1, label.ID)
-
-	_, err = GetLabelByID(db.DefaultContext, unittest.NonexistentID)
-	assert.True(t, IsErrLabelNotExist(err))
-}
-
-func TestGetLabelInRepoByName(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	label, err := GetLabelInRepoByName(db.DefaultContext, 1, "label1")
-	assert.NoError(t, err)
-	assert.EqualValues(t, 1, label.ID)
-	assert.Equal(t, "label1", label.Name)
-
-	_, err = GetLabelInRepoByName(db.DefaultContext, 1, "")
-	assert.True(t, IsErrRepoLabelNotExist(err))
-
-	_, err = GetLabelInRepoByName(db.DefaultContext, unittest.NonexistentID, "nonexistent")
-	assert.True(t, IsErrRepoLabelNotExist(err))
-}
-
-func TestGetLabelInRepoByNames(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	labelIDs, err := GetLabelIDsInRepoByNames(1, []string{"label1", "label2"})
-	assert.NoError(t, err)
-
-	assert.Len(t, labelIDs, 2)
-
-	assert.Equal(t, int64(1), labelIDs[0])
-	assert.Equal(t, int64(2), labelIDs[1])
-}
-
-func TestGetLabelInRepoByNamesDiscardsNonExistentLabels(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	// label3 doesn't exists.. See labels.yml
-	labelIDs, err := GetLabelIDsInRepoByNames(1, []string{"label1", "label2", "label3"})
-	assert.NoError(t, err)
-
-	assert.Len(t, labelIDs, 2)
-
-	assert.Equal(t, int64(1), labelIDs[0])
-	assert.Equal(t, int64(2), labelIDs[1])
-	assert.NoError(t, err)
-}
-
-func TestGetLabelInRepoByID(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	label, err := GetLabelInRepoByID(db.DefaultContext, 1, 1)
-	assert.NoError(t, err)
-	assert.EqualValues(t, 1, label.ID)
-
-	_, err = GetLabelInRepoByID(db.DefaultContext, 1, -1)
-	assert.True(t, IsErrRepoLabelNotExist(err))
-
-	_, err = GetLabelInRepoByID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID)
-	assert.True(t, IsErrRepoLabelNotExist(err))
-}
-
-func TestGetLabelsInRepoByIDs(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	labels, err := GetLabelsInRepoByIDs(1, []int64{1, 2, unittest.NonexistentID})
-	assert.NoError(t, err)
-	if assert.Len(t, labels, 2) {
-		assert.EqualValues(t, 1, labels[0].ID)
-		assert.EqualValues(t, 2, labels[1].ID)
-	}
-}
-
-func TestGetLabelsByRepoID(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	testSuccess := func(repoID int64, sortType string, expectedIssueIDs []int64) {
-		labels, err := GetLabelsByRepoID(db.DefaultContext, repoID, sortType, db.ListOptions{})
-		assert.NoError(t, err)
-		assert.Len(t, labels, len(expectedIssueIDs))
-		for i, label := range labels {
-			assert.EqualValues(t, expectedIssueIDs[i], label.ID)
-		}
-	}
-	testSuccess(1, "leastissues", []int64{2, 1})
-	testSuccess(1, "mostissues", []int64{1, 2})
-	testSuccess(1, "reversealphabetically", []int64{2, 1})
-	testSuccess(1, "default", []int64{1, 2})
-}
-
-// Org versions
-
-func TestGetLabelInOrgByName(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	label, err := GetLabelInOrgByName(db.DefaultContext, 3, "orglabel3")
-	assert.NoError(t, err)
-	assert.EqualValues(t, 3, label.ID)
-	assert.Equal(t, "orglabel3", label.Name)
-
-	_, err = GetLabelInOrgByName(db.DefaultContext, 3, "")
-	assert.True(t, IsErrOrgLabelNotExist(err))
-
-	_, err = GetLabelInOrgByName(db.DefaultContext, 0, "orglabel3")
-	assert.True(t, IsErrOrgLabelNotExist(err))
-
-	_, err = GetLabelInOrgByName(db.DefaultContext, -1, "orglabel3")
-	assert.True(t, IsErrOrgLabelNotExist(err))
-
-	_, err = GetLabelInOrgByName(db.DefaultContext, unittest.NonexistentID, "nonexistent")
-	assert.True(t, IsErrOrgLabelNotExist(err))
-}
-
-func TestGetLabelInOrgByNames(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	labelIDs, err := GetLabelIDsInOrgByNames(3, []string{"orglabel3", "orglabel4"})
-	assert.NoError(t, err)
-
-	assert.Len(t, labelIDs, 2)
-
-	assert.Equal(t, int64(3), labelIDs[0])
-	assert.Equal(t, int64(4), labelIDs[1])
-}
-
-func TestGetLabelInOrgByNamesDiscardsNonExistentLabels(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	// orglabel99 doesn't exists.. See labels.yml
-	labelIDs, err := GetLabelIDsInOrgByNames(3, []string{"orglabel3", "orglabel4", "orglabel99"})
-	assert.NoError(t, err)
-
-	assert.Len(t, labelIDs, 2)
-
-	assert.Equal(t, int64(3), labelIDs[0])
-	assert.Equal(t, int64(4), labelIDs[1])
-	assert.NoError(t, err)
-}
-
-func TestGetLabelInOrgByID(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	label, err := GetLabelInOrgByID(db.DefaultContext, 3, 3)
-	assert.NoError(t, err)
-	assert.EqualValues(t, 3, label.ID)
-
-	_, err = GetLabelInOrgByID(db.DefaultContext, 3, -1)
-	assert.True(t, IsErrOrgLabelNotExist(err))
-
-	_, err = GetLabelInOrgByID(db.DefaultContext, 0, 3)
-	assert.True(t, IsErrOrgLabelNotExist(err))
-
-	_, err = GetLabelInOrgByID(db.DefaultContext, -1, 3)
-	assert.True(t, IsErrOrgLabelNotExist(err))
-
-	_, err = GetLabelInOrgByID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID)
-	assert.True(t, IsErrOrgLabelNotExist(err))
-}
-
-func TestGetLabelsInOrgByIDs(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	labels, err := GetLabelsInOrgByIDs(3, []int64{3, 4, unittest.NonexistentID})
-	assert.NoError(t, err)
-	if assert.Len(t, labels, 2) {
-		assert.EqualValues(t, 3, labels[0].ID)
-		assert.EqualValues(t, 4, labels[1].ID)
-	}
-}
-
-func TestGetLabelsByOrgID(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	testSuccess := func(orgID int64, sortType string, expectedIssueIDs []int64) {
-		labels, err := GetLabelsByOrgID(db.DefaultContext, orgID, sortType, db.ListOptions{})
-		assert.NoError(t, err)
-		assert.Len(t, labels, len(expectedIssueIDs))
-		for i, label := range labels {
-			assert.EqualValues(t, expectedIssueIDs[i], label.ID)
-		}
-	}
-	testSuccess(3, "leastissues", []int64{3, 4})
-	testSuccess(3, "mostissues", []int64{4, 3})
-	testSuccess(3, "reversealphabetically", []int64{4, 3})
-	testSuccess(3, "default", []int64{3, 4})
-
-	var err error
-	_, err = GetLabelsByOrgID(db.DefaultContext, 0, "leastissues", db.ListOptions{})
-	assert.True(t, IsErrOrgLabelNotExist(err))
-
-	_, err = GetLabelsByOrgID(db.DefaultContext, -1, "leastissues", db.ListOptions{})
-	assert.True(t, IsErrOrgLabelNotExist(err))
-}
-
-//
-
-func TestGetLabelsByIssueID(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	labels, err := GetLabelsByIssueID(db.DefaultContext, 1)
-	assert.NoError(t, err)
-	if assert.Len(t, labels, 1) {
-		assert.EqualValues(t, 1, labels[0].ID)
-	}
-
-	labels, err = GetLabelsByIssueID(db.DefaultContext, unittest.NonexistentID)
-	assert.NoError(t, err)
-	assert.Len(t, labels, 0)
-}
-
-func TestUpdateLabel(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	label := unittest.AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label)
-	// make sure update wont overwrite it
-	update := &Label{
-		ID:          label.ID,
-		Color:       "#ffff00",
-		Name:        "newLabelName",
-		Description: label.Description,
-	}
-	label.Color = update.Color
-	label.Name = update.Name
-	assert.NoError(t, UpdateLabel(update))
-	newLabel := unittest.AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label)
-	assert.EqualValues(t, label.ID, newLabel.ID)
-	assert.EqualValues(t, label.Color, newLabel.Color)
-	assert.EqualValues(t, label.Name, newLabel.Name)
-	assert.EqualValues(t, label.Description, newLabel.Description)
-	unittest.CheckConsistencyFor(t, &Label{}, &repo_model.Repository{})
-}
-
-func TestDeleteLabel(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	label := unittest.AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label)
-	assert.NoError(t, DeleteLabel(label.RepoID, label.ID))
-	unittest.AssertNotExistsBean(t, &Label{ID: label.ID, RepoID: label.RepoID})
-
-	assert.NoError(t, DeleteLabel(label.RepoID, label.ID))
-	unittest.AssertNotExistsBean(t, &Label{ID: label.ID})
-
-	assert.NoError(t, DeleteLabel(unittest.NonexistentID, unittest.NonexistentID))
-	unittest.CheckConsistencyFor(t, &Label{}, &repo_model.Repository{})
-}
-
-func TestHasIssueLabel(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	assert.True(t, HasIssueLabel(db.DefaultContext, 1, 1))
-	assert.False(t, HasIssueLabel(db.DefaultContext, 1, 2))
-	assert.False(t, HasIssueLabel(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID))
-}
-
-func TestNewIssueLabel(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	label := unittest.AssertExistsAndLoadBean(t, &Label{ID: 2}).(*Label)
-	issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
-	doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
-
-	// add new IssueLabel
-	prevNumIssues := label.NumIssues
-	assert.NoError(t, NewIssueLabel(issue, label, doer))
-	unittest.AssertExistsAndLoadBean(t, &IssueLabel{IssueID: issue.ID, LabelID: label.ID})
-	unittest.AssertExistsAndLoadBean(t, &Comment{
-		Type:     CommentTypeLabel,
-		PosterID: doer.ID,
-		IssueID:  issue.ID,
-		LabelID:  label.ID,
-		Content:  "1",
-	})
-	label = unittest.AssertExistsAndLoadBean(t, &Label{ID: 2}).(*Label)
-	assert.EqualValues(t, prevNumIssues+1, label.NumIssues)
-
-	// re-add existing IssueLabel
-	assert.NoError(t, NewIssueLabel(issue, label, doer))
-	unittest.CheckConsistencyFor(t, &Issue{}, &Label{})
-}
-
-func TestNewIssueLabels(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	label1 := unittest.AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label)
-	label2 := unittest.AssertExistsAndLoadBean(t, &Label{ID: 2}).(*Label)
-	issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 5}).(*Issue)
-	doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
-
-	assert.NoError(t, NewIssueLabels(issue, []*Label{label1, label2}, doer))
-	unittest.AssertExistsAndLoadBean(t, &IssueLabel{IssueID: issue.ID, LabelID: label1.ID})
-	unittest.AssertExistsAndLoadBean(t, &Comment{
-		Type:     CommentTypeLabel,
-		PosterID: doer.ID,
-		IssueID:  issue.ID,
-		LabelID:  label1.ID,
-		Content:  "1",
-	})
-	unittest.AssertExistsAndLoadBean(t, &IssueLabel{IssueID: issue.ID, LabelID: label1.ID})
-	label1 = unittest.AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label)
-	assert.EqualValues(t, 3, label1.NumIssues)
-	assert.EqualValues(t, 1, label1.NumClosedIssues)
-	label2 = unittest.AssertExistsAndLoadBean(t, &Label{ID: 2}).(*Label)
-	assert.EqualValues(t, 1, label2.NumIssues)
-	assert.EqualValues(t, 1, label2.NumClosedIssues)
-
-	// corner case: test empty slice
-	assert.NoError(t, NewIssueLabels(issue, []*Label{}, doer))
-
-	unittest.CheckConsistencyFor(t, &Issue{}, &Label{})
-}
-
-func TestDeleteIssueLabel(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	testSuccess := func(labelID, issueID, doerID int64) {
-		label := unittest.AssertExistsAndLoadBean(t, &Label{ID: labelID}).(*Label)
-		issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: issueID}).(*Issue)
-		doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: doerID}).(*user_model.User)
-
-		expectedNumIssues := label.NumIssues
-		expectedNumClosedIssues := label.NumClosedIssues
-		if unittest.BeanExists(t, &IssueLabel{IssueID: issueID, LabelID: labelID}) {
-			expectedNumIssues--
-			if issue.IsClosed {
-				expectedNumClosedIssues--
-			}
-		}
-
-		ctx, committer, err := db.TxContext()
-		defer committer.Close()
-		assert.NoError(t, err)
-		assert.NoError(t, DeleteIssueLabel(ctx, issue, label, doer))
-		assert.NoError(t, committer.Commit())
-
-		unittest.AssertNotExistsBean(t, &IssueLabel{IssueID: issueID, LabelID: labelID})
-		unittest.AssertExistsAndLoadBean(t, &Comment{
-			Type:     CommentTypeLabel,
-			PosterID: doerID,
-			IssueID:  issueID,
-			LabelID:  labelID,
-		}, `content=""`)
-		label = unittest.AssertExistsAndLoadBean(t, &Label{ID: labelID}).(*Label)
-		assert.EqualValues(t, expectedNumIssues, label.NumIssues)
-		assert.EqualValues(t, expectedNumClosedIssues, label.NumClosedIssues)
-	}
-	testSuccess(1, 1, 2)
-	testSuccess(2, 5, 2)
-	testSuccess(1, 1, 2) // delete non-existent IssueLabel
-
-	unittest.CheckConsistencyFor(t, &Issue{}, &Label{})
-}
diff --git a/models/issue_stopwatch_test.go b/models/issue_stopwatch_test.go
deleted file mode 100644
index 15d5f234fd..0000000000
--- a/models/issue_stopwatch_test.go
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright 2020 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-package models
-
-import (
-	"testing"
-
-	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/models/unittest"
-	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/timeutil"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func TestCancelStopwatch(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-
-	user1, err := user_model.GetUserByID(1)
-	assert.NoError(t, err)
-
-	issue1, err := GetIssueByID(1)
-	assert.NoError(t, err)
-	issue2, err := GetIssueByID(2)
-	assert.NoError(t, err)
-
-	err = CancelStopwatch(user1, issue1)
-	assert.NoError(t, err)
-	unittest.AssertNotExistsBean(t, &Stopwatch{UserID: user1.ID, IssueID: issue1.ID})
-
-	_ = unittest.AssertExistsAndLoadBean(t, &Comment{Type: CommentTypeCancelTracking, PosterID: user1.ID, IssueID: issue1.ID})
-
-	assert.Nil(t, CancelStopwatch(user1, issue2))
-}
-
-func TestStopwatchExists(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-
-	assert.True(t, StopwatchExists(1, 1))
-	assert.False(t, StopwatchExists(1, 2))
-}
-
-func TestHasUserStopwatch(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-
-	exists, sw, err := HasUserStopwatch(db.DefaultContext, 1)
-	assert.NoError(t, err)
-	assert.True(t, exists)
-	assert.Equal(t, int64(1), sw.ID)
-
-	exists, _, err = HasUserStopwatch(db.DefaultContext, 3)
-	assert.NoError(t, err)
-	assert.False(t, exists)
-}
-
-func TestCreateOrStopIssueStopwatch(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-
-	user2, err := user_model.GetUserByID(2)
-	assert.NoError(t, err)
-	user3, err := user_model.GetUserByID(3)
-	assert.NoError(t, err)
-
-	issue1, err := GetIssueByID(1)
-	assert.NoError(t, err)
-	issue2, err := GetIssueByID(2)
-	assert.NoError(t, err)
-
-	assert.NoError(t, CreateOrStopIssueStopwatch(user3, issue1))
-	sw := unittest.AssertExistsAndLoadBean(t, &Stopwatch{UserID: 3, IssueID: 1}).(*Stopwatch)
-	assert.LessOrEqual(t, sw.CreatedUnix, timeutil.TimeStampNow())
-
-	assert.NoError(t, CreateOrStopIssueStopwatch(user2, issue2))
-	unittest.AssertNotExistsBean(t, &Stopwatch{UserID: 2, IssueID: 2})
-	unittest.AssertExistsAndLoadBean(t, &TrackedTime{UserID: 2, IssueID: 2})
-}
diff --git a/models/issue_user_test.go b/models/issue_user_test.go
deleted file mode 100644
index 946da6e18d..0000000000
--- a/models/issue_user_test.go
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright 2017 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-package models
-
-import (
-	"testing"
-
-	"code.gitea.io/gitea/models/db"
-	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/models/unittest"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func Test_newIssueUsers(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-
-	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
-	newIssue := &Issue{
-		RepoID:   repo.ID,
-		PosterID: 4,
-		Index:    6,
-		Title:    "newTestIssueTitle",
-		Content:  "newTestIssueContent",
-	}
-
-	// artificially insert new issue
-	unittest.AssertSuccessfulInsert(t, newIssue)
-
-	assert.NoError(t, newIssueUsers(db.DefaultContext, repo, newIssue))
-
-	// issue_user table should now have entries for new issue
-	unittest.AssertExistsAndLoadBean(t, &IssueUser{IssueID: newIssue.ID, UID: newIssue.PosterID})
-	unittest.AssertExistsAndLoadBean(t, &IssueUser{IssueID: newIssue.ID, UID: repo.OwnerID})
-}
-
-func TestUpdateIssueUserByRead(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
-
-	assert.NoError(t, UpdateIssueUserByRead(4, issue.ID))
-	unittest.AssertExistsAndLoadBean(t, &IssueUser{IssueID: issue.ID, UID: 4}, "is_read=1")
-
-	assert.NoError(t, UpdateIssueUserByRead(4, issue.ID))
-	unittest.AssertExistsAndLoadBean(t, &IssueUser{IssueID: issue.ID, UID: 4}, "is_read=1")
-
-	assert.NoError(t, UpdateIssueUserByRead(unittest.NonexistentID, unittest.NonexistentID))
-}
-
-func TestUpdateIssueUsersByMentions(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-	issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
-
-	uids := []int64{2, 5}
-	assert.NoError(t, UpdateIssueUsersByMentions(db.DefaultContext, issue.ID, uids))
-	for _, uid := range uids {
-		unittest.AssertExistsAndLoadBean(t, &IssueUser{IssueID: issue.ID, UID: uid}, "is_mentioned=1")
-	}
-}
diff --git a/models/issue_assignees.go b/models/issues/assignees.go
similarity index 99%
rename from models/issue_assignees.go
rename to models/issues/assignees.go
index c6ccb6e9d2..5921112fea 100644
--- a/models/issue_assignees.go
+++ b/models/issues/assignees.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues
 
 import (
 	"context"
diff --git a/models/issue_assignees_test.go b/models/issues/assignees_test.go
similarity index 65%
rename from models/issue_assignees_test.go
rename to models/issues/assignees_test.go
index 80317e1604..37d966f140 100644
--- a/models/issue_assignees_test.go
+++ b/models/issues/assignees_test.go
@@ -2,12 +2,13 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues_test
 
 import (
 	"testing"
 
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 
@@ -18,27 +19,27 @@ func TestUpdateAssignee(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	// Fake issue with assignees
-	issue, err := GetIssueWithAttrsByID(1)
+	issue, err := issues_model.GetIssueWithAttrsByID(1)
 	assert.NoError(t, err)
 
 	// Assign multiple users
 	user2, err := user_model.GetUserByID(2)
 	assert.NoError(t, err)
-	_, _, err = ToggleIssueAssignee(issue, &user_model.User{ID: 1}, user2.ID)
+	_, _, err = issues_model.ToggleIssueAssignee(issue, &user_model.User{ID: 1}, user2.ID)
 	assert.NoError(t, err)
 
 	user3, err := user_model.GetUserByID(3)
 	assert.NoError(t, err)
-	_, _, err = ToggleIssueAssignee(issue, &user_model.User{ID: 1}, user3.ID)
+	_, _, err = issues_model.ToggleIssueAssignee(issue, &user_model.User{ID: 1}, user3.ID)
 	assert.NoError(t, err)
 
 	user1, err := user_model.GetUserByID(1) // This user is already assigned (see the definition in fixtures), so running  UpdateAssignee should unassign him
 	assert.NoError(t, err)
-	_, _, err = ToggleIssueAssignee(issue, &user_model.User{ID: 1}, user1.ID)
+	_, _, err = issues_model.ToggleIssueAssignee(issue, &user_model.User{ID: 1}, user1.ID)
 	assert.NoError(t, err)
 
 	// Check if he got removed
-	isAssigned, err := IsUserAssignedToIssue(db.DefaultContext, issue, user1)
+	isAssigned, err := issues_model.IsUserAssignedToIssue(db.DefaultContext, issue, user1)
 	assert.NoError(t, err)
 	assert.False(t, isAssigned)
 
@@ -54,12 +55,12 @@ func TestUpdateAssignee(t *testing.T) {
 	}
 
 	// Check if the user is assigned
-	isAssigned, err = IsUserAssignedToIssue(db.DefaultContext, issue, user2)
+	isAssigned, err = issues_model.IsUserAssignedToIssue(db.DefaultContext, issue, user2)
 	assert.NoError(t, err)
 	assert.True(t, isAssigned)
 
 	// This user should not be assigned
-	isAssigned, err = IsUserAssignedToIssue(db.DefaultContext, issue, &user_model.User{ID: 4})
+	isAssigned, err = issues_model.IsUserAssignedToIssue(db.DefaultContext, issue, &user_model.User{ID: 4})
 	assert.NoError(t, err)
 	assert.False(t, isAssigned)
 }
@@ -70,22 +71,22 @@ func TestMakeIDsFromAPIAssigneesToAdd(t *testing.T) {
 	_ = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
 	_ = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
 
-	IDs, err := MakeIDsFromAPIAssigneesToAdd("", []string{""})
+	IDs, err := issues_model.MakeIDsFromAPIAssigneesToAdd("", []string{""})
 	assert.NoError(t, err)
 	assert.Equal(t, []int64{}, IDs)
 
-	_, err = MakeIDsFromAPIAssigneesToAdd("", []string{"none_existing_user"})
+	_, err = issues_model.MakeIDsFromAPIAssigneesToAdd("", []string{"none_existing_user"})
 	assert.Error(t, err)
 
-	IDs, err = MakeIDsFromAPIAssigneesToAdd("user1", []string{"user1"})
+	IDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd("user1", []string{"user1"})
 	assert.NoError(t, err)
 	assert.Equal(t, []int64{1}, IDs)
 
-	IDs, err = MakeIDsFromAPIAssigneesToAdd("user2", []string{""})
+	IDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd("user2", []string{""})
 	assert.NoError(t, err)
 	assert.Equal(t, []int64{2}, IDs)
 
-	IDs, err = MakeIDsFromAPIAssigneesToAdd("", []string{"user1", "user2"})
+	IDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd("", []string{"user1", "user2"})
 	assert.NoError(t, err)
 	assert.Equal(t, []int64{1, 2}, IDs)
 }
diff --git a/models/issue_comment.go b/models/issues/comment.go
similarity index 92%
rename from models/issue_comment.go
rename to models/issues/comment.go
index 21cd87108d..a4e69e7118 100644
--- a/models/issue_comment.go
+++ b/models/issues/comment.go
@@ -4,7 +4,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues
 
 import (
 	"context"
@@ -16,7 +16,6 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
-	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
 	project_model "code.gitea.io/gitea/models/project"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -34,6 +33,22 @@ import (
 	"xorm.io/xorm"
 )
 
+// ErrCommentNotExist represents a "CommentNotExist" kind of error.
+type ErrCommentNotExist struct {
+	ID      int64
+	IssueID int64
+}
+
+// IsErrCommentNotExist checks if an error is a ErrCommentNotExist.
+func IsErrCommentNotExist(err error) bool {
+	_, ok := err.(ErrCommentNotExist)
+	return ok
+}
+
+func (err ErrCommentNotExist) Error() string {
+	return fmt.Sprintf("comment does not exist [id: %d, issue_id: %d]", err.ID, err.IssueID)
+}
+
 // CommentType defines whether a comment is just a simple comment, an action (like close) or a reference.
 type CommentType int
 
@@ -216,8 +231,8 @@ type Comment struct {
 	Project          *project_model.Project `xorm:"-"`
 	OldMilestoneID   int64
 	MilestoneID      int64
-	OldMilestone     *issues_model.Milestone `xorm:"-"`
-	Milestone        *issues_model.Milestone `xorm:"-"`
+	OldMilestone     *Milestone `xorm:"-"`
+	Milestone        *Milestone `xorm:"-"`
 	TimeID           int64
 	Time             *TrackedTime `xorm:"-"`
 	AssigneeID       int64
@@ -250,8 +265,8 @@ type Comment struct {
 	// Reference issue in commit message
 	CommitSHA string `xorm:"VARCHAR(40)"`
 
-	Attachments []*repo_model.Attachment  `xorm:"-"`
-	Reactions   issues_model.ReactionList `xorm:"-"`
+	Attachments []*repo_model.Attachment `xorm:"-"`
+	Reactions   ReactionList             `xorm:"-"`
 
 	// For view issue page.
 	ShowRole RoleDescriptor `xorm:"-"`
@@ -299,7 +314,7 @@ func (c *Comment) LoadIssueCtx(ctx context.Context) (err error) {
 	if c.Issue != nil {
 		return nil
 	}
-	c.Issue, err = getIssueByID(ctx, c.IssueID)
+	c.Issue, err = GetIssueByID(ctx, c.IssueID)
 	return
 }
 
@@ -503,7 +518,7 @@ func (c *Comment) LoadProject() error {
 // LoadMilestone if comment.Type is CommentTypeMilestone, then load milestone
 func (c *Comment) LoadMilestone() error {
 	if c.OldMilestoneID > 0 {
-		var oldMilestone issues_model.Milestone
+		var oldMilestone Milestone
 		has, err := db.GetEngine(db.DefaultContext).ID(c.OldMilestoneID).Get(&oldMilestone)
 		if err != nil {
 			return err
@@ -513,7 +528,7 @@ func (c *Comment) LoadMilestone() error {
 	}
 
 	if c.MilestoneID > 0 {
-		var milestone issues_model.Milestone
+		var milestone Milestone
 		has, err := db.GetEngine(db.DefaultContext).ID(c.MilestoneID).Get(&milestone)
 		if err != nil {
 			return err
@@ -625,7 +640,7 @@ func (c *Comment) LoadDepIssueDetails() (err error) {
 	if c.DependentIssueID <= 0 || c.DependentIssue != nil {
 		return nil
 	}
-	c.DependentIssue, err = getIssueByID(db.DefaultContext, c.DependentIssueID)
+	c.DependentIssue, err = GetIssueByID(db.DefaultContext, c.DependentIssueID)
 	return err
 }
 
@@ -643,7 +658,7 @@ func (c *Comment) loadReactions(ctx context.Context, repo *repo_model.Repository
 	if c.Reactions != nil {
 		return nil
 	}
-	c.Reactions, _, err = issues_model.FindReactions(ctx, issues_model.FindReactionsOptions{
+	c.Reactions, _, err = FindReactions(ctx, FindReactionsOptions{
 		IssueID:   c.IssueID,
 		CommentID: c.ID,
 	})
@@ -823,7 +838,7 @@ func CreateCommentCtx(ctx context.Context, opts *CreateCommentOptions) (_ *Comme
 		return nil, err
 	}
 
-	if err = comment.addCrossReferences(ctx, opts.Doer, false); err != nil {
+	if err = comment.AddCrossReferences(ctx, opts.Doer, false); err != nil {
 		return nil, err
 	}
 
@@ -1128,7 +1143,7 @@ func UpdateComment(c *Comment, doer *user_model.User) error {
 	if err := c.LoadIssueCtx(ctx); err != nil {
 		return err
 	}
-	if err := c.addCrossReferences(ctx, doer, true); err != nil {
+	if err := c.AddCrossReferences(ctx, doer, true); err != nil {
 		return err
 	}
 	if err := committer.Commit(); err != nil {
@@ -1139,27 +1154,13 @@ func UpdateComment(c *Comment, doer *user_model.User) error {
 }
 
 // DeleteComment deletes the comment
-func DeleteComment(comment *Comment) error {
-	ctx, committer, err := db.TxContext()
-	if err != nil {
-		return err
-	}
-	defer committer.Close()
-
-	if err := deleteComment(ctx, comment); err != nil {
-		return err
-	}
-
-	return committer.Commit()
-}
-
-func deleteComment(ctx context.Context, comment *Comment) error {
+func DeleteComment(ctx context.Context, comment *Comment) error {
 	e := db.GetEngine(ctx)
 	if _, err := e.ID(comment.ID).NoAutoCondition().Delete(comment); err != nil {
 		return err
 	}
 
-	if _, err := db.DeleteByBean(ctx, &issues_model.ContentHistory{
+	if _, err := db.DeleteByBean(ctx, &ContentHistory{
 		CommentID: comment.ID,
 	}); err != nil {
 		return err
@@ -1170,7 +1171,11 @@ func deleteComment(ctx context.Context, comment *Comment) error {
 			return err
 		}
 	}
-	if _, err := e.Where("comment_id = ?", comment.ID).Cols("is_deleted").Update(&Action{IsDeleted: true}); err != nil {
+	if _, err := e.Table("action").
+		Where("comment_id = ?", comment.ID).
+		Update(map[string]interface{}{
+			"is_deleted": true,
+		}); err != nil {
 		return err
 	}
 
@@ -1178,7 +1183,7 @@ func deleteComment(ctx context.Context, comment *Comment) error {
 		return err
 	}
 
-	return issues_model.DeleteReaction(ctx, &issues_model.ReactionOptions{CommentID: comment.ID})
+	return DeleteReaction(ctx, &ReactionOptions{CommentID: comment.ID})
 }
 
 // CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS
@@ -1500,3 +1505,42 @@ func (c *Comment) GetExternalName() string { return c.OriginalAuthor }
 
 // GetExternalID ExternalUserRemappable interface
 func (c *Comment) GetExternalID() int64 { return c.OriginalAuthorID }
+
+// CountCommentTypeLabelWithEmptyLabel count label comments with empty label
+func CountCommentTypeLabelWithEmptyLabel() (int64, error) {
+	return db.GetEngine(db.DefaultContext).Where(builder.Eq{"type": CommentTypeLabel, "label_id": 0}).Count(new(Comment))
+}
+
+// FixCommentTypeLabelWithEmptyLabel count label comments with empty label
+func FixCommentTypeLabelWithEmptyLabel() (int64, error) {
+	return db.GetEngine(db.DefaultContext).Where(builder.Eq{"type": CommentTypeLabel, "label_id": 0}).Delete(new(Comment))
+}
+
+// CountCommentTypeLabelWithOutsideLabels count label comments with outside label
+func CountCommentTypeLabelWithOutsideLabels() (int64, error) {
+	return db.GetEngine(db.DefaultContext).Where("comment.type = ? AND ((label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id))", CommentTypeLabel).
+		Table("comment").
+		Join("inner", "label", "label.id = comment.label_id").
+		Join("inner", "issue", "issue.id = comment.issue_id ").
+		Join("inner", "repository", "issue.repo_id = repository.id").
+		Count()
+}
+
+// FixCommentTypeLabelWithOutsideLabels count label comments with outside label
+func FixCommentTypeLabelWithOutsideLabels() (int64, error) {
+	res, err := db.GetEngine(db.DefaultContext).Exec(`DELETE FROM comment WHERE comment.id IN (
+		SELECT il_too.id FROM (
+			SELECT com.id
+				FROM comment AS com
+					INNER JOIN label ON com.label_id = label.id
+					INNER JOIN issue on issue.id = com.issue_id
+					INNER JOIN repository ON issue.repo_id = repository.id
+				WHERE
+					com.type = ? AND ((label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id))
+	) AS il_too)`, CommentTypeLabel)
+	if err != nil {
+		return 0, err
+	}
+
+	return res.RowsAffected()
+}
diff --git a/models/issue_comment_list.go b/models/issues/comment_list.go
similarity index 95%
rename from models/issue_comment_list.go
rename to models/issues/comment_list.go
index d62984c1e6..e3406a5cbe 100644
--- a/models/issue_comment_list.go
+++ b/models/issues/comment_list.go
@@ -2,13 +2,12 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues
 
 import (
 	"context"
 
 	"code.gitea.io/gitea/models/db"
-	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/container"
@@ -36,7 +35,7 @@ func (comments CommentList) loadPosters(ctx context.Context) error {
 	posterMaps := make(map[int64]*user_model.User, len(posterIDs))
 	left := len(posterIDs)
 	for left > 0 {
-		limit := defaultMaxInSize
+		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
@@ -80,7 +79,7 @@ func (comments CommentList) getLabelIDs() []int64 {
 	return container.KeysInt64(ids)
 }
 
-func (comments CommentList) loadLabels(ctx context.Context) error {
+func (comments CommentList) loadLabels(ctx context.Context) error { //nolint
 	if len(comments) == 0 {
 		return nil
 	}
@@ -89,7 +88,7 @@ func (comments CommentList) loadLabels(ctx context.Context) error {
 	commentLabels := make(map[int64]*Label, len(labelIDs))
 	left := len(labelIDs)
 	for left > 0 {
-		limit := defaultMaxInSize
+		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
@@ -140,10 +139,10 @@ func (comments CommentList) loadMilestones(ctx context.Context) error {
 		return nil
 	}
 
-	milestoneMaps := make(map[int64]*issues_model.Milestone, len(milestoneIDs))
+	milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs))
 	left := len(milestoneIDs)
 	for left > 0 {
-		limit := defaultMaxInSize
+		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
@@ -183,10 +182,10 @@ func (comments CommentList) loadOldMilestones(ctx context.Context) error {
 		return nil
 	}
 
-	milestoneMaps := make(map[int64]*issues_model.Milestone, len(milestoneIDs))
+	milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs))
 	left := len(milestoneIDs)
 	for left > 0 {
-		limit := defaultMaxInSize
+		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
@@ -225,7 +224,7 @@ func (comments CommentList) loadAssignees(ctx context.Context) error {
 	assignees := make(map[int64]*user_model.User, len(assigneeIDs))
 	left := len(assigneeIDs)
 	for left > 0 {
-		limit := defaultMaxInSize
+		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
@@ -299,7 +298,7 @@ func (comments CommentList) loadIssues(ctx context.Context) error {
 	issues := make(map[int64]*Issue, len(issueIDs))
 	left := len(issueIDs)
 	for left > 0 {
-		limit := defaultMaxInSize
+		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
@@ -357,7 +356,7 @@ func (comments CommentList) loadDependentIssues(ctx context.Context) error {
 	issues := make(map[int64]*Issue, len(issueIDs))
 	left := len(issueIDs)
 	for left > 0 {
-		limit := defaultMaxInSize
+		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
@@ -406,7 +405,7 @@ func (comments CommentList) loadAttachments(ctx context.Context) (err error) {
 	commentsIDs := comments.getCommentIDs()
 	left := len(commentsIDs)
 	for left > 0 {
-		limit := defaultMaxInSize
+		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
@@ -449,7 +448,7 @@ func (comments CommentList) getReviewIDs() []int64 {
 	return container.KeysInt64(ids)
 }
 
-func (comments CommentList) loadReviews(ctx context.Context) error {
+func (comments CommentList) loadReviews(ctx context.Context) error { //nolint
 	if len(comments) == 0 {
 		return nil
 	}
@@ -458,7 +457,7 @@ func (comments CommentList) loadReviews(ctx context.Context) error {
 	reviews := make(map[int64]*Review, len(reviewIDs))
 	left := len(reviewIDs)
 	for left > 0 {
-		limit := defaultMaxInSize
+		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
diff --git a/models/issue_comment_test.go b/models/issues/comment_test.go
similarity index 71%
rename from models/issue_comment_test.go
rename to models/issues/comment_test.go
index d323a08167..06b0b85e3c 100644
--- a/models/issue_comment_test.go
+++ b/models/issues/comment_test.go
@@ -2,13 +2,14 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues_test
 
 import (
 	"testing"
 	"time"
 
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
@@ -19,13 +20,13 @@ import (
 func TestCreateComment(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	issue := unittest.AssertExistsAndLoadBean(t, &Issue{}).(*Issue)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{}).(*issues_model.Issue)
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository)
 	doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
 
 	now := time.Now().Unix()
-	comment, err := CreateComment(&CreateCommentOptions{
-		Type:    CommentTypeComment,
+	comment, err := issues_model.CreateComment(&issues_model.CreateCommentOptions{
+		Type:    issues_model.CommentTypeComment,
 		Doer:    doer,
 		Repo:    repo,
 		Issue:   issue,
@@ -34,23 +35,23 @@ func TestCreateComment(t *testing.T) {
 	assert.NoError(t, err)
 	then := time.Now().Unix()
 
-	assert.EqualValues(t, CommentTypeComment, comment.Type)
+	assert.EqualValues(t, issues_model.CommentTypeComment, comment.Type)
 	assert.EqualValues(t, "Hello", comment.Content)
 	assert.EqualValues(t, issue.ID, comment.IssueID)
 	assert.EqualValues(t, doer.ID, comment.PosterID)
 	unittest.AssertInt64InRange(t, now, then, int64(comment.CreatedUnix))
 	unittest.AssertExistsAndLoadBean(t, comment) // assert actually added to DB
 
-	updatedIssue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: issue.ID}).(*Issue)
+	updatedIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID}).(*issues_model.Issue)
 	unittest.AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix))
 }
 
 func TestFetchCodeComments(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}).(*issues_model.Issue)
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
-	res, err := FetchCodeComments(db.DefaultContext, issue, user)
+	res, err := issues_model.FetchCodeComments(db.DefaultContext, issue, user)
 	assert.NoError(t, err)
 	assert.Contains(t, res, "README.md")
 	assert.Contains(t, res["README.md"], int64(4))
@@ -58,7 +59,7 @@ func TestFetchCodeComments(t *testing.T) {
 	assert.Equal(t, int64(4), res["README.md"][4][0].ID)
 
 	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
-	res, err = FetchCodeComments(db.DefaultContext, issue, user2)
+	res, err = issues_model.FetchCodeComments(db.DefaultContext, issue, user2)
 	assert.NoError(t, err)
 	assert.Len(t, res, 1)
 }
diff --git a/models/issues/content_history.go b/models/issues/content_history.go
index 4c5af13db7..3e321784bd 100644
--- a/models/issues/content_history.go
+++ b/models/issues/content_history.go
@@ -53,13 +53,13 @@ func SaveIssueContentHistory(ctx context.Context, posterID, issueID, commentID i
 	}
 	// We only keep at most 20 history revisions now. It is enough in most cases.
 	// If there is a special requirement to keep more, we can consider introducing a new setting option then, but not now.
-	keepLimitedContentHistory(ctx, issueID, commentID, 20)
+	KeepLimitedContentHistory(ctx, issueID, commentID, 20)
 	return nil
 }
 
-// keepLimitedContentHistory keeps at most `limit` history revisions, it will hard delete out-dated revisions, sorting by revision interval
+// KeepLimitedContentHistory keeps at most `limit` history revisions, it will hard delete out-dated revisions, sorting by revision interval
 // we can ignore all errors in this function, so we just log them
-func keepLimitedContentHistory(ctx context.Context, issueID, commentID int64, limit int) {
+func KeepLimitedContentHistory(ctx context.Context, issueID, commentID int64, limit int) {
 	type IDEditTime struct {
 		ID         int64
 		EditedUnix timeutil.TimeStamp
diff --git a/models/issues/content_history_test.go b/models/issues/content_history_test.go
index 3cbc0ad5e0..1218d871d0 100644
--- a/models/issues/content_history_test.go
+++ b/models/issues/content_history_test.go
@@ -2,12 +2,13 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package issues
+package issues_test
 
 import (
 	"testing"
 
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/timeutil"
 
@@ -20,20 +21,20 @@ func TestContentHistory(t *testing.T) {
 	dbCtx := db.DefaultContext
 	timeStampNow := timeutil.TimeStampNow()
 
-	_ = SaveIssueContentHistory(dbCtx, 1, 10, 0, timeStampNow, "i-a", true)
-	_ = SaveIssueContentHistory(dbCtx, 1, 10, 0, timeStampNow.Add(2), "i-b", false)
-	_ = SaveIssueContentHistory(dbCtx, 1, 10, 0, timeStampNow.Add(7), "i-c", false)
+	_ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 0, timeStampNow, "i-a", true)
+	_ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 0, timeStampNow.Add(2), "i-b", false)
+	_ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 0, timeStampNow.Add(7), "i-c", false)
 
-	_ = SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow, "c-a", true)
-	_ = SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(5), "c-b", false)
-	_ = SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(20), "c-c", false)
-	_ = SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(50), "c-d", false)
-	_ = SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(51), "c-e", false)
+	_ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow, "c-a", true)
+	_ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(5), "c-b", false)
+	_ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(20), "c-c", false)
+	_ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(50), "c-d", false)
+	_ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(51), "c-e", false)
 
-	h1, _ := GetIssueContentHistoryByID(dbCtx, 1)
+	h1, _ := issues_model.GetIssueContentHistoryByID(dbCtx, 1)
 	assert.EqualValues(t, 1, h1.ID)
 
-	m, _ := QueryIssueContentHistoryEditedCountMap(dbCtx, 10)
+	m, _ := issues_model.QueryIssueContentHistoryEditedCountMap(dbCtx, 10)
 	assert.Equal(t, 3, m[0])
 	assert.Equal(t, 5, m[100])
 
@@ -48,31 +49,31 @@ func TestContentHistory(t *testing.T) {
 	}
 	_ = db.GetEngine(dbCtx).Sync2(&User{})
 
-	list1, _ := FetchIssueContentHistoryList(dbCtx, 10, 0)
+	list1, _ := issues_model.FetchIssueContentHistoryList(dbCtx, 10, 0)
 	assert.Len(t, list1, 3)
-	list2, _ := FetchIssueContentHistoryList(dbCtx, 10, 100)
+	list2, _ := issues_model.FetchIssueContentHistoryList(dbCtx, 10, 100)
 	assert.Len(t, list2, 5)
 
-	hasHistory1, _ := HasIssueContentHistory(dbCtx, 10, 0)
+	hasHistory1, _ := issues_model.HasIssueContentHistory(dbCtx, 10, 0)
 	assert.True(t, hasHistory1)
-	hasHistory2, _ := HasIssueContentHistory(dbCtx, 10, 1)
+	hasHistory2, _ := issues_model.HasIssueContentHistory(dbCtx, 10, 1)
 	assert.False(t, hasHistory2)
 
-	h6, h6Prev, _ := GetIssueContentHistoryAndPrev(dbCtx, 6)
+	h6, h6Prev, _ := issues_model.GetIssueContentHistoryAndPrev(dbCtx, 6)
 	assert.EqualValues(t, 6, h6.ID)
 	assert.EqualValues(t, 5, h6Prev.ID)
 
 	// soft-delete
-	_ = SoftDeleteIssueContentHistory(dbCtx, 5)
-	h6, h6Prev, _ = GetIssueContentHistoryAndPrev(dbCtx, 6)
+	_ = issues_model.SoftDeleteIssueContentHistory(dbCtx, 5)
+	h6, h6Prev, _ = issues_model.GetIssueContentHistoryAndPrev(dbCtx, 6)
 	assert.EqualValues(t, 6, h6.ID)
 	assert.EqualValues(t, 4, h6Prev.ID)
 
 	// only keep 3 history revisions for comment_id=100, the first and the last should never be deleted
-	keepLimitedContentHistory(dbCtx, 10, 100, 3)
-	list1, _ = FetchIssueContentHistoryList(dbCtx, 10, 0)
+	issues_model.KeepLimitedContentHistory(dbCtx, 10, 100, 3)
+	list1, _ = issues_model.FetchIssueContentHistoryList(dbCtx, 10, 0)
 	assert.Len(t, list1, 3)
-	list2, _ = FetchIssueContentHistoryList(dbCtx, 10, 100)
+	list2, _ = issues_model.FetchIssueContentHistoryList(dbCtx, 10, 100)
 	assert.Len(t, list2, 3)
 	assert.EqualValues(t, 8, list2[0].HistoryID)
 	assert.EqualValues(t, 7, list2[1].HistoryID)
diff --git a/models/issue_dependency.go b/models/issues/dependency.go
similarity index 59%
rename from models/issue_dependency.go
rename to models/issues/dependency.go
index af40aa45d3..d664c0758e 100644
--- a/models/issue_dependency.go
+++ b/models/issues/dependency.go
@@ -2,16 +2,95 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues
 
 import (
 	"context"
+	"fmt"
 
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/timeutil"
 )
 
+// ErrDependencyExists represents a "DependencyAlreadyExists" kind of error.
+type ErrDependencyExists struct {
+	IssueID      int64
+	DependencyID int64
+}
+
+// IsErrDependencyExists checks if an error is a ErrDependencyExists.
+func IsErrDependencyExists(err error) bool {
+	_, ok := err.(ErrDependencyExists)
+	return ok
+}
+
+func (err ErrDependencyExists) Error() string {
+	return fmt.Sprintf("issue dependency does already exist [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID)
+}
+
+// ErrDependencyNotExists represents a "DependencyAlreadyExists" kind of error.
+type ErrDependencyNotExists struct {
+	IssueID      int64
+	DependencyID int64
+}
+
+// IsErrDependencyNotExists checks if an error is a ErrDependencyExists.
+func IsErrDependencyNotExists(err error) bool {
+	_, ok := err.(ErrDependencyNotExists)
+	return ok
+}
+
+func (err ErrDependencyNotExists) Error() string {
+	return fmt.Sprintf("issue dependency does not exist [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID)
+}
+
+// ErrCircularDependency represents a "DependencyCircular" kind of error.
+type ErrCircularDependency struct {
+	IssueID      int64
+	DependencyID int64
+}
+
+// IsErrCircularDependency checks if an error is a ErrCircularDependency.
+func IsErrCircularDependency(err error) bool {
+	_, ok := err.(ErrCircularDependency)
+	return ok
+}
+
+func (err ErrCircularDependency) Error() string {
+	return fmt.Sprintf("circular dependencies exists (two issues blocking each other) [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID)
+}
+
+// ErrDependenciesLeft represents an error where the issue you're trying to close still has dependencies left.
+type ErrDependenciesLeft struct {
+	IssueID int64
+}
+
+// IsErrDependenciesLeft checks if an error is a ErrDependenciesLeft.
+func IsErrDependenciesLeft(err error) bool {
+	_, ok := err.(ErrDependenciesLeft)
+	return ok
+}
+
+func (err ErrDependenciesLeft) Error() string {
+	return fmt.Sprintf("issue has open dependencies [issue id: %d]", err.IssueID)
+}
+
+// ErrUnknownDependencyType represents an error where an unknown dependency type was passed
+type ErrUnknownDependencyType struct {
+	Type DependencyType
+}
+
+// IsErrUnknownDependencyType checks if an error is ErrUnknownDependencyType
+func IsErrUnknownDependencyType(err error) bool {
+	_, ok := err.(ErrUnknownDependencyType)
+	return ok
+}
+
+func (err ErrUnknownDependencyType) Error() string {
+	return fmt.Sprintf("unknown dependency type [type: %d]", err.Type)
+}
+
 // IssueDependency represents an issue dependency
 type IssueDependency struct {
 	ID           int64              `xorm:"pk autoincr"`
diff --git a/models/issue_dependency_test.go b/models/issues/dependency_test.go
similarity index 51%
rename from models/issue_dependency_test.go
rename to models/issues/dependency_test.go
index 345a9077cd..3ea0b4ff5c 100644
--- a/models/issue_dependency_test.go
+++ b/models/issues/dependency_test.go
@@ -2,12 +2,13 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues_test
 
 import (
 	"testing"
 
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 
@@ -21,42 +22,42 @@ func TestCreateIssueDependency(t *testing.T) {
 	user1, err := user_model.GetUserByID(1)
 	assert.NoError(t, err)
 
-	issue1, err := GetIssueByID(1)
+	issue1, err := issues_model.GetIssueByID(db.DefaultContext, 1)
 	assert.NoError(t, err)
 
-	issue2, err := GetIssueByID(2)
+	issue2, err := issues_model.GetIssueByID(db.DefaultContext, 2)
 	assert.NoError(t, err)
 
 	// Create a dependency and check if it was successful
-	err = CreateIssueDependency(user1, issue1, issue2)
+	err = issues_model.CreateIssueDependency(user1, issue1, issue2)
 	assert.NoError(t, err)
 
 	// Do it again to see if it will check if the dependency already exists
-	err = CreateIssueDependency(user1, issue1, issue2)
+	err = issues_model.CreateIssueDependency(user1, issue1, issue2)
 	assert.Error(t, err)
-	assert.True(t, IsErrDependencyExists(err))
+	assert.True(t, issues_model.IsErrDependencyExists(err))
 
 	// Check for circular dependencies
-	err = CreateIssueDependency(user1, issue2, issue1)
+	err = issues_model.CreateIssueDependency(user1, issue2, issue1)
 	assert.Error(t, err)
-	assert.True(t, IsErrCircularDependency(err))
+	assert.True(t, issues_model.IsErrCircularDependency(err))
 
-	_ = unittest.AssertExistsAndLoadBean(t, &Comment{Type: CommentTypeAddDependency, PosterID: user1.ID, IssueID: issue1.ID})
+	_ = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{Type: issues_model.CommentTypeAddDependency, PosterID: user1.ID, IssueID: issue1.ID})
 
 	// Check if dependencies left is correct
-	left, err := IssueNoDependenciesLeft(db.DefaultContext, issue1)
+	left, err := issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1)
 	assert.NoError(t, err)
 	assert.False(t, left)
 
 	// Close #2 and check again
-	_, err = ChangeIssueStatus(db.DefaultContext, issue2, user1, true)
+	_, err = issues_model.ChangeIssueStatus(db.DefaultContext, issue2, user1, true)
 	assert.NoError(t, err)
 
-	left, err = IssueNoDependenciesLeft(db.DefaultContext, issue1)
+	left, err = issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1)
 	assert.NoError(t, err)
 	assert.True(t, left)
 
 	// Test removing the dependency
-	err = RemoveIssueDependency(user1, issue1, issue2, DependencyTypeBlockedBy)
+	err = issues_model.RemoveIssueDependency(user1, issue1, issue2, issues_model.DependencyTypeBlockedBy)
 	assert.NoError(t, err)
 }
diff --git a/models/issue.go b/models/issues/issue.go
similarity index 89%
rename from models/issue.go
rename to models/issues/issue.go
index a22c115523..0f4af3e84f 100644
--- a/models/issue.go
+++ b/models/issues/issue.go
@@ -3,7 +3,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues
 
 import (
 	"context"
@@ -16,7 +16,6 @@ import (
 	admin_model "code.gitea.io/gitea/models/admin"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/foreignreference"
-	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
@@ -29,7 +28,6 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/references"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/storage"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
@@ -38,6 +36,71 @@ import (
 	"xorm.io/xorm"
 )
 
+// ErrIssueNotExist represents a "IssueNotExist" kind of error.
+type ErrIssueNotExist struct {
+	ID     int64
+	RepoID int64
+	Index  int64
+}
+
+// IsErrIssueNotExist checks if an error is a ErrIssueNotExist.
+func IsErrIssueNotExist(err error) bool {
+	_, ok := err.(ErrIssueNotExist)
+	return ok
+}
+
+func (err ErrIssueNotExist) Error() string {
+	return fmt.Sprintf("issue does not exist [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index)
+}
+
+// ErrIssueIsClosed represents a "IssueIsClosed" kind of error.
+type ErrIssueIsClosed struct {
+	ID     int64
+	RepoID int64
+	Index  int64
+}
+
+// IsErrIssueIsClosed checks if an error is a ErrIssueNotExist.
+func IsErrIssueIsClosed(err error) bool {
+	_, ok := err.(ErrIssueIsClosed)
+	return ok
+}
+
+func (err ErrIssueIsClosed) Error() string {
+	return fmt.Sprintf("issue is closed [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index)
+}
+
+// ErrNewIssueInsert is used when the INSERT statement in newIssue fails
+type ErrNewIssueInsert struct {
+	OriginalError error
+}
+
+// IsErrNewIssueInsert checks if an error is a ErrNewIssueInsert.
+func IsErrNewIssueInsert(err error) bool {
+	_, ok := err.(ErrNewIssueInsert)
+	return ok
+}
+
+func (err ErrNewIssueInsert) Error() string {
+	return err.OriginalError.Error()
+}
+
+// ErrIssueWasClosed is used when close a closed issue
+type ErrIssueWasClosed struct {
+	ID    int64
+	Index int64
+}
+
+// IsErrIssueWasClosed checks if an error is a ErrIssueWasClosed.
+func IsErrIssueWasClosed(err error) bool {
+	_, ok := err.(ErrIssueWasClosed)
+	return ok
+}
+
+func (err ErrIssueWasClosed) Error() string {
+	return fmt.Sprintf("Issue [%d] %d was already closed", err.ID, err.Index)
+}
+
 // Issue represents an issue or pull request of repository.
 type Issue struct {
 	ID               int64                  `xorm:"pk autoincr"`
@@ -47,14 +110,14 @@ type Issue struct {
 	PosterID         int64                  `xorm:"INDEX"`
 	Poster           *user_model.User       `xorm:"-"`
 	OriginalAuthor   string
-	OriginalAuthorID int64                   `xorm:"index"`
-	Title            string                  `xorm:"name"`
-	Content          string                  `xorm:"LONGTEXT"`
-	RenderedContent  string                  `xorm:"-"`
-	Labels           []*Label                `xorm:"-"`
-	MilestoneID      int64                   `xorm:"INDEX"`
-	Milestone        *issues_model.Milestone `xorm:"-"`
-	Project          *project_model.Project  `xorm:"-"`
+	OriginalAuthorID int64                  `xorm:"index"`
+	Title            string                 `xorm:"name"`
+	Content          string                 `xorm:"LONGTEXT"`
+	RenderedContent  string                 `xorm:"-"`
+	Labels           []*Label               `xorm:"-"`
+	MilestoneID      int64                  `xorm:"INDEX"`
+	Milestone        *Milestone             `xorm:"-"`
+	Project          *project_model.Project `xorm:"-"`
 	Priority         int
 	AssigneeID       int64            `xorm:"-"`
 	Assignee         *user_model.User `xorm:"-"`
@@ -73,7 +136,7 @@ type Issue struct {
 
 	Attachments      []*repo_model.Attachment           `xorm:"-"`
 	Comments         []*Comment                         `xorm:"-"`
-	Reactions        issues_model.ReactionList          `xorm:"-"`
+	Reactions        ReactionList                       `xorm:"-"`
 	TotalTrackedTime int64                              `xorm:"-"`
 	Assignees        []*user_model.User                 `xorm:"-"`
 	ForeignReference *foreignreference.ForeignReference `xorm:"-"`
@@ -107,7 +170,8 @@ func init() {
 	db.RegisterModel(new(IssueIndex))
 }
 
-func (issue *Issue) loadTotalTimes(ctx context.Context) (err error) {
+// LoadTotalTimes load total tracked time
+func (issue *Issue) LoadTotalTimes(ctx context.Context) (err error) {
 	opts := FindTrackedTimesOptions{IssueID: issue.ID}
 	issue.TotalTrackedTime, err = opts.toSession(db.GetEngine(ctx)).SumInt(&TrackedTime{}, "time")
 	if err != nil {
@@ -237,7 +301,7 @@ func (issue *Issue) loadReactions(ctx context.Context) (err error) {
 	if issue.Reactions != nil {
 		return nil
 	}
-	reactions, _, err := issues_model.FindReactions(ctx, issues_model.FindReactionsOptions{
+	reactions, _, err := FindReactions(ctx, FindReactionsOptions{
 		IssueID: issue.ID,
 	})
 	if err != nil {
@@ -247,7 +311,7 @@ func (issue *Issue) loadReactions(ctx context.Context) (err error) {
 		return err
 	}
 	// Load reaction user data
-	if _, err := issues_model.ReactionList(reactions).LoadUsers(ctx, issue.Repo); err != nil {
+	if _, err := ReactionList(reactions).LoadUsers(ctx, issue.Repo); err != nil {
 		return err
 	}
 
@@ -292,15 +356,16 @@ func (issue *Issue) loadForeignReference(ctx context.Context) (err error) {
 
 func (issue *Issue) loadMilestone(ctx context.Context) (err error) {
 	if (issue.Milestone == nil || issue.Milestone.ID != issue.MilestoneID) && issue.MilestoneID > 0 {
-		issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, issue.RepoID, issue.MilestoneID)
-		if err != nil && !issues_model.IsErrMilestoneNotExist(err) {
+		issue.Milestone, err = GetMilestoneByRepoID(ctx, issue.RepoID, issue.MilestoneID)
+		if err != nil && !IsErrMilestoneNotExist(err) {
 			return fmt.Errorf("getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %v", issue.RepoID, issue.MilestoneID, err)
 		}
 	}
 	return nil
 }
 
-func (issue *Issue) loadAttributes(ctx context.Context) (err error) {
+// LoadAttributes loads the attribute of this issue.
+func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
 	if err = issue.LoadRepo(ctx); err != nil {
 		return
 	}
@@ -345,7 +410,7 @@ func (issue *Issue) loadAttributes(ctx context.Context) (err error) {
 		return err
 	}
 	if issue.isTimetrackerEnabled(ctx) {
-		if err = issue.loadTotalTimes(ctx); err != nil {
+		if err = issue.LoadTotalTimes(ctx); err != nil {
 			return err
 		}
 	}
@@ -357,11 +422,6 @@ func (issue *Issue) loadAttributes(ctx context.Context) (err error) {
 	return issue.loadReactions(ctx)
 }
 
-// LoadAttributes loads the attribute of this issue.
-func (issue *Issue) LoadAttributes() error {
-	return issue.loadAttributes(db.DefaultContext)
-}
-
 // LoadMilestone load milestone of this issue.
 func (issue *Issue) LoadMilestone() error {
 	return issue.loadMilestone(db.DefaultContext)
@@ -590,15 +650,6 @@ func ReplaceIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (e
 	return committer.Commit()
 }
 
-// ReadBy sets issue to be read by given user.
-func (issue *Issue) ReadBy(ctx context.Context, userID int64) error {
-	if err := UpdateIssueUserByRead(userID, issue.ID); err != nil {
-		return err
-	}
-
-	return setIssueNotificationStatusReadIfUnread(ctx, userID, issue.ID)
-}
-
 // UpdateIssueCols updates cols of issue
 func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error {
 	if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(cols...).Update(issue); err != nil {
@@ -609,7 +660,7 @@ func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error {
 
 func changeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed, isMergePull bool) (*Comment, error) {
 	// Reload the issue
-	currentIssue, err := getIssueByID(ctx, issue.ID)
+	currentIssue, err := GetIssueByID(ctx, issue.ID)
 	if err != nil {
 		return nil, err
 	}
@@ -666,7 +717,7 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use
 
 	// Update issue count of milestone
 	if issue.MilestoneID > 0 {
-		if err := issues_model.UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil {
+		if err := UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil {
 			return nil, err
 		}
 	}
@@ -730,7 +781,7 @@ func ChangeIssueTitle(issue *Issue, doer *user_model.User, oldTitle string) (err
 	if _, err = CreateCommentCtx(ctx, opts); err != nil {
 		return fmt.Errorf("createComment: %v", err)
 	}
-	if err = issue.addCrossReferences(ctx, doer, true); err != nil {
+	if err = issue.AddCrossReferences(ctx, doer, true); err != nil {
 		return err
 	}
 
@@ -772,7 +823,7 @@ func ChangeIssueRef(issue *Issue, doer *user_model.User, oldRef string) (err err
 
 // AddDeletePRBranchComment adds delete branch comment for pull request issue
 func AddDeletePRBranchComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issueID int64, branchName string) error {
-	issue, err := getIssueByID(ctx, issueID)
+	issue, err := GetIssueByID(ctx, issueID)
 	if err != nil {
 		return err
 	}
@@ -815,12 +866,12 @@ func ChangeIssueContent(issue *Issue, doer *user_model.User, content string) (er
 	}
 	defer committer.Close()
 
-	hasContentHistory, err := issues_model.HasIssueContentHistory(ctx, issue.ID, 0)
+	hasContentHistory, err := HasIssueContentHistory(ctx, issue.ID, 0)
 	if err != nil {
 		return fmt.Errorf("HasIssueContentHistory: %v", err)
 	}
 	if !hasContentHistory {
-		if err = issues_model.SaveIssueContentHistory(ctx, issue.PosterID, issue.ID, 0,
+		if err = SaveIssueContentHistory(ctx, issue.PosterID, issue.ID, 0,
 			issue.CreatedUnix, issue.Content, true); err != nil {
 			return fmt.Errorf("SaveIssueContentHistory: %v", err)
 		}
@@ -832,12 +883,12 @@ func ChangeIssueContent(issue *Issue, doer *user_model.User, content string) (er
 		return fmt.Errorf("UpdateIssueCols: %v", err)
 	}
 
-	if err = issues_model.SaveIssueContentHistory(ctx, doer.ID, issue.ID, 0,
+	if err = SaveIssueContentHistory(ctx, doer.ID, issue.ID, 0,
 		timeutil.TimeStampNow(), issue.Content, false); err != nil {
 		return fmt.Errorf("SaveIssueContentHistory: %v", err)
 	}
 
-	if err = issue.addCrossReferences(ctx, doer, true); err != nil {
+	if err = issue.AddCrossReferences(ctx, doer, true); err != nil {
 		return fmt.Errorf("addCrossReferences: %v", err)
 	}
 
@@ -907,13 +958,14 @@ type NewIssueOptions struct {
 	IsPull      bool
 }
 
-func newIssue(ctx context.Context, doer *user_model.User, opts NewIssueOptions) (err error) {
+// NewIssueWithIndex creates issue with given index
+func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssueOptions) (err error) {
 	e := db.GetEngine(ctx)
 	opts.Issue.Title = strings.TrimSpace(opts.Issue.Title)
 
 	if opts.Issue.MilestoneID > 0 {
-		milestone, err := issues_model.GetMilestoneByRepoID(ctx, opts.Issue.RepoID, opts.Issue.MilestoneID)
-		if err != nil && !issues_model.IsErrMilestoneNotExist(err) {
+		milestone, err := GetMilestoneByRepoID(ctx, opts.Issue.RepoID, opts.Issue.MilestoneID)
+		if err != nil && !IsErrMilestoneNotExist(err) {
 			return fmt.Errorf("getMilestoneByID: %v", err)
 		}
 
@@ -937,7 +989,7 @@ func newIssue(ctx context.Context, doer *user_model.User, opts NewIssueOptions)
 	}
 
 	if opts.Issue.MilestoneID > 0 {
-		if err := issues_model.UpdateMilestoneCounters(ctx, opts.Issue.MilestoneID); err != nil {
+		if err := UpdateMilestoneCounters(ctx, opts.Issue.MilestoneID); err != nil {
 			return err
 		}
 
@@ -987,7 +1039,7 @@ func newIssue(ctx context.Context, doer *user_model.User, opts NewIssueOptions)
 		}
 	}
 
-	if err = newIssueUsers(ctx, opts.Repo, opts.Issue); err != nil {
+	if err = NewIssueUsers(ctx, opts.Repo, opts.Issue); err != nil {
 		return err
 	}
 
@@ -1004,36 +1056,11 @@ func newIssue(ctx context.Context, doer *user_model.User, opts NewIssueOptions)
 			}
 		}
 	}
-	if err = opts.Issue.loadAttributes(ctx); err != nil {
+	if err = opts.Issue.LoadAttributes(ctx); err != nil {
 		return err
 	}
 
-	return opts.Issue.addCrossReferences(ctx, doer, false)
-}
-
-// RecalculateIssueIndexForRepo create issue_index for repo if not exist and
-// update it based on highest index of existing issues assigned to a repo
-func RecalculateIssueIndexForRepo(repoID int64) error {
-	ctx, committer, err := db.TxContext()
-	if err != nil {
-		return err
-	}
-	defer committer.Close()
-
-	if err := db.UpsertResourceIndex(db.GetEngine(ctx), "issue_index", repoID); err != nil {
-		return err
-	}
-
-	var max int64
-	if _, err := db.GetEngine(ctx).Select(" MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&max); err != nil {
-		return err
-	}
-
-	if _, err := db.GetEngine(ctx).Exec("UPDATE `issue_index` SET max_index=? WHERE group_id=?", max, repoID); err != nil {
-		return err
-	}
-
-	return committer.Commit()
+	return opts.Issue.AddCrossReferences(ctx, doer, false)
 }
 
 // NewIssue creates new issue with labels for repository.
@@ -1051,13 +1078,13 @@ func NewIssue(repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids
 	}
 	defer committer.Close()
 
-	if err = newIssue(ctx, issue.Poster, NewIssueOptions{
+	if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{
 		Repo:        repo,
 		Issue:       issue,
 		LabelIDs:    labelIDs,
 		Attachments: uuids,
 	}); err != nil {
-		if IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) {
+		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) {
 			return err
 		}
 		return fmt.Errorf("newIssue: %v", err)
@@ -1114,10 +1141,11 @@ func GetIssueWithAttrsByIndex(repoID, index int64) (*Issue, error) {
 	if err != nil {
 		return nil, err
 	}
-	return issue, issue.LoadAttributes()
+	return issue, issue.LoadAttributes(db.DefaultContext)
 }
 
-func getIssueByID(ctx context.Context, id int64) (*Issue, error) {
+// GetIssueByID returns an issue by given ID.
+func GetIssueByID(ctx context.Context, id int64) (*Issue, error) {
 	issue := new(Issue)
 	has, err := db.GetEngine(ctx).ID(id).Get(issue)
 	if err != nil {
@@ -1130,16 +1158,11 @@ func getIssueByID(ctx context.Context, id int64) (*Issue, error) {
 
 // GetIssueWithAttrsByID returns an issue with attributes by given ID.
 func GetIssueWithAttrsByID(id int64) (*Issue, error) {
-	issue, err := getIssueByID(db.DefaultContext, id)
+	issue, err := GetIssueByID(db.DefaultContext, id)
 	if err != nil {
 		return nil, err
 	}
-	return issue, issue.loadAttributes(db.DefaultContext)
-}
-
-// GetIssueByID returns an issue by given ID.
-func GetIssueByID(id int64) (*Issue, error) {
-	return getIssueByID(db.DefaultContext, id)
+	return issue, issue.LoadAttributes(db.DefaultContext)
 }
 
 // GetIssuesByIDs return issues with the given IDs.
@@ -1156,7 +1179,7 @@ func GetIssueIDsByRepoID(ctx context.Context, repoID int64) ([]int64, error) {
 }
 
 // IssuesOptions represents options of an issue.
-type IssuesOptions struct {
+type IssuesOptions struct { //nolint
 	db.ListOptions
 	RepoID             int64 // overwrites RepoCond if not 0
 	RepoCond           builder.Cond
@@ -1534,7 +1557,7 @@ func GetParticipantsIDsByIssueID(issueID int64) ([]int64, error) {
 
 // IsUserParticipantsOfIssue return true if user is participants of an issue
 func IsUserParticipantsOfIssue(user *user_model.User, issue *Issue) bool {
-	userIDs, err := issue.getParticipantIDsByIssue(db.DefaultContext)
+	userIDs, err := issue.GetParticipantIDsByIssue(db.DefaultContext)
 	if err != nil {
 		log.Error(err.Error())
 		return false
@@ -1577,17 +1600,6 @@ const (
 	FilterModeYourRepositories
 )
 
-func parseCountResult(results []map[string][]byte) int64 {
-	if len(results) == 0 {
-		return 0
-	}
-	for _, result := range results[0] {
-		c, _ := strconv.ParseInt(string(result), 10, 64)
-		return c
-	}
-	return 0
-}
-
 // IssueStatsOptions contains parameters accepted by GetIssueStats.
 type IssueStatsOptions struct {
 	RepoID            int64
@@ -1602,14 +1614,15 @@ type IssueStatsOptions struct {
 }
 
 const (
+	// MaxQueryParameters represents the max query parameters
 	// When queries are broken down in parts because of the number
 	// of parameters, attempt to break by this amount
-	maxQueryParameters = 300
+	MaxQueryParameters = 300
 )
 
 // GetIssueStats returns issue statistic information by given conditions.
 func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) {
-	if len(opts.IssueIDs) <= maxQueryParameters {
+	if len(opts.IssueIDs) <= MaxQueryParameters {
 		return getIssueStatsChunk(opts, opts.IssueIDs)
 	}
 
@@ -1619,7 +1632,7 @@ func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) {
 	// ids in a temporary table and join from them.
 	accum := &IssueStats{}
 	for i := 0; i < len(opts.IssueIDs); {
-		chunk := i + maxQueryParameters
+		chunk := i + MaxQueryParameters
 		if chunk > len(opts.IssueIDs) {
 			chunk = len(opts.IssueIDs)
 		}
@@ -1950,7 +1963,7 @@ func UpdateIssueByAPI(issue *Issue, doer *user_model.User) (statusChangeComment
 	}
 
 	// Reload the issue
-	currentIssue, err := getIssueByID(ctx, issue.ID)
+	currentIssue, err := GetIssueByID(ctx, issue.ID)
 	if err != nil {
 		return nil, false, err
 	}
@@ -1985,7 +1998,7 @@ func UpdateIssueByAPI(issue *Issue, doer *user_model.User) (statusChangeComment
 		}
 	}
 
-	if err := issue.addCrossReferences(ctx, doer, true); err != nil {
+	if err := issue.AddCrossReferences(ctx, doer, true); err != nil {
 		return nil, false, err
 	}
 	return statusChangeComment, titleChanged, committer.Commit()
@@ -2016,22 +2029,8 @@ func UpdateIssueDeadline(issue *Issue, deadlineUnix timeutil.TimeStamp, doer *us
 	return committer.Commit()
 }
 
-// DeleteIssue deletes the issue
-func DeleteIssue(issue *Issue) error {
-	ctx, committer, err := db.TxContext()
-	if err != nil {
-		return err
-	}
-	defer committer.Close()
-
-	if err := deleteIssue(ctx, issue); err != nil {
-		return err
-	}
-
-	return committer.Commit()
-}
-
-func deleteInIssue(ctx context.Context, issueID int64, beans ...interface{}) error {
+// DeleteInIssue delete records in beans with external key issue_id = ?
+func DeleteInIssue(ctx context.Context, issueID int64, beans ...interface{}) error {
 	e := db.GetEngine(ctx)
 	for _, bean := range beans {
 		if _, err := e.In("issue_id", issueID).Delete(bean); err != nil {
@@ -2041,103 +2040,14 @@ func deleteInIssue(ctx context.Context, issueID int64, beans ...interface{}) err
 	return nil
 }
 
-func deleteIssue(ctx context.Context, issue *Issue) error {
-	e := db.GetEngine(ctx)
-	if _, err := e.ID(issue.ID).NoAutoCondition().Delete(issue); err != nil {
-		return err
-	}
-
-	if issue.IsPull {
-		if _, err := e.ID(issue.RepoID).Decr("num_pulls").Update(new(repo_model.Repository)); err != nil {
-			return err
-		}
-		if issue.IsClosed {
-			if _, err := e.ID(issue.RepoID).Decr("num_closed_pulls").Update(new(repo_model.Repository)); err != nil {
-				return err
-			}
-		}
-	} else {
-		if _, err := e.ID(issue.RepoID).Decr("num_issues").Update(new(repo_model.Repository)); err != nil {
-			return err
-		}
-		if issue.IsClosed {
-			if _, err := e.ID(issue.RepoID).Decr("num_closed_issues").Update(new(repo_model.Repository)); err != nil {
-				return err
-			}
-		}
-	}
-
-	// delete actions assigned to this issue
-	subQuery := builder.Select("`id`").
-		From("`comment`").
-		Where(builder.Eq{"`issue_id`": issue.ID})
-	if _, err := e.In("comment_id", subQuery).Delete(&Action{}); err != nil {
-		return err
-	}
-
-	if _, err := e.Table("action").Where("repo_id = ?", issue.RepoID).
-		In("op_type", ActionCreateIssue, ActionCreatePullRequest).
-		Where("content LIKE ?", strconv.FormatInt(issue.ID, 10)+"|%").
-		Delete(&Action{}); err != nil {
-		return err
-	}
-
-	// find attachments related to this issue and remove them
-	var attachments []*repo_model.Attachment
-	if err := e.In("issue_id", issue.ID).Find(&attachments); err != nil {
-		return err
-	}
-
-	for i := range attachments {
-		admin_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", attachments[i].RelativePath())
-	}
-
-	// delete all database data still assigned to this issue
-	if err := deleteInIssue(ctx, issue.ID,
-		&issues_model.ContentHistory{},
-		&Comment{},
-		&IssueLabel{},
-		&IssueDependency{},
-		&IssueAssignees{},
-		&IssueUser{},
-		&Notification{},
-		&issues_model.Reaction{},
-		&IssueWatch{},
-		&Stopwatch{},
-		&TrackedTime{},
-		&project_model.ProjectIssue{},
-		&repo_model.Attachment{},
-		&PullRequest{},
-	); err != nil {
-		return err
-	}
-
-	// References to this issue in other issues
-	if _, err := e.In("ref_issue_id", issue.ID).Delete(&Comment{}); err != nil {
-		return err
-	}
-
-	// Delete dependencies for issues in other repositories
-	if _, err := e.In("dependency_id", issue.ID).Delete(&IssueDependency{}); err != nil {
-		return err
-	}
-
-	// delete from dependent issues
-	if _, err := e.In("dependent_issue_id", issue.ID).Delete(&Comment{}); err != nil {
-		return err
-	}
-
-	return nil
-}
-
 // DependencyInfo represents high level information about an issue which is a dependency of another issue.
 type DependencyInfo struct {
 	Issue                 `xorm:"extends"`
 	repo_model.Repository `xorm:"extends"`
 }
 
-// getParticipantIDsByIssue returns all userIDs who are participated in comments of an issue and issue author
-func (issue *Issue) getParticipantIDsByIssue(ctx context.Context) ([]int64, error) {
+// GetParticipantIDsByIssue returns all userIDs who are participated in comments of an issue and issue author
+func (issue *Issue) GetParticipantIDsByIssue(ctx context.Context) ([]int64, error) {
 	if issue == nil {
 		return nil, nil
 	}
@@ -2196,9 +2106,9 @@ func (issue *Issue) BlockingDependencies(ctx context.Context) (issueDeps []*Depe
 
 func updateIssueClosedNum(ctx context.Context, issue *Issue) (err error) {
 	if issue.IsPull {
-		err = repoStatsCorrectNumClosed(ctx, issue.RepoID, true, "num_closed_pulls")
+		err = repo_model.StatsCorrectNumClosed(ctx, issue.RepoID, true, "num_closed_pulls")
 	} else {
-		err = repoStatsCorrectNumClosed(ctx, issue.RepoID, false, "num_closed_issues")
+		err = repo_model.StatsCorrectNumClosed(ctx, issue.RepoID, false, "num_closed_issues")
 	}
 	return
 }
@@ -2363,6 +2273,17 @@ func UpdateIssuesMigrationsByType(gitServiceType api.GitServiceType, originalAut
 	return err
 }
 
+func migratedIssueCond(tp api.GitServiceType) builder.Cond {
+	return builder.In("issue_id",
+		builder.Select("issue.id").
+			From("issue").
+			InnerJoin("repository", "issue.repo_id = repository.id").
+			Where(builder.Eq{
+				"repository.original_service_type": tp,
+			}),
+	)
+}
+
 // UpdateReactionsMigrationsByType updates all migrated repositories' reactions from gitServiceType to replace originalAuthorID to posterID
 func UpdateReactionsMigrationsByType(gitServiceType api.GitServiceType, originalAuthorID string, userID int64) error {
 	_, err := db.GetEngine(db.DefaultContext).Table("reaction").
@@ -2376,13 +2297,14 @@ func UpdateReactionsMigrationsByType(gitServiceType api.GitServiceType, original
 	return err
 }
 
-func deleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) {
+// DeleteIssuesByRepoID deletes issues by repositories id
+func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) {
 	deleteCond := builder.Select("id").From("issue").Where(builder.Eq{"issue.repo_id": repoID})
 
 	sess := db.GetEngine(ctx)
 	// Delete content histories
 	if _, err = sess.In("issue_id", deleteCond).
-		Delete(&issues_model.ContentHistory{}); err != nil {
+		Delete(&ContentHistory{}); err != nil {
 		return
 	}
 
@@ -2410,7 +2332,7 @@ func deleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []
 	}
 
 	if _, err = sess.In("issue_id", deleteCond).
-		Delete(&issues_model.Reaction{}); err != nil {
+		Delete(&Reaction{}); err != nil {
 		return
 	}
 
@@ -2477,3 +2399,50 @@ func (issue *Issue) GetExternalName() string { return issue.OriginalAuthor }
 
 // GetExternalID ExternalUserRemappable interface
 func (issue *Issue) GetExternalID() int64 { return issue.OriginalAuthorID }
+
+// CountOrphanedIssues count issues without a repo
+func CountOrphanedIssues() (int64, error) {
+	return db.GetEngine(db.DefaultContext).Table("issue").
+		Join("LEFT", "repository", "issue.repo_id=repository.id").
+		Where(builder.IsNull{"repository.id"}).
+		Select("COUNT(`issue`.`id`)").
+		Count()
+}
+
+// DeleteOrphanedIssues delete issues without a repo
+func DeleteOrphanedIssues() error {
+	ctx, committer, err := db.TxContext()
+	if err != nil {
+		return err
+	}
+	defer committer.Close()
+
+	var ids []int64
+
+	if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id").
+		Join("LEFT", "repository", "issue.repo_id=repository.id").
+		Where(builder.IsNull{"repository.id"}).GroupBy("issue.repo_id").
+		Find(&ids); err != nil {
+		return err
+	}
+
+	var attachmentPaths []string
+	for i := range ids {
+		paths, err := DeleteIssuesByRepoID(ctx, ids[i])
+		if err != nil {
+			return err
+		}
+		attachmentPaths = append(attachmentPaths, paths...)
+	}
+
+	if err := committer.Commit(); err != nil {
+		return err
+	}
+	committer.Close()
+
+	// Remove issue attachment files.
+	for i := range attachmentPaths {
+		admin_model.RemoveAllWithNotice(db.DefaultContext, "Delete issue attachment", attachmentPaths[i])
+	}
+	return nil
+}
diff --git a/models/issues/issue_index.go b/models/issues/issue_index.go
new file mode 100644
index 0000000000..100e814317
--- /dev/null
+++ b/models/issues/issue_index.go
@@ -0,0 +1,32 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package issues
+
+import "code.gitea.io/gitea/models/db"
+
+// RecalculateIssueIndexForRepo create issue_index for repo if not exist and
+// update it based on highest index of existing issues assigned to a repo
+func RecalculateIssueIndexForRepo(repoID int64) error {
+	ctx, committer, err := db.TxContext()
+	if err != nil {
+		return err
+	}
+	defer committer.Close()
+
+	if err := db.UpsertResourceIndex(ctx, "issue_index", repoID); err != nil {
+		return err
+	}
+
+	var max int64
+	if _, err := db.GetEngine(ctx).Select(" MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&max); err != nil {
+		return err
+	}
+
+	if _, err := db.GetEngine(ctx).Exec("UPDATE `issue_index` SET max_index=? WHERE group_id=?", max, repoID); err != nil {
+		return err
+	}
+
+	return committer.Commit()
+}
diff --git a/models/issue_list.go b/models/issues/issue_list.go
similarity index 96%
rename from models/issue_list.go
rename to models/issues/issue_list.go
index a5fc095e12..20e9949b66 100644
--- a/models/issue_list.go
+++ b/models/issues/issue_list.go
@@ -2,14 +2,13 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues
 
 import (
 	"context"
 	"fmt"
 
 	"code.gitea.io/gitea/models/db"
-	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/container"
@@ -20,11 +19,6 @@ import (
 // IssueList defines a list of issues
 type IssueList []*Issue
 
-const (
-	// default variables number on IN () in SQL
-	defaultMaxInSize = 50
-)
-
 // get the repo IDs to be loaded later, these IDs are for issue.Repo and issue.PullRequest.HeadRepo
 func (issues IssueList) getRepoIDs() []int64 {
 	repoIDs := make(map[int64]struct{}, len(issues))
@@ -48,7 +42,7 @@ func (issues IssueList) loadRepositories(ctx context.Context) ([]*repo_model.Rep
 	repoMaps := make(map[int64]*repo_model.Repository, len(repoIDs))
 	left := len(repoIDs)
 	for left > 0 {
-		limit := defaultMaxInSize
+		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
@@ -102,7 +96,7 @@ func (issues IssueList) loadPosters(ctx context.Context) error {
 	posterMaps := make(map[int64]*user_model.User, len(posterIDs))
 	left := len(posterIDs)
 	for left > 0 {
-		limit := defaultMaxInSize
+		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
@@ -150,7 +144,7 @@ func (issues IssueList) loadLabels(ctx context.Context) error {
 	issueIDs := issues.getIssueIDs()
 	left := len(issueIDs)
 	for left > 0 {
-		limit := defaultMaxInSize
+		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
@@ -205,10 +199,10 @@ func (issues IssueList) loadMilestones(ctx context.Context) error {
 		return nil
 	}
 
-	milestoneMaps := make(map[int64]*issues_model.Milestone, len(milestoneIDs))
+	milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs))
 	left := len(milestoneIDs)
 	for left > 0 {
-		limit := defaultMaxInSize
+		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
@@ -242,7 +236,7 @@ func (issues IssueList) loadAssignees(ctx context.Context) error {
 	issueIDs := issues.getIssueIDs()
 	left := len(issueIDs)
 	for left > 0 {
-		limit := defaultMaxInSize
+		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
@@ -298,7 +292,7 @@ func (issues IssueList) loadPullRequests(ctx context.Context) error {
 	pullRequestMaps := make(map[int64]*PullRequest, len(issuesIDs))
 	left := len(issuesIDs)
 	for left > 0 {
-		limit := defaultMaxInSize
+		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
@@ -342,7 +336,7 @@ func (issues IssueList) loadAttachments(ctx context.Context) (err error) {
 	issuesIDs := issues.getIssueIDs()
 	left := len(issuesIDs)
 	for left > 0 {
-		limit := defaultMaxInSize
+		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
@@ -387,7 +381,7 @@ func (issues IssueList) loadComments(ctx context.Context, cond builder.Cond) (er
 	issuesIDs := issues.getIssueIDs()
 	left := len(issuesIDs)
 	for left > 0 {
-		limit := defaultMaxInSize
+		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
@@ -443,7 +437,7 @@ func (issues IssueList) loadTotalTrackedTimes(ctx context.Context) (err error) {
 
 	left := len(ids)
 	for left > 0 {
-		limit := defaultMaxInSize
+		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
diff --git a/models/issue_list_test.go b/models/issues/issue_list_test.go
similarity index 71%
rename from models/issue_list_test.go
rename to models/issues/issue_list_test.go
index 2916a7302f..6b978f9ae6 100644
--- a/models/issue_list_test.go
+++ b/models/issues/issue_list_test.go
@@ -2,11 +2,12 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues_test
 
 import (
 	"testing"
 
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/setting"
 
@@ -16,10 +17,10 @@ import (
 func TestIssueList_LoadRepositories(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	issueList := IssueList{
-		unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue),
-		unittest.AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue),
-		unittest.AssertExistsAndLoadBean(t, &Issue{ID: 4}).(*Issue),
+	issueList := issues_model.IssueList{
+		unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue),
+		unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}).(*issues_model.Issue),
+		unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 4}).(*issues_model.Issue),
 	}
 
 	repos, err := issueList.LoadRepositories()
@@ -33,9 +34,9 @@ func TestIssueList_LoadRepositories(t *testing.T) {
 func TestIssueList_LoadAttributes(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	setting.Service.EnableTimetracking = true
-	issueList := IssueList{
-		unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue),
-		unittest.AssertExistsAndLoadBean(t, &Issue{ID: 4}).(*Issue),
+	issueList := issues_model.IssueList{
+		unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue),
+		unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 4}).(*issues_model.Issue),
 	}
 
 	assert.NoError(t, issueList.LoadAttributes())
@@ -43,7 +44,7 @@ func TestIssueList_LoadAttributes(t *testing.T) {
 		assert.EqualValues(t, issue.RepoID, issue.Repo.ID)
 		for _, label := range issue.Labels {
 			assert.EqualValues(t, issue.RepoID, label.RepoID)
-			unittest.AssertExistsAndLoadBean(t, &IssueLabel{IssueID: issue.ID, LabelID: label.ID})
+			unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label.ID})
 		}
 		if issue.PosterID > 0 {
 			assert.EqualValues(t, issue.PosterID, issue.Poster.ID)
diff --git a/models/issue_lock.go b/models/issues/issue_lock.go
similarity index 98%
rename from models/issue_lock.go
rename to models/issues/issue_lock.go
index a122f618d0..7b52429ef7 100644
--- a/models/issue_lock.go
+++ b/models/issues/issue_lock.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues
 
 import (
 	"code.gitea.io/gitea/models/db"
diff --git a/models/issue_project.go b/models/issues/issue_project.go
similarity index 99%
rename from models/issue_project.go
rename to models/issues/issue_project.go
index 0f8c61977b..5e0a337f7d 100644
--- a/models/issue_project.go
+++ b/models/issues/issue_project.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues
 
 import (
 	"context"
diff --git a/models/issue_test.go b/models/issues/issue_test.go
similarity index 73%
rename from models/issue_test.go
rename to models/issues/issue_test.go
index 5b2f461a84..019e578da8 100644
--- a/models/issue_test.go
+++ b/models/issues/issue_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues_test
 
 import (
 	"context"
@@ -29,18 +29,18 @@ func TestIssue_ReplaceLabels(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	testSuccess := func(issueID int64, labelIDs []int64) {
-		issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: issueID}).(*Issue)
+		issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueID}).(*issues_model.Issue)
 		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository)
 		doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
 
-		labels := make([]*Label, len(labelIDs))
+		labels := make([]*issues_model.Label, len(labelIDs))
 		for i, labelID := range labelIDs {
-			labels[i] = unittest.AssertExistsAndLoadBean(t, &Label{ID: labelID, RepoID: repo.ID}).(*Label)
+			labels[i] = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID, RepoID: repo.ID}).(*issues_model.Label)
 		}
-		assert.NoError(t, ReplaceIssueLabels(issue, labels, doer))
-		unittest.AssertCount(t, &IssueLabel{IssueID: issueID}, len(labelIDs))
+		assert.NoError(t, issues_model.ReplaceIssueLabels(issue, labels, doer))
+		unittest.AssertCount(t, &issues_model.IssueLabel{IssueID: issueID}, len(labelIDs))
 		for _, labelID := range labelIDs {
-			unittest.AssertExistsAndLoadBean(t, &IssueLabel{IssueID: issueID, LabelID: labelID})
+			unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID})
 		}
 	}
 
@@ -52,15 +52,15 @@ func TestIssue_ReplaceLabels(t *testing.T) {
 func Test_GetIssueIDsByRepoID(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	ids, err := GetIssueIDsByRepoID(db.DefaultContext, 1)
+	ids, err := issues_model.GetIssueIDsByRepoID(db.DefaultContext, 1)
 	assert.NoError(t, err)
 	assert.Len(t, ids, 5)
 }
 
 func TestIssueAPIURL(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
-	err := issue.LoadAttributes()
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue)
+	err := issue.LoadAttributes(db.DefaultContext)
 
 	assert.NoError(t, err)
 	assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/issues/1", issue.APIURL())
@@ -69,7 +69,7 @@ func TestIssueAPIURL(t *testing.T) {
 func TestGetIssuesByIDs(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	testSuccess := func(expectedIssueIDs, nonExistentIssueIDs []int64) {
-		issues, err := GetIssuesByIDs(db.DefaultContext, append(expectedIssueIDs, nonExistentIssueIDs...))
+		issues, err := issues_model.GetIssuesByIDs(db.DefaultContext, append(expectedIssueIDs, nonExistentIssueIDs...))
 		assert.NoError(t, err)
 		actualIssueIDs := make([]int64, len(issues))
 		for i, issue := range issues {
@@ -85,9 +85,9 @@ func TestGetParticipantIDsByIssue(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	checkParticipants := func(issueID int64, userIDs []int) {
-		issue, err := GetIssueByID(issueID)
+		issue, err := issues_model.GetIssueByID(db.DefaultContext, issueID)
 		assert.NoError(t, err)
-		participants, err := issue.getParticipantIDsByIssue(db.DefaultContext)
+		participants, err := issue.GetParticipantIDsByIssue(db.DefaultContext)
 		if assert.NoError(t, err) {
 			participantsIDs := make([]int, len(participants))
 			for i, uid := range participants {
@@ -117,16 +117,16 @@ func TestIssue_ClearLabels(t *testing.T) {
 	}
 	for _, test := range tests {
 		assert.NoError(t, unittest.PrepareTestDatabase())
-		issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: test.issueID}).(*Issue)
+		issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: test.issueID}).(*issues_model.Issue)
 		doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: test.doerID}).(*user_model.User)
-		assert.NoError(t, ClearIssueLabels(issue, doer))
-		unittest.AssertNotExistsBean(t, &IssueLabel{IssueID: test.issueID})
+		assert.NoError(t, issues_model.ClearIssueLabels(issue, doer))
+		unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: test.issueID})
 	}
 }
 
 func TestUpdateIssueCols(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	issue := unittest.AssertExistsAndLoadBean(t, &Issue{}).(*Issue)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{}).(*issues_model.Issue)
 
 	const newTitle = "New Title for unit test"
 	issue.Title = newTitle
@@ -135,10 +135,10 @@ func TestUpdateIssueCols(t *testing.T) {
 	issue.Content = "This should have no effect"
 
 	now := time.Now().Unix()
-	assert.NoError(t, UpdateIssueCols(db.DefaultContext, issue, "name"))
+	assert.NoError(t, issues_model.UpdateIssueCols(db.DefaultContext, issue, "name"))
 	then := time.Now().Unix()
 
-	updatedIssue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: issue.ID}).(*Issue)
+	updatedIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID}).(*issues_model.Issue)
 	assert.EqualValues(t, newTitle, updatedIssue.Title)
 	assert.EqualValues(t, prevContent, updatedIssue.Content)
 	unittest.AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix))
@@ -147,18 +147,18 @@ func TestUpdateIssueCols(t *testing.T) {
 func TestIssues(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	for _, test := range []struct {
-		Opts             IssuesOptions
+		Opts             issues_model.IssuesOptions
 		ExpectedIssueIDs []int64
 	}{
 		{
-			IssuesOptions{
+			issues_model.IssuesOptions{
 				AssigneeID: 1,
 				SortType:   "oldest",
 			},
 			[]int64{1, 6},
 		},
 		{
-			IssuesOptions{
+			issues_model.IssuesOptions{
 				RepoCond: builder.In("repo_id", 1, 3),
 				SortType: "oldest",
 				ListOptions: db.ListOptions{
@@ -169,7 +169,7 @@ func TestIssues(t *testing.T) {
 			[]int64{1, 2, 3, 5},
 		},
 		{
-			IssuesOptions{
+			issues_model.IssuesOptions{
 				LabelIDs: []int64{1},
 				ListOptions: db.ListOptions{
 					Page:     1,
@@ -179,7 +179,7 @@ func TestIssues(t *testing.T) {
 			[]int64{2, 1},
 		},
 		{
-			IssuesOptions{
+			issues_model.IssuesOptions{
 				LabelIDs: []int64{1, 2},
 				ListOptions: db.ListOptions{
 					Page:     1,
@@ -189,7 +189,7 @@ func TestIssues(t *testing.T) {
 			[]int64{}, // issues with **both** label 1 and 2, none of these issues matches, TODO: add more tests
 		},
 	} {
-		issues, err := Issues(&test.Opts)
+		issues, err := issues_model.Issues(&test.Opts)
 		assert.NoError(t, err)
 		if assert.Len(t, issues, len(test.ExpectedIssueIDs)) {
 			for i, issue := range issues {
@@ -202,16 +202,16 @@ func TestIssues(t *testing.T) {
 func TestGetUserIssueStats(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	for _, test := range []struct {
-		Opts               UserIssueStatsOptions
-		ExpectedIssueStats IssueStats
+		Opts               issues_model.UserIssueStatsOptions
+		ExpectedIssueStats issues_model.IssueStats
 	}{
 		{
-			UserIssueStatsOptions{
+			issues_model.UserIssueStatsOptions{
 				UserID:     1,
 				RepoIDs:    []int64{1},
-				FilterMode: FilterModeAll,
+				FilterMode: issues_model.FilterModeAll,
 			},
-			IssueStats{
+			issues_model.IssueStats{
 				YourRepositoriesCount: 1, // 6
 				AssignCount:           1, // 6
 				CreateCount:           1, // 6
@@ -220,13 +220,13 @@ func TestGetUserIssueStats(t *testing.T) {
 			},
 		},
 		{
-			UserIssueStatsOptions{
+			issues_model.UserIssueStatsOptions{
 				UserID:     1,
 				RepoIDs:    []int64{1},
-				FilterMode: FilterModeAll,
+				FilterMode: issues_model.FilterModeAll,
 				IsClosed:   true,
 			},
-			IssueStats{
+			issues_model.IssueStats{
 				YourRepositoriesCount: 1, // 6
 				AssignCount:           0,
 				CreateCount:           0,
@@ -235,11 +235,11 @@ func TestGetUserIssueStats(t *testing.T) {
 			},
 		},
 		{
-			UserIssueStatsOptions{
+			issues_model.UserIssueStatsOptions{
 				UserID:     1,
-				FilterMode: FilterModeAssign,
+				FilterMode: issues_model.FilterModeAssign,
 			},
-			IssueStats{
+			issues_model.IssueStats{
 				YourRepositoriesCount: 1, // 6
 				AssignCount:           1, // 6
 				CreateCount:           1, // 6
@@ -248,11 +248,11 @@ func TestGetUserIssueStats(t *testing.T) {
 			},
 		},
 		{
-			UserIssueStatsOptions{
+			issues_model.UserIssueStatsOptions{
 				UserID:     1,
-				FilterMode: FilterModeCreate,
+				FilterMode: issues_model.FilterModeCreate,
 			},
-			IssueStats{
+			issues_model.IssueStats{
 				YourRepositoriesCount: 1, // 6
 				AssignCount:           1, // 6
 				CreateCount:           1, // 6
@@ -261,11 +261,11 @@ func TestGetUserIssueStats(t *testing.T) {
 			},
 		},
 		{
-			UserIssueStatsOptions{
+			issues_model.UserIssueStatsOptions{
 				UserID:     1,
-				FilterMode: FilterModeMention,
+				FilterMode: issues_model.FilterModeMention,
 			},
-			IssueStats{
+			issues_model.IssueStats{
 				YourRepositoriesCount: 1, // 6
 				AssignCount:           1, // 6
 				CreateCount:           1, // 6
@@ -275,12 +275,12 @@ func TestGetUserIssueStats(t *testing.T) {
 			},
 		},
 		{
-			UserIssueStatsOptions{
+			issues_model.UserIssueStatsOptions{
 				UserID:     1,
-				FilterMode: FilterModeCreate,
+				FilterMode: issues_model.FilterModeCreate,
 				IssueIDs:   []int64{1},
 			},
-			IssueStats{
+			issues_model.IssueStats{
 				YourRepositoriesCount: 1, // 1
 				AssignCount:           1, // 1
 				CreateCount:           1, // 1
@@ -289,13 +289,13 @@ func TestGetUserIssueStats(t *testing.T) {
 			},
 		},
 		{
-			UserIssueStatsOptions{
+			issues_model.UserIssueStatsOptions{
 				UserID:     2,
 				Org:        unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}).(*organization.Organization),
 				Team:       unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 7}).(*organization.Team),
-				FilterMode: FilterModeAll,
+				FilterMode: issues_model.FilterModeAll,
 			},
-			IssueStats{
+			issues_model.IssueStats{
 				YourRepositoriesCount: 2,
 				AssignCount:           1,
 				CreateCount:           1,
@@ -304,7 +304,7 @@ func TestGetUserIssueStats(t *testing.T) {
 		},
 	} {
 		t.Run(fmt.Sprintf("%#v", test.Opts), func(t *testing.T) {
-			stats, err := GetUserIssueStats(test.Opts)
+			stats, err := issues_model.GetUserIssueStats(test.Opts)
 			if !assert.NoError(t, err) {
 				return
 			}
@@ -315,31 +315,31 @@ func TestGetUserIssueStats(t *testing.T) {
 
 func TestIssue_loadTotalTimes(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	ms, err := GetIssueByID(2)
+	ms, err := issues_model.GetIssueByID(db.DefaultContext, 2)
 	assert.NoError(t, err)
-	assert.NoError(t, ms.loadTotalTimes(db.DefaultContext))
+	assert.NoError(t, ms.LoadTotalTimes(db.DefaultContext))
 	assert.Equal(t, int64(3682), ms.TotalTrackedTime)
 }
 
 func TestIssue_SearchIssueIDsByKeyword(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	total, ids, err := SearchIssueIDsByKeyword(context.TODO(), "issue2", []int64{1}, 10, 0)
+	total, ids, err := issues_model.SearchIssueIDsByKeyword(context.TODO(), "issue2", []int64{1}, 10, 0)
 	assert.NoError(t, err)
 	assert.EqualValues(t, 1, total)
 	assert.EqualValues(t, []int64{2}, ids)
 
-	total, ids, err = SearchIssueIDsByKeyword(context.TODO(), "first", []int64{1}, 10, 0)
+	total, ids, err = issues_model.SearchIssueIDsByKeyword(context.TODO(), "first", []int64{1}, 10, 0)
 	assert.NoError(t, err)
 	assert.EqualValues(t, 1, total)
 	assert.EqualValues(t, []int64{1}, ids)
 
-	total, ids, err = SearchIssueIDsByKeyword(context.TODO(), "for", []int64{1}, 10, 0)
+	total, ids, err = issues_model.SearchIssueIDsByKeyword(context.TODO(), "for", []int64{1}, 10, 0)
 	assert.NoError(t, err)
 	assert.EqualValues(t, 5, total)
 	assert.ElementsMatch(t, []int64{1, 2, 3, 5, 11}, ids)
 
 	// issue1's comment id 2
-	total, ids, err = SearchIssueIDsByKeyword(context.TODO(), "good", []int64{1}, 10, 0)
+	total, ids, err = issues_model.SearchIssueIDsByKeyword(context.TODO(), "good", []int64{1}, 10, 0)
 	assert.NoError(t, err)
 	assert.EqualValues(t, 1, total)
 	assert.EqualValues(t, []int64{1}, ids)
@@ -349,23 +349,23 @@ func TestGetRepoIDsForIssuesOptions(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
 	for _, test := range []struct {
-		Opts            IssuesOptions
+		Opts            issues_model.IssuesOptions
 		ExpectedRepoIDs []int64
 	}{
 		{
-			IssuesOptions{
+			issues_model.IssuesOptions{
 				AssigneeID: 2,
 			},
 			[]int64{3, 32},
 		},
 		{
-			IssuesOptions{
+			issues_model.IssuesOptions{
 				RepoCond: builder.In("repo_id", 1, 2),
 			},
 			[]int64{1, 2},
 		},
 	} {
-		repoIDs, err := GetRepoIDsForIssuesOptions(&test.Opts, user)
+		repoIDs, err := issues_model.GetRepoIDsForIssuesOptions(&test.Opts, user)
 		assert.NoError(t, err)
 		if assert.Len(t, repoIDs, len(test.ExpectedRepoIDs)) {
 			for i, repoID := range repoIDs {
@@ -375,20 +375,20 @@ func TestGetRepoIDsForIssuesOptions(t *testing.T) {
 	}
 }
 
-func testInsertIssue(t *testing.T, title, content string, expectIndex int64) *Issue {
-	var newIssue Issue
+func testInsertIssue(t *testing.T, title, content string, expectIndex int64) *issues_model.Issue {
+	var newIssue issues_model.Issue
 	t.Run(title, func(t *testing.T) {
 		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
 		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
 
-		issue := Issue{
+		issue := issues_model.Issue{
 			RepoID:   repo.ID,
 			PosterID: user.ID,
 			Poster:   user,
 			Title:    title,
 			Content:  content,
 		}
-		err := NewIssue(repo, &issue, nil, nil)
+		err := issues_model.NewIssue(repo, &issue, nil, nil)
 		assert.NoError(t, err)
 
 		has, err := db.GetEngine(db.DefaultContext).ID(issue.ID).Get(&newIssue)
@@ -408,75 +408,23 @@ func TestIssue_InsertIssue(t *testing.T) {
 
 	// there are 5 issues and max index is 5 on repository 1, so this one should 6
 	issue := testInsertIssue(t, "my issue1", "special issue's comments?", 6)
-	_, err := db.GetEngine(db.DefaultContext).ID(issue.ID).Delete(new(Issue))
+	_, err := db.GetEngine(db.DefaultContext).ID(issue.ID).Delete(new(issues_model.Issue))
 	assert.NoError(t, err)
 
 	issue = testInsertIssue(t, `my issue2, this is my son's love \n \r \ `, "special issue's '' comments?", 7)
-	_, err = db.GetEngine(db.DefaultContext).ID(issue.ID).Delete(new(Issue))
+	_, err = db.GetEngine(db.DefaultContext).ID(issue.ID).Delete(new(issues_model.Issue))
 	assert.NoError(t, err)
 }
 
-func TestIssue_DeleteIssue(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-
-	issueIDs, err := GetIssueIDsByRepoID(db.DefaultContext, 1)
-	assert.NoError(t, err)
-	assert.EqualValues(t, 5, len(issueIDs))
-
-	issue := &Issue{
-		RepoID: 1,
-		ID:     issueIDs[2],
-	}
-
-	err = DeleteIssue(issue)
-	assert.NoError(t, err)
-	issueIDs, err = GetIssueIDsByRepoID(db.DefaultContext, 1)
-	assert.NoError(t, err)
-	assert.EqualValues(t, 4, len(issueIDs))
-
-	// check attachment removal
-	attachments, err := repo_model.GetAttachmentsByIssueID(db.DefaultContext, 4)
-	assert.NoError(t, err)
-	issue, err = GetIssueByID(4)
-	assert.NoError(t, err)
-	err = DeleteIssue(issue)
-	assert.NoError(t, err)
-	assert.EqualValues(t, 2, len(attachments))
-	for i := range attachments {
-		attachment, err := repo_model.GetAttachmentByUUID(db.DefaultContext, attachments[i].UUID)
-		assert.Error(t, err)
-		assert.True(t, repo_model.IsErrAttachmentNotExist(err))
-		assert.Nil(t, attachment)
-	}
-
-	// check issue dependencies
-	user, err := user_model.GetUserByID(1)
-	assert.NoError(t, err)
-	issue1, err := GetIssueByID(1)
-	assert.NoError(t, err)
-	issue2, err := GetIssueByID(2)
-	assert.NoError(t, err)
-	err = CreateIssueDependency(user, issue1, issue2)
-	assert.NoError(t, err)
-	left, err := IssueNoDependenciesLeft(db.DefaultContext, issue1)
-	assert.NoError(t, err)
-	assert.False(t, left)
-	err = DeleteIssue(&Issue{ID: 2})
-	assert.NoError(t, err)
-	left, err = IssueNoDependenciesLeft(db.DefaultContext, issue1)
-	assert.NoError(t, err)
-	assert.True(t, left)
-}
-
 func TestIssue_ResolveMentions(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	testSuccess := func(owner, repo, doer string, mentions []string, expected []int64) {
 		o := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: owner}).(*user_model.User)
 		r := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: o.ID, LowerName: repo}).(*repo_model.Repository)
-		issue := &Issue{RepoID: r.ID}
+		issue := &issues_model.Issue{RepoID: r.ID}
 		d := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: doer}).(*user_model.User)
-		resolved, err := ResolveIssueMentionsByVisibility(db.DefaultContext, issue, d, mentions)
+		resolved, err := issues_model.ResolveIssueMentionsByVisibility(db.DefaultContext, issue, d, mentions)
 		assert.NoError(t, err)
 		ids := make([]int64, len(resolved))
 		for i, user := range resolved {
@@ -523,7 +471,7 @@ func TestCorrectIssueStats(t *testing.T) {
 	// Each new issues will have a constant description "Bugs are nasty"
 	// Which will be used later on.
 
-	issueAmount := maxQueryParameters + 10
+	issueAmount := issues_model.MaxQueryParameters + 10
 
 	var wg sync.WaitGroup
 	for i := 0; i < issueAmount; i++ {
@@ -536,7 +484,7 @@ func TestCorrectIssueStats(t *testing.T) {
 	wg.Wait()
 
 	// Now we will get all issueID's that match the "Bugs are nasty" query.
-	total, ids, err := SearchIssueIDsByKeyword(context.TODO(), "Bugs are nasty", []int64{1}, issueAmount, 0)
+	total, ids, err := issues_model.SearchIssueIDsByKeyword(context.TODO(), "Bugs are nasty", []int64{1}, issueAmount, 0)
 
 	// Just to be sure.
 	assert.NoError(t, err)
@@ -544,7 +492,7 @@ func TestCorrectIssueStats(t *testing.T) {
 
 	// Now we will call the GetIssueStats with these IDs and if working,
 	// get the correct stats back.
-	issueStats, err := GetIssueStats(&IssueStatsOptions{
+	issueStats, err := issues_model.GetIssueStats(&issues_model.IssueStatsOptions{
 		RepoID:   1,
 		IssueIDs: ids,
 	})
@@ -556,19 +504,19 @@ func TestCorrectIssueStats(t *testing.T) {
 
 func TestIssueForeignReference(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 4}).(*Issue)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 4}).(*issues_model.Issue)
 	assert.NotEqualValues(t, issue.Index, issue.ID) // make sure they are different to avoid false positive
 
 	// it is fine for an issue to not have a foreign reference
-	err := issue.LoadAttributes()
+	err := issue.LoadAttributes(db.DefaultContext)
 	assert.NoError(t, err)
 	assert.Nil(t, issue.ForeignReference)
 
 	var foreignIndex int64 = 12345
-	_, err = GetIssueByForeignIndex(context.Background(), issue.RepoID, foreignIndex)
+	_, err = issues_model.GetIssueByForeignIndex(context.Background(), issue.RepoID, foreignIndex)
 	assert.True(t, foreignreference.IsErrLocalIndexNotExist(err))
 
-	_, err = db.GetEngine(db.DefaultContext).Insert(&foreignreference.ForeignReference{
+	err = db.Insert(db.DefaultContext, &foreignreference.ForeignReference{
 		LocalIndex:   issue.Index,
 		ForeignIndex: strconv.FormatInt(foreignIndex, 10),
 		RepoID:       issue.RepoID,
@@ -576,12 +524,12 @@ func TestIssueForeignReference(t *testing.T) {
 	})
 	assert.NoError(t, err)
 
-	err = issue.LoadAttributes()
+	err = issue.LoadAttributes(db.DefaultContext)
 	assert.NoError(t, err)
 
 	assert.EqualValues(t, issue.ForeignReference.ForeignIndex, strconv.FormatInt(foreignIndex, 10))
 
-	found, err := GetIssueByForeignIndex(context.Background(), issue.RepoID, foreignIndex)
+	found, err := issues_model.GetIssueByForeignIndex(context.Background(), issue.RepoID, foreignIndex)
 	assert.NoError(t, err)
 	assert.EqualValues(t, found.Index, issue.Index)
 }
@@ -608,7 +556,7 @@ func TestLoadTotalTrackedTime(t *testing.T) {
 
 func TestCountIssues(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	count, err := CountIssues(&IssuesOptions{})
+	count, err := issues_model.CountIssues(&issues_model.IssuesOptions{})
 	assert.NoError(t, err)
 	assert.EqualValues(t, 17, count)
 }
diff --git a/models/issue_user.go b/models/issues/issue_user.go
similarity index 94%
rename from models/issue_user.go
rename to models/issues/issue_user.go
index 19c64094a1..f5d22589af 100644
--- a/models/issue_user.go
+++ b/models/issues/issue_user.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues
 
 import (
 	"context"
@@ -25,7 +25,8 @@ func init() {
 	db.RegisterModel(new(IssueUser))
 }
 
-func newIssueUsers(ctx context.Context, repo *repo_model.Repository, issue *Issue) error {
+// NewIssueUsers inserts an issue related users
+func NewIssueUsers(ctx context.Context, repo *repo_model.Repository, issue *Issue) error {
 	assignees, err := repo_model.GetRepoAssignees(ctx, repo)
 	if err != nil {
 		return fmt.Errorf("getAssignees: %v", err)
diff --git a/models/issues/issue_user_test.go b/models/issues/issue_user_test.go
new file mode 100644
index 0000000000..33e9f98ecc
--- /dev/null
+++ b/models/issues/issue_user_test.go
@@ -0,0 +1,62 @@
+// Copyright 2017 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package issues_test
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unittest"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_NewIssueUsers(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
+	newIssue := &issues_model.Issue{
+		RepoID:   repo.ID,
+		PosterID: 4,
+		Index:    6,
+		Title:    "newTestIssueTitle",
+		Content:  "newTestIssueContent",
+	}
+
+	// artificially insert new issue
+	unittest.AssertSuccessfulInsert(t, newIssue)
+
+	assert.NoError(t, issues_model.NewIssueUsers(db.DefaultContext, repo, newIssue))
+
+	// issue_user table should now have entries for new issue
+	unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: newIssue.ID, UID: newIssue.PosterID})
+	unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: newIssue.ID, UID: repo.OwnerID})
+}
+
+func TestUpdateIssueUserByRead(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue)
+
+	assert.NoError(t, issues_model.UpdateIssueUserByRead(4, issue.ID))
+	unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: issue.ID, UID: 4}, "is_read=1")
+
+	assert.NoError(t, issues_model.UpdateIssueUserByRead(4, issue.ID))
+	unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: issue.ID, UID: 4}, "is_read=1")
+
+	assert.NoError(t, issues_model.UpdateIssueUserByRead(unittest.NonexistentID, unittest.NonexistentID))
+}
+
+func TestUpdateIssueUsersByMentions(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue)
+
+	uids := []int64{2, 5}
+	assert.NoError(t, issues_model.UpdateIssueUsersByMentions(db.DefaultContext, issue.ID, uids))
+	for _, uid := range uids {
+		unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: issue.ID, UID: uid}, "is_mentioned=1")
+	}
+}
diff --git a/models/issue_watch.go b/models/issues/issue_watch.go
similarity index 96%
rename from models/issue_watch.go
rename to models/issues/issue_watch.go
index 9f41d36e19..bf907aa8fd 100644
--- a/models/issue_watch.go
+++ b/models/issues/issue_watch.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues
 
 import (
 	"context"
@@ -125,7 +125,8 @@ func CountIssueWatchers(ctx context.Context, issueID int64) (int64, error) {
 		Join("INNER", "`user`", "`user`.id = `issue_watch`.user_id").Count(new(IssueWatch))
 }
 
-func removeIssueWatchersByRepoID(ctx context.Context, userID, repoID int64) error {
+// RemoveIssueWatchersByRepoID remove issue watchers by repoID
+func RemoveIssueWatchersByRepoID(ctx context.Context, userID, repoID int64) error {
 	_, err := db.GetEngine(ctx).
 		Join("INNER", "issue", "`issue`.id = `issue_watch`.issue_id AND `issue`.repo_id = ?", repoID).
 		Where("`issue_watch`.user_id = ?", userID).
diff --git a/models/issue_watch_test.go b/models/issues/issue_watch_test.go
similarity index 53%
rename from models/issue_watch_test.go
rename to models/issues/issue_watch_test.go
index b686196ae1..c6b6416d9b 100644
--- a/models/issue_watch_test.go
+++ b/models/issues/issue_watch_test.go
@@ -2,12 +2,13 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues_test
 
 import (
 	"testing"
 
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
 
 	"github.com/stretchr/testify/assert"
@@ -16,28 +17,28 @@ import (
 func TestCreateOrUpdateIssueWatch(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	assert.NoError(t, CreateOrUpdateIssueWatch(3, 1, true))
-	iw := unittest.AssertExistsAndLoadBean(t, &IssueWatch{UserID: 3, IssueID: 1}).(*IssueWatch)
+	assert.NoError(t, issues_model.CreateOrUpdateIssueWatch(3, 1, true))
+	iw := unittest.AssertExistsAndLoadBean(t, &issues_model.IssueWatch{UserID: 3, IssueID: 1}).(*issues_model.IssueWatch)
 	assert.True(t, iw.IsWatching)
 
-	assert.NoError(t, CreateOrUpdateIssueWatch(1, 1, false))
-	iw = unittest.AssertExistsAndLoadBean(t, &IssueWatch{UserID: 1, IssueID: 1}).(*IssueWatch)
+	assert.NoError(t, issues_model.CreateOrUpdateIssueWatch(1, 1, false))
+	iw = unittest.AssertExistsAndLoadBean(t, &issues_model.IssueWatch{UserID: 1, IssueID: 1}).(*issues_model.IssueWatch)
 	assert.False(t, iw.IsWatching)
 }
 
 func TestGetIssueWatch(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	_, exists, err := GetIssueWatch(db.DefaultContext, 9, 1)
+	_, exists, err := issues_model.GetIssueWatch(db.DefaultContext, 9, 1)
 	assert.True(t, exists)
 	assert.NoError(t, err)
 
-	iw, exists, err := GetIssueWatch(db.DefaultContext, 2, 2)
+	iw, exists, err := issues_model.GetIssueWatch(db.DefaultContext, 2, 2)
 	assert.True(t, exists)
 	assert.NoError(t, err)
 	assert.False(t, iw.IsWatching)
 
-	_, exists, err = GetIssueWatch(db.DefaultContext, 3, 1)
+	_, exists, err = issues_model.GetIssueWatch(db.DefaultContext, 3, 1)
 	assert.False(t, exists)
 	assert.NoError(t, err)
 }
@@ -45,22 +46,22 @@ func TestGetIssueWatch(t *testing.T) {
 func TestGetIssueWatchers(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	iws, err := GetIssueWatchers(db.DefaultContext, 1, db.ListOptions{})
+	iws, err := issues_model.GetIssueWatchers(db.DefaultContext, 1, db.ListOptions{})
 	assert.NoError(t, err)
 	// Watcher is inactive, thus 0
 	assert.Len(t, iws, 0)
 
-	iws, err = GetIssueWatchers(db.DefaultContext, 2, db.ListOptions{})
+	iws, err = issues_model.GetIssueWatchers(db.DefaultContext, 2, db.ListOptions{})
 	assert.NoError(t, err)
 	// Watcher is explicit not watching
 	assert.Len(t, iws, 0)
 
-	iws, err = GetIssueWatchers(db.DefaultContext, 5, db.ListOptions{})
+	iws, err = issues_model.GetIssueWatchers(db.DefaultContext, 5, db.ListOptions{})
 	assert.NoError(t, err)
 	// Issue has no Watchers
 	assert.Len(t, iws, 0)
 
-	iws, err = GetIssueWatchers(db.DefaultContext, 7, db.ListOptions{})
+	iws, err = issues_model.GetIssueWatchers(db.DefaultContext, 7, db.ListOptions{})
 	assert.NoError(t, err)
 	// Issue has one watcher
 	assert.Len(t, iws, 1)
diff --git a/models/issue_xref.go b/models/issues/issue_xref.go
similarity index 93%
rename from models/issue_xref.go
rename to models/issues/issue_xref.go
index 0c1623b5a4..f4380a02ec 100644
--- a/models/issue_xref.go
+++ b/models/issues/issue_xref.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues
 
 import (
 	"context"
@@ -55,15 +55,8 @@ func neuterCrossReferencesIds(ctx context.Context, ids []int64) error {
 	return err
 }
 
-// .___
-// |   | ______ ________ __   ____
-// |   |/  ___//  ___/  |  \_/ __ \
-// |   |\___ \ \___ \|  |  /\  ___/
-// |___/____  >____  >____/  \___  >
-//          \/     \/            \/
-//
-
-func (issue *Issue) addCrossReferences(stdCtx context.Context, doer *user_model.User, removeOld bool) error {
+// AddCrossReferences add cross repositories references.
+func (issue *Issue) AddCrossReferences(stdCtx context.Context, doer *user_model.User, removeOld bool) error {
 	var commentType CommentType
 	if issue.IsPull {
 		commentType = CommentTypePullRef
@@ -237,15 +230,8 @@ func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossRefe
 	return refIssue, refAction, nil
 }
 
-// _________                                       __
-// \_   ___ \  ____   _____   _____   ____   _____/  |_
-// /    \  \/ /  _ \ /     \ /     \_/ __ \ /    \   __\
-// \     \___(  <_> )  Y Y  \  Y Y  \  ___/|   |  \  |
-//  \______  /\____/|__|_|  /__|_|  /\___  >___|  /__|
-//         \/             \/      \/     \/     \/
-//
-
-func (comment *Comment) addCrossReferences(stdCtx context.Context, doer *user_model.User, removeOld bool) error {
+// AddCrossReferences add cross references
+func (comment *Comment) AddCrossReferences(stdCtx context.Context, doer *user_model.User, removeOld bool) error {
 	if comment.Type != CommentTypeCode && comment.Type != CommentTypeComment {
 		return nil
 	}
@@ -280,7 +266,7 @@ func (comment *Comment) LoadRefIssue() (err error) {
 	if comment.RefIssue != nil {
 		return nil
 	}
-	comment.RefIssue, err = GetIssueByID(comment.RefIssueID)
+	comment.RefIssue, err = GetIssueByID(db.DefaultContext, comment.RefIssueID)
 	if err == nil {
 		err = comment.RefIssue.LoadRepo(db.DefaultContext)
 	}
diff --git a/models/issue_xref_test.go b/models/issues/issue_xref_test.go
similarity index 61%
rename from models/issue_xref_test.go
rename to models/issues/issue_xref_test.go
index b4ad5b2708..6bb19d5328 100644
--- a/models/issue_xref_test.go
+++ b/models/issues/issue_xref_test.go
@@ -2,13 +2,14 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues_test
 
 import (
 	"fmt"
 	"testing"
 
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
@@ -26,8 +27,8 @@ func TestXRef_AddCrossReferences(t *testing.T) {
 	// PR to close issue #1
 	content := fmt.Sprintf("content2, closes #%d", itarget.Index)
 	pr := testCreateIssue(t, 1, 2, "title2", content, true)
-	ref := unittest.AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: pr.ID, RefCommentID: 0}).(*Comment)
-	assert.Equal(t, CommentTypePullRef, ref.Type)
+	ref := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: pr.ID, RefCommentID: 0}).(*issues_model.Comment)
+	assert.Equal(t, issues_model.CommentTypePullRef, ref.Type)
 	assert.Equal(t, pr.RepoID, ref.RefRepoID)
 	assert.True(t, ref.RefIsPull)
 	assert.Equal(t, references.XRefActionCloses, ref.RefAction)
@@ -35,8 +36,8 @@ func TestXRef_AddCrossReferences(t *testing.T) {
 	// Comment on PR to reopen issue #1
 	content = fmt.Sprintf("content2, reopens #%d", itarget.Index)
 	c := testCreateComment(t, 1, 2, pr.ID, content)
-	ref = unittest.AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: pr.ID, RefCommentID: c.ID}).(*Comment)
-	assert.Equal(t, CommentTypeCommentRef, ref.Type)
+	ref = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: pr.ID, RefCommentID: c.ID}).(*issues_model.Comment)
+	assert.Equal(t, issues_model.CommentTypeCommentRef, ref.Type)
 	assert.Equal(t, pr.RepoID, ref.RefRepoID)
 	assert.True(t, ref.RefIsPull)
 	assert.Equal(t, references.XRefActionReopens, ref.RefAction)
@@ -44,8 +45,8 @@ func TestXRef_AddCrossReferences(t *testing.T) {
 	// Issue mentioning issue #1
 	content = fmt.Sprintf("content3, mentions #%d", itarget.Index)
 	i := testCreateIssue(t, 1, 2, "title3", content, false)
-	ref = unittest.AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*Comment)
-	assert.Equal(t, CommentTypeIssueRef, ref.Type)
+	ref = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*issues_model.Comment)
+	assert.Equal(t, issues_model.CommentTypeIssueRef, ref.Type)
 	assert.Equal(t, pr.RepoID, ref.RefRepoID)
 	assert.False(t, ref.RefIsPull)
 	assert.Equal(t, references.XRefActionNone, ref.RefAction)
@@ -56,8 +57,8 @@ func TestXRef_AddCrossReferences(t *testing.T) {
 	// Cross-reference to issue #4 by admin
 	content = fmt.Sprintf("content5, mentions user3/repo3#%d", itarget.Index)
 	i = testCreateIssue(t, 2, 1, "title5", content, false)
-	ref = unittest.AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*Comment)
-	assert.Equal(t, CommentTypeIssueRef, ref.Type)
+	ref = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*issues_model.Comment)
+	assert.Equal(t, issues_model.CommentTypeIssueRef, ref.Type)
 	assert.Equal(t, i.RepoID, ref.RefRepoID)
 	assert.False(t, ref.RefIsPull)
 	assert.Equal(t, references.XRefActionNone, ref.RefAction)
@@ -65,7 +66,7 @@ func TestXRef_AddCrossReferences(t *testing.T) {
 	// Cross-reference to issue #4 with no permission
 	content = fmt.Sprintf("content6, mentions user3/repo3#%d", itarget.Index)
 	i = testCreateIssue(t, 4, 5, "title6", content, false)
-	unittest.AssertNotExistsBean(t, &Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0})
+	unittest.AssertNotExistsBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0})
 }
 
 func TestXRef_NeuterCrossReferences(t *testing.T) {
@@ -77,16 +78,16 @@ func TestXRef_NeuterCrossReferences(t *testing.T) {
 	// Issue mentioning issue #1
 	title := fmt.Sprintf("title2, mentions #%d", itarget.Index)
 	i := testCreateIssue(t, 1, 2, title, "content2", false)
-	ref := unittest.AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*Comment)
-	assert.Equal(t, CommentTypeIssueRef, ref.Type)
+	ref := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*issues_model.Comment)
+	assert.Equal(t, issues_model.CommentTypeIssueRef, ref.Type)
 	assert.Equal(t, references.XRefActionNone, ref.RefAction)
 
 	d := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
 	i.Title = "title2, no mentions"
-	assert.NoError(t, ChangeIssueTitle(i, d, title))
+	assert.NoError(t, issues_model.ChangeIssueTitle(i, d, title))
 
-	ref = unittest.AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*Comment)
-	assert.Equal(t, CommentTypeIssueRef, ref.Type)
+	ref = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*issues_model.Comment)
+	assert.Equal(t, issues_model.CommentTypeIssueRef, ref.Type)
 	assert.Equal(t, references.XRefActionNeutered, ref.RefAction)
 }
 
@@ -98,25 +99,25 @@ func TestXRef_ResolveCrossReferences(t *testing.T) {
 	i1 := testCreateIssue(t, 1, 2, "title1", "content1", false)
 	i2 := testCreateIssue(t, 1, 2, "title2", "content2", false)
 	i3 := testCreateIssue(t, 1, 2, "title3", "content3", false)
-	_, err := ChangeIssueStatus(db.DefaultContext, i3, d, true)
+	_, err := issues_model.ChangeIssueStatus(db.DefaultContext, i3, d, true)
 	assert.NoError(t, err)
 
 	pr := testCreatePR(t, 1, 2, "titlepr", fmt.Sprintf("closes #%d", i1.Index))
-	rp := unittest.AssertExistsAndLoadBean(t, &Comment{IssueID: i1.ID, RefIssueID: pr.Issue.ID, RefCommentID: 0}).(*Comment)
+	rp := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i1.ID, RefIssueID: pr.Issue.ID, RefCommentID: 0}).(*issues_model.Comment)
 
 	c1 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("closes #%d", i2.Index))
-	r1 := unittest.AssertExistsAndLoadBean(t, &Comment{IssueID: i2.ID, RefIssueID: pr.Issue.ID, RefCommentID: c1.ID}).(*Comment)
+	r1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i2.ID, RefIssueID: pr.Issue.ID, RefCommentID: c1.ID}).(*issues_model.Comment)
 
 	// Must be ignored
 	c2 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("mentions #%d", i2.Index))
-	unittest.AssertExistsAndLoadBean(t, &Comment{IssueID: i2.ID, RefIssueID: pr.Issue.ID, RefCommentID: c2.ID})
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i2.ID, RefIssueID: pr.Issue.ID, RefCommentID: c2.ID})
 
 	// Must be superseded by c4/r4
 	c3 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("reopens #%d", i3.Index))
-	unittest.AssertExistsAndLoadBean(t, &Comment{IssueID: i3.ID, RefIssueID: pr.Issue.ID, RefCommentID: c3.ID})
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i3.ID, RefIssueID: pr.Issue.ID, RefCommentID: c3.ID})
 
 	c4 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("closes #%d", i3.Index))
-	r4 := unittest.AssertExistsAndLoadBean(t, &Comment{IssueID: i3.ID, RefIssueID: pr.Issue.ID, RefCommentID: c4.ID}).(*Comment)
+	r4 := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i3.ID, RefIssueID: pr.Issue.ID, RefCommentID: c4.ID}).(*issues_model.Comment)
 
 	refs, err := pr.ResolveCrossReferences(db.DefaultContext)
 	assert.NoError(t, err)
@@ -126,13 +127,13 @@ func TestXRef_ResolveCrossReferences(t *testing.T) {
 	assert.Equal(t, r4.ID, refs[2].ID, "bad ref r4: %+v", refs[2])
 }
 
-func testCreateIssue(t *testing.T, repo, doer int64, title, content string, ispull bool) *Issue {
+func testCreateIssue(t *testing.T, repo, doer int64, title, content string, ispull bool) *issues_model.Issue {
 	r := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo}).(*repo_model.Repository)
 	d := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: doer}).(*user_model.User)
 
 	idx, err := db.GetNextResourceIndex("issue_index", r.ID)
 	assert.NoError(t, err)
-	i := &Issue{
+	i := &issues_model.Issue{
 		RepoID:   r.ID,
 		PosterID: d.ID,
 		Poster:   d,
@@ -145,39 +146,39 @@ func testCreateIssue(t *testing.T, repo, doer int64, title, content string, ispu
 	ctx, committer, err := db.TxContext()
 	assert.NoError(t, err)
 	defer committer.Close()
-	err = newIssue(ctx, d, NewIssueOptions{
+	err = issues_model.NewIssueWithIndex(ctx, d, issues_model.NewIssueOptions{
 		Repo:  r,
 		Issue: i,
 	})
 	assert.NoError(t, err)
-	i, err = getIssueByID(ctx, i.ID)
+	i, err = issues_model.GetIssueByID(ctx, i.ID)
 	assert.NoError(t, err)
-	assert.NoError(t, i.addCrossReferences(ctx, d, false))
+	assert.NoError(t, i.AddCrossReferences(ctx, d, false))
 	assert.NoError(t, committer.Commit())
 	return i
 }
 
-func testCreatePR(t *testing.T, repo, doer int64, title, content string) *PullRequest {
+func testCreatePR(t *testing.T, repo, doer int64, title, content string) *issues_model.PullRequest {
 	r := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo}).(*repo_model.Repository)
 	d := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: doer}).(*user_model.User)
-	i := &Issue{RepoID: r.ID, PosterID: d.ID, Poster: d, Title: title, Content: content, IsPull: true}
-	pr := &PullRequest{HeadRepoID: repo, BaseRepoID: repo, HeadBranch: "head", BaseBranch: "base", Status: PullRequestStatusMergeable}
-	assert.NoError(t, NewPullRequest(db.DefaultContext, r, i, nil, nil, pr))
+	i := &issues_model.Issue{RepoID: r.ID, PosterID: d.ID, Poster: d, Title: title, Content: content, IsPull: true}
+	pr := &issues_model.PullRequest{HeadRepoID: repo, BaseRepoID: repo, HeadBranch: "head", BaseBranch: "base", Status: issues_model.PullRequestStatusMergeable}
+	assert.NoError(t, issues_model.NewPullRequest(db.DefaultContext, r, i, nil, nil, pr))
 	pr.Issue = i
 	return pr
 }
 
-func testCreateComment(t *testing.T, repo, doer, issue int64, content string) *Comment {
+func testCreateComment(t *testing.T, repo, doer, issue int64, content string) *issues_model.Comment {
 	d := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: doer}).(*user_model.User)
-	i := unittest.AssertExistsAndLoadBean(t, &Issue{ID: issue}).(*Issue)
-	c := &Comment{Type: CommentTypeComment, PosterID: doer, Poster: d, IssueID: issue, Issue: i, Content: content}
+	i := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue}).(*issues_model.Issue)
+	c := &issues_model.Comment{Type: issues_model.CommentTypeComment, PosterID: doer, Poster: d, IssueID: issue, Issue: i, Content: content}
 
 	ctx, committer, err := db.TxContext()
 	assert.NoError(t, err)
 	defer committer.Close()
 	err = db.Insert(ctx, c)
 	assert.NoError(t, err)
-	assert.NoError(t, c.addCrossReferences(ctx, d, false))
+	assert.NoError(t, c.AddCrossReferences(ctx, d, false))
 	assert.NoError(t, committer.Commit())
 	return c
 }
diff --git a/models/issue_label.go b/models/issues/label.go
similarity index 79%
rename from models/issue_label.go
rename to models/issues/label.go
index 48a48dbb7c..98e2e43961 100644
--- a/models/issue_label.go
+++ b/models/issues/label.go
@@ -3,7 +3,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues
 
 import (
 	"context"
@@ -21,6 +21,53 @@ import (
 	"xorm.io/builder"
 )
 
+// ErrRepoLabelNotExist represents a "RepoLabelNotExist" kind of error.
+type ErrRepoLabelNotExist struct {
+	LabelID int64
+	RepoID  int64
+}
+
+// IsErrRepoLabelNotExist checks if an error is a RepoErrLabelNotExist.
+func IsErrRepoLabelNotExist(err error) bool {
+	_, ok := err.(ErrRepoLabelNotExist)
+	return ok
+}
+
+func (err ErrRepoLabelNotExist) Error() string {
+	return fmt.Sprintf("label does not exist [label_id: %d, repo_id: %d]", err.LabelID, err.RepoID)
+}
+
+// ErrOrgLabelNotExist represents a "OrgLabelNotExist" kind of error.
+type ErrOrgLabelNotExist struct {
+	LabelID int64
+	OrgID   int64
+}
+
+// IsErrOrgLabelNotExist checks if an error is a OrgErrLabelNotExist.
+func IsErrOrgLabelNotExist(err error) bool {
+	_, ok := err.(ErrOrgLabelNotExist)
+	return ok
+}
+
+func (err ErrOrgLabelNotExist) Error() string {
+	return fmt.Sprintf("label does not exist [label_id: %d, org_id: %d]", err.LabelID, err.OrgID)
+}
+
+// ErrLabelNotExist represents a "LabelNotExist" kind of error.
+type ErrLabelNotExist struct {
+	LabelID int64
+}
+
+// IsErrLabelNotExist checks if an error is a ErrLabelNotExist.
+func IsErrLabelNotExist(err error) bool {
+	_, ok := err.(ErrLabelNotExist)
+	return ok
+}
+
+func (err ErrLabelNotExist) Error() string {
+	return fmt.Sprintf("label does not exist [label_id: %d]", err.LabelID)
+}
+
 // LabelColorPattern is a regexp witch can validate LabelColor
 var LabelColorPattern = regexp.MustCompile("^#?(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$")
 
@@ -671,7 +718,8 @@ func DeleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *use
 	return issue.LoadLabels(ctx)
 }
 
-func deleteLabelsByRepoID(ctx context.Context, repoID int64) error {
+// DeleteLabelsByRepoID  deletes labels of some repository
+func DeleteLabelsByRepoID(ctx context.Context, repoID int64) error {
 	deleteCond := builder.Select("id").From("label").Where(builder.Eq{"label.repo_id": repoID})
 
 	if _, err := db.GetEngine(ctx).In("label_id", deleteCond).
@@ -682,3 +730,107 @@ func deleteLabelsByRepoID(ctx context.Context, repoID int64) error {
 	_, err := db.DeleteByBean(ctx, &Label{RepoID: repoID})
 	return err
 }
+
+// CountOrphanedLabels return count of labels witch are broken and not accessible via ui anymore
+func CountOrphanedLabels() (int64, error) {
+	noref, err := db.GetEngine(db.DefaultContext).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Count("label.id")
+	if err != nil {
+		return 0, err
+	}
+
+	norepo, err := db.GetEngine(db.DefaultContext).Table("label").
+		Where(builder.And(
+			builder.Gt{"repo_id": 0},
+			builder.NotIn("repo_id", builder.Select("id").From("repository")),
+		)).
+		Count()
+	if err != nil {
+		return 0, err
+	}
+
+	noorg, err := db.GetEngine(db.DefaultContext).Table("label").
+		Where(builder.And(
+			builder.Gt{"org_id": 0},
+			builder.NotIn("org_id", builder.Select("id").From("user")),
+		)).
+		Count()
+	if err != nil {
+		return 0, err
+	}
+
+	return noref + norepo + noorg, nil
+}
+
+// DeleteOrphanedLabels delete labels witch are broken and not accessible via ui anymore
+func DeleteOrphanedLabels() error {
+	// delete labels with no reference
+	if _, err := db.GetEngine(db.DefaultContext).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Delete(new(Label)); err != nil {
+		return err
+	}
+
+	// delete labels with none existing repos
+	if _, err := db.GetEngine(db.DefaultContext).
+		Where(builder.And(
+			builder.Gt{"repo_id": 0},
+			builder.NotIn("repo_id", builder.Select("id").From("repository")),
+		)).
+		Delete(Label{}); err != nil {
+		return err
+	}
+
+	// delete labels with none existing orgs
+	if _, err := db.GetEngine(db.DefaultContext).
+		Where(builder.And(
+			builder.Gt{"org_id": 0},
+			builder.NotIn("org_id", builder.Select("id").From("user")),
+		)).
+		Delete(Label{}); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// CountOrphanedIssueLabels return count of IssueLabels witch have no label behind anymore
+func CountOrphanedIssueLabels() (int64, error) {
+	return db.GetEngine(db.DefaultContext).Table("issue_label").
+		NotIn("label_id", builder.Select("id").From("label")).
+		Count()
+}
+
+// DeleteOrphanedIssueLabels delete IssueLabels witch have no label behind anymore
+func DeleteOrphanedIssueLabels() error {
+	_, err := db.GetEngine(db.DefaultContext).
+		NotIn("label_id", builder.Select("id").From("label")).
+		Delete(IssueLabel{})
+	return err
+}
+
+// CountIssueLabelWithOutsideLabels count label comments with outside label
+func CountIssueLabelWithOutsideLabels() (int64, error) {
+	return db.GetEngine(db.DefaultContext).Where(builder.Expr("(label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id)")).
+		Table("issue_label").
+		Join("inner", "label", "issue_label.label_id = label.id ").
+		Join("inner", "issue", "issue.id = issue_label.issue_id ").
+		Join("inner", "repository", "issue.repo_id = repository.id").
+		Count(new(IssueLabel))
+}
+
+// FixIssueLabelWithOutsideLabels fix label comments with outside label
+func FixIssueLabelWithOutsideLabels() (int64, error) {
+	res, err := db.GetEngine(db.DefaultContext).Exec(`DELETE FROM issue_label WHERE issue_label.id IN (
+		SELECT il_too.id FROM (
+			SELECT il_too_too.id
+				FROM issue_label AS il_too_too
+					INNER JOIN label ON il_too_too.label_id = label.id
+					INNER JOIN issue on issue.id = il_too_too.issue_id
+					INNER JOIN repository on repository.id = issue.repo_id
+				WHERE
+					(label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id)
+	) AS il_too )`)
+	if err != nil {
+		return 0, err
+	}
+
+	return res.RowsAffected()
+}
diff --git a/models/issues/label_test.go b/models/issues/label_test.go
new file mode 100644
index 0000000000..33f114b5fe
--- /dev/null
+++ b/models/issues/label_test.go
@@ -0,0 +1,395 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package issues_test
+
+import (
+	"html/template"
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+
+	"github.com/stretchr/testify/assert"
+)
+
+// TODO TestGetLabelTemplateFile
+
+func TestLabel_CalOpenIssues(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label)
+	label.CalOpenIssues()
+	assert.EqualValues(t, 2, label.NumOpenIssues)
+}
+
+func TestLabel_ForegroundColor(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label)
+	assert.Equal(t, template.CSS("#000"), label.ForegroundColor())
+
+	label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2}).(*issues_model.Label)
+	assert.Equal(t, template.CSS("#fff"), label.ForegroundColor())
+}
+
+func TestNewLabels(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	labels := []*issues_model.Label{
+		{RepoID: 2, Name: "labelName2", Color: "#123456"},
+		{RepoID: 3, Name: "labelName3", Color: "#123"},
+		{RepoID: 4, Name: "labelName4", Color: "ABCDEF"},
+		{RepoID: 5, Name: "labelName5", Color: "DEF"},
+	}
+	assert.Error(t, issues_model.NewLabel(db.DefaultContext, &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: ""}))
+	assert.Error(t, issues_model.NewLabel(db.DefaultContext, &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: "#45G"}))
+	assert.Error(t, issues_model.NewLabel(db.DefaultContext, &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: "#12345G"}))
+	assert.Error(t, issues_model.NewLabel(db.DefaultContext, &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: "45G"}))
+	assert.Error(t, issues_model.NewLabel(db.DefaultContext, &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: "12345G"}))
+	for _, label := range labels {
+		unittest.AssertNotExistsBean(t, label)
+	}
+	assert.NoError(t, issues_model.NewLabels(labels...))
+	for _, label := range labels {
+		unittest.AssertExistsAndLoadBean(t, label, unittest.Cond("id = ?", label.ID))
+	}
+	unittest.CheckConsistencyFor(t, &issues_model.Label{}, &repo_model.Repository{})
+}
+
+func TestGetLabelByID(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	label, err := issues_model.GetLabelByID(db.DefaultContext, 1)
+	assert.NoError(t, err)
+	assert.EqualValues(t, 1, label.ID)
+
+	_, err = issues_model.GetLabelByID(db.DefaultContext, unittest.NonexistentID)
+	assert.True(t, issues_model.IsErrLabelNotExist(err))
+}
+
+func TestGetLabelInRepoByName(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	label, err := issues_model.GetLabelInRepoByName(db.DefaultContext, 1, "label1")
+	assert.NoError(t, err)
+	assert.EqualValues(t, 1, label.ID)
+	assert.Equal(t, "label1", label.Name)
+
+	_, err = issues_model.GetLabelInRepoByName(db.DefaultContext, 1, "")
+	assert.True(t, issues_model.IsErrRepoLabelNotExist(err))
+
+	_, err = issues_model.GetLabelInRepoByName(db.DefaultContext, unittest.NonexistentID, "nonexistent")
+	assert.True(t, issues_model.IsErrRepoLabelNotExist(err))
+}
+
+func TestGetLabelInRepoByNames(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	labelIDs, err := issues_model.GetLabelIDsInRepoByNames(1, []string{"label1", "label2"})
+	assert.NoError(t, err)
+
+	assert.Len(t, labelIDs, 2)
+
+	assert.Equal(t, int64(1), labelIDs[0])
+	assert.Equal(t, int64(2), labelIDs[1])
+}
+
+func TestGetLabelInRepoByNamesDiscardsNonExistentLabels(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	// label3 doesn't exists.. See labels.yml
+	labelIDs, err := issues_model.GetLabelIDsInRepoByNames(1, []string{"label1", "label2", "label3"})
+	assert.NoError(t, err)
+
+	assert.Len(t, labelIDs, 2)
+
+	assert.Equal(t, int64(1), labelIDs[0])
+	assert.Equal(t, int64(2), labelIDs[1])
+	assert.NoError(t, err)
+}
+
+func TestGetLabelInRepoByID(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	label, err := issues_model.GetLabelInRepoByID(db.DefaultContext, 1, 1)
+	assert.NoError(t, err)
+	assert.EqualValues(t, 1, label.ID)
+
+	_, err = issues_model.GetLabelInRepoByID(db.DefaultContext, 1, -1)
+	assert.True(t, issues_model.IsErrRepoLabelNotExist(err))
+
+	_, err = issues_model.GetLabelInRepoByID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID)
+	assert.True(t, issues_model.IsErrRepoLabelNotExist(err))
+}
+
+func TestGetLabelsInRepoByIDs(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	labels, err := issues_model.GetLabelsInRepoByIDs(1, []int64{1, 2, unittest.NonexistentID})
+	assert.NoError(t, err)
+	if assert.Len(t, labels, 2) {
+		assert.EqualValues(t, 1, labels[0].ID)
+		assert.EqualValues(t, 2, labels[1].ID)
+	}
+}
+
+func TestGetLabelsByRepoID(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	testSuccess := func(repoID int64, sortType string, expectedIssueIDs []int64) {
+		labels, err := issues_model.GetLabelsByRepoID(db.DefaultContext, repoID, sortType, db.ListOptions{})
+		assert.NoError(t, err)
+		assert.Len(t, labels, len(expectedIssueIDs))
+		for i, label := range labels {
+			assert.EqualValues(t, expectedIssueIDs[i], label.ID)
+		}
+	}
+	testSuccess(1, "leastissues", []int64{2, 1})
+	testSuccess(1, "mostissues", []int64{1, 2})
+	testSuccess(1, "reversealphabetically", []int64{2, 1})
+	testSuccess(1, "default", []int64{1, 2})
+}
+
+// Org versions
+
+func TestGetLabelInOrgByName(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	label, err := issues_model.GetLabelInOrgByName(db.DefaultContext, 3, "orglabel3")
+	assert.NoError(t, err)
+	assert.EqualValues(t, 3, label.ID)
+	assert.Equal(t, "orglabel3", label.Name)
+
+	_, err = issues_model.GetLabelInOrgByName(db.DefaultContext, 3, "")
+	assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
+
+	_, err = issues_model.GetLabelInOrgByName(db.DefaultContext, 0, "orglabel3")
+	assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
+
+	_, err = issues_model.GetLabelInOrgByName(db.DefaultContext, -1, "orglabel3")
+	assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
+
+	_, err = issues_model.GetLabelInOrgByName(db.DefaultContext, unittest.NonexistentID, "nonexistent")
+	assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
+}
+
+func TestGetLabelInOrgByNames(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	labelIDs, err := issues_model.GetLabelIDsInOrgByNames(3, []string{"orglabel3", "orglabel4"})
+	assert.NoError(t, err)
+
+	assert.Len(t, labelIDs, 2)
+
+	assert.Equal(t, int64(3), labelIDs[0])
+	assert.Equal(t, int64(4), labelIDs[1])
+}
+
+func TestGetLabelInOrgByNamesDiscardsNonExistentLabels(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	// orglabel99 doesn't exists.. See labels.yml
+	labelIDs, err := issues_model.GetLabelIDsInOrgByNames(3, []string{"orglabel3", "orglabel4", "orglabel99"})
+	assert.NoError(t, err)
+
+	assert.Len(t, labelIDs, 2)
+
+	assert.Equal(t, int64(3), labelIDs[0])
+	assert.Equal(t, int64(4), labelIDs[1])
+	assert.NoError(t, err)
+}
+
+func TestGetLabelInOrgByID(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	label, err := issues_model.GetLabelInOrgByID(db.DefaultContext, 3, 3)
+	assert.NoError(t, err)
+	assert.EqualValues(t, 3, label.ID)
+
+	_, err = issues_model.GetLabelInOrgByID(db.DefaultContext, 3, -1)
+	assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
+
+	_, err = issues_model.GetLabelInOrgByID(db.DefaultContext, 0, 3)
+	assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
+
+	_, err = issues_model.GetLabelInOrgByID(db.DefaultContext, -1, 3)
+	assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
+
+	_, err = issues_model.GetLabelInOrgByID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID)
+	assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
+}
+
+func TestGetLabelsInOrgByIDs(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	labels, err := issues_model.GetLabelsInOrgByIDs(3, []int64{3, 4, unittest.NonexistentID})
+	assert.NoError(t, err)
+	if assert.Len(t, labels, 2) {
+		assert.EqualValues(t, 3, labels[0].ID)
+		assert.EqualValues(t, 4, labels[1].ID)
+	}
+}
+
+func TestGetLabelsByOrgID(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	testSuccess := func(orgID int64, sortType string, expectedIssueIDs []int64) {
+		labels, err := issues_model.GetLabelsByOrgID(db.DefaultContext, orgID, sortType, db.ListOptions{})
+		assert.NoError(t, err)
+		assert.Len(t, labels, len(expectedIssueIDs))
+		for i, label := range labels {
+			assert.EqualValues(t, expectedIssueIDs[i], label.ID)
+		}
+	}
+	testSuccess(3, "leastissues", []int64{3, 4})
+	testSuccess(3, "mostissues", []int64{4, 3})
+	testSuccess(3, "reversealphabetically", []int64{4, 3})
+	testSuccess(3, "default", []int64{3, 4})
+
+	var err error
+	_, err = issues_model.GetLabelsByOrgID(db.DefaultContext, 0, "leastissues", db.ListOptions{})
+	assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
+
+	_, err = issues_model.GetLabelsByOrgID(db.DefaultContext, -1, "leastissues", db.ListOptions{})
+	assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
+}
+
+//
+
+func TestGetLabelsByIssueID(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	labels, err := issues_model.GetLabelsByIssueID(db.DefaultContext, 1)
+	assert.NoError(t, err)
+	if assert.Len(t, labels, 1) {
+		assert.EqualValues(t, 1, labels[0].ID)
+	}
+
+	labels, err = issues_model.GetLabelsByIssueID(db.DefaultContext, unittest.NonexistentID)
+	assert.NoError(t, err)
+	assert.Len(t, labels, 0)
+}
+
+func TestUpdateLabel(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label)
+	// make sure update wont overwrite it
+	update := &issues_model.Label{
+		ID:          label.ID,
+		Color:       "#ffff00",
+		Name:        "newLabelName",
+		Description: label.Description,
+	}
+	label.Color = update.Color
+	label.Name = update.Name
+	assert.NoError(t, issues_model.UpdateLabel(update))
+	newLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label)
+	assert.EqualValues(t, label.ID, newLabel.ID)
+	assert.EqualValues(t, label.Color, newLabel.Color)
+	assert.EqualValues(t, label.Name, newLabel.Name)
+	assert.EqualValues(t, label.Description, newLabel.Description)
+	unittest.CheckConsistencyFor(t, &issues_model.Label{}, &repo_model.Repository{})
+}
+
+func TestDeleteLabel(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label)
+	assert.NoError(t, issues_model.DeleteLabel(label.RepoID, label.ID))
+	unittest.AssertNotExistsBean(t, &issues_model.Label{ID: label.ID, RepoID: label.RepoID})
+
+	assert.NoError(t, issues_model.DeleteLabel(label.RepoID, label.ID))
+	unittest.AssertNotExistsBean(t, &issues_model.Label{ID: label.ID})
+
+	assert.NoError(t, issues_model.DeleteLabel(unittest.NonexistentID, unittest.NonexistentID))
+	unittest.CheckConsistencyFor(t, &issues_model.Label{}, &repo_model.Repository{})
+}
+
+func TestHasIssueLabel(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	assert.True(t, issues_model.HasIssueLabel(db.DefaultContext, 1, 1))
+	assert.False(t, issues_model.HasIssueLabel(db.DefaultContext, 1, 2))
+	assert.False(t, issues_model.HasIssueLabel(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID))
+}
+
+func TestNewIssueLabel(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2}).(*issues_model.Label)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue)
+	doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+
+	// add new IssueLabel
+	prevNumIssues := label.NumIssues
+	assert.NoError(t, issues_model.NewIssueLabel(issue, label, doer))
+	unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label.ID})
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
+		Type:     issues_model.CommentTypeLabel,
+		PosterID: doer.ID,
+		IssueID:  issue.ID,
+		LabelID:  label.ID,
+		Content:  "1",
+	})
+	label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2}).(*issues_model.Label)
+	assert.EqualValues(t, prevNumIssues+1, label.NumIssues)
+
+	// re-add existing IssueLabel
+	assert.NoError(t, issues_model.NewIssueLabel(issue, label, doer))
+	unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.Label{})
+}
+
+func TestNewIssueLabels(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	label1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label)
+	label2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2}).(*issues_model.Label)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 5}).(*issues_model.Issue)
+	doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+
+	assert.NoError(t, issues_model.NewIssueLabels(issue, []*issues_model.Label{label1, label2}, doer))
+	unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label1.ID})
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
+		Type:     issues_model.CommentTypeLabel,
+		PosterID: doer.ID,
+		IssueID:  issue.ID,
+		LabelID:  label1.ID,
+		Content:  "1",
+	})
+	unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label1.ID})
+	label1 = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label)
+	assert.EqualValues(t, 3, label1.NumIssues)
+	assert.EqualValues(t, 1, label1.NumClosedIssues)
+	label2 = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2}).(*issues_model.Label)
+	assert.EqualValues(t, 1, label2.NumIssues)
+	assert.EqualValues(t, 1, label2.NumClosedIssues)
+
+	// corner case: test empty slice
+	assert.NoError(t, issues_model.NewIssueLabels(issue, []*issues_model.Label{}, doer))
+
+	unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.Label{})
+}
+
+func TestDeleteIssueLabel(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	testSuccess := func(labelID, issueID, doerID int64) {
+		label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID}).(*issues_model.Label)
+		issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueID}).(*issues_model.Issue)
+		doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: doerID}).(*user_model.User)
+
+		expectedNumIssues := label.NumIssues
+		expectedNumClosedIssues := label.NumClosedIssues
+		if unittest.BeanExists(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID}) {
+			expectedNumIssues--
+			if issue.IsClosed {
+				expectedNumClosedIssues--
+			}
+		}
+
+		ctx, committer, err := db.TxContext()
+		defer committer.Close()
+		assert.NoError(t, err)
+		assert.NoError(t, issues_model.DeleteIssueLabel(ctx, issue, label, doer))
+		assert.NoError(t, committer.Commit())
+
+		unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID})
+		unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
+			Type:     issues_model.CommentTypeLabel,
+			PosterID: doerID,
+			IssueID:  issueID,
+			LabelID:  labelID,
+		}, `content=""`)
+		label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID}).(*issues_model.Label)
+		assert.EqualValues(t, expectedNumIssues, label.NumIssues)
+		assert.EqualValues(t, expectedNumClosedIssues, label.NumClosedIssues)
+	}
+	testSuccess(1, 1, 2)
+	testSuccess(2, 5, 2)
+	testSuccess(1, 1, 2) // delete non-existent IssueLabel
+
+	unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.Label{})
+}
diff --git a/models/issues/main_test.go b/models/issues/main_test.go
index 30f6ff02fb..e34bef62ca 100644
--- a/models/issues/main_test.go
+++ b/models/issues/main_test.go
@@ -2,14 +2,20 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package issues
+package issues_test
 
 import (
 	"path/filepath"
 	"testing"
 
+	_ "code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
+	_ "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
+	_ "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/setting"
+
+	"github.com/stretchr/testify/assert"
 )
 
 func init() {
@@ -17,14 +23,18 @@ func init() {
 	setting.LoadForTest()
 }
 
+func TestFixturesAreConsistent(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	unittest.CheckConsistencyFor(t,
+		&issues_model.Issue{},
+		&issues_model.PullRequest{},
+		&issues_model.Milestone{},
+		&issues_model.Label{},
+	)
+}
+
 func TestMain(m *testing.M) {
 	unittest.MainTest(m, &unittest.TestOptions{
 		GiteaRootPath: filepath.Join("..", ".."),
-		FixtureFiles: []string{
-			"reaction.yml",
-			"user.yml",
-			"repository.yml",
-			"milestone.yml",
-		},
 	})
 }
diff --git a/models/issues/milestone.go b/models/issues/milestone.go
index f7172f6448..6c10959108 100644
--- a/models/issues/milestone.go
+++ b/models/issues/milestone.go
@@ -292,11 +292,17 @@ func DeleteMilestoneByRepoID(repoID, id int64) error {
 		return err
 	}
 
-	numMilestones, err := countRepoMilestones(ctx, repo.ID)
+	numMilestones, err := CountMilestones(ctx, GetMilestonesOption{
+		RepoID: repo.ID,
+		State:  api.StateAll,
+	})
 	if err != nil {
 		return err
 	}
-	numClosedMilestones, err := countRepoClosedMilestones(ctx, repo.ID)
+	numClosedMilestones, err := CountMilestones(ctx, GetMilestonesOption{
+		RepoID: repo.ID,
+		State:  api.StateClosed,
+	})
 	if err != nil {
 		return err
 	}
@@ -428,13 +434,6 @@ func GetMilestonesByRepoIDs(repoIDs []int64, page int, isClosed bool, sortType s
 	)
 }
 
-//  ____  _        _
-// / ___|| |_ __ _| |_ ___
-// \___ \| __/ _` | __/ __|
-//  ___) | || (_| | |_\__ \
-// |____/ \__\__,_|\__|___/
-//
-
 // MilestonesStats represents milestone statistic information.
 type MilestonesStats struct {
 	OpenCount, ClosedCount int64
@@ -503,23 +502,13 @@ func GetMilestonesStatsByRepoCondAndKw(repoCond builder.Cond, keyword string) (*
 	return stats, nil
 }
 
-func countRepoMilestones(ctx context.Context, repoID int64) (int64, error) {
+// CountMilestones returns number of milestones in given repository with other options
+func CountMilestones(ctx context.Context, opts GetMilestonesOption) (int64, error) {
 	return db.GetEngine(ctx).
-		Where("repo_id=?", repoID).
+		Where(opts.toCond()).
 		Count(new(Milestone))
 }
 
-func countRepoClosedMilestones(ctx context.Context, repoID int64) (int64, error) {
-	return db.GetEngine(ctx).
-		Where("repo_id=? AND is_closed=?", repoID, true).
-		Count(new(Milestone))
-}
-
-// CountRepoClosedMilestones returns number of closed milestones in given repository.
-func CountRepoClosedMilestones(repoID int64) (int64, error) {
-	return countRepoClosedMilestones(db.DefaultContext, repoID)
-}
-
 // CountMilestonesByRepoCond map from repo conditions to number of milestones matching the options`
 func CountMilestonesByRepoCond(repoCond builder.Cond, isClosed bool) (map[int64]int64, error) {
 	sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed)
diff --git a/models/issues/milestone_test.go b/models/issues/milestone_test.go
index e087318320..a6fbf9c23b 100644
--- a/models/issues/milestone_test.go
+++ b/models/issues/milestone_test.go
@@ -2,44 +2,46 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package issues
+package issues_test
 
 import (
 	"sort"
 	"testing"
 
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/timeutil"
 
 	"github.com/stretchr/testify/assert"
 	"xorm.io/builder"
 )
 
 func TestMilestone_State(t *testing.T) {
-	assert.Equal(t, api.StateOpen, (&Milestone{IsClosed: false}).State())
-	assert.Equal(t, api.StateClosed, (&Milestone{IsClosed: true}).State())
+	assert.Equal(t, api.StateOpen, (&issues_model.Milestone{IsClosed: false}).State())
+	assert.Equal(t, api.StateClosed, (&issues_model.Milestone{IsClosed: true}).State())
 }
 
 func TestGetMilestoneByRepoID(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	milestone, err := GetMilestoneByRepoID(db.DefaultContext, 1, 1)
+	milestone, err := issues_model.GetMilestoneByRepoID(db.DefaultContext, 1, 1)
 	assert.NoError(t, err)
 	assert.EqualValues(t, 1, milestone.ID)
 	assert.EqualValues(t, 1, milestone.RepoID)
 
-	_, err = GetMilestoneByRepoID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID)
-	assert.True(t, IsErrMilestoneNotExist(err))
+	_, err = issues_model.GetMilestoneByRepoID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID)
+	assert.True(t, issues_model.IsErrMilestoneNotExist(err))
 }
 
 func TestGetMilestonesByRepoID(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	test := func(repoID int64, state api.StateType) {
 		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}).(*repo_model.Repository)
-		milestones, _, err := GetMilestones(GetMilestonesOption{
+		milestones, _, err := issues_model.GetMilestones(issues_model.GetMilestonesOption{
 			RepoID: repo.ID,
 			State:  state,
 		})
@@ -76,7 +78,7 @@ func TestGetMilestonesByRepoID(t *testing.T) {
 	test(3, api.StateClosed)
 	test(3, api.StateAll)
 
-	milestones, _, err := GetMilestones(GetMilestonesOption{
+	milestones, _, err := issues_model.GetMilestones(issues_model.GetMilestonesOption{
 		RepoID: unittest.NonexistentID,
 		State:  api.StateOpen,
 	})
@@ -87,9 +89,9 @@ func TestGetMilestonesByRepoID(t *testing.T) {
 func TestGetMilestones(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
-	test := func(sortType string, sortCond func(*Milestone) int) {
+	test := func(sortType string, sortCond func(*issues_model.Milestone) int) {
 		for _, page := range []int{0, 1} {
-			milestones, _, err := GetMilestones(GetMilestonesOption{
+			milestones, _, err := issues_model.GetMilestones(issues_model.GetMilestonesOption{
 				ListOptions: db.ListOptions{
 					Page:     page,
 					PageSize: setting.UI.IssuePagingNum,
@@ -106,7 +108,7 @@ func TestGetMilestones(t *testing.T) {
 			}
 			assert.True(t, sort.IntsAreSorted(values))
 
-			milestones, _, err = GetMilestones(GetMilestonesOption{
+			milestones, _, err = issues_model.GetMilestones(issues_model.GetMilestonesOption{
 				ListOptions: db.ListOptions{
 					Page:     page,
 					PageSize: setting.UI.IssuePagingNum,
@@ -125,22 +127,22 @@ func TestGetMilestones(t *testing.T) {
 			assert.True(t, sort.IntsAreSorted(values))
 		}
 	}
-	test("furthestduedate", func(milestone *Milestone) int {
+	test("furthestduedate", func(milestone *issues_model.Milestone) int {
 		return -int(milestone.DeadlineUnix)
 	})
-	test("leastcomplete", func(milestone *Milestone) int {
+	test("leastcomplete", func(milestone *issues_model.Milestone) int {
 		return milestone.Completeness
 	})
-	test("mostcomplete", func(milestone *Milestone) int {
+	test("mostcomplete", func(milestone *issues_model.Milestone) int {
 		return -milestone.Completeness
 	})
-	test("leastissues", func(milestone *Milestone) int {
+	test("leastissues", func(milestone *issues_model.Milestone) int {
 		return milestone.NumIssues
 	})
-	test("mostissues", func(milestone *Milestone) int {
+	test("mostissues", func(milestone *issues_model.Milestone) int {
 		return -milestone.NumIssues
 	})
-	test("soonestduedate", func(milestone *Milestone) int {
+	test("soonestduedate", func(milestone *issues_model.Milestone) int {
 		return int(milestone.DeadlineUnix)
 	})
 }
@@ -149,7 +151,10 @@ func TestCountRepoMilestones(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	test := func(repoID int64) {
 		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}).(*repo_model.Repository)
-		count, err := countRepoMilestones(db.DefaultContext, repoID)
+		count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
+			RepoID: repoID,
+			State:  api.StateAll,
+		})
 		assert.NoError(t, err)
 		assert.EqualValues(t, repo.NumMilestones, count)
 	}
@@ -157,7 +162,10 @@ func TestCountRepoMilestones(t *testing.T) {
 	test(2)
 	test(3)
 
-	count, err := countRepoMilestones(db.DefaultContext, unittest.NonexistentID)
+	count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
+		RepoID: unittest.NonexistentID,
+		State:  api.StateAll,
+	})
 	assert.NoError(t, err)
 	assert.EqualValues(t, 0, count)
 }
@@ -166,7 +174,10 @@ func TestCountRepoClosedMilestones(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	test := func(repoID int64) {
 		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}).(*repo_model.Repository)
-		count, err := CountRepoClosedMilestones(repoID)
+		count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
+			RepoID: repoID,
+			State:  api.StateClosed,
+		})
 		assert.NoError(t, err)
 		assert.EqualValues(t, repo.NumClosedMilestones, count)
 	}
@@ -174,7 +185,10 @@ func TestCountRepoClosedMilestones(t *testing.T) {
 	test(2)
 	test(3)
 
-	count, err := CountRepoClosedMilestones(unittest.NonexistentID)
+	count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
+		RepoID: unittest.NonexistentID,
+		State:  api.StateClosed,
+	})
 	assert.NoError(t, err)
 	assert.EqualValues(t, 0, count)
 }
@@ -188,12 +202,12 @@ func TestCountMilestonesByRepoIDs(t *testing.T) {
 	repo1OpenCount, repo1ClosedCount := milestonesCount(1)
 	repo2OpenCount, repo2ClosedCount := milestonesCount(2)
 
-	openCounts, err := CountMilestonesByRepoCond(builder.In("repo_id", []int64{1, 2}), false)
+	openCounts, err := issues_model.CountMilestonesByRepoCond(builder.In("repo_id", []int64{1, 2}), false)
 	assert.NoError(t, err)
 	assert.EqualValues(t, repo1OpenCount, openCounts[1])
 	assert.EqualValues(t, repo2OpenCount, openCounts[2])
 
-	closedCounts, err := CountMilestonesByRepoCond(builder.In("repo_id", []int64{1, 2}), true)
+	closedCounts, err := issues_model.CountMilestonesByRepoCond(builder.In("repo_id", []int64{1, 2}), true)
 	assert.NoError(t, err)
 	assert.EqualValues(t, repo1ClosedCount, closedCounts[1])
 	assert.EqualValues(t, repo2ClosedCount, closedCounts[2])
@@ -203,9 +217,9 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
 	repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository)
-	test := func(sortType string, sortCond func(*Milestone) int) {
+	test := func(sortType string, sortCond func(*issues_model.Milestone) int) {
 		for _, page := range []int{0, 1} {
-			openMilestones, err := GetMilestonesByRepoIDs([]int64{repo1.ID, repo2.ID}, page, false, sortType)
+			openMilestones, err := issues_model.GetMilestonesByRepoIDs([]int64{repo1.ID, repo2.ID}, page, false, sortType)
 			assert.NoError(t, err)
 			assert.Len(t, openMilestones, repo1.NumOpenMilestones+repo2.NumOpenMilestones)
 			values := make([]int, len(openMilestones))
@@ -214,7 +228,7 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
 			}
 			assert.True(t, sort.IntsAreSorted(values))
 
-			closedMilestones, err := GetMilestonesByRepoIDs([]int64{repo1.ID, repo2.ID}, page, true, sortType)
+			closedMilestones, err := issues_model.GetMilestonesByRepoIDs([]int64{repo1.ID, repo2.ID}, page, true, sortType)
 			assert.NoError(t, err)
 			assert.Len(t, closedMilestones, repo1.NumClosedMilestones+repo2.NumClosedMilestones)
 			values = make([]int, len(closedMilestones))
@@ -224,22 +238,22 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
 			assert.True(t, sort.IntsAreSorted(values))
 		}
 	}
-	test("furthestduedate", func(milestone *Milestone) int {
+	test("furthestduedate", func(milestone *issues_model.Milestone) int {
 		return -int(milestone.DeadlineUnix)
 	})
-	test("leastcomplete", func(milestone *Milestone) int {
+	test("leastcomplete", func(milestone *issues_model.Milestone) int {
 		return milestone.Completeness
 	})
-	test("mostcomplete", func(milestone *Milestone) int {
+	test("mostcomplete", func(milestone *issues_model.Milestone) int {
 		return -milestone.Completeness
 	})
-	test("leastissues", func(milestone *Milestone) int {
+	test("leastissues", func(milestone *issues_model.Milestone) int {
 		return milestone.NumIssues
 	})
-	test("mostissues", func(milestone *Milestone) int {
+	test("mostissues", func(milestone *issues_model.Milestone) int {
 		return -milestone.NumIssues
 	})
-	test("soonestduedate", func(milestone *Milestone) int {
+	test("soonestduedate", func(milestone *issues_model.Milestone) int {
 		return int(milestone.DeadlineUnix)
 	})
 }
@@ -249,7 +263,7 @@ func TestGetMilestonesStats(t *testing.T) {
 
 	test := func(repoID int64) {
 		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}).(*repo_model.Repository)
-		stats, err := GetMilestonesStatsByRepoCond(builder.And(builder.Eq{"repo_id": repoID}))
+		stats, err := issues_model.GetMilestonesStatsByRepoCond(builder.And(builder.Eq{"repo_id": repoID}))
 		assert.NoError(t, err)
 		assert.EqualValues(t, repo.NumMilestones-repo.NumClosedMilestones, stats.OpenCount)
 		assert.EqualValues(t, repo.NumClosedMilestones, stats.ClosedCount)
@@ -258,7 +272,7 @@ func TestGetMilestonesStats(t *testing.T) {
 	test(2)
 	test(3)
 
-	stats, err := GetMilestonesStatsByRepoCond(builder.And(builder.Eq{"repo_id": unittest.NonexistentID}))
+	stats, err := issues_model.GetMilestonesStatsByRepoCond(builder.And(builder.Eq{"repo_id": unittest.NonexistentID}))
 	assert.NoError(t, err)
 	assert.EqualValues(t, 0, stats.OpenCount)
 	assert.EqualValues(t, 0, stats.ClosedCount)
@@ -266,8 +280,75 @@ func TestGetMilestonesStats(t *testing.T) {
 	repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
 	repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository)
 
-	milestoneStats, err := GetMilestonesStatsByRepoCond(builder.In("repo_id", []int64{repo1.ID, repo2.ID}))
+	milestoneStats, err := issues_model.GetMilestonesStatsByRepoCond(builder.In("repo_id", []int64{repo1.ID, repo2.ID}))
 	assert.NoError(t, err)
 	assert.EqualValues(t, repo1.NumOpenMilestones+repo2.NumOpenMilestones, milestoneStats.OpenCount)
 	assert.EqualValues(t, repo1.NumClosedMilestones+repo2.NumClosedMilestones, milestoneStats.ClosedCount)
 }
+
+func TestNewMilestone(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	milestone := &issues_model.Milestone{
+		RepoID:  1,
+		Name:    "milestoneName",
+		Content: "milestoneContent",
+	}
+
+	assert.NoError(t, issues_model.NewMilestone(milestone))
+	unittest.AssertExistsAndLoadBean(t, milestone)
+	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &issues_model.Milestone{})
+}
+
+func TestChangeMilestoneStatus(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone)
+
+	assert.NoError(t, issues_model.ChangeMilestoneStatus(milestone, true))
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}, "is_closed=1")
+	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &issues_model.Milestone{})
+
+	assert.NoError(t, issues_model.ChangeMilestoneStatus(milestone, false))
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}, "is_closed=0")
+	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &issues_model.Milestone{})
+}
+
+func TestDeleteMilestoneByRepoID(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	assert.NoError(t, issues_model.DeleteMilestoneByRepoID(1, 1))
+	unittest.AssertNotExistsBean(t, &issues_model.Milestone{ID: 1})
+	unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: 1})
+
+	assert.NoError(t, issues_model.DeleteMilestoneByRepoID(unittest.NonexistentID, unittest.NonexistentID))
+}
+
+func TestUpdateMilestone(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone)
+	milestone.Name = " newMilestoneName  "
+	milestone.Content = "newMilestoneContent"
+	assert.NoError(t, issues_model.UpdateMilestone(milestone, milestone.IsClosed))
+	milestone = unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone)
+	assert.EqualValues(t, "newMilestoneName", milestone.Name)
+	unittest.CheckConsistencyFor(t, &issues_model.Milestone{})
+}
+
+func TestUpdateMilestoneCounters(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{MilestoneID: 1},
+		"is_closed=0").(*issues_model.Issue)
+
+	issue.IsClosed = true
+	issue.ClosedUnix = timeutil.TimeStampNow()
+	_, err := db.GetEngine(db.DefaultContext).ID(issue.ID).Cols("is_closed", "closed_unix").Update(issue)
+	assert.NoError(t, err)
+	assert.NoError(t, issues_model.UpdateMilestoneCounters(db.DefaultContext, issue.MilestoneID))
+	unittest.CheckConsistencyFor(t, &issues_model.Milestone{})
+
+	issue.IsClosed = false
+	issue.ClosedUnix = 0
+	_, err = db.GetEngine(db.DefaultContext).ID(issue.ID).Cols("is_closed", "closed_unix").Update(issue)
+	assert.NoError(t, err)
+	assert.NoError(t, issues_model.UpdateMilestoneCounters(db.DefaultContext, issue.MilestoneID))
+	unittest.CheckConsistencyFor(t, &issues_model.Milestone{})
+}
diff --git a/models/pull.go b/models/issues/pull.go
similarity index 80%
rename from models/pull.go
rename to models/issues/pull.go
index 238eb16636..f2ca19b03e 100644
--- a/models/pull.go
+++ b/models/issues/pull.go
@@ -3,7 +3,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues
 
 import (
 	"context"
@@ -25,6 +25,83 @@ import (
 	"xorm.io/builder"
 )
 
+// ErrPullRequestNotExist represents a "PullRequestNotExist" kind of error.
+type ErrPullRequestNotExist struct {
+	ID         int64
+	IssueID    int64
+	HeadRepoID int64
+	BaseRepoID int64
+	HeadBranch string
+	BaseBranch string
+}
+
+// IsErrPullRequestNotExist checks if an error is a ErrPullRequestNotExist.
+func IsErrPullRequestNotExist(err error) bool {
+	_, ok := err.(ErrPullRequestNotExist)
+	return ok
+}
+
+func (err ErrPullRequestNotExist) Error() string {
+	return fmt.Sprintf("pull request does not exist [id: %d, issue_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]",
+		err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch)
+}
+
+// ErrPullRequestAlreadyExists represents a "PullRequestAlreadyExists"-error
+type ErrPullRequestAlreadyExists struct {
+	ID         int64
+	IssueID    int64
+	HeadRepoID int64
+	BaseRepoID int64
+	HeadBranch string
+	BaseBranch string
+}
+
+// IsErrPullRequestAlreadyExists checks if an error is a ErrPullRequestAlreadyExists.
+func IsErrPullRequestAlreadyExists(err error) bool {
+	_, ok := err.(ErrPullRequestAlreadyExists)
+	return ok
+}
+
+// Error does pretty-printing :D
+func (err ErrPullRequestAlreadyExists) Error() string {
+	return fmt.Sprintf("pull request already exists for these targets [id: %d, issue_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]",
+		err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch)
+}
+
+// ErrPullRequestHeadRepoMissing represents a "ErrPullRequestHeadRepoMissing" error
+type ErrPullRequestHeadRepoMissing struct {
+	ID         int64
+	HeadRepoID int64
+}
+
+// IsErrErrPullRequestHeadRepoMissing checks if an error is a ErrPullRequestHeadRepoMissing.
+func IsErrErrPullRequestHeadRepoMissing(err error) bool {
+	_, ok := err.(ErrPullRequestHeadRepoMissing)
+	return ok
+}
+
+// Error does pretty-printing :D
+func (err ErrPullRequestHeadRepoMissing) Error() string {
+	return fmt.Sprintf("pull request head repo missing [id: %d, head_repo_id: %d]",
+		err.ID, err.HeadRepoID)
+}
+
+// ErrPullWasClosed is used close a closed pull request
+type ErrPullWasClosed struct {
+	ID    int64
+	Index int64
+}
+
+// IsErrPullWasClosed checks if an error is a ErrErrPullWasClosed.
+func IsErrPullWasClosed(err error) bool {
+	_, ok := err.(ErrPullWasClosed)
+	return ok
+}
+
+func (err ErrPullWasClosed) Error() string {
+	return fmt.Sprintf("Pull request [%d] %d was already closed", err.ID, err.Index)
+}
+
 // PullRequestType defines pull request type
 type PullRequestType int
 
@@ -98,7 +175,8 @@ func init() {
 	db.RegisterModel(new(PullRequest))
 }
 
-func deletePullsByBaseRepoID(ctx context.Context, repoID int64) error {
+// DeletePullsByBaseRepoID deletes all pull requests by the base repository ID
+func DeletePullsByBaseRepoID(ctx context.Context, repoID int64) error {
 	deleteCond := builder.Select("id").From("pull_request").Where(builder.Eq{"pull_request.base_repo_id": repoID})
 
 	// Delete scheduled auto merges
@@ -219,7 +297,7 @@ func (pr *PullRequest) LoadIssueCtx(ctx context.Context) (err error) {
 		return nil
 	}
 
-	pr.Issue, err = getIssueByID(ctx, pr.IssueID)
+	pr.Issue, err = GetIssueByID(ctx, pr.IssueID)
 	if err == nil {
 		pr.Issue.PullRequest = pr
 	}
@@ -420,14 +498,14 @@ func NewPullRequest(outerCtx context.Context, repo *repo_model.Repository, issue
 	defer committer.Close()
 	ctx.WithContext(outerCtx)
 
-	if err = newIssue(ctx, issue.Poster, NewIssueOptions{
+	if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{
 		Repo:        repo,
 		Issue:       issue,
 		LabelIDs:    labelIDs,
 		Attachments: uuids,
 		IsPull:      true,
 	}); err != nil {
-		if IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) {
+		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) {
 			return err
 		}
 		return fmt.Errorf("newIssue: %v", err)
@@ -691,3 +769,70 @@ func (pr *PullRequest) Mergeable() bool {
 	return pr.Status != PullRequestStatusChecking && pr.Status != PullRequestStatusConflict &&
 		pr.Status != PullRequestStatusError && !pr.IsWorkInProgress()
 }
+
+// HasEnoughApprovals returns true if pr has enough granted approvals.
+func HasEnoughApprovals(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool {
+	if protectBranch.RequiredApprovals == 0 {
+		return true
+	}
+	return GetGrantedApprovalsCount(ctx, protectBranch, pr) >= protectBranch.RequiredApprovals
+}
+
+// GetGrantedApprovalsCount returns the number of granted approvals for pr. A granted approval must be authored by a user in an approval whitelist.
+func GetGrantedApprovalsCount(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) int64 {
+	sess := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID).
+		And("type = ?", ReviewTypeApprove).
+		And("official = ?", true).
+		And("dismissed = ?", false)
+	if protectBranch.DismissStaleApprovals {
+		sess = sess.And("stale = ?", false)
+	}
+	approvals, err := sess.Count(new(Review))
+	if err != nil {
+		log.Error("GetGrantedApprovalsCount: %v", err)
+		return 0
+	}
+
+	return approvals
+}
+
+// MergeBlockedByRejectedReview returns true if merge is blocked by rejected reviews
+func MergeBlockedByRejectedReview(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool {
+	if !protectBranch.BlockOnRejectedReviews {
+		return false
+	}
+	rejectExist, err := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID).
+		And("type = ?", ReviewTypeReject).
+		And("official = ?", true).
+		And("dismissed = ?", false).
+		Exist(new(Review))
+	if err != nil {
+		log.Error("MergeBlockedByRejectedReview: %v", err)
+		return true
+	}
+
+	return rejectExist
+}
+
+// MergeBlockedByOfficialReviewRequests block merge because of some review request to official reviewer
+// of from official review
+func MergeBlockedByOfficialReviewRequests(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool {
+	if !protectBranch.BlockOnOfficialReviewRequests {
+		return false
+	}
+	has, err := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID).
+		And("type = ?", ReviewTypeRequest).
+		And("official = ?", true).
+		Exist(new(Review))
+	if err != nil {
+		log.Error("MergeBlockedByOfficialReviewRequests: %v", err)
+		return true
+	}
+
+	return has
+}
+
+// MergeBlockedByOutdatedBranch returns true if merge is blocked by an outdated head branch
+func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool {
+	return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0
+}
diff --git a/models/pull_list.go b/models/issues/pull_list.go
similarity index 99%
rename from models/pull_list.go
rename to models/issues/pull_list.go
index fb14d3beac..9ca536909e 100644
--- a/models/pull_list.go
+++ b/models/issues/pull_list.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues
 
 import (
 	"context"
diff --git a/models/pull_test.go b/models/issues/pull_test.go
similarity index 58%
rename from models/pull_test.go
rename to models/issues/pull_test.go
index 00bbfc798a..0d1991383d 100644
--- a/models/pull_test.go
+++ b/models/issues/pull_test.go
@@ -2,12 +2,13 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues_test
 
 import (
 	"testing"
 
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
 
 	"github.com/stretchr/testify/assert"
@@ -15,7 +16,7 @@ import (
 
 func TestPullRequest_LoadAttributes(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 1}).(*PullRequest)
+	pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest)
 	assert.NoError(t, pr.LoadAttributes())
 	assert.NotNil(t, pr.Merger)
 	assert.Equal(t, pr.MergerID, pr.Merger.ID)
@@ -23,7 +24,7 @@ func TestPullRequest_LoadAttributes(t *testing.T) {
 
 func TestPullRequest_LoadIssue(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 1}).(*PullRequest)
+	pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest)
 	assert.NoError(t, pr.LoadIssue())
 	assert.NotNil(t, pr.Issue)
 	assert.Equal(t, int64(2), pr.Issue.ID)
@@ -34,7 +35,7 @@ func TestPullRequest_LoadIssue(t *testing.T) {
 
 func TestPullRequest_LoadBaseRepo(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 1}).(*PullRequest)
+	pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest)
 	assert.NoError(t, pr.LoadBaseRepo())
 	assert.NotNil(t, pr.BaseRepo)
 	assert.Equal(t, pr.BaseRepoID, pr.BaseRepo.ID)
@@ -45,7 +46,7 @@ func TestPullRequest_LoadBaseRepo(t *testing.T) {
 
 func TestPullRequest_LoadHeadRepo(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 1}).(*PullRequest)
+	pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest)
 	assert.NoError(t, pr.LoadHeadRepo())
 	assert.NotNil(t, pr.HeadRepo)
 	assert.Equal(t, pr.HeadRepoID, pr.HeadRepo.ID)
@@ -57,7 +58,7 @@ func TestPullRequest_LoadHeadRepo(t *testing.T) {
 
 func TestPullRequestsNewest(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	prs, count, err := PullRequests(1, &PullRequestsOptions{
+	prs, count, err := issues_model.PullRequests(1, &issues_model.PullRequestsOptions{
 		ListOptions: db.ListOptions{
 			Page: 1,
 		},
@@ -76,7 +77,7 @@ func TestPullRequestsNewest(t *testing.T) {
 
 func TestPullRequestsOldest(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	prs, count, err := PullRequests(1, &PullRequestsOptions{
+	prs, count, err := issues_model.PullRequests(1, &issues_model.PullRequestsOptions{
 		ListOptions: db.ListOptions{
 			Page: 1,
 		},
@@ -95,30 +96,30 @@ func TestPullRequestsOldest(t *testing.T) {
 
 func TestGetUnmergedPullRequest(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	pr, err := GetUnmergedPullRequest(1, 1, "branch2", "master", PullRequestFlowGithub)
+	pr, err := issues_model.GetUnmergedPullRequest(1, 1, "branch2", "master", issues_model.PullRequestFlowGithub)
 	assert.NoError(t, err)
 	assert.Equal(t, int64(2), pr.ID)
 
-	_, err = GetUnmergedPullRequest(1, 9223372036854775807, "branch1", "master", PullRequestFlowGithub)
+	_, err = issues_model.GetUnmergedPullRequest(1, 9223372036854775807, "branch1", "master", issues_model.PullRequestFlowGithub)
 	assert.Error(t, err)
-	assert.True(t, IsErrPullRequestNotExist(err))
+	assert.True(t, issues_model.IsErrPullRequestNotExist(err))
 }
 
 func TestHasUnmergedPullRequestsByHeadInfo(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	exist, err := HasUnmergedPullRequestsByHeadInfo(db.DefaultContext, 1, "branch2")
+	exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(db.DefaultContext, 1, "branch2")
 	assert.NoError(t, err)
 	assert.Equal(t, true, exist)
 
-	exist, err = HasUnmergedPullRequestsByHeadInfo(db.DefaultContext, 1, "not_exist_branch")
+	exist, err = issues_model.HasUnmergedPullRequestsByHeadInfo(db.DefaultContext, 1, "not_exist_branch")
 	assert.NoError(t, err)
 	assert.Equal(t, false, exist)
 }
 
 func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	prs, err := GetUnmergedPullRequestsByHeadInfo(1, "branch2")
+	prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(1, "branch2")
 	assert.NoError(t, err)
 	assert.Len(t, prs, 1)
 	for _, pr := range prs {
@@ -129,7 +130,7 @@ func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) {
 
 func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	prs, err := GetUnmergedPullRequestsByBaseInfo(1, "master")
+	prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(1, "master")
 	assert.NoError(t, err)
 	assert.Len(t, prs, 1)
 	pr := prs[0]
@@ -140,51 +141,51 @@ func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) {
 
 func TestGetPullRequestByIndex(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	pr, err := GetPullRequestByIndex(db.DefaultContext, 1, 2)
+	pr, err := issues_model.GetPullRequestByIndex(db.DefaultContext, 1, 2)
 	assert.NoError(t, err)
 	assert.Equal(t, int64(1), pr.BaseRepoID)
 	assert.Equal(t, int64(2), pr.Index)
 
-	_, err = GetPullRequestByIndex(db.DefaultContext, 9223372036854775807, 9223372036854775807)
+	_, err = issues_model.GetPullRequestByIndex(db.DefaultContext, 9223372036854775807, 9223372036854775807)
 	assert.Error(t, err)
-	assert.True(t, IsErrPullRequestNotExist(err))
+	assert.True(t, issues_model.IsErrPullRequestNotExist(err))
 
-	_, err = GetPullRequestByIndex(db.DefaultContext, 1, 0)
+	_, err = issues_model.GetPullRequestByIndex(db.DefaultContext, 1, 0)
 	assert.Error(t, err)
-	assert.True(t, IsErrPullRequestNotExist(err))
+	assert.True(t, issues_model.IsErrPullRequestNotExist(err))
 }
 
 func TestGetPullRequestByID(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	pr, err := GetPullRequestByID(db.DefaultContext, 1)
+	pr, err := issues_model.GetPullRequestByID(db.DefaultContext, 1)
 	assert.NoError(t, err)
 	assert.Equal(t, int64(1), pr.ID)
 	assert.Equal(t, int64(2), pr.IssueID)
 
-	_, err = GetPullRequestByID(db.DefaultContext, 9223372036854775807)
+	_, err = issues_model.GetPullRequestByID(db.DefaultContext, 9223372036854775807)
 	assert.Error(t, err)
-	assert.True(t, IsErrPullRequestNotExist(err))
+	assert.True(t, issues_model.IsErrPullRequestNotExist(err))
 }
 
 func TestGetPullRequestByIssueID(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	pr, err := GetPullRequestByIssueID(db.DefaultContext, 2)
+	pr, err := issues_model.GetPullRequestByIssueID(db.DefaultContext, 2)
 	assert.NoError(t, err)
 	assert.Equal(t, int64(2), pr.IssueID)
 
-	_, err = GetPullRequestByIssueID(db.DefaultContext, 9223372036854775807)
+	_, err = issues_model.GetPullRequestByIssueID(db.DefaultContext, 9223372036854775807)
 	assert.Error(t, err)
-	assert.True(t, IsErrPullRequestNotExist(err))
+	assert.True(t, issues_model.IsErrPullRequestNotExist(err))
 }
 
 func TestPullRequest_Update(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 1}).(*PullRequest)
+	pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest)
 	pr.BaseBranch = "baseBranch"
 	pr.HeadBranch = "headBranch"
 	pr.Update()
 
-	pr = unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: pr.ID}).(*PullRequest)
+	pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID}).(*issues_model.PullRequest)
 	assert.Equal(t, "baseBranch", pr.BaseBranch)
 	assert.Equal(t, "headBranch", pr.HeadBranch)
 	unittest.CheckConsistencyFor(t, pr)
@@ -192,14 +193,14 @@ func TestPullRequest_Update(t *testing.T) {
 
 func TestPullRequest_UpdateCols(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	pr := &PullRequest{
+	pr := &issues_model.PullRequest{
 		ID:         1,
 		BaseBranch: "baseBranch",
 		HeadBranch: "headBranch",
 	}
 	assert.NoError(t, pr.UpdateCols("head_branch"))
 
-	pr = unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 1}).(*PullRequest)
+	pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest)
 	assert.Equal(t, "master", pr.BaseBranch)
 	assert.Equal(t, "headBranch", pr.HeadBranch)
 	unittest.CheckConsistencyFor(t, pr)
@@ -208,17 +209,17 @@ func TestPullRequest_UpdateCols(t *testing.T) {
 func TestPullRequestList_LoadAttributes(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	prs := []*PullRequest{
-		unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 1}).(*PullRequest),
-		unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 2}).(*PullRequest),
+	prs := []*issues_model.PullRequest{
+		unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest),
+		unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}).(*issues_model.PullRequest),
 	}
-	assert.NoError(t, PullRequestList(prs).LoadAttributes())
+	assert.NoError(t, issues_model.PullRequestList(prs).LoadAttributes())
 	for _, pr := range prs {
 		assert.NotNil(t, pr.Issue)
 		assert.Equal(t, pr.IssueID, pr.Issue.ID)
 	}
 
-	assert.NoError(t, PullRequestList([]*PullRequest{}).LoadAttributes())
+	assert.NoError(t, issues_model.PullRequestList([]*issues_model.PullRequest{}).LoadAttributes())
 }
 
 // TODO TestAddTestPullRequestTask
@@ -226,7 +227,7 @@ func TestPullRequestList_LoadAttributes(t *testing.T) {
 func TestPullRequest_IsWorkInProgress(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 2}).(*PullRequest)
+	pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}).(*issues_model.PullRequest)
 	pr.LoadIssue()
 
 	assert.False(t, pr.IsWorkInProgress())
@@ -241,7 +242,7 @@ func TestPullRequest_IsWorkInProgress(t *testing.T) {
 func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 2}).(*PullRequest)
+	pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}).(*issues_model.PullRequest)
 	pr.LoadIssue()
 
 	assert.Empty(t, pr.GetWorkInProgressPrefix())
@@ -253,3 +254,24 @@ func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) {
 	pr.Issue.Title = "[wip] " + original
 	assert.Equal(t, "[wip]", pr.GetWorkInProgressPrefix())
 }
+
+func TestDeleteOrphanedObjects(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	countBefore, err := db.GetEngine(db.DefaultContext).Count(&issues_model.PullRequest{})
+	assert.NoError(t, err)
+
+	_, err = db.GetEngine(db.DefaultContext).Insert(&issues_model.PullRequest{IssueID: 1000}, &issues_model.PullRequest{IssueID: 1001}, &issues_model.PullRequest{IssueID: 1003})
+	assert.NoError(t, err)
+
+	orphaned, err := db.CountOrphanedObjects("pull_request", "issue", "pull_request.issue_id=issue.id")
+	assert.NoError(t, err)
+	assert.EqualValues(t, 3, orphaned)
+
+	err = db.DeleteOrphanedObjects("pull_request", "issue", "pull_request.issue_id=issue.id")
+	assert.NoError(t, err)
+
+	countAfter, err := db.GetEngine(db.DefaultContext).Count(&issues_model.PullRequest{})
+	assert.NoError(t, err)
+	assert.EqualValues(t, countBefore, countAfter)
+}
diff --git a/models/issues/reaction_test.go b/models/issues/reaction_test.go
index b1216a3a69..ee1b6687a2 100644
--- a/models/issues/reaction_test.go
+++ b/models/issues/reaction_test.go
@@ -2,12 +2,13 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package issues
+package issues_test
 
 import (
 	"testing"
 
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
@@ -17,12 +18,12 @@ import (
 )
 
 func addReaction(t *testing.T, doerID, issueID, commentID int64, content string) {
-	var reaction *Reaction
+	var reaction *issues_model.Reaction
 	var err error
 	if commentID == 0 {
-		reaction, err = CreateIssueReaction(doerID, issueID, content)
+		reaction, err = issues_model.CreateIssueReaction(doerID, issueID, content)
 	} else {
-		reaction, err = CreateCommentReaction(doerID, issueID, commentID, content)
+		reaction, err = issues_model.CreateCommentReaction(doerID, issueID, commentID, content)
 	}
 	assert.NoError(t, err)
 	assert.NotNil(t, reaction)
@@ -37,7 +38,7 @@ func TestIssueAddReaction(t *testing.T) {
 
 	addReaction(t, user1.ID, issue1ID, 0, "heart")
 
-	unittest.AssertExistsAndLoadBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
 }
 
 func TestIssueAddDuplicateReaction(t *testing.T) {
@@ -49,15 +50,15 @@ func TestIssueAddDuplicateReaction(t *testing.T) {
 
 	addReaction(t, user1.ID, issue1ID, 0, "heart")
 
-	reaction, err := CreateReaction(&ReactionOptions{
+	reaction, err := issues_model.CreateReaction(&issues_model.ReactionOptions{
 		DoerID:  user1.ID,
 		IssueID: issue1ID,
 		Type:    "heart",
 	})
 	assert.Error(t, err)
-	assert.Equal(t, ErrReactionAlreadyExist{Reaction: "heart"}, err)
+	assert.Equal(t, issues_model.ErrReactionAlreadyExist{Reaction: "heart"}, err)
 
-	existingR := unittest.AssertExistsAndLoadBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID}).(*Reaction)
+	existingR := unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID}).(*issues_model.Reaction)
 	assert.Equal(t, existingR.ID, reaction.ID)
 }
 
@@ -70,10 +71,10 @@ func TestIssueDeleteReaction(t *testing.T) {
 
 	addReaction(t, user1.ID, issue1ID, 0, "heart")
 
-	err := DeleteIssueReaction(user1.ID, issue1ID, "heart")
+	err := issues_model.DeleteIssueReaction(user1.ID, issue1ID, "heart")
 	assert.NoError(t, err)
 
-	unittest.AssertNotExistsBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
+	unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
 }
 
 func TestIssueReactionCount(t *testing.T) {
@@ -98,7 +99,7 @@ func TestIssueReactionCount(t *testing.T) {
 	addReaction(t, user4.ID, issueID, 0, "heart")
 	addReaction(t, ghost.ID, issueID, 0, "-1")
 
-	reactionsList, _, err := FindReactions(db.DefaultContext, FindReactionsOptions{
+	reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{
 		IssueID: issueID,
 	})
 	assert.NoError(t, err)
@@ -128,7 +129,7 @@ func TestIssueCommentAddReaction(t *testing.T) {
 
 	addReaction(t, user1.ID, issue1ID, comment1ID, "heart")
 
-	unittest.AssertExistsAndLoadBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID})
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID})
 }
 
 func TestIssueCommentDeleteReaction(t *testing.T) {
@@ -147,7 +148,7 @@ func TestIssueCommentDeleteReaction(t *testing.T) {
 	addReaction(t, user3.ID, issue1ID, comment1ID, "heart")
 	addReaction(t, user4.ID, issue1ID, comment1ID, "+1")
 
-	reactionsList, _, err := FindReactions(db.DefaultContext, FindReactionsOptions{
+	reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{
 		IssueID:   issue1ID,
 		CommentID: comment1ID,
 	})
@@ -168,7 +169,7 @@ func TestIssueCommentReactionCount(t *testing.T) {
 	var comment1ID int64 = 1
 
 	addReaction(t, user1.ID, issue1ID, comment1ID, "heart")
-	assert.NoError(t, DeleteCommentReaction(user1.ID, issue1ID, comment1ID, "heart"))
+	assert.NoError(t, issues_model.DeleteCommentReaction(user1.ID, issue1ID, comment1ID, "heart"))
 
-	unittest.AssertNotExistsBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID})
+	unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID})
 }
diff --git a/models/review.go b/models/issues/review.go
similarity index 94%
rename from models/review.go
rename to models/issues/review.go
index e92caba938..ee65bec3f8 100644
--- a/models/review.go
+++ b/models/issues/review.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues
 
 import (
 	"context"
@@ -17,11 +17,47 @@ import (
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
 
 	"xorm.io/builder"
 )
 
+// ErrReviewNotExist represents a "ReviewNotExist" kind of error.
+type ErrReviewNotExist struct {
+	ID int64
+}
+
+// IsErrReviewNotExist checks if an error is a ErrReviewNotExist.
+func IsErrReviewNotExist(err error) bool {
+	_, ok := err.(ErrReviewNotExist)
+	return ok
+}
+
+func (err ErrReviewNotExist) Error() string {
+	return fmt.Sprintf("review does not exist [id: %d]", err.ID)
+}
+
+// ErrNotValidReviewRequest an not allowed review request modify
+type ErrNotValidReviewRequest struct {
+	Reason string
+	UserID int64
+	RepoID int64
+}
+
+// IsErrNotValidReviewRequest checks if an error is a ErrNotValidReviewRequest.
+func IsErrNotValidReviewRequest(err error) bool {
+	_, ok := err.(ErrNotValidReviewRequest)
+	return ok
+}
+
+func (err ErrNotValidReviewRequest) Error() string {
+	return fmt.Sprintf("%s [user_id: %d, repo_id: %d]",
+		err.Reason,
+		err.UserID,
+		err.RepoID)
+}
+
 // ReviewType defines the sort of feedback a review gives
 type ReviewType int
 
@@ -105,7 +141,7 @@ func (r *Review) loadIssue(ctx context.Context) (err error) {
 	if r.Issue != nil {
 		return
 	}
-	r.Issue, err = getIssueByID(ctx, r.IssueID)
+	r.Issue, err = GetIssueByID(ctx, r.IssueID)
 	return
 }
 
@@ -967,3 +1003,16 @@ func (r *Review) GetExternalName() string { return r.OriginalAuthor }
 
 // GetExternalID ExternalUserRemappable interface
 func (r *Review) GetExternalID() int64 { return r.OriginalAuthorID }
+
+// UpdateReviewsMigrationsByType updates reviews' migrations information via given git service type and original id and poster id
+func UpdateReviewsMigrationsByType(tp structs.GitServiceType, originalAuthorID string, posterID int64) error {
+	_, err := db.GetEngine(db.DefaultContext).Table("review").
+		Where("original_author_id = ?", originalAuthorID).
+		And(migratedIssueCond(tp)).
+		Update(map[string]interface{}{
+			"reviewer_id":        posterID,
+			"original_author":    "",
+			"original_author_id": 0,
+		})
+	return err
+}
diff --git a/models/review_test.go b/models/issues/review_test.go
similarity index 50%
rename from models/review_test.go
rename to models/issues/review_test.go
index 93291f9f57..3506604b46 100644
--- a/models/review_test.go
+++ b/models/issues/review_test.go
@@ -2,12 +2,13 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues_test
 
 import (
 	"testing"
 
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 
@@ -16,34 +17,34 @@ import (
 
 func TestGetReviewByID(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	review, err := GetReviewByID(db.DefaultContext, 1)
+	review, err := issues_model.GetReviewByID(db.DefaultContext, 1)
 	assert.NoError(t, err)
 	assert.Equal(t, "Demo Review", review.Content)
-	assert.Equal(t, ReviewTypeApprove, review.Type)
+	assert.Equal(t, issues_model.ReviewTypeApprove, review.Type)
 
-	_, err = GetReviewByID(db.DefaultContext, 23892)
+	_, err = issues_model.GetReviewByID(db.DefaultContext, 23892)
 	assert.Error(t, err)
-	assert.True(t, IsErrReviewNotExist(err), "IsErrReviewNotExist")
+	assert.True(t, issues_model.IsErrReviewNotExist(err), "IsErrReviewNotExist")
 }
 
 func TestReview_LoadAttributes(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	review := unittest.AssertExistsAndLoadBean(t, &Review{ID: 1}).(*Review)
+	review := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 1}).(*issues_model.Review)
 	assert.NoError(t, review.LoadAttributes(db.DefaultContext))
 	assert.NotNil(t, review.Issue)
 	assert.NotNil(t, review.Reviewer)
 
-	invalidReview1 := unittest.AssertExistsAndLoadBean(t, &Review{ID: 2}).(*Review)
+	invalidReview1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 2}).(*issues_model.Review)
 	assert.Error(t, invalidReview1.LoadAttributes(db.DefaultContext))
 
-	invalidReview2 := unittest.AssertExistsAndLoadBean(t, &Review{ID: 3}).(*Review)
+	invalidReview2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 3}).(*issues_model.Review)
 	assert.Error(t, invalidReview2.LoadAttributes(db.DefaultContext))
 }
 
 func TestReview_LoadCodeComments(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	review := unittest.AssertExistsAndLoadBean(t, &Review{ID: 4}).(*Review)
+	review := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 4}).(*issues_model.Review)
 	assert.NoError(t, review.LoadAttributes(db.DefaultContext))
 	assert.NoError(t, review.LoadCodeComments(db.DefaultContext))
 	assert.Len(t, review.CodeComments, 1)
@@ -51,18 +52,18 @@ func TestReview_LoadCodeComments(t *testing.T) {
 }
 
 func TestReviewType_Icon(t *testing.T) {
-	assert.Equal(t, "check", ReviewTypeApprove.Icon())
-	assert.Equal(t, "diff", ReviewTypeReject.Icon())
-	assert.Equal(t, "comment", ReviewTypeComment.Icon())
-	assert.Equal(t, "comment", ReviewTypeUnknown.Icon())
-	assert.Equal(t, "dot-fill", ReviewTypeRequest.Icon())
-	assert.Equal(t, "comment", ReviewType(6).Icon())
+	assert.Equal(t, "check", issues_model.ReviewTypeApprove.Icon())
+	assert.Equal(t, "diff", issues_model.ReviewTypeReject.Icon())
+	assert.Equal(t, "comment", issues_model.ReviewTypeComment.Icon())
+	assert.Equal(t, "comment", issues_model.ReviewTypeUnknown.Icon())
+	assert.Equal(t, "dot-fill", issues_model.ReviewTypeRequest.Icon())
+	assert.Equal(t, "comment", issues_model.ReviewType(6).Icon())
 }
 
 func TestFindReviews(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	reviews, err := FindReviews(db.DefaultContext, FindReviewOptions{
-		Type:       ReviewTypeApprove,
+	reviews, err := issues_model.FindReviews(db.DefaultContext, issues_model.FindReviewOptions{
+		Type:       issues_model.ReviewTypeApprove,
 		IssueID:    2,
 		ReviewerID: 1,
 	})
@@ -73,66 +74,66 @@ func TestFindReviews(t *testing.T) {
 
 func TestGetCurrentReview(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}).(*issues_model.Issue)
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
 
-	review, err := GetCurrentReview(db.DefaultContext, user, issue)
+	review, err := issues_model.GetCurrentReview(db.DefaultContext, user, issue)
 	assert.NoError(t, err)
 	assert.NotNil(t, review)
-	assert.Equal(t, ReviewTypePending, review.Type)
+	assert.Equal(t, issues_model.ReviewTypePending, review.Type)
 	assert.Equal(t, "Pending Review", review.Content)
 
 	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 7}).(*user_model.User)
-	review2, err := GetCurrentReview(db.DefaultContext, user2, issue)
+	review2, err := issues_model.GetCurrentReview(db.DefaultContext, user2, issue)
 	assert.Error(t, err)
-	assert.True(t, IsErrReviewNotExist(err))
+	assert.True(t, issues_model.IsErrReviewNotExist(err))
 	assert.Nil(t, review2)
 }
 
 func TestCreateReview(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}).(*issues_model.Issue)
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
 
-	review, err := CreateReview(db.DefaultContext, CreateReviewOptions{
+	review, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{
 		Content:  "New Review",
-		Type:     ReviewTypePending,
+		Type:     issues_model.ReviewTypePending,
 		Issue:    issue,
 		Reviewer: user,
 	})
 	assert.NoError(t, err)
 	assert.Equal(t, "New Review", review.Content)
-	unittest.AssertExistsAndLoadBean(t, &Review{Content: "New Review"})
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Review{Content: "New Review"})
 }
 
 func TestGetReviewersByIssueID(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 3}).(*Issue)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}).(*issues_model.Issue)
 	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
 	user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}).(*user_model.User)
 	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}).(*user_model.User)
 
-	expectedReviews := []*Review{}
+	expectedReviews := []*issues_model.Review{}
 	expectedReviews = append(expectedReviews,
-		&Review{
+		&issues_model.Review{
 			Reviewer:    user3,
-			Type:        ReviewTypeReject,
+			Type:        issues_model.ReviewTypeReject,
 			UpdatedUnix: 946684812,
 		},
-		&Review{
+		&issues_model.Review{
 			Reviewer:    user4,
-			Type:        ReviewTypeApprove,
+			Type:        issues_model.ReviewTypeApprove,
 			UpdatedUnix: 946684813,
 		},
-		&Review{
+		&issues_model.Review{
 			Reviewer:    user2,
-			Type:        ReviewTypeReject,
+			Type:        issues_model.ReviewTypeReject,
 			UpdatedUnix: 946684814,
 		})
 
-	allReviews, err := GetReviewersByIssueID(issue.ID)
+	allReviews, err := issues_model.GetReviewersByIssueID(issue.ID)
 	for _, reviewer := range allReviews {
 		assert.NoError(t, reviewer.LoadReviewer())
 	}
@@ -149,53 +150,53 @@ func TestGetReviewersByIssueID(t *testing.T) {
 func TestDismissReview(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	rejectReviewExample := unittest.AssertExistsAndLoadBean(t, &Review{ID: 9}).(*Review)
-	requestReviewExample := unittest.AssertExistsAndLoadBean(t, &Review{ID: 11}).(*Review)
-	approveReviewExample := unittest.AssertExistsAndLoadBean(t, &Review{ID: 8}).(*Review)
+	rejectReviewExample := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 9}).(*issues_model.Review)
+	requestReviewExample := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 11}).(*issues_model.Review)
+	approveReviewExample := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 8}).(*issues_model.Review)
 	assert.False(t, rejectReviewExample.Dismissed)
 	assert.False(t, requestReviewExample.Dismissed)
 	assert.False(t, approveReviewExample.Dismissed)
 
-	assert.NoError(t, DismissReview(rejectReviewExample, true))
-	rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &Review{ID: 9}).(*Review)
-	requestReviewExample = unittest.AssertExistsAndLoadBean(t, &Review{ID: 11}).(*Review)
+	assert.NoError(t, issues_model.DismissReview(rejectReviewExample, true))
+	rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 9}).(*issues_model.Review)
+	requestReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 11}).(*issues_model.Review)
 	assert.True(t, rejectReviewExample.Dismissed)
 	assert.False(t, requestReviewExample.Dismissed)
 
-	assert.NoError(t, DismissReview(requestReviewExample, true))
-	rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &Review{ID: 9}).(*Review)
-	requestReviewExample = unittest.AssertExistsAndLoadBean(t, &Review{ID: 11}).(*Review)
+	assert.NoError(t, issues_model.DismissReview(requestReviewExample, true))
+	rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 9}).(*issues_model.Review)
+	requestReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 11}).(*issues_model.Review)
 	assert.True(t, rejectReviewExample.Dismissed)
 	assert.False(t, requestReviewExample.Dismissed)
 	assert.False(t, approveReviewExample.Dismissed)
 
-	assert.NoError(t, DismissReview(requestReviewExample, true))
-	rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &Review{ID: 9}).(*Review)
-	requestReviewExample = unittest.AssertExistsAndLoadBean(t, &Review{ID: 11}).(*Review)
+	assert.NoError(t, issues_model.DismissReview(requestReviewExample, true))
+	rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 9}).(*issues_model.Review)
+	requestReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 11}).(*issues_model.Review)
 	assert.True(t, rejectReviewExample.Dismissed)
 	assert.False(t, requestReviewExample.Dismissed)
 	assert.False(t, approveReviewExample.Dismissed)
 
-	assert.NoError(t, DismissReview(requestReviewExample, false))
-	rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &Review{ID: 9}).(*Review)
-	requestReviewExample = unittest.AssertExistsAndLoadBean(t, &Review{ID: 11}).(*Review)
+	assert.NoError(t, issues_model.DismissReview(requestReviewExample, false))
+	rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 9}).(*issues_model.Review)
+	requestReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 11}).(*issues_model.Review)
 	assert.True(t, rejectReviewExample.Dismissed)
 	assert.False(t, requestReviewExample.Dismissed)
 	assert.False(t, approveReviewExample.Dismissed)
 
-	assert.NoError(t, DismissReview(requestReviewExample, false))
-	rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &Review{ID: 9}).(*Review)
-	requestReviewExample = unittest.AssertExistsAndLoadBean(t, &Review{ID: 11}).(*Review)
+	assert.NoError(t, issues_model.DismissReview(requestReviewExample, false))
+	rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 9}).(*issues_model.Review)
+	requestReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 11}).(*issues_model.Review)
 	assert.True(t, rejectReviewExample.Dismissed)
 	assert.False(t, requestReviewExample.Dismissed)
 	assert.False(t, approveReviewExample.Dismissed)
 
-	assert.NoError(t, DismissReview(rejectReviewExample, false))
+	assert.NoError(t, issues_model.DismissReview(rejectReviewExample, false))
 	assert.False(t, rejectReviewExample.Dismissed)
 	assert.False(t, requestReviewExample.Dismissed)
 	assert.False(t, approveReviewExample.Dismissed)
 
-	assert.NoError(t, DismissReview(approveReviewExample, true))
+	assert.NoError(t, issues_model.DismissReview(approveReviewExample, true))
 	assert.False(t, rejectReviewExample.Dismissed)
 	assert.False(t, requestReviewExample.Dismissed)
 	assert.True(t, approveReviewExample.Dismissed)
diff --git a/models/issue_stopwatch.go b/models/issues/stopwatch.go
similarity index 99%
rename from models/issue_stopwatch.go
rename to models/issues/stopwatch.go
index 2cb4a62bd3..e7ac1314e9 100644
--- a/models/issue_stopwatch.go
+++ b/models/issues/stopwatch.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues
 
 import (
 	"context"
@@ -215,7 +215,7 @@ func CreateIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss
 		return err
 	}
 	if exists {
-		issue, err := getIssueByID(ctx, sw.IssueID)
+		issue, err := GetIssueByID(ctx, sw.IssueID)
 		if err != nil {
 			return err
 		}
diff --git a/models/issues/stopwatch_test.go b/models/issues/stopwatch_test.go
new file mode 100644
index 0000000000..c0573964d5
--- /dev/null
+++ b/models/issues/stopwatch_test.go
@@ -0,0 +1,79 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package issues_test
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/timeutil"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCancelStopwatch(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	user1, err := user_model.GetUserByID(1)
+	assert.NoError(t, err)
+
+	issue1, err := issues_model.GetIssueByID(db.DefaultContext, 1)
+	assert.NoError(t, err)
+	issue2, err := issues_model.GetIssueByID(db.DefaultContext, 2)
+	assert.NoError(t, err)
+
+	err = issues_model.CancelStopwatch(user1, issue1)
+	assert.NoError(t, err)
+	unittest.AssertNotExistsBean(t, &issues_model.Stopwatch{UserID: user1.ID, IssueID: issue1.ID})
+
+	_ = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{Type: issues_model.CommentTypeCancelTracking, PosterID: user1.ID, IssueID: issue1.ID})
+
+	assert.Nil(t, issues_model.CancelStopwatch(user1, issue2))
+}
+
+func TestStopwatchExists(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	assert.True(t, issues_model.StopwatchExists(1, 1))
+	assert.False(t, issues_model.StopwatchExists(1, 2))
+}
+
+func TestHasUserStopwatch(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	exists, sw, err := issues_model.HasUserStopwatch(db.DefaultContext, 1)
+	assert.NoError(t, err)
+	assert.True(t, exists)
+	assert.Equal(t, int64(1), sw.ID)
+
+	exists, _, err = issues_model.HasUserStopwatch(db.DefaultContext, 3)
+	assert.NoError(t, err)
+	assert.False(t, exists)
+}
+
+func TestCreateOrStopIssueStopwatch(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	user2, err := user_model.GetUserByID(2)
+	assert.NoError(t, err)
+	user3, err := user_model.GetUserByID(3)
+	assert.NoError(t, err)
+
+	issue1, err := issues_model.GetIssueByID(db.DefaultContext, 1)
+	assert.NoError(t, err)
+	issue2, err := issues_model.GetIssueByID(db.DefaultContext, 2)
+	assert.NoError(t, err)
+
+	assert.NoError(t, issues_model.CreateOrStopIssueStopwatch(user3, issue1))
+	sw := unittest.AssertExistsAndLoadBean(t, &issues_model.Stopwatch{UserID: 3, IssueID: 1}).(*issues_model.Stopwatch)
+	assert.LessOrEqual(t, sw.CreatedUnix, timeutil.TimeStampNow())
+
+	assert.NoError(t, issues_model.CreateOrStopIssueStopwatch(user2, issue2))
+	unittest.AssertNotExistsBean(t, &issues_model.Stopwatch{UserID: 2, IssueID: 2})
+	unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{UserID: 2, IssueID: 2})
+}
diff --git a/models/issue_tracked_time.go b/models/issues/tracked_time.go
similarity index 99%
rename from models/issue_tracked_time.go
rename to models/issues/tracked_time.go
index 30b3905bbc..54179bd3ab 100644
--- a/models/issue_tracked_time.go
+++ b/models/issues/tracked_time.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues
 
 import (
 	"context"
@@ -48,7 +48,7 @@ func (t *TrackedTime) LoadAttributes() (err error) {
 
 func (t *TrackedTime) loadAttributes(ctx context.Context) (err error) {
 	if t.Issue == nil {
-		t.Issue, err = getIssueByID(ctx, t.IssueID)
+		t.Issue, err = GetIssueByID(ctx, t.IssueID)
 		if err != nil {
 			return
 		}
diff --git a/models/issue_tracked_time_test.go b/models/issues/tracked_time_test.go
similarity index 56%
rename from models/issue_tracked_time_test.go
rename to models/issues/tracked_time_test.go
index a628329712..787ba9b701 100644
--- a/models/issue_tracked_time_test.go
+++ b/models/issues/tracked_time_test.go
@@ -2,13 +2,14 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package issues_test
 
 import (
 	"testing"
 	"time"
 
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 
@@ -21,20 +22,20 @@ func TestAddTime(t *testing.T) {
 	user3, err := user_model.GetUserByID(3)
 	assert.NoError(t, err)
 
-	issue1, err := GetIssueByID(1)
+	issue1, err := issues_model.GetIssueByID(db.DefaultContext, 1)
 	assert.NoError(t, err)
 
 	// 3661 = 1h 1min 1s
-	trackedTime, err := AddTime(user3, issue1, 3661, time.Now())
+	trackedTime, err := issues_model.AddTime(user3, issue1, 3661, time.Now())
 	assert.NoError(t, err)
 	assert.Equal(t, int64(3), trackedTime.UserID)
 	assert.Equal(t, int64(1), trackedTime.IssueID)
 	assert.Equal(t, int64(3661), trackedTime.Time)
 
-	tt := unittest.AssertExistsAndLoadBean(t, &TrackedTime{UserID: 3, IssueID: 1}).(*TrackedTime)
+	tt := unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{UserID: 3, IssueID: 1}).(*issues_model.TrackedTime)
 	assert.Equal(t, int64(3661), tt.Time)
 
-	comment := unittest.AssertExistsAndLoadBean(t, &Comment{Type: CommentTypeAddTimeManual, PosterID: 3, IssueID: 1}).(*Comment)
+	comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{Type: issues_model.CommentTypeAddTimeManual, PosterID: 3, IssueID: 1}).(*issues_model.Comment)
 	assert.Equal(t, comment.Content, "1 hour 1 minute")
 }
 
@@ -42,39 +43,39 @@ func TestGetTrackedTimes(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	// by Issue
-	times, err := GetTrackedTimes(db.DefaultContext, &FindTrackedTimesOptions{IssueID: 1})
+	times, err := issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{IssueID: 1})
 	assert.NoError(t, err)
 	assert.Len(t, times, 1)
 	assert.Equal(t, int64(400), times[0].Time)
 
-	times, err = GetTrackedTimes(db.DefaultContext, &FindTrackedTimesOptions{IssueID: -1})
+	times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{IssueID: -1})
 	assert.NoError(t, err)
 	assert.Len(t, times, 0)
 
 	// by User
-	times, err = GetTrackedTimes(db.DefaultContext, &FindTrackedTimesOptions{UserID: 1})
+	times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{UserID: 1})
 	assert.NoError(t, err)
 	assert.Len(t, times, 3)
 	assert.Equal(t, int64(400), times[0].Time)
 
-	times, err = GetTrackedTimes(db.DefaultContext, &FindTrackedTimesOptions{UserID: 3})
+	times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{UserID: 3})
 	assert.NoError(t, err)
 	assert.Len(t, times, 0)
 
 	// by Repo
-	times, err = GetTrackedTimes(db.DefaultContext, &FindTrackedTimesOptions{RepositoryID: 2})
+	times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{RepositoryID: 2})
 	assert.NoError(t, err)
 	assert.Len(t, times, 3)
 	assert.Equal(t, int64(1), times[0].Time)
-	issue, err := GetIssueByID(times[0].IssueID)
+	issue, err := issues_model.GetIssueByID(db.DefaultContext, times[0].IssueID)
 	assert.NoError(t, err)
 	assert.Equal(t, issue.RepoID, int64(2))
 
-	times, err = GetTrackedTimes(db.DefaultContext, &FindTrackedTimesOptions{RepositoryID: 1})
+	times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{RepositoryID: 1})
 	assert.NoError(t, err)
 	assert.Len(t, times, 5)
 
-	times, err = GetTrackedTimes(db.DefaultContext, &FindTrackedTimesOptions{RepositoryID: 10})
+	times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{RepositoryID: 10})
 	assert.NoError(t, err)
 	assert.Len(t, times, 0)
 }
@@ -82,7 +83,7 @@ func TestGetTrackedTimes(t *testing.T) {
 func TestTotalTimes(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	total, err := TotalTimes(&FindTrackedTimesOptions{IssueID: 1})
+	total, err := issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: 1})
 	assert.NoError(t, err)
 	assert.Len(t, total, 1)
 	for user, time := range total {
@@ -90,7 +91,7 @@ func TestTotalTimes(t *testing.T) {
 		assert.Equal(t, "6 minutes 40 seconds", time)
 	}
 
-	total, err = TotalTimes(&FindTrackedTimesOptions{IssueID: 2})
+	total, err = issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: 2})
 	assert.NoError(t, err)
 	assert.Len(t, total, 2)
 	for user, time := range total {
@@ -103,7 +104,7 @@ func TestTotalTimes(t *testing.T) {
 		}
 	}
 
-	total, err = TotalTimes(&FindTrackedTimesOptions{IssueID: 5})
+	total, err = issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: 5})
 	assert.NoError(t, err)
 	assert.Len(t, total, 1)
 	for user, time := range total {
@@ -111,7 +112,7 @@ func TestTotalTimes(t *testing.T) {
 		assert.Equal(t, "1 second", time)
 	}
 
-	total, err = TotalTimes(&FindTrackedTimesOptions{IssueID: 4})
+	total, err = issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: 4})
 	assert.NoError(t, err)
 	assert.Len(t, total, 2)
 }
diff --git a/models/main_test.go b/models/main_test.go
index 96231e4704..bb2fedc15a 100644
--- a/models/main_test.go
+++ b/models/main_test.go
@@ -7,7 +7,6 @@ package models
 import (
 	"testing"
 
-	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
@@ -28,10 +27,6 @@ func TestFixturesAreConsistent(t *testing.T) {
 	unittest.CheckConsistencyFor(t,
 		&user_model.User{},
 		&repo_model.Repository{},
-		&Issue{},
-		&PullRequest{},
-		&issues_model.Milestone{},
-		&Label{},
 		&organization.Team{},
 		&Action{})
 }
diff --git a/models/migrate.go b/models/migrate.go
index 7b12bc9c93..0af3891cb8 100644
--- a/models/migrate.go
+++ b/models/migrate.go
@@ -10,8 +10,6 @@ import (
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/structs"
-
-	"xorm.io/builder"
 )
 
 // InsertMilestones creates milestones of repository.
@@ -41,7 +39,7 @@ func InsertMilestones(ms ...*issues_model.Milestone) (err error) {
 }
 
 // InsertIssues insert issues to database
-func InsertIssues(issues ...*Issue) error {
+func InsertIssues(issues ...*issues_model.Issue) error {
 	ctx, committer, err := db.TxContext()
 	if err != nil {
 		return err
@@ -56,14 +54,14 @@ func InsertIssues(issues ...*Issue) error {
 	return committer.Commit()
 }
 
-func insertIssue(ctx context.Context, issue *Issue) error {
+func insertIssue(ctx context.Context, issue *issues_model.Issue) error {
 	sess := db.GetEngine(ctx)
 	if _, err := sess.NoAutoTime().Insert(issue); err != nil {
 		return err
 	}
-	issueLabels := make([]IssueLabel, 0, len(issue.Labels))
+	issueLabels := make([]issues_model.IssueLabel, 0, len(issue.Labels))
 	for _, label := range issue.Labels {
-		issueLabels = append(issueLabels, IssueLabel{
+		issueLabels = append(issueLabels, issues_model.IssueLabel{
 			IssueID: issue.ID,
 			LabelID: label.ID,
 		})
@@ -95,7 +93,7 @@ func insertIssue(ctx context.Context, issue *Issue) error {
 }
 
 // InsertIssueComments inserts many comments of issues.
-func InsertIssueComments(comments []*Comment) error {
+func InsertIssueComments(comments []*issues_model.Comment) error {
 	if len(comments) == 0 {
 		return nil
 	}
@@ -127,7 +125,8 @@ func InsertIssueComments(comments []*Comment) error {
 	}
 
 	for issueID := range issueIDs {
-		if _, err := db.Exec(ctx, "UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ? AND `type`=?) WHERE id = ?", issueID, CommentTypeComment, issueID); err != nil {
+		if _, err := db.Exec(ctx, "UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ? AND `type`=?) WHERE id = ?",
+			issueID, issues_model.CommentTypeComment, issueID); err != nil {
 			return err
 		}
 	}
@@ -135,7 +134,7 @@ func InsertIssueComments(comments []*Comment) error {
 }
 
 // InsertPullRequests inserted pull requests
-func InsertPullRequests(prs ...*PullRequest) error {
+func InsertPullRequests(prs ...*issues_model.PullRequest) error {
 	ctx, committer, err := db.TxContext()
 	if err != nil {
 		return err
@@ -182,37 +181,13 @@ func InsertReleases(rels ...*Release) error {
 	return committer.Commit()
 }
 
-func migratedIssueCond(tp structs.GitServiceType) builder.Cond {
-	return builder.In("issue_id",
-		builder.Select("issue.id").
-			From("issue").
-			InnerJoin("repository", "issue.repo_id = repository.id").
-			Where(builder.Eq{
-				"repository.original_service_type": tp,
-			}),
-	)
-}
-
-// UpdateReviewsMigrationsByType updates reviews' migrations information via given git service type and original id and poster id
-func UpdateReviewsMigrationsByType(tp structs.GitServiceType, originalAuthorID string, posterID int64) error {
-	_, err := db.GetEngine(db.DefaultContext).Table("review").
-		Where("original_author_id = ?", originalAuthorID).
-		And(migratedIssueCond(tp)).
-		Update(map[string]interface{}{
-			"reviewer_id":        posterID,
-			"original_author":    "",
-			"original_author_id": 0,
-		})
-	return err
-}
-
 // UpdateMigrationsByType updates all migrated repositories' posterid from gitServiceType to replace originalAuthorID to posterID
 func UpdateMigrationsByType(tp structs.GitServiceType, externalUserID string, userID int64) error {
-	if err := UpdateIssuesMigrationsByType(tp, externalUserID, userID); err != nil {
+	if err := issues_model.UpdateIssuesMigrationsByType(tp, externalUserID, userID); err != nil {
 		return err
 	}
 
-	if err := UpdateCommentsMigrationsByType(tp, externalUserID, userID); err != nil {
+	if err := issues_model.UpdateCommentsMigrationsByType(tp, externalUserID, userID); err != nil {
 		return err
 	}
 
@@ -220,8 +195,8 @@ func UpdateMigrationsByType(tp structs.GitServiceType, externalUserID string, us
 		return err
 	}
 
-	if err := UpdateReactionsMigrationsByType(tp, externalUserID, userID); err != nil {
+	if err := issues_model.UpdateReactionsMigrationsByType(tp, externalUserID, userID); err != nil {
 		return err
 	}
-	return UpdateReviewsMigrationsByType(tp, externalUserID, userID)
+	return issues_model.UpdateReviewsMigrationsByType(tp, externalUserID, userID)
 }
diff --git a/models/migrate_test.go b/models/migrate_test.go
index ce28b3ca7c..b6525278ec 100644
--- a/models/migrate_test.go
+++ b/models/migrate_test.go
@@ -41,7 +41,7 @@ func assertCreateIssues(t *testing.T, isPull bool) {
 	reponame := "repo1"
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}).(*repo_model.Repository)
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
-	label := unittest.AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label)
+	label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label)
 	milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone)
 	assert.EqualValues(t, milestone.ID, 1)
 	reaction := &issues_model.Reaction{
@@ -51,7 +51,7 @@ func assertCreateIssues(t *testing.T, isPull bool) {
 
 	foreignIndex := int64(12345)
 	title := "issuetitle1"
-	is := &Issue{
+	is := &issues_model.Issue{
 		RepoID:      repo.ID,
 		MilestoneID: milestone.ID,
 		Repo:        repo,
@@ -61,7 +61,7 @@ func assertCreateIssues(t *testing.T, isPull bool) {
 		PosterID:    owner.ID,
 		Poster:      owner,
 		IsClosed:    true,
-		Labels:      []*Label{label},
+		Labels:      []*issues_model.Label{label},
 		Reactions:   []*issues_model.Reaction{reaction},
 		ForeignReference: &foreignreference.ForeignReference{
 			ForeignIndex: strconv.FormatInt(foreignIndex, 10),
@@ -72,9 +72,9 @@ func assertCreateIssues(t *testing.T, isPull bool) {
 	err := InsertIssues(is)
 	assert.NoError(t, err)
 
-	i := unittest.AssertExistsAndLoadBean(t, &Issue{Title: title}).(*Issue)
+	i := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: title}).(*issues_model.Issue)
 	assert.Nil(t, i.ForeignReference)
-	err = i.LoadAttributes()
+	err = i.LoadAttributes(db.DefaultContext)
 	assert.NoError(t, err)
 	assert.EqualValues(t, strconv.FormatInt(foreignIndex, 10), i.ForeignReference.ForeignIndex)
 	unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: owner.ID, IssueID: i.ID})
@@ -90,7 +90,7 @@ func TestMigrate_CreateIssuesIsPullTrue(t *testing.T) {
 
 func TestMigrate_InsertIssueComments(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue)
 	_ = issue.LoadRepo(db.DefaultContext)
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID}).(*user_model.User)
 	reaction := &issues_model.Reaction{
@@ -98,7 +98,7 @@ func TestMigrate_InsertIssueComments(t *testing.T) {
 		UserID: owner.ID,
 	}
 
-	comment := &Comment{
+	comment := &issues_model.Comment{
 		PosterID:  owner.ID,
 		Poster:    owner,
 		IssueID:   issue.ID,
@@ -106,13 +106,13 @@ func TestMigrate_InsertIssueComments(t *testing.T) {
 		Reactions: []*issues_model.Reaction{reaction},
 	}
 
-	err := InsertIssueComments([]*Comment{comment})
+	err := InsertIssueComments([]*issues_model.Comment{comment})
 	assert.NoError(t, err)
 
-	issueModified := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
+	issueModified := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue)
 	assert.EqualValues(t, issue.NumComments+1, issueModified.NumComments)
 
-	unittest.CheckConsistencyFor(t, &Issue{})
+	unittest.CheckConsistencyFor(t, &issues_model.Issue{})
 }
 
 func TestMigrate_InsertPullRequests(t *testing.T) {
@@ -121,7 +121,7 @@ func TestMigrate_InsertPullRequests(t *testing.T) {
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}).(*repo_model.Repository)
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
 
-	i := &Issue{
+	i := &issues_model.Issue{
 		RepoID:   repo.ID,
 		Repo:     repo,
 		Title:    "title1",
@@ -131,16 +131,16 @@ func TestMigrate_InsertPullRequests(t *testing.T) {
 		Poster:   owner,
 	}
 
-	p := &PullRequest{
+	p := &issues_model.PullRequest{
 		Issue: i,
 	}
 
 	err := InsertPullRequests(p)
 	assert.NoError(t, err)
 
-	_ = unittest.AssertExistsAndLoadBean(t, &PullRequest{IssueID: i.ID}).(*PullRequest)
+	_ = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{IssueID: i.ID}).(*issues_model.PullRequest)
 
-	unittest.CheckConsistencyFor(t, &Issue{}, &PullRequest{})
+	unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.PullRequest{})
 }
 
 func TestMigrate_InsertReleases(t *testing.T) {
diff --git a/models/migrations/v111.go b/models/migrations/v111.go
index 02624da66a..65fe7c5332 100644
--- a/models/migrations/v111.go
+++ b/models/migrations/v111.go
@@ -131,7 +131,7 @@ func addBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error {
 		Authorize int
 	}
 
-	// getUserRepoPermission static function based on models.IsOfficialReviewer at 5d78792385
+	// getUserRepoPermission static function based on issues_model.IsOfficialReviewer at 5d78792385
 	getUserRepoPermission := func(sess *xorm.Session, repo *Repository, user *User) (Permission, error) {
 		var perm Permission
 
diff --git a/models/notification.go b/models/notification.go
index ac5abc6f92..3f0e374b83 100644
--- a/models/notification.go
+++ b/models/notification.go
@@ -11,6 +11,7 @@ import (
 	"strconv"
 
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
@@ -66,9 +67,9 @@ type Notification struct {
 
 	UpdatedBy int64 `xorm:"INDEX NOT NULL"`
 
-	Issue      *Issue                 `xorm:"-"`
+	Issue      *issues_model.Issue    `xorm:"-"`
 	Repository *repo_model.Repository `xorm:"-"`
-	Comment    *Comment               `xorm:"-"`
+	Comment    *issues_model.Comment  `xorm:"-"`
 	User       *user_model.User       `xorm:"-"`
 
 	CreatedUnix timeutil.TimeStamp `xorm:"created INDEX NOT NULL"`
@@ -204,7 +205,7 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n
 		return err
 	}
 
-	issue, err := getIssueByID(ctx, issueID)
+	issue, err := issues_model.GetIssueByID(ctx, issueID)
 	if err != nil {
 		return err
 	}
@@ -214,14 +215,14 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n
 		toNotify[receiverID] = struct{}{}
 	} else {
 		toNotify = make(map[int64]struct{}, 32)
-		issueWatches, err := GetIssueWatchersIDs(ctx, issueID, true)
+		issueWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, true)
 		if err != nil {
 			return err
 		}
 		for _, id := range issueWatches {
 			toNotify[id] = struct{}{}
 		}
-		if !(issue.IsPull && HasWorkInProgressPrefix(issue.Title)) {
+		if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) {
 			repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID)
 			if err != nil {
 				return err
@@ -230,7 +231,7 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n
 				toNotify[id] = struct{}{}
 			}
 		}
-		issueParticipants, err := issue.getParticipantIDsByIssue(ctx)
+		issueParticipants, err := issue.GetParticipantIDsByIssue(ctx)
 		if err != nil {
 			return err
 		}
@@ -241,7 +242,7 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n
 		// dont notify user who cause notification
 		delete(toNotify, notificationAuthorID)
 		// explicit unwatch on issue
-		issueUnWatches, err := GetIssueWatchersIDs(ctx, issueID, false)
+		issueUnWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, false)
 		if err != nil {
 			return err
 		}
@@ -303,7 +304,7 @@ func notificationExists(notifications []*Notification, issueID, userID int64) bo
 	return false
 }
 
-func createIssueNotification(ctx context.Context, userID int64, issue *Issue, commentID, updatedByID int64) error {
+func createIssueNotification(ctx context.Context, userID int64, issue *issues_model.Issue, commentID, updatedByID int64) error {
 	notification := &Notification{
 		UserID:    userID,
 		RepoID:    issue.RepoID,
@@ -415,21 +416,21 @@ func (n *Notification) loadRepo(ctx context.Context) (err error) {
 
 func (n *Notification) loadIssue(ctx context.Context) (err error) {
 	if n.Issue == nil && n.IssueID != 0 {
-		n.Issue, err = getIssueByID(ctx, n.IssueID)
+		n.Issue, err = issues_model.GetIssueByID(ctx, n.IssueID)
 		if err != nil {
 			return fmt.Errorf("getIssueByID [%d]: %v", n.IssueID, err)
 		}
-		return n.Issue.loadAttributes(ctx)
+		return n.Issue.LoadAttributes(ctx)
 	}
 	return nil
 }
 
 func (n *Notification) loadComment(ctx context.Context) (err error) {
 	if n.Comment == nil && n.CommentID != 0 {
-		n.Comment, err = GetCommentByID(ctx, n.CommentID)
+		n.Comment, err = issues_model.GetCommentByID(ctx, n.CommentID)
 		if err != nil {
-			if IsErrCommentNotExist(err) {
-				return ErrCommentNotExist{
+			if issues_model.IsErrCommentNotExist(err) {
+				return issues_model.ErrCommentNotExist{
 					ID:      n.CommentID,
 					IssueID: n.IssueID,
 				}
@@ -456,7 +457,7 @@ func (n *Notification) GetRepo() (*repo_model.Repository, error) {
 }
 
 // GetIssue returns the issue of the notification
-func (n *Notification) GetIssue() (*Issue, error) {
+func (n *Notification) GetIssue() (*issues_model.Issue, error) {
 	return n.Issue, n.loadIssue(db.DefaultContext)
 }
 
@@ -489,7 +490,7 @@ func (nl NotificationList) LoadAttributes() error {
 	var err error
 	for i := 0; i < len(nl); i++ {
 		err = nl[i].LoadAttributes()
-		if err != nil && !IsErrCommentNotExist(err) {
+		if err != nil && !issues_model.IsErrCommentNotExist(err) {
 			return err
 		}
 	}
@@ -519,7 +520,7 @@ func (nl NotificationList) LoadRepos() (repo_model.RepositoryList, []int, error)
 	repos := make(map[int64]*repo_model.Repository, len(repoIDs))
 	left := len(repoIDs)
 	for left > 0 {
-		limit := defaultMaxInSize
+		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
@@ -592,22 +593,22 @@ func (nl NotificationList) LoadIssues() ([]int, error) {
 	}
 
 	issueIDs := nl.getPendingIssueIDs()
-	issues := make(map[int64]*Issue, len(issueIDs))
+	issues := make(map[int64]*issues_model.Issue, len(issueIDs))
 	left := len(issueIDs)
 	for left > 0 {
-		limit := defaultMaxInSize
+		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
 		rows, err := db.GetEngine(db.DefaultContext).
 			In("id", issueIDs[:limit]).
-			Rows(new(Issue))
+			Rows(new(issues_model.Issue))
 		if err != nil {
 			return nil, err
 		}
 
 		for rows.Next() {
-			var issue Issue
+			var issue issues_model.Issue
 			err = rows.Scan(&issue)
 			if err != nil {
 				rows.Close()
@@ -678,22 +679,22 @@ func (nl NotificationList) LoadComments() ([]int, error) {
 	}
 
 	commentIDs := nl.getPendingCommentIDs()
-	comments := make(map[int64]*Comment, len(commentIDs))
+	comments := make(map[int64]*issues_model.Comment, len(commentIDs))
 	left := len(commentIDs)
 	for left > 0 {
-		limit := defaultMaxInSize
+		limit := db.DefaultMaxInSize
 		if left < limit {
 			limit = left
 		}
 		rows, err := db.GetEngine(db.DefaultContext).
 			In("id", commentIDs[:limit]).
-			Rows(new(Comment))
+			Rows(new(issues_model.Comment))
 		if err != nil {
 			return nil, err
 		}
 
 		for rows.Next() {
-			var comment Comment
+			var comment issues_model.Comment
 			err = rows.Scan(&comment)
 			if err != nil {
 				rows.Close()
@@ -747,6 +748,15 @@ func GetUIDsAndNotificationCounts(since, until timeutil.TimeStamp) ([]UserIDCoun
 	return res, db.GetEngine(db.DefaultContext).SQL(sql, since, until, NotificationStatusUnread).Find(&res)
 }
 
+// SetIssueReadBy sets issue to be read by given user.
+func SetIssueReadBy(ctx context.Context, issueID, userID int64) error {
+	if err := issues_model.UpdateIssueUserByRead(userID, issueID); err != nil {
+		return err
+	}
+
+	return setIssueNotificationStatusReadIfUnread(ctx, userID, issueID)
+}
+
 func setIssueNotificationStatusReadIfUnread(ctx context.Context, userID, issueID int64) error {
 	notification, err := getIssueNotification(ctx, userID, issueID)
 	// ignore if not exists
diff --git a/models/notification_test.go b/models/notification_test.go
index 15c29389c8..16ff02d6c0 100644
--- a/models/notification_test.go
+++ b/models/notification_test.go
@@ -8,6 +8,7 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 
@@ -16,14 +17,14 @@ import (
 
 func TestCreateOrUpdateIssueNotifications(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue)
 
 	assert.NoError(t, CreateOrUpdateIssueNotifications(issue.ID, 0, 2, 0))
 
 	// User 9 is inactive, thus notifications for user 1 and 4 are created
 	notf := unittest.AssertExistsAndLoadBean(t, &Notification{UserID: 1, IssueID: issue.ID}).(*Notification)
 	assert.Equal(t, NotificationStatusUnread, notf.Status)
-	unittest.CheckConsistencyFor(t, &Issue{ID: issue.ID})
+	unittest.CheckConsistencyFor(t, &issues_model.Issue{ID: issue.ID})
 
 	notf = unittest.AssertExistsAndLoadBean(t, &Notification{UserID: 4, IssueID: issue.ID}).(*Notification)
 	assert.Equal(t, NotificationStatusUnread, notf.Status)
diff --git a/models/org_team.go b/models/org_team.go
index 7ff3095273..5d29e33337 100644
--- a/models/org_team.go
+++ b/models/org_team.go
@@ -13,6 +13,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -153,7 +154,7 @@ func removeAllRepositories(ctx context.Context, t *organization.Team) (err error
 			}
 
 			// Remove all IssueWatches a user has subscribed to in the repositories
-			if err = removeIssueWatchersByRepoID(ctx, user.ID, repo.ID); err != nil {
+			if err = issues_model.RemoveIssueWatchersByRepoID(ctx, user.ID, repo.ID); err != nil {
 				return err
 			}
 		}
@@ -216,7 +217,7 @@ func removeRepository(ctx context.Context, t *organization.Team, repo *repo_mode
 		}
 
 		// Remove all IssueWatches a user has subscribed to in the repositories
-		if err := removeIssueWatchersByRepoID(ctx, teamUser.UID, repo.ID); err != nil {
+		if err := issues_model.RemoveIssueWatchersByRepoID(ctx, teamUser.UID, repo.ID); err != nil {
 			return err
 		}
 	}
diff --git a/models/repo.go b/models/repo.go
index a8aa18381d..e9d83f5f32 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -281,7 +281,7 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error {
 		&access_model.Access{RepoID: repo.ID},
 		&Action{RepoID: repo.ID},
 		&repo_model.Collaboration{RepoID: repoID},
-		&Comment{RefRepoID: repoID},
+		&issues_model.Comment{RefRepoID: repoID},
 		&git_model.CommitStatus{RepoID: repoID},
 		&git_model.DeletedBranch{RepoID: repoID},
 		&webhook.HookTask{RepoID: repoID},
@@ -306,18 +306,18 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error {
 	}
 
 	// Delete Labels and related objects
-	if err := deleteLabelsByRepoID(ctx, repoID); err != nil {
+	if err := issues_model.DeleteLabelsByRepoID(ctx, repoID); err != nil {
 		return err
 	}
 
 	// Delete Pulls and related objects
-	if err := deletePullsByBaseRepoID(ctx, repoID); err != nil {
+	if err := issues_model.DeletePullsByBaseRepoID(ctx, repoID); err != nil {
 		return err
 	}
 
 	// Delete Issues and related objects
 	var attachmentPaths []string
-	if attachmentPaths, err = deleteIssuesByRepoID(ctx, repoID); err != nil {
+	if attachmentPaths, err = issues_model.DeleteIssuesByRepoID(ctx, repoID); err != nil {
 		return err
 	}
 
@@ -576,16 +576,11 @@ func repoStatsCorrectNum(ctx context.Context, id int64, isPull bool, field strin
 }
 
 func repoStatsCorrectNumClosedIssues(ctx context.Context, id int64) error {
-	return repoStatsCorrectNumClosed(ctx, id, false, "num_closed_issues")
+	return repo_model.StatsCorrectNumClosed(ctx, id, false, "num_closed_issues")
 }
 
 func repoStatsCorrectNumClosedPulls(ctx context.Context, id int64) error {
-	return repoStatsCorrectNumClosed(ctx, id, true, "num_closed_pulls")
-}
-
-func repoStatsCorrectNumClosed(ctx context.Context, id int64, isPull bool, field string) error {
-	_, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET "+field+"=(SELECT COUNT(*) FROM `issue` WHERE repo_id=? AND is_closed=? AND is_pull=?) WHERE id=?", id, true, isPull, id)
-	return err
+	return repo_model.StatsCorrectNumClosed(ctx, id, true, "num_closed_pulls")
 }
 
 func statsQuery(args ...interface{}) func(context.Context) ([]map[string][]byte, error) {
@@ -687,12 +682,11 @@ func CheckRepoStats(ctx context.Context) error {
 				continue
 			}
 
-			rawResult, err := e.Query("SELECT COUNT(*) FROM `repository` WHERE fork_id=?", repo.ID)
+			_, err = e.SQL("SELECT COUNT(*) FROM `repository` WHERE fork_id=?", repo.ID).Get(&repo.NumForks)
 			if err != nil {
 				log.Error("Select count of forks[%d]: %v", repo.ID, err)
 				continue
 			}
-			repo.NumForks = int(parseCountResult(rawResult))
 
 			if _, err = e.ID(repo.ID).Cols("num_forks").Update(repo); err != nil {
 				log.Error("UpdateRepository[%d]: %v", id, err)
diff --git a/models/repo/repo.go b/models/repo/repo.go
index 57d85435eb..f6097d2d6a 100644
--- a/models/repo/repo.go
+++ b/models/repo/repo.go
@@ -25,6 +25,22 @@ import (
 	"code.gitea.io/gitea/modules/util"
 )
 
+// ErrUserDoesNotHaveAccessToRepo represets an error where the user doesn't has access to a given repo.
+type ErrUserDoesNotHaveAccessToRepo struct {
+	UserID   int64
+	RepoName string
+}
+
+// IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrRepoFileAlreadyExists.
+func IsErrUserDoesNotHaveAccessToRepo(err error) bool {
+	_, ok := err.(ErrUserDoesNotHaveAccessToRepo)
+	return ok
+}
+
+func (err ErrUserDoesNotHaveAccessToRepo) Error() string {
+	return fmt.Sprintf("user doesn't have access to repo [user_id: %d, repo_name: %s]", err.UserID, err.RepoName)
+}
+
 var (
 	reservedRepoNames    = []string{".", "..", "-"}
 	reservedRepoPatterns = []string{"*.git", "*.wiki", "*.rss", "*.atom"}
@@ -743,3 +759,34 @@ func CountRepositories(ctx context.Context, opts CountRepositoryOptions) (int64,
 	}
 	return count, nil
 }
+
+// StatsCorrectNumClosed update repository's issue related numbers
+func StatsCorrectNumClosed(ctx context.Context, id int64, isPull bool, field string) error {
+	_, err := db.Exec(ctx, "UPDATE `repository` SET "+field+"=(SELECT COUNT(*) FROM `issue` WHERE repo_id=? AND is_closed=? AND is_pull=?) WHERE id=?", id, true, isPull, id)
+	return err
+}
+
+// UpdateRepoIssueNumbers update repository issue numbers
+func UpdateRepoIssueNumbers(ctx context.Context, repoID int64, isPull, isClosed bool) error {
+	e := db.GetEngine(ctx)
+	if isPull {
+		if _, err := e.ID(repoID).Decr("num_pulls").Update(new(Repository)); err != nil {
+			return err
+		}
+		if isClosed {
+			if _, err := e.ID(repoID).Decr("num_closed_pulls").Update(new(Repository)); err != nil {
+				return err
+			}
+		}
+	} else {
+		if _, err := e.ID(repoID).Decr("num_issues").Update(new(Repository)); err != nil {
+			return err
+		}
+		if isClosed {
+			if _, err := e.ID(repoID).Decr("num_closed_issues").Update(new(Repository)); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
diff --git a/models/repo_activity.go b/models/repo_activity.go
index 06710ff1ac..6a3636ab07 100644
--- a/models/repo_activity.go
+++ b/models/repo_activity.go
@@ -11,6 +11,7 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
@@ -29,15 +30,15 @@ type ActivityAuthorData struct {
 
 // ActivityStats represets issue and pull request information.
 type ActivityStats struct {
-	OpenedPRs                   PullRequestList
+	OpenedPRs                   issues_model.PullRequestList
 	OpenedPRAuthorCount         int64
-	MergedPRs                   PullRequestList
+	MergedPRs                   issues_model.PullRequestList
 	MergedPRAuthorCount         int64
-	OpenedIssues                IssueList
+	OpenedIssues                issues_model.IssueList
 	OpenedIssueAuthorCount      int64
-	ClosedIssues                IssueList
+	ClosedIssues                issues_model.IssueList
 	ClosedIssueAuthorCount      int64
-	UnresolvedIssues            IssueList
+	UnresolvedIssues            issues_model.IssueList
 	PublishedReleases           []*Release
 	PublishedReleaseAuthorCount int64
 	Code                        *git.CodeActivityStats
@@ -212,7 +213,7 @@ func (stats *ActivityStats) FillPullRequests(repoID int64, fromTime time.Time) e
 	// Merged pull requests
 	sess := pullRequestsForActivityStatement(repoID, fromTime, true)
 	sess.OrderBy("pull_request.merged_unix DESC")
-	stats.MergedPRs = make(PullRequestList, 0)
+	stats.MergedPRs = make(issues_model.PullRequestList, 0)
 	if err = sess.Find(&stats.MergedPRs); err != nil {
 		return err
 	}
@@ -230,7 +231,7 @@ func (stats *ActivityStats) FillPullRequests(repoID int64, fromTime time.Time) e
 	// Opened pull requests
 	sess = pullRequestsForActivityStatement(repoID, fromTime, false)
 	sess.OrderBy("issue.created_unix ASC")
-	stats.OpenedPRs = make(PullRequestList, 0)
+	stats.OpenedPRs = make(issues_model.PullRequestList, 0)
 	if err = sess.Find(&stats.OpenedPRs); err != nil {
 		return err
 	}
@@ -271,7 +272,7 @@ func (stats *ActivityStats) FillIssues(repoID int64, fromTime time.Time) error {
 	// Closed issues
 	sess := issuesForActivityStatement(repoID, fromTime, true, false)
 	sess.OrderBy("issue.closed_unix DESC")
-	stats.ClosedIssues = make(IssueList, 0)
+	stats.ClosedIssues = make(issues_model.IssueList, 0)
 	if err = sess.Find(&stats.ClosedIssues); err != nil {
 		return err
 	}
@@ -286,7 +287,7 @@ func (stats *ActivityStats) FillIssues(repoID int64, fromTime time.Time) error {
 	// New issues
 	sess = issuesForActivityStatement(repoID, fromTime, false, false)
 	sess.OrderBy("issue.created_unix ASC")
-	stats.OpenedIssues = make(IssueList, 0)
+	stats.OpenedIssues = make(issues_model.IssueList, 0)
 	if err = sess.Find(&stats.OpenedIssues); err != nil {
 		return err
 	}
@@ -312,7 +313,7 @@ func (stats *ActivityStats) FillUnresolvedIssues(repoID int64, fromTime time.Tim
 		sess.And("issue.is_pull = ?", prs)
 	}
 	sess.OrderBy("issue.updated_unix DESC")
-	stats.UnresolvedIssues = make(IssueList, 0)
+	stats.UnresolvedIssues = make(issues_model.IssueList, 0)
 	return sess.Find(&stats.UnresolvedIssues)
 }
 
diff --git a/models/repo_collaboration.go b/models/repo_collaboration.go
index 7d43115b23..c8866421bd 100644
--- a/models/repo_collaboration.go
+++ b/models/repo_collaboration.go
@@ -10,6 +10,7 @@ import (
 	"fmt"
 
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -101,7 +102,7 @@ func reconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Reposito
 
 	if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": uid}).
 		In("issue_id", builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repo.ID})).
-		Delete(&IssueAssignees{}); err != nil {
+		Delete(&issues_model.IssueAssignees{}); err != nil {
 		return fmt.Errorf("Could not delete assignee[%d] %v", uid, err)
 	}
 	return nil
@@ -116,5 +117,5 @@ func reconsiderWatches(ctx context.Context, repo *repo_model.Repository, uid int
 	}
 
 	// Remove all IssueWatches a user has subscribed to in the repository
-	return removeIssueWatchersByRepoID(ctx, uid, repo.ID)
+	return issues_model.RemoveIssueWatchersByRepoID(ctx, uid, repo.ID)
 }
diff --git a/models/repo_transfer.go b/models/repo_transfer.go
index 79cfc699c8..7d07fb252c 100644
--- a/models/repo_transfer.go
+++ b/models/repo_transfer.go
@@ -10,6 +10,7 @@ import (
 	"os"
 
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -376,7 +377,7 @@ func TransferOwnership(doer *user_model.User, newOwnerName string, repo *repo_mo
 						INNER JOIN issue ON issue.id = com.issue_id
 					WHERE
 						com.type = ? AND issue.repo_id = ? AND ((label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != ?))
-		) AS il_too)`, CommentTypeLabel, repo.ID, newOwner.ID); err != nil {
+		) AS il_too)`, issues_model.CommentTypeLabel, repo.ID, newOwner.ID); err != nil {
 			return fmt.Errorf("Unable to remove old org label comments: %v", err)
 		}
 	}
diff --git a/models/statistic.go b/models/statistic.go
index dfc236ec58..55ace626c8 100644
--- a/models/statistic.go
+++ b/models/statistic.go
@@ -97,7 +97,7 @@ func GetStatistic() (stats Statistic) {
 
 	stats.Counter.Issue = stats.Counter.IssueClosed + stats.Counter.IssueOpen
 
-	stats.Counter.Comment, _ = e.Count(new(Comment))
+	stats.Counter.Comment, _ = e.Count(new(issues_model.Comment))
 	stats.Counter.Oauth = 0
 	stats.Counter.Follow, _ = e.Count(new(user_model.Follow))
 	stats.Counter.Mirror, _ = e.Count(new(repo_model.Mirror))
@@ -105,7 +105,7 @@ func GetStatistic() (stats Statistic) {
 	stats.Counter.AuthSource = auth.CountSources()
 	stats.Counter.Webhook, _ = e.Count(new(webhook.Webhook))
 	stats.Counter.Milestone, _ = e.Count(new(issues_model.Milestone))
-	stats.Counter.Label, _ = e.Count(new(Label))
+	stats.Counter.Label, _ = e.Count(new(issues_model.Label))
 	stats.Counter.HookTask, _ = e.Count(new(webhook.HookTask))
 	stats.Counter.Team, _ = e.Count(new(organization.Team))
 	stats.Counter.Attachment, _ = e.Count(new(repo_model.Attachment))
diff --git a/models/user.go b/models/user.go
index 59ec643d55..49374014aa 100644
--- a/models/user.go
+++ b/models/user.go
@@ -16,7 +16,7 @@ import (
 	auth_model "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
-	"code.gitea.io/gitea/models/issues"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	pull_model "code.gitea.io/gitea/models/pull"
@@ -78,12 +78,12 @@ func DeleteUser(ctx context.Context, u *user_model.User) (err error) {
 		&user_model.Follow{UserID: u.ID},
 		&user_model.Follow{FollowID: u.ID},
 		&Action{UserID: u.ID},
-		&IssueUser{UID: u.ID},
+		&issues_model.IssueUser{UID: u.ID},
 		&user_model.EmailAddress{UID: u.ID},
 		&user_model.UserOpenID{UID: u.ID},
-		&issues.Reaction{UserID: u.ID},
+		&issues_model.Reaction{UserID: u.ID},
 		&organization.TeamUser{UID: u.ID},
-		&Stopwatch{UserID: u.ID},
+		&issues_model.Stopwatch{UserID: u.ID},
 		&user_model.Setting{UserID: u.ID},
 		&pull_model.AutoMerge{DoerID: u.ID},
 		&pull_model.ReviewState{UserID: u.ID},
@@ -101,8 +101,8 @@ func DeleteUser(ctx context.Context, u *user_model.User) (err error) {
 		// Delete Comments
 		const batchSize = 50
 		for start := 0; ; start += batchSize {
-			comments := make([]*Comment, 0, batchSize)
-			if err = e.Where("type=? AND poster_id=?", CommentTypeComment, u.ID).Limit(batchSize, start).Find(&comments); err != nil {
+			comments := make([]*issues_model.Comment, 0, batchSize)
+			if err = e.Where("type=? AND poster_id=?", issues_model.CommentTypeComment, u.ID).Limit(batchSize, start).Find(&comments); err != nil {
 				return err
 			}
 			if len(comments) == 0 {
@@ -110,14 +110,14 @@ func DeleteUser(ctx context.Context, u *user_model.User) (err error) {
 			}
 
 			for _, comment := range comments {
-				if err = deleteComment(ctx, comment); err != nil {
+				if err = issues_model.DeleteComment(ctx, comment); err != nil {
 					return err
 				}
 			}
 		}
 
 		// Delete Reactions
-		if err = issues.DeleteReaction(ctx, &issues.ReactionOptions{DoerID: u.ID}); err != nil {
+		if err = issues_model.DeleteReaction(ctx, &issues_model.ReactionOptions{DoerID: u.ID}); err != nil {
 			return err
 		}
 	}
@@ -189,7 +189,7 @@ func DeleteUser(ctx context.Context, u *user_model.User) (err error) {
 	// ***** END: GPGPublicKey *****
 
 	// Clear assignee.
-	if _, err = db.DeleteByBean(ctx, &IssueAssignees{AssigneeID: u.ID}); err != nil {
+	if _, err = db.DeleteByBean(ctx, &issues_model.IssueAssignees{AssigneeID: u.ID}); err != nil {
 		return fmt.Errorf("clear assignee: %v", err)
 	}
 
diff --git a/modules/context/repo.go b/modules/context/repo.go
index 05ced909a8..c2b8306b9d 100644
--- a/modules/context/repo.go
+++ b/modules/context/repo.go
@@ -18,6 +18,7 @@ import (
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
+	issues_model "code.gitea.io/gitea/models/issues"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	unit_model "code.gitea.io/gitea/models/unit"
@@ -83,7 +84,7 @@ type Repository struct {
 
 // CanWriteToBranch checks if the branch is writable by the user
 func (r *Repository) CanWriteToBranch(user *user_model.User, branch string) bool {
-	return models.CanMaintainerWriteToBranch(r.Permission, branch, user)
+	return issues_model.CanMaintainerWriteToBranch(r.Permission, branch, user)
 }
 
 // CanEnableEditor returns true if repository is editable and user has proper access level.
@@ -158,11 +159,11 @@ func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.Use
 }
 
 // CanUseTimetracker returns whether or not a user can use the timetracker.
-func (r *Repository) CanUseTimetracker(issue *models.Issue, user *user_model.User) bool {
+func (r *Repository) CanUseTimetracker(issue *issues_model.Issue, user *user_model.User) bool {
 	// Checking for following:
 	// 1. Is timetracker enabled
 	// 2. Is the user a contributor, admin, poster or assignee and do the repository policies require this?
-	isAssigned, _ := models.IsUserAssignedToIssue(db.DefaultContext, issue, user)
+	isAssigned, _ := issues_model.IsUserAssignedToIssue(db.DefaultContext, issue, user)
 	return r.Repository.IsTimetrackerEnabled() && (!r.Repository.AllowOnlyContributorsToTrackTime() ||
 		r.Permission.CanWriteIssuesOrPulls(issue.IsPull) || issue.IsPoster(user.ID) || isAssigned)
 }
diff --git a/modules/convert/convert.go b/modules/convert/convert.go
index 4e8aa59067..c8cb23261e 100644
--- a/modules/convert/convert.go
+++ b/modules/convert/convert.go
@@ -11,11 +11,11 @@ import (
 	"strings"
 	"time"
 
-	"code.gitea.io/gitea/models"
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
@@ -55,7 +55,7 @@ func ToBranch(repo *repo_model.Repository, b *git.Branch, c *git.Commit, bp *git
 			if err != nil {
 				return nil, err
 			}
-			canPush = models.CanMaintainerWriteToBranch(perms, b.Name, user)
+			canPush = issues_model.CanMaintainerWriteToBranch(perms, b.Name, user)
 		}
 
 		return &api.Branch{
diff --git a/modules/convert/issue.go b/modules/convert/issue.go
index a4512e424f..35eff05229 100644
--- a/modules/convert/issue.go
+++ b/modules/convert/issue.go
@@ -9,7 +9,6 @@ import (
 	"net/url"
 	"strings"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -23,7 +22,7 @@ import (
 // it assumes some fields assigned with values:
 // Required - Poster, Labels,
 // Optional - Milestone, Assignee, PullRequest
-func ToAPIIssue(issue *models.Issue) *api.Issue {
+func ToAPIIssue(issue *issues_model.Issue) *api.Issue {
 	if err := issue.LoadLabels(db.DefaultContext); err != nil {
 		return &api.Issue{}
 	}
@@ -100,7 +99,7 @@ func ToAPIIssue(issue *models.Issue) *api.Issue {
 }
 
 // ToAPIIssueList converts an IssueList to API format
-func ToAPIIssueList(il models.IssueList) []*api.Issue {
+func ToAPIIssueList(il issues_model.IssueList) []*api.Issue {
 	result := make([]*api.Issue, len(il))
 	for i := range il {
 		result[i] = ToAPIIssue(il[i])
@@ -109,7 +108,7 @@ func ToAPIIssueList(il models.IssueList) []*api.Issue {
 }
 
 // ToTrackedTime converts TrackedTime to API format
-func ToTrackedTime(t *models.TrackedTime) (apiT *api.TrackedTime) {
+func ToTrackedTime(t *issues_model.TrackedTime) (apiT *api.TrackedTime) {
 	apiT = &api.TrackedTime{
 		ID:       t.ID,
 		IssueID:  t.IssueID,
@@ -128,13 +127,13 @@ func ToTrackedTime(t *models.TrackedTime) (apiT *api.TrackedTime) {
 }
 
 // ToStopWatches convert Stopwatch list to api.StopWatches
-func ToStopWatches(sws []*models.Stopwatch) (api.StopWatches, error) {
+func ToStopWatches(sws []*issues_model.Stopwatch) (api.StopWatches, error) {
 	result := api.StopWatches(make([]api.StopWatch, 0, len(sws)))
 
-	issueCache := make(map[int64]*models.Issue)
+	issueCache := make(map[int64]*issues_model.Issue)
 	repoCache := make(map[int64]*repo_model.Repository)
 	var (
-		issue *models.Issue
+		issue *issues_model.Issue
 		repo  *repo_model.Repository
 		ok    bool
 		err   error
@@ -143,7 +142,7 @@ func ToStopWatches(sws []*models.Stopwatch) (api.StopWatches, error) {
 	for _, sw := range sws {
 		issue, ok = issueCache[sw.IssueID]
 		if !ok {
-			issue, err = models.GetIssueByID(sw.IssueID)
+			issue, err = issues_model.GetIssueByID(db.DefaultContext, sw.IssueID)
 			if err != nil {
 				return nil, err
 			}
@@ -170,7 +169,7 @@ func ToStopWatches(sws []*models.Stopwatch) (api.StopWatches, error) {
 }
 
 // ToTrackedTimeList converts TrackedTimeList to API format
-func ToTrackedTimeList(tl models.TrackedTimeList) api.TrackedTimeList {
+func ToTrackedTimeList(tl issues_model.TrackedTimeList) api.TrackedTimeList {
 	result := make([]*api.TrackedTime, 0, len(tl))
 	for _, t := range tl {
 		result = append(result, ToTrackedTime(t))
@@ -179,7 +178,7 @@ func ToTrackedTimeList(tl models.TrackedTimeList) api.TrackedTimeList {
 }
 
 // ToLabel converts Label to API format
-func ToLabel(label *models.Label, repo *repo_model.Repository, org *user_model.User) *api.Label {
+func ToLabel(label *issues_model.Label, repo *repo_model.Repository, org *user_model.User) *api.Label {
 	result := &api.Label{
 		ID:          label.ID,
 		Name:        label.Name,
@@ -206,7 +205,7 @@ func ToLabel(label *models.Label, repo *repo_model.Repository, org *user_model.U
 }
 
 // ToLabelList converts list of Label to API format
-func ToLabelList(labels []*models.Label, repo *repo_model.Repository, org *user_model.User) []*api.Label {
+func ToLabelList(labels []*issues_model.Label, repo *repo_model.Repository, org *user_model.User) []*api.Label {
 	result := make([]*api.Label, len(labels))
 	for i := range labels {
 		result[i] = ToLabel(labels[i], repo, org)
diff --git a/modules/convert/issue_comment.go b/modules/convert/issue_comment.go
index eaa7f64ea3..ccc94b2496 100644
--- a/modules/convert/issue_comment.go
+++ b/modules/convert/issue_comment.go
@@ -5,16 +5,16 @@
 package convert
 
 import (
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
 	api "code.gitea.io/gitea/modules/structs"
 )
 
-// ToComment converts a models.Comment to the api.Comment format
-func ToComment(c *models.Comment) *api.Comment {
+// ToComment converts a issues_model.Comment to the api.Comment format
+func ToComment(c *issues_model.Comment) *api.Comment {
 	return &api.Comment{
 		ID:       c.ID,
 		Poster:   ToUser(c.Poster, nil),
@@ -27,8 +27,8 @@ func ToComment(c *models.Comment) *api.Comment {
 	}
 }
 
-// ToTimelineComment converts a models.Comment to the api.TimelineComment format
-func ToTimelineComment(c *models.Comment, doer *user_model.User) *api.TimelineComment {
+// ToTimelineComment converts a issues_model.Comment to the api.TimelineComment format
+func ToTimelineComment(c *issues_model.Comment, doer *user_model.User) *api.TimelineComment {
 	err := c.LoadMilestone()
 	if err != nil {
 		log.Error("LoadMilestone: %v", err)
@@ -105,7 +105,7 @@ func ToTimelineComment(c *models.Comment, doer *user_model.User) *api.TimelineCo
 	}
 
 	if c.RefIssueID != 0 {
-		issue, err := models.GetIssueByID(c.RefIssueID)
+		issue, err := issues_model.GetIssueByID(db.DefaultContext, c.RefIssueID)
 		if err != nil {
 			log.Error("GetIssueByID(%d): %v", c.RefIssueID, err)
 			return nil
@@ -114,7 +114,7 @@ func ToTimelineComment(c *models.Comment, doer *user_model.User) *api.TimelineCo
 	}
 
 	if c.RefCommentID != 0 {
-		com, err := models.GetCommentByID(db.DefaultContext, c.RefCommentID)
+		com, err := issues_model.GetCommentByID(db.DefaultContext, c.RefCommentID)
 		if err != nil {
 			log.Error("GetCommentByID(%d): %v", c.RefCommentID, err)
 			return nil
diff --git a/modules/convert/issue_test.go b/modules/convert/issue_test.go
index b237c18f69..5bf04bcb52 100644
--- a/modules/convert/issue_test.go
+++ b/modules/convert/issue_test.go
@@ -9,7 +9,6 @@ import (
 	"testing"
 	"time"
 
-	"code.gitea.io/gitea/models"
 	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
@@ -22,7 +21,7 @@ import (
 
 func TestLabel_ToLabel(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	label := unittest.AssertExistsAndLoadBean(t, &models.Label{ID: 1}).(*models.Label)
+	label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label)
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: label.RepoID}).(*repo_model.Repository)
 	assert.Equal(t, &api.Label{
 		ID:    label.ID,
diff --git a/modules/convert/pull.go b/modules/convert/pull.go
index 310a7626c9..9c31f9bd2c 100644
--- a/modules/convert/pull.go
+++ b/modules/convert/pull.go
@@ -8,7 +8,7 @@ import (
 	"context"
 	"fmt"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	user_model "code.gitea.io/gitea/models/user"
@@ -20,7 +20,7 @@ import (
 // ToAPIPullRequest assumes following fields have been assigned with valid values:
 // Required - Issue
 // Optional - Merger
-func ToAPIPullRequest(ctx context.Context, pr *models.PullRequest, doer *user_model.User) *api.PullRequest {
+func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) *api.PullRequest {
 	var (
 		baseBranch *git.Branch
 		headBranch *git.Branch
@@ -114,7 +114,7 @@ func ToAPIPullRequest(ctx context.Context, pr *models.PullRequest, doer *user_mo
 		}
 	}
 
-	if pr.Flow == models.PullRequestFlowAGit {
+	if pr.Flow == issues_model.PullRequestFlowAGit {
 		gitRepo, err := git.OpenRepository(ctx, pr.BaseRepo.RepoPath())
 		if err != nil {
 			log.Error("OpenRepository[%s]: %v", pr.GetGitRefName(), err)
@@ -132,7 +132,7 @@ func ToAPIPullRequest(ctx context.Context, pr *models.PullRequest, doer *user_mo
 		apiPullRequest.Head.Name = ""
 	}
 
-	if pr.HeadRepo != nil && pr.Flow == models.PullRequestFlowGithub {
+	if pr.HeadRepo != nil && pr.Flow == issues_model.PullRequestFlowGithub {
 		p, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, doer)
 		if err != nil {
 			log.Error("GetUserRepoPermission[%d]: %v", pr.HeadRepoID, err)
diff --git a/modules/convert/pull_review.go b/modules/convert/pull_review.go
index 907ccafb66..93ce208224 100644
--- a/modules/convert/pull_review.go
+++ b/modules/convert/pull_review.go
@@ -8,13 +8,13 @@ import (
 	"context"
 	"strings"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	user_model "code.gitea.io/gitea/models/user"
 	api "code.gitea.io/gitea/modules/structs"
 )
 
 // ToPullReview convert a review to api format
-func ToPullReview(ctx context.Context, r *models.Review, doer *user_model.User) (*api.PullReview, error) {
+func ToPullReview(ctx context.Context, r *issues_model.Review, doer *user_model.User) (*api.PullReview, error) {
 	if err := r.LoadAttributes(ctx); err != nil {
 		if !user_model.IsErrUserNotExist(err) {
 			return nil, err
@@ -44,15 +44,15 @@ func ToPullReview(ctx context.Context, r *models.Review, doer *user_model.User)
 	}
 
 	switch r.Type {
-	case models.ReviewTypeApprove:
+	case issues_model.ReviewTypeApprove:
 		result.State = api.ReviewStateApproved
-	case models.ReviewTypeReject:
+	case issues_model.ReviewTypeReject:
 		result.State = api.ReviewStateRequestChanges
-	case models.ReviewTypeComment:
+	case issues_model.ReviewTypeComment:
 		result.State = api.ReviewStateComment
-	case models.ReviewTypePending:
+	case issues_model.ReviewTypePending:
 		result.State = api.ReviewStatePending
-	case models.ReviewTypeRequest:
+	case issues_model.ReviewTypeRequest:
 		result.State = api.ReviewStateRequestReview
 	}
 
@@ -60,11 +60,11 @@ func ToPullReview(ctx context.Context, r *models.Review, doer *user_model.User)
 }
 
 // ToPullReviewList convert a list of review to it's api format
-func ToPullReviewList(ctx context.Context, rl []*models.Review, doer *user_model.User) ([]*api.PullReview, error) {
+func ToPullReviewList(ctx context.Context, rl []*issues_model.Review, doer *user_model.User) ([]*api.PullReview, error) {
 	result := make([]*api.PullReview, 0, len(rl))
 	for i := range rl {
 		// show pending reviews only for the user who created them
-		if rl[i].Type == models.ReviewTypePending && !(doer.IsAdmin || doer.ID == rl[i].ReviewerID) {
+		if rl[i].Type == issues_model.ReviewTypePending && !(doer.IsAdmin || doer.ID == rl[i].ReviewerID) {
 			continue
 		}
 		r, err := ToPullReview(ctx, rl[i], doer)
@@ -77,7 +77,7 @@ func ToPullReviewList(ctx context.Context, rl []*models.Review, doer *user_model
 }
 
 // ToPullReviewCommentList convert the CodeComments of an review to it's api format
-func ToPullReviewCommentList(ctx context.Context, review *models.Review, doer *user_model.User) ([]*api.PullReviewComment, error) {
+func ToPullReviewCommentList(ctx context.Context, review *issues_model.Review, doer *user_model.User) ([]*api.PullReviewComment, error) {
 	if err := review.LoadAttributes(ctx); err != nil {
 		if !user_model.IsErrUserNotExist(err) {
 			return nil, err
diff --git a/modules/convert/pull_test.go b/modules/convert/pull_test.go
index 8574ccfd26..10ef311399 100644
--- a/modules/convert/pull_test.go
+++ b/modules/convert/pull_test.go
@@ -7,7 +7,7 @@ package convert
 import (
 	"testing"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/perm"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
@@ -21,7 +21,7 @@ func TestPullRequest_APIFormat(t *testing.T) {
 	// with HeadRepo
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	headRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
-	pr := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 1}).(*models.PullRequest)
+	pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest)
 	assert.NoError(t, pr.LoadAttributes())
 	assert.NoError(t, pr.LoadIssue())
 	apiPullRequest := ToAPIPullRequest(git.DefaultContext, pr, nil)
@@ -35,7 +35,7 @@ func TestPullRequest_APIFormat(t *testing.T) {
 	}, apiPullRequest.Head)
 
 	// withOut HeadRepo
-	pr = unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 1}).(*models.PullRequest)
+	pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest)
 	assert.NoError(t, pr.LoadIssue())
 	assert.NoError(t, pr.LoadAttributes())
 	// simulate fork deletion
diff --git a/modules/doctor/dbconsistency.go b/modules/doctor/dbconsistency.go
index 18cb2cdac6..b23807cca2 100644
--- a/modules/doctor/dbconsistency.go
+++ b/modules/doctor/dbconsistency.go
@@ -9,6 +9,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/migrations"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/log"
@@ -64,10 +65,10 @@ func genericOrphanCheck(name, subject, refobject, joincond string) consistencyCh
 	return consistencyCheck{
 		Name: name,
 		Counter: func() (int64, error) {
-			return models.CountOrphanedObjects(subject, refobject, joincond)
+			return db.CountOrphanedObjects(subject, refobject, joincond)
 		},
 		Fixer: func() (int64, error) {
-			err := models.DeleteOrphanedObjects(subject, refobject, joincond)
+			err := db.DeleteOrphanedObjects(subject, refobject, joincond)
 			return -1, err
 		},
 	}
@@ -84,20 +85,20 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
 		{
 			// find labels without existing repo or org
 			Name:    "Orphaned Labels without existing repository or organisation",
-			Counter: models.CountOrphanedLabels,
-			Fixer:   asFixer(models.DeleteOrphanedLabels),
+			Counter: issues_model.CountOrphanedLabels,
+			Fixer:   asFixer(issues_model.DeleteOrphanedLabels),
 		},
 		{
 			// find IssueLabels without existing label
 			Name:    "Orphaned Issue Labels without existing label",
-			Counter: models.CountOrphanedIssueLabels,
-			Fixer:   asFixer(models.DeleteOrphanedIssueLabels),
+			Counter: issues_model.CountOrphanedIssueLabels,
+			Fixer:   asFixer(issues_model.DeleteOrphanedIssueLabels),
 		},
 		{
 			// find issues without existing repository
 			Name:    "Orphaned Issues without existing repository",
-			Counter: models.CountOrphanedIssues,
-			Fixer:   asFixer(models.DeleteOrphanedIssues),
+			Counter: issues_model.CountOrphanedIssues,
+			Fixer:   asFixer(issues_model.DeleteOrphanedIssues),
 		},
 		// find releases without existing repository
 		genericOrphanCheck("Orphaned Releases without existing repository",
@@ -127,22 +128,22 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
 		// find label comments with empty labels
 		{
 			Name:         "Label comments with empty labels",
-			Counter:      models.CountCommentTypeLabelWithEmptyLabel,
-			Fixer:        models.FixCommentTypeLabelWithEmptyLabel,
+			Counter:      issues_model.CountCommentTypeLabelWithEmptyLabel,
+			Fixer:        issues_model.FixCommentTypeLabelWithEmptyLabel,
 			FixedMessage: "Fixed",
 		},
 		// find label comments with labels from outside the repository
 		{
 			Name:         "Label comments with labels from outside the repository",
-			Counter:      models.CountCommentTypeLabelWithOutsideLabels,
-			Fixer:        models.FixCommentTypeLabelWithOutsideLabels,
+			Counter:      issues_model.CountCommentTypeLabelWithOutsideLabels,
+			Fixer:        issues_model.FixCommentTypeLabelWithOutsideLabels,
 			FixedMessage: "Removed",
 		},
 		// find issue_label with labels from outside the repository
 		{
 			Name:         "IssueLabels with Labels from outside the repository",
-			Counter:      models.CountIssueLabelWithOutsideLabels,
-			Fixer:        models.FixIssueLabelWithOutsideLabels,
+			Counter:      issues_model.CountIssueLabelWithOutsideLabels,
+			Fixer:        issues_model.FixIssueLabelWithOutsideLabels,
 			FixedMessage: "Removed",
 		},
 		{
diff --git a/modules/doctor/mergebase.go b/modules/doctor/mergebase.go
index 61ee9e212b..46369290a1 100644
--- a/modules/doctor/mergebase.go
+++ b/modules/doctor/mergebase.go
@@ -9,8 +9,8 @@ import (
 	"fmt"
 	"strings"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
@@ -18,13 +18,13 @@ import (
 	"xorm.io/builder"
 )
 
-func iteratePRs(ctx context.Context, repo *repo_model.Repository, each func(*repo_model.Repository, *models.PullRequest) error) error {
+func iteratePRs(ctx context.Context, repo *repo_model.Repository, each func(*repo_model.Repository, *issues_model.PullRequest) error) error {
 	return db.Iterate(
 		ctx,
-		new(models.PullRequest),
+		new(issues_model.PullRequest),
 		builder.Eq{"base_repo_id": repo.ID},
 		func(idx int, bean interface{}) error {
-			return each(repo, bean.(*models.PullRequest))
+			return each(repo, bean.(*issues_model.PullRequest))
 		},
 	)
 }
@@ -35,7 +35,7 @@ func checkPRMergeBase(ctx context.Context, logger log.Logger, autofix bool) erro
 	numPRsUpdated := 0
 	err := iterateRepositories(ctx, func(repo *repo_model.Repository) error {
 		numRepos++
-		return iteratePRs(ctx, repo, func(repo *repo_model.Repository, pr *models.PullRequest) error {
+		return iteratePRs(ctx, repo, func(repo *repo_model.Repository, pr *issues_model.PullRequest) error {
 			numPRs++
 			pr.BaseRepo = repo
 			repoPath := repo.RepoPath()
diff --git a/modules/eventsource/manager_run.go b/modules/eventsource/manager_run.go
index 127979ad63..6055cf7232 100644
--- a/modules/eventsource/manager_run.go
+++ b/modules/eventsource/manager_run.go
@@ -9,6 +9,7 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/convert"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/json"
@@ -84,7 +85,7 @@ loop:
 			then = now
 
 			if setting.Service.EnableTimetracking {
-				usersStopwatches, err := models.GetUIDsAndStopwatch()
+				usersStopwatches, err := issues_model.GetUIDsAndStopwatch()
 				if err != nil {
 					log.Error("Unable to get GetUIDsAndStopwatch: %v", err)
 					return
diff --git a/modules/indexer/issues/db.go b/modules/indexer/issues/db.go
index e2badf64f2..d21c86337e 100644
--- a/modules/indexer/issues/db.go
+++ b/modules/indexer/issues/db.go
@@ -7,8 +7,8 @@ package issues
 import (
 	"context"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 )
 
 // DBIndexer implements Indexer interface to use database's like search
@@ -44,7 +44,7 @@ func (i *DBIndexer) Close() {
 
 // Search dummy function
 func (i *DBIndexer) Search(ctx context.Context, kw string, repoIDs []int64, limit, start int) (*SearchResult, error) {
-	total, ids, err := models.SearchIssueIDsByKeyword(ctx, kw, repoIDs, limit, start)
+	total, ids, err := issues_model.SearchIssueIDsByKeyword(ctx, kw, repoIDs, limit, start)
 	if err != nil {
 		return nil, err
 	}
diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go
index 85de4c75b3..da6a200aef 100644
--- a/modules/indexer/issues/indexer.go
+++ b/modules/indexer/issues/indexer.go
@@ -12,8 +12,8 @@ import (
 	"sync"
 	"time"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/log"
@@ -320,7 +320,7 @@ func populateIssueIndexer(ctx context.Context) {
 
 // UpdateRepoIndexer add/update all issues of the repositories
 func UpdateRepoIndexer(repo *repo_model.Repository) {
-	is, err := models.Issues(&models.IssuesOptions{
+	is, err := issues_model.Issues(&issues_model.IssuesOptions{
 		RepoID:   repo.ID,
 		IsClosed: util.OptionalBoolNone,
 		IsPull:   util.OptionalBoolNone,
@@ -329,7 +329,7 @@ func UpdateRepoIndexer(repo *repo_model.Repository) {
 		log.Error("Issues: %v", err)
 		return
 	}
-	if err = models.IssueList(is).LoadDiscussComments(); err != nil {
+	if err = issues_model.IssueList(is).LoadDiscussComments(); err != nil {
 		log.Error("LoadComments: %v", err)
 		return
 	}
@@ -339,10 +339,10 @@ func UpdateRepoIndexer(repo *repo_model.Repository) {
 }
 
 // UpdateIssueIndexer add/update an issue to the issue indexer
-func UpdateIssueIndexer(issue *models.Issue) {
+func UpdateIssueIndexer(issue *issues_model.Issue) {
 	var comments []string
 	for _, comment := range issue.Comments {
-		if comment.Type == models.CommentTypeComment {
+		if comment.Type == issues_model.CommentTypeComment {
 			comments = append(comments, comment.Content)
 		}
 	}
@@ -362,7 +362,7 @@ func UpdateIssueIndexer(issue *models.Issue) {
 // DeleteRepoIssueIndexer deletes repo's all issues indexes
 func DeleteRepoIssueIndexer(repo *repo_model.Repository) {
 	var ids []int64
-	ids, err := models.GetIssueIDsByRepoID(db.DefaultContext, repo.ID)
+	ids, err := issues_model.GetIssueIDsByRepoID(db.DefaultContext, repo.ID)
 	if err != nil {
 		log.Error("getIssueIDsByRepoID failed: %v", err)
 		return
diff --git a/modules/notification/action/action.go b/modules/notification/action/action.go
index 547498a9dc..e438f41485 100644
--- a/modules/notification/action/action.go
+++ b/modules/notification/action/action.go
@@ -11,6 +11,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/graceful"
@@ -33,7 +34,7 @@ func NewNotifier() base.Notifier {
 	return &actionNotifier{}
 }
 
-func (a *actionNotifier) NotifyNewIssue(issue *models.Issue, mentions []*user_model.User) {
+func (a *actionNotifier) NotifyNewIssue(issue *issues_model.Issue, mentions []*user_model.User) {
 	if err := issue.LoadPoster(); err != nil {
 		log.Error("issue.LoadPoster: %v", err)
 		return
@@ -58,7 +59,7 @@ func (a *actionNotifier) NotifyNewIssue(issue *models.Issue, mentions []*user_mo
 }
 
 // NotifyIssueChangeStatus notifies close or reopen issue to notifiers
-func (a *actionNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *models.Issue, actionComment *models.Comment, closeOrReopen bool) {
+func (a *actionNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *issues_model.Issue, actionComment *issues_model.Comment, closeOrReopen bool) {
 	// Compose comment action, could be plain comment, close or reopen issue/pull request.
 	// This object will be used to notify watchers in the end of function.
 	act := &models.Action{
@@ -92,7 +93,7 @@ func (a *actionNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *m
 
 // NotifyCreateIssueComment notifies comment on an issue to notifiers
 func (a *actionNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository,
-	issue *models.Issue, comment *models.Comment, mentions []*user_model.User,
+	issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User,
 ) {
 	act := &models.Action{
 		ActUserID: doer.ID,
@@ -126,7 +127,7 @@ func (a *actionNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *r
 	}
 }
 
-func (a *actionNotifier) NotifyNewPullRequest(pull *models.PullRequest, mentions []*user_model.User) {
+func (a *actionNotifier) NotifyNewPullRequest(pull *issues_model.PullRequest, mentions []*user_model.User) {
 	if err := pull.LoadIssue(); err != nil {
 		log.Error("pull.LoadIssue: %v", err)
 		return
@@ -207,7 +208,7 @@ func (a *actionNotifier) NotifyForkRepository(doer *user_model.User, oldRepo, re
 	}
 }
 
-func (a *actionNotifier) NotifyPullRequestReview(pr *models.PullRequest, review *models.Review, comment *models.Comment, mentions []*user_model.User) {
+func (a *actionNotifier) NotifyPullRequestReview(pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) {
 	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("actionNotifier.NotifyPullRequestReview Pull[%d] #%d in [%d]", pr.ID, pr.Index, pr.BaseRepoID))
 	defer finished()
 
@@ -239,7 +240,7 @@ func (a *actionNotifier) NotifyPullRequestReview(pr *models.PullRequest, review
 		}
 	}
 
-	if review.Type != models.ReviewTypeComment || strings.TrimSpace(comment.Content) != "" {
+	if review.Type != issues_model.ReviewTypeComment || strings.TrimSpace(comment.Content) != "" {
 		action := &models.Action{
 			ActUserID: review.Reviewer.ID,
 			ActUser:   review.Reviewer,
@@ -252,9 +253,9 @@ func (a *actionNotifier) NotifyPullRequestReview(pr *models.PullRequest, review
 		}
 
 		switch review.Type {
-		case models.ReviewTypeApprove:
+		case issues_model.ReviewTypeApprove:
 			action.OpType = models.ActionApprovePullRequest
-		case models.ReviewTypeReject:
+		case issues_model.ReviewTypeReject:
 			action.OpType = models.ActionRejectPullRequest
 		default:
 			action.OpType = models.ActionCommentPull
@@ -268,7 +269,7 @@ func (a *actionNotifier) NotifyPullRequestReview(pr *models.PullRequest, review
 	}
 }
 
-func (*actionNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *user_model.User) {
+func (*actionNotifier) NotifyMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) {
 	if err := models.NotifyWatchers(&models.Action{
 		ActUserID: doer.ID,
 		ActUser:   doer,
@@ -282,7 +283,7 @@ func (*actionNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *user
 	}
 }
 
-func (*actionNotifier) NotifyPullRevieweDismiss(doer *user_model.User, review *models.Review, comment *models.Comment) {
+func (*actionNotifier) NotifyPullRevieweDismiss(doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) {
 	reviewerName := review.Reviewer.Name
 	if len(review.OriginalAuthor) > 0 {
 		reviewerName = review.OriginalAuthor
diff --git a/modules/notification/base/notifier.go b/modules/notification/base/notifier.go
index 2b8be18ad3..31fa8f5f18 100644
--- a/modules/notification/base/notifier.go
+++ b/modules/notification/base/notifier.go
@@ -6,6 +6,7 @@ package base
 
 import (
 	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	packages_model "code.gitea.io/gitea/models/packages"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
@@ -21,30 +22,30 @@ type Notifier interface {
 	NotifyForkRepository(doer *user_model.User, oldRepo, repo *repo_model.Repository)
 	NotifyRenameRepository(doer *user_model.User, repo *repo_model.Repository, oldRepoName string)
 	NotifyTransferRepository(doer *user_model.User, repo *repo_model.Repository, oldOwnerName string)
-	NotifyNewIssue(issue *models.Issue, mentions []*user_model.User)
-	NotifyIssueChangeStatus(*user_model.User, *models.Issue, *models.Comment, bool)
-	NotifyDeleteIssue(*user_model.User, *models.Issue)
-	NotifyIssueChangeMilestone(doer *user_model.User, issue *models.Issue, oldMilestoneID int64)
-	NotifyIssueChangeAssignee(doer *user_model.User, issue *models.Issue, assignee *user_model.User, removed bool, comment *models.Comment)
-	NotifyPullReviewRequest(doer *user_model.User, issue *models.Issue, reviewer *user_model.User, isRequest bool, comment *models.Comment)
-	NotifyIssueChangeContent(doer *user_model.User, issue *models.Issue, oldContent string)
-	NotifyIssueClearLabels(doer *user_model.User, issue *models.Issue)
-	NotifyIssueChangeTitle(doer *user_model.User, issue *models.Issue, oldTitle string)
-	NotifyIssueChangeRef(doer *user_model.User, issue *models.Issue, oldRef string)
-	NotifyIssueChangeLabels(doer *user_model.User, issue *models.Issue,
-		addedLabels, removedLabels []*models.Label)
-	NotifyNewPullRequest(pr *models.PullRequest, mentions []*user_model.User)
-	NotifyMergePullRequest(*models.PullRequest, *user_model.User)
-	NotifyPullRequestSynchronized(doer *user_model.User, pr *models.PullRequest)
-	NotifyPullRequestReview(pr *models.PullRequest, review *models.Review, comment *models.Comment, mentions []*user_model.User)
-	NotifyPullRequestCodeComment(pr *models.PullRequest, comment *models.Comment, mentions []*user_model.User)
-	NotifyPullRequestChangeTargetBranch(doer *user_model.User, pr *models.PullRequest, oldBranch string)
-	NotifyPullRequestPushCommits(doer *user_model.User, pr *models.PullRequest, comment *models.Comment)
-	NotifyPullRevieweDismiss(doer *user_model.User, review *models.Review, comment *models.Comment)
+	NotifyNewIssue(issue *issues_model.Issue, mentions []*user_model.User)
+	NotifyIssueChangeStatus(*user_model.User, *issues_model.Issue, *issues_model.Comment, bool)
+	NotifyDeleteIssue(*user_model.User, *issues_model.Issue)
+	NotifyIssueChangeMilestone(doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64)
+	NotifyIssueChangeAssignee(doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment)
+	NotifyPullReviewRequest(doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment)
+	NotifyIssueChangeContent(doer *user_model.User, issue *issues_model.Issue, oldContent string)
+	NotifyIssueClearLabels(doer *user_model.User, issue *issues_model.Issue)
+	NotifyIssueChangeTitle(doer *user_model.User, issue *issues_model.Issue, oldTitle string)
+	NotifyIssueChangeRef(doer *user_model.User, issue *issues_model.Issue, oldRef string)
+	NotifyIssueChangeLabels(doer *user_model.User, issue *issues_model.Issue,
+		addedLabels, removedLabels []*issues_model.Label)
+	NotifyNewPullRequest(pr *issues_model.PullRequest, mentions []*user_model.User)
+	NotifyMergePullRequest(*issues_model.PullRequest, *user_model.User)
+	NotifyPullRequestSynchronized(doer *user_model.User, pr *issues_model.PullRequest)
+	NotifyPullRequestReview(pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User)
+	NotifyPullRequestCodeComment(pr *issues_model.PullRequest, comment *issues_model.Comment, mentions []*user_model.User)
+	NotifyPullRequestChangeTargetBranch(doer *user_model.User, pr *issues_model.PullRequest, oldBranch string)
+	NotifyPullRequestPushCommits(doer *user_model.User, pr *issues_model.PullRequest, comment *issues_model.Comment)
+	NotifyPullRevieweDismiss(doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment)
 	NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository,
-		issue *models.Issue, comment *models.Comment, mentions []*user_model.User)
-	NotifyUpdateComment(*user_model.User, *models.Comment, string)
-	NotifyDeleteComment(*user_model.User, *models.Comment)
+		issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User)
+	NotifyUpdateComment(*user_model.User, *issues_model.Comment, string)
+	NotifyDeleteComment(*user_model.User, *issues_model.Comment)
 	NotifyNewRelease(rel *models.Release)
 	NotifyUpdateRelease(doer *user_model.User, rel *models.Release)
 	NotifyDeleteRelease(doer *user_model.User, rel *models.Release)
diff --git a/modules/notification/base/null.go b/modules/notification/base/null.go
index 29b5f0c97e..d336f09301 100644
--- a/modules/notification/base/null.go
+++ b/modules/notification/base/null.go
@@ -6,6 +6,7 @@ package base
 
 import (
 	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	packages_model "code.gitea.io/gitea/models/packages"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
@@ -23,59 +24,59 @@ func (*NullNotifier) Run() {
 
 // NotifyCreateIssueComment places a place holder function
 func (*NullNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository,
-	issue *models.Issue, comment *models.Comment, mentions []*user_model.User) {
+	issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User) {
 }
 
 // NotifyNewIssue places a place holder function
-func (*NullNotifier) NotifyNewIssue(issue *models.Issue, mentions []*user_model.User) {
+func (*NullNotifier) NotifyNewIssue(issue *issues_model.Issue, mentions []*user_model.User) {
 }
 
 // NotifyIssueChangeStatus places a place holder function
-func (*NullNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *models.Issue, actionComment *models.Comment, isClosed bool) {
+func (*NullNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *issues_model.Issue, actionComment *issues_model.Comment, isClosed bool) {
 }
 
 // NotifyDeleteIssue notify when some issue deleted
-func (*NullNotifier) NotifyDeleteIssue(doer *user_model.User, issue *models.Issue) {
+func (*NullNotifier) NotifyDeleteIssue(doer *user_model.User, issue *issues_model.Issue) {
 }
 
 // NotifyNewPullRequest places a place holder function
-func (*NullNotifier) NotifyNewPullRequest(pr *models.PullRequest, mentions []*user_model.User) {
+func (*NullNotifier) NotifyNewPullRequest(pr *issues_model.PullRequest, mentions []*user_model.User) {
 }
 
 // NotifyPullRequestReview places a place holder function
-func (*NullNotifier) NotifyPullRequestReview(pr *models.PullRequest, r *models.Review, comment *models.Comment, mentions []*user_model.User) {
+func (*NullNotifier) NotifyPullRequestReview(pr *issues_model.PullRequest, r *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) {
 }
 
 // NotifyPullRequestCodeComment places a place holder function
-func (*NullNotifier) NotifyPullRequestCodeComment(pr *models.PullRequest, comment *models.Comment, mentions []*user_model.User) {
+func (*NullNotifier) NotifyPullRequestCodeComment(pr *issues_model.PullRequest, comment *issues_model.Comment, mentions []*user_model.User) {
 }
 
 // NotifyMergePullRequest places a place holder function
-func (*NullNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *user_model.User) {
+func (*NullNotifier) NotifyMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) {
 }
 
 // NotifyPullRequestSynchronized places a place holder function
-func (*NullNotifier) NotifyPullRequestSynchronized(doer *user_model.User, pr *models.PullRequest) {
+func (*NullNotifier) NotifyPullRequestSynchronized(doer *user_model.User, pr *issues_model.PullRequest) {
 }
 
 // NotifyPullRequestChangeTargetBranch places a place holder function
-func (*NullNotifier) NotifyPullRequestChangeTargetBranch(doer *user_model.User, pr *models.PullRequest, oldBranch string) {
+func (*NullNotifier) NotifyPullRequestChangeTargetBranch(doer *user_model.User, pr *issues_model.PullRequest, oldBranch string) {
 }
 
 // NotifyPullRequestPushCommits notifies when push commits to pull request's head branch
-func (*NullNotifier) NotifyPullRequestPushCommits(doer *user_model.User, pr *models.PullRequest, comment *models.Comment) {
+func (*NullNotifier) NotifyPullRequestPushCommits(doer *user_model.User, pr *issues_model.PullRequest, comment *issues_model.Comment) {
 }
 
 // NotifyPullRevieweDismiss notifies when a review was dismissed by repo admin
-func (*NullNotifier) NotifyPullRevieweDismiss(doer *user_model.User, review *models.Review, comment *models.Comment) {
+func (*NullNotifier) NotifyPullRevieweDismiss(doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) {
 }
 
 // NotifyUpdateComment places a place holder function
-func (*NullNotifier) NotifyUpdateComment(doer *user_model.User, c *models.Comment, oldContent string) {
+func (*NullNotifier) NotifyUpdateComment(doer *user_model.User, c *issues_model.Comment, oldContent string) {
 }
 
 // NotifyDeleteComment places a place holder function
-func (*NullNotifier) NotifyDeleteComment(doer *user_model.User, c *models.Comment) {
+func (*NullNotifier) NotifyDeleteComment(doer *user_model.User, c *issues_model.Comment) {
 }
 
 // NotifyNewRelease places a place holder function
@@ -91,36 +92,36 @@ func (*NullNotifier) NotifyDeleteRelease(doer *user_model.User, rel *models.Rele
 }
 
 // NotifyIssueChangeMilestone places a place holder function
-func (*NullNotifier) NotifyIssueChangeMilestone(doer *user_model.User, issue *models.Issue, oldMilestoneID int64) {
+func (*NullNotifier) NotifyIssueChangeMilestone(doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64) {
 }
 
 // NotifyIssueChangeContent places a place holder function
-func (*NullNotifier) NotifyIssueChangeContent(doer *user_model.User, issue *models.Issue, oldContent string) {
+func (*NullNotifier) NotifyIssueChangeContent(doer *user_model.User, issue *issues_model.Issue, oldContent string) {
 }
 
 // NotifyIssueChangeAssignee places a place holder function
-func (*NullNotifier) NotifyIssueChangeAssignee(doer *user_model.User, issue *models.Issue, assignee *user_model.User, removed bool, comment *models.Comment) {
+func (*NullNotifier) NotifyIssueChangeAssignee(doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) {
 }
 
 // NotifyPullReviewRequest places a place holder function
-func (*NullNotifier) NotifyPullReviewRequest(doer *user_model.User, issue *models.Issue, reviewer *user_model.User, isRequest bool, comment *models.Comment) {
+func (*NullNotifier) NotifyPullReviewRequest(doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
 }
 
 // NotifyIssueClearLabels places a place holder function
-func (*NullNotifier) NotifyIssueClearLabels(doer *user_model.User, issue *models.Issue) {
+func (*NullNotifier) NotifyIssueClearLabels(doer *user_model.User, issue *issues_model.Issue) {
 }
 
 // NotifyIssueChangeTitle places a place holder function
-func (*NullNotifier) NotifyIssueChangeTitle(doer *user_model.User, issue *models.Issue, oldTitle string) {
+func (*NullNotifier) NotifyIssueChangeTitle(doer *user_model.User, issue *issues_model.Issue, oldTitle string) {
 }
 
 // NotifyIssueChangeRef places a place holder function
-func (*NullNotifier) NotifyIssueChangeRef(doer *user_model.User, issue *models.Issue, oldTitle string) {
+func (*NullNotifier) NotifyIssueChangeRef(doer *user_model.User, issue *issues_model.Issue, oldTitle string) {
 }
 
 // NotifyIssueChangeLabels places a place holder function
-func (*NullNotifier) NotifyIssueChangeLabels(doer *user_model.User, issue *models.Issue,
-	addedLabels, removedLabels []*models.Label) {
+func (*NullNotifier) NotifyIssueChangeLabels(doer *user_model.User, issue *issues_model.Issue,
+	addedLabels, removedLabels []*issues_model.Label) {
 }
 
 // NotifyCreateRepository places a place holder function
diff --git a/modules/notification/indexer/indexer.go b/modules/notification/indexer/indexer.go
index 48a491f3f1..fc9afdd4bc 100644
--- a/modules/notification/indexer/indexer.go
+++ b/modules/notification/indexer/indexer.go
@@ -5,7 +5,7 @@
 package indexer
 
 import (
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
@@ -30,9 +30,9 @@ func NewNotifier() base.Notifier {
 }
 
 func (r *indexerNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository,
-	issue *models.Issue, comment *models.Comment, mentions []*user_model.User,
+	issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User,
 ) {
-	if comment.Type == models.CommentTypeComment {
+	if comment.Type == issues_model.CommentTypeComment {
 		if issue.Comments == nil {
 			if err := issue.LoadDiscussComments(); err != nil {
 				log.Error("LoadComments failed: %v", err)
@@ -46,16 +46,16 @@ func (r *indexerNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *
 	}
 }
 
-func (r *indexerNotifier) NotifyNewIssue(issue *models.Issue, mentions []*user_model.User) {
+func (r *indexerNotifier) NotifyNewIssue(issue *issues_model.Issue, mentions []*user_model.User) {
 	issue_indexer.UpdateIssueIndexer(issue)
 }
 
-func (r *indexerNotifier) NotifyNewPullRequest(pr *models.PullRequest, mentions []*user_model.User) {
+func (r *indexerNotifier) NotifyNewPullRequest(pr *issues_model.PullRequest, mentions []*user_model.User) {
 	issue_indexer.UpdateIssueIndexer(pr.Issue)
 }
 
-func (r *indexerNotifier) NotifyUpdateComment(doer *user_model.User, c *models.Comment, oldContent string) {
-	if c.Type == models.CommentTypeComment {
+func (r *indexerNotifier) NotifyUpdateComment(doer *user_model.User, c *issues_model.Comment, oldContent string) {
+	if c.Type == issues_model.CommentTypeComment {
 		var found bool
 		if c.Issue.Comments != nil {
 			for i := 0; i < len(c.Issue.Comments); i++ {
@@ -78,8 +78,8 @@ func (r *indexerNotifier) NotifyUpdateComment(doer *user_model.User, c *models.C
 	}
 }
 
-func (r *indexerNotifier) NotifyDeleteComment(doer *user_model.User, comment *models.Comment) {
-	if comment.Type == models.CommentTypeComment {
+func (r *indexerNotifier) NotifyDeleteComment(doer *user_model.User, comment *issues_model.Comment) {
+	if comment.Type == issues_model.CommentTypeComment {
 		if err := comment.LoadIssue(); err != nil {
 			log.Error("LoadIssue: %v", err)
 			return
@@ -142,14 +142,14 @@ func (r *indexerNotifier) NotifySyncPushCommits(pusher *user_model.User, repo *r
 	}
 }
 
-func (r *indexerNotifier) NotifyIssueChangeContent(doer *user_model.User, issue *models.Issue, oldContent string) {
+func (r *indexerNotifier) NotifyIssueChangeContent(doer *user_model.User, issue *issues_model.Issue, oldContent string) {
 	issue_indexer.UpdateIssueIndexer(issue)
 }
 
-func (r *indexerNotifier) NotifyIssueChangeTitle(doer *user_model.User, issue *models.Issue, oldTitle string) {
+func (r *indexerNotifier) NotifyIssueChangeTitle(doer *user_model.User, issue *issues_model.Issue, oldTitle string) {
 	issue_indexer.UpdateIssueIndexer(issue)
 }
 
-func (r *indexerNotifier) NotifyIssueChangeRef(doer *user_model.User, issue *models.Issue, oldRef string) {
+func (r *indexerNotifier) NotifyIssueChangeRef(doer *user_model.User, issue *issues_model.Issue, oldRef string) {
 	issue_indexer.UpdateIssueIndexer(issue)
 }
diff --git a/modules/notification/mail/mail.go b/modules/notification/mail/mail.go
index 138e438751..1f217304b0 100644
--- a/modules/notification/mail/mail.go
+++ b/modules/notification/mail/mail.go
@@ -8,6 +8,7 @@ import (
 	"fmt"
 
 	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/graceful"
@@ -29,21 +30,21 @@ func NewNotifier() base.Notifier {
 }
 
 func (m *mailNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository,
-	issue *models.Issue, comment *models.Comment, mentions []*user_model.User,
+	issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User,
 ) {
 	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("mailNotifier.NotifyCreateIssueComment Issue[%d] #%d in [%d]", issue.ID, issue.Index, issue.RepoID))
 	defer finished()
 
 	var act models.ActionType
-	if comment.Type == models.CommentTypeClose {
+	if comment.Type == issues_model.CommentTypeClose {
 		act = models.ActionCloseIssue
-	} else if comment.Type == models.CommentTypeReopen {
+	} else if comment.Type == issues_model.CommentTypeReopen {
 		act = models.ActionReopenIssue
-	} else if comment.Type == models.CommentTypeComment {
+	} else if comment.Type == issues_model.CommentTypeComment {
 		act = models.ActionCommentIssue
-	} else if comment.Type == models.CommentTypeCode {
+	} else if comment.Type == issues_model.CommentTypeCode {
 		act = models.ActionCommentIssue
-	} else if comment.Type == models.CommentTypePullRequestPush {
+	} else if comment.Type == issues_model.CommentTypePullRequestPush {
 		act = 0
 	}
 
@@ -52,13 +53,13 @@ func (m *mailNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *rep
 	}
 }
 
-func (m *mailNotifier) NotifyNewIssue(issue *models.Issue, mentions []*user_model.User) {
+func (m *mailNotifier) NotifyNewIssue(issue *issues_model.Issue, mentions []*user_model.User) {
 	if err := mailer.MailParticipants(issue, issue.Poster, models.ActionCreateIssue, mentions); err != nil {
 		log.Error("MailParticipants: %v", err)
 	}
 }
 
-func (m *mailNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *models.Issue, actionComment *models.Comment, isClosed bool) {
+func (m *mailNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *issues_model.Issue, actionComment *issues_model.Comment, isClosed bool) {
 	var actionType models.ActionType
 	if issue.IsPull {
 		if isClosed {
@@ -79,34 +80,34 @@ func (m *mailNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *mod
 	}
 }
 
-func (m *mailNotifier) NotifyIssueChangeTitle(doer *user_model.User, issue *models.Issue, oldTitle string) {
+func (m *mailNotifier) NotifyIssueChangeTitle(doer *user_model.User, issue *issues_model.Issue, oldTitle string) {
 	if err := issue.LoadPullRequest(); err != nil {
 		log.Error("issue.LoadPullRequest: %v", err)
 		return
 	}
-	if issue.IsPull && models.HasWorkInProgressPrefix(oldTitle) && !issue.PullRequest.IsWorkInProgress() {
+	if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issue.PullRequest.IsWorkInProgress() {
 		if err := mailer.MailParticipants(issue, doer, models.ActionPullRequestReadyForReview, nil); err != nil {
 			log.Error("MailParticipants: %v", err)
 		}
 	}
 }
 
-func (m *mailNotifier) NotifyNewPullRequest(pr *models.PullRequest, mentions []*user_model.User) {
+func (m *mailNotifier) NotifyNewPullRequest(pr *issues_model.PullRequest, mentions []*user_model.User) {
 	if err := mailer.MailParticipants(pr.Issue, pr.Issue.Poster, models.ActionCreatePullRequest, mentions); err != nil {
 		log.Error("MailParticipants: %v", err)
 	}
 }
 
-func (m *mailNotifier) NotifyPullRequestReview(pr *models.PullRequest, r *models.Review, comment *models.Comment, mentions []*user_model.User) {
+func (m *mailNotifier) NotifyPullRequestReview(pr *issues_model.PullRequest, r *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) {
 	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("mailNotifier.NotifyPullRequestReview Pull[%d] #%d in [%d]", pr.ID, pr.Index, pr.BaseRepoID))
 	defer finished()
 
 	var act models.ActionType
-	if comment.Type == models.CommentTypeClose {
+	if comment.Type == issues_model.CommentTypeClose {
 		act = models.ActionCloseIssue
-	} else if comment.Type == models.CommentTypeReopen {
+	} else if comment.Type == issues_model.CommentTypeReopen {
 		act = models.ActionReopenIssue
-	} else if comment.Type == models.CommentTypeComment {
+	} else if comment.Type == issues_model.CommentTypeComment {
 		act = models.ActionCommentPull
 	}
 	if err := mailer.MailParticipantsComment(ctx, comment, act, pr.Issue, mentions); err != nil {
@@ -114,7 +115,7 @@ func (m *mailNotifier) NotifyPullRequestReview(pr *models.PullRequest, r *models
 	}
 }
 
-func (m *mailNotifier) NotifyPullRequestCodeComment(pr *models.PullRequest, comment *models.Comment, mentions []*user_model.User) {
+func (m *mailNotifier) NotifyPullRequestCodeComment(pr *issues_model.PullRequest, comment *issues_model.Comment, mentions []*user_model.User) {
 	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("mailNotifier.NotifyPullRequestCodeComment Pull[%d] #%d in [%d]", pr.ID, pr.Index, pr.BaseRepoID))
 	defer finished()
 
@@ -123,7 +124,7 @@ func (m *mailNotifier) NotifyPullRequestCodeComment(pr *models.PullRequest, comm
 	}
 }
 
-func (m *mailNotifier) NotifyIssueChangeAssignee(doer *user_model.User, issue *models.Issue, assignee *user_model.User, removed bool, comment *models.Comment) {
+func (m *mailNotifier) NotifyIssueChangeAssignee(doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) {
 	// mail only sent to added assignees and not self-assignee
 	if !removed && doer.ID != assignee.ID && (assignee.EmailNotifications() == user_model.EmailNotificationsEnabled || assignee.EmailNotifications() == user_model.EmailNotificationsOnMention) {
 		ct := fmt.Sprintf("Assigned #%d.", issue.Index)
@@ -133,7 +134,7 @@ func (m *mailNotifier) NotifyIssueChangeAssignee(doer *user_model.User, issue *m
 	}
 }
 
-func (m *mailNotifier) NotifyPullReviewRequest(doer *user_model.User, issue *models.Issue, reviewer *user_model.User, isRequest bool, comment *models.Comment) {
+func (m *mailNotifier) NotifyPullReviewRequest(doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
 	if isRequest && doer.ID != reviewer.ID && (reviewer.EmailNotifications() == user_model.EmailNotificationsEnabled || reviewer.EmailNotifications() == user_model.EmailNotificationsOnMention) {
 		ct := fmt.Sprintf("Requested to review %s.", issue.HTMLURL())
 		if err := mailer.SendIssueAssignedMail(issue, doer, ct, comment, []*user_model.User{reviewer}); err != nil {
@@ -142,7 +143,7 @@ func (m *mailNotifier) NotifyPullReviewRequest(doer *user_model.User, issue *mod
 	}
 }
 
-func (m *mailNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *user_model.User) {
+func (m *mailNotifier) NotifyMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) {
 	if err := pr.LoadIssue(); err != nil {
 		log.Error("pr.LoadIssue: %v", err)
 		return
@@ -152,7 +153,7 @@ func (m *mailNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *user
 	}
 }
 
-func (m *mailNotifier) NotifyPullRequestPushCommits(doer *user_model.User, pr *models.PullRequest, comment *models.Comment) {
+func (m *mailNotifier) NotifyPullRequestPushCommits(doer *user_model.User, pr *issues_model.PullRequest, comment *issues_model.Comment) {
 	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("mailNotifier.NotifyPullRequestPushCommits Pull[%d] #%d in [%d]", pr.ID, pr.Index, pr.BaseRepoID))
 	defer finished()
 
@@ -179,7 +180,7 @@ func (m *mailNotifier) NotifyPullRequestPushCommits(doer *user_model.User, pr *m
 	m.NotifyCreateIssueComment(doer, comment.Issue.Repo, comment.Issue, comment, nil)
 }
 
-func (m *mailNotifier) NotifyPullRevieweDismiss(doer *user_model.User, review *models.Review, comment *models.Comment) {
+func (m *mailNotifier) NotifyPullRevieweDismiss(doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) {
 	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("mailNotifier.NotifyPullRevieweDismiss Review[%d] in Issue[%d]", review.ID, review.IssueID))
 	defer finished()
 
diff --git a/modules/notification/notification.go b/modules/notification/notification.go
index 90ff87941f..d60a880bec 100644
--- a/modules/notification/notification.go
+++ b/modules/notification/notification.go
@@ -6,6 +6,7 @@ package notification
 
 import (
 	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	packages_model "code.gitea.io/gitea/models/packages"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
@@ -40,7 +41,7 @@ func NewContext() {
 
 // NotifyCreateIssueComment notifies issue comment related message to notifiers
 func NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository,
-	issue *models.Issue, comment *models.Comment, mentions []*user_model.User,
+	issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User,
 ) {
 	for _, notifier := range notifiers {
 		notifier.NotifyCreateIssueComment(doer, repo, issue, comment, mentions)
@@ -48,91 +49,91 @@ func NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository
 }
 
 // NotifyNewIssue notifies new issue to notifiers
-func NotifyNewIssue(issue *models.Issue, mentions []*user_model.User) {
+func NotifyNewIssue(issue *issues_model.Issue, mentions []*user_model.User) {
 	for _, notifier := range notifiers {
 		notifier.NotifyNewIssue(issue, mentions)
 	}
 }
 
 // NotifyIssueChangeStatus notifies close or reopen issue to notifiers
-func NotifyIssueChangeStatus(doer *user_model.User, issue *models.Issue, actionComment *models.Comment, closeOrReopen bool) {
+func NotifyIssueChangeStatus(doer *user_model.User, issue *issues_model.Issue, actionComment *issues_model.Comment, closeOrReopen bool) {
 	for _, notifier := range notifiers {
 		notifier.NotifyIssueChangeStatus(doer, issue, actionComment, closeOrReopen)
 	}
 }
 
 // NotifyDeleteIssue notify when some issue deleted
-func NotifyDeleteIssue(doer *user_model.User, issue *models.Issue) {
+func NotifyDeleteIssue(doer *user_model.User, issue *issues_model.Issue) {
 	for _, notifier := range notifiers {
 		notifier.NotifyDeleteIssue(doer, issue)
 	}
 }
 
 // NotifyMergePullRequest notifies merge pull request to notifiers
-func NotifyMergePullRequest(pr *models.PullRequest, doer *user_model.User) {
+func NotifyMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) {
 	for _, notifier := range notifiers {
 		notifier.NotifyMergePullRequest(pr, doer)
 	}
 }
 
 // NotifyNewPullRequest notifies new pull request to notifiers
-func NotifyNewPullRequest(pr *models.PullRequest, mentions []*user_model.User) {
+func NotifyNewPullRequest(pr *issues_model.PullRequest, mentions []*user_model.User) {
 	for _, notifier := range notifiers {
 		notifier.NotifyNewPullRequest(pr, mentions)
 	}
 }
 
 // NotifyPullRequestSynchronized notifies Synchronized pull request
-func NotifyPullRequestSynchronized(doer *user_model.User, pr *models.PullRequest) {
+func NotifyPullRequestSynchronized(doer *user_model.User, pr *issues_model.PullRequest) {
 	for _, notifier := range notifiers {
 		notifier.NotifyPullRequestSynchronized(doer, pr)
 	}
 }
 
 // NotifyPullRequestReview notifies new pull request review
-func NotifyPullRequestReview(pr *models.PullRequest, review *models.Review, comment *models.Comment, mentions []*user_model.User) {
+func NotifyPullRequestReview(pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) {
 	for _, notifier := range notifiers {
 		notifier.NotifyPullRequestReview(pr, review, comment, mentions)
 	}
 }
 
 // NotifyPullRequestCodeComment notifies new pull request code comment
-func NotifyPullRequestCodeComment(pr *models.PullRequest, comment *models.Comment, mentions []*user_model.User) {
+func NotifyPullRequestCodeComment(pr *issues_model.PullRequest, comment *issues_model.Comment, mentions []*user_model.User) {
 	for _, notifier := range notifiers {
 		notifier.NotifyPullRequestCodeComment(pr, comment, mentions)
 	}
 }
 
 // NotifyPullRequestChangeTargetBranch notifies when a pull request's target branch was changed
-func NotifyPullRequestChangeTargetBranch(doer *user_model.User, pr *models.PullRequest, oldBranch string) {
+func NotifyPullRequestChangeTargetBranch(doer *user_model.User, pr *issues_model.PullRequest, oldBranch string) {
 	for _, notifier := range notifiers {
 		notifier.NotifyPullRequestChangeTargetBranch(doer, pr, oldBranch)
 	}
 }
 
 // NotifyPullRequestPushCommits notifies when push commits to pull request's head branch
-func NotifyPullRequestPushCommits(doer *user_model.User, pr *models.PullRequest, comment *models.Comment) {
+func NotifyPullRequestPushCommits(doer *user_model.User, pr *issues_model.PullRequest, comment *issues_model.Comment) {
 	for _, notifier := range notifiers {
 		notifier.NotifyPullRequestPushCommits(doer, pr, comment)
 	}
 }
 
 // NotifyPullRevieweDismiss notifies when a review was dismissed by repo admin
-func NotifyPullRevieweDismiss(doer *user_model.User, review *models.Review, comment *models.Comment) {
+func NotifyPullRevieweDismiss(doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) {
 	for _, notifier := range notifiers {
 		notifier.NotifyPullRevieweDismiss(doer, review, comment)
 	}
 }
 
 // NotifyUpdateComment notifies update comment to notifiers
-func NotifyUpdateComment(doer *user_model.User, c *models.Comment, oldContent string) {
+func NotifyUpdateComment(doer *user_model.User, c *issues_model.Comment, oldContent string) {
 	for _, notifier := range notifiers {
 		notifier.NotifyUpdateComment(doer, c, oldContent)
 	}
 }
 
 // NotifyDeleteComment notifies delete comment to notifiers
-func NotifyDeleteComment(doer *user_model.User, c *models.Comment) {
+func NotifyDeleteComment(doer *user_model.User, c *issues_model.Comment) {
 	for _, notifier := range notifiers {
 		notifier.NotifyDeleteComment(doer, c)
 	}
@@ -160,57 +161,57 @@ func NotifyDeleteRelease(doer *user_model.User, rel *models.Release) {
 }
 
 // NotifyIssueChangeMilestone notifies change milestone to notifiers
-func NotifyIssueChangeMilestone(doer *user_model.User, issue *models.Issue, oldMilestoneID int64) {
+func NotifyIssueChangeMilestone(doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64) {
 	for _, notifier := range notifiers {
 		notifier.NotifyIssueChangeMilestone(doer, issue, oldMilestoneID)
 	}
 }
 
 // NotifyIssueChangeContent notifies change content to notifiers
-func NotifyIssueChangeContent(doer *user_model.User, issue *models.Issue, oldContent string) {
+func NotifyIssueChangeContent(doer *user_model.User, issue *issues_model.Issue, oldContent string) {
 	for _, notifier := range notifiers {
 		notifier.NotifyIssueChangeContent(doer, issue, oldContent)
 	}
 }
 
 // NotifyIssueChangeAssignee notifies change content to notifiers
-func NotifyIssueChangeAssignee(doer *user_model.User, issue *models.Issue, assignee *user_model.User, removed bool, comment *models.Comment) {
+func NotifyIssueChangeAssignee(doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) {
 	for _, notifier := range notifiers {
 		notifier.NotifyIssueChangeAssignee(doer, issue, assignee, removed, comment)
 	}
 }
 
 // NotifyPullReviewRequest notifies Request Review change
-func NotifyPullReviewRequest(doer *user_model.User, issue *models.Issue, reviewer *user_model.User, isRequest bool, comment *models.Comment) {
+func NotifyPullReviewRequest(doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
 	for _, notifier := range notifiers {
 		notifier.NotifyPullReviewRequest(doer, issue, reviewer, isRequest, comment)
 	}
 }
 
 // NotifyIssueClearLabels notifies clear labels to notifiers
-func NotifyIssueClearLabels(doer *user_model.User, issue *models.Issue) {
+func NotifyIssueClearLabels(doer *user_model.User, issue *issues_model.Issue) {
 	for _, notifier := range notifiers {
 		notifier.NotifyIssueClearLabels(doer, issue)
 	}
 }
 
 // NotifyIssueChangeTitle notifies change title to notifiers
-func NotifyIssueChangeTitle(doer *user_model.User, issue *models.Issue, oldTitle string) {
+func NotifyIssueChangeTitle(doer *user_model.User, issue *issues_model.Issue, oldTitle string) {
 	for _, notifier := range notifiers {
 		notifier.NotifyIssueChangeTitle(doer, issue, oldTitle)
 	}
 }
 
 // NotifyIssueChangeRef notifies change reference to notifiers
-func NotifyIssueChangeRef(doer *user_model.User, issue *models.Issue, oldRef string) {
+func NotifyIssueChangeRef(doer *user_model.User, issue *issues_model.Issue, oldRef string) {
 	for _, notifier := range notifiers {
 		notifier.NotifyIssueChangeRef(doer, issue, oldRef)
 	}
 }
 
 // NotifyIssueChangeLabels notifies change labels to notifiers
-func NotifyIssueChangeLabels(doer *user_model.User, issue *models.Issue,
-	addedLabels, removedLabels []*models.Label,
+func NotifyIssueChangeLabels(doer *user_model.User, issue *issues_model.Issue,
+	addedLabels, removedLabels []*issues_model.Label,
 ) {
 	for _, notifier := range notifiers {
 		notifier.NotifyIssueChangeLabels(doer, issue, addedLabels, removedLabels)
diff --git a/modules/notification/ui/ui.go b/modules/notification/ui/ui.go
index 037167f640..74866a3363 100644
--- a/modules/notification/ui/ui.go
+++ b/modules/notification/ui/ui.go
@@ -7,6 +7,7 @@ package ui
 import (
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/graceful"
@@ -53,7 +54,7 @@ func (ns *notificationService) Run() {
 }
 
 func (ns *notificationService) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository,
-	issue *models.Issue, comment *models.Comment, mentions []*user_model.User,
+	issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User,
 ) {
 	opts := issueNotificationOpts{
 		IssueID:              issue.ID,
@@ -76,7 +77,7 @@ func (ns *notificationService) NotifyCreateIssueComment(doer *user_model.User, r
 	}
 }
 
-func (ns *notificationService) NotifyNewIssue(issue *models.Issue, mentions []*user_model.User) {
+func (ns *notificationService) NotifyNewIssue(issue *issues_model.Issue, mentions []*user_model.User) {
 	_ = ns.issueQueue.Push(issueNotificationOpts{
 		IssueID:              issue.ID,
 		NotificationAuthorID: issue.Poster.ID,
@@ -90,19 +91,19 @@ func (ns *notificationService) NotifyNewIssue(issue *models.Issue, mentions []*u
 	}
 }
 
-func (ns *notificationService) NotifyIssueChangeStatus(doer *user_model.User, issue *models.Issue, actionComment *models.Comment, isClosed bool) {
+func (ns *notificationService) NotifyIssueChangeStatus(doer *user_model.User, issue *issues_model.Issue, actionComment *issues_model.Comment, isClosed bool) {
 	_ = ns.issueQueue.Push(issueNotificationOpts{
 		IssueID:              issue.ID,
 		NotificationAuthorID: doer.ID,
 	})
 }
 
-func (ns *notificationService) NotifyIssueChangeTitle(doer *user_model.User, issue *models.Issue, oldTitle string) {
+func (ns *notificationService) NotifyIssueChangeTitle(doer *user_model.User, issue *issues_model.Issue, oldTitle string) {
 	if err := issue.LoadPullRequest(); err != nil {
 		log.Error("issue.LoadPullRequest: %v", err)
 		return
 	}
-	if issue.IsPull && models.HasWorkInProgressPrefix(oldTitle) && !issue.PullRequest.IsWorkInProgress() {
+	if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issue.PullRequest.IsWorkInProgress() {
 		_ = ns.issueQueue.Push(issueNotificationOpts{
 			IssueID:              issue.ID,
 			NotificationAuthorID: doer.ID,
@@ -110,14 +111,14 @@ func (ns *notificationService) NotifyIssueChangeTitle(doer *user_model.User, iss
 	}
 }
 
-func (ns *notificationService) NotifyMergePullRequest(pr *models.PullRequest, doer *user_model.User) {
+func (ns *notificationService) NotifyMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) {
 	_ = ns.issueQueue.Push(issueNotificationOpts{
 		IssueID:              pr.Issue.ID,
 		NotificationAuthorID: doer.ID,
 	})
 }
 
-func (ns *notificationService) NotifyNewPullRequest(pr *models.PullRequest, mentions []*user_model.User) {
+func (ns *notificationService) NotifyNewPullRequest(pr *issues_model.PullRequest, mentions []*user_model.User) {
 	if err := pr.LoadIssue(); err != nil {
 		log.Error("Unable to load issue: %d for pr: %d: Error: %v", pr.IssueID, pr.ID, err)
 		return
@@ -131,7 +132,7 @@ func (ns *notificationService) NotifyNewPullRequest(pr *models.PullRequest, ment
 	for _, id := range repoWatchers {
 		toNotify[id] = struct{}{}
 	}
-	issueParticipants, err := models.GetParticipantsIDsByIssueID(pr.IssueID)
+	issueParticipants, err := issues_model.GetParticipantsIDsByIssueID(pr.IssueID)
 	if err != nil {
 		log.Error("GetParticipantsIDsByIssueID: %v", err)
 		return
@@ -152,7 +153,7 @@ func (ns *notificationService) NotifyNewPullRequest(pr *models.PullRequest, ment
 	}
 }
 
-func (ns *notificationService) NotifyPullRequestReview(pr *models.PullRequest, r *models.Review, c *models.Comment, mentions []*user_model.User) {
+func (ns *notificationService) NotifyPullRequestReview(pr *issues_model.PullRequest, r *issues_model.Review, c *issues_model.Comment, mentions []*user_model.User) {
 	opts := issueNotificationOpts{
 		IssueID:              pr.Issue.ID,
 		NotificationAuthorID: r.Reviewer.ID,
@@ -174,7 +175,7 @@ func (ns *notificationService) NotifyPullRequestReview(pr *models.PullRequest, r
 	}
 }
 
-func (ns *notificationService) NotifyPullRequestCodeComment(pr *models.PullRequest, c *models.Comment, mentions []*user_model.User) {
+func (ns *notificationService) NotifyPullRequestCodeComment(pr *issues_model.PullRequest, c *issues_model.Comment, mentions []*user_model.User) {
 	for _, mention := range mentions {
 		_ = ns.issueQueue.Push(issueNotificationOpts{
 			IssueID:              pr.Issue.ID,
@@ -185,7 +186,7 @@ func (ns *notificationService) NotifyPullRequestCodeComment(pr *models.PullReque
 	}
 }
 
-func (ns *notificationService) NotifyPullRequestPushCommits(doer *user_model.User, pr *models.PullRequest, comment *models.Comment) {
+func (ns *notificationService) NotifyPullRequestPushCommits(doer *user_model.User, pr *issues_model.PullRequest, comment *issues_model.Comment) {
 	opts := issueNotificationOpts{
 		IssueID:              pr.IssueID,
 		NotificationAuthorID: doer.ID,
@@ -194,7 +195,7 @@ func (ns *notificationService) NotifyPullRequestPushCommits(doer *user_model.Use
 	_ = ns.issueQueue.Push(opts)
 }
 
-func (ns *notificationService) NotifyPullRevieweDismiss(doer *user_model.User, review *models.Review, comment *models.Comment) {
+func (ns *notificationService) NotifyPullRevieweDismiss(doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) {
 	opts := issueNotificationOpts{
 		IssueID:              review.IssueID,
 		NotificationAuthorID: doer.ID,
@@ -203,7 +204,7 @@ func (ns *notificationService) NotifyPullRevieweDismiss(doer *user_model.User, r
 	_ = ns.issueQueue.Push(opts)
 }
 
-func (ns *notificationService) NotifyIssueChangeAssignee(doer *user_model.User, issue *models.Issue, assignee *user_model.User, removed bool, comment *models.Comment) {
+func (ns *notificationService) NotifyIssueChangeAssignee(doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) {
 	if !removed && doer.ID != assignee.ID {
 		opts := issueNotificationOpts{
 			IssueID:              issue.ID,
@@ -219,7 +220,7 @@ func (ns *notificationService) NotifyIssueChangeAssignee(doer *user_model.User,
 	}
 }
 
-func (ns *notificationService) NotifyPullReviewRequest(doer *user_model.User, issue *models.Issue, reviewer *user_model.User, isRequest bool, comment *models.Comment) {
+func (ns *notificationService) NotifyPullReviewRequest(doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
 	if isRequest {
 		opts := issueNotificationOpts{
 			IssueID:              issue.ID,
diff --git a/modules/notification/webhook/webhook.go b/modules/notification/webhook/webhook.go
index 38077f2180..be71d18fda 100644
--- a/modules/notification/webhook/webhook.go
+++ b/modules/notification/webhook/webhook.go
@@ -9,6 +9,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	packages_model "code.gitea.io/gitea/models/packages"
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
@@ -39,7 +40,7 @@ func NewNotifier() base.Notifier {
 	return &webhookNotifier{}
 }
 
-func (m *webhookNotifier) NotifyIssueClearLabels(doer *user_model.User, issue *models.Issue) {
+func (m *webhookNotifier) NotifyIssueClearLabels(doer *user_model.User, issue *issues_model.Issue) {
 	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyIssueClearLabels User: %s[%d] Issue[%d] #%d in [%d]", doer.Name, doer.ID, issue.ID, issue.Index, issue.RepoID))
 	defer finished()
 
@@ -147,7 +148,7 @@ func (m *webhookNotifier) NotifyMigrateRepository(doer, u *user_model.User, repo
 	}
 }
 
-func (m *webhookNotifier) NotifyIssueChangeAssignee(doer *user_model.User, issue *models.Issue, assignee *user_model.User, removed bool, comment *models.Comment) {
+func (m *webhookNotifier) NotifyIssueChangeAssignee(doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) {
 	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyIssueChangeAssignee User: %s[%d] Issue[%d] #%d in [%d] Assignee %s[%d] removed: %t", doer.Name, doer.ID, issue.ID, issue.Index, issue.RepoID, assignee.Name, assignee.ID, removed))
 	defer finished()
 
@@ -196,7 +197,7 @@ func (m *webhookNotifier) NotifyIssueChangeAssignee(doer *user_model.User, issue
 	}
 }
 
-func (m *webhookNotifier) NotifyIssueChangeTitle(doer *user_model.User, issue *models.Issue, oldTitle string) {
+func (m *webhookNotifier) NotifyIssueChangeTitle(doer *user_model.User, issue *issues_model.Issue, oldTitle string) {
 	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyIssueChangeTitle User: %s[%d] Issue[%d] #%d in [%d]", doer.Name, doer.ID, issue.ID, issue.Index, issue.RepoID))
 	defer finished()
 
@@ -240,7 +241,7 @@ func (m *webhookNotifier) NotifyIssueChangeTitle(doer *user_model.User, issue *m
 	}
 }
 
-func (m *webhookNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *models.Issue, actionComment *models.Comment, isClosed bool) {
+func (m *webhookNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *issues_model.Issue, actionComment *issues_model.Comment, isClosed bool) {
 	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyIssueChangeStatus User: %s[%d] Issue[%d] #%d in [%d]", doer.Name, doer.ID, issue.ID, issue.Index, issue.RepoID))
 	defer finished()
 
@@ -283,7 +284,7 @@ func (m *webhookNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *
 	}
 }
 
-func (m *webhookNotifier) NotifyNewIssue(issue *models.Issue, mentions []*user_model.User) {
+func (m *webhookNotifier) NotifyNewIssue(issue *issues_model.Issue, mentions []*user_model.User) {
 	if err := issue.LoadRepo(db.DefaultContext); err != nil {
 		log.Error("issue.LoadRepo: %v", err)
 		return
@@ -305,7 +306,7 @@ func (m *webhookNotifier) NotifyNewIssue(issue *models.Issue, mentions []*user_m
 	}
 }
 
-func (m *webhookNotifier) NotifyNewPullRequest(pull *models.PullRequest, mentions []*user_model.User) {
+func (m *webhookNotifier) NotifyNewPullRequest(pull *issues_model.PullRequest, mentions []*user_model.User) {
 	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyNewPullRequest Pull[%d] #%d in [%d]", pull.ID, pull.Index, pull.BaseRepoID))
 	defer finished()
 
@@ -334,7 +335,7 @@ func (m *webhookNotifier) NotifyNewPullRequest(pull *models.PullRequest, mention
 	}
 }
 
-func (m *webhookNotifier) NotifyIssueChangeContent(doer *user_model.User, issue *models.Issue, oldContent string) {
+func (m *webhookNotifier) NotifyIssueChangeContent(doer *user_model.User, issue *issues_model.Issue, oldContent string) {
 	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyIssueChangeContent User: %s[%d] Issue[%d] #%d in [%d]", doer.Name, doer.ID, issue.ID, issue.Index, issue.RepoID))
 	defer finished()
 
@@ -373,7 +374,7 @@ func (m *webhookNotifier) NotifyIssueChangeContent(doer *user_model.User, issue
 	}
 }
 
-func (m *webhookNotifier) NotifyUpdateComment(doer *user_model.User, c *models.Comment, oldContent string) {
+func (m *webhookNotifier) NotifyUpdateComment(doer *user_model.User, c *issues_model.Comment, oldContent string) {
 	var err error
 
 	if err = c.LoadPoster(); err != nil {
@@ -385,7 +386,7 @@ func (m *webhookNotifier) NotifyUpdateComment(doer *user_model.User, c *models.C
 		return
 	}
 
-	if err = c.Issue.LoadAttributes(); err != nil {
+	if err = c.Issue.LoadAttributes(db.DefaultContext); err != nil {
 		log.Error("LoadAttributes: %v", err)
 		return
 	}
@@ -427,7 +428,7 @@ func (m *webhookNotifier) NotifyUpdateComment(doer *user_model.User, c *models.C
 }
 
 func (m *webhookNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository,
-	issue *models.Issue, comment *models.Comment, mentions []*user_model.User,
+	issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User,
 ) {
 	mode, _ := access_model.AccessLevel(doer, repo)
 
@@ -457,7 +458,7 @@ func (m *webhookNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *
 	}
 }
 
-func (m *webhookNotifier) NotifyDeleteComment(doer *user_model.User, comment *models.Comment) {
+func (m *webhookNotifier) NotifyDeleteComment(doer *user_model.User, comment *issues_model.Comment) {
 	var err error
 
 	if err = comment.LoadPoster(); err != nil {
@@ -469,7 +470,7 @@ func (m *webhookNotifier) NotifyDeleteComment(doer *user_model.User, comment *mo
 		return
 	}
 
-	if err = comment.Issue.LoadAttributes(); err != nil {
+	if err = comment.Issue.LoadAttributes(db.DefaultContext); err != nil {
 		log.Error("LoadAttributes: %v", err)
 		return
 	}
@@ -501,8 +502,8 @@ func (m *webhookNotifier) NotifyDeleteComment(doer *user_model.User, comment *mo
 	}
 }
 
-func (m *webhookNotifier) NotifyIssueChangeLabels(doer *user_model.User, issue *models.Issue,
-	addedLabels, removedLabels []*models.Label,
+func (m *webhookNotifier) NotifyIssueChangeLabels(doer *user_model.User, issue *issues_model.Issue,
+	addedLabels, removedLabels []*issues_model.Label,
 ) {
 	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyIssueChangeLabels User: %s[%d] Issue[%d] #%d in [%d]", doer.Name, doer.ID, issue.ID, issue.Index, issue.RepoID))
 	defer finished()
@@ -550,7 +551,7 @@ func (m *webhookNotifier) NotifyIssueChangeLabels(doer *user_model.User, issue *
 	}
 }
 
-func (m *webhookNotifier) NotifyIssueChangeMilestone(doer *user_model.User, issue *models.Issue, oldMilestoneID int64) {
+func (m *webhookNotifier) NotifyIssueChangeMilestone(doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64) {
 	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyIssueChangeMilestone User: %s[%d] Issue[%d] #%d in [%d]", doer.Name, doer.ID, issue.ID, issue.Index, issue.RepoID))
 	defer finished()
 
@@ -562,7 +563,7 @@ func (m *webhookNotifier) NotifyIssueChangeMilestone(doer *user_model.User, issu
 		hookAction = api.HookIssueDemilestoned
 	}
 
-	if err = issue.LoadAttributes(); err != nil {
+	if err = issue.LoadAttributes(db.DefaultContext); err != nil {
 		log.Error("issue.LoadAttributes failed: %v", err)
 		return
 	}
@@ -621,7 +622,7 @@ func (m *webhookNotifier) NotifyPushCommits(pusher *user_model.User, repo *repo_
 	}
 }
 
-func (*webhookNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *user_model.User) {
+func (*webhookNotifier) NotifyMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) {
 	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyMergePullRequest Pull[%d] #%d in [%d]", pr.ID, pr.Index, pr.BaseRepoID))
 	defer finished()
 
@@ -662,7 +663,7 @@ func (*webhookNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *use
 	}
 }
 
-func (m *webhookNotifier) NotifyPullRequestChangeTargetBranch(doer *user_model.User, pr *models.PullRequest, oldBranch string) {
+func (m *webhookNotifier) NotifyPullRequestChangeTargetBranch(doer *user_model.User, pr *issues_model.PullRequest, oldBranch string) {
 	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyPullRequestChangeTargetBranch Pull[%d] #%d in [%d]", pr.ID, pr.Index, pr.BaseRepoID))
 	defer finished()
 
@@ -696,18 +697,18 @@ func (m *webhookNotifier) NotifyPullRequestChangeTargetBranch(doer *user_model.U
 	}
 }
 
-func (m *webhookNotifier) NotifyPullRequestReview(pr *models.PullRequest, review *models.Review, comment *models.Comment, mentions []*user_model.User) {
+func (m *webhookNotifier) NotifyPullRequestReview(pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) {
 	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyPullRequestReview Pull[%d] #%d in [%d]", pr.ID, pr.Index, pr.BaseRepoID))
 	defer finished()
 
 	var reviewHookType webhook.HookEventType
 
 	switch review.Type {
-	case models.ReviewTypeApprove:
+	case issues_model.ReviewTypeApprove:
 		reviewHookType = webhook.HookEventPullRequestReviewApproved
-	case models.ReviewTypeComment:
+	case issues_model.ReviewTypeComment:
 		reviewHookType = webhook.HookEventPullRequestComment
-	case models.ReviewTypeReject:
+	case issues_model.ReviewTypeReject:
 		reviewHookType = webhook.HookEventPullRequestReviewRejected
 	default:
 		// unsupported review webhook type here
@@ -756,7 +757,7 @@ func (m *webhookNotifier) NotifyCreateRef(pusher *user_model.User, repo *repo_mo
 	}
 }
 
-func (m *webhookNotifier) NotifyPullRequestSynchronized(doer *user_model.User, pr *models.PullRequest) {
+func (m *webhookNotifier) NotifyPullRequestSynchronized(doer *user_model.User, pr *issues_model.PullRequest) {
 	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyPullRequestSynchronized Pull[%d] #%d in [%d]", pr.ID, pr.Index, pr.BaseRepoID))
 	defer finished()
 
@@ -764,7 +765,7 @@ func (m *webhookNotifier) NotifyPullRequestSynchronized(doer *user_model.User, p
 		log.Error("pr.LoadIssue: %v", err)
 		return
 	}
-	if err := pr.Issue.LoadAttributes(); err != nil {
+	if err := pr.Issue.LoadAttributes(db.DefaultContext); err != nil {
 		log.Error("LoadAttributes: %v", err)
 		return
 	}
diff --git a/modules/repository/init.go b/modules/repository/init.go
index 285fe81dbe..f5cef3301d 100644
--- a/modules/repository/init.go
+++ b/modules/repository/init.go
@@ -16,6 +16,7 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
@@ -113,7 +114,7 @@ func GetLabelTemplateFile(name string) ([][3]string, error) {
 		if len(color) == 6 {
 			color = "#" + color
 		}
-		if !models.LabelColorPattern.MatchString(color) {
+		if !issues_model.LabelColorPattern.MatchString(color) {
 			return nil, ErrIssueLabelTemplateLoad{name, fmt.Errorf("bad HTML color code in line: %s", line)}
 		}
 
@@ -453,9 +454,9 @@ func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg
 		return err
 	}
 
-	labels := make([]*models.Label, len(list))
+	labels := make([]*issues_model.Label, len(list))
 	for i := 0; i < len(list); i++ {
-		labels[i] = &models.Label{
+		labels[i] = &issues_model.Label{
 			Name:        list[i][0],
 			Description: list[i][2],
 			Color:       list[i][1],
@@ -467,7 +468,7 @@ func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg
 		}
 	}
 	for _, label := range labels {
-		if err = models.NewLabel(ctx, label); err != nil {
+		if err = issues_model.NewLabel(ctx, label); err != nil {
 			return err
 		}
 	}
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index c0be5c1fa5..b77928fc9c 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -26,6 +26,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/avatars"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
@@ -374,7 +375,7 @@ func NewFuncMap() []template.FuncMap {
 			// the table is NOT sorted with this header
 			return ""
 		},
-		"RenderLabels": func(labels []*models.Label) template.HTML {
+		"RenderLabels": func(labels []*issues_model.Label) template.HTML {
 			html := `<span class="labels-list">`
 			for _, label := range labels {
 				// Protect against nil value in labels - shouldn't happen but would cause a panic if so
diff --git a/routers/api/v1/misc/nodeinfo.go b/routers/api/v1/misc/nodeinfo.go
index c786544e14..bd629b87ca 100644
--- a/routers/api/v1/misc/nodeinfo.go
+++ b/routers/api/v1/misc/nodeinfo.go
@@ -8,7 +8,7 @@ import (
 	"net/http"
 	"time"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
@@ -42,8 +42,8 @@ func NodeInfo(ctx *context.APIContext) {
 			usersActiveMonth := int(user_model.CountUsers(&user_model.CountUserFilter{LastLoginSince: &timeOneMonthAgo}))
 			usersActiveHalfyear := int(user_model.CountUsers(&user_model.CountUserFilter{LastLoginSince: &timeHaveYearAgo}))
 
-			allIssues, _ := models.CountIssues(&models.IssuesOptions{})
-			allComments, _ := models.CountComments(&models.FindCommentsOptions{})
+			allIssues, _ := issues_model.CountIssues(&issues_model.IssuesOptions{})
+			allComments, _ := issues_model.CountComments(&issues_model.FindCommentsOptions{})
 
 			nodeInfoUsage = structs.NodeInfoUsage{
 				Users: structs.NodeInfoUsageUsers{
diff --git a/routers/api/v1/notify/threads.go b/routers/api/v1/notify/threads.go
index 4effd6b3e0..7d8d34504f 100644
--- a/routers/api/v1/notify/threads.go
+++ b/routers/api/v1/notify/threads.go
@@ -10,6 +10,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/convert"
 )
@@ -41,7 +42,7 @@ func GetThread(ctx *context.APIContext) {
 	if n == nil {
 		return
 	}
-	if err := n.LoadAttributes(); err != nil && !models.IsErrCommentNotExist(err) {
+	if err := n.LoadAttributes(); err != nil && !issues_model.IsErrCommentNotExist(err) {
 		ctx.InternalServerError(err)
 		return
 	}
@@ -93,7 +94,7 @@ func ReadThread(ctx *context.APIContext) {
 		ctx.InternalServerError(err)
 		return
 	}
-	if err = notif.LoadAttributes(); err != nil && !models.IsErrCommentNotExist(err) {
+	if err = notif.LoadAttributes(); err != nil && !issues_model.IsErrCommentNotExist(err) {
 		ctx.InternalServerError(err)
 		return
 	}
diff --git a/routers/api/v1/org/label.go b/routers/api/v1/org/label.go
index 9844ea21d2..a67bd56dfc 100644
--- a/routers/api/v1/org/label.go
+++ b/routers/api/v1/org/label.go
@@ -10,7 +10,7 @@ import (
 	"strconv"
 	"strings"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/convert"
 	api "code.gitea.io/gitea/modules/structs"
@@ -43,13 +43,13 @@ func ListLabels(ctx *context.APIContext) {
 	//   "200":
 	//     "$ref": "#/responses/LabelList"
 
-	labels, err := models.GetLabelsByOrgID(ctx, ctx.Org.Organization.ID, ctx.FormString("sort"), utils.GetListOptions(ctx))
+	labels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Org.Organization.ID, ctx.FormString("sort"), utils.GetListOptions(ctx))
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "GetLabelsByOrgID", err)
 		return
 	}
 
-	count, err := models.CountLabelsByOrgID(ctx.Org.Organization.ID)
+	count, err := issues_model.CountLabelsByOrgID(ctx.Org.Organization.ID)
 	if err != nil {
 		ctx.InternalServerError(err)
 		return
@@ -88,18 +88,18 @@ func CreateLabel(ctx *context.APIContext) {
 	if len(form.Color) == 6 {
 		form.Color = "#" + form.Color
 	}
-	if !models.LabelColorPattern.MatchString(form.Color) {
+	if !issues_model.LabelColorPattern.MatchString(form.Color) {
 		ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", form.Color))
 		return
 	}
 
-	label := &models.Label{
+	label := &issues_model.Label{
 		Name:        form.Name,
 		Color:       form.Color,
 		OrgID:       ctx.Org.Organization.ID,
 		Description: form.Description,
 	}
-	if err := models.NewLabel(ctx, label); err != nil {
+	if err := issues_model.NewLabel(ctx, label); err != nil {
 		ctx.Error(http.StatusInternalServerError, "NewLabel", err)
 		return
 	}
@@ -131,17 +131,17 @@ func GetLabel(ctx *context.APIContext) {
 	//     "$ref": "#/responses/Label"
 
 	var (
-		label *models.Label
+		label *issues_model.Label
 		err   error
 	)
 	strID := ctx.Params(":id")
 	if intID, err2 := strconv.ParseInt(strID, 10, 64); err2 != nil {
-		label, err = models.GetLabelInOrgByName(ctx, ctx.Org.Organization.ID, strID)
+		label, err = issues_model.GetLabelInOrgByName(ctx, ctx.Org.Organization.ID, strID)
 	} else {
-		label, err = models.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, intID)
+		label, err = issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, intID)
 	}
 	if err != nil {
-		if models.IsErrOrgLabelNotExist(err) {
+		if issues_model.IsErrOrgLabelNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetLabelByOrgID", err)
@@ -183,9 +183,9 @@ func EditLabel(ctx *context.APIContext) {
 	//   "422":
 	//     "$ref": "#/responses/validationError"
 	form := web.GetForm(ctx).(*api.EditLabelOption)
-	label, err := models.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, ctx.ParamsInt64(":id"))
+	label, err := issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if models.IsErrOrgLabelNotExist(err) {
+		if issues_model.IsErrOrgLabelNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetLabelByRepoID", err)
@@ -201,7 +201,7 @@ func EditLabel(ctx *context.APIContext) {
 		if len(label.Color) == 6 {
 			label.Color = "#" + label.Color
 		}
-		if !models.LabelColorPattern.MatchString(label.Color) {
+		if !issues_model.LabelColorPattern.MatchString(label.Color) {
 			ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", label.Color))
 			return
 		}
@@ -209,7 +209,7 @@ func EditLabel(ctx *context.APIContext) {
 	if form.Description != nil {
 		label.Description = *form.Description
 	}
-	if err := models.UpdateLabel(label); err != nil {
+	if err := issues_model.UpdateLabel(label); err != nil {
 		ctx.Error(http.StatusInternalServerError, "UpdateLabel", err)
 		return
 	}
@@ -238,7 +238,7 @@ func DeleteLabel(ctx *context.APIContext) {
 	//   "204":
 	//     "$ref": "#/responses/empty"
 
-	if err := models.DeleteLabel(ctx.Org.Organization.ID, ctx.ParamsInt64(":id")); err != nil {
+	if err := issues_model.DeleteLabel(ctx.Org.Organization.ID, ctx.ParamsInt64(":id")); err != nil {
 		ctx.Error(http.StatusInternalServerError, "DeleteLabel", err)
 		return
 	}
diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go
index ffa3ddc784..2190094bac 100644
--- a/routers/api/v1/repo/file.go
+++ b/routers/api/v1/repo/file.go
@@ -557,7 +557,7 @@ func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) {
 // Called from both CreateFile or UpdateFile to handle both
 func createOrUpdateFile(ctx *context.APIContext, opts *files_service.UpdateRepoFileOptions) (*api.FileResponse, error) {
 	if !canWriteFiles(ctx, opts.OldBranch) {
-		return nil, models.ErrUserDoesNotHaveAccessToRepo{
+		return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{
 			UserID:   ctx.Doer.ID,
 			RepoName: ctx.Repo.Repository.LowerName,
 		}
@@ -614,7 +614,7 @@ func DeleteFile(ctx *context.APIContext) {
 
 	apiOpts := web.GetForm(ctx).(*api.DeleteFileOptions)
 	if !canWriteFiles(ctx, apiOpts.BranchName) {
-		ctx.Error(http.StatusForbidden, "DeleteFile", models.ErrUserDoesNotHaveAccessToRepo{
+		ctx.Error(http.StatusForbidden, "DeleteFile", repo_model.ErrUserDoesNotHaveAccessToRepo{
 			UserID:   ctx.Doer.ID,
 			RepoName: ctx.Repo.Repository.LowerName,
 		})
@@ -712,7 +712,7 @@ func GetContents(ctx *context.APIContext) {
 	//     "$ref": "#/responses/notFound"
 
 	if !canReadFiles(ctx.Repo) {
-		ctx.Error(http.StatusInternalServerError, "GetContentsOrList", models.ErrUserDoesNotHaveAccessToRepo{
+		ctx.Error(http.StatusInternalServerError, "GetContentsOrList", repo_model.ErrUserDoesNotHaveAccessToRepo{
 			UserID:   ctx.Doer.ID,
 			RepoName: ctx.Repo.Repository.LowerName,
 		})
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index c394ad1756..ddad18ef62 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -12,7 +12,6 @@ import (
 	"strings"
 	"time"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
@@ -184,7 +183,7 @@ func SearchIssues(ctx *context.APIContext) {
 		return
 	}
 
-	var issues []*models.Issue
+	var issues []*issues_model.Issue
 	var filteredCount int64
 
 	keyword := ctx.FormTrim("q")
@@ -233,7 +232,7 @@ func SearchIssues(ctx *context.APIContext) {
 	// Only fetch the issues if we either don't have a keyword or the search returned issues
 	// This would otherwise return all issues if no issues were found by the search.
 	if len(keyword) == 0 || len(issueIDs) > 0 || len(includedLabelNames) > 0 || len(includedMilestones) > 0 {
-		issuesOpt := &models.IssuesOptions{
+		issuesOpt := &issues_model.IssuesOptions{
 			ListOptions: db.ListOptions{
 				Page:     ctx.FormInt("page"),
 				PageSize: limit,
@@ -269,7 +268,7 @@ func SearchIssues(ctx *context.APIContext) {
 			issuesOpt.ReviewRequestedID = ctxUserID
 		}
 
-		if issues, err = models.Issues(issuesOpt); err != nil {
+		if issues, err = issues_model.Issues(issuesOpt); err != nil {
 			ctx.Error(http.StatusInternalServerError, "Issues", err)
 			return
 		}
@@ -277,7 +276,7 @@ func SearchIssues(ctx *context.APIContext) {
 		issuesOpt.ListOptions = db.ListOptions{
 			Page: -1,
 		}
-		if filteredCount, err = models.CountIssues(issuesOpt); err != nil {
+		if filteredCount, err = issues_model.CountIssues(issuesOpt); err != nil {
 			ctx.Error(http.StatusInternalServerError, "CountIssues", err)
 			return
 		}
@@ -379,7 +378,7 @@ func ListIssues(ctx *context.APIContext) {
 		isClosed = util.OptionalBoolFalse
 	}
 
-	var issues []*models.Issue
+	var issues []*issues_model.Issue
 	var filteredCount int64
 
 	keyword := ctx.FormTrim("q")
@@ -397,7 +396,7 @@ func ListIssues(ctx *context.APIContext) {
 	}
 
 	if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
-		labelIDs, err = models.GetLabelIDsInRepoByNames(ctx.Repo.Repository.ID, splitted)
+		labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx.Repo.Repository.ID, splitted)
 		if err != nil {
 			ctx.Error(http.StatusInternalServerError, "GetLabelIDsInRepoByNames", err)
 			return
@@ -463,7 +462,7 @@ func ListIssues(ctx *context.APIContext) {
 	// Only fetch the issues if we either don't have a keyword or the search returned issues
 	// This would otherwise return all issues if no issues were found by the search.
 	if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
-		issuesOpt := &models.IssuesOptions{
+		issuesOpt := &issues_model.IssuesOptions{
 			ListOptions:       listOptions,
 			RepoID:            ctx.Repo.Repository.ID,
 			IsClosed:          isClosed,
@@ -478,7 +477,7 @@ func ListIssues(ctx *context.APIContext) {
 			MentionedID:       mentionedByID,
 		}
 
-		if issues, err = models.Issues(issuesOpt); err != nil {
+		if issues, err = issues_model.Issues(issuesOpt); err != nil {
 			ctx.Error(http.StatusInternalServerError, "Issues", err)
 			return
 		}
@@ -486,7 +485,7 @@ func ListIssues(ctx *context.APIContext) {
 		issuesOpt.ListOptions = db.ListOptions{
 			Page: -1,
 		}
-		if filteredCount, err = models.CountIssues(issuesOpt); err != nil {
+		if filteredCount, err = issues_model.CountIssues(issuesOpt); err != nil {
 			ctx.Error(http.StatusInternalServerError, "CountIssues", err)
 			return
 		}
@@ -547,9 +546,9 @@ func GetIssue(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	issue, err := models.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, err := issues_model.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -598,7 +597,7 @@ func CreateIssue(ctx *context.APIContext) {
 		deadlineUnix = timeutil.TimeStamp(form.Deadline.Unix())
 	}
 
-	issue := &models.Issue{
+	issue := &issues_model.Issue{
 		RepoID:       ctx.Repo.Repository.ID,
 		Repo:         ctx.Repo.Repository,
 		Title:        form.Title,
@@ -613,7 +612,7 @@ func CreateIssue(ctx *context.APIContext) {
 	var err error
 	if ctx.Repo.CanWrite(unit.TypeIssues) {
 		issue.MilestoneID = form.Milestone
-		assigneeIDs, err = models.MakeIDsFromAPIAssigneesToAdd(form.Assignee, form.Assignees)
+		assigneeIDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd(form.Assignee, form.Assignees)
 		if err != nil {
 			if user_model.IsErrUserNotExist(err) {
 				ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
@@ -637,7 +636,7 @@ func CreateIssue(ctx *context.APIContext) {
 				return
 			}
 			if !valid {
-				ctx.Error(http.StatusUnprocessableEntity, "canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: ctx.Repo.Repository.Name})
+				ctx.Error(http.StatusUnprocessableEntity, "canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: ctx.Repo.Repository.Name})
 				return
 			}
 		}
@@ -647,7 +646,7 @@ func CreateIssue(ctx *context.APIContext) {
 	}
 
 	if err := issue_service.NewIssue(ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil {
-		if models.IsErrUserDoesNotHaveAccessToRepo(err) {
+		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
 			return
 		}
@@ -657,7 +656,7 @@ func CreateIssue(ctx *context.APIContext) {
 
 	if form.Closed {
 		if err := issue_service.ChangeStatus(issue, ctx.Doer, true); err != nil {
-			if models.IsErrDependenciesLeft(err) {
+			if issues_model.IsErrDependenciesLeft(err) {
 				ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
 				return
 			}
@@ -667,7 +666,7 @@ func CreateIssue(ctx *context.APIContext) {
 	}
 
 	// Refetch from database to assign some automatic values
-	issue, err = models.GetIssueByID(issue.ID)
+	issue, err = issues_model.GetIssueByID(ctx, issue.ID)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "GetIssueByID", err)
 		return
@@ -716,9 +715,9 @@ func EditIssue(ctx *context.APIContext) {
 	//     "$ref": "#/responses/error"
 
 	form := web.GetForm(ctx).(*api.EditIssueOption)
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -728,7 +727,7 @@ func EditIssue(ctx *context.APIContext) {
 	issue.Repo = ctx.Repo.Repository
 	canWrite := ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
 
-	err = issue.LoadAttributes()
+	err = issue.LoadAttributes(ctx)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
 		return
@@ -764,7 +763,7 @@ func EditIssue(ctx *context.APIContext) {
 			deadlineUnix = timeutil.TimeStamp(deadline.Unix())
 		}
 
-		if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil {
+		if err := issues_model.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil {
 			ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
 			return
 		}
@@ -813,9 +812,9 @@ func EditIssue(ctx *context.APIContext) {
 		}
 		issue.IsClosed = api.StateClosed == api.StateType(*form.State)
 	}
-	statusChangeComment, titleChanged, err := models.UpdateIssueByAPI(issue, ctx.Doer)
+	statusChangeComment, titleChanged, err := issues_model.UpdateIssueByAPI(issue, ctx.Doer)
 	if err != nil {
-		if models.IsErrDependenciesLeft(err) {
+		if issues_model.IsErrDependenciesLeft(err) {
 			ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
 			return
 		}
@@ -832,7 +831,7 @@ func EditIssue(ctx *context.APIContext) {
 	}
 
 	// Refetch from database to assign some automatic values
-	issue, err = models.GetIssueByID(issue.ID)
+	issue, err = issues_model.GetIssueByID(ctx, issue.ID)
 	if err != nil {
 		ctx.InternalServerError(err)
 		return
@@ -872,9 +871,9 @@ func DeleteIssue(ctx *context.APIContext) {
 	//     "$ref": "#/responses/forbidden"
 	//   "404":
 	//     "$ref": "#/responses/notFound"
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			ctx.NotFound(err)
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetIssueByID", err)
@@ -928,9 +927,9 @@ func UpdateIssueDeadline(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 	form := web.GetForm(ctx).(*api.EditDeadlineOption)
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -951,7 +950,7 @@ func UpdateIssueDeadline(ctx *context.APIContext) {
 		deadlineUnix = timeutil.TimeStamp(deadline.Unix())
 	}
 
-	if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil {
+	if err := issues_model.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil {
 		ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
 		return
 	}
diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go
index 22533c3810..89038e4f16 100644
--- a/routers/api/v1/repo/issue_comment.go
+++ b/routers/api/v1/repo/issue_comment.go
@@ -10,7 +10,7 @@ import (
 	"errors"
 	"net/http"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
@@ -65,33 +65,33 @@ func ListIssueComments(ctx *context.APIContext) {
 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
 		return
 	}
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err)
 		return
 	}
 	issue.Repo = ctx.Repo.Repository
 
-	opts := &models.FindCommentsOptions{
+	opts := &issues_model.FindCommentsOptions{
 		IssueID: issue.ID,
 		Since:   since,
 		Before:  before,
-		Type:    models.CommentTypeComment,
+		Type:    issues_model.CommentTypeComment,
 	}
 
-	comments, err := models.FindComments(ctx, opts)
+	comments, err := issues_model.FindComments(ctx, opts)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "FindComments", err)
 		return
 	}
 
-	totalCount, err := models.CountComments(opts)
+	totalCount, err := issues_model.CountComments(opts)
 	if err != nil {
 		ctx.InternalServerError(err)
 		return
 	}
 
-	if err := models.CommentList(comments).LoadPosters(); err != nil {
+	if err := issues_model.CommentList(comments).LoadPosters(); err != nil {
 		ctx.Error(http.StatusInternalServerError, "LoadPosters", err)
 		return
 	}
@@ -157,35 +157,35 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) {
 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
 		return
 	}
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err)
 		return
 	}
 	issue.Repo = ctx.Repo.Repository
 
-	opts := &models.FindCommentsOptions{
+	opts := &issues_model.FindCommentsOptions{
 		ListOptions: utils.GetListOptions(ctx),
 		IssueID:     issue.ID,
 		Since:       since,
 		Before:      before,
-		Type:        models.CommentTypeUnknown,
+		Type:        issues_model.CommentTypeUnknown,
 	}
 
-	comments, err := models.FindComments(ctx, opts)
+	comments, err := issues_model.FindComments(ctx, opts)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "FindComments", err)
 		return
 	}
 
-	if err := models.CommentList(comments).LoadPosters(); err != nil {
+	if err := issues_model.CommentList(comments).LoadPosters(); err != nil {
 		ctx.Error(http.StatusInternalServerError, "LoadPosters", err)
 		return
 	}
 
 	var apiComments []*api.TimelineComment
 	for _, comment := range comments {
-		if comment.Type != models.CommentTypeCode && isXRefCommentAccessible(ctx, ctx.Doer, comment, issue.RepoID) {
+		if comment.Type != issues_model.CommentTypeCode && isXRefCommentAccessible(ctx, ctx.Doer, comment, issue.RepoID) {
 			comment.Issue = issue
 			apiComments = append(apiComments, convert.ToTimelineComment(comment, ctx.Doer))
 		}
@@ -195,9 +195,9 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) {
 	ctx.JSON(http.StatusOK, &apiComments)
 }
 
-func isXRefCommentAccessible(ctx stdCtx.Context, user *user_model.User, c *models.Comment, issueRepoID int64) bool {
+func isXRefCommentAccessible(ctx stdCtx.Context, user *user_model.User, c *issues_model.Comment, issueRepoID int64) bool {
 	// Remove comments that the user has no permissions to see
-	if models.CommentTypeIsRef(c.Type) && c.RefRepoID != issueRepoID && c.RefRepoID != 0 {
+	if issues_model.CommentTypeIsRef(c.Type) && c.RefRepoID != issueRepoID && c.RefRepoID != 0 {
 		var err error
 		// Set RefRepo for description in template
 		c.RefRepo, err = repo_model.GetRepositoryByIDCtx(ctx, c.RefRepoID)
@@ -261,41 +261,41 @@ func ListRepoIssueComments(ctx *context.APIContext) {
 		return
 	}
 
-	opts := &models.FindCommentsOptions{
+	opts := &issues_model.FindCommentsOptions{
 		ListOptions: utils.GetListOptions(ctx),
 		RepoID:      ctx.Repo.Repository.ID,
-		Type:        models.CommentTypeComment,
+		Type:        issues_model.CommentTypeComment,
 		Since:       since,
 		Before:      before,
 	}
 
-	comments, err := models.FindComments(ctx, opts)
+	comments, err := issues_model.FindComments(ctx, opts)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "FindComments", err)
 		return
 	}
 
-	totalCount, err := models.CountComments(opts)
+	totalCount, err := issues_model.CountComments(opts)
 	if err != nil {
 		ctx.InternalServerError(err)
 		return
 	}
 
-	if err = models.CommentList(comments).LoadPosters(); err != nil {
+	if err = issues_model.CommentList(comments).LoadPosters(); err != nil {
 		ctx.Error(http.StatusInternalServerError, "LoadPosters", err)
 		return
 	}
 
 	apiComments := make([]*api.Comment, len(comments))
-	if err := models.CommentList(comments).LoadIssues(); err != nil {
+	if err := issues_model.CommentList(comments).LoadIssues(); err != nil {
 		ctx.Error(http.StatusInternalServerError, "LoadIssues", err)
 		return
 	}
-	if err := models.CommentList(comments).LoadPosters(); err != nil {
+	if err := issues_model.CommentList(comments).LoadPosters(); err != nil {
 		ctx.Error(http.StatusInternalServerError, "LoadPosters", err)
 		return
 	}
-	if _, err := models.CommentList(comments).Issues().LoadRepositories(); err != nil {
+	if _, err := issues_model.CommentList(comments).Issues().LoadRepositories(); err != nil {
 		ctx.Error(http.StatusInternalServerError, "LoadRepositories", err)
 		return
 	}
@@ -343,7 +343,7 @@ func CreateIssueComment(ctx *context.APIContext) {
 	//   "403":
 	//     "$ref": "#/responses/forbidden"
 	form := web.GetForm(ctx).(*api.CreateIssueCommentOption)
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
 		return
@@ -399,9 +399,9 @@ func GetIssueComment(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	comment, err := models.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
+	comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if models.IsErrCommentNotExist(err) {
+		if issues_model.IsErrCommentNotExist(err) {
 			ctx.NotFound(err)
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
@@ -418,7 +418,7 @@ func GetIssueComment(ctx *context.APIContext) {
 		return
 	}
 
-	if comment.Type != models.CommentTypeComment {
+	if comment.Type != issues_model.CommentTypeComment {
 		ctx.Status(http.StatusNoContent)
 		return
 	}
@@ -526,9 +526,9 @@ func EditIssueCommentDeprecated(ctx *context.APIContext) {
 }
 
 func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) {
-	comment, err := models.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
+	comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if models.IsErrCommentNotExist(err) {
+		if issues_model.IsErrCommentNotExist(err) {
 			ctx.NotFound(err)
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
@@ -541,7 +541,7 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
 		return
 	}
 
-	if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeReview && comment.Type != models.CommentTypeCode {
+	if comment.Type != issues_model.CommentTypeComment && comment.Type != issues_model.CommentTypeReview && comment.Type != issues_model.CommentTypeCode {
 		ctx.Status(http.StatusNoContent)
 		return
 	}
@@ -629,9 +629,9 @@ func DeleteIssueCommentDeprecated(ctx *context.APIContext) {
 }
 
 func deleteIssueComment(ctx *context.APIContext) {
-	comment, err := models.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
+	comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if models.IsErrCommentNotExist(err) {
+		if issues_model.IsErrCommentNotExist(err) {
 			ctx.NotFound(err)
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
@@ -642,7 +642,7 @@ func deleteIssueComment(ctx *context.APIContext) {
 	if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
 		ctx.Status(http.StatusForbidden)
 		return
-	} else if comment.Type != models.CommentTypeComment {
+	} else if comment.Type != issues_model.CommentTypeComment {
 		ctx.Status(http.StatusNoContent)
 		return
 	}
diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go
index 0193eb4230..50c09e02fa 100644
--- a/routers/api/v1/repo/issue_label.go
+++ b/routers/api/v1/repo/issue_label.go
@@ -8,7 +8,7 @@ package repo
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/convert"
 	api "code.gitea.io/gitea/modules/structs"
@@ -46,9 +46,9 @@ func ListIssueLabels(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -56,7 +56,7 @@ func ListIssueLabels(ctx *context.APIContext) {
 		return
 	}
 
-	if err := issue.LoadAttributes(); err != nil {
+	if err := issue.LoadAttributes(ctx); err != nil {
 		ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
 		return
 	}
@@ -111,7 +111,7 @@ func AddIssueLabels(ctx *context.APIContext) {
 		return
 	}
 
-	labels, err = models.GetLabelsByIssueID(ctx, issue.ID)
+	labels, err = issues_model.GetLabelsByIssueID(ctx, issue.ID)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "GetLabelsByIssueID", err)
 		return
@@ -158,9 +158,9 @@ func DeleteIssueLabel(ctx *context.APIContext) {
 	//   "422":
 	//     "$ref": "#/responses/validationError"
 
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -173,9 +173,9 @@ func DeleteIssueLabel(ctx *context.APIContext) {
 		return
 	}
 
-	label, err := models.GetLabelByID(ctx, ctx.ParamsInt64(":id"))
+	label, err := issues_model.GetLabelByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if models.IsErrLabelNotExist(err) {
+		if issues_model.IsErrLabelNotExist(err) {
 			ctx.Error(http.StatusUnprocessableEntity, "", err)
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetLabelByID", err)
@@ -237,7 +237,7 @@ func ReplaceIssueLabels(ctx *context.APIContext) {
 		return
 	}
 
-	labels, err = models.GetLabelsByIssueID(ctx, issue.ID)
+	labels, err = issues_model.GetLabelsByIssueID(ctx, issue.ID)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "GetLabelsByIssueID", err)
 		return
@@ -276,9 +276,9 @@ func ClearIssueLabels(ctx *context.APIContext) {
 	//   "403":
 	//     "$ref": "#/responses/forbidden"
 
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -299,10 +299,10 @@ func ClearIssueLabels(ctx *context.APIContext) {
 	ctx.Status(http.StatusNoContent)
 }
 
-func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) (issue *models.Issue, labels []*models.Label, err error) {
-	issue, err = models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) (issue *issues_model.Issue, labels []*issues_model.Label, err error) {
+	issue, err = issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -310,7 +310,7 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption)
 		return
 	}
 
-	labels, err = models.GetLabelsByIDs(form.Labels)
+	labels, err = issues_model.GetLabelsByIDs(form.Labels)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "GetLabelsByIDs", err)
 		return
diff --git a/routers/api/v1/repo/issue_reaction.go b/routers/api/v1/repo/issue_reaction.go
index 45be7a92dd..f4c40d2bcd 100644
--- a/routers/api/v1/repo/issue_reaction.go
+++ b/routers/api/v1/repo/issue_reaction.go
@@ -8,7 +8,6 @@ import (
 	"errors"
 	"net/http"
 
-	"code.gitea.io/gitea/models"
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/convert"
@@ -49,9 +48,9 @@ func GetIssueCommentReactions(ctx *context.APIContext) {
 	//   "403":
 	//     "$ref": "#/responses/forbidden"
 
-	comment, err := models.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
+	comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if models.IsErrCommentNotExist(err) {
+		if issues_model.IsErrCommentNotExist(err) {
 			ctx.NotFound(err)
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
@@ -176,9 +175,9 @@ func DeleteIssueCommentReaction(ctx *context.APIContext) {
 }
 
 func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) {
-	comment, err := models.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
+	comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if models.IsErrCommentNotExist(err) {
+		if issues_model.IsErrCommentNotExist(err) {
 			ctx.NotFound(err)
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
@@ -271,9 +270,9 @@ func GetIssueReactions(ctx *context.APIContext) {
 	//   "403":
 	//     "$ref": "#/responses/forbidden"
 
-	issue, err := models.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, err := issues_model.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -391,9 +390,9 @@ func DeleteIssueReaction(ctx *context.APIContext) {
 }
 
 func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) {
-	issue, err := models.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, err := issues_model.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
diff --git a/routers/api/v1/repo/issue_stopwatch.go b/routers/api/v1/repo/issue_stopwatch.go
index 382f294346..941b22e454 100644
--- a/routers/api/v1/repo/issue_stopwatch.go
+++ b/routers/api/v1/repo/issue_stopwatch.go
@@ -8,7 +8,7 @@ import (
 	"errors"
 	"net/http"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/convert"
 	"code.gitea.io/gitea/routers/api/v1/utils"
@@ -55,7 +55,7 @@ func StartIssueStopwatch(ctx *context.APIContext) {
 		return
 	}
 
-	if err := models.CreateIssueStopwatch(ctx, ctx.Doer, issue); err != nil {
+	if err := issues_model.CreateIssueStopwatch(ctx, ctx.Doer, issue); err != nil {
 		ctx.Error(http.StatusInternalServerError, "CreateOrStopIssueStopwatch", err)
 		return
 	}
@@ -104,7 +104,7 @@ func StopIssueStopwatch(ctx *context.APIContext) {
 		return
 	}
 
-	if err := models.FinishIssueStopwatch(ctx, ctx.Doer, issue); err != nil {
+	if err := issues_model.FinishIssueStopwatch(ctx, ctx.Doer, issue); err != nil {
 		ctx.Error(http.StatusInternalServerError, "CreateOrStopIssueStopwatch", err)
 		return
 	}
@@ -153,7 +153,7 @@ func DeleteIssueStopwatch(ctx *context.APIContext) {
 		return
 	}
 
-	if err := models.CancelStopwatch(ctx.Doer, issue); err != nil {
+	if err := issues_model.CancelStopwatch(ctx.Doer, issue); err != nil {
 		ctx.Error(http.StatusInternalServerError, "CancelStopwatch", err)
 		return
 	}
@@ -161,10 +161,10 @@ func DeleteIssueStopwatch(ctx *context.APIContext) {
 	ctx.Status(http.StatusNoContent)
 }
 
-func prepareIssueStopwatch(ctx *context.APIContext, shouldExist bool) (*models.Issue, error) {
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+func prepareIssueStopwatch(ctx *context.APIContext, shouldExist bool) (*issues_model.Issue, error) {
+	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -183,7 +183,7 @@ func prepareIssueStopwatch(ctx *context.APIContext, shouldExist bool) (*models.I
 		return nil, errors.New("Cannot use time tracker")
 	}
 
-	if models.StopwatchExists(ctx.Doer.ID, issue.ID) != shouldExist {
+	if issues_model.StopwatchExists(ctx.Doer.ID, issue.ID) != shouldExist {
 		if shouldExist {
 			ctx.Error(http.StatusConflict, "StopwatchExists", "cannot stop/cancel a non existent stopwatch")
 			err = errors.New("cannot stop/cancel a non existent stopwatch")
@@ -219,13 +219,13 @@ func GetStopwatches(ctx *context.APIContext) {
 	//   "200":
 	//     "$ref": "#/responses/StopWatchList"
 
-	sws, err := models.GetUserStopwatches(ctx.Doer.ID, utils.GetListOptions(ctx))
+	sws, err := issues_model.GetUserStopwatches(ctx.Doer.ID, utils.GetListOptions(ctx))
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "GetUserStopwatches", err)
 		return
 	}
 
-	count, err := models.CountUserStopwatches(ctx.Doer.ID)
+	count, err := issues_model.CountUserStopwatches(ctx.Doer.ID)
 	if err != nil {
 		ctx.InternalServerError(err)
 		return
diff --git a/routers/api/v1/repo/issue_subscription.go b/routers/api/v1/repo/issue_subscription.go
index a608ba2278..5e03e42b4c 100644
--- a/routers/api/v1/repo/issue_subscription.go
+++ b/routers/api/v1/repo/issue_subscription.go
@@ -8,7 +8,7 @@ import (
 	"fmt"
 	"net/http"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/convert"
@@ -105,9 +105,9 @@ func DelIssueSubscription(ctx *context.APIContext) {
 }
 
 func setIssueSubscription(ctx *context.APIContext, watch bool) {
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -133,7 +133,7 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) {
 		return
 	}
 
-	current, err := models.CheckIssueWatch(user, issue)
+	current, err := issues_model.CheckIssueWatch(user, issue)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "CheckIssueWatch", err)
 		return
@@ -146,7 +146,7 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) {
 	}
 
 	// Update watch state
-	if err := models.CreateOrUpdateIssueWatch(user.ID, issue.ID, watch); err != nil {
+	if err := issues_model.CreateOrUpdateIssueWatch(user.ID, issue.ID, watch); err != nil {
 		ctx.Error(http.StatusInternalServerError, "CreateOrUpdateIssueWatch", err)
 		return
 	}
@@ -186,9 +186,9 @@ func CheckIssueSubscription(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -197,7 +197,7 @@ func CheckIssueSubscription(ctx *context.APIContext) {
 		return
 	}
 
-	watching, err := models.CheckIssueWatch(ctx.Doer, issue)
+	watching, err := issues_model.CheckIssueWatch(ctx.Doer, issue)
 	if err != nil {
 		ctx.InternalServerError(err)
 		return
@@ -252,9 +252,9 @@ func GetIssueSubscribers(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -263,7 +263,7 @@ func GetIssueSubscribers(ctx *context.APIContext) {
 		return
 	}
 
-	iwl, err := models.GetIssueWatchers(ctx, issue.ID, utils.GetListOptions(ctx))
+	iwl, err := issues_model.GetIssueWatchers(ctx, issue.ID, utils.GetListOptions(ctx))
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "GetIssueWatchers", err)
 		return
@@ -284,7 +284,7 @@ func GetIssueSubscribers(ctx *context.APIContext) {
 		apiUsers = append(apiUsers, convert.ToUser(v, ctx.Doer))
 	}
 
-	count, err := models.CountIssueWatchers(ctx, issue.ID)
+	count, err := issues_model.CountIssueWatchers(ctx, issue.ID)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "CountIssueWatchers", err)
 		return
diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go
index 19e1a82590..b9a6c5af64 100644
--- a/routers/api/v1/repo/issue_tracked_time.go
+++ b/routers/api/v1/repo/issue_tracked_time.go
@@ -9,8 +9,8 @@ import (
 	"net/http"
 	"time"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/context"
@@ -76,9 +76,9 @@ func ListTrackedTimes(ctx *context.APIContext) {
 		ctx.NotFound("Timetracker is disabled")
 		return
 	}
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			ctx.NotFound(err)
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -86,7 +86,7 @@ func ListTrackedTimes(ctx *context.APIContext) {
 		return
 	}
 
-	opts := &models.FindTrackedTimesOptions{
+	opts := &issues_model.FindTrackedTimesOptions{
 		ListOptions:  utils.GetListOptions(ctx),
 		RepositoryID: ctx.Repo.Repository.ID,
 		IssueID:      issue.ID,
@@ -122,13 +122,13 @@ func ListTrackedTimes(ctx *context.APIContext) {
 		}
 	}
 
-	count, err := models.CountTrackedTimes(opts)
+	count, err := issues_model.CountTrackedTimes(opts)
 	if err != nil {
 		ctx.InternalServerError(err)
 		return
 	}
 
-	trackedTimes, err := models.GetTrackedTimes(ctx, opts)
+	trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err)
 		return
@@ -180,9 +180,9 @@ func AddTime(ctx *context.APIContext) {
 	//   "403":
 	//     "$ref": "#/responses/forbidden"
 	form := web.GetForm(ctx).(*api.AddTimeOption)
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			ctx.NotFound(err)
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -215,7 +215,7 @@ func AddTime(ctx *context.APIContext) {
 		created = form.Created
 	}
 
-	trackedTime, err := models.AddTime(user, issue, form.Time, created)
+	trackedTime, err := issues_model.AddTime(user, issue, form.Time, created)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "AddTime", err)
 		return
@@ -261,9 +261,9 @@ func ResetIssueTime(ctx *context.APIContext) {
 	//   "403":
 	//     "$ref": "#/responses/forbidden"
 
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			ctx.NotFound(err)
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -280,7 +280,7 @@ func ResetIssueTime(ctx *context.APIContext) {
 		return
 	}
 
-	err = models.DeleteIssueUserTimes(issue, ctx.Doer)
+	err = issues_model.DeleteIssueUserTimes(issue, ctx.Doer)
 	if err != nil {
 		if db.IsErrNotExist(err) {
 			ctx.Error(http.StatusNotFound, "DeleteIssueUserTimes", err)
@@ -332,9 +332,9 @@ func DeleteTime(ctx *context.APIContext) {
 	//   "403":
 	//     "$ref": "#/responses/forbidden"
 
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			ctx.NotFound(err)
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -351,7 +351,7 @@ func DeleteTime(ctx *context.APIContext) {
 		return
 	}
 
-	time, err := models.GetTrackedTimeByID(ctx.ParamsInt64(":id"))
+	time, err := issues_model.GetTrackedTimeByID(ctx.ParamsInt64(":id"))
 	if err != nil {
 		if db.IsErrNotExist(err) {
 			ctx.NotFound(err)
@@ -371,7 +371,7 @@ func DeleteTime(ctx *context.APIContext) {
 		return
 	}
 
-	err = models.DeleteTime(time)
+	err = issues_model.DeleteTime(time)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "DeleteTime", err)
 		return
@@ -434,12 +434,12 @@ func ListTrackedTimesByUser(ctx *context.APIContext) {
 		return
 	}
 
-	opts := &models.FindTrackedTimesOptions{
+	opts := &issues_model.FindTrackedTimesOptions{
 		UserID:       user.ID,
 		RepositoryID: ctx.Repo.Repository.ID,
 	}
 
-	trackedTimes, err := models.GetTrackedTimes(ctx, opts)
+	trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err)
 		return
@@ -504,7 +504,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
 		return
 	}
 
-	opts := &models.FindTrackedTimesOptions{
+	opts := &issues_model.FindTrackedTimesOptions{
 		ListOptions:  utils.GetListOptions(ctx),
 		RepositoryID: ctx.Repo.Repository.ID,
 	}
@@ -541,13 +541,13 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
 		}
 	}
 
-	count, err := models.CountTrackedTimes(opts)
+	count, err := issues_model.CountTrackedTimes(opts)
 	if err != nil {
 		ctx.InternalServerError(err)
 		return
 	}
 
-	trackedTimes, err := models.GetTrackedTimes(ctx, opts)
+	trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err)
 		return
@@ -592,7 +592,7 @@ func ListMyTrackedTimes(ctx *context.APIContext) {
 	//   "200":
 	//     "$ref": "#/responses/TrackedTimeList"
 
-	opts := &models.FindTrackedTimesOptions{
+	opts := &issues_model.FindTrackedTimesOptions{
 		ListOptions: utils.GetListOptions(ctx),
 		UserID:      ctx.Doer.ID,
 	}
@@ -603,13 +603,13 @@ func ListMyTrackedTimes(ctx *context.APIContext) {
 		return
 	}
 
-	count, err := models.CountTrackedTimes(opts)
+	count, err := issues_model.CountTrackedTimes(opts)
 	if err != nil {
 		ctx.InternalServerError(err)
 		return
 	}
 
-	trackedTimes, err := models.GetTrackedTimes(ctx, opts)
+	trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "GetTrackedTimesByUser", err)
 		return
diff --git a/routers/api/v1/repo/label.go b/routers/api/v1/repo/label.go
index 4332b8e627..8b1e298668 100644
--- a/routers/api/v1/repo/label.go
+++ b/routers/api/v1/repo/label.go
@@ -11,7 +11,7 @@ import (
 	"strconv"
 	"strings"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/convert"
 	api "code.gitea.io/gitea/modules/structs"
@@ -49,13 +49,13 @@ func ListLabels(ctx *context.APIContext) {
 	//   "200":
 	//     "$ref": "#/responses/LabelList"
 
-	labels, err := models.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormString("sort"), utils.GetListOptions(ctx))
+	labels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormString("sort"), utils.GetListOptions(ctx))
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "GetLabelsByRepoID", err)
 		return
 	}
 
-	count, err := models.CountLabelsByRepoID(ctx.Repo.Repository.ID)
+	count, err := issues_model.CountLabelsByRepoID(ctx.Repo.Repository.ID)
 	if err != nil {
 		ctx.InternalServerError(err)
 		return
@@ -94,17 +94,17 @@ func GetLabel(ctx *context.APIContext) {
 	//     "$ref": "#/responses/Label"
 
 	var (
-		label *models.Label
+		label *issues_model.Label
 		err   error
 	)
 	strID := ctx.Params(":id")
 	if intID, err2 := strconv.ParseInt(strID, 10, 64); err2 != nil {
-		label, err = models.GetLabelInRepoByName(ctx, ctx.Repo.Repository.ID, strID)
+		label, err = issues_model.GetLabelInRepoByName(ctx, ctx.Repo.Repository.ID, strID)
 	} else {
-		label, err = models.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, intID)
+		label, err = issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, intID)
 	}
 	if err != nil {
-		if models.IsErrRepoLabelNotExist(err) {
+		if issues_model.IsErrRepoLabelNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetLabelByRepoID", err)
@@ -150,18 +150,18 @@ func CreateLabel(ctx *context.APIContext) {
 	if len(form.Color) == 6 {
 		form.Color = "#" + form.Color
 	}
-	if !models.LabelColorPattern.MatchString(form.Color) {
+	if !issues_model.LabelColorPattern.MatchString(form.Color) {
 		ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", form.Color))
 		return
 	}
 
-	label := &models.Label{
+	label := &issues_model.Label{
 		Name:        form.Name,
 		Color:       form.Color,
 		RepoID:      ctx.Repo.Repository.ID,
 		Description: form.Description,
 	}
-	if err := models.NewLabel(ctx, label); err != nil {
+	if err := issues_model.NewLabel(ctx, label); err != nil {
 		ctx.Error(http.StatusInternalServerError, "NewLabel", err)
 		return
 	}
@@ -206,9 +206,9 @@ func EditLabel(ctx *context.APIContext) {
 	//     "$ref": "#/responses/validationError"
 
 	form := web.GetForm(ctx).(*api.EditLabelOption)
-	label, err := models.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
+	label, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if models.IsErrRepoLabelNotExist(err) {
+		if issues_model.IsErrRepoLabelNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetLabelByRepoID", err)
@@ -224,7 +224,7 @@ func EditLabel(ctx *context.APIContext) {
 		if len(label.Color) == 6 {
 			label.Color = "#" + label.Color
 		}
-		if !models.LabelColorPattern.MatchString(label.Color) {
+		if !issues_model.LabelColorPattern.MatchString(label.Color) {
 			ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", label.Color))
 			return
 		}
@@ -232,7 +232,7 @@ func EditLabel(ctx *context.APIContext) {
 	if form.Description != nil {
 		label.Description = *form.Description
 	}
-	if err := models.UpdateLabel(label); err != nil {
+	if err := issues_model.UpdateLabel(label); err != nil {
 		ctx.Error(http.StatusInternalServerError, "UpdateLabel", err)
 		return
 	}
@@ -266,7 +266,7 @@ func DeleteLabel(ctx *context.APIContext) {
 	//   "204":
 	//     "$ref": "#/responses/empty"
 
-	if err := models.DeleteLabel(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")); err != nil {
+	if err := issues_model.DeleteLabel(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")); err != nil {
 		ctx.Error(http.StatusInternalServerError, "DeleteLabel", err)
 		return
 	}
diff --git a/routers/api/v1/repo/patch.go b/routers/api/v1/repo/patch.go
index 6dbf979701..40afe2d92b 100644
--- a/routers/api/v1/repo/patch.go
+++ b/routers/api/v1/repo/patch.go
@@ -9,6 +9,7 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/models"
+	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	api "code.gitea.io/gitea/modules/structs"
@@ -78,7 +79,7 @@ func ApplyDiffPatch(ctx *context.APIContext) {
 	}
 
 	if !canWriteFiles(ctx, apiOpts.BranchName) {
-		ctx.Error(http.StatusInternalServerError, "ApplyPatch", models.ErrUserDoesNotHaveAccessToRepo{
+		ctx.Error(http.StatusInternalServerError, "ApplyPatch", repo_model.ErrUserDoesNotHaveAccessToRepo{
 			UserID:   ctx.Doer.ID,
 			RepoName: ctx.Repo.Repository.LowerName,
 		})
diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
index 393f2d1576..50d2c9484f 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -92,7 +92,7 @@ func ListPullRequests(ctx *context.APIContext) {
 
 	listOptions := utils.GetListOptions(ctx)
 
-	prs, maxResults, err := models.PullRequests(ctx.Repo.Repository.ID, &models.PullRequestsOptions{
+	prs, maxResults, err := issues_model.PullRequests(ctx.Repo.Repository.ID, &issues_model.PullRequestsOptions{
 		ListOptions: listOptions,
 		State:       ctx.FormTrim("state"),
 		SortType:    ctx.FormTrim("sort"),
@@ -160,9 +160,9 @@ func GetPullRequest(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrPullRequestNotExist(err) {
+		if issues_model.IsErrPullRequestNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
@@ -220,9 +220,9 @@ func DownloadPullDiffOrPatch(ctx *context.APIContext) {
 	//     "$ref": "#/responses/string"
 	//   "404":
 	//     "$ref": "#/responses/notFound"
-	pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrPullRequestNotExist(err) {
+		if issues_model.IsErrPullRequestNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.InternalServerError(err)
@@ -297,14 +297,14 @@ func CreatePullRequest(ctx *context.APIContext) {
 	defer headGitRepo.Close()
 
 	// Check if another PR exists with the same targets
-	existingPr, err := models.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch, models.PullRequestFlowGithub)
+	existingPr, err := issues_model.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch, issues_model.PullRequestFlowGithub)
 	if err != nil {
-		if !models.IsErrPullRequestNotExist(err) {
+		if !issues_model.IsErrPullRequestNotExist(err) {
 			ctx.Error(http.StatusInternalServerError, "GetUnmergedPullRequest", err)
 			return
 		}
 	} else {
-		err = models.ErrPullRequestAlreadyExists{
+		err = issues_model.ErrPullRequestAlreadyExists{
 			ID:         existingPr.ID,
 			IssueID:    existingPr.Index,
 			HeadRepoID: existingPr.HeadRepoID,
@@ -317,7 +317,7 @@ func CreatePullRequest(ctx *context.APIContext) {
 	}
 
 	if len(form.Labels) > 0 {
-		labels, err := models.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels)
+		labels, err := issues_model.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels)
 		if err != nil {
 			ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDs", err)
 			return
@@ -331,7 +331,7 @@ func CreatePullRequest(ctx *context.APIContext) {
 		}
 
 		if ctx.Repo.Owner.IsOrganization() {
-			orgLabels, err := models.GetLabelsInOrgByIDs(ctx.Repo.Owner.ID, form.Labels)
+			orgLabels, err := issues_model.GetLabelsInOrgByIDs(ctx.Repo.Owner.ID, form.Labels)
 			if err != nil {
 				ctx.Error(http.StatusInternalServerError, "GetLabelsInOrgByIDs", err)
 				return
@@ -364,7 +364,7 @@ func CreatePullRequest(ctx *context.APIContext) {
 		deadlineUnix = timeutil.TimeStamp(form.Deadline.Unix())
 	}
 
-	prIssue := &models.Issue{
+	prIssue := &issues_model.Issue{
 		RepoID:       repo.ID,
 		Title:        form.Title,
 		PosterID:     ctx.Doer.ID,
@@ -374,7 +374,7 @@ func CreatePullRequest(ctx *context.APIContext) {
 		Content:      form.Body,
 		DeadlineUnix: deadlineUnix,
 	}
-	pr := &models.PullRequest{
+	pr := &issues_model.PullRequest{
 		HeadRepoID: headRepo.ID,
 		BaseRepoID: repo.ID,
 		HeadBranch: headBranch,
@@ -382,11 +382,11 @@ func CreatePullRequest(ctx *context.APIContext) {
 		HeadRepo:   headRepo,
 		BaseRepo:   repo,
 		MergeBase:  compareInfo.MergeBase,
-		Type:       models.PullRequestGitea,
+		Type:       issues_model.PullRequestGitea,
 	}
 
 	// Get all assignee IDs
-	assigneeIDs, err := models.MakeIDsFromAPIAssigneesToAdd(form.Assignee, form.Assignees)
+	assigneeIDs, err := issues_model.MakeIDsFromAPIAssigneesToAdd(form.Assignee, form.Assignees)
 	if err != nil {
 		if user_model.IsErrUserNotExist(err) {
 			ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
@@ -409,13 +409,13 @@ func CreatePullRequest(ctx *context.APIContext) {
 			return
 		}
 		if !valid {
-			ctx.Error(http.StatusUnprocessableEntity, "canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
+			ctx.Error(http.StatusUnprocessableEntity, "canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
 			return
 		}
 	}
 
 	if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs); err != nil {
-		if models.IsErrUserDoesNotHaveAccessToRepo(err) {
+		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
 			return
 		}
@@ -470,9 +470,9 @@ func EditPullRequest(ctx *context.APIContext) {
 	//     "$ref": "#/responses/validationError"
 
 	form := web.GetForm(ctx).(*api.EditPullRequestOption)
-	pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrPullRequestNotExist(err) {
+		if issues_model.IsErrPullRequestNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
@@ -510,7 +510,7 @@ func EditPullRequest(ctx *context.APIContext) {
 			deadlineUnix = timeutil.TimeStamp(deadline.Unix())
 		}
 
-		if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil {
+		if err := issues_model.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil {
 			ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
 			return
 		}
@@ -548,14 +548,14 @@ func EditPullRequest(ctx *context.APIContext) {
 	}
 
 	if ctx.Repo.CanWrite(unit.TypePullRequests) && form.Labels != nil {
-		labels, err := models.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels)
+		labels, err := issues_model.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels)
 		if err != nil {
 			ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDsError", err)
 			return
 		}
 
 		if ctx.Repo.Owner.IsOrganization() {
-			orgLabels, err := models.GetLabelsInOrgByIDs(ctx.Repo.Owner.ID, form.Labels)
+			orgLabels, err := issues_model.GetLabelsInOrgByIDs(ctx.Repo.Owner.ID, form.Labels)
 			if err != nil {
 				ctx.Error(http.StatusInternalServerError, "GetLabelsInOrgByIDs", err)
 				return
@@ -564,7 +564,7 @@ func EditPullRequest(ctx *context.APIContext) {
 			labels = append(labels, orgLabels...)
 		}
 
-		if err = models.ReplaceIssueLabels(issue, labels, ctx.Doer); err != nil {
+		if err = issues_model.ReplaceIssueLabels(issue, labels, ctx.Doer); err != nil {
 			ctx.Error(http.StatusInternalServerError, "ReplaceLabelsError", err)
 			return
 		}
@@ -577,9 +577,9 @@ func EditPullRequest(ctx *context.APIContext) {
 		}
 		issue.IsClosed = api.StateClosed == api.StateType(*form.State)
 	}
-	statusChangeComment, titleChanged, err := models.UpdateIssueByAPI(issue, ctx.Doer)
+	statusChangeComment, titleChanged, err := issues_model.UpdateIssueByAPI(issue, ctx.Doer)
 	if err != nil {
-		if models.IsErrDependenciesLeft(err) {
+		if issues_model.IsErrDependenciesLeft(err) {
 			ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this pull request because it still has open dependencies")
 			return
 		}
@@ -602,10 +602,10 @@ func EditPullRequest(ctx *context.APIContext) {
 			return
 		}
 		if err := pull_service.ChangeTargetBranch(ctx, pr, ctx.Doer, form.Base); err != nil {
-			if models.IsErrPullRequestAlreadyExists(err) {
+			if issues_model.IsErrPullRequestAlreadyExists(err) {
 				ctx.Error(http.StatusConflict, "IsErrPullRequestAlreadyExists", err)
 				return
-			} else if models.IsErrIssueIsClosed(err) {
+			} else if issues_model.IsErrIssueIsClosed(err) {
 				ctx.Error(http.StatusUnprocessableEntity, "IsErrIssueIsClosed", err)
 				return
 			} else if models.IsErrPullRequestHasMerged(err) {
@@ -632,9 +632,9 @@ func EditPullRequest(ctx *context.APIContext) {
 	}
 
 	// Refetch from database
-	pr, err = models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, pr.Index)
+	pr, err = issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, pr.Index)
 	if err != nil {
-		if models.IsErrPullRequestNotExist(err) {
+		if issues_model.IsErrPullRequestNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
@@ -676,9 +676,9 @@ func IsPullRequestMerged(ctx *context.APIContext) {
 	//   "404":
 	//     description: pull request has not been merged
 
-	pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrPullRequestNotExist(err) {
+		if issues_model.IsErrPullRequestNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
@@ -730,9 +730,9 @@ func MergePullRequest(ctx *context.APIContext) {
 
 	form := web.GetForm(ctx).(*forms.MergePullRequestForm)
 
-	pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrPullRequestNotExist(err) {
+		if issues_model.IsErrPullRequestNotExist(err) {
 			ctx.NotFound("GetPullRequestByIndex", err)
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
@@ -753,7 +753,7 @@ func MergePullRequest(ctx *context.APIContext) {
 
 	if ctx.IsSigned {
 		// Update issue-user.
-		if err = pr.Issue.ReadBy(ctx, ctx.Doer.ID); err != nil {
+		if err = models.SetIssueReadBy(ctx, pr.Issue.ID, ctx.Doer.ID); err != nil {
 			ctx.Error(http.StatusInternalServerError, "ReadBy", err)
 			return
 		}
@@ -868,7 +868,7 @@ func MergePullRequest(ctx *context.APIContext) {
 
 	if form.DeleteBranchAfterMerge {
 		// Don't cleanup when there are other PR's that use this branch as head branch.
-		exist, err := models.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch)
+		exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch)
 		if err != nil {
 			ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
 			return
@@ -902,7 +902,7 @@ func MergePullRequest(ctx *context.APIContext) {
 			}
 			return
 		}
-		if err := models.AddDeletePRBranchComment(ctx, ctx.Doer, pr.BaseRepo, pr.Issue.ID, pr.HeadBranch); err != nil {
+		if err := issues_model.AddDeletePRBranchComment(ctx, ctx.Doer, pr.BaseRepo, pr.Issue.ID, pr.HeadBranch); err != nil {
 			// Do not fail here as branch has already been deleted
 			log.Error("DeleteBranch: %v", err)
 		}
@@ -1079,9 +1079,9 @@ func UpdatePullRequest(ctx *context.APIContext) {
 	//   "422":
 	//     "$ref": "#/responses/validationError"
 
-	pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrPullRequestNotExist(err) {
+		if issues_model.IsErrPullRequestNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
@@ -1177,9 +1177,9 @@ func CancelScheduledAutoMerge(ctx *context.APIContext) {
 	//     "$ref": "#/responses/notFound"
 
 	pullIndex := ctx.ParamsInt64(":index")
-	pull, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, pullIndex)
+	pull, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, pullIndex)
 	if err != nil {
-		if models.IsErrPullRequestNotExist(err) {
+		if issues_model.IsErrPullRequestNotExist(err) {
 			ctx.NotFound()
 			return
 		}
@@ -1254,9 +1254,9 @@ func GetPullRequestCommits(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrPullRequestNotExist(err) {
+		if issues_model.IsErrPullRequestNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go
index 5175fa921f..57548f2102 100644
--- a/routers/api/v1/repo/pull_review.go
+++ b/routers/api/v1/repo/pull_review.go
@@ -9,7 +9,7 @@ import (
 	"net/http"
 	"strings"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	user_model "code.gitea.io/gitea/models/user"
@@ -61,9 +61,9 @@ func ListPullReviews(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrPullRequestNotExist(err) {
+		if issues_model.IsErrPullRequestNotExist(err) {
 			ctx.NotFound("GetPullRequestByIndex", err)
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
@@ -81,19 +81,19 @@ func ListPullReviews(ctx *context.APIContext) {
 		return
 	}
 
-	opts := models.FindReviewOptions{
+	opts := issues_model.FindReviewOptions{
 		ListOptions: utils.GetListOptions(ctx),
-		Type:        models.ReviewTypeUnknown,
+		Type:        issues_model.ReviewTypeUnknown,
 		IssueID:     pr.IssueID,
 	}
 
-	allReviews, err := models.FindReviews(ctx, opts)
+	allReviews, err := issues_model.FindReviews(ctx, opts)
 	if err != nil {
 		ctx.InternalServerError(err)
 		return
 	}
 
-	count, err := models.CountReviews(opts)
+	count, err := issues_model.CountReviews(opts)
 	if err != nil {
 		ctx.InternalServerError(err)
 		return
@@ -261,7 +261,7 @@ func DeletePullReview(ctx *context.APIContext) {
 		return
 	}
 
-	if err := models.DeleteReview(review); err != nil {
+	if err := issues_model.DeleteReview(review); err != nil {
 		ctx.Error(http.StatusInternalServerError, "DeleteReview", fmt.Errorf("can not delete ReviewID: %d", review.ID))
 		return
 	}
@@ -307,9 +307,9 @@ func CreatePullReview(ctx *context.APIContext) {
 	//     "$ref": "#/responses/validationError"
 
 	opts := web.GetForm(ctx).(*api.CreatePullReviewOptions)
-	pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrPullRequestNotExist(err) {
+		if issues_model.IsErrPullRequestNotExist(err) {
 			ctx.NotFound("GetPullRequestByIndex", err)
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
@@ -435,7 +435,7 @@ func SubmitPullReview(ctx *context.APIContext) {
 		return
 	}
 
-	if review.Type != models.ReviewTypePending {
+	if review.Type != issues_model.ReviewTypePending {
 		ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("only a pending review can be submitted"))
 		return
 	}
@@ -447,7 +447,7 @@ func SubmitPullReview(ctx *context.APIContext) {
 	}
 
 	// if review stay pending return
-	if reviewType == models.ReviewTypePending {
+	if reviewType == issues_model.ReviewTypePending {
 		ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review stay pending"))
 		return
 	}
@@ -475,7 +475,7 @@ func SubmitPullReview(ctx *context.APIContext) {
 }
 
 // preparePullReviewType return ReviewType and false or nil and true if an error happen
-func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, event api.ReviewStateType, body string, hasComments bool) (models.ReviewType, bool) {
+func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest, event api.ReviewStateType, body string, hasComments bool) (issues_model.ReviewType, bool) {
 	if err := pr.LoadIssue(); err != nil {
 		ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
 		return -1, true
@@ -484,7 +484,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, even
 	needsBody := true
 	hasBody := len(strings.TrimSpace(body)) > 0
 
-	var reviewType models.ReviewType
+	var reviewType issues_model.ReviewType
 	switch event {
 	case api.ReviewStateApproved:
 		// can not approve your own PR
@@ -492,7 +492,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, even
 			ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("approve your own pull is not allowed"))
 			return -1, true
 		}
-		reviewType = models.ReviewTypeApprove
+		reviewType = issues_model.ReviewTypeApprove
 		needsBody = false
 
 	case api.ReviewStateRequestChanges:
@@ -501,10 +501,10 @@ func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, even
 			ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("reject your own pull is not allowed"))
 			return -1, true
 		}
-		reviewType = models.ReviewTypeReject
+		reviewType = issues_model.ReviewTypeReject
 
 	case api.ReviewStateComment:
-		reviewType = models.ReviewTypeComment
+		reviewType = issues_model.ReviewTypeComment
 		needsBody = false
 		// if there is no body we need to ensure that there are comments
 		if !hasBody && !hasComments {
@@ -512,7 +512,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, even
 			return -1, true
 		}
 	default:
-		reviewType = models.ReviewTypePending
+		reviewType = issues_model.ReviewTypePending
 	}
 
 	// reject reviews with empty body if a body is required for this call
@@ -525,10 +525,10 @@ func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, even
 }
 
 // prepareSingleReview return review, related pull and false or nil, nil and true if an error happen
-func prepareSingleReview(ctx *context.APIContext) (*models.Review, *models.PullRequest, bool) {
-	pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues_model.PullRequest, bool) {
+	pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrPullRequestNotExist(err) {
+		if issues_model.IsErrPullRequestNotExist(err) {
 			ctx.NotFound("GetPullRequestByIndex", err)
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
@@ -536,9 +536,9 @@ func prepareSingleReview(ctx *context.APIContext) (*models.Review, *models.PullR
 		return nil, nil, true
 	}
 
-	review, err := models.GetReviewByID(ctx, ctx.ParamsInt64(":id"))
+	review, err := issues_model.GetReviewByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if models.IsErrReviewNotExist(err) {
+		if issues_model.IsErrReviewNotExist(err) {
 			ctx.NotFound("GetReviewByID", err)
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetReviewByID", err)
@@ -553,7 +553,7 @@ func prepareSingleReview(ctx *context.APIContext) (*models.Review, *models.PullR
 	}
 
 	// make sure that the user has access to this review if it is pending
-	if review.Type == models.ReviewTypePending && review.ReviewerID != ctx.Doer.ID && !ctx.Doer.IsAdmin {
+	if review.Type == issues_model.ReviewTypePending && review.ReviewerID != ctx.Doer.ID && !ctx.Doer.IsAdmin {
 		ctx.NotFound("GetReviewByID")
 		return nil, nil, true
 	}
@@ -648,9 +648,9 @@ func DeleteReviewRequests(ctx *context.APIContext) {
 }
 
 func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions, isAdd bool) {
-	pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrPullRequestNotExist(err) {
+		if issues_model.IsErrPullRequestNotExist(err) {
 			ctx.NotFound("GetPullRequestByIndex", err)
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
@@ -690,7 +690,7 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
 
 		err = issue_service.IsValidReviewRequest(ctx, reviewer, ctx.Doer, isAdd, pr.Issue, &permDoer)
 		if err != nil {
-			if models.IsErrNotValidReviewRequest(err) {
+			if issues_model.IsErrNotValidReviewRequest(err) {
 				ctx.Error(http.StatusUnprocessableEntity, "NotValidReviewRequest", err)
 				return
 			}
@@ -701,9 +701,9 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
 		reviewers = append(reviewers, reviewer)
 	}
 
-	var reviews []*models.Review
+	var reviews []*issues_model.Review
 	if isAdd {
-		reviews = make([]*models.Review, 0, len(reviewers))
+		reviews = make([]*issues_model.Review, 0, len(reviewers))
 	}
 
 	for _, reviewer := range reviewers {
@@ -739,7 +739,7 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
 
 			err = issue_service.IsValidTeamReviewRequest(ctx, teamReviewer, ctx.Doer, isAdd, pr.Issue)
 			if err != nil {
-				if models.IsErrNotValidReviewRequest(err) {
+				if issues_model.IsErrNotValidReviewRequest(err) {
 					ctx.Error(http.StatusUnprocessableEntity, "NotValidReviewRequest", err)
 					return
 				}
@@ -876,7 +876,7 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss bool) {
 		return
 	}
 
-	if review.Type != models.ReviewTypeApprove && review.Type != models.ReviewTypeReject {
+	if review.Type != issues_model.ReviewTypeApprove && review.Type != issues_model.ReviewTypeReject {
 		ctx.Error(http.StatusForbidden, "", "not need to dismiss this review because it's type is not Approve or change request")
 		return
 	}
@@ -892,7 +892,7 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss bool) {
 		return
 	}
 
-	if review, err = models.GetReviewByID(ctx, review.ID); err != nil {
+	if review, err = issues_model.GetReviewByID(ctx, review.ID); err != nil {
 		ctx.Error(http.StatusInternalServerError, "GetReviewByID", err)
 		return
 	}
diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go
index eb2bbc1e5f..93aa450f9c 100644
--- a/routers/private/hook_post_receive.go
+++ b/routers/private/hook_post_receive.go
@@ -11,7 +11,7 @@ import (
 	"strconv"
 	"strings"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
@@ -141,8 +141,8 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 				continue
 			}
 
-			pr, err := models.GetPullRequestByIndex(ctx, repo.ID, pullIndex)
-			if err != nil && !models.IsErrPullRequestNotExist(err) {
+			pr, err := issues_model.GetPullRequestByIndex(ctx, repo.ID, pullIndex)
+			if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
 				log.Error("Failed to get PR by index %v Error: %v", pullIndex, err)
 				ctx.JSON(http.StatusInternalServerError, private.Response{
 					Err: fmt.Sprintf("Failed to get PR by index %v Error: %v", pullIndex, err),
@@ -202,8 +202,8 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
 				continue
 			}
 
-			pr, err := models.GetUnmergedPullRequest(repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch, models.PullRequestFlowGithub)
-			if err != nil && !models.IsErrPullRequestNotExist(err) {
+			pr, err := issues_model.GetUnmergedPullRequest(repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch, issues_model.PullRequestFlowGithub)
+			if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
 				log.Error("Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err)
 				ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
 					Err: fmt.Sprintf(
diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go
index 411319f2e6..cadfea782c 100644
--- a/routers/private/hook_pre_receive.go
+++ b/routers/private/hook_pre_receive.go
@@ -14,6 +14,7 @@ import (
 	"code.gitea.io/gitea/models"
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	git_model "code.gitea.io/gitea/models/git"
+	issues_model "code.gitea.io/gitea/models/issues"
 	perm_model "code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	"code.gitea.io/gitea/models/unit"
@@ -57,7 +58,7 @@ func (ctx *preReceiveContext) CanWriteCode() bool {
 		if !ctx.loadPusherAndPermission() {
 			return false
 		}
-		ctx.canWriteCode = models.CanMaintainerWriteToBranch(ctx.userPerm, ctx.branchName, ctx.user) || ctx.deployKeyAccessMode >= perm_model.AccessModeWrite
+		ctx.canWriteCode = issues_model.CanMaintainerWriteToBranch(ctx.userPerm, ctx.branchName, ctx.user) || ctx.deployKeyAccessMode >= perm_model.AccessModeWrite
 		ctx.checkedCanWriteCode = true
 	}
 	return ctx.canWriteCode
@@ -296,7 +297,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN
 		// 6b. Merge (from UI or API)
 
 		// Get the PR, user and permissions for the user in the repository
-		pr, err := models.GetPullRequestByID(ctx, ctx.opts.PullRequestID)
+		pr, err := issues_model.GetPullRequestByID(ctx, ctx.opts.PullRequestID)
 		if err != nil {
 			log.Error("Unable to get PullRequest %d Error: %v", ctx.opts.PullRequestID, err)
 			ctx.JSON(http.StatusInternalServerError, private.Response{
diff --git a/routers/web/org/org_labels.go b/routers/web/org/org_labels.go
index bfa9f162c3..185e1eee31 100644
--- a/routers/web/org/org_labels.go
+++ b/routers/web/org/org_labels.go
@@ -7,8 +7,8 @@ package org
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/context"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/web"
@@ -17,7 +17,7 @@ import (
 
 // RetrieveLabels find all the labels of an organization
 func RetrieveLabels(ctx *context.Context) {
-	labels, err := models.GetLabelsByOrgID(ctx, ctx.Org.Organization.ID, ctx.FormString("sort"), db.ListOptions{})
+	labels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Org.Organization.ID, ctx.FormString("sort"), db.ListOptions{})
 	if err != nil {
 		ctx.ServerError("RetrieveLabels.GetLabels", err)
 		return
@@ -43,13 +43,13 @@ func NewLabel(ctx *context.Context) {
 		return
 	}
 
-	l := &models.Label{
+	l := &issues_model.Label{
 		OrgID:       ctx.Org.Organization.ID,
 		Name:        form.Title,
 		Description: form.Description,
 		Color:       form.Color,
 	}
-	if err := models.NewLabel(ctx, l); err != nil {
+	if err := issues_model.NewLabel(ctx, l); err != nil {
 		ctx.ServerError("NewLabel", err)
 		return
 	}
@@ -59,10 +59,10 @@ func NewLabel(ctx *context.Context) {
 // UpdateLabel update a label's name and color
 func UpdateLabel(ctx *context.Context) {
 	form := web.GetForm(ctx).(*forms.CreateLabelForm)
-	l, err := models.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, form.ID)
+	l, err := issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, form.ID)
 	if err != nil {
 		switch {
-		case models.IsErrOrgLabelNotExist(err):
+		case issues_model.IsErrOrgLabelNotExist(err):
 			ctx.Error(http.StatusNotFound)
 		default:
 			ctx.ServerError("UpdateLabel", err)
@@ -73,7 +73,7 @@ func UpdateLabel(ctx *context.Context) {
 	l.Name = form.Title
 	l.Description = form.Description
 	l.Color = form.Color
-	if err := models.UpdateLabel(l); err != nil {
+	if err := issues_model.UpdateLabel(l); err != nil {
 		ctx.ServerError("UpdateLabel", err)
 		return
 	}
@@ -82,7 +82,7 @@ func UpdateLabel(ctx *context.Context) {
 
 // DeleteLabel delete a label
 func DeleteLabel(ctx *context.Context) {
-	if err := models.DeleteLabel(ctx.Org.Organization.ID, ctx.FormInt64("id")); err != nil {
+	if err := issues_model.DeleteLabel(ctx.Org.Organization.ID, ctx.FormInt64("id")); err != nil {
 		ctx.Flash.Error("DeleteLabel: " + err.Error())
 	} else {
 		ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go
index 84ad803ee5..4bd2af4e8e 100644
--- a/routers/web/repo/branch.go
+++ b/routers/web/repo/branch.go
@@ -13,6 +13,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	git_model "code.gitea.io/gitea/models/git"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/base"
@@ -44,7 +45,7 @@ type Branch struct {
 	DeletedBranch     *git_model.DeletedBranch
 	CommitsAhead      int
 	CommitsBehind     int
-	LatestPullRequest *models.PullRequest
+	LatestPullRequest *issues_model.PullRequest
 	MergeMovedOn      bool
 }
 
@@ -264,7 +265,7 @@ func loadOneBranch(ctx *context.Context, rawBranch, defaultBranch *git.Branch, p
 		}
 	}
 
-	pr, err := models.GetLatestPullRequestByHeadInfo(ctx.Repo.Repository.ID, branchName)
+	pr, err := issues_model.GetLatestPullRequestByHeadInfo(ctx.Repo.Repository.ID, branchName)
 	if err != nil {
 		ctx.ServerError("GetLatestPullRequestByHeadInfo", err)
 		return nil
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index 44e89062e8..605594d5a9 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -19,6 +19,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	git_model "code.gitea.io/gitea/models/git"
+	issues_model "code.gitea.io/gitea/models/issues"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
@@ -747,9 +748,9 @@ func CompareDiff(ctx *context.Context) {
 	ctx.Data["HeadTags"] = headTags
 
 	if ctx.Data["PageIsComparePull"] == true {
-		pr, err := models.GetUnmergedPullRequest(ci.HeadRepo.ID, ctx.Repo.Repository.ID, ci.HeadBranch, ci.BaseBranch, models.PullRequestFlowGithub)
+		pr, err := issues_model.GetUnmergedPullRequest(ci.HeadRepo.ID, ctx.Repo.Repository.ID, ci.HeadBranch, ci.BaseBranch, issues_model.PullRequestFlowGithub)
 		if err != nil {
-			if !models.IsErrPullRequestNotExist(err) {
+			if !issues_model.IsErrPullRequestNotExist(err) {
 				ctx.ServerError("GetUnmergedPullRequest", err)
 				return
 			}
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 38ee933044..11d2daeeff 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -183,11 +183,11 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
 		}
 	}
 
-	var issueStats *models.IssueStats
+	var issueStats *issues_model.IssueStats
 	if forceEmpty {
-		issueStats = &models.IssueStats{}
+		issueStats = &issues_model.IssueStats{}
 	} else {
-		issueStats, err = models.GetIssueStats(&models.IssueStatsOptions{
+		issueStats, err = issues_model.GetIssueStats(&issues_model.IssueStatsOptions{
 			RepoID:            repo.ID,
 			Labels:            selectLabels,
 			MilestoneID:       milestoneID,
@@ -228,11 +228,11 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
 		mileIDs = []int64{milestoneID}
 	}
 
-	var issues []*models.Issue
+	var issues []*issues_model.Issue
 	if forceEmpty {
-		issues = []*models.Issue{}
+		issues = []*issues_model.Issue{}
 	} else {
-		issues, err = models.Issues(&models.IssuesOptions{
+		issues, err = issues_model.Issues(&issues_model.IssuesOptions{
 			ListOptions: db.ListOptions{
 				Page:     pager.Paginater.Current(),
 				PageSize: setting.UI.IssuePagingNum,
@@ -256,7 +256,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
 		}
 	}
 
-	issueList := models.IssueList(issues)
+	issueList := issues_model.IssueList(issues)
 	approvalCounts, err := issueList.GetApprovalCounts(ctx)
 	if err != nil {
 		ctx.ServerError("ApprovalCounts", err)
@@ -296,14 +296,14 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
 		return
 	}
 
-	labels, err := models.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
+	labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
 	if err != nil {
 		ctx.ServerError("GetLabelsByRepoID", err)
 		return
 	}
 
 	if repo.Owner.IsOrganization() {
-		orgLabels, err := models.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
+		orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
 		if err != nil {
 			ctx.ServerError("GetLabelsByOrgID", err)
 			return
@@ -330,11 +330,11 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
 		if !ok || len(counts) == 0 {
 			return 0
 		}
-		reviewTyp := models.ReviewTypeApprove
+		reviewTyp := issues_model.ReviewTypeApprove
 		if typ == "reject" {
-			reviewTyp = models.ReviewTypeReject
+			reviewTyp = issues_model.ReviewTypeReject
 		} else if typ == "waiting" {
-			reviewTyp = models.ReviewTypeRequest
+			reviewTyp = issues_model.ReviewTypeRequest
 		}
 		for _, count := range counts {
 			if count.Type == reviewTyp {
@@ -483,24 +483,24 @@ type repoReviewerSelection struct {
 	IsTeam    bool
 	Team      *organization.Team
 	User      *user_model.User
-	Review    *models.Review
+	Review    *issues_model.Review
 	CanChange bool
 	Checked   bool
 	ItemID    int64
 }
 
 // RetrieveRepoReviewers find all reviewers of a repository
-func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, issue *models.Issue, canChooseReviewer bool) {
+func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, issue *issues_model.Issue, canChooseReviewer bool) {
 	ctx.Data["CanChooseReviewer"] = canChooseReviewer
 
-	originalAuthorReviews, err := models.GetReviewersFromOriginalAuthorsByIssueID(issue.ID)
+	originalAuthorReviews, err := issues_model.GetReviewersFromOriginalAuthorsByIssueID(issue.ID)
 	if err != nil {
 		ctx.ServerError("GetReviewersFromOriginalAuthorsByIssueID", err)
 		return
 	}
 	ctx.Data["OriginalReviews"] = originalAuthorReviews
 
-	reviews, err := models.GetReviewersByIssueID(issue.ID)
+	reviews, err := issues_model.GetReviewersByIssueID(issue.ID)
 	if err != nil {
 		ctx.ServerError("GetReviewersByIssueID", err)
 		return
@@ -549,7 +549,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
 
 	for _, review := range reviews {
 		tmp := &repoReviewerSelection{
-			Checked: review.Type == models.ReviewTypeRequest,
+			Checked: review.Type == issues_model.ReviewTypeRequest,
 			Review:  review,
 			ItemID:  review.ReviewerID,
 		}
@@ -561,10 +561,10 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
 		if ctx.Repo.IsAdmin() {
 			// Admin can dismiss or re-request any review requests
 			tmp.CanChange = true
-		} else if ctx.Doer != nil && ctx.Doer.ID == review.ReviewerID && review.Type == models.ReviewTypeRequest {
+		} else if ctx.Doer != nil && ctx.Doer.ID == review.ReviewerID && review.Type == issues_model.ReviewTypeRequest {
 			// A user can refuse review requests
 			tmp.CanChange = true
-		} else if (canChooseReviewer || (ctx.Doer != nil && ctx.Doer.ID == issue.PosterID)) && review.Type != models.ReviewTypeRequest &&
+		} else if (canChooseReviewer || (ctx.Doer != nil && ctx.Doer.ID == issue.PosterID)) && review.Type != issues_model.ReviewTypeRequest &&
 			ctx.Doer.ID != review.ReviewerID {
 			// The poster of the PR, a manager, or official reviewers can re-request review from other reviewers
 			tmp.CanChange = true
@@ -670,19 +670,19 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
 }
 
 // RetrieveRepoMetas find all the meta information of a repository
-func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull bool) []*models.Label {
+func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull bool) []*issues_model.Label {
 	if !ctx.Repo.CanWriteIssuesOrPulls(isPull) {
 		return nil
 	}
 
-	labels, err := models.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
+	labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
 	if err != nil {
 		ctx.ServerError("GetLabelsByRepoID", err)
 		return nil
 	}
 	ctx.Data["Labels"] = labels
 	if repo.Owner.IsOrganization() {
-		orgLabels, err := models.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
+		orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
 		if err != nil {
 			return nil
 		}
@@ -763,10 +763,10 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleDirs,
 			ctx.Data[issueTemplateTitleKey] = meta.Title
 			ctx.Data[ctxDataKey] = templateBody
 			labelIDs := make([]string, 0, len(meta.Labels))
-			if repoLabels, err := models.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, "", db.ListOptions{}); err == nil {
+			if repoLabels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, "", db.ListOptions{}); err == nil {
 				ctx.Data["Labels"] = repoLabels
 				if ctx.Repo.Owner.IsOrganization() {
-					if orgLabels, err := models.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}); err == nil {
+					if orgLabels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}); err == nil {
 						ctx.Data["OrgLabels"] = orgLabels
 						repoLabels = append(repoLabels, orgLabels...)
 					}
@@ -969,7 +969,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
 			}
 
 			if !valid {
-				ctx.ServerError("canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
+				ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
 				return nil, nil, 0, 0
 			}
 		}
@@ -1017,7 +1017,7 @@ func NewIssuePost(ctx *context.Context) {
 		return
 	}
 
-	issue := &models.Issue{
+	issue := &issues_model.Issue{
 		RepoID:      repo.ID,
 		Repo:        repo,
 		Title:       form.Title,
@@ -1029,7 +1029,7 @@ func NewIssuePost(ctx *context.Context) {
 	}
 
 	if err := issue_service.NewIssue(repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
-		if models.IsErrUserDoesNotHaveAccessToRepo(err) {
+		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
 			return
 		}
@@ -1038,7 +1038,7 @@ func NewIssuePost(ctx *context.Context) {
 	}
 
 	if projectID > 0 {
-		if err := models.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil {
+		if err := issues_model.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil {
 			ctx.ServerError("ChangeProjectAssign", err)
 			return
 		}
@@ -1053,29 +1053,29 @@ func NewIssuePost(ctx *context.Context) {
 }
 
 // roleDescriptor returns the Role Descriptor for a comment in/with the given repo, poster and issue
-func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *user_model.User, issue *models.Issue) (models.RoleDescriptor, error) {
+func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *user_model.User, issue *issues_model.Issue) (issues_model.RoleDescriptor, error) {
 	perm, err := access_model.GetUserRepoPermission(ctx, repo, poster)
 	if err != nil {
-		return models.RoleDescriptorNone, err
+		return issues_model.RoleDescriptorNone, err
 	}
 
 	// By default the poster has no roles on the comment.
-	roleDescriptor := models.RoleDescriptorNone
+	roleDescriptor := issues_model.RoleDescriptorNone
 
 	// Check if the poster is owner of the repo.
 	if perm.IsOwner() {
 		// If the poster isn't a admin, enable the owner role.
 		if !poster.IsAdmin {
-			roleDescriptor = roleDescriptor.WithRole(models.RoleDescriptorOwner)
+			roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorOwner)
 		} else {
 
 			// Otherwise check if poster is the real repo admin.
 			ok, err := access_model.IsUserRealRepoAdmin(repo, poster)
 			if err != nil {
-				return models.RoleDescriptorNone, err
+				return issues_model.RoleDescriptorNone, err
 			}
 			if ok {
-				roleDescriptor = roleDescriptor.WithRole(models.RoleDescriptorOwner)
+				roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorOwner)
 			}
 		}
 	}
@@ -1083,18 +1083,18 @@ func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *use
 	// Is the poster can write issues or pulls to the repo, enable the Writer role.
 	// Only enable this if the poster doesn't have the owner role already.
 	if !roleDescriptor.HasRole("Owner") && perm.CanWriteIssuesOrPulls(issue.IsPull) {
-		roleDescriptor = roleDescriptor.WithRole(models.RoleDescriptorWriter)
+		roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorWriter)
 	}
 
 	// If the poster is the actual poster of the issue, enable Poster role.
 	if issue.IsPoster(poster.ID) {
-		roleDescriptor = roleDescriptor.WithRole(models.RoleDescriptorPoster)
+		roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorPoster)
 	}
 
 	return roleDescriptor, nil
 }
 
-func getBranchData(ctx *context.Context, issue *models.Issue) {
+func getBranchData(ctx *context.Context, issue *issues_model.Issue) {
 	ctx.Data["BaseBranch"] = nil
 	ctx.Data["HeadBranch"] = nil
 	ctx.Data["HeadUserName"] = nil
@@ -1131,9 +1131,9 @@ func ViewIssue(ctx *context.Context) {
 		}
 	}
 
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			ctx.NotFound("GetIssueByIndex", err)
 		} else {
 			ctx.ServerError("GetIssueByIndex", err)
@@ -1182,7 +1182,7 @@ func ViewIssue(ctx *context.Context) {
 	ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
 	upload.AddUploadContext(ctx, "comment")
 
-	if err = issue.LoadAttributes(); err != nil {
+	if err = issue.LoadAttributes(ctx); err != nil {
 		ctx.ServerError("LoadAttributes", err)
 		return
 	}
@@ -1194,11 +1194,11 @@ func ViewIssue(ctx *context.Context) {
 
 	ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
 
-	iw := new(models.IssueWatch)
+	iw := new(issues_model.IssueWatch)
 	if ctx.Doer != nil {
 		iw.UserID = ctx.Doer.ID
 		iw.IssueID = issue.ID
-		iw.IsWatching, err = models.CheckIssueWatch(ctx.Doer, issue)
+		iw.IsWatching, err = issues_model.CheckIssueWatch(ctx.Doer, issue)
 		if err != nil {
 			ctx.ServerError("CheckIssueWatch", err)
 			return
@@ -1239,7 +1239,7 @@ func ViewIssue(ctx *context.Context) {
 	for i := range issue.Labels {
 		labelIDMark[issue.Labels[i].ID] = true
 	}
-	labels, err := models.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
+	labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
 	if err != nil {
 		ctx.ServerError("GetLabelsByRepoID", err)
 		return
@@ -1247,7 +1247,7 @@ func ViewIssue(ctx *context.Context) {
 	ctx.Data["Labels"] = labels
 
 	if repo.Owner.IsOrganization() {
-		orgLabels, err := models.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
+		orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
 		if err != nil {
 			ctx.ServerError("GetLabelsByOrgID", err)
 			return
@@ -1279,7 +1279,7 @@ func ViewIssue(ctx *context.Context) {
 	if issue.IsPull {
 		canChooseReviewer := ctx.Repo.CanWrite(unit.TypePullRequests)
 		if !canChooseReviewer && ctx.Doer != nil && ctx.IsSigned {
-			canChooseReviewer, err = models.IsOfficialReviewer(ctx, issue, ctx.Doer)
+			canChooseReviewer, err = issues_model.IsOfficialReviewer(ctx, issue, ctx.Doer)
 			if err != nil {
 				ctx.ServerError("IsOfficialReviewer", err)
 				return
@@ -1294,35 +1294,35 @@ func ViewIssue(ctx *context.Context) {
 
 	if ctx.IsSigned {
 		// Update issue-user.
-		if err = issue.ReadBy(ctx, ctx.Doer.ID); err != nil {
+		if err = models.SetIssueReadBy(ctx, issue.ID, ctx.Doer.ID); err != nil {
 			ctx.ServerError("ReadBy", err)
 			return
 		}
 	}
 
 	var (
-		role         models.RoleDescriptor
+		role         issues_model.RoleDescriptor
 		ok           bool
-		marked       = make(map[int64]models.RoleDescriptor)
-		comment      *models.Comment
+		marked       = make(map[int64]issues_model.RoleDescriptor)
+		comment      *issues_model.Comment
 		participants = make([]*user_model.User, 1, 10)
 	)
 	if ctx.Repo.Repository.IsTimetrackerEnabled() {
 		if ctx.IsSigned {
 			// Deal with the stopwatch
-			ctx.Data["IsStopwatchRunning"] = models.StopwatchExists(ctx.Doer.ID, issue.ID)
+			ctx.Data["IsStopwatchRunning"] = issues_model.StopwatchExists(ctx.Doer.ID, issue.ID)
 			if !ctx.Data["IsStopwatchRunning"].(bool) {
 				var exists bool
-				var sw *models.Stopwatch
-				if exists, sw, err = models.HasUserStopwatch(ctx, ctx.Doer.ID); err != nil {
+				var sw *issues_model.Stopwatch
+				if exists, sw, err = issues_model.HasUserStopwatch(ctx, ctx.Doer.ID); err != nil {
 					ctx.ServerError("HasUserStopwatch", err)
 					return
 				}
 				ctx.Data["HasUserStopwatch"] = exists
 				if exists {
 					// Add warning if the user has already a stopwatch
-					var otherIssue *models.Issue
-					if otherIssue, err = models.GetIssueByID(sw.IssueID); err != nil {
+					var otherIssue *issues_model.Issue
+					if otherIssue, err = issues_model.GetIssueByID(ctx, sw.IssueID); err != nil {
 						ctx.ServerError("GetIssueByID", err)
 						return
 					}
@@ -1338,7 +1338,7 @@ func ViewIssue(ctx *context.Context) {
 		} else {
 			ctx.Data["CanUseTimetracker"] = false
 		}
-		if ctx.Data["WorkingUsers"], err = models.TotalTimes(&models.FindTrackedTimesOptions{IssueID: issue.ID}); err != nil {
+		if ctx.Data["WorkingUsers"], err = issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: issue.ID}); err != nil {
 			ctx.ServerError("TotalTimes", err)
 			return
 		}
@@ -1366,7 +1366,7 @@ func ViewIssue(ctx *context.Context) {
 			return
 		}
 
-		if comment.Type == models.CommentTypeComment || comment.Type == models.CommentTypeReview {
+		if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview {
 			if err := comment.LoadAttachments(); err != nil {
 				ctx.ServerError("LoadAttachments", err)
 				return
@@ -1396,12 +1396,12 @@ func ViewIssue(ctx *context.Context) {
 			}
 			marked[comment.PosterID] = comment.ShowRole
 			participants = addParticipant(comment.Poster, participants)
-		} else if comment.Type == models.CommentTypeLabel {
+		} else if comment.Type == issues_model.CommentTypeLabel {
 			if err = comment.LoadLabel(); err != nil {
 				ctx.ServerError("LoadLabel", err)
 				return
 			}
-		} else if comment.Type == models.CommentTypeMilestone {
+		} else if comment.Type == issues_model.CommentTypeMilestone {
 			if err = comment.LoadMilestone(); err != nil {
 				ctx.ServerError("LoadMilestone", err)
 				return
@@ -1416,7 +1416,7 @@ func ViewIssue(ctx *context.Context) {
 			if comment.MilestoneID > 0 && comment.Milestone == nil {
 				comment.Milestone = ghostMilestone
 			}
-		} else if comment.Type == models.CommentTypeProject {
+		} else if comment.Type == issues_model.CommentTypeProject {
 
 			if err = comment.LoadProject(); err != nil {
 				ctx.ServerError("LoadProject", err)
@@ -1436,19 +1436,19 @@ func ViewIssue(ctx *context.Context) {
 				comment.Project = ghostProject
 			}
 
-		} else if comment.Type == models.CommentTypeAssignees || comment.Type == models.CommentTypeReviewRequest {
+		} else if comment.Type == issues_model.CommentTypeAssignees || comment.Type == issues_model.CommentTypeReviewRequest {
 			if err = comment.LoadAssigneeUserAndTeam(); err != nil {
 				ctx.ServerError("LoadAssigneeUserAndTeam", err)
 				return
 			}
-		} else if comment.Type == models.CommentTypeRemoveDependency || comment.Type == models.CommentTypeAddDependency {
+		} else if comment.Type == issues_model.CommentTypeRemoveDependency || comment.Type == issues_model.CommentTypeAddDependency {
 			if err = comment.LoadDepIssueDetails(); err != nil {
-				if !models.IsErrIssueNotExist(err) {
+				if !issues_model.IsErrIssueNotExist(err) {
 					ctx.ServerError("LoadDepIssueDetails", err)
 					return
 				}
 			}
-		} else if comment.Type == models.CommentTypeCode || comment.Type == models.CommentTypeReview || comment.Type == models.CommentTypeDismissReview {
+		} else if comment.Type == issues_model.CommentTypeCode || comment.Type == issues_model.CommentTypeReview || comment.Type == issues_model.CommentTypeDismissReview {
 			comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
 				URLPrefix: ctx.Repo.RepoLink,
 				Metas:     ctx.Repo.Repository.ComposeMetas(),
@@ -1459,7 +1459,7 @@ func ViewIssue(ctx *context.Context) {
 				ctx.ServerError("RenderString", err)
 				return
 			}
-			if err = comment.LoadReview(); err != nil && !models.IsErrReviewNotExist(err) {
+			if err = comment.LoadReview(); err != nil && !issues_model.IsErrReviewNotExist(err) {
 				ctx.ServerError("LoadReview", err)
 				return
 			}
@@ -1502,14 +1502,14 @@ func ViewIssue(ctx *context.Context) {
 				ctx.ServerError("LoadResolveDoer", err)
 				return
 			}
-		} else if comment.Type == models.CommentTypePullRequestPush {
+		} else if comment.Type == issues_model.CommentTypePullRequestPush {
 			participants = addParticipant(comment.Poster, participants)
 			if err = comment.LoadPushCommits(ctx); err != nil {
 				ctx.ServerError("LoadPushCommits", err)
 				return
 			}
-		} else if comment.Type == models.CommentTypeAddTimeManual ||
-			comment.Type == models.CommentTypeStopTracking {
+		} else if comment.Type == issues_model.CommentTypeAddTimeManual ||
+			comment.Type == issues_model.CommentTypeStopTracking {
 			// drop error since times could be pruned from DB..
 			_ = comment.LoadTime()
 		}
@@ -1562,7 +1562,7 @@ func ViewIssue(ctx *context.Context) {
 				return
 			}
 
-			if ctx.Data["CanMarkConversation"], err = models.CanMarkConversation(issue, ctx.Doer); err != nil {
+			if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(issue, ctx.Doer); err != nil {
 				ctx.ServerError("CanMarkConversation", err)
 				return
 			}
@@ -1621,11 +1621,11 @@ func ViewIssue(ctx *context.Context) {
 			if ctx.Doer != nil {
 				showMergeInstructions = pull.ProtectedBranch.CanUserPush(ctx.Doer.ID)
 			}
-			ctx.Data["IsBlockedByApprovals"] = !models.HasEnoughApprovals(ctx, pull.ProtectedBranch, pull)
-			ctx.Data["IsBlockedByRejection"] = models.MergeBlockedByRejectedReview(ctx, pull.ProtectedBranch, pull)
-			ctx.Data["IsBlockedByOfficialReviewRequests"] = models.MergeBlockedByOfficialReviewRequests(ctx, pull.ProtectedBranch, pull)
-			ctx.Data["IsBlockedByOutdatedBranch"] = models.MergeBlockedByOutdatedBranch(pull.ProtectedBranch, pull)
-			ctx.Data["GrantedApprovals"] = models.GetGrantedApprovalsCount(ctx, pull.ProtectedBranch, pull)
+			ctx.Data["IsBlockedByApprovals"] = !issues_model.HasEnoughApprovals(ctx, pull.ProtectedBranch, pull)
+			ctx.Data["IsBlockedByRejection"] = issues_model.MergeBlockedByRejectedReview(ctx, pull.ProtectedBranch, pull)
+			ctx.Data["IsBlockedByOfficialReviewRequests"] = issues_model.MergeBlockedByOfficialReviewRequests(ctx, pull.ProtectedBranch, pull)
+			ctx.Data["IsBlockedByOutdatedBranch"] = issues_model.MergeBlockedByOutdatedBranch(pull.ProtectedBranch, pull)
+			ctx.Data["GrantedApprovals"] = issues_model.GetGrantedApprovalsCount(ctx, pull.ProtectedBranch, pull)
 			ctx.Data["RequireSigned"] = pull.ProtectedBranch.RequireSignedCommits
 			ctx.Data["ChangedProtectedFiles"] = pull.ChangedProtectedFiles
 			ctx.Data["IsBlockedByChangedProtectedFiles"] = len(pull.ChangedProtectedFiles) != 0
@@ -1655,7 +1655,7 @@ func ViewIssue(ctx *context.Context) {
 			(!pull.HasMerged || ctx.Data["HeadBranchCommitID"] == ctx.Data["PullHeadCommitID"])
 
 		if isPullBranchDeletable && pull.HasMerged {
-			exist, err := models.HasUnmergedPullRequestsByHeadInfo(ctx, pull.HeadRepoID, pull.HeadBranch)
+			exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pull.HeadRepoID, pull.HeadBranch)
 			if err != nil {
 				ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
 				return
@@ -1722,7 +1722,7 @@ func ViewIssue(ctx *context.Context) {
 		}
 		hiddenCommentTypes, _ = new(big.Int).SetString(val, 10) // we can safely ignore the failed conversion here
 	}
-	ctx.Data["ShouldShowCommentType"] = func(commentType models.CommentType) bool {
+	ctx.Data["ShouldShowCommentType"] = func(commentType issues_model.CommentType) bool {
 		return hiddenCommentTypes == nil || hiddenCommentTypes.Bit(int(commentType)) == 0
 	}
 
@@ -1730,10 +1730,10 @@ func ViewIssue(ctx *context.Context) {
 }
 
 // GetActionIssue will return the issue which is used in the context.
-func GetActionIssue(ctx *context.Context) *models.Issue {
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+func GetActionIssue(ctx *context.Context) *issues_model.Issue {
+	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		ctx.NotFoundOrServerError("GetIssueByIndex", models.IsErrIssueNotExist, err)
+		ctx.NotFoundOrServerError("GetIssueByIndex", issues_model.IsErrIssueNotExist, err)
 		return nil
 	}
 	issue.Repo = ctx.Repo.Repository
@@ -1741,21 +1741,21 @@ func GetActionIssue(ctx *context.Context) *models.Issue {
 	if ctx.Written() {
 		return nil
 	}
-	if err = issue.LoadAttributes(); err != nil {
+	if err = issue.LoadAttributes(ctx); err != nil {
 		ctx.ServerError("LoadAttributes", nil)
 		return nil
 	}
 	return issue
 }
 
-func checkIssueRights(ctx *context.Context, issue *models.Issue) {
+func checkIssueRights(ctx *context.Context, issue *issues_model.Issue) {
 	if issue.IsPull && !ctx.Repo.CanRead(unit.TypePullRequests) ||
 		!issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
 		ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
 	}
 }
 
-func getActionIssues(ctx *context.Context) []*models.Issue {
+func getActionIssues(ctx *context.Context) []*issues_model.Issue {
 	commaSeparatedIssueIDs := ctx.FormString("issue_ids")
 	if len(commaSeparatedIssueIDs) == 0 {
 		return nil
@@ -1769,7 +1769,7 @@ func getActionIssues(ctx *context.Context) []*models.Issue {
 		}
 		issueIDs = append(issueIDs, issueID)
 	}
-	issues, err := models.GetIssuesByIDs(ctx, issueIDs)
+	issues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
 	if err != nil {
 		ctx.ServerError("GetIssuesByIDs", err)
 		return nil
@@ -1782,7 +1782,7 @@ func getActionIssues(ctx *context.Context) []*models.Issue {
 			ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
 			return nil
 		}
-		if err = issue.LoadAttributes(); err != nil {
+		if err = issue.LoadAttributes(ctx); err != nil {
 			ctx.ServerError("LoadAttributes", err)
 			return nil
 		}
@@ -1792,9 +1792,9 @@ func getActionIssues(ctx *context.Context) []*models.Issue {
 
 // GetIssueInfo get an issue of a repository
 func GetIssueInfo(ctx *context.Context) {
-	issue, err := models.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, err := issues_model.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			ctx.Error(http.StatusNotFound)
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error())
@@ -1916,9 +1916,9 @@ func UpdateIssueContent(ctx *context.Context) {
 // UpdateIssueDeadline updates an issue deadline
 func UpdateIssueDeadline(ctx *context.Context) {
 	form := web.GetForm(ctx).(*api.EditDeadlineOption)
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			ctx.NotFound("GetIssueByIndex", err)
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error())
@@ -1939,7 +1939,7 @@ func UpdateIssueDeadline(ctx *context.Context) {
 		deadlineUnix = timeutil.TimeStamp(deadline.Unix())
 	}
 
-	if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil {
+	if err := issues_model.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil {
 		ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err.Error())
 		return
 	}
@@ -2002,7 +2002,7 @@ func UpdateIssueAssignee(ctx *context.Context) {
 				return
 			}
 			if !valid {
-				ctx.ServerError("canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name})
+				ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name})
 				return
 			}
 
@@ -2080,7 +2080,7 @@ func UpdatePullReviewRequest(ctx *context.Context) {
 
 			err = issue_service.IsValidTeamReviewRequest(ctx, team, ctx.Doer, action == "attach", issue)
 			if err != nil {
-				if models.IsErrNotValidReviewRequest(err) {
+				if issues_model.IsErrNotValidReviewRequest(err) {
 					log.Warn(
 						"UpdatePullReviewRequest: refusing to add invalid team review request for UID[%d] team %s to %s#%d owned by UID[%d]: Error: %v",
 						team.OrgID, team.Name, issue.Repo.FullName(), issue.Index, issue.Repo.ID,
@@ -2118,7 +2118,7 @@ func UpdatePullReviewRequest(ctx *context.Context) {
 
 		err = issue_service.IsValidReviewRequest(ctx, reviewer, ctx.Doer, action == "attach", issue, nil)
 		if err != nil {
-			if models.IsErrNotValidReviewRequest(err) {
+			if issues_model.IsErrNotValidReviewRequest(err) {
 				log.Warn(
 					"UpdatePullReviewRequest: refusing to add invalid review request for %-v to %-v#%d: Error: %v",
 					reviewer, issue.Repo, issue.Index,
@@ -2215,7 +2215,7 @@ func SearchIssues(ctx *context.Context) {
 		return
 	}
 
-	var issues []*models.Issue
+	var issues []*issues_model.Issue
 	var filteredCount int64
 
 	keyword := ctx.FormTrim("q")
@@ -2264,7 +2264,7 @@ func SearchIssues(ctx *context.Context) {
 	// Only fetch the issues if we either don't have a keyword or the search returned issues
 	// This would otherwise return all issues if no issues were found by the search.
 	if len(keyword) == 0 || len(issueIDs) > 0 || len(includedLabelNames) > 0 || len(includedMilestones) > 0 {
-		issuesOpt := &models.IssuesOptions{
+		issuesOpt := &issues_model.IssuesOptions{
 			ListOptions: db.ListOptions{
 				Page:     ctx.FormInt("page"),
 				PageSize: limit,
@@ -2300,7 +2300,7 @@ func SearchIssues(ctx *context.Context) {
 			issuesOpt.ReviewRequestedID = ctxUserID
 		}
 
-		if issues, err = models.Issues(issuesOpt); err != nil {
+		if issues, err = issues_model.Issues(issuesOpt); err != nil {
 			ctx.Error(http.StatusInternalServerError, "Issues", err.Error())
 			return
 		}
@@ -2308,7 +2308,7 @@ func SearchIssues(ctx *context.Context) {
 		issuesOpt.ListOptions = db.ListOptions{
 			Page: -1,
 		}
-		if filteredCount, err = models.CountIssues(issuesOpt); err != nil {
+		if filteredCount, err = issues_model.CountIssues(issuesOpt); err != nil {
 			ctx.Error(http.StatusInternalServerError, "CountIssues", err.Error())
 			return
 		}
@@ -2356,7 +2356,7 @@ func ListIssues(ctx *context.Context) {
 		isClosed = util.OptionalBoolFalse
 	}
 
-	var issues []*models.Issue
+	var issues []*issues_model.Issue
 	var filteredCount int64
 
 	keyword := ctx.FormTrim("q")
@@ -2374,7 +2374,7 @@ func ListIssues(ctx *context.Context) {
 	}
 
 	if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
-		labelIDs, err = models.GetLabelIDsInRepoByNames(ctx.Repo.Repository.ID, splitted)
+		labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx.Repo.Repository.ID, splitted)
 		if err != nil {
 			ctx.Error(http.StatusInternalServerError, err.Error())
 			return
@@ -2443,7 +2443,7 @@ func ListIssues(ctx *context.Context) {
 	// Only fetch the issues if we either don't have a keyword or the search returned issues
 	// This would otherwise return all issues if no issues were found by the search.
 	if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
-		issuesOpt := &models.IssuesOptions{
+		issuesOpt := &issues_model.IssuesOptions{
 			ListOptions:       listOptions,
 			RepoID:            ctx.Repo.Repository.ID,
 			IsClosed:          isClosed,
@@ -2458,7 +2458,7 @@ func ListIssues(ctx *context.Context) {
 			MentionedID:       mentionedByID,
 		}
 
-		if issues, err = models.Issues(issuesOpt); err != nil {
+		if issues, err = issues_model.Issues(issuesOpt); err != nil {
 			ctx.Error(http.StatusInternalServerError, err.Error())
 			return
 		}
@@ -2466,7 +2466,7 @@ func ListIssues(ctx *context.Context) {
 		issuesOpt.ListOptions = db.ListOptions{
 			Page: -1,
 		}
-		if filteredCount, err = models.CountIssues(issuesOpt); err != nil {
+		if filteredCount, err = issues_model.CountIssues(issuesOpt); err != nil {
 			ctx.Error(http.StatusInternalServerError, err.Error())
 			return
 		}
@@ -2493,14 +2493,14 @@ func UpdateIssueStatus(ctx *context.Context) {
 		log.Warn("Unrecognized action: %s", action)
 	}
 
-	if _, err := models.IssueList(issues).LoadRepositories(); err != nil {
+	if _, err := issues_model.IssueList(issues).LoadRepositories(); err != nil {
 		ctx.ServerError("LoadRepositories", err)
 		return
 	}
 	for _, issue := range issues {
 		if issue.IsClosed != isClosed {
 			if err := issue_service.ChangeStatus(issue, ctx.Doer, isClosed); err != nil {
-				if models.IsErrDependenciesLeft(err) {
+				if issues_model.IsErrDependenciesLeft(err) {
 					ctx.JSON(http.StatusPreconditionFailed, map[string]interface{}{
 						"error": "cannot close this issue because it still has open dependencies",
 					})
@@ -2564,7 +2564,7 @@ func NewComment(ctx *context.Context) {
 		return
 	}
 
-	var comment *models.Comment
+	var comment *issues_model.Comment
 	defer func() {
 		// Check if issue admin/poster changes the status of issue.
 		if (ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) || (ctx.IsSigned && issue.IsPoster(ctx.Doer.ID))) &&
@@ -2572,14 +2572,14 @@ func NewComment(ctx *context.Context) {
 			!(issue.IsPull && issue.PullRequest.HasMerged) {
 
 			// Duplication and conflict check should apply to reopen pull request.
-			var pr *models.PullRequest
+			var pr *issues_model.PullRequest
 
 			if form.Status == "reopen" && issue.IsPull {
 				pull := issue.PullRequest
 				var err error
-				pr, err = models.GetUnmergedPullRequest(pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch, pull.Flow)
+				pr, err = issues_model.GetUnmergedPullRequest(pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch, pull.Flow)
 				if err != nil {
-					if !models.IsErrPullRequestNotExist(err) {
+					if !issues_model.IsErrPullRequestNotExist(err) {
 						ctx.ServerError("GetUnmergedPullRequest", err)
 						return
 					}
@@ -2599,7 +2599,7 @@ func NewComment(ctx *context.Context) {
 				if err := issue_service.ChangeStatus(issue, ctx.Doer, isClosed); err != nil {
 					log.Error("ChangeStatus: %v", err)
 
-					if models.IsErrDependenciesLeft(err) {
+					if issues_model.IsErrDependenciesLeft(err) {
 						if issue.IsPull {
 							ctx.Flash.Error(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
 							ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, issue.Index))
@@ -2648,14 +2648,14 @@ func NewComment(ctx *context.Context) {
 
 // UpdateCommentContent change comment of issue's content
 func UpdateCommentContent(ctx *context.Context) {
-	comment, err := models.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
+	comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
+		ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
 		return
 	}
 
 	if err := comment.LoadIssue(); err != nil {
-		ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
+		ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
 		return
 	}
 
@@ -2664,7 +2664,7 @@ func UpdateCommentContent(ctx *context.Context) {
 		return
 	}
 
-	if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeReview && comment.Type != models.CommentTypeCode {
+	if comment.Type != issues_model.CommentTypeComment && comment.Type != issues_model.CommentTypeReview && comment.Type != issues_model.CommentTypeCode {
 		ctx.Error(http.StatusNoContent)
 		return
 	}
@@ -2714,21 +2714,21 @@ func UpdateCommentContent(ctx *context.Context) {
 
 // DeleteComment delete comment of issue
 func DeleteComment(ctx *context.Context) {
-	comment, err := models.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
+	comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
+		ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
 		return
 	}
 
 	if err := comment.LoadIssue(); err != nil {
-		ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
+		ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
 		return
 	}
 
 	if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
 		ctx.Error(http.StatusForbidden)
 		return
-	} else if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode {
+	} else if comment.Type != issues_model.CommentTypeComment && comment.Type != issues_model.CommentTypeCode {
 		ctx.Error(http.StatusNoContent)
 		return
 	}
@@ -2790,7 +2790,7 @@ func ChangeIssueReaction(ctx *context.Context) {
 		}
 		// Reload new reactions
 		issue.Reactions = nil
-		if err = issue.LoadAttributes(); err != nil {
+		if err = issue.LoadAttributes(ctx); err != nil {
 			log.Info("issue.LoadAttributes: %s", err)
 			break
 		}
@@ -2804,7 +2804,7 @@ func ChangeIssueReaction(ctx *context.Context) {
 
 		// Reload new reactions
 		issue.Reactions = nil
-		if err := issue.LoadAttributes(); err != nil {
+		if err := issue.LoadAttributes(ctx); err != nil {
 			log.Info("issue.LoadAttributes: %s", err)
 			break
 		}
@@ -2840,14 +2840,14 @@ func ChangeIssueReaction(ctx *context.Context) {
 // ChangeCommentReaction create a reaction for comment
 func ChangeCommentReaction(ctx *context.Context) {
 	form := web.GetForm(ctx).(*forms.ReactionForm)
-	comment, err := models.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
+	comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
+		ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
 		return
 	}
 
 	if err := comment.LoadIssue(); err != nil {
-		ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
+		ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
 		return
 	}
 
@@ -2874,7 +2874,7 @@ func ChangeCommentReaction(ctx *context.Context) {
 		return
 	}
 
-	if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode && comment.Type != models.CommentTypeReview {
+	if comment.Type != issues_model.CommentTypeComment && comment.Type != issues_model.CommentTypeCode && comment.Type != issues_model.CommentTypeReview {
 		ctx.Error(http.StatusNoContent)
 		return
 	}
@@ -2948,11 +2948,11 @@ func addParticipant(poster *user_model.User, participants []*user_model.User) []
 	return append(participants, poster)
 }
 
-func filterXRefComments(ctx *context.Context, issue *models.Issue) error {
+func filterXRefComments(ctx *context.Context, issue *issues_model.Issue) error {
 	// Remove comments that the user has no permissions to see
 	for i := 0; i < len(issue.Comments); {
 		c := issue.Comments[i]
-		if models.CommentTypeIsRef(c.Type) && c.RefRepoID != issue.RepoID && c.RefRepoID != 0 {
+		if issues_model.CommentTypeIsRef(c.Type) && c.RefRepoID != issue.RepoID && c.RefRepoID != 0 {
 			var err error
 			// Set RefRepo for description in template
 			c.RefRepo, err = repo_model.GetRepositoryByID(c.RefRepoID)
@@ -2985,13 +2985,13 @@ func GetIssueAttachments(ctx *context.Context) {
 
 // GetCommentAttachments returns attachments for the comment
 func GetCommentAttachments(ctx *context.Context) {
-	comment, err := models.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
+	comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
-		ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
+		ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
 		return
 	}
 	attachments := make([]*api.Attachment, 0)
-	if comment.Type == models.CommentTypeComment {
+	if comment.Type == issues_model.CommentTypeComment {
 		if err := comment.LoadAttachments(); err != nil {
 			ctx.ServerError("LoadAttachments", err)
 			return
@@ -3006,9 +3006,9 @@ func GetCommentAttachments(ctx *context.Context) {
 func updateAttachments(ctx *context.Context, item interface{}, files []string) error {
 	var attachments []*repo_model.Attachment
 	switch content := item.(type) {
-	case *models.Issue:
+	case *issues_model.Issue:
 		attachments = content.Attachments
-	case *models.Comment:
+	case *issues_model.Comment:
 		attachments = content.Attachments
 	default:
 		return fmt.Errorf("unknown Type: %T", content)
@@ -3024,9 +3024,9 @@ func updateAttachments(ctx *context.Context, item interface{}, files []string) e
 	var err error
 	if len(files) > 0 {
 		switch content := item.(type) {
-		case *models.Issue:
-			err = models.UpdateIssueAttachments(content.ID, files)
-		case *models.Comment:
+		case *issues_model.Issue:
+			err = issues_model.UpdateIssueAttachments(content.ID, files)
+		case *issues_model.Comment:
 			err = content.UpdateAttachments(files)
 		default:
 			return fmt.Errorf("unknown Type: %T", content)
@@ -3036,9 +3036,9 @@ func updateAttachments(ctx *context.Context, item interface{}, files []string) e
 		}
 	}
 	switch content := item.(type) {
-	case *models.Issue:
+	case *issues_model.Issue:
 		content.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, content.ID)
-	case *models.Comment:
+	case *issues_model.Comment:
 		content.Attachments, err = repo_model.GetAttachmentsByCommentID(ctx, content.ID)
 	default:
 		return fmt.Errorf("unknown Type: %T", content)
@@ -3060,17 +3060,17 @@ func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment,
 }
 
 // combineLabelComments combine the nearby label comments as one.
-func combineLabelComments(issue *models.Issue) {
-	var prev, cur *models.Comment
+func combineLabelComments(issue *issues_model.Issue) {
+	var prev, cur *issues_model.Comment
 	for i := 0; i < len(issue.Comments); i++ {
 		cur = issue.Comments[i]
 		if i > 0 {
 			prev = issue.Comments[i-1]
 		}
-		if i == 0 || cur.Type != models.CommentTypeLabel ||
+		if i == 0 || cur.Type != issues_model.CommentTypeLabel ||
 			(prev != nil && prev.PosterID != cur.PosterID) ||
 			(prev != nil && cur.CreatedUnix-prev.CreatedUnix >= 60) {
-			if cur.Type == models.CommentTypeLabel && cur.Label != nil {
+			if cur.Type == issues_model.CommentTypeLabel && cur.Label != nil {
 				if cur.Content != "1" {
 					cur.RemovedLabels = append(cur.RemovedLabels, cur.Label)
 				} else {
@@ -3081,7 +3081,7 @@ func combineLabelComments(issue *models.Issue) {
 		}
 
 		if cur.Label != nil { // now cur MUST be label comment
-			if prev.Type == models.CommentTypeLabel { // we can combine them only prev is a label comment
+			if prev.Type == issues_model.CommentTypeLabel { // we can combine them only prev is a label comment
 				if cur.Content != "1" {
 					// remove labels from the AddedLabels list if the label that was removed is already
 					// in this list, and if it's not in this list, add the label to RemovedLabels
diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go
index 407832dffe..d8a21c7fd7 100644
--- a/routers/web/repo/issue_content_history.go
+++ b/routers/web/repo/issue_content_history.go
@@ -11,8 +11,7 @@ import (
 	"net/http"
 	"strings"
 
-	"code.gitea.io/gitea/models"
-	issuesModel "code.gitea.io/gitea/models/issues"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
@@ -31,7 +30,7 @@ func GetContentHistoryOverview(ctx *context.Context) {
 	}
 
 	lang := ctx.Locale.Language()
-	editedHistoryCountMap, _ := issuesModel.QueryIssueContentHistoryEditedCountMap(ctx, issue.ID)
+	editedHistoryCountMap, _ := issues_model.QueryIssueContentHistoryEditedCountMap(ctx, issue.ID)
 	ctx.JSON(http.StatusOK, map[string]interface{}{
 		"i18n": map[string]interface{}{
 			"textEdited":                   i18n.Tr(lang, "repo.issues.content_history.edited"),
@@ -51,7 +50,7 @@ func GetContentHistoryList(ctx *context.Context) {
 		return
 	}
 
-	items, _ := issuesModel.FetchIssueContentHistoryList(ctx, issue.ID, commentID)
+	items, _ := issues_model.FetchIssueContentHistoryList(ctx, issue.ID, commentID)
 
 	// render history list to HTML for frontend dropdown items: (name, value)
 	// name is HTML of "avatar + userName + userAction + timeSince"
@@ -89,8 +88,8 @@ func GetContentHistoryList(ctx *context.Context) {
 
 // canSoftDeleteContentHistory checks whether current user can soft-delete a history revision
 // Admins or owners can always delete history revisions. Normal users can only delete own history revisions.
-func canSoftDeleteContentHistory(ctx *context.Context, issue *models.Issue, comment *models.Comment,
-	history *issuesModel.ContentHistory,
+func canSoftDeleteContentHistory(ctx *context.Context, issue *issues_model.Issue, comment *issues_model.Comment,
+	history *issues_model.ContentHistory,
 ) bool {
 	canSoftDelete := false
 	if ctx.Repo.IsOwner() {
@@ -118,7 +117,7 @@ func GetContentHistoryDetail(ctx *context.Context) {
 	}
 
 	historyID := ctx.FormInt64("history_id")
-	history, prevHistory, err := issuesModel.GetIssueContentHistoryAndPrev(ctx, historyID)
+	history, prevHistory, err := issues_model.GetIssueContentHistoryAndPrev(ctx, historyID)
 	if err != nil {
 		ctx.JSON(http.StatusNotFound, map[string]interface{}{
 			"message": "Can not find the content history",
@@ -127,10 +126,10 @@ func GetContentHistoryDetail(ctx *context.Context) {
 	}
 
 	// get the related comment if this history revision is for a comment, otherwise the history revision is for an issue.
-	var comment *models.Comment
+	var comment *issues_model.Comment
 	if history.CommentID != 0 {
 		var err error
-		if comment, err = models.GetCommentByID(ctx, history.CommentID); err != nil {
+		if comment, err = issues_model.GetCommentByID(ctx, history.CommentID); err != nil {
 			log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
 			return
 		}
@@ -186,16 +185,16 @@ func SoftDeleteContentHistory(ctx *context.Context) {
 	commentID := ctx.FormInt64("comment_id")
 	historyID := ctx.FormInt64("history_id")
 
-	var comment *models.Comment
-	var history *issuesModel.ContentHistory
+	var comment *issues_model.Comment
+	var history *issues_model.ContentHistory
 	var err error
 	if commentID != 0 {
-		if comment, err = models.GetCommentByID(ctx, commentID); err != nil {
+		if comment, err = issues_model.GetCommentByID(ctx, commentID); err != nil {
 			log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
 			return
 		}
 	}
-	if history, err = issuesModel.GetIssueContentHistoryByID(ctx, historyID); err != nil {
+	if history, err = issues_model.GetIssueContentHistoryByID(ctx, historyID); err != nil {
 		log.Error("can not get issue content history %v. err=%v", historyID, err)
 		return
 	}
@@ -208,7 +207,7 @@ func SoftDeleteContentHistory(ctx *context.Context) {
 		return
 	}
 
-	err = issuesModel.SoftDeleteIssueContentHistory(ctx, historyID)
+	err = issues_model.SoftDeleteIssueContentHistory(ctx, historyID)
 	log.Debug("soft delete issue content history. issue=%d, comment=%d, history=%d", issue.ID, commentID, historyID)
 	ctx.JSON(http.StatusOK, map[string]interface{}{
 		"ok": err == nil,
diff --git a/routers/web/repo/issue_dependency.go b/routers/web/repo/issue_dependency.go
index ec713238c6..d8d934ea1c 100644
--- a/routers/web/repo/issue_dependency.go
+++ b/routers/web/repo/issue_dependency.go
@@ -7,7 +7,7 @@ package repo
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 )
@@ -15,7 +15,7 @@ import (
 // AddDependency adds new dependencies
 func AddDependency(ctx *context.Context) {
 	issueIndex := ctx.ParamsInt64("index")
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex)
+	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex)
 	if err != nil {
 		ctx.ServerError("GetIssueByIndex", err)
 		return
@@ -38,7 +38,7 @@ func AddDependency(ctx *context.Context) {
 	defer ctx.Redirect(issue.HTMLURL())
 
 	// Dependency
-	dep, err := models.GetIssueByID(depID)
+	dep, err := issues_model.GetIssueByID(ctx, depID)
 	if err != nil {
 		ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_issue_not_exist"))
 		return
@@ -56,12 +56,12 @@ func AddDependency(ctx *context.Context) {
 		return
 	}
 
-	err = models.CreateIssueDependency(ctx.Doer, issue, dep)
+	err = issues_model.CreateIssueDependency(ctx.Doer, issue, dep)
 	if err != nil {
-		if models.IsErrDependencyExists(err) {
+		if issues_model.IsErrDependencyExists(err) {
 			ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_exists"))
 			return
-		} else if models.IsErrCircularDependency(err) {
+		} else if issues_model.IsErrCircularDependency(err) {
 			ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_cannot_create_circular"))
 			return
 		} else {
@@ -74,7 +74,7 @@ func AddDependency(ctx *context.Context) {
 // RemoveDependency removes the dependency
 func RemoveDependency(ctx *context.Context) {
 	issueIndex := ctx.ParamsInt64("index")
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex)
+	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex)
 	if err != nil {
 		ctx.ServerError("GetIssueByIndex", err)
 		return
@@ -96,27 +96,27 @@ func RemoveDependency(ctx *context.Context) {
 	// Dependency Type
 	depTypeStr := ctx.Req.PostForm.Get("dependencyType")
 
-	var depType models.DependencyType
+	var depType issues_model.DependencyType
 
 	switch depTypeStr {
 	case "blockedBy":
-		depType = models.DependencyTypeBlockedBy
+		depType = issues_model.DependencyTypeBlockedBy
 	case "blocking":
-		depType = models.DependencyTypeBlocking
+		depType = issues_model.DependencyTypeBlocking
 	default:
 		ctx.Error(http.StatusBadRequest, "GetDependecyType")
 		return
 	}
 
 	// Dependency
-	dep, err := models.GetIssueByID(depID)
+	dep, err := issues_model.GetIssueByID(ctx, depID)
 	if err != nil {
 		ctx.ServerError("GetIssueByID", err)
 		return
 	}
 
-	if err = models.RemoveIssueDependency(ctx.Doer, issue, dep, depType); err != nil {
-		if models.IsErrDependencyNotExists(err) {
+	if err = issues_model.RemoveIssueDependency(ctx.Doer, issue, dep, depType); err != nil {
+		if issues_model.IsErrDependencyNotExists(err) {
 			ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_not_exist"))
 			return
 		}
diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go
index 2e72d659be..7af415a8fa 100644
--- a/routers/web/repo/issue_label.go
+++ b/routers/web/repo/issue_label.go
@@ -7,8 +7,8 @@ package repo
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/context"
@@ -56,7 +56,7 @@ func InitializeLabels(ctx *context.Context) {
 
 // RetrieveLabels find all the labels of a repository and organization
 func RetrieveLabels(ctx *context.Context) {
-	labels, err := models.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormString("sort"), db.ListOptions{})
+	labels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormString("sort"), db.ListOptions{})
 	if err != nil {
 		ctx.ServerError("RetrieveLabels.GetLabels", err)
 		return
@@ -69,7 +69,7 @@ func RetrieveLabels(ctx *context.Context) {
 	ctx.Data["Labels"] = labels
 
 	if ctx.Repo.Owner.IsOrganization() {
-		orgLabels, err := models.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
+		orgLabels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
 		if err != nil {
 			ctx.ServerError("GetLabelsByOrgID", err)
 			return
@@ -111,13 +111,13 @@ func NewLabel(ctx *context.Context) {
 		return
 	}
 
-	l := &models.Label{
+	l := &issues_model.Label{
 		RepoID:      ctx.Repo.Repository.ID,
 		Name:        form.Title,
 		Description: form.Description,
 		Color:       form.Color,
 	}
-	if err := models.NewLabel(ctx, l); err != nil {
+	if err := issues_model.NewLabel(ctx, l); err != nil {
 		ctx.ServerError("NewLabel", err)
 		return
 	}
@@ -127,10 +127,10 @@ func NewLabel(ctx *context.Context) {
 // UpdateLabel update a label's name and color
 func UpdateLabel(ctx *context.Context) {
 	form := web.GetForm(ctx).(*forms.CreateLabelForm)
-	l, err := models.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, form.ID)
+	l, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, form.ID)
 	if err != nil {
 		switch {
-		case models.IsErrRepoLabelNotExist(err):
+		case issues_model.IsErrRepoLabelNotExist(err):
 			ctx.Error(http.StatusNotFound)
 		default:
 			ctx.ServerError("UpdateLabel", err)
@@ -141,7 +141,7 @@ func UpdateLabel(ctx *context.Context) {
 	l.Name = form.Title
 	l.Description = form.Description
 	l.Color = form.Color
-	if err := models.UpdateLabel(l); err != nil {
+	if err := issues_model.UpdateLabel(l); err != nil {
 		ctx.ServerError("UpdateLabel", err)
 		return
 	}
@@ -150,7 +150,7 @@ func UpdateLabel(ctx *context.Context) {
 
 // DeleteLabel delete a label
 func DeleteLabel(ctx *context.Context) {
-	if err := models.DeleteLabel(ctx.Repo.Repository.ID, ctx.FormInt64("id")); err != nil {
+	if err := issues_model.DeleteLabel(ctx.Repo.Repository.ID, ctx.FormInt64("id")); err != nil {
 		ctx.Flash.Error("DeleteLabel: " + err.Error())
 	} else {
 		ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
@@ -177,9 +177,9 @@ func UpdateIssueLabel(ctx *context.Context) {
 			}
 		}
 	case "attach", "detach", "toggle":
-		label, err := models.GetLabelByID(ctx, ctx.FormInt64("id"))
+		label, err := issues_model.GetLabelByID(ctx, ctx.FormInt64("id"))
 		if err != nil {
-			if models.IsErrRepoLabelNotExist(err) {
+			if issues_model.IsErrRepoLabelNotExist(err) {
 				ctx.Error(http.StatusNotFound, "GetLabelByID")
 			} else {
 				ctx.ServerError("GetLabelByID", err)
@@ -191,7 +191,7 @@ func UpdateIssueLabel(ctx *context.Context) {
 			// detach if any issues already have label, otherwise attach
 			action = "attach"
 			for _, issue := range issues {
-				if models.HasIssueLabel(ctx, issue.ID, label.ID) {
+				if issues_model.HasIssueLabel(ctx, issue.ID, label.ID) {
 					action = "detach"
 					break
 				}
diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go
index 5d7a29ee93..ea078e215c 100644
--- a/routers/web/repo/issue_label_test.go
+++ b/routers/web/repo/issue_label_test.go
@@ -9,7 +9,7 @@ import (
 	"strconv"
 	"testing"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/modules/web"
@@ -37,7 +37,7 @@ func TestInitializeLabels(t *testing.T) {
 	web.SetForm(ctx, &forms.InitializeLabelsForm{TemplateName: "Default"})
 	InitializeLabels(ctx)
 	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
-	unittest.AssertExistsAndLoadBean(t, &models.Label{
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
 		RepoID: 2,
 		Name:   "enhancement",
 		Color:  "#84b6eb",
@@ -62,7 +62,7 @@ func TestRetrieveLabels(t *testing.T) {
 		ctx.Req.Form.Set("sort", testCase.Sort)
 		RetrieveLabels(ctx)
 		assert.False(t, ctx.Written())
-		labels, ok := ctx.Data["Labels"].([]*models.Label)
+		labels, ok := ctx.Data["Labels"].([]*issues_model.Label)
 		assert.True(t, ok)
 		if assert.Len(t, labels, len(testCase.ExpectedLabelIDs)) {
 			for i, label := range labels {
@@ -83,7 +83,7 @@ func TestNewLabel(t *testing.T) {
 	})
 	NewLabel(ctx)
 	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
-	unittest.AssertExistsAndLoadBean(t, &models.Label{
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
 		Name:  "newlabel",
 		Color: "#abcdef",
 	})
@@ -102,7 +102,7 @@ func TestUpdateLabel(t *testing.T) {
 	})
 	UpdateLabel(ctx)
 	assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
-	unittest.AssertExistsAndLoadBean(t, &models.Label{
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
 		ID:    2,
 		Name:  "newnameforlabel",
 		Color: "#abcdef",
@@ -118,8 +118,8 @@ func TestDeleteLabel(t *testing.T) {
 	ctx.Req.Form.Set("id", "2")
 	DeleteLabel(ctx)
 	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
-	unittest.AssertNotExistsBean(t, &models.Label{ID: 2})
-	unittest.AssertNotExistsBean(t, &models.IssueLabel{LabelID: 2})
+	unittest.AssertNotExistsBean(t, &issues_model.Label{ID: 2})
+	unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{LabelID: 2})
 	assert.Equal(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg)
 }
 
@@ -132,9 +132,9 @@ func TestUpdateIssueLabel_Clear(t *testing.T) {
 	ctx.Req.Form.Set("action", "clear")
 	UpdateIssueLabel(ctx)
 	assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
-	unittest.AssertNotExistsBean(t, &models.IssueLabel{IssueID: 1})
-	unittest.AssertNotExistsBean(t, &models.IssueLabel{IssueID: 3})
-	unittest.CheckConsistencyFor(t, &models.Label{})
+	unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: 1})
+	unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: 3})
+	unittest.CheckConsistencyFor(t, &issues_model.Label{})
 }
 
 func TestUpdateIssueLabel_Toggle(t *testing.T) {
@@ -159,11 +159,11 @@ func TestUpdateIssueLabel_Toggle(t *testing.T) {
 		UpdateIssueLabel(ctx)
 		assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
 		for _, issueID := range testCase.IssueIDs {
-			unittest.AssertExistsIf(t, testCase.ExpectedAdd, &models.IssueLabel{
+			unittest.AssertExistsIf(t, testCase.ExpectedAdd, &issues_model.IssueLabel{
 				IssueID: issueID,
 				LabelID: testCase.LabelID,
 			})
 		}
-		unittest.CheckConsistencyFor(t, &models.Label{})
+		unittest.CheckConsistencyFor(t, &issues_model.Label{})
 	}
 }
diff --git a/routers/web/repo/issue_lock.go b/routers/web/repo/issue_lock.go
index 5ac5cac52e..a89ea47571 100644
--- a/routers/web/repo/issue_lock.go
+++ b/routers/web/repo/issue_lock.go
@@ -5,7 +5,7 @@
 package repo
 
 import (
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/services/forms"
@@ -32,7 +32,7 @@ func LockIssue(ctx *context.Context) {
 		return
 	}
 
-	if err := models.LockIssue(&models.IssueLockOptions{
+	if err := issues_model.LockIssue(&issues_model.IssueLockOptions{
 		Doer:   ctx.Doer,
 		Issue:  issue,
 		Reason: form.Reason,
@@ -57,7 +57,7 @@ func UnlockIssue(ctx *context.Context) {
 		return
 	}
 
-	if err := models.UnlockIssue(&models.IssueLockOptions{
+	if err := issues_model.UnlockIssue(&issues_model.IssueLockOptions{
 		Doer:  ctx.Doer,
 		Issue: issue,
 	}); err != nil {
diff --git a/routers/web/repo/issue_stopwatch.go b/routers/web/repo/issue_stopwatch.go
index 4e1f6af039..68f89b258d 100644
--- a/routers/web/repo/issue_stopwatch.go
+++ b/routers/web/repo/issue_stopwatch.go
@@ -8,8 +8,8 @@ import (
 	"net/http"
 	"strings"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/eventsource"
 )
@@ -23,7 +23,7 @@ func IssueStopwatch(c *context.Context) {
 
 	var showSuccessMessage bool
 
-	if !models.StopwatchExists(c.Doer.ID, issue.ID) {
+	if !issues_model.StopwatchExists(c.Doer.ID, issue.ID) {
 		showSuccessMessage = true
 	}
 
@@ -32,7 +32,7 @@ func IssueStopwatch(c *context.Context) {
 		return
 	}
 
-	if err := models.CreateOrStopIssueStopwatch(c.Doer, issue); err != nil {
+	if err := issues_model.CreateOrStopIssueStopwatch(c.Doer, issue); err != nil {
 		c.ServerError("CreateOrStopIssueStopwatch", err)
 		return
 	}
@@ -56,12 +56,12 @@ func CancelStopwatch(c *context.Context) {
 		return
 	}
 
-	if err := models.CancelStopwatch(c.Doer, issue); err != nil {
+	if err := issues_model.CancelStopwatch(c.Doer, issue); err != nil {
 		c.ServerError("CancelStopwatch", err)
 		return
 	}
 
-	stopwatches, err := models.GetUserStopwatches(c.Doer.ID, db.ListOptions{})
+	stopwatches, err := issues_model.GetUserStopwatches(c.Doer.ID, db.ListOptions{})
 	if err != nil {
 		c.ServerError("GetUserStopwatches", err)
 		return
@@ -87,7 +87,7 @@ func GetActiveStopwatch(ctx *context.Context) {
 		return
 	}
 
-	_, sw, err := models.HasUserStopwatch(ctx, ctx.Doer.ID)
+	_, sw, err := issues_model.HasUserStopwatch(ctx, ctx.Doer.ID)
 	if err != nil {
 		ctx.ServerError("HasUserStopwatch", err)
 		return
@@ -97,7 +97,7 @@ func GetActiveStopwatch(ctx *context.Context) {
 		return
 	}
 
-	issue, err := models.GetIssueByID(sw.IssueID)
+	issue, err := issues_model.GetIssueByID(ctx, sw.IssueID)
 	if err != nil || issue == nil {
 		ctx.ServerError("GetIssueByID", err)
 		return
diff --git a/routers/web/repo/issue_test.go b/routers/web/repo/issue_test.go
index debd2a8a3c..ad82fe0f36 100644
--- a/routers/web/repo/issue_test.go
+++ b/routers/web/repo/issue_test.go
@@ -7,7 +7,7 @@ package repo
 import (
 	"testing"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -15,50 +15,50 @@ import (
 func TestCombineLabelComments(t *testing.T) {
 	kases := []struct {
 		name           string
-		beforeCombined []*models.Comment
-		afterCombined  []*models.Comment
+		beforeCombined []*issues_model.Comment
+		afterCombined  []*issues_model.Comment
 	}{
 		{
 			name: "kase 1",
-			beforeCombined: []*models.Comment{
+			beforeCombined: []*issues_model.Comment{
 				{
-					Type:     models.CommentTypeLabel,
+					Type:     issues_model.CommentTypeLabel,
 					PosterID: 1,
 					Content:  "1",
-					Label: &models.Label{
+					Label: &issues_model.Label{
 						Name: "kind/bug",
 					},
 					CreatedUnix: 0,
 				},
 				{
-					Type:     models.CommentTypeLabel,
+					Type:     issues_model.CommentTypeLabel,
 					PosterID: 1,
 					Content:  "",
-					Label: &models.Label{
+					Label: &issues_model.Label{
 						Name: "kind/bug",
 					},
 					CreatedUnix: 0,
 				},
 				{
-					Type:        models.CommentTypeComment,
+					Type:        issues_model.CommentTypeComment,
 					PosterID:    1,
 					Content:     "test",
 					CreatedUnix: 0,
 				},
 			},
-			afterCombined: []*models.Comment{
+			afterCombined: []*issues_model.Comment{
 				{
-					Type:        models.CommentTypeLabel,
+					Type:        issues_model.CommentTypeLabel,
 					PosterID:    1,
 					Content:     "1",
 					CreatedUnix: 0,
-					AddedLabels: []*models.Label{},
-					Label: &models.Label{
+					AddedLabels: []*issues_model.Label{},
+					Label: &issues_model.Label{
 						Name: "kind/bug",
 					},
 				},
 				{
-					Type:        models.CommentTypeComment,
+					Type:        issues_model.CommentTypeComment,
 					PosterID:    1,
 					Content:     "test",
 					CreatedUnix: 0,
@@ -67,63 +67,63 @@ func TestCombineLabelComments(t *testing.T) {
 		},
 		{
 			name: "kase 2",
-			beforeCombined: []*models.Comment{
+			beforeCombined: []*issues_model.Comment{
 				{
-					Type:     models.CommentTypeLabel,
+					Type:     issues_model.CommentTypeLabel,
 					PosterID: 1,
 					Content:  "1",
-					Label: &models.Label{
+					Label: &issues_model.Label{
 						Name: "kind/bug",
 					},
 					CreatedUnix: 0,
 				},
 				{
-					Type:     models.CommentTypeLabel,
+					Type:     issues_model.CommentTypeLabel,
 					PosterID: 1,
 					Content:  "",
-					Label: &models.Label{
+					Label: &issues_model.Label{
 						Name: "kind/bug",
 					},
 					CreatedUnix: 70,
 				},
 				{
-					Type:        models.CommentTypeComment,
+					Type:        issues_model.CommentTypeComment,
 					PosterID:    1,
 					Content:     "test",
 					CreatedUnix: 0,
 				},
 			},
-			afterCombined: []*models.Comment{
+			afterCombined: []*issues_model.Comment{
 				{
-					Type:        models.CommentTypeLabel,
+					Type:        issues_model.CommentTypeLabel,
 					PosterID:    1,
 					Content:     "1",
 					CreatedUnix: 0,
-					AddedLabels: []*models.Label{
+					AddedLabels: []*issues_model.Label{
 						{
 							Name: "kind/bug",
 						},
 					},
-					Label: &models.Label{
+					Label: &issues_model.Label{
 						Name: "kind/bug",
 					},
 				},
 				{
-					Type:        models.CommentTypeLabel,
+					Type:        issues_model.CommentTypeLabel,
 					PosterID:    1,
 					Content:     "",
 					CreatedUnix: 70,
-					RemovedLabels: []*models.Label{
+					RemovedLabels: []*issues_model.Label{
 						{
 							Name: "kind/bug",
 						},
 					},
-					Label: &models.Label{
+					Label: &issues_model.Label{
 						Name: "kind/bug",
 					},
 				},
 				{
-					Type:        models.CommentTypeComment,
+					Type:        issues_model.CommentTypeComment,
 					PosterID:    1,
 					Content:     "test",
 					CreatedUnix: 0,
@@ -132,63 +132,63 @@ func TestCombineLabelComments(t *testing.T) {
 		},
 		{
 			name: "kase 3",
-			beforeCombined: []*models.Comment{
+			beforeCombined: []*issues_model.Comment{
 				{
-					Type:     models.CommentTypeLabel,
+					Type:     issues_model.CommentTypeLabel,
 					PosterID: 1,
 					Content:  "1",
-					Label: &models.Label{
+					Label: &issues_model.Label{
 						Name: "kind/bug",
 					},
 					CreatedUnix: 0,
 				},
 				{
-					Type:     models.CommentTypeLabel,
+					Type:     issues_model.CommentTypeLabel,
 					PosterID: 2,
 					Content:  "",
-					Label: &models.Label{
+					Label: &issues_model.Label{
 						Name: "kind/bug",
 					},
 					CreatedUnix: 0,
 				},
 				{
-					Type:        models.CommentTypeComment,
+					Type:        issues_model.CommentTypeComment,
 					PosterID:    1,
 					Content:     "test",
 					CreatedUnix: 0,
 				},
 			},
-			afterCombined: []*models.Comment{
+			afterCombined: []*issues_model.Comment{
 				{
-					Type:        models.CommentTypeLabel,
+					Type:        issues_model.CommentTypeLabel,
 					PosterID:    1,
 					Content:     "1",
 					CreatedUnix: 0,
-					AddedLabels: []*models.Label{
+					AddedLabels: []*issues_model.Label{
 						{
 							Name: "kind/bug",
 						},
 					},
-					Label: &models.Label{
+					Label: &issues_model.Label{
 						Name: "kind/bug",
 					},
 				},
 				{
-					Type:        models.CommentTypeLabel,
+					Type:        issues_model.CommentTypeLabel,
 					PosterID:    2,
 					Content:     "",
 					CreatedUnix: 0,
-					RemovedLabels: []*models.Label{
+					RemovedLabels: []*issues_model.Label{
 						{
 							Name: "kind/bug",
 						},
 					},
-					Label: &models.Label{
+					Label: &issues_model.Label{
 						Name: "kind/bug",
 					},
 				},
 				{
-					Type:        models.CommentTypeComment,
+					Type:        issues_model.CommentTypeComment,
 					PosterID:    1,
 					Content:     "test",
 					CreatedUnix: 0,
@@ -197,33 +197,33 @@ func TestCombineLabelComments(t *testing.T) {
 		},
 		{
 			name: "kase 4",
-			beforeCombined: []*models.Comment{
+			beforeCombined: []*issues_model.Comment{
 				{
-					Type:     models.CommentTypeLabel,
+					Type:     issues_model.CommentTypeLabel,
 					PosterID: 1,
 					Content:  "1",
-					Label: &models.Label{
+					Label: &issues_model.Label{
 						Name: "kind/bug",
 					},
 					CreatedUnix: 0,
 				},
 				{
-					Type:     models.CommentTypeLabel,
+					Type:     issues_model.CommentTypeLabel,
 					PosterID: 1,
 					Content:  "1",
-					Label: &models.Label{
+					Label: &issues_model.Label{
 						Name: "kind/backport",
 					},
 					CreatedUnix: 10,
 				},
 			},
-			afterCombined: []*models.Comment{
+			afterCombined: []*issues_model.Comment{
 				{
-					Type:        models.CommentTypeLabel,
+					Type:        issues_model.CommentTypeLabel,
 					PosterID:    1,
 					Content:     "1",
 					CreatedUnix: 10,
-					AddedLabels: []*models.Label{
+					AddedLabels: []*issues_model.Label{
 						{
 							Name: "kind/bug",
 						},
@@ -231,7 +231,7 @@ func TestCombineLabelComments(t *testing.T) {
 							Name: "kind/backport",
 						},
 					},
-					Label: &models.Label{
+					Label: &issues_model.Label{
 						Name: "kind/bug",
 					},
 				},
@@ -239,41 +239,41 @@ func TestCombineLabelComments(t *testing.T) {
 		},
 		{
 			name: "kase 5",
-			beforeCombined: []*models.Comment{
+			beforeCombined: []*issues_model.Comment{
 				{
-					Type:     models.CommentTypeLabel,
+					Type:     issues_model.CommentTypeLabel,
 					PosterID: 1,
 					Content:  "1",
-					Label: &models.Label{
+					Label: &issues_model.Label{
 						Name: "kind/bug",
 					},
 					CreatedUnix: 0,
 				},
 				{
-					Type:        models.CommentTypeComment,
+					Type:        issues_model.CommentTypeComment,
 					PosterID:    2,
 					Content:     "testtest",
 					CreatedUnix: 0,
 				},
 				{
-					Type:     models.CommentTypeLabel,
+					Type:     issues_model.CommentTypeLabel,
 					PosterID: 1,
 					Content:  "",
-					Label: &models.Label{
+					Label: &issues_model.Label{
 						Name: "kind/bug",
 					},
 					CreatedUnix: 0,
 				},
 			},
-			afterCombined: []*models.Comment{
+			afterCombined: []*issues_model.Comment{
 				{
-					Type:     models.CommentTypeLabel,
+					Type:     issues_model.CommentTypeLabel,
 					PosterID: 1,
 					Content:  "1",
-					Label: &models.Label{
+					Label: &issues_model.Label{
 						Name: "kind/bug",
 					},
-					AddedLabels: []*models.Label{
+					AddedLabels: []*issues_model.Label{
 						{
 							Name: "kind/bug",
 						},
@@ -281,21 +281,21 @@ func TestCombineLabelComments(t *testing.T) {
 					CreatedUnix: 0,
 				},
 				{
-					Type:        models.CommentTypeComment,
+					Type:        issues_model.CommentTypeComment,
 					PosterID:    2,
 					Content:     "testtest",
 					CreatedUnix: 0,
 				},
 				{
-					Type:     models.CommentTypeLabel,
+					Type:     issues_model.CommentTypeLabel,
 					PosterID: 1,
 					Content:  "",
-					RemovedLabels: []*models.Label{
+					RemovedLabels: []*issues_model.Label{
 						{
 							Name: "kind/bug",
 						},
 					},
-					Label: &models.Label{
+					Label: &issues_model.Label{
 						Name: "kind/bug",
 					},
 					CreatedUnix: 0,
@@ -304,53 +304,53 @@ func TestCombineLabelComments(t *testing.T) {
 		},
 		{
 			name: "kase 6",
-			beforeCombined: []*models.Comment{
+			beforeCombined: []*issues_model.Comment{
 				{
-					Type:     models.CommentTypeLabel,
+					Type:     issues_model.CommentTypeLabel,
 					PosterID: 1,
 					Content:  "1",
-					Label: &models.Label{
+					Label: &issues_model.Label{
 						Name: "kind/bug",
 					},
 					CreatedUnix: 0,
 				},
 				{
-					Type:     models.CommentTypeLabel,
+					Type:     issues_model.CommentTypeLabel,
 					PosterID: 1,
 					Content:  "1",
-					Label: &models.Label{
+					Label: &issues_model.Label{
 						Name: "reviewed/confirmed",
 					},
 					CreatedUnix: 0,
 				},
 				{
-					Type:     models.CommentTypeLabel,
+					Type:     issues_model.CommentTypeLabel,
 					PosterID: 1,
 					Content:  "",
-					Label: &models.Label{
+					Label: &issues_model.Label{
 						Name: "kind/bug",
 					},
 					CreatedUnix: 0,
 				},
 				{
-					Type:     models.CommentTypeLabel,
+					Type:     issues_model.CommentTypeLabel,
 					PosterID: 1,
 					Content:  "1",
-					Label: &models.Label{
+					Label: &issues_model.Label{
 						Name: "kind/feature",
 					},
 					CreatedUnix: 0,
 				},
 			},
-			afterCombined: []*models.Comment{
+			afterCombined: []*issues_model.Comment{
 				{
-					Type:     models.CommentTypeLabel,
+					Type:     issues_model.CommentTypeLabel,
 					PosterID: 1,
 					Content:  "1",
-					Label: &models.Label{
+					Label: &issues_model.Label{
 						Name: "kind/bug",
 					},
-					AddedLabels: []*models.Label{
+					AddedLabels: []*issues_model.Label{
 						{
 							Name: "reviewed/confirmed",
 						},
@@ -366,7 +366,7 @@ func TestCombineLabelComments(t *testing.T) {
 
 	for _, kase := range kases {
 		t.Run(kase.name, func(t *testing.T) {
-			issue := models.Issue{
+			issue := issues_model.Issue{
 				Comments: kase.beforeCombined,
 			}
 			combineLabelComments(&issue)
diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go
index 28274a7f7b..817a2c6d20 100644
--- a/routers/web/repo/issue_timetrack.go
+++ b/routers/web/repo/issue_timetrack.go
@@ -8,8 +8,8 @@ import (
 	"net/http"
 	"time"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
@@ -43,7 +43,7 @@ func AddTimeManually(c *context.Context) {
 		return
 	}
 
-	if _, err := models.AddTime(c.Doer, issue, int64(total.Seconds()), time.Now()); err != nil {
+	if _, err := issues_model.AddTime(c.Doer, issue, int64(total.Seconds()), time.Now()); err != nil {
 		c.ServerError("AddTime", err)
 		return
 	}
@@ -62,7 +62,7 @@ func DeleteTime(c *context.Context) {
 		return
 	}
 
-	t, err := models.GetTrackedTimeByID(c.ParamsInt64(":timeid"))
+	t, err := issues_model.GetTrackedTimeByID(c.ParamsInt64(":timeid"))
 	if err != nil {
 		if db.IsErrNotExist(err) {
 			c.NotFound("time not found", err)
@@ -78,7 +78,7 @@ func DeleteTime(c *context.Context) {
 		return
 	}
 
-	if err = models.DeleteTime(t); err != nil {
+	if err = issues_model.DeleteTime(t); err != nil {
 		c.ServerError("DeleteTime", err)
 		return
 	}
diff --git a/routers/web/repo/issue_watch.go b/routers/web/repo/issue_watch.go
index 53fec11cdc..5210ecf777 100644
--- a/routers/web/repo/issue_watch.go
+++ b/routers/web/repo/issue_watch.go
@@ -8,7 +8,7 @@ import (
 	"net/http"
 	"strconv"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 )
@@ -48,7 +48,7 @@ func IssueWatch(ctx *context.Context) {
 		return
 	}
 
-	if err := models.CreateOrUpdateIssueWatch(ctx.Doer.ID, issue.ID, watch); err != nil {
+	if err := issues_model.CreateOrUpdateIssueWatch(ctx.Doer.ID, issue.ID, watch); err != nil {
 		ctx.ServerError("CreateOrUpdateIssueWatch", err)
 		return
 	}
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index c1805944db..51c891dbf0 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -10,7 +10,7 @@ import (
 	"net/url"
 	"strings"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/perm"
 	project_model "code.gitea.io/gitea/models/project"
 	"code.gitea.io/gitea/models/unit"
@@ -296,13 +296,13 @@ func ViewProject(ctx *context.Context) {
 		boards[0].Title = ctx.Tr("repo.projects.type.uncategorized")
 	}
 
-	issuesMap, err := models.LoadIssuesFromBoardList(boards)
+	issuesMap, err := issues_model.LoadIssuesFromBoardList(boards)
 	if err != nil {
 		ctx.ServerError("LoadIssuesOfBoards", err)
 		return
 	}
 
-	linkedPrsMap := make(map[int64][]*models.Issue)
+	linkedPrsMap := make(map[int64][]*issues_model.Issue)
 	for _, issuesList := range issuesMap {
 		for _, issue := range issuesList {
 			var referencedIds []int64
@@ -313,7 +313,7 @@ func ViewProject(ctx *context.Context) {
 			}
 
 			if len(referencedIds) > 0 {
-				if linkedPrs, err := models.Issues(&models.IssuesOptions{
+				if linkedPrs, err := issues_model.Issues(&issues_model.IssuesOptions{
 					IssueIDs: referencedIds,
 					IsPull:   util.OptionalBoolTrue,
 				}); err == nil {
@@ -358,7 +358,7 @@ func UpdateIssueProject(ctx *context.Context) {
 			continue
 		}
 
-		if err := models.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil {
+		if err := issues_model.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil {
 			ctx.ServerError("ChangeProjectAssign", err)
 			return
 		}
@@ -622,9 +622,9 @@ func MoveIssues(ctx *context.Context) {
 		issueIDs = append(issueIDs, issue.IssueID)
 		sortedIssueIDs[issue.Sorting] = issue.IssueID
 	}
-	movedIssues, err := models.GetIssuesByIDs(ctx, issueIDs)
+	movedIssues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			ctx.NotFound("IssueNotExisting", nil)
 		} else {
 			ctx.ServerError("GetIssueByID", err)
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index c1a59ca8c0..6e8f575ad5 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -19,6 +19,7 @@ import (
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	pull_model "code.gitea.io/gitea/models/pull"
@@ -256,10 +257,10 @@ func ForkPost(ctx *context.Context) {
 	ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
 }
 
-func checkPullInfo(ctx *context.Context) *models.Issue {
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+func checkPullInfo(ctx *context.Context) *issues_model.Issue {
+	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			ctx.NotFound("GetIssueByIndex", err)
 		} else {
 			ctx.ServerError("GetIssueByIndex", err)
@@ -294,7 +295,7 @@ func checkPullInfo(ctx *context.Context) *models.Issue {
 
 	if ctx.IsSigned {
 		// Update issue-user.
-		if err = issue.ReadBy(ctx, ctx.Doer.ID); err != nil {
+		if err = models.SetIssueReadBy(ctx, issue.ID, ctx.Doer.ID); err != nil {
 			ctx.ServerError("ReadBy", err)
 			return nil
 		}
@@ -303,7 +304,7 @@ func checkPullInfo(ctx *context.Context) *models.Issue {
 	return issue
 }
 
-func setMergeTarget(ctx *context.Context, pull *models.PullRequest) {
+func setMergeTarget(ctx *context.Context, pull *issues_model.PullRequest) {
 	if ctx.Repo.Owner.Name == pull.MustHeadUserName() {
 		ctx.Data["HeadTarget"] = pull.HeadBranch
 	} else if pull.HeadRepo == nil {
@@ -317,7 +318,7 @@ func setMergeTarget(ctx *context.Context, pull *models.PullRequest) {
 }
 
 // PrepareMergedViewPullInfo show meta information for a merged pull request view page
-func PrepareMergedViewPullInfo(ctx *context.Context, issue *models.Issue) *git.CompareInfo {
+func PrepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.CompareInfo {
 	pull := issue.PullRequest
 
 	setMergeTarget(ctx, pull)
@@ -395,7 +396,7 @@ func PrepareMergedViewPullInfo(ctx *context.Context, issue *models.Issue) *git.C
 }
 
 // PrepareViewPullInfo show meta information for a pull request preview page
-func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.CompareInfo {
+func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.CompareInfo {
 	ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
 
 	repo := ctx.Repo.Repository
@@ -482,14 +483,14 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare
 		}
 		defer headGitRepo.Close()
 
-		if pull.Flow == models.PullRequestFlowGithub {
+		if pull.Flow == issues_model.PullRequestFlowGithub {
 			headBranchExist = headGitRepo.IsBranchExist(pull.HeadBranch)
 		} else {
 			headBranchExist = git.IsReferenceExist(ctx, baseGitRepo.Path, pull.GetGitRefName())
 		}
 
 		if headBranchExist {
-			if pull.Flow != models.PullRequestFlowGithub {
+			if pull.Flow != issues_model.PullRequestFlowGithub {
 				headBranchSha, err = baseGitRepo.GetRefCommitID(pull.GetGitRefName())
 			} else {
 				headBranchSha, err = headGitRepo.GetBranchCommitID(pull.HeadBranch)
@@ -752,7 +753,7 @@ func ViewPullFiles(ctx *context.Context) {
 	}
 
 	if ctx.IsSigned && ctx.Doer != nil {
-		if ctx.Data["CanMarkConversation"], err = models.CanMarkConversation(issue, ctx.Doer); err != nil {
+		if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(issue, ctx.Doer); err != nil {
 			ctx.ServerError("CanMarkConversation", err)
 			return
 		}
@@ -770,15 +771,15 @@ func ViewPullFiles(ctx *context.Context) {
 		return
 	}
 
-	currentReview, err := models.GetCurrentReview(ctx, ctx.Doer, issue)
-	if err != nil && !models.IsErrReviewNotExist(err) {
+	currentReview, err := issues_model.GetCurrentReview(ctx, ctx.Doer, issue)
+	if err != nil && !issues_model.IsErrReviewNotExist(err) {
 		ctx.ServerError("GetCurrentReview", err)
 		return
 	}
 	numPendingCodeComments := int64(0)
 	if currentReview != nil {
-		numPendingCodeComments, err = models.CountComments(&models.FindCommentsOptions{
-			Type:     models.CommentTypeCode,
+		numPendingCodeComments, err = issues_model.CountComments(&issues_model.FindCommentsOptions{
+			Type:     issues_model.CommentTypeCode,
 			ReviewID: currentReview.ID,
 			IssueID:  issue.ID,
 		})
@@ -1062,7 +1063,7 @@ func MergePullRequest(ctx *context.Context) {
 
 	if form.DeleteBranchAfterMerge {
 		// Don't cleanup when other pr use this branch as head branch
-		exist, err := models.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch)
+		exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch)
 		if err != nil {
 			ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
 			return
@@ -1109,9 +1110,9 @@ func CancelAutoMergePullRequest(ctx *context.Context) {
 	ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, issue.Index))
 }
 
-func stopTimerIfAvailable(user *user_model.User, issue *models.Issue) error {
-	if models.StopwatchExists(user.ID, issue.ID) {
-		if err := models.CreateOrStopIssueStopwatch(user, issue); err != nil {
+func stopTimerIfAvailable(user *user_model.User, issue *issues_model.Issue) error {
+	if issues_model.StopwatchExists(user.ID, issue.ID) {
+		if err := issues_model.CreateOrStopIssueStopwatch(user, issue); err != nil {
 			return err
 		}
 	}
@@ -1190,7 +1191,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
 		return
 	}
 
-	pullIssue := &models.Issue{
+	pullIssue := &issues_model.Issue{
 		RepoID:      repo.ID,
 		Repo:        repo,
 		Title:       form.Title,
@@ -1200,7 +1201,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
 		IsPull:      true,
 		Content:     form.Content,
 	}
-	pullRequest := &models.PullRequest{
+	pullRequest := &issues_model.PullRequest{
 		HeadRepoID:          ci.HeadRepo.ID,
 		BaseRepoID:          repo.ID,
 		HeadBranch:          ci.HeadBranch,
@@ -1208,14 +1209,14 @@ func CompareAndPullRequestPost(ctx *context.Context) {
 		HeadRepo:            ci.HeadRepo,
 		BaseRepo:            repo,
 		MergeBase:           ci.CompareInfo.MergeBase,
-		Type:                models.PullRequestGitea,
+		Type:                issues_model.PullRequestGitea,
 		AllowMaintainerEdit: form.AllowMaintainerEdit,
 	}
 	// FIXME: check error in the case two people send pull request at almost same time, give nice error prompt
 	// instead of 500.
 
 	if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
-		if models.IsErrUserDoesNotHaveAccessToRepo(err) {
+		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
 			return
 		} else if git.IsErrPushRejected(err) {
@@ -1262,7 +1263,7 @@ func CleanUpPullRequest(ctx *context.Context) {
 	}
 
 	// Don't cleanup when there are other PR's that use this branch as head branch.
-	exist, err := models.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch)
+	exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch)
 	if err != nil {
 		ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
 		return
@@ -1356,7 +1357,7 @@ func CleanUpPullRequest(ctx *context.Context) {
 	deleteBranch(ctx, pr, gitRepo)
 }
 
-func deleteBranch(ctx *context.Context, pr *models.PullRequest, gitRepo *git.Repository) {
+func deleteBranch(ctx *context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository) {
 	fullBranchName := pr.HeadRepo.Owner.Name + "/" + pr.HeadBranch
 	if err := repo_service.DeleteBranch(ctx.Doer, pr.HeadRepo, gitRepo, pr.HeadBranch); err != nil {
 		switch {
@@ -1373,7 +1374,7 @@ func deleteBranch(ctx *context.Context, pr *models.PullRequest, gitRepo *git.Rep
 		return
 	}
 
-	if err := models.AddDeletePRBranchComment(ctx, ctx.Doer, pr.BaseRepo, pr.IssueID, pr.HeadBranch); err != nil {
+	if err := issues_model.AddDeletePRBranchComment(ctx, ctx.Doer, pr.BaseRepo, pr.IssueID, pr.HeadBranch); err != nil {
 		// Do not fail here as branch has already been deleted
 		log.Error("DeleteBranch: %v", err)
 	}
@@ -1393,9 +1394,9 @@ func DownloadPullPatch(ctx *context.Context) {
 
 // DownloadPullDiffOrPatch render a pull's raw diff or patch
 func DownloadPullDiffOrPatch(ctx *context.Context, patch bool) {
-	pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrPullRequestNotExist(err) {
+		if issues_model.IsErrPullRequestNotExist(err) {
 			ctx.NotFound("GetPullRequestByIndex", err)
 		} else {
 			ctx.ServerError("GetPullRequestByIndex", err)
@@ -1435,8 +1436,8 @@ func UpdatePullRequestTarget(ctx *context.Context) {
 	}
 
 	if err := pull_service.ChangeTargetBranch(ctx, pr, ctx.Doer, targetBranch); err != nil {
-		if models.IsErrPullRequestAlreadyExists(err) {
-			err := err.(models.ErrPullRequestAlreadyExists)
+		if issues_model.IsErrPullRequestAlreadyExists(err) {
+			err := err.(issues_model.ErrPullRequestAlreadyExists)
 
 			RepoRelPath := ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name
 			errorMessage := ctx.Tr("repo.pulls.has_pull_request", html.EscapeString(ctx.Repo.RepoLink+"/pulls/"+strconv.FormatInt(err.IssueID, 10)), html.EscapeString(RepoRelPath), err.IssueID) // FIXME: Creates url insidde locale string
@@ -1446,7 +1447,7 @@ func UpdatePullRequestTarget(ctx *context.Context) {
 				"error":      err.Error(),
 				"user_error": errorMessage,
 			})
-		} else if models.IsErrIssueIsClosed(err) {
+		} else if issues_model.IsErrIssueIsClosed(err) {
 			errorMessage := ctx.Tr("repo.pulls.is_closed")
 
 			ctx.Flash.Error(errorMessage)
@@ -1486,9 +1487,9 @@ func UpdatePullRequestTarget(ctx *context.Context) {
 func SetAllowEdits(ctx *context.Context) {
 	form := web.GetForm(ctx).(*forms.UpdateAllowEditsForm)
 
-	pr, err := models.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
-		if models.IsErrPullRequestNotExist(err) {
+		if issues_model.IsErrPullRequestNotExist(err) {
 			ctx.NotFound("GetPullRequestByIndex", err)
 		} else {
 			ctx.ServerError("GetPullRequestByIndex", err)
diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go
index e051290200..cc7ae9bbfa 100644
--- a/routers/web/repo/pull_review.go
+++ b/routers/web/repo/pull_review.go
@@ -8,7 +8,7 @@ import (
 	"fmt"
 	"net/http"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	pull_model "code.gitea.io/gitea/models/pull"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/context"
@@ -31,8 +31,8 @@ func RenderNewCodeCommentForm(ctx *context.Context) {
 	if !issue.IsPull {
 		return
 	}
-	currentReview, err := models.GetCurrentReview(ctx, ctx.Doer, issue)
-	if err != nil && !models.IsErrReviewNotExist(err) {
+	currentReview, err := issues_model.GetCurrentReview(ctx, ctx.Doer, issue)
+	if err != nil && !issues_model.IsErrReviewNotExist(err) {
 		ctx.ServerError("GetCurrentReview", err)
 		return
 	}
@@ -107,7 +107,7 @@ func UpdateResolveConversation(ctx *context.Context) {
 	action := ctx.FormString("action")
 	commentID := ctx.FormInt64("comment_id")
 
-	comment, err := models.GetCommentByID(ctx, commentID)
+	comment, err := issues_model.GetCommentByID(ctx, commentID)
 	if err != nil {
 		ctx.ServerError("GetIssueByID", err)
 		return
@@ -119,7 +119,7 @@ func UpdateResolveConversation(ctx *context.Context) {
 	}
 
 	var permResult bool
-	if permResult, err = models.CanMarkConversation(comment.Issue, ctx.Doer); err != nil {
+	if permResult, err = issues_model.CanMarkConversation(comment.Issue, ctx.Doer); err != nil {
 		ctx.ServerError("CanMarkConversation", err)
 		return
 	}
@@ -134,7 +134,7 @@ func UpdateResolveConversation(ctx *context.Context) {
 	}
 
 	if action == "Resolve" || action == "UnResolve" {
-		err = models.MarkConversation(comment, ctx.Doer, action == "Resolve")
+		err = issues_model.MarkConversation(comment, ctx.Doer, action == "Resolve")
 		if err != nil {
 			ctx.ServerError("MarkConversation", err)
 			return
@@ -153,8 +153,8 @@ func UpdateResolveConversation(ctx *context.Context) {
 	})
 }
 
-func renderConversation(ctx *context.Context, comment *models.Comment) {
-	comments, err := models.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line)
+func renderConversation(ctx *context.Context, comment *issues_model.Comment) {
+	comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line)
 	if err != nil {
 		ctx.ServerError("FetchCodeCommentsByLine", err)
 		return
@@ -194,15 +194,15 @@ func SubmitReview(ctx *context.Context) {
 
 	reviewType := form.ReviewType()
 	switch reviewType {
-	case models.ReviewTypeUnknown:
+	case issues_model.ReviewTypeUnknown:
 		ctx.ServerError("ReviewType", fmt.Errorf("unknown ReviewType: %s", form.Type))
 		return
 
 	// can not approve/reject your own PR
-	case models.ReviewTypeApprove, models.ReviewTypeReject:
+	case issues_model.ReviewTypeApprove, issues_model.ReviewTypeReject:
 		if issue.IsPoster(ctx.Doer.ID) {
 			var translated string
-			if reviewType == models.ReviewTypeApprove {
+			if reviewType == issues_model.ReviewTypeApprove {
 				translated = ctx.Tr("repo.issues.review.self.approval")
 			} else {
 				translated = ctx.Tr("repo.issues.review.self.rejection")
@@ -221,7 +221,7 @@ func SubmitReview(ctx *context.Context) {
 
 	_, comm, err := pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, issue, reviewType, form.Content, form.CommitID, attachments)
 	if err != nil {
-		if models.IsContentEmptyErr(err) {
+		if issues_model.IsContentEmptyErr(err) {
 			ctx.Flash.Error(ctx.Tr("repo.issues.review.content.empty"))
 			ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
 		} else {
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index 9b4fc652f1..7fe80a2a4b 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -385,17 +385,17 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 	viewType = ctx.FormString("type")
 	switch viewType {
 	case "assigned":
-		filterMode = models.FilterModeAssign
+		filterMode = issues_model.FilterModeAssign
 	case "created_by":
-		filterMode = models.FilterModeCreate
+		filterMode = issues_model.FilterModeCreate
 	case "mentioned":
-		filterMode = models.FilterModeMention
+		filterMode = issues_model.FilterModeMention
 	case "review_requested":
-		filterMode = models.FilterModeReviewRequested
+		filterMode = issues_model.FilterModeReviewRequested
 	case "your_repositories":
 		fallthrough
 	default:
-		filterMode = models.FilterModeYourRepositories
+		filterMode = issues_model.FilterModeYourRepositories
 		viewType = "your_repositories"
 	}
 
@@ -416,7 +416,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 	}
 
 	isPullList := unitType == unit.TypePullRequests
-	opts := &models.IssuesOptions{
+	opts := &issues_model.IssuesOptions{
 		IsPull:     util.OptionalBoolOf(isPullList),
 		SortType:   sortType,
 		IsArchived: util.OptionalBoolFalse,
@@ -450,15 +450,15 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 	}
 
 	switch filterMode {
-	case models.FilterModeAll:
-	case models.FilterModeYourRepositories:
-	case models.FilterModeAssign:
+	case issues_model.FilterModeAll:
+	case issues_model.FilterModeYourRepositories:
+	case issues_model.FilterModeAssign:
 		opts.AssigneeID = ctx.Doer.ID
-	case models.FilterModeCreate:
+	case issues_model.FilterModeCreate:
 		opts.PosterID = ctx.Doer.ID
-	case models.FilterModeMention:
+	case issues_model.FilterModeMention:
 		opts.MentionedID = ctx.Doer.ID
-	case models.FilterModeReviewRequested:
+	case issues_model.FilterModeReviewRequested:
 		opts.ReviewRequestedID = ctx.Doer.ID
 	}
 
@@ -491,7 +491,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 	// USING NON-FINAL STATE OF opts FOR A QUERY.
 	var issueCountByRepo map[int64]int64
 	if !forceEmpty {
-		issueCountByRepo, err = models.CountIssuesByRepo(opts)
+		issueCountByRepo, err = issues_model.CountIssuesByRepo(opts)
 		if err != nil {
 			ctx.ServerError("CountIssuesByRepo", err)
 			return
@@ -532,15 +532,15 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 
 	// Slice of Issues that will be displayed on the overview page
 	// USING FINAL STATE OF opts FOR A QUERY.
-	var issues []*models.Issue
+	var issues []*issues_model.Issue
 	if !forceEmpty {
-		issues, err = models.Issues(opts)
+		issues, err = issues_model.Issues(opts)
 		if err != nil {
 			ctx.ServerError("Issues", err)
 			return
 		}
 	} else {
-		issues = []*models.Issue{}
+		issues = []*issues_model.Issue{}
 	}
 
 	// ----------------------------------
@@ -578,9 +578,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 	// -------------------------------
 	// Fill stats to post to ctx.Data.
 	// -------------------------------
-	var issueStats *models.IssueStats
+	var issueStats *issues_model.IssueStats
 	if !forceEmpty {
-		statsOpts := models.UserIssueStatsOptions{
+		statsOpts := issues_model.UserIssueStatsOptions{
 			UserID:     ctx.Doer.ID,
 			FilterMode: filterMode,
 			IsPull:     isPullList,
@@ -592,13 +592,13 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 			Team:       team,
 		}
 
-		issueStats, err = models.GetUserIssueStats(statsOpts)
+		issueStats, err = issues_model.GetUserIssueStats(statsOpts)
 		if err != nil {
 			ctx.ServerError("GetUserIssueStats Shown", err)
 			return
 		}
 	} else {
-		issueStats = &models.IssueStats{}
+		issueStats = &issues_model.IssueStats{}
 	}
 
 	// Will be posted to ctx.Data.
@@ -623,7 +623,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 
 	ctx.Data["Issues"] = issues
 
-	approvalCounts, err := models.IssueList(issues).GetApprovalCounts(ctx)
+	approvalCounts, err := issues_model.IssueList(issues).GetApprovalCounts(ctx)
 	if err != nil {
 		ctx.ServerError("ApprovalCounts", err)
 		return
@@ -633,11 +633,11 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 		if !ok || len(counts) == 0 {
 			return 0
 		}
-		reviewTyp := models.ReviewTypeApprove
+		reviewTyp := issues_model.ReviewTypeApprove
 		if typ == "reject" {
-			reviewTyp = models.ReviewTypeReject
+			reviewTyp = issues_model.ReviewTypeReject
 		} else if typ == "waiting" {
-			reviewTyp = models.ReviewTypeRequest
+			reviewTyp = issues_model.ReviewTypeRequest
 		}
 		for _, count := range counts {
 			if count.Type == reviewTyp {
@@ -708,12 +708,12 @@ func getRepoIDs(reposQuery string) []int64 {
 	return repoIDs
 }
 
-func issueIDsFromSearch(ctx *context.Context, ctxUser *user_model.User, keyword string, opts *models.IssuesOptions) ([]int64, error) {
+func issueIDsFromSearch(ctx *context.Context, ctxUser *user_model.User, keyword string, opts *issues_model.IssuesOptions) ([]int64, error) {
 	if len(keyword) == 0 {
 		return []int64{}, nil
 	}
 
-	searchRepoIDs, err := models.GetRepoIDsForIssuesOptions(opts, ctxUser)
+	searchRepoIDs, err := issues_model.GetRepoIDsForIssuesOptions(opts, ctxUser)
 	if err != nil {
 		return nil, fmt.Errorf("GetRepoIDsForIssuesOptions: %v", err)
 	}
diff --git a/routers/web/user/stop_watch.go b/routers/web/user/stop_watch.go
index 4b16c9aeda..f40d850fc1 100644
--- a/routers/web/user/stop_watch.go
+++ b/routers/web/user/stop_watch.go
@@ -7,15 +7,15 @@ package user
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/convert"
 )
 
 // GetStopwatches get all stopwatches
 func GetStopwatches(ctx *context.Context) {
-	sws, err := models.GetUserStopwatches(ctx.Doer.ID, db.ListOptions{
+	sws, err := issues_model.GetUserStopwatches(ctx.Doer.ID, db.ListOptions{
 		Page:     ctx.FormInt("page"),
 		PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
 	})
@@ -24,7 +24,7 @@ func GetStopwatches(ctx *context.Context) {
 		return
 	}
 
-	count, err := models.CountUserStopwatches(ctx.Doer.ID)
+	count, err := issues_model.CountUserStopwatches(ctx.Doer.ID)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, err.Error())
 		return
diff --git a/services/agit/agit.go b/services/agit/agit.go
index cc520dbc76..7666093c51 100644
--- a/services/agit/agit.go
+++ b/services/agit/agit.go
@@ -10,7 +10,8 @@ import (
 	"os"
 	"strings"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
+	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
@@ -97,9 +98,9 @@ func ProcReceive(ctx *context.PrivateContext, opts *private.HookOptions) []priva
 			headBranch = curentTopicBranch
 		}
 
-		pr, err := models.GetUnmergedPullRequest(repo.ID, repo.ID, headBranch, baseBranchName, models.PullRequestFlowAGit)
+		pr, err := issues_model.GetUnmergedPullRequest(repo.ID, repo.ID, headBranch, baseBranchName, issues_model.PullRequestFlowAGit)
 		if err != nil {
-			if !models.IsErrPullRequestNotExist(err) {
+			if !issues_model.IsErrPullRequestNotExist(err) {
 				log.Error("Failed to get unmerged agit flow pull request in repository: %s/%s Error: %v", ownerName, repoName, err)
 				ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
 					"Err": fmt.Sprintf("Failed to get unmerged agit flow pull request in repository: %s/%s Error: %v", ownerName, repoName, err),
@@ -134,7 +135,7 @@ func ProcReceive(ctx *context.PrivateContext, opts *private.HookOptions) []priva
 				return nil
 			}
 
-			prIssue := &models.Issue{
+			prIssue := &issues_model.Issue{
 				RepoID:   repo.ID,
 				Title:    title,
 				PosterID: pusher.ID,
@@ -143,7 +144,7 @@ func ProcReceive(ctx *context.PrivateContext, opts *private.HookOptions) []priva
 				Content:  description,
 			}
 
-			pr := &models.PullRequest{
+			pr := &issues_model.PullRequest{
 				HeadRepoID:   repo.ID,
 				BaseRepoID:   repo.ID,
 				HeadBranch:   headBranch,
@@ -152,12 +153,12 @@ func ProcReceive(ctx *context.PrivateContext, opts *private.HookOptions) []priva
 				HeadRepo:     repo,
 				BaseRepo:     repo,
 				MergeBase:    "",
-				Type:         models.PullRequestGitea,
-				Flow:         models.PullRequestFlowAGit,
+				Type:         issues_model.PullRequestGitea,
+				Flow:         issues_model.PullRequestFlowAGit,
 			}
 
 			if err := pull_service.NewPullRequest(ctx, repo, prIssue, []int64{}, []string{}, pr, []int64{}); err != nil {
-				if models.IsErrUserDoesNotHaveAccessToRepo(err) {
+				if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
 					ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
 					return nil
 				}
@@ -249,7 +250,7 @@ func ProcReceive(ctx *context.PrivateContext, opts *private.HookOptions) []priva
 			})
 			return nil
 		}
-		comment, err := models.CreatePushPullComment(ctx, pusher, pr, oldCommitID, opts.NewCommitIDs[i])
+		comment, err := issues_model.CreatePushPullComment(ctx, pusher, pr, oldCommitID, opts.NewCommitIDs[i])
 		if err == nil && comment != nil {
 			notification.NotifyPullRequestPushCommits(pusher, pr, comment)
 		}
@@ -270,7 +271,7 @@ func ProcReceive(ctx *context.PrivateContext, opts *private.HookOptions) []priva
 
 // UserNameChanged handle user name change for agit flow pull
 func UserNameChanged(user *user_model.User, newName string) error {
-	pulls, err := models.GetAllUnmergedAgitPullRequestByPoster(user.ID)
+	pulls, err := issues_model.GetAllUnmergedAgitPullRequestByPoster(user.ID)
 	if err != nil {
 		return err
 	}
diff --git a/services/asymkey/sign.go b/services/asymkey/sign.go
index 0f74cd4b2a..edfd0f6cad 100644
--- a/services/asymkey/sign.go
+++ b/services/asymkey/sign.go
@@ -9,11 +9,11 @@ import (
 	"fmt"
 	"strings"
 
-	"code.gitea.io/gitea/models"
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
+	issues_model "code.gitea.io/gitea/models/issues"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
@@ -271,7 +271,7 @@ Loop:
 }
 
 // SignMerge determines if we should sign a PR merge commit to the base repository
-func SignMerge(ctx context.Context, pr *models.PullRequest, u *user_model.User, tmpBasePath, baseCommit, headCommit string) (bool, string, *git.Signature, error) {
+func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model.User, tmpBasePath, baseCommit, headCommit string) (bool, string, *git.Signature, error) {
 	if err := pr.LoadBaseRepoCtx(ctx); err != nil {
 		log.Error("Unable to get Base Repo for pull request")
 		return false, "", nil, err
@@ -318,7 +318,7 @@ Loop:
 			if protectedBranch == nil {
 				return false, "", nil, &ErrWontSign{approved}
 			}
-			if models.GetGrantedApprovalsCount(ctx, protectedBranch, pr) < 1 {
+			if issues_model.GetGrantedApprovalsCount(ctx, protectedBranch, pr) < 1 {
 				return false, "", nil, &ErrWontSign{approved}
 			}
 		case baseSigned:
diff --git a/services/automerge/automerge.go b/services/automerge/automerge.go
index 3c7346ab58..d0f83f4a93 100644
--- a/services/automerge/automerge.go
+++ b/services/automerge/automerge.go
@@ -11,8 +11,8 @@ import (
 	"strconv"
 	"strings"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	pull_model "code.gitea.io/gitea/models/pull"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -52,7 +52,7 @@ func handle(data ...queue.Data) []queue.Data {
 	return nil
 }
 
-func addToQueue(pr *models.PullRequest, sha string) {
+func addToQueue(pr *issues_model.PullRequest, sha string) {
 	if err := prAutoMergeQueue.PushFunc(fmt.Sprintf("%d_%s", pr.ID, sha), func() error {
 		log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha)
 		return nil
@@ -62,7 +62,7 @@ func addToQueue(pr *models.PullRequest, sha string) {
 }
 
 // ScheduleAutoMerge if schedule is false and no error, pull can be merged directly
-func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *models.PullRequest, style repo_model.MergeStyle, message string) (scheduled bool, err error) {
+func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest, style repo_model.MergeStyle, message string) (scheduled bool, err error) {
 	err = db.WithTx(func(ctx context.Context) error {
 		lastCommitStatus, err := pull_service.GetPullRequestCommitStatusState(ctx, pull)
 		if err != nil {
@@ -79,27 +79,27 @@ func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *models.
 		}
 		scheduled = true
 
-		_, err = models.CreateAutoMergeComment(ctx, models.CommentTypePRScheduledToAutoMerge, pull, doer)
+		_, err = issues_model.CreateAutoMergeComment(ctx, issues_model.CommentTypePRScheduledToAutoMerge, pull, doer)
 		return err
 	}, ctx)
 	return
 }
 
 // RemoveScheduledAutoMerge cancels a previously scheduled pull request
-func RemoveScheduledAutoMerge(ctx context.Context, doer *user_model.User, pull *models.PullRequest) error {
+func RemoveScheduledAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest) error {
 	return db.WithTx(func(ctx context.Context) error {
 		if err := pull_model.DeleteScheduledAutoMerge(ctx, pull.ID); err != nil {
 			return err
 		}
 
-		_, err := models.CreateAutoMergeComment(ctx, models.CommentTypePRUnScheduledToAutoMerge, pull, doer)
+		_, err := issues_model.CreateAutoMergeComment(ctx, issues_model.CommentTypePRUnScheduledToAutoMerge, pull, doer)
 		return err
 	}, ctx)
 }
 
 // MergeScheduledPullRequest merges a previously scheduled pull request when all checks succeeded
 func MergeScheduledPullRequest(ctx context.Context, sha string, repo *repo_model.Repository) error {
-	pulls, err := getPullRequestsByHeadSHA(ctx, sha, repo, func(pr *models.PullRequest) bool {
+	pulls, err := getPullRequestsByHeadSHA(ctx, sha, repo, func(pr *issues_model.PullRequest) bool {
 		return !pr.HasMerged && pr.CanAutoMerge()
 	})
 	if err != nil {
@@ -113,7 +113,7 @@ func MergeScheduledPullRequest(ctx context.Context, sha string, repo *repo_model
 	return nil
 }
 
-func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*models.PullRequest) bool) (map[int64]*models.PullRequest, error) {
+func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) {
 	gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
 	if err != nil {
 		return nil, err
@@ -125,7 +125,7 @@ func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.
 		return nil, err
 	}
 
-	pulls := make(map[int64]*models.PullRequest)
+	pulls := make(map[int64]*issues_model.PullRequest)
 
 	for _, ref := range refs {
 		// Each pull branch starts with refs/pull/ we then go from there to find the index of the pr and then
@@ -145,10 +145,10 @@ func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.
 				continue
 			}
 
-			p, err := models.GetPullRequestByIndex(ctx, repo.ID, prIndex)
+			p, err := issues_model.GetPullRequestByIndex(ctx, repo.ID, prIndex)
 			if err != nil {
 				// If there is no pull request for this branch, we don't try to merge it.
-				if models.IsErrPullRequestNotExist(err) {
+				if issues_model.IsErrPullRequestNotExist(err) {
 					continue
 				}
 				return nil, err
@@ -168,7 +168,7 @@ func handlePull(pullID int64, sha string) {
 		fmt.Sprintf("Handle AutoMerge of pull[%d] with sha[%s]", pullID, sha))
 	defer finished()
 
-	pr, err := models.GetPullRequestByID(ctx, pullID)
+	pr, err := issues_model.GetPullRequestByID(ctx, pullID)
 	if err != nil {
 		log.Error("GetPullRequestByID[%d]: %v", pullID, err)
 		return
diff --git a/services/comments/comments.go b/services/comments/comments.go
index b80fddf93f..c40631359b 100644
--- a/services/comments/comments.go
+++ b/services/comments/comments.go
@@ -5,9 +5,8 @@
 package comments
 
 import (
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/models/issues"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/notification"
@@ -15,9 +14,9 @@ import (
 )
 
 // CreateIssueComment creates a plain issue comment.
-func CreateIssueComment(doer *user_model.User, repo *repo_model.Repository, issue *models.Issue, content string, attachments []string) (*models.Comment, error) {
-	comment, err := models.CreateComment(&models.CreateCommentOptions{
-		Type:        models.CommentTypeComment,
+func CreateIssueComment(doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content string, attachments []string) (*issues_model.Comment, error) {
+	comment, err := issues_model.CreateComment(&issues_model.CreateCommentOptions{
+		Type:        issues_model.CommentTypeComment,
 		Doer:        doer,
 		Repo:        repo,
 		Issue:       issue,
@@ -28,7 +27,7 @@ func CreateIssueComment(doer *user_model.User, repo *repo_model.Repository, issu
 		return nil, err
 	}
 
-	mentions, err := models.FindAndUpdateIssueMentions(db.DefaultContext, issue, doer, comment.Content)
+	mentions, err := issues_model.FindAndUpdateIssueMentions(db.DefaultContext, issue, doer, comment.Content)
 	if err != nil {
 		return nil, err
 	}
@@ -39,28 +38,28 @@ func CreateIssueComment(doer *user_model.User, repo *repo_model.Repository, issu
 }
 
 // UpdateComment updates information of comment.
-func UpdateComment(c *models.Comment, doer *user_model.User, oldContent string) error {
+func UpdateComment(c *issues_model.Comment, doer *user_model.User, oldContent string) error {
 	needsContentHistory := c.Content != oldContent &&
-		(c.Type == models.CommentTypeComment || c.Type == models.CommentTypeReview || c.Type == models.CommentTypeCode)
+		(c.Type == issues_model.CommentTypeComment || c.Type == issues_model.CommentTypeReview || c.Type == issues_model.CommentTypeCode)
 	if needsContentHistory {
-		hasContentHistory, err := issues.HasIssueContentHistory(db.DefaultContext, c.IssueID, c.ID)
+		hasContentHistory, err := issues_model.HasIssueContentHistory(db.DefaultContext, c.IssueID, c.ID)
 		if err != nil {
 			return err
 		}
 		if !hasContentHistory {
-			if err = issues.SaveIssueContentHistory(db.DefaultContext, c.PosterID, c.IssueID, c.ID,
+			if err = issues_model.SaveIssueContentHistory(db.DefaultContext, c.PosterID, c.IssueID, c.ID,
 				c.CreatedUnix, oldContent, true); err != nil {
 				return err
 			}
 		}
 	}
 
-	if err := models.UpdateComment(c, doer); err != nil {
+	if err := issues_model.UpdateComment(c, doer); err != nil {
 		return err
 	}
 
 	if needsContentHistory {
-		err := issues.SaveIssueContentHistory(db.DefaultContext, doer.ID, c.IssueID, c.ID, timeutil.TimeStampNow(), c.Content, false)
+		err := issues_model.SaveIssueContentHistory(db.DefaultContext, doer.ID, c.IssueID, c.ID, timeutil.TimeStampNow(), c.Content, false)
 		if err != nil {
 			return err
 		}
@@ -72,8 +71,17 @@ func UpdateComment(c *models.Comment, doer *user_model.User, oldContent string)
 }
 
 // DeleteComment deletes the comment
-func DeleteComment(doer *user_model.User, comment *models.Comment) error {
-	if err := models.DeleteComment(comment); err != nil {
+func DeleteComment(doer *user_model.User, comment *issues_model.Comment) error {
+	ctx, committer, err := db.TxContext()
+	if err != nil {
+		return err
+	}
+	defer committer.Close()
+
+	if err := issues_model.DeleteComment(ctx, comment); err != nil {
+		return err
+	}
+	if err := committer.Commit(); err != nil {
 		return err
 	}
 
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index 23ac1abe3c..c9327bbd9b 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -11,6 +11,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	project_model "code.gitea.io/gitea/models/project"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
@@ -636,18 +637,18 @@ func (f *SubmitReviewForm) Validate(req *http.Request, errs binding.Errors) bind
 }
 
 // ReviewType will return the corresponding ReviewType for type
-func (f SubmitReviewForm) ReviewType() models.ReviewType {
+func (f SubmitReviewForm) ReviewType() issues_model.ReviewType {
 	switch f.Type {
 	case "approve":
-		return models.ReviewTypeApprove
+		return issues_model.ReviewTypeApprove
 	case "comment":
-		return models.ReviewTypeComment
+		return issues_model.ReviewTypeComment
 	case "reject":
-		return models.ReviewTypeReject
+		return issues_model.ReviewTypeReject
 	case "":
-		return models.ReviewTypeComment // default to comment when doing quick-submit (Ctrl+Enter) on the review form
+		return issues_model.ReviewTypeComment // default to comment when doing quick-submit (Ctrl+Enter) on the review form
 	default:
-		return models.ReviewTypeUnknown
+		return issues_model.ReviewTypeUnknown
 	}
 }
 
@@ -655,7 +656,7 @@ func (f SubmitReviewForm) ReviewType() models.ReviewType {
 func (f SubmitReviewForm) HasEmptyContent() bool {
 	reviewType := f.ReviewType()
 
-	return (reviewType == models.ReviewTypeComment || reviewType == models.ReviewTypeReject) &&
+	return (reviewType == issues_model.ReviewTypeComment || reviewType == issues_model.ReviewTypeReject) &&
 		len(strings.TrimSpace(f.Content)) == 0
 }
 
diff --git a/services/forms/user_form_hidden_comments.go b/services/forms/user_form_hidden_comments.go
index e0c26e8ddf..35c1a6dd2a 100644
--- a/services/forms/user_form_hidden_comments.go
+++ b/services/forms/user_form_hidden_comments.go
@@ -7,69 +7,69 @@ package forms
 import (
 	"math/big"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 )
 
-type hiddenCommentTypeGroupsType map[string][]models.CommentType
+type hiddenCommentTypeGroupsType map[string][]issues_model.CommentType
 
 // hiddenCommentTypeGroups maps the group names to comment types, these group names comes from the Web UI (appearance.tmpl)
 var hiddenCommentTypeGroups = hiddenCommentTypeGroupsType{
 	"reference": {
-		/*3*/ models.CommentTypeIssueRef,
-		/*4*/ models.CommentTypeCommitRef,
-		/*5*/ models.CommentTypeCommentRef,
-		/*6*/ models.CommentTypePullRef,
+		/*3*/ issues_model.CommentTypeIssueRef,
+		/*4*/ issues_model.CommentTypeCommitRef,
+		/*5*/ issues_model.CommentTypeCommentRef,
+		/*6*/ issues_model.CommentTypePullRef,
 	},
 	"label": {
-		/*7*/ models.CommentTypeLabel,
+		/*7*/ issues_model.CommentTypeLabel,
 	},
 	"milestone": {
-		/*8*/ models.CommentTypeMilestone,
+		/*8*/ issues_model.CommentTypeMilestone,
 	},
 	"assignee": {
-		/*9*/ models.CommentTypeAssignees,
+		/*9*/ issues_model.CommentTypeAssignees,
 	},
 	"title": {
-		/*10*/ models.CommentTypeChangeTitle,
+		/*10*/ issues_model.CommentTypeChangeTitle,
 	},
 	"branch": {
-		/*11*/ models.CommentTypeDeleteBranch,
-		/*25*/ models.CommentTypeChangeTargetBranch,
+		/*11*/ issues_model.CommentTypeDeleteBranch,
+		/*25*/ issues_model.CommentTypeChangeTargetBranch,
 	},
 	"time_tracking": {
-		/*12*/ models.CommentTypeStartTracking,
-		/*13*/ models.CommentTypeStopTracking,
-		/*14*/ models.CommentTypeAddTimeManual,
-		/*15*/ models.CommentTypeCancelTracking,
-		/*26*/ models.CommentTypeDeleteTimeManual,
+		/*12*/ issues_model.CommentTypeStartTracking,
+		/*13*/ issues_model.CommentTypeStopTracking,
+		/*14*/ issues_model.CommentTypeAddTimeManual,
+		/*15*/ issues_model.CommentTypeCancelTracking,
+		/*26*/ issues_model.CommentTypeDeleteTimeManual,
 	},
 	"deadline": {
-		/*16*/ models.CommentTypeAddedDeadline,
-		/*17*/ models.CommentTypeModifiedDeadline,
-		/*18*/ models.CommentTypeRemovedDeadline,
+		/*16*/ issues_model.CommentTypeAddedDeadline,
+		/*17*/ issues_model.CommentTypeModifiedDeadline,
+		/*18*/ issues_model.CommentTypeRemovedDeadline,
 	},
 	"dependency": {
-		/*19*/ models.CommentTypeAddDependency,
-		/*20*/ models.CommentTypeRemoveDependency,
+		/*19*/ issues_model.CommentTypeAddDependency,
+		/*20*/ issues_model.CommentTypeRemoveDependency,
 	},
 	"lock": {
-		/*23*/ models.CommentTypeLock,
-		/*24*/ models.CommentTypeUnlock,
+		/*23*/ issues_model.CommentTypeLock,
+		/*24*/ issues_model.CommentTypeUnlock,
 	},
 	"review_request": {
-		/*27*/ models.CommentTypeReviewRequest,
+		/*27*/ issues_model.CommentTypeReviewRequest,
 	},
 	"pull_request_push": {
-		/*29*/ models.CommentTypePullRequestPush,
+		/*29*/ issues_model.CommentTypePullRequestPush,
 	},
 	"project": {
-		/*30*/ models.CommentTypeProject,
-		/*31*/ models.CommentTypeProjectBoard,
+		/*30*/ issues_model.CommentTypeProject,
+		/*31*/ issues_model.CommentTypeProjectBoard,
 	},
 	"issue_ref": {
-		/*33*/ models.CommentTypeChangeIssueRef,
+		/*33*/ issues_model.CommentTypeChangeIssueRef,
 	},
 }
 
diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go
index e56c2de8fa..97daadbc67 100644
--- a/services/gitdiff/gitdiff.go
+++ b/services/gitdiff/gitdiff.go
@@ -20,9 +20,9 @@ import (
 	"strings"
 	"time"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
+	issues_model "code.gitea.io/gitea/models/issues"
 	pull_model "code.gitea.io/gitea/models/pull"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/analyze"
@@ -82,7 +82,7 @@ type DiffLine struct {
 	Match       int
 	Type        DiffLineType
 	Content     string
-	Comments    []*models.Comment
+	Comments    []*issues_model.Comment
 	SectionInfo *DiffLineSectionInfo
 }
 
@@ -704,8 +704,8 @@ type Diff struct {
 }
 
 // LoadComments loads comments into each line
-func (diff *Diff) LoadComments(ctx context.Context, issue *models.Issue, currentUser *user_model.User) error {
-	allComments, err := models.FetchCodeComments(ctx, issue, currentUser)
+func (diff *Diff) LoadComments(ctx context.Context, issue *issues_model.Issue, currentUser *user_model.User) error {
+	allComments, err := issues_model.FetchCodeComments(ctx, issue, currentUser)
 	if err != nil {
 		return err
 	}
@@ -1520,7 +1520,7 @@ func GetDiff(gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff
 
 // SyncAndGetUserSpecificDiff is like GetDiff, except that user specific data such as which files the given user has already viewed on the given PR will also be set
 // Additionally, the database asynchronously is updated if files have changed since the last review
-func SyncAndGetUserSpecificDiff(ctx context.Context, userID int64, pull *models.PullRequest, gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff, error) {
+func SyncAndGetUserSpecificDiff(ctx context.Context, userID int64, pull *issues_model.PullRequest, gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff, error) {
 	diff, err := GetDiff(gitRepo, opts, files...)
 	if err != nil {
 		return nil, err
@@ -1583,7 +1583,7 @@ outer:
 }
 
 // CommentAsDiff returns c.Patch as *Diff
-func CommentAsDiff(c *models.Comment) (*Diff, error) {
+func CommentAsDiff(c *issues_model.Comment) (*Diff, error) {
 	diff, err := ParsePatch(setting.Git.MaxGitDiffLines,
 		setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(c.Patch), "")
 	if err != nil {
@@ -1601,7 +1601,7 @@ func CommentAsDiff(c *models.Comment) (*Diff, error) {
 }
 
 // CommentMustAsDiff executes AsDiff and logs the error instead of returning
-func CommentMustAsDiff(c *models.Comment) *Diff {
+func CommentMustAsDiff(c *issues_model.Comment) *Diff {
 	if c == nil {
 		return nil
 	}
diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go
index 3457785e5d..caca0e91d8 100644
--- a/services/gitdiff/gitdiff_test.go
+++ b/services/gitdiff/gitdiff_test.go
@@ -12,8 +12,8 @@ import (
 	"strings"
 	"testing"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
@@ -669,7 +669,7 @@ func setupDefaultDiff() *Diff {
 func TestDiff_LoadComments(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 2}).(*models.Issue)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}).(*issues_model.Issue)
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
 	diff := setupDefaultDiff()
 	assert.NoError(t, diff.LoadComments(db.DefaultContext, issue, user))
@@ -678,15 +678,15 @@ func TestDiff_LoadComments(t *testing.T) {
 
 func TestDiffLine_CanComment(t *testing.T) {
 	assert.False(t, (&DiffLine{Type: DiffLineSection}).CanComment())
-	assert.False(t, (&DiffLine{Type: DiffLineAdd, Comments: []*models.Comment{{Content: "bla"}}}).CanComment())
+	assert.False(t, (&DiffLine{Type: DiffLineAdd, Comments: []*issues_model.Comment{{Content: "bla"}}}).CanComment())
 	assert.True(t, (&DiffLine{Type: DiffLineAdd}).CanComment())
 	assert.True(t, (&DiffLine{Type: DiffLineDel}).CanComment())
 	assert.True(t, (&DiffLine{Type: DiffLinePlain}).CanComment())
 }
 
 func TestDiffLine_GetCommentSide(t *testing.T) {
-	assert.Equal(t, "previous", (&DiffLine{Comments: []*models.Comment{{Line: -3}}}).GetCommentSide())
-	assert.Equal(t, "proposed", (&DiffLine{Comments: []*models.Comment{{Line: 3}}}).GetCommentSide())
+	assert.Equal(t, "previous", (&DiffLine{Comments: []*issues_model.Comment{{Line: -3}}}).GetCommentSide())
+	assert.Equal(t, "proposed", (&DiffLine{Comments: []*issues_model.Comment{{Line: 3}}}).GetCommentSide())
 }
 
 func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) {
diff --git a/services/gitdiff/main_test.go b/services/gitdiff/main_test.go
index d4d9364ebf..17d0da6276 100644
--- a/services/gitdiff/main_test.go
+++ b/services/gitdiff/main_test.go
@@ -8,6 +8,7 @@ import (
 	"path/filepath"
 	"testing"
 
+	_ "code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/unittest"
 )
 
diff --git a/services/issue/assignee.go b/services/issue/assignee.go
index 8cad03351c..7c00f472dd 100644
--- a/services/issue/assignee.go
+++ b/services/issue/assignee.go
@@ -7,8 +7,8 @@ package issue
 import (
 	"context"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
@@ -19,7 +19,7 @@ import (
 )
 
 // DeleteNotPassedAssignee deletes all assignees who aren't passed via the "assignees" array
-func DeleteNotPassedAssignee(issue *models.Issue, doer *user_model.User, assignees []*user_model.User) (err error) {
+func DeleteNotPassedAssignee(issue *issues_model.Issue, doer *user_model.User, assignees []*user_model.User) (err error) {
 	var found bool
 	oriAssignes := make([]*user_model.User, len(issue.Assignees))
 	_ = copy(oriAssignes, issue.Assignees)
@@ -45,8 +45,8 @@ func DeleteNotPassedAssignee(issue *models.Issue, doer *user_model.User, assigne
 }
 
 // ToggleAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it.
-func ToggleAssignee(issue *models.Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *models.Comment, err error) {
-	removed, comment, err = models.ToggleIssueAssignee(issue, doer, assigneeID)
+func ToggleAssignee(issue *issues_model.Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *issues_model.Comment, err error) {
+	removed, comment, err = issues_model.ToggleIssueAssignee(issue, doer, assigneeID)
 	if err != nil {
 		return
 	}
@@ -63,11 +63,11 @@ func ToggleAssignee(issue *models.Issue, doer *user_model.User, assigneeID int64
 }
 
 // ReviewRequest add or remove a review request from a user for this PR, and make comment for it.
-func ReviewRequest(issue *models.Issue, doer, reviewer *user_model.User, isAdd bool) (comment *models.Comment, err error) {
+func ReviewRequest(issue *issues_model.Issue, doer, reviewer *user_model.User, isAdd bool) (comment *issues_model.Comment, err error) {
 	if isAdd {
-		comment, err = models.AddReviewRequest(issue, reviewer, doer)
+		comment, err = issues_model.AddReviewRequest(issue, reviewer, doer)
 	} else {
-		comment, err = models.RemoveReviewRequest(issue, reviewer, doer)
+		comment, err = issues_model.RemoveReviewRequest(issue, reviewer, doer)
 	}
 
 	if err != nil {
@@ -82,16 +82,16 @@ func ReviewRequest(issue *models.Issue, doer, reviewer *user_model.User, isAdd b
 }
 
 // IsValidReviewRequest Check permission for ReviewRequest
-func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User, isAdd bool, issue *models.Issue, permDoer *access_model.Permission) error {
+func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User, isAdd bool, issue *issues_model.Issue, permDoer *access_model.Permission) error {
 	if reviewer.IsOrganization() {
-		return models.ErrNotValidReviewRequest{
+		return issues_model.ErrNotValidReviewRequest{
 			Reason: "Organization can't be added as reviewer",
 			UserID: doer.ID,
 			RepoID: issue.Repo.ID,
 		}
 	}
 	if doer.IsOrganization() {
-		return models.ErrNotValidReviewRequest{
+		return issues_model.ErrNotValidReviewRequest{
 			Reason: "Organization can't be doer to add reviewer",
 			UserID: doer.ID,
 			RepoID: issue.Repo.ID,
@@ -111,8 +111,8 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
 		}
 	}
 
-	lastreview, err := models.GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID)
-	if err != nil && !models.IsErrReviewNotExist(err) {
+	lastreview, err := issues_model.GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID)
+	if err != nil && !issues_model.IsErrReviewNotExist(err) {
 		return err
 	}
 
@@ -120,25 +120,25 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
 	if isAdd {
 		pemResult = permReviewer.CanAccessAny(perm.AccessModeRead, unit.TypePullRequests)
 		if !pemResult {
-			return models.ErrNotValidReviewRequest{
+			return issues_model.ErrNotValidReviewRequest{
 				Reason: "Reviewer can't read",
 				UserID: doer.ID,
 				RepoID: issue.Repo.ID,
 			}
 		}
 
-		if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastreview != nil && lastreview.Type != models.ReviewTypeRequest {
+		if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastreview != nil && lastreview.Type != issues_model.ReviewTypeRequest {
 			return nil
 		}
 
 		pemResult = permDoer.CanAccessAny(perm.AccessModeWrite, unit.TypePullRequests)
 		if !pemResult {
-			pemResult, err = models.IsOfficialReviewer(ctx, issue, doer)
+			pemResult, err = issues_model.IsOfficialReviewer(ctx, issue, doer)
 			if err != nil {
 				return err
 			}
 			if !pemResult {
-				return models.ErrNotValidReviewRequest{
+				return issues_model.ErrNotValidReviewRequest{
 					Reason: "Doer can't choose reviewer",
 					UserID: doer.ID,
 					RepoID: issue.Repo.ID,
@@ -147,20 +147,20 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
 		}
 
 		if reviewer.ID == issue.PosterID && issue.OriginalAuthorID == 0 {
-			return models.ErrNotValidReviewRequest{
+			return issues_model.ErrNotValidReviewRequest{
 				Reason: "poster of pr can't be reviewer",
 				UserID: doer.ID,
 				RepoID: issue.Repo.ID,
 			}
 		}
 	} else {
-		if lastreview != nil && lastreview.Type == models.ReviewTypeRequest && lastreview.ReviewerID == doer.ID {
+		if lastreview != nil && lastreview.Type == issues_model.ReviewTypeRequest && lastreview.ReviewerID == doer.ID {
 			return nil
 		}
 
 		pemResult = permDoer.IsAdmin()
 		if !pemResult {
-			return models.ErrNotValidReviewRequest{
+			return issues_model.ErrNotValidReviewRequest{
 				Reason: "Doer is not admin",
 				UserID: doer.ID,
 				RepoID: issue.Repo.ID,
@@ -172,9 +172,9 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
 }
 
 // IsValidTeamReviewRequest Check permission for ReviewRequest Team
-func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team, doer *user_model.User, isAdd bool, issue *models.Issue) error {
+func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team, doer *user_model.User, isAdd bool, issue *issues_model.Issue) error {
 	if doer.IsOrganization() {
-		return models.ErrNotValidReviewRequest{
+		return issues_model.ErrNotValidReviewRequest{
 			Reason: "Organization can't be doer to add reviewer",
 			UserID: doer.ID,
 			RepoID: issue.Repo.ID,
@@ -192,7 +192,7 @@ func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team,
 			hasTeam := organization.HasTeamRepo(ctx, reviewer.OrgID, reviewer.ID, issue.RepoID)
 
 			if !hasTeam {
-				return models.ErrNotValidReviewRequest{
+				return issues_model.ErrNotValidReviewRequest{
 					Reason: "Reviewing team can't read repo",
 					UserID: doer.ID,
 					RepoID: issue.Repo.ID,
@@ -202,13 +202,13 @@ func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team,
 
 		doerCanWrite := permission.CanAccessAny(perm.AccessModeWrite, unit.TypePullRequests)
 		if !doerCanWrite {
-			official, err := models.IsOfficialReviewer(ctx, issue, doer)
+			official, err := issues_model.IsOfficialReviewer(ctx, issue, doer)
 			if err != nil {
 				log.Error("Unable to Check if IsOfficialReviewer for %-v in %-v#%d", doer, issue.Repo, issue.Index)
 				return err
 			}
 			if !official {
-				return models.ErrNotValidReviewRequest{
+				return issues_model.ErrNotValidReviewRequest{
 					Reason: "Doer can't choose reviewer",
 					UserID: doer.ID,
 					RepoID: issue.Repo.ID,
@@ -216,7 +216,7 @@ func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team,
 			}
 		}
 	} else if !permission.IsAdmin() {
-		return models.ErrNotValidReviewRequest{
+		return issues_model.ErrNotValidReviewRequest{
 			Reason: "Only admin users can remove team requests. Doer is not admin",
 			UserID: doer.ID,
 			RepoID: issue.Repo.ID,
@@ -227,11 +227,11 @@ func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team,
 }
 
 // TeamReviewRequest add or remove a review request from a team for this PR, and make comment for it.
-func TeamReviewRequest(issue *models.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool) (comment *models.Comment, err error) {
+func TeamReviewRequest(issue *issues_model.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool) (comment *issues_model.Comment, err error) {
 	if isAdd {
-		comment, err = models.AddTeamReviewRequest(issue, reviewer, doer)
+		comment, err = issues_model.AddTeamReviewRequest(issue, reviewer, doer)
 	} else {
-		comment, err = models.RemoveTeamReviewRequest(issue, reviewer, doer)
+		comment, err = issues_model.RemoveTeamReviewRequest(issue, reviewer, doer)
 	}
 
 	if err != nil {
diff --git a/services/issue/assignee_test.go b/services/issue/assignee_test.go
index ff4d7029eb..5c8b822499 100644
--- a/services/issue/assignee_test.go
+++ b/services/issue/assignee_test.go
@@ -7,8 +7,8 @@ package issue
 import (
 	"testing"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 
@@ -19,7 +19,7 @@ func TestDeleteNotPassedAssignee(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	// Fake issue with assignees
-	issue, err := models.GetIssueWithAttrsByID(1)
+	issue, err := issues_model.GetIssueWithAttrsByID(1)
 	assert.NoError(t, err)
 	assert.EqualValues(t, 1, len(issue.Assignees))
 
@@ -27,7 +27,7 @@ func TestDeleteNotPassedAssignee(t *testing.T) {
 	assert.NoError(t, err)
 
 	// Check if he got removed
-	isAssigned, err := models.IsUserAssignedToIssue(db.DefaultContext, issue, user1)
+	isAssigned, err := issues_model.IsUserAssignedToIssue(db.DefaultContext, issue, user1)
 	assert.NoError(t, err)
 	assert.True(t, isAssigned)
 
diff --git a/services/issue/commit.go b/services/issue/commit.go
index 5140eebed1..1053a81162 100644
--- a/services/issue/commit.go
+++ b/services/issue/commit.go
@@ -15,6 +15,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
@@ -76,22 +77,22 @@ func timeLogToAmount(str string) int64 {
 	return a
 }
 
-func issueAddTime(issue *models.Issue, doer *user_model.User, time time.Time, timeLog string) error {
+func issueAddTime(issue *issues_model.Issue, doer *user_model.User, time time.Time, timeLog string) error {
 	amount := timeLogToAmount(timeLog)
 	if amount == 0 {
 		return nil
 	}
 
-	_, err := models.AddTime(doer, issue, amount, time)
+	_, err := issues_model.AddTime(doer, issue, amount, time)
 	return err
 }
 
 // getIssueFromRef returns the issue referenced by a ref. Returns a nil *Issue
 // if the provided ref references a non-existent issue.
-func getIssueFromRef(repo *repo_model.Repository, index int64) (*models.Issue, error) {
-	issue, err := models.GetIssueByIndex(repo.ID, index)
+func getIssueFromRef(repo *repo_model.Repository, index int64) (*issues_model.Issue, error) {
+	issue, err := issues_model.GetIssueByIndex(repo.ID, index)
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
+		if issues_model.IsErrIssueNotExist(err) {
 			return nil, nil
 		}
 		return nil, err
@@ -112,7 +113,7 @@ func UpdateIssuesCommit(doer *user_model.User, repo *repo_model.Repository, comm
 
 		refMarked := make(map[markKey]bool)
 		var refRepo *repo_model.Repository
-		var refIssue *models.Issue
+		var refIssue *issues_model.Issue
 		var err error
 		for _, ref := range references.FindAllIssueReferences(c.Message) {
 
@@ -153,7 +154,7 @@ func UpdateIssuesCommit(doer *user_model.User, repo *repo_model.Repository, comm
 			}
 
 			message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, html.EscapeString(repo.Link()), html.EscapeString(url.PathEscape(c.Sha1)), html.EscapeString(strings.SplitN(c.Message, "\n", 2)[0]))
-			if err = models.CreateRefComment(doer, refRepo, refIssue, message, c.Sha1); err != nil {
+			if err = issues_model.CreateRefComment(doer, refRepo, refIssue, message, c.Sha1); err != nil {
 				return err
 			}
 
diff --git a/services/issue/commit_test.go b/services/issue/commit_test.go
index 37283a7890..ce3f913627 100644
--- a/services/issue/commit_test.go
+++ b/services/issue/commit_test.go
@@ -8,6 +8,7 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
@@ -50,16 +51,16 @@ func TestUpdateIssuesCommit(t *testing.T) {
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
 	repo.Owner = user
 
-	commentBean := &models.Comment{
-		Type:      models.CommentTypeCommitRef,
+	commentBean := &issues_model.Comment{
+		Type:      issues_model.CommentTypeCommitRef,
 		CommitSHA: "abcdef1",
 		PosterID:  user.ID,
 		IssueID:   1,
 	}
-	issueBean := &models.Issue{RepoID: repo.ID, Index: 4}
+	issueBean := &issues_model.Issue{RepoID: repo.ID, Index: 4}
 
 	unittest.AssertNotExistsBean(t, commentBean)
-	unittest.AssertNotExistsBean(t, &models.Issue{RepoID: repo.ID, Index: 2}, "is_closed=1")
+	unittest.AssertNotExistsBean(t, &issues_model.Issue{RepoID: repo.ID, Index: 2}, "is_closed=1")
 	assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, repo.DefaultBranch))
 	unittest.AssertExistsAndLoadBean(t, commentBean)
 	unittest.AssertExistsAndLoadBean(t, issueBean, "is_closed=1")
@@ -77,16 +78,16 @@ func TestUpdateIssuesCommit(t *testing.T) {
 		},
 	}
 	repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}).(*repo_model.Repository)
-	commentBean = &models.Comment{
-		Type:      models.CommentTypeCommitRef,
+	commentBean = &issues_model.Comment{
+		Type:      issues_model.CommentTypeCommitRef,
 		CommitSHA: "abcdef1",
 		PosterID:  user.ID,
 		IssueID:   6,
 	}
-	issueBean = &models.Issue{RepoID: repo.ID, Index: 1}
+	issueBean = &issues_model.Issue{RepoID: repo.ID, Index: 1}
 
 	unittest.AssertNotExistsBean(t, commentBean)
-	unittest.AssertNotExistsBean(t, &models.Issue{RepoID: repo.ID, Index: 1}, "is_closed=1")
+	unittest.AssertNotExistsBean(t, &issues_model.Issue{RepoID: repo.ID, Index: 1}, "is_closed=1")
 	assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, "non-existing-branch"))
 	unittest.AssertExistsAndLoadBean(t, commentBean)
 	unittest.AssertNotExistsBean(t, issueBean, "is_closed=1")
@@ -103,16 +104,16 @@ func TestUpdateIssuesCommit(t *testing.T) {
 		},
 	}
 	repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}).(*repo_model.Repository)
-	commentBean = &models.Comment{
-		Type:      models.CommentTypeCommitRef,
+	commentBean = &issues_model.Comment{
+		Type:      issues_model.CommentTypeCommitRef,
 		CommitSHA: "abcdef3",
 		PosterID:  user.ID,
 		IssueID:   6,
 	}
-	issueBean = &models.Issue{RepoID: repo.ID, Index: 1}
+	issueBean = &issues_model.Issue{RepoID: repo.ID, Index: 1}
 
 	unittest.AssertNotExistsBean(t, commentBean)
-	unittest.AssertNotExistsBean(t, &models.Issue{RepoID: repo.ID, Index: 1}, "is_closed=1")
+	unittest.AssertNotExistsBean(t, &issues_model.Issue{RepoID: repo.ID, Index: 1}, "is_closed=1")
 	assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, repo.DefaultBranch))
 	unittest.AssertExistsAndLoadBean(t, commentBean)
 	unittest.AssertExistsAndLoadBean(t, issueBean, "is_closed=1")
@@ -136,9 +137,9 @@ func TestUpdateIssuesCommit_Colon(t *testing.T) {
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
 	repo.Owner = user
 
-	issueBean := &models.Issue{RepoID: repo.ID, Index: 4}
+	issueBean := &issues_model.Issue{RepoID: repo.ID, Index: 4}
 
-	unittest.AssertNotExistsBean(t, &models.Issue{RepoID: repo.ID, Index: 2}, "is_closed=1")
+	unittest.AssertNotExistsBean(t, &issues_model.Issue{RepoID: repo.ID, Index: 2}, "is_closed=1")
 	assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, repo.DefaultBranch))
 	unittest.AssertExistsAndLoadBean(t, issueBean, "is_closed=1")
 	unittest.CheckConsistencyFor(t, &models.Action{})
@@ -161,14 +162,14 @@ func TestUpdateIssuesCommit_Issue5957(t *testing.T) {
 	}
 
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository)
-	commentBean := &models.Comment{
-		Type:      models.CommentTypeCommitRef,
+	commentBean := &issues_model.Comment{
+		Type:      issues_model.CommentTypeCommitRef,
 		CommitSHA: "abcdef1",
 		PosterID:  user.ID,
 		IssueID:   7,
 	}
 
-	issueBean := &models.Issue{RepoID: repo.ID, Index: 2, ID: 7}
+	issueBean := &issues_model.Issue{RepoID: repo.ID, Index: 2, ID: 7}
 
 	unittest.AssertNotExistsBean(t, commentBean)
 	unittest.AssertNotExistsBean(t, issueBean, "is_closed=1")
@@ -196,14 +197,14 @@ func TestUpdateIssuesCommit_AnotherRepo(t *testing.T) {
 	}
 
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository)
-	commentBean := &models.Comment{
-		Type:      models.CommentTypeCommitRef,
+	commentBean := &issues_model.Comment{
+		Type:      issues_model.CommentTypeCommitRef,
 		CommitSHA: "abcdef1",
 		PosterID:  user.ID,
 		IssueID:   1,
 	}
 
-	issueBean := &models.Issue{RepoID: 1, Index: 1, ID: 1}
+	issueBean := &issues_model.Issue{RepoID: 1, Index: 1, ID: 1}
 
 	unittest.AssertNotExistsBean(t, commentBean)
 	unittest.AssertNotExistsBean(t, issueBean, "is_closed=1")
@@ -231,14 +232,14 @@ func TestUpdateIssuesCommit_AnotherRepo_FullAddress(t *testing.T) {
 	}
 
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository)
-	commentBean := &models.Comment{
-		Type:      models.CommentTypeCommitRef,
+	commentBean := &issues_model.Comment{
+		Type:      issues_model.CommentTypeCommitRef,
 		CommitSHA: "abcdef1",
 		PosterID:  user.ID,
 		IssueID:   1,
 	}
 
-	issueBean := &models.Issue{RepoID: 1, Index: 1, ID: 1}
+	issueBean := &issues_model.Issue{RepoID: 1, Index: 1, ID: 1}
 
 	unittest.AssertNotExistsBean(t, commentBean)
 	unittest.AssertNotExistsBean(t, issueBean, "is_closed=1")
@@ -274,20 +275,20 @@ func TestUpdateIssuesCommit_AnotherRepoNoPermission(t *testing.T) {
 	}
 
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 6}).(*repo_model.Repository)
-	commentBean := &models.Comment{
-		Type:      models.CommentTypeCommitRef,
+	commentBean := &issues_model.Comment{
+		Type:      issues_model.CommentTypeCommitRef,
 		CommitSHA: "abcdef3",
 		PosterID:  user.ID,
 		IssueID:   6,
 	}
-	commentBean2 := &models.Comment{
-		Type:      models.CommentTypeCommitRef,
+	commentBean2 := &issues_model.Comment{
+		Type:      issues_model.CommentTypeCommitRef,
 		CommitSHA: "abcdef4",
 		PosterID:  user.ID,
 		IssueID:   6,
 	}
 
-	issueBean := &models.Issue{RepoID: 3, Index: 1, ID: 6}
+	issueBean := &issues_model.Issue{RepoID: 3, Index: 1, ID: 6}
 
 	unittest.AssertNotExistsBean(t, commentBean)
 	unittest.AssertNotExistsBean(t, commentBean2)
diff --git a/services/issue/content.go b/services/issue/content.go
index a60878479b..6f493892f4 100644
--- a/services/issue/content.go
+++ b/services/issue/content.go
@@ -5,16 +5,16 @@
 package issue
 
 import (
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/notification"
 )
 
 // ChangeContent changes issue content, as the given user.
-func ChangeContent(issue *models.Issue, doer *user_model.User, content string) (err error) {
+func ChangeContent(issue *issues_model.Issue, doer *user_model.User, content string) (err error) {
 	oldContent := issue.Content
 
-	if err := models.ChangeIssueContent(issue, doer, content); err != nil {
+	if err := issues_model.ChangeIssueContent(issue, doer, content); err != nil {
 		return err
 	}
 
diff --git a/services/issue/issue.go b/services/issue/issue.go
index 78a486727a..ded281e209 100644
--- a/services/issue/issue.go
+++ b/services/issue/issue.go
@@ -8,18 +8,22 @@ import (
 	"fmt"
 
 	"code.gitea.io/gitea/models"
+	admin_model "code.gitea.io/gitea/models/admin"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	access_model "code.gitea.io/gitea/models/perm/access"
+	project_model "code.gitea.io/gitea/models/project"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/notification"
+	"code.gitea.io/gitea/modules/storage"
 	"code.gitea.io/gitea/modules/util"
 )
 
 // NewIssue creates new issue with labels for repository.
-func NewIssue(repo *repo_model.Repository, issue *models.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64) error {
-	if err := models.NewIssue(repo, issue, labelIDs, uuids); err != nil {
+func NewIssue(repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64) error {
+	if err := issues_model.NewIssue(repo, issue, labelIDs, uuids); err != nil {
 		return err
 	}
 
@@ -29,7 +33,7 @@ func NewIssue(repo *repo_model.Repository, issue *models.Issue, labelIDs []int64
 		}
 	}
 
-	mentions, err := models.FindAndUpdateIssueMentions(db.DefaultContext, issue, issue.Poster, issue.Content)
+	mentions, err := issues_model.FindAndUpdateIssueMentions(db.DefaultContext, issue, issue.Poster, issue.Content)
 	if err != nil {
 		return err
 	}
@@ -46,11 +50,11 @@ func NewIssue(repo *repo_model.Repository, issue *models.Issue, labelIDs []int64
 }
 
 // ChangeTitle changes the title of this issue, as the given user.
-func ChangeTitle(issue *models.Issue, doer *user_model.User, title string) (err error) {
+func ChangeTitle(issue *issues_model.Issue, doer *user_model.User, title string) (err error) {
 	oldTitle := issue.Title
 	issue.Title = title
 
-	if err = models.ChangeIssueTitle(issue, doer, oldTitle); err != nil {
+	if err = issues_model.ChangeIssueTitle(issue, doer, oldTitle); err != nil {
 		return
 	}
 
@@ -60,11 +64,11 @@ func ChangeTitle(issue *models.Issue, doer *user_model.User, title string) (err
 }
 
 // ChangeIssueRef changes the branch of this issue, as the given user.
-func ChangeIssueRef(issue *models.Issue, doer *user_model.User, ref string) error {
+func ChangeIssueRef(issue *issues_model.Issue, doer *user_model.User, ref string) error {
 	oldRef := issue.Ref
 	issue.Ref = ref
 
-	if err := models.ChangeIssueRef(issue, doer, oldRef); err != nil {
+	if err := issues_model.ChangeIssueRef(issue, doer, oldRef); err != nil {
 		return err
 	}
 
@@ -79,7 +83,7 @@ func ChangeIssueRef(issue *models.Issue, doer *user_model.User, ref string) erro
 // "assignees" (array): Logins for Users to assign to this issue.
 // Pass one or more user logins to replace the set of assignees on this Issue.
 // Send an empty array ([]) to clear all assignees from the Issue.
-func UpdateAssignees(issue *models.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) {
+func UpdateAssignees(issue *issues_model.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) {
 	var allNewAssignees []*user_model.User
 
 	// Keep the old assignee thingy for compatibility reasons
@@ -129,9 +133,9 @@ func UpdateAssignees(issue *models.Issue, oneAssignee string, multipleAssignees
 }
 
 // DeleteIssue deletes an issue
-func DeleteIssue(doer *user_model.User, gitRepo *git.Repository, issue *models.Issue) error {
+func DeleteIssue(doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue) error {
 	// load issue before deleting it
-	if err := issue.LoadAttributes(); err != nil {
+	if err := issue.LoadAttributes(db.DefaultContext); err != nil {
 		return err
 	}
 	if err := issue.LoadPullRequest(); err != nil {
@@ -139,7 +143,7 @@ func DeleteIssue(doer *user_model.User, gitRepo *git.Repository, issue *models.I
 	}
 
 	// delete entries in database
-	if err := models.DeleteIssue(issue); err != nil {
+	if err := deleteIssue(issue); err != nil {
 		return err
 	}
 
@@ -157,14 +161,14 @@ func DeleteIssue(doer *user_model.User, gitRepo *git.Repository, issue *models.I
 
 // AddAssigneeIfNotAssigned adds an assignee only if he isn't already assigned to the issue.
 // Also checks for access of assigned user
-func AddAssigneeIfNotAssigned(issue *models.Issue, doer *user_model.User, assigneeID int64) (err error) {
+func AddAssigneeIfNotAssigned(issue *issues_model.Issue, doer *user_model.User, assigneeID int64) (err error) {
 	assignee, err := user_model.GetUserByID(assigneeID)
 	if err != nil {
 		return err
 	}
 
 	// Check if the user is already assigned
-	isAssigned, err := models.IsUserAssignedToIssue(db.DefaultContext, issue, assignee)
+	isAssigned, err := issues_model.IsUserAssignedToIssue(db.DefaultContext, issue, assignee)
 	if err != nil {
 		return err
 	}
@@ -178,7 +182,7 @@ func AddAssigneeIfNotAssigned(issue *models.Issue, doer *user_model.User, assign
 		return err
 	}
 	if !valid {
-		return models.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name}
+		return repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name}
 	}
 
 	_, _, err = ToggleAssignee(issue, doer, assigneeID)
@@ -191,7 +195,7 @@ func AddAssigneeIfNotAssigned(issue *models.Issue, doer *user_model.User, assign
 
 // GetRefEndNamesAndURLs retrieves the ref end names (e.g. refs/heads/branch-name -> branch-name)
 // and their respective URLs.
-func GetRefEndNamesAndURLs(issues []*models.Issue, repoLink string) (map[int64]string, map[int64]string) {
+func GetRefEndNamesAndURLs(issues []*issues_model.Issue, repoLink string) (map[int64]string, map[int64]string) {
 	issueRefEndNames := make(map[int64]string, len(issues))
 	issueRefURLs := make(map[int64]string, len(issues))
 	for _, issue := range issues {
@@ -202,3 +206,77 @@ func GetRefEndNamesAndURLs(issues []*models.Issue, repoLink string) (map[int64]s
 	}
 	return issueRefEndNames, issueRefURLs
 }
+
+// deleteIssue deletes the issue
+func deleteIssue(issue *issues_model.Issue) error {
+	ctx, committer, err := db.TxContext()
+	if err != nil {
+		return err
+	}
+	defer committer.Close()
+
+	e := db.GetEngine(ctx)
+	if _, err := e.ID(issue.ID).NoAutoCondition().Delete(issue); err != nil {
+		return err
+	}
+
+	if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, issue.IsClosed); err != nil {
+		return err
+	}
+
+	if err := models.DeleteIssueActions(ctx, issue.RepoID, issue.ID); err != nil {
+		return err
+	}
+
+	// find attachments related to this issue and remove them
+	if err := issue.LoadAttributes(ctx); err != nil {
+		return err
+	}
+
+	for i := range issue.Attachments {
+		admin_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", issue.Attachments[i].RelativePath())
+	}
+
+	// delete all database data still assigned to this issue
+	if err := issues_model.DeleteInIssue(ctx, issue.ID,
+		&issues_model.ContentHistory{},
+		&issues_model.Comment{},
+		&issues_model.IssueLabel{},
+		&issues_model.IssueDependency{},
+		&issues_model.IssueAssignees{},
+		&issues_model.IssueUser{},
+		&models.Notification{},
+		&issues_model.Reaction{},
+		&issues_model.IssueWatch{},
+		&issues_model.Stopwatch{},
+		&issues_model.TrackedTime{},
+		&project_model.ProjectIssue{},
+		&repo_model.Attachment{},
+		&issues_model.PullRequest{},
+	); err != nil {
+		return err
+	}
+
+	// References to this issue in other issues
+	if _, err := db.DeleteByBean(ctx, &issues_model.Comment{
+		RefIssueID: issue.ID,
+	}); err != nil {
+		return err
+	}
+
+	// Delete dependencies for issues in other repositories
+	if _, err := db.DeleteByBean(ctx, &issues_model.IssueDependency{
+		DependencyID: issue.ID,
+	}); err != nil {
+		return err
+	}
+
+	// delete from dependent issues
+	if _, err := db.DeleteByBean(ctx, &issues_model.Comment{
+		DependentIssueID: issue.ID,
+	}); err != nil {
+		return err
+	}
+
+	return committer.Commit()
+}
diff --git a/services/issue/issue_test.go b/services/issue/issue_test.go
index caae773616..20f3a3296c 100644
--- a/services/issue/issue_test.go
+++ b/services/issue/issue_test.go
@@ -7,13 +7,17 @@ package issue
 import (
 	"testing"
 
-	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
 
 	"github.com/stretchr/testify/assert"
 )
 
 func TestGetRefEndNamesAndURLs(t *testing.T) {
-	issues := []*models.Issue{
+	issues := []*issues_model.Issue{
 		{ID: 1, Ref: "refs/heads/branch1"},
 		{ID: 2, Ref: "refs/tags/tag1"},
 		{ID: 3, Ref: "c0ffee"},
@@ -28,3 +32,56 @@ func TestGetRefEndNamesAndURLs(t *testing.T) {
 		3: repoLink + "/src/commit/c0ffee",
 	}, urls)
 }
+
+func TestIssue_DeleteIssue(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	issueIDs, err := issues_model.GetIssueIDsByRepoID(db.DefaultContext, 1)
+	assert.NoError(t, err)
+	assert.EqualValues(t, 5, len(issueIDs))
+
+	issue := &issues_model.Issue{
+		RepoID: 1,
+		ID:     issueIDs[2],
+	}
+
+	err = deleteIssue(issue)
+	assert.NoError(t, err)
+	issueIDs, err = issues_model.GetIssueIDsByRepoID(db.DefaultContext, 1)
+	assert.NoError(t, err)
+	assert.EqualValues(t, 4, len(issueIDs))
+
+	// check attachment removal
+	attachments, err := repo_model.GetAttachmentsByIssueID(db.DefaultContext, 4)
+	assert.NoError(t, err)
+	issue, err = issues_model.GetIssueByID(db.DefaultContext, 4)
+	assert.NoError(t, err)
+	err = deleteIssue(issue)
+	assert.NoError(t, err)
+	assert.EqualValues(t, 2, len(attachments))
+	for i := range attachments {
+		attachment, err := repo_model.GetAttachmentByUUID(db.DefaultContext, attachments[i].UUID)
+		assert.Error(t, err)
+		assert.True(t, repo_model.IsErrAttachmentNotExist(err))
+		assert.Nil(t, attachment)
+	}
+
+	// check issue dependencies
+	user, err := user_model.GetUserByID(1)
+	assert.NoError(t, err)
+	issue1, err := issues_model.GetIssueByID(db.DefaultContext, 1)
+	assert.NoError(t, err)
+	issue2, err := issues_model.GetIssueByID(db.DefaultContext, 2)
+	assert.NoError(t, err)
+	err = issues_model.CreateIssueDependency(user, issue1, issue2)
+	assert.NoError(t, err)
+	left, err := issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1)
+	assert.NoError(t, err)
+	assert.False(t, left)
+
+	err = deleteIssue(issue2)
+	assert.NoError(t, err)
+	left, err = issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1)
+	assert.NoError(t, err)
+	assert.True(t, left)
+}
diff --git a/services/issue/label.go b/services/issue/label.go
index 289466f604..bc5f9b910e 100644
--- a/services/issue/label.go
+++ b/services/issue/label.go
@@ -5,16 +5,16 @@
 package issue
 
 import (
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/notification"
 )
 
 // ClearLabels clears all of an issue's labels
-func ClearLabels(issue *models.Issue, doer *user_model.User) (err error) {
-	if err = models.ClearIssueLabels(issue, doer); err != nil {
+func ClearLabels(issue *issues_model.Issue, doer *user_model.User) (err error) {
+	if err = issues_model.ClearIssueLabels(issue, doer); err != nil {
 		return
 	}
 
@@ -24,18 +24,18 @@ func ClearLabels(issue *models.Issue, doer *user_model.User) (err error) {
 }
 
 // AddLabel adds a new label to the issue.
-func AddLabel(issue *models.Issue, doer *user_model.User, label *models.Label) error {
-	if err := models.NewIssueLabel(issue, label, doer); err != nil {
+func AddLabel(issue *issues_model.Issue, doer *user_model.User, label *issues_model.Label) error {
+	if err := issues_model.NewIssueLabel(issue, label, doer); err != nil {
 		return err
 	}
 
-	notification.NotifyIssueChangeLabels(doer, issue, []*models.Label{label}, nil)
+	notification.NotifyIssueChangeLabels(doer, issue, []*issues_model.Label{label}, nil)
 	return nil
 }
 
 // AddLabels adds a list of new labels to the issue.
-func AddLabels(issue *models.Issue, doer *user_model.User, labels []*models.Label) error {
-	if err := models.NewIssueLabels(issue, labels, doer); err != nil {
+func AddLabels(issue *issues_model.Issue, doer *user_model.User, labels []*issues_model.Label) error {
+	if err := issues_model.NewIssueLabels(issue, labels, doer); err != nil {
 		return err
 	}
 
@@ -44,7 +44,7 @@ func AddLabels(issue *models.Issue, doer *user_model.User, labels []*models.Labe
 }
 
 // RemoveLabel removes a label from issue by given ID.
-func RemoveLabel(issue *models.Issue, doer *user_model.User, label *models.Label) error {
+func RemoveLabel(issue *issues_model.Issue, doer *user_model.User, label *issues_model.Label) error {
 	ctx, committer, err := db.TxContext()
 	if err != nil {
 		return err
@@ -61,12 +61,12 @@ func RemoveLabel(issue *models.Issue, doer *user_model.User, label *models.Label
 	}
 	if !perm.CanWriteIssuesOrPulls(issue.IsPull) {
 		if label.OrgID > 0 {
-			return models.ErrOrgLabelNotExist{}
+			return issues_model.ErrOrgLabelNotExist{}
 		}
-		return models.ErrRepoLabelNotExist{}
+		return issues_model.ErrRepoLabelNotExist{}
 	}
 
-	if err := models.DeleteIssueLabel(ctx, issue, label, doer); err != nil {
+	if err := issues_model.DeleteIssueLabel(ctx, issue, label, doer); err != nil {
 		return err
 	}
 
@@ -74,18 +74,18 @@ func RemoveLabel(issue *models.Issue, doer *user_model.User, label *models.Label
 		return err
 	}
 
-	notification.NotifyIssueChangeLabels(doer, issue, nil, []*models.Label{label})
+	notification.NotifyIssueChangeLabels(doer, issue, nil, []*issues_model.Label{label})
 	return nil
 }
 
 // ReplaceLabels removes all current labels and add new labels to the issue.
-func ReplaceLabels(issue *models.Issue, doer *user_model.User, labels []*models.Label) error {
-	old, err := models.GetLabelsByIssueID(db.DefaultContext, issue.ID)
+func ReplaceLabels(issue *issues_model.Issue, doer *user_model.User, labels []*issues_model.Label) error {
+	old, err := issues_model.GetLabelsByIssueID(db.DefaultContext, issue.ID)
 	if err != nil {
 		return err
 	}
 
-	if err := models.ReplaceIssueLabels(issue, labels, doer); err != nil {
+	if err := issues_model.ReplaceIssueLabels(issue, labels, doer); err != nil {
 		return err
 	}
 
diff --git a/services/issue/label_test.go b/services/issue/label_test.go
index 73e30e894f..120c9ea4f1 100644
--- a/services/issue/label_test.go
+++ b/services/issue/label_test.go
@@ -7,7 +7,7 @@ package issue
 import (
 	"testing"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 
@@ -27,15 +27,15 @@ func TestIssue_AddLabels(t *testing.T) {
 	}
 	for _, test := range tests {
 		assert.NoError(t, unittest.PrepareTestDatabase())
-		issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: test.issueID}).(*models.Issue)
-		labels := make([]*models.Label, len(test.labelIDs))
+		issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: test.issueID}).(*issues_model.Issue)
+		labels := make([]*issues_model.Label, len(test.labelIDs))
 		for i, labelID := range test.labelIDs {
-			labels[i] = unittest.AssertExistsAndLoadBean(t, &models.Label{ID: labelID}).(*models.Label)
+			labels[i] = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID}).(*issues_model.Label)
 		}
 		doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: test.doerID}).(*user_model.User)
 		assert.NoError(t, AddLabels(issue, doer, labels))
 		for _, labelID := range test.labelIDs {
-			unittest.AssertExistsAndLoadBean(t, &models.IssueLabel{IssueID: test.issueID, LabelID: labelID})
+			unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: test.issueID, LabelID: labelID})
 		}
 	}
 }
@@ -53,10 +53,10 @@ func TestIssue_AddLabel(t *testing.T) {
 	}
 	for _, test := range tests {
 		assert.NoError(t, unittest.PrepareTestDatabase())
-		issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: test.issueID}).(*models.Issue)
-		label := unittest.AssertExistsAndLoadBean(t, &models.Label{ID: test.labelID}).(*models.Label)
+		issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: test.issueID}).(*issues_model.Issue)
+		label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: test.labelID}).(*issues_model.Label)
 		doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: test.doerID}).(*user_model.User)
 		assert.NoError(t, AddLabel(issue, doer, label))
-		unittest.AssertExistsAndLoadBean(t, &models.IssueLabel{IssueID: test.issueID, LabelID: test.labelID})
+		unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: test.issueID, LabelID: test.labelID})
 	}
 }
diff --git a/services/issue/milestone.go b/services/issue/milestone.go
index 287f8ae285..af337c3f14 100644
--- a/services/issue/milestone.go
+++ b/services/issue/milestone.go
@@ -8,15 +8,14 @@ import (
 	"context"
 	"fmt"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/notification"
 )
 
-func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *models.Issue, oldMilestoneID int64) error {
-	if err := models.UpdateIssueCols(ctx, issue, "milestone_id"); err != nil {
+func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64) error {
+	if err := issues_model.UpdateIssueCols(ctx, issue, "milestone_id"); err != nil {
 		return err
 	}
 
@@ -37,15 +36,15 @@ func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *mo
 			return err
 		}
 
-		opts := &models.CreateCommentOptions{
-			Type:           models.CommentTypeMilestone,
+		opts := &issues_model.CreateCommentOptions{
+			Type:           issues_model.CommentTypeMilestone,
 			Doer:           doer,
 			Repo:           issue.Repo,
 			Issue:          issue,
 			OldMilestoneID: oldMilestoneID,
 			MilestoneID:    issue.MilestoneID,
 		}
-		if _, err := models.CreateCommentCtx(ctx, opts); err != nil {
+		if _, err := issues_model.CreateCommentCtx(ctx, opts); err != nil {
 			return err
 		}
 	}
@@ -54,7 +53,7 @@ func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *mo
 }
 
 // ChangeMilestoneAssign changes assignment of milestone for issue.
-func ChangeMilestoneAssign(issue *models.Issue, doer *user_model.User, oldMilestoneID int64) (err error) {
+func ChangeMilestoneAssign(issue *issues_model.Issue, doer *user_model.User, oldMilestoneID int64) (err error) {
 	ctx, committer, err := db.TxContext()
 	if err != nil {
 		return err
diff --git a/services/issue/milestone_test.go b/services/issue/milestone_test.go
index 80e37a8acd..d08b1ae8c7 100644
--- a/services/issue/milestone_test.go
+++ b/services/issue/milestone_test.go
@@ -7,7 +7,6 @@ package issue
 import (
 	"testing"
 
-	"code.gitea.io/gitea/models"
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
@@ -17,7 +16,7 @@ import (
 
 func TestChangeMilestoneAssign(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{RepoID: 1}).(*models.Issue)
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: 1}).(*issues_model.Issue)
 	doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
 	assert.NotNil(t, issue)
 	assert.NotNil(t, doer)
@@ -25,11 +24,11 @@ func TestChangeMilestoneAssign(t *testing.T) {
 	oldMilestoneID := issue.MilestoneID
 	issue.MilestoneID = 2
 	assert.NoError(t, ChangeMilestoneAssign(issue, doer, oldMilestoneID))
-	unittest.AssertExistsAndLoadBean(t, &models.Comment{
+	unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
 		IssueID:        issue.ID,
-		Type:           models.CommentTypeMilestone,
+		Type:           issues_model.CommentTypeMilestone,
 		MilestoneID:    issue.MilestoneID,
 		OldMilestoneID: oldMilestoneID,
 	})
-	unittest.CheckConsistencyFor(t, &issues_model.Milestone{}, &models.Issue{})
+	unittest.CheckConsistencyFor(t, &issues_model.Milestone{}, &issues_model.Issue{})
 }
diff --git a/services/issue/status.go b/services/issue/status.go
index d2b4fc303e..0da5c88762 100644
--- a/services/issue/status.go
+++ b/services/issue/status.go
@@ -7,25 +7,25 @@ package issue
 import (
 	"context"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/notification"
 )
 
 // ChangeStatus changes issue status to open or closed.
-func ChangeStatus(issue *models.Issue, doer *user_model.User, closed bool) error {
+func ChangeStatus(issue *issues_model.Issue, doer *user_model.User, closed bool) error {
 	return changeStatusCtx(db.DefaultContext, issue, doer, closed)
 }
 
 // changeStatusCtx changes issue status to open or closed.
 // TODO: if context is not db.DefaultContext we get a deadlock!!!
-func changeStatusCtx(ctx context.Context, issue *models.Issue, doer *user_model.User, closed bool) error {
-	comment, err := models.ChangeIssueStatus(ctx, issue, doer, closed)
+func changeStatusCtx(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, closed bool) error {
+	comment, err := issues_model.ChangeIssueStatus(ctx, issue, doer, closed)
 	if err != nil {
-		if models.IsErrDependenciesLeft(err) && closed {
-			if err := models.FinishIssueStopwatchIfPossible(ctx, doer, issue); err != nil {
+		if issues_model.IsErrDependenciesLeft(err) && closed {
+			if err := issues_model.FinishIssueStopwatchIfPossible(ctx, doer, issue); err != nil {
 				log.Error("Unable to stop stopwatch for issue[%d]#%d: %v", issue.ID, issue.Index, err)
 			}
 		}
@@ -33,7 +33,7 @@ func changeStatusCtx(ctx context.Context, issue *models.Issue, doer *user_model.
 	}
 
 	if closed {
-		if err := models.FinishIssueStopwatchIfPossible(ctx, doer, issue); err != nil {
+		if err := issues_model.FinishIssueStopwatchIfPossible(ctx, doer, issue); err != nil {
 			return err
 		}
 	}
diff --git a/services/mailer/mail.go b/services/mailer/mail.go
index bdd7e25cab..81cfb2e31a 100644
--- a/services/mailer/mail.go
+++ b/services/mailer/mail.go
@@ -19,6 +19,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
@@ -220,10 +221,10 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
 		prefix  string
 		// Fall back subject for bad templates, make sure subject is never empty
 		fallback       string
-		reviewComments []*models.Comment
+		reviewComments []*issues_model.Comment
 	)
 
-	commentType := models.CommentTypeComment
+	commentType := issues_model.CommentTypeComment
 	if ctx.Comment != nil {
 		commentType = ctx.Comment.Type
 		link = ctx.Issue.HTMLURL() + "#" + ctx.Comment.HashTag()
@@ -231,7 +232,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
 		link = ctx.Issue.HTMLURL()
 	}
 
-	reviewType := models.ReviewTypeComment
+	reviewType := issues_model.ReviewTypeComment
 	if ctx.Comment != nil && ctx.Comment.Review != nil {
 		reviewType = ctx.Comment.Review.Type
 	}
@@ -254,7 +255,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
 	fallback = prefix + fallbackMailSubject(ctx.Issue)
 
 	if ctx.Comment != nil && ctx.Comment.Review != nil {
-		reviewComments = make([]*models.Comment, 0, 10)
+		reviewComments = make([]*issues_model.Comment, 0, 10)
 		for _, lines := range ctx.Comment.Review.CodeComments {
 			for _, comments := range lines {
 				reviewComments = append(reviewComments, comments...)
@@ -328,7 +329,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
 	return msgs, nil
 }
 
-func createReference(issue *models.Issue, comment *models.Comment, actionType models.ActionType) string {
+func createReference(issue *issues_model.Issue, comment *issues_model.Comment, actionType models.ActionType) string {
 	var path string
 	if issue.IsPull {
 		path = "pulls"
@@ -400,7 +401,7 @@ func sanitizeSubject(subject string) string {
 }
 
 // SendIssueAssignedMail composes and sends issue assigned email
-func SendIssueAssignedMail(issue *models.Issue, doer *user_model.User, content string, comment *models.Comment, recipients []*user_model.User) error {
+func SendIssueAssignedMail(issue *issues_model.Issue, doer *user_model.User, content string, comment *issues_model.Comment, recipients []*user_model.User) error {
 	if setting.MailService == nil {
 		// No mail service configured
 		return nil
@@ -439,8 +440,8 @@ func SendIssueAssignedMail(issue *models.Issue, doer *user_model.User, content s
 
 // actionToTemplate returns the type and name of the action facing the user
 // (slightly different from models.ActionType) and the name of the template to use (based on availability)
-func actionToTemplate(issue *models.Issue, actionType models.ActionType,
-	commentType models.CommentType, reviewType models.ReviewType,
+func actionToTemplate(issue *issues_model.Issue, actionType models.ActionType,
+	commentType issues_model.CommentType, reviewType issues_model.ReviewType,
 ) (typeName, name, template string) {
 	if issue.IsPull {
 		typeName = "pull"
@@ -464,20 +465,20 @@ func actionToTemplate(issue *models.Issue, actionType models.ActionType,
 		name = "ready_for_review"
 	default:
 		switch commentType {
-		case models.CommentTypeReview:
+		case issues_model.CommentTypeReview:
 			switch reviewType {
-			case models.ReviewTypeApprove:
+			case issues_model.ReviewTypeApprove:
 				name = "approve"
-			case models.ReviewTypeReject:
+			case issues_model.ReviewTypeReject:
 				name = "reject"
 			default:
 				name = "review"
 			}
-		case models.CommentTypeCode:
+		case issues_model.CommentTypeCode:
 			name = "code"
-		case models.CommentTypeAssignees:
+		case issues_model.CommentTypeAssignees:
 			name = "assigned"
-		case models.CommentTypePullRequestPush:
+		case issues_model.CommentTypePullRequestPush:
 			name = "push"
 		default:
 			name = "default"
diff --git a/services/mailer/mail_comment.go b/services/mailer/mail_comment.go
index baecd2a101..95d11ae8a1 100644
--- a/services/mailer/mail_comment.go
+++ b/services/mailer/mail_comment.go
@@ -8,20 +8,21 @@ import (
 	"context"
 
 	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 )
 
 // MailParticipantsComment sends new comment emails to repository watchers and mentioned people.
-func MailParticipantsComment(ctx context.Context, c *models.Comment, opType models.ActionType, issue *models.Issue, mentions []*user_model.User) error {
+func MailParticipantsComment(ctx context.Context, c *issues_model.Comment, opType models.ActionType, issue *issues_model.Issue, mentions []*user_model.User) error {
 	if setting.MailService == nil {
 		// No mail service configured
 		return nil
 	}
 
 	content := c.Content
-	if c.Type == models.CommentTypePullRequestPush {
+	if c.Type == issues_model.CommentTypePullRequestPush {
 		content = ""
 	}
 	if err := mailIssueCommentToParticipants(
@@ -39,7 +40,7 @@ func MailParticipantsComment(ctx context.Context, c *models.Comment, opType mode
 }
 
 // MailMentionsComment sends email to users mentioned in a code comment
-func MailMentionsComment(ctx context.Context, pr *models.PullRequest, c *models.Comment, mentions []*user_model.User) (err error) {
+func MailMentionsComment(ctx context.Context, pr *issues_model.PullRequest, c *issues_model.Comment, mentions []*user_model.User) (err error) {
 	if setting.MailService == nil {
 		// No mail service configured
 		return nil
diff --git a/services/mailer/mail_issue.go b/services/mailer/mail_issue.go
index d479dd0d44..5c330f6e00 100644
--- a/services/mailer/mail_issue.go
+++ b/services/mailer/mail_issue.go
@@ -9,6 +9,7 @@ import (
 	"fmt"
 
 	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
@@ -16,17 +17,17 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 )
 
-func fallbackMailSubject(issue *models.Issue) string {
+func fallbackMailSubject(issue *issues_model.Issue) string {
 	return fmt.Sprintf("[%s] %s (#%d)", issue.Repo.FullName(), issue.Title, issue.Index)
 }
 
 type mailCommentContext struct {
 	context.Context
-	Issue      *models.Issue
+	Issue      *issues_model.Issue
 	Doer       *user_model.User
 	ActionType models.ActionType
 	Content    string
-	Comment    *models.Comment
+	Comment    *issues_model.Comment
 }
 
 const (
@@ -57,21 +58,21 @@ func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []*user_mo
 	unfiltered[0] = ctx.Issue.PosterID
 
 	// =========== Assignees ===========
-	ids, err := models.GetAssigneeIDsByIssue(ctx.Issue.ID)
+	ids, err := issues_model.GetAssigneeIDsByIssue(ctx.Issue.ID)
 	if err != nil {
 		return fmt.Errorf("GetAssigneeIDsByIssue(%d): %v", ctx.Issue.ID, err)
 	}
 	unfiltered = append(unfiltered, ids...)
 
 	// =========== Participants (i.e. commenters, reviewers) ===========
-	ids, err = models.GetParticipantsIDsByIssueID(ctx.Issue.ID)
+	ids, err = issues_model.GetParticipantsIDsByIssueID(ctx.Issue.ID)
 	if err != nil {
 		return fmt.Errorf("GetParticipantsIDsByIssueID(%d): %v", ctx.Issue.ID, err)
 	}
 	unfiltered = append(unfiltered, ids...)
 
 	// =========== Issue watchers ===========
-	ids, err = models.GetIssueWatchersIDs(ctx, ctx.Issue.ID, true)
+	ids, err = issues_model.GetIssueWatchersIDs(ctx, ctx.Issue.ID, true)
 	if err != nil {
 		return fmt.Errorf("GetIssueWatchersIDs(%d): %v", ctx.Issue.ID, err)
 	}
@@ -98,7 +99,7 @@ func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []*user_mo
 	}
 
 	// Avoid mailing explicit unwatched
-	ids, err = models.GetIssueWatchersIDs(ctx, ctx.Issue.ID, false)
+	ids, err = issues_model.GetIssueWatchersIDs(ctx, ctx.Issue.ID, false)
 	if err != nil {
 		return fmt.Errorf("GetIssueWatchersIDs(%d): %v", ctx.Issue.ID, err)
 	}
@@ -171,7 +172,7 @@ func mailIssueCommentBatch(ctx *mailCommentContext, users []*user_model.User, vi
 
 // MailParticipants sends new issue thread created emails to repository watchers
 // and mentioned people.
-func MailParticipants(issue *models.Issue, doer *user_model.User, opType models.ActionType, mentions []*user_model.User) error {
+func MailParticipants(issue *issues_model.Issue, doer *user_model.User, opType models.ActionType, mentions []*user_model.User) error {
 	if setting.MailService == nil {
 		// No mail service configured
 		return nil
diff --git a/services/mailer/mail_test.go b/services/mailer/mail_test.go
index baf426146a..83955a5896 100644
--- a/services/mailer/mail_test.go
+++ b/services/mailer/mail_test.go
@@ -15,6 +15,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
@@ -46,7 +47,7 @@ const bodyTpl = `
 </html>
 `
 
-func prepareMailerTest(t *testing.T) (doer *user_model.User, repo *repo_model.Repository, issue *models.Issue, comment *models.Comment) {
+func prepareMailerTest(t *testing.T) (doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, comment *issues_model.Comment) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 	mailService := setting.Mailer{
 		From: "test@gitea.com",
@@ -57,9 +58,9 @@ func prepareMailerTest(t *testing.T) (doer *user_model.User, repo *repo_model.Re
 
 	doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
 	repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, Owner: doer}).(*repo_model.Repository)
-	issue = unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 1, Repo: repo, Poster: doer}).(*models.Issue)
+	issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1, Repo: repo, Poster: doer}).(*issues_model.Issue)
 	assert.NoError(t, issue.LoadRepo(db.DefaultContext))
-	comment = unittest.AssertExistsAndLoadBean(t, &models.Comment{ID: 2, Issue: issue}).(*models.Comment)
+	comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2, Issue: issue}).(*issues_model.Comment)
 	return
 }
 
@@ -162,8 +163,8 @@ func TestTemplateSelection(t *testing.T) {
 	}, recipients, false, "TestTemplateSelection")
 	expect(t, msg, "issue/default/subject", "issue/default/body")
 
-	pull := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 2, Repo: repo, Poster: doer}).(*models.Issue)
-	comment = unittest.AssertExistsAndLoadBean(t, &models.Comment{ID: 4, Issue: pull}).(*models.Comment)
+	pull := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2, Repo: repo, Poster: doer}).(*issues_model.Issue)
+	comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 4, Issue: pull}).(*issues_model.Comment)
 	msg = testComposeIssueCommentMessage(t, &mailCommentContext{
 		Context: context.TODO(), // TODO: use a correct context
 		Issue:   pull, Doer: doer, ActionType: models.ActionCommentPull,
@@ -183,7 +184,7 @@ func TestTemplateServices(t *testing.T) {
 	doer, _, issue, comment := prepareMailerTest(t)
 	assert.NoError(t, issue.LoadRepo(db.DefaultContext))
 
-	expect := func(t *testing.T, issue *models.Issue, comment *models.Comment, doer *user_model.User,
+	expect := func(t *testing.T, issue *issues_model.Issue, comment *issues_model.Comment, doer *user_model.User,
 		actionType models.ActionType, fromMention bool, tplSubject, tplBody, expSubject, expBody string,
 	) {
 		stpl := texttmpl.Must(texttmpl.New("issue/default").Parse(tplSubject))
@@ -268,8 +269,8 @@ func Test_createReference(t *testing.T) {
 	pullIssue.IsPull = true
 
 	type args struct {
-		issue      *models.Issue
-		comment    *models.Comment
+		issue      *issues_model.Issue
+		comment    *issues_model.Comment
 		actionType models.ActionType
 	}
 	tests := []struct {
diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go
index 408704adef..e71b2ca17a 100644
--- a/services/migrations/gitea_uploader.go
+++ b/services/migrations/gitea_uploader.go
@@ -45,14 +45,14 @@ type GiteaLocalUploader struct {
 	repoOwner      string
 	repoName       string
 	repo           *repo_model.Repository
-	labels         map[string]*models.Label
+	labels         map[string]*issues_model.Label
 	milestones     map[string]int64
-	issues         map[int64]*models.Issue
+	issues         map[int64]*issues_model.Issue
 	gitRepo        *git.Repository
 	prHeadCache    map[string]struct{}
 	sameApp        bool
 	userMap        map[int64]int64 // external user id mapping to user id
-	prCache        map[int64]*models.PullRequest
+	prCache        map[int64]*issues_model.PullRequest
 	gitServiceType structs.GitServiceType
 }
 
@@ -63,12 +63,12 @@ func NewGiteaLocalUploader(ctx context.Context, doer *user_model.User, repoOwner
 		doer:        doer,
 		repoOwner:   repoOwner,
 		repoName:    repoName,
-		labels:      make(map[string]*models.Label),
+		labels:      make(map[string]*issues_model.Label),
 		milestones:  make(map[string]int64),
-		issues:      make(map[int64]*models.Issue),
+		issues:      make(map[int64]*issues_model.Issue),
 		prHeadCache: make(map[string]struct{}),
 		userMap:     make(map[int64]int64),
-		prCache:     make(map[int64]*models.PullRequest),
+		prCache:     make(map[int64]*issues_model.PullRequest),
 	}
 }
 
@@ -76,17 +76,17 @@ func NewGiteaLocalUploader(ctx context.Context, doer *user_model.User, repoOwner
 func (g *GiteaLocalUploader) MaxBatchInsertSize(tp string) int {
 	switch tp {
 	case "issue":
-		return db.MaxBatchInsertSize(new(models.Issue))
+		return db.MaxBatchInsertSize(new(issues_model.Issue))
 	case "comment":
-		return db.MaxBatchInsertSize(new(models.Comment))
+		return db.MaxBatchInsertSize(new(issues_model.Comment))
 	case "milestone":
 		return db.MaxBatchInsertSize(new(issues_model.Milestone))
 	case "label":
-		return db.MaxBatchInsertSize(new(models.Label))
+		return db.MaxBatchInsertSize(new(issues_model.Label))
 	case "release":
 		return db.MaxBatchInsertSize(new(models.Release))
 	case "pullrequest":
-		return db.MaxBatchInsertSize(new(models.PullRequest))
+		return db.MaxBatchInsertSize(new(issues_model.PullRequest))
 	}
 	return 10
 }
@@ -216,9 +216,9 @@ func (g *GiteaLocalUploader) CreateMilestones(milestones ...*base.Milestone) err
 
 // CreateLabels creates labels
 func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error {
-	lbs := make([]*models.Label, 0, len(labels))
+	lbs := make([]*issues_model.Label, 0, len(labels))
 	for _, label := range labels {
-		lbs = append(lbs, &models.Label{
+		lbs = append(lbs, &issues_model.Label{
 			RepoID:      g.repo.ID,
 			Name:        label.Name,
 			Description: label.Description,
@@ -226,7 +226,7 @@ func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error {
 		})
 	}
 
-	err := models.NewLabels(lbs...)
+	err := issues_model.NewLabels(lbs...)
 	if err != nil {
 		return err
 	}
@@ -339,9 +339,9 @@ func (g *GiteaLocalUploader) SyncTags() error {
 
 // CreateIssues creates issues
 func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error {
-	iss := make([]*models.Issue, 0, len(issues))
+	iss := make([]*issues_model.Issue, 0, len(issues))
 	for _, issue := range issues {
-		var labels []*models.Label
+		var labels []*issues_model.Label
 		for _, label := range issue.Labels {
 			lb, ok := g.labels[label.Name]
 			if ok {
@@ -366,7 +366,7 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error {
 			}
 		}
 
-		is := models.Issue{
+		is := issues_model.Issue{
 			RepoID:      g.repo.ID,
 			Repo:        g.repo,
 			Index:       issue.Number,
@@ -423,9 +423,9 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error {
 
 // CreateComments creates comments of issues
 func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
-	cms := make([]*models.Comment, 0, len(comments))
+	cms := make([]*issues_model.Comment, 0, len(comments))
 	for _, comment := range comments {
-		var issue *models.Issue
+		var issue *issues_model.Issue
 		issue, ok := g.issues[comment.IssueIndex]
 		if !ok {
 			return fmt.Errorf("comment references non existent IssueIndex %d", comment.IssueIndex)
@@ -438,9 +438,9 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
 			comment.Updated = comment.Created
 		}
 
-		cm := models.Comment{
+		cm := issues_model.Comment{
 			IssueID:     issue.ID,
-			Type:        models.CommentTypeComment,
+			Type:        issues_model.CommentTypeComment,
 			Content:     comment.Content,
 			CreatedUnix: timeutil.TimeStamp(comment.Created.Unix()),
 			UpdatedUnix: timeutil.TimeStamp(comment.Updated.Unix()),
@@ -473,7 +473,7 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
 
 // CreatePullRequests creates pull requests
 func (g *GiteaLocalUploader) CreatePullRequests(prs ...*base.PullRequest) error {
-	gprs := make([]*models.PullRequest, 0, len(prs))
+	gprs := make([]*issues_model.PullRequest, 0, len(prs))
 	for _, pr := range prs {
 		gpr, err := g.newPullRequest(pr)
 		if err != nil {
@@ -600,8 +600,8 @@ func (g *GiteaLocalUploader) updateGitForPullRequest(pr *base.PullRequest) (head
 	return head, nil
 }
 
-func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullRequest, error) {
-	var labels []*models.Label
+func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*issues_model.PullRequest, error) {
+	var labels []*issues_model.Label
 	for _, label := range pr.Labels {
 		lb, ok := g.labels[label.Name]
 		if ok {
@@ -629,7 +629,7 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR
 		pr.Updated = pr.Created
 	}
 
-	issue := models.Issue{
+	issue := issues_model.Issue{
 		RepoID:      g.repo.ID,
 		Repo:        g.repo,
 		Title:       pr.Title,
@@ -660,7 +660,7 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR
 		issue.Reactions = append(issue.Reactions, &res)
 	}
 
-	pullRequest := models.PullRequest{
+	pullRequest := issues_model.PullRequest{
 		HeadRepoID: g.repo.ID,
 		HeadBranch: head,
 		BaseRepoID: g.repo.ID,
@@ -686,28 +686,28 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR
 	return &pullRequest, nil
 }
 
-func convertReviewState(state string) models.ReviewType {
+func convertReviewState(state string) issues_model.ReviewType {
 	switch state {
 	case base.ReviewStatePending:
-		return models.ReviewTypePending
+		return issues_model.ReviewTypePending
 	case base.ReviewStateApproved:
-		return models.ReviewTypeApprove
+		return issues_model.ReviewTypeApprove
 	case base.ReviewStateChangesRequested:
-		return models.ReviewTypeReject
+		return issues_model.ReviewTypeReject
 	case base.ReviewStateCommented:
-		return models.ReviewTypeComment
+		return issues_model.ReviewTypeComment
 	case base.ReviewStateRequestReview:
-		return models.ReviewTypeRequest
+		return issues_model.ReviewTypeRequest
 	default:
-		return models.ReviewTypePending
+		return issues_model.ReviewTypePending
 	}
 }
 
 // CreateReviews create pull request reviews of currently migrated issues
 func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
-	cms := make([]*models.Review, 0, len(reviews))
+	cms := make([]*issues_model.Review, 0, len(reviews))
 	for _, review := range reviews {
-		var issue *models.Issue
+		var issue *issues_model.Issue
 		issue, ok := g.issues[review.IssueIndex]
 		if !ok {
 			return fmt.Errorf("review references non existent IssueIndex %d", review.IssueIndex)
@@ -716,7 +716,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
 			review.CreatedAt = time.Unix(int64(issue.CreatedUnix), 0)
 		}
 
-		cm := models.Review{
+		cm := issues_model.Review{
 			Type:        convertReviewState(review.State),
 			IssueID:     issue.ID,
 			Content:     review.Content,
@@ -733,7 +733,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
 		pr, ok := g.prCache[issue.ID]
 		if !ok {
 			var err error
-			pr, err = models.GetPullRequestByIssueIDWithNoAttributes(issue.ID)
+			pr, err = issues_model.GetPullRequestByIssueIDWithNoAttributes(issue.ID)
 			if err != nil {
 				return err
 			}
@@ -767,7 +767,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
 				_ = writer.Close()
 			}(comment)
 
-			patch, _ = git.CutDiffAroundLine(reader, int64((&models.Comment{Line: int64(line + comment.Position - 1)}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines)
+			patch, _ = git.CutDiffAroundLine(reader, int64((&issues_model.Comment{Line: int64(line + comment.Position - 1)}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines)
 
 			if comment.CreatedAt.IsZero() {
 				comment.CreatedAt = review.CreatedAt
@@ -776,8 +776,8 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
 				comment.UpdatedAt = comment.CreatedAt
 			}
 
-			c := models.Comment{
-				Type:        models.CommentTypeCode,
+			c := issues_model.Comment{
+				Type:        issues_model.CommentTypeCode,
 				IssueID:     issue.ID,
 				Content:     comment.Content,
 				Line:        int64(line + comment.Position - 1),
@@ -798,7 +798,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
 		cms = append(cms, &cm)
 	}
 
-	return models.InsertReviews(cms)
+	return issues_model.InsertReviews(cms)
 }
 
 // Rollback when migrating failed, this will rollback all the changes.
@@ -819,7 +819,7 @@ func (g *GiteaLocalUploader) Finish() error {
 	}
 
 	// update issue_index
-	if err := models.RecalculateIssueIndexForRepo(g.repo.ID); err != nil {
+	if err := issues_model.RecalculateIssueIndexForRepo(g.repo.ID); err != nil {
 		return err
 	}
 
diff --git a/services/migrations/gitea_uploader_test.go b/services/migrations/gitea_uploader_test.go
index bd7c6e0657..6ea1c20592 100644
--- a/services/migrations/gitea_uploader_test.go
+++ b/services/migrations/gitea_uploader_test.go
@@ -81,7 +81,7 @@ func TestGiteaUploadRepo(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Empty(t, milestones)
 
-	labels, err := models.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
+	labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
 	assert.NoError(t, err)
 	assert.Len(t, labels, 12)
 
@@ -105,7 +105,7 @@ func TestGiteaUploadRepo(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Len(t, releases, 1)
 
-	issues, err := models.Issues(&models.IssuesOptions{
+	issues, err := issues_model.Issues(&issues_model.IssuesOptions{
 		RepoID:   repo.ID,
 		IsPull:   util.OptionalBoolFalse,
 		SortType: "oldest",
@@ -115,7 +115,7 @@ func TestGiteaUploadRepo(t *testing.T) {
 	assert.NoError(t, issues[0].LoadDiscussComments())
 	assert.Empty(t, issues[0].Comments)
 
-	pulls, _, err := models.PullRequests(repo.ID, &models.PullRequestsOptions{
+	pulls, _, err := issues_model.PullRequests(repo.ID, &issues_model.PullRequestsOptions{
 		SortType: "oldest",
 	})
 	assert.NoError(t, err)
diff --git a/services/pull/check.go b/services/pull/check.go
index 94e7ca7161..6621a281fa 100644
--- a/services/pull/check.go
+++ b/services/pull/check.go
@@ -15,6 +15,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
@@ -44,9 +45,9 @@ var (
 )
 
 // AddToTaskQueue adds itself to pull request test task queue.
-func AddToTaskQueue(pr *models.PullRequest) {
+func AddToTaskQueue(pr *issues_model.PullRequest) {
 	err := prPatchCheckerQueue.PushFunc(strconv.FormatInt(pr.ID, 10), func() error {
-		pr.Status = models.PullRequestStatusChecking
+		pr.Status = issues_model.PullRequestStatusChecking
 		err := pr.UpdateColsIfNotMerged("status")
 		if err != nil {
 			log.Error("AddToTaskQueue.UpdateCols[%d].(add to queue): %v", pr.ID, err)
@@ -61,7 +62,7 @@ func AddToTaskQueue(pr *models.PullRequest) {
 }
 
 // CheckPullMergable check if the pull mergable based on all conditions (branch protection, merge options, ...)
-func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *access_model.Permission, pr *models.PullRequest, manuallMerge, force bool) error {
+func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *access_model.Permission, pr *issues_model.PullRequest, manuallMerge, force bool) error {
 	return db.WithTx(func(ctx context.Context) error {
 		if pr.HasMerged {
 			return ErrHasMerged
@@ -114,7 +115,7 @@ func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *acce
 			return err
 		}
 
-		if noDeps, err := models.IssueNoDependenciesLeft(ctx, pr.Issue); err != nil {
+		if noDeps, err := issues_model.IssueNoDependenciesLeft(ctx, pr.Issue); err != nil {
 			return err
 		} else if !noDeps {
 			return ErrDependenciesLeft
@@ -125,7 +126,7 @@ func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *acce
 }
 
 // isSignedIfRequired check if merge will be signed if required
-func isSignedIfRequired(ctx context.Context, pr *models.PullRequest, doer *user_model.User) (bool, error) {
+func isSignedIfRequired(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) (bool, error) {
 	if err := pr.LoadProtectedBranchCtx(ctx); err != nil {
 		return false, err
 	}
@@ -141,10 +142,10 @@ func isSignedIfRequired(ctx context.Context, pr *models.PullRequest, doer *user_
 
 // checkAndUpdateStatus checks if pull request is possible to leaving checking status,
 // and set to be either conflict or mergeable.
-func checkAndUpdateStatus(pr *models.PullRequest) {
+func checkAndUpdateStatus(pr *issues_model.PullRequest) {
 	// Status is not changed to conflict means mergeable.
-	if pr.Status == models.PullRequestStatusChecking {
-		pr.Status = models.PullRequestStatusMergeable
+	if pr.Status == issues_model.PullRequestStatusChecking {
+		pr.Status = issues_model.PullRequestStatusMergeable
 	}
 
 	// Make sure there is no waiting test to process before leaving the checking status.
@@ -162,7 +163,7 @@ func checkAndUpdateStatus(pr *models.PullRequest) {
 
 // getMergeCommit checks if a pull request got merged
 // Returns the git.Commit of the pull request if merged
-func getMergeCommit(ctx context.Context, pr *models.PullRequest) (*git.Commit, error) {
+func getMergeCommit(ctx context.Context, pr *issues_model.PullRequest) (*git.Commit, error) {
 	if pr.BaseRepo == nil {
 		var err error
 		pr.BaseRepo, err = repo_model.GetRepositoryByID(pr.BaseRepoID)
@@ -230,7 +231,7 @@ func getMergeCommit(ctx context.Context, pr *models.PullRequest) (*git.Commit, e
 
 // manuallyMerged checks if a pull request got manually merged
 // When a pull request got manually merged mark the pull request as merged
-func manuallyMerged(ctx context.Context, pr *models.PullRequest) bool {
+func manuallyMerged(ctx context.Context, pr *issues_model.PullRequest) bool {
 	if err := pr.LoadBaseRepoCtx(ctx); err != nil {
 		log.Error("PullRequest[%d].LoadBaseRepo: %v", pr.ID, err)
 		return false
@@ -254,7 +255,7 @@ func manuallyMerged(ctx context.Context, pr *models.PullRequest) bool {
 	if commit != nil {
 		pr.MergedCommitID = commit.ID.String()
 		pr.MergedUnix = timeutil.TimeStamp(commit.Author.When.Unix())
-		pr.Status = models.PullRequestStatusManuallyMerged
+		pr.Status = issues_model.PullRequestStatusManuallyMerged
 		merger, _ := user_model.GetUserByEmail(commit.Author.Email)
 
 		// When the commit author is unknown set the BaseRepo owner as merger
@@ -287,7 +288,7 @@ func manuallyMerged(ctx context.Context, pr *models.PullRequest) bool {
 
 // InitializePullRequests checks and tests untested patches of pull requests.
 func InitializePullRequests(ctx context.Context) {
-	prs, err := models.GetPullRequestIDsByCheckStatus(models.PullRequestStatusChecking)
+	prs, err := issues_model.GetPullRequestIDsByCheckStatus(issues_model.PullRequestStatusChecking)
 	if err != nil {
 		log.Error("Find Checking PRs: %v", err)
 		return
@@ -323,7 +324,7 @@ func testPR(id int64) {
 	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("Test PR[%d] from patch checking queue", id))
 	defer finished()
 
-	pr, err := models.GetPullRequestByID(ctx, id)
+	pr, err := issues_model.GetPullRequestByID(ctx, id)
 	if err != nil {
 		log.Error("GetPullRequestByID[%d]: %v", id, err)
 		return
@@ -339,7 +340,7 @@ func testPR(id int64) {
 
 	if err := TestPatch(pr); err != nil {
 		log.Error("testPatch[%d]: %v", pr.ID, err)
-		pr.Status = models.PullRequestStatusError
+		pr.Status = issues_model.PullRequestStatusError
 		if err := pr.UpdateCols("status"); err != nil {
 			log.Error("update pr [%d] status to PullRequestStatusError failed: %v", pr.ID, err)
 		}
@@ -350,7 +351,7 @@ func testPR(id int64) {
 
 // CheckPrsForBaseBranch check all pulls with bseBrannch
 func CheckPrsForBaseBranch(baseRepo *repo_model.Repository, baseBranchName string) error {
-	prs, err := models.GetUnmergedPullRequestsByBaseInfo(baseRepo.ID, baseBranchName)
+	prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(baseRepo.ID, baseBranchName)
 	if err != nil {
 		return err
 	}
diff --git a/services/pull/check_test.go b/services/pull/check_test.go
index bc4c45ffad..21fe675bbc 100644
--- a/services/pull/check_test.go
+++ b/services/pull/check_test.go
@@ -10,7 +10,7 @@ import (
 	"testing"
 	"time"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/queue"
 
@@ -43,12 +43,12 @@ func TestPullRequest_AddToTaskQueue(t *testing.T) {
 
 	prPatchCheckerQueue = q.(queue.UniqueQueue)
 
-	pr := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2}).(*models.PullRequest)
+	pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}).(*issues_model.PullRequest)
 	AddToTaskQueue(pr)
 
 	assert.Eventually(t, func() bool {
-		pr = unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2}).(*models.PullRequest)
-		return pr.Status == models.PullRequestStatusChecking
+		pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}).(*issues_model.PullRequest)
+		return pr.Status == issues_model.PullRequestStatusChecking
 	}, 1*time.Second, 100*time.Millisecond)
 
 	has, err := prPatchCheckerQueue.Has(strconv.FormatInt(pr.ID, 10))
@@ -72,8 +72,8 @@ func TestPullRequest_AddToTaskQueue(t *testing.T) {
 	assert.False(t, has)
 	assert.NoError(t, err)
 
-	pr = unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2}).(*models.PullRequest)
-	assert.Equal(t, models.PullRequestStatusChecking, pr.Status)
+	pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}).(*issues_model.PullRequest)
+	assert.Equal(t, issues_model.PullRequestStatusChecking, pr.Status)
 
 	for _, callback := range queueShutdown {
 		callback()
diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go
index c0894c6c98..5d846129f6 100644
--- a/services/pull/commit_status.go
+++ b/services/pull/commit_status.go
@@ -8,9 +8,9 @@ package pull
 import (
 	"context"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/structs"
 
@@ -83,7 +83,7 @@ func IsCommitStatusContextSuccess(commitStatuses []*git_model.CommitStatus, requ
 }
 
 // IsPullCommitStatusPass returns if all required status checks PASS
-func IsPullCommitStatusPass(ctx context.Context, pr *models.PullRequest) (bool, error) {
+func IsPullCommitStatusPass(ctx context.Context, pr *issues_model.PullRequest) (bool, error) {
 	if err := pr.LoadProtectedBranchCtx(ctx); err != nil {
 		return false, errors.Wrap(err, "GetLatestCommitStatus")
 	}
@@ -99,7 +99,7 @@ func IsPullCommitStatusPass(ctx context.Context, pr *models.PullRequest) (bool,
 }
 
 // GetPullRequestCommitStatusState returns pull request merged commit status state
-func GetPullRequestCommitStatusState(ctx context.Context, pr *models.PullRequest) (structs.CommitStatusState, error) {
+func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullRequest) (structs.CommitStatusState, error) {
 	// Ensure HeadRepo is loaded
 	if err := pr.LoadHeadRepoCtx(ctx); err != nil {
 		return "", errors.Wrap(err, "LoadHeadRepo")
@@ -112,15 +112,15 @@ func GetPullRequestCommitStatusState(ctx context.Context, pr *models.PullRequest
 	}
 	defer closer.Close()
 
-	if pr.Flow == models.PullRequestFlowGithub && !headGitRepo.IsBranchExist(pr.HeadBranch) {
+	if pr.Flow == issues_model.PullRequestFlowGithub && !headGitRepo.IsBranchExist(pr.HeadBranch) {
 		return "", errors.New("Head branch does not exist, can not merge")
 	}
-	if pr.Flow == models.PullRequestFlowAGit && !git.IsReferenceExist(ctx, headGitRepo.Path, pr.GetGitRefName()) {
+	if pr.Flow == issues_model.PullRequestFlowAGit && !git.IsReferenceExist(ctx, headGitRepo.Path, pr.GetGitRefName()) {
 		return "", errors.New("Head branch does not exist, can not merge")
 	}
 
 	var sha string
-	if pr.Flow == models.PullRequestFlowGithub {
+	if pr.Flow == issues_model.PullRequestFlowGithub {
 		sha, err = headGitRepo.GetBranchCommitID(pr.HeadBranch)
 	} else {
 		sha, err = headGitRepo.GetRefCommitID(pr.GetGitRefName())
diff --git a/services/pull/edits.go b/services/pull/edits.go
index 11932d9ab8..2938f2b108 100644
--- a/services/pull/edits.go
+++ b/services/pull/edits.go
@@ -9,7 +9,7 @@ import (
 	"context"
 	"errors"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	unit_model "code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
@@ -18,7 +18,7 @@ import (
 var ErrUserHasNoPermissionForAction = errors.New("user not allowed to do this action")
 
 // SetAllowEdits allow edits from maintainers to PRs
-func SetAllowEdits(ctx context.Context, doer *user_model.User, pr *models.PullRequest, allow bool) error {
+func SetAllowEdits(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, allow bool) error {
 	if doer == nil || !pr.Issue.IsPoster(doer.ID) {
 		return ErrUserHasNoPermissionForAction
 	}
@@ -37,5 +37,5 @@ func SetAllowEdits(ctx context.Context, doer *user_model.User, pr *models.PullRe
 	}
 
 	pr.AllowMaintainerEdit = allow
-	return models.UpdateAllowEdits(ctx, pr)
+	return issues_model.UpdateAllowEdits(ctx, pr)
 }
diff --git a/services/pull/lfs.go b/services/pull/lfs.go
index 490a904584..8cca0a91b7 100644
--- a/services/pull/lfs.go
+++ b/services/pull/lfs.go
@@ -12,15 +12,15 @@ import (
 	"strconv"
 	"sync"
 
-	"code.gitea.io/gitea/models"
 	git_model "code.gitea.io/gitea/models/git"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/git/pipeline"
 	"code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/log"
 )
 
 // LFSPush pushes lfs objects referred to in new commits in the head repository from the base repository
-func LFSPush(ctx context.Context, tmpBasePath, mergeHeadSHA, mergeBaseSHA string, pr *models.PullRequest) error {
+func LFSPush(ctx context.Context, tmpBasePath, mergeHeadSHA, mergeBaseSHA string, pr *issues_model.PullRequest) error {
 	// Now we have to implement git lfs push
 	// git rev-list --objects --filter=blob:limit=1k HEAD --not base
 	// pass blob shas in to git cat-file --batch-check (possibly unnecessary)
@@ -68,7 +68,7 @@ func LFSPush(ctx context.Context, tmpBasePath, mergeHeadSHA, mergeBaseSHA string
 	return nil
 }
 
-func createLFSMetaObjectsFromCatFileBatch(catFileBatchReader *io.PipeReader, wg *sync.WaitGroup, pr *models.PullRequest) {
+func createLFSMetaObjectsFromCatFileBatch(catFileBatchReader *io.PipeReader, wg *sync.WaitGroup, pr *issues_model.PullRequest) {
 	defer wg.Done()
 	defer catFileBatchReader.Close()
 
diff --git a/services/pull/merge.go b/services/pull/merge.go
index eef1d17b64..aff800a1b6 100644
--- a/services/pull/merge.go
+++ b/services/pull/merge.go
@@ -20,6 +20,7 @@ import (
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
+	issues_model "code.gitea.io/gitea/models/issues"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	pull_model "code.gitea.io/gitea/models/pull"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -38,7 +39,7 @@ import (
 )
 
 // GetDefaultMergeMessage returns default message used when merging pull request
-func GetDefaultMergeMessage(baseGitRepo *git.Repository, pr *models.PullRequest, mergeStyle repo_model.MergeStyle) (string, error) {
+func GetDefaultMergeMessage(baseGitRepo *git.Repository, pr *issues_model.PullRequest, mergeStyle repo_model.MergeStyle) (string, error) {
 	if err := pr.LoadHeadRepo(); err != nil {
 		return "", err
 	}
@@ -131,7 +132,7 @@ func GetDefaultMergeMessage(baseGitRepo *git.Repository, pr *models.PullRequest,
 
 // Merge merges pull request to base repository.
 // Caller should check PR is ready to be merged (review and status checks)
-func Merge(ctx context.Context, pr *models.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) error {
+func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) error {
 	if err := pr.LoadHeadRepo(); err != nil {
 		log.Error("LoadHeadRepo: %v", err)
 		return fmt.Errorf("LoadHeadRepo: %v", err)
@@ -213,7 +214,7 @@ func Merge(ctx context.Context, pr *models.PullRequest, doer *user_model.User, b
 		if close != ref.Issue.IsClosed {
 			if err = issue_service.ChangeStatus(ref.Issue, doer, close); err != nil {
 				// Allow ErrDependenciesLeft
-				if !models.IsErrDependenciesLeft(err) {
+				if !issues_model.IsErrDependenciesLeft(err) {
 					return err
 				}
 			}
@@ -223,7 +224,7 @@ func Merge(ctx context.Context, pr *models.PullRequest, doer *user_model.User, b
 }
 
 // rawMerge perform the merge operation without changing any pull information in database
-func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) (string, error) {
+func rawMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) (string, error) {
 	// Clone base repo.
 	tmpBasePath, err := createTemporaryRepo(ctx, pr)
 	if err != nil {
@@ -635,7 +636,7 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
 	return mergeCommitID, nil
 }
 
-func commitAndSignNoAuthor(ctx context.Context, pr *models.PullRequest, message, signArg, tmpBasePath string, env []string) error {
+func commitAndSignNoAuthor(ctx context.Context, pr *issues_model.PullRequest, message, signArg, tmpBasePath string, env []string) error {
 	var outbuf, errbuf strings.Builder
 	if signArg == "" {
 		if err := git.NewCommand(ctx, "commit", "-m", message).
@@ -663,7 +664,7 @@ func commitAndSignNoAuthor(ctx context.Context, pr *models.PullRequest, message,
 	return nil
 }
 
-func runMergeCommand(pr *models.PullRequest, mergeStyle repo_model.MergeStyle, cmd *git.Command, tmpBasePath string) error {
+func runMergeCommand(pr *issues_model.PullRequest, mergeStyle repo_model.MergeStyle, cmd *git.Command, tmpBasePath string) error {
 	var outbuf, errbuf strings.Builder
 	if err := cmd.Run(&git.RunOpts{
 		Dir:    tmpBasePath,
@@ -747,7 +748,7 @@ func getDiffTree(ctx context.Context, repoPath, baseBranch, headBranch string) (
 }
 
 // IsUserAllowedToMerge check if user is allowed to merge PR with given permissions and branch protections
-func IsUserAllowedToMerge(ctx context.Context, pr *models.PullRequest, p access_model.Permission, user *user_model.User) (bool, error) {
+func IsUserAllowedToMerge(ctx context.Context, pr *issues_model.PullRequest, p access_model.Permission, user *user_model.User) (bool, error) {
 	if user == nil {
 		return false, nil
 	}
@@ -765,7 +766,7 @@ func IsUserAllowedToMerge(ctx context.Context, pr *models.PullRequest, p access_
 }
 
 // CheckPullBranchProtections checks whether the PR is ready to be merged (reviews and status checks)
-func CheckPullBranchProtections(ctx context.Context, pr *models.PullRequest, skipProtectedFilesCheck bool) (err error) {
+func CheckPullBranchProtections(ctx context.Context, pr *issues_model.PullRequest, skipProtectedFilesCheck bool) (err error) {
 	if err = pr.LoadBaseRepoCtx(ctx); err != nil {
 		return fmt.Errorf("LoadBaseRepo: %v", err)
 	}
@@ -787,23 +788,23 @@ func CheckPullBranchProtections(ctx context.Context, pr *models.PullRequest, ski
 		}
 	}
 
-	if !models.HasEnoughApprovals(ctx, pr.ProtectedBranch, pr) {
+	if !issues_model.HasEnoughApprovals(ctx, pr.ProtectedBranch, pr) {
 		return models.ErrDisallowedToMerge{
 			Reason: "Does not have enough approvals",
 		}
 	}
-	if models.MergeBlockedByRejectedReview(ctx, pr.ProtectedBranch, pr) {
+	if issues_model.MergeBlockedByRejectedReview(ctx, pr.ProtectedBranch, pr) {
 		return models.ErrDisallowedToMerge{
 			Reason: "There are requested changes",
 		}
 	}
-	if models.MergeBlockedByOfficialReviewRequests(ctx, pr.ProtectedBranch, pr) {
+	if issues_model.MergeBlockedByOfficialReviewRequests(ctx, pr.ProtectedBranch, pr) {
 		return models.ErrDisallowedToMerge{
 			Reason: "There are official review requests",
 		}
 	}
 
-	if models.MergeBlockedByOutdatedBranch(pr.ProtectedBranch, pr) {
+	if issues_model.MergeBlockedByOutdatedBranch(pr.ProtectedBranch, pr) {
 		return models.ErrDisallowedToMerge{
 			Reason: "The head branch is behind the base branch",
 		}
@@ -823,7 +824,7 @@ func CheckPullBranchProtections(ctx context.Context, pr *models.PullRequest, ski
 }
 
 // MergedManually mark pr as merged manually
-func MergedManually(pr *models.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, commitID string) error {
+func MergedManually(pr *issues_model.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, commitID string) error {
 	pullWorkingPool.CheckIn(fmt.Sprint(pr.ID))
 	defer pullWorkingPool.CheckOut(fmt.Sprint(pr.ID))
 
@@ -862,7 +863,7 @@ func MergedManually(pr *models.PullRequest, doer *user_model.User, baseGitRepo *
 
 		pr.MergedCommitID = commitID
 		pr.MergedUnix = timeutil.TimeStamp(commit.Author.When.Unix())
-		pr.Status = models.PullRequestStatusManuallyMerged
+		pr.Status = issues_model.PullRequestStatusManuallyMerged
 		pr.Merger = doer
 		pr.MergerID = doer.ID
 
diff --git a/services/pull/patch.go b/services/pull/patch.go
index 6e2889b060..c7a69501c3 100644
--- a/services/pull/patch.go
+++ b/services/pull/patch.go
@@ -15,6 +15,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/graceful"
@@ -27,7 +28,7 @@ import (
 )
 
 // DownloadDiffOrPatch will write the patch for the pr to the writer
-func DownloadDiffOrPatch(ctx context.Context, pr *models.PullRequest, w io.Writer, patch, binary bool) error {
+func DownloadDiffOrPatch(ctx context.Context, pr *issues_model.PullRequest, w io.Writer, patch, binary bool) error {
 	if err := pr.LoadBaseRepoCtx(ctx); err != nil {
 		log.Error("Unable to load base repository ID %d for pr #%d [%d]", pr.BaseRepoID, pr.Index, pr.ID)
 		return err
@@ -54,7 +55,7 @@ var patchErrorSuffices = []string{
 }
 
 // TestPatch will test whether a simple patch will apply
-func TestPatch(pr *models.PullRequest) error {
+func TestPatch(pr *issues_model.PullRequest) error {
 	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("TestPatch: Repo[%d]#%d", pr.BaseRepoID, pr.Index))
 	defer finished()
 
@@ -88,7 +89,7 @@ func TestPatch(pr *models.PullRequest) error {
 	pr.MergeBase = strings.TrimSpace(pr.MergeBase)
 
 	// 2. Check for conflicts
-	if conflicts, err := checkConflicts(ctx, pr, gitRepo, tmpBasePath); err != nil || conflicts || pr.Status == models.PullRequestStatusEmpty {
+	if conflicts, err := checkConflicts(ctx, pr, gitRepo, tmpBasePath); err != nil || conflicts || pr.Status == issues_model.PullRequestStatusEmpty {
 		return err
 	}
 
@@ -101,7 +102,7 @@ func TestPatch(pr *models.PullRequest) error {
 		log.Trace("Found %d protected files changed", len(pr.ChangedProtectedFiles))
 	}
 
-	pr.Status = models.PullRequestStatusMergeable
+	pr.Status = issues_model.PullRequestStatusMergeable
 
 	return nil
 }
@@ -270,7 +271,7 @@ func AttemptThreeWayMerge(ctx context.Context, gitPath string, gitRepo *git.Repo
 	return conflict, conflictedFiles, nil
 }
 
-func checkConflicts(ctx context.Context, pr *models.PullRequest, gitRepo *git.Repository, tmpBasePath string) (bool, error) {
+func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository, tmpBasePath string) (bool, error) {
 	// 1. checkConflicts resets the conflict status - therefore - reset the conflict status
 	pr.ConflictedFiles = nil
 
@@ -295,7 +296,7 @@ func checkConflicts(ctx context.Context, pr *models.PullRequest, gitRepo *git.Re
 		}
 		if treeHash == baseTree.ID.String() {
 			log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID)
-			pr.Status = models.PullRequestStatusEmpty
+			pr.Status = issues_model.PullRequestStatusEmpty
 		}
 
 		return false, nil
@@ -329,7 +330,7 @@ func checkConflicts(ctx context.Context, pr *models.PullRequest, gitRepo *git.Re
 	// 3b. if the size of that patch is 0 - there can be no conflicts!
 	if stat.Size() == 0 {
 		log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID)
-		pr.Status = models.PullRequestStatusEmpty
+		pr.Status = issues_model.PullRequestStatusEmpty
 		return false, nil
 	}
 
@@ -449,7 +450,7 @@ func checkConflicts(ctx context.Context, pr *models.PullRequest, gitRepo *git.Re
 	// Note: `"err" could be non-nil` is due that if enable 3-way merge, it doesn't return any error on found conflicts.
 	if len(pr.ConflictedFiles) > 0 {
 		if conflict {
-			pr.Status = models.PullRequestStatusConflict
+			pr.Status = issues_model.PullRequestStatusConflict
 			log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles)
 
 			return true, nil
@@ -516,8 +517,8 @@ func CheckUnprotectedFiles(repo *git.Repository, oldCommitID, newCommitID string
 }
 
 // checkPullFilesProtection check if pr changed protected files and save results
-func checkPullFilesProtection(pr *models.PullRequest, gitRepo *git.Repository) error {
-	if pr.Status == models.PullRequestStatusEmpty {
+func checkPullFilesProtection(pr *issues_model.PullRequest, gitRepo *git.Repository) error {
+	if pr.Status == issues_model.PullRequestStatusEmpty {
 		pr.ChangedProtectedFiles = nil
 		return nil
 	}
diff --git a/services/pull/pull.go b/services/pull/pull.go
index 736520fda2..103fdc340d 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -17,6 +17,7 @@ import (
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
@@ -35,7 +36,7 @@ import (
 var pullWorkingPool = sync.NewExclusivePool()
 
 // NewPullRequest creates new pull request with labels for repository.
-func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *models.Issue, labelIDs []int64, uuids []string, pr *models.PullRequest, assigneeIDs []int64) error {
+func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64) error {
 	if err := TestPatch(pr); err != nil {
 		return err
 	}
@@ -47,7 +48,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *mode
 	pr.CommitsAhead = divergence.Ahead
 	pr.CommitsBehind = divergence.Behind
 
-	if err := models.NewPullRequest(ctx, repo, pull, labelIDs, uuids, pr); err != nil {
+	if err := issues_model.NewPullRequest(ctx, repo, pull, labelIDs, uuids, pr); err != nil {
 		return err
 	}
 
@@ -66,7 +67,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *mode
 	prCtx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("NewPullRequest: %s:%d", repo.FullName(), pr.Index))
 	defer finished()
 
-	if pr.Flow == models.PullRequestFlowGithub {
+	if pr.Flow == issues_model.PullRequestFlowGithub {
 		err = PushToBaseRepo(prCtx, pr)
 	} else {
 		err = UpdateRef(prCtx, pr)
@@ -75,7 +76,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *mode
 		return err
 	}
 
-	mentions, err := models.FindAndUpdateIssueMentions(ctx, pull, pull.Poster, pull.Content)
+	mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, pull, pull.Poster, pull.Content)
 	if err != nil {
 		return err
 	}
@@ -102,7 +103,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *mode
 	}
 
 	if len(compareInfo.Commits) > 0 {
-		data := models.PushActionContent{IsForcePush: false}
+		data := issues_model.PushActionContent{IsForcePush: false}
 		data.CommitIDs = make([]string, 0, len(compareInfo.Commits))
 		for i := len(compareInfo.Commits) - 1; i >= 0; i-- {
 			data.CommitIDs = append(data.CommitIDs, compareInfo.Commits[i].ID.String())
@@ -113,8 +114,8 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *mode
 			return err
 		}
 
-		ops := &models.CreateCommentOptions{
-			Type:        models.CommentTypePullRequestPush,
+		ops := &issues_model.CreateCommentOptions{
+			Type:        issues_model.CommentTypePullRequestPush,
 			Doer:        pull.Poster,
 			Repo:        repo,
 			Issue:       pr.Issue,
@@ -122,14 +123,14 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *mode
 			Content:     string(dataJSON),
 		}
 
-		_, _ = models.CreateComment(ops)
+		_, _ = issues_model.CreateComment(ops)
 	}
 
 	return nil
 }
 
 // ChangeTargetBranch changes the target branch of this pull request, as the given user.
-func ChangeTargetBranch(ctx context.Context, pr *models.PullRequest, doer *user_model.User, targetBranch string) (err error) {
+func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, targetBranch string) (err error) {
 	pullWorkingPool.CheckIn(fmt.Sprint(pr.ID))
 	defer pullWorkingPool.CheckOut(fmt.Sprint(pr.ID))
 
@@ -139,7 +140,7 @@ func ChangeTargetBranch(ctx context.Context, pr *models.PullRequest, doer *user_
 	}
 
 	if pr.Issue.IsClosed {
-		return models.ErrIssueIsClosed{
+		return issues_model.ErrIssueIsClosed{
 			ID:     pr.Issue.ID,
 			RepoID: pr.Issue.RepoID,
 			Index:  pr.Issue.Index,
@@ -170,9 +171,9 @@ func ChangeTargetBranch(ctx context.Context, pr *models.PullRequest, doer *user_
 	}
 
 	// Check if pull request for the new target branch already exists
-	existingPr, err := models.GetUnmergedPullRequest(pr.HeadRepoID, pr.BaseRepoID, pr.HeadBranch, targetBranch, models.PullRequestFlowGithub)
+	existingPr, err := issues_model.GetUnmergedPullRequest(pr.HeadRepoID, pr.BaseRepoID, pr.HeadBranch, targetBranch, issues_model.PullRequestFlowGithub)
 	if existingPr != nil {
-		return models.ErrPullRequestAlreadyExists{
+		return issues_model.ErrPullRequestAlreadyExists{
 			ID:         existingPr.ID,
 			IssueID:    existingPr.Index,
 			HeadRepoID: existingPr.HeadRepoID,
@@ -181,7 +182,7 @@ func ChangeTargetBranch(ctx context.Context, pr *models.PullRequest, doer *user_
 			BaseBranch: existingPr.BaseBranch,
 		}
 	}
-	if err != nil && !models.IsErrPullRequestNotExist(err) {
+	if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
 		return err
 	}
 
@@ -196,8 +197,8 @@ func ChangeTargetBranch(ctx context.Context, pr *models.PullRequest, doer *user_
 
 	// Update target branch, PR diff and status
 	// This is the same as checkAndUpdateStatus in check service, but also updates base_branch
-	if pr.Status == models.PullRequestStatusChecking {
-		pr.Status = models.PullRequestStatusMergeable
+	if pr.Status == issues_model.PullRequestStatusChecking {
+		pr.Status = issues_model.PullRequestStatusMergeable
 	}
 
 	// Update Commit Divergence
@@ -213,22 +214,22 @@ func ChangeTargetBranch(ctx context.Context, pr *models.PullRequest, doer *user_
 	}
 
 	// Create comment
-	options := &models.CreateCommentOptions{
-		Type:   models.CommentTypeChangeTargetBranch,
+	options := &issues_model.CreateCommentOptions{
+		Type:   issues_model.CommentTypeChangeTargetBranch,
 		Doer:   doer,
 		Repo:   pr.Issue.Repo,
 		Issue:  pr.Issue,
 		OldRef: oldBranch,
 		NewRef: targetBranch,
 	}
-	if _, err = models.CreateComment(options); err != nil {
+	if _, err = issues_model.CreateComment(options); err != nil {
 		return fmt.Errorf("CreateChangeTargetBranchComment: %v", err)
 	}
 
 	return nil
 }
 
-func checkForInvalidation(ctx context.Context, requests models.PullRequestList, repoID int64, doer *user_model.User, branch string) error {
+func checkForInvalidation(ctx context.Context, requests issues_model.PullRequestList, repoID int64, doer *user_model.User, branch string) error {
 	repo, err := repo_model.GetRepositoryByID(repoID)
 	if err != nil {
 		return fmt.Errorf("GetRepositoryByID: %v", err)
@@ -257,14 +258,14 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
 		// If you don't let it run all the way then you will lose data
 		// TODO: graceful: AddTestPullRequestTask needs to become a queue!
 
-		prs, err := models.GetUnmergedPullRequestsByHeadInfo(repoID, branch)
+		prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(repoID, branch)
 		if err != nil {
 			log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", repoID, branch, err)
 			return
 		}
 
 		if isSync {
-			requests := models.PullRequestList(prs)
+			requests := issues_model.PullRequestList(prs)
 			if err = requests.LoadAttributes(); err != nil {
 				log.Error("PullRequestList.LoadAttributes: %v", err)
 			}
@@ -280,11 +281,11 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
 						}
 						if changed {
 							// Mark old reviews as stale if diff to mergebase has changed
-							if err := models.MarkReviewsAsStale(pr.IssueID); err != nil {
+							if err := issues_model.MarkReviewsAsStale(pr.IssueID); err != nil {
 								log.Error("MarkReviewsAsStale: %v", err)
 							}
 						}
-						if err := models.MarkReviewsAsNotStale(pr.IssueID, newCommitID); err != nil {
+						if err := issues_model.MarkReviewsAsNotStale(pr.IssueID, newCommitID); err != nil {
 							log.Error("MarkReviewsAsNotStale: %v", err)
 						}
 						divergence, err := GetDiverging(ctx, pr)
@@ -306,7 +307,7 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
 
 		for _, pr := range prs {
 			log.Trace("Updating PR[%d]: composing new test task", pr.ID)
-			if pr.Flow == models.PullRequestFlowGithub {
+			if pr.Flow == issues_model.PullRequestFlowGithub {
 				if err := PushToBaseRepo(ctx, pr); err != nil {
 					log.Error("PushToBaseRepo: %v", err)
 					continue
@@ -316,14 +317,14 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
 			}
 
 			AddToTaskQueue(pr)
-			comment, err := models.CreatePushPullComment(ctx, doer, pr, oldCommitID, newCommitID)
+			comment, err := issues_model.CreatePushPullComment(ctx, doer, pr, oldCommitID, newCommitID)
 			if err == nil && comment != nil {
 				notification.NotifyPullRequestPushCommits(doer, pr, comment)
 			}
 		}
 
 		log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", repoID, branch)
-		prs, err = models.GetUnmergedPullRequestsByBaseInfo(repoID, branch)
+		prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(repoID, branch)
 		if err != nil {
 			log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", repoID, branch, err)
 			return
@@ -349,7 +350,7 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
 
 // checkIfPRContentChanged checks if diff to target branch has changed by push
 // A commit can be considered to leave the PR untouched if the patch/diff with its merge base is unchanged
-func checkIfPRContentChanged(ctx context.Context, pr *models.PullRequest, oldCommitID, newCommitID string) (hasChanged bool, err error) {
+func checkIfPRContentChanged(ctx context.Context, pr *issues_model.PullRequest, oldCommitID, newCommitID string) (hasChanged bool, err error) {
 	if err = pr.LoadHeadRepoCtx(ctx); err != nil {
 		return false, fmt.Errorf("LoadHeadRepo: %v", err)
 	} else if pr.HeadRepo == nil {
@@ -421,11 +422,11 @@ func checkIfPRContentChanged(ctx context.Context, pr *models.PullRequest, oldCom
 // PushToBaseRepo pushes commits from branches of head repository to
 // corresponding branches of base repository.
 // FIXME: Only push branches that are actually updates?
-func PushToBaseRepo(ctx context.Context, pr *models.PullRequest) (err error) {
+func PushToBaseRepo(ctx context.Context, pr *issues_model.PullRequest) (err error) {
 	return pushToBaseRepoHelper(ctx, pr, "")
 }
 
-func pushToBaseRepoHelper(ctx context.Context, pr *models.PullRequest, prefixHeadBranch string) (err error) {
+func pushToBaseRepoHelper(ctx context.Context, pr *issues_model.PullRequest, prefixHeadBranch string) (err error) {
 	log.Trace("PushToBaseRepo[%d]: pushing commits to base repo '%s'", pr.BaseRepoID, pr.GetGitRefName())
 
 	if err := pr.LoadHeadRepoCtx(ctx); err != nil {
@@ -481,7 +482,7 @@ func pushToBaseRepoHelper(ctx context.Context, pr *models.PullRequest, prefixHea
 }
 
 // UpdateRef update refs/pull/id/head directly for agit flow pull request
-func UpdateRef(ctx context.Context, pr *models.PullRequest) (err error) {
+func UpdateRef(ctx context.Context, pr *issues_model.PullRequest) (err error) {
 	log.Trace("UpdateRef[%d]: upgate pull request ref in base repo '%s'", pr.ID, pr.GetGitRefName())
 	if err := pr.LoadBaseRepoCtx(ctx); err != nil {
 		log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err)
@@ -514,24 +515,24 @@ func (errs errlist) Error() string {
 
 // CloseBranchPulls close all the pull requests who's head branch is the branch
 func CloseBranchPulls(doer *user_model.User, repoID int64, branch string) error {
-	prs, err := models.GetUnmergedPullRequestsByHeadInfo(repoID, branch)
+	prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(repoID, branch)
 	if err != nil {
 		return err
 	}
 
-	prs2, err := models.GetUnmergedPullRequestsByBaseInfo(repoID, branch)
+	prs2, err := issues_model.GetUnmergedPullRequestsByBaseInfo(repoID, branch)
 	if err != nil {
 		return err
 	}
 
 	prs = append(prs, prs2...)
-	if err := models.PullRequestList(prs).LoadAttributes(); err != nil {
+	if err := issues_model.PullRequestList(prs).LoadAttributes(); err != nil {
 		return err
 	}
 
 	var errs errlist
 	for _, pr := range prs {
-		if err = issue_service.ChangeStatus(pr.Issue, doer, true); err != nil && !models.IsErrPullWasClosed(err) && !models.IsErrDependenciesLeft(err) {
+		if err = issue_service.ChangeStatus(pr.Issue, doer, true); err != nil && !issues_model.IsErrPullWasClosed(err) && !issues_model.IsErrDependenciesLeft(err) {
 			errs = append(errs, err)
 		}
 	}
@@ -550,12 +551,12 @@ func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *re
 
 	var errs errlist
 	for _, branch := range branches {
-		prs, err := models.GetUnmergedPullRequestsByHeadInfo(repo.ID, branch.Name)
+		prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(repo.ID, branch.Name)
 		if err != nil {
 			return err
 		}
 
-		if err = models.PullRequestList(prs).LoadAttributes(); err != nil {
+		if err = issues_model.PullRequestList(prs).LoadAttributes(); err != nil {
 			return err
 		}
 
@@ -565,7 +566,7 @@ func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *re
 			if pr.BaseRepoID == repo.ID {
 				continue
 			}
-			if err = issue_service.ChangeStatus(pr.Issue, doer, true); err != nil && !models.IsErrPullWasClosed(err) {
+			if err = issue_service.ChangeStatus(pr.Issue, doer, true); err != nil && !issues_model.IsErrPullWasClosed(err) {
 				errs = append(errs, err)
 			}
 		}
@@ -580,7 +581,7 @@ func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *re
 var commitMessageTrailersPattern = regexp.MustCompile(`(?:^|\n\n)(?:[\w-]+[ \t]*:[^\n]+\n*(?:[ \t]+[^\n]+\n*)*)+$`)
 
 // GetSquashMergeCommitMessages returns the commit messages between head and merge base (if there is one)
-func GetSquashMergeCommitMessages(ctx context.Context, pr *models.PullRequest) string {
+func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequest) string {
 	if err := pr.LoadIssue(); err != nil {
 		log.Error("Cannot load issue %d for PR id %d: Error: %v", pr.IssueID, pr.ID, err)
 		return ""
@@ -608,7 +609,7 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *models.PullRequest) s
 	defer closer.Close()
 
 	var headCommit *git.Commit
-	if pr.Flow == models.PullRequestFlowGithub {
+	if pr.Flow == issues_model.PullRequestFlowGithub {
 		headCommit, err = gitRepo.GetBranchCommit(pr.HeadBranch)
 	} else {
 		pr.HeadCommitID, err = gitRepo.GetRefCommitID(pr.GetGitRefName())
@@ -736,13 +737,13 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *models.PullRequest) s
 }
 
 // GetIssuesLastCommitStatus returns a map of issue ID to the most recent commit's latest status
-func GetIssuesLastCommitStatus(ctx context.Context, issues models.IssueList) (map[int64]*git_model.CommitStatus, error) {
+func GetIssuesLastCommitStatus(ctx context.Context, issues issues_model.IssueList) (map[int64]*git_model.CommitStatus, error) {
 	_, lastStatus, err := GetIssuesAllCommitStatus(ctx, issues)
 	return lastStatus, err
 }
 
 // GetIssuesAllCommitStatus returns a map of issue ID to a list of all statuses for the most recent commit as well as a map of issue ID to only the commit's latest status
-func GetIssuesAllCommitStatus(ctx context.Context, issues models.IssueList) (map[int64][]*git_model.CommitStatus, map[int64]*git_model.CommitStatus, error) {
+func GetIssuesAllCommitStatus(ctx context.Context, issues issues_model.IssueList) (map[int64][]*git_model.CommitStatus, map[int64]*git_model.CommitStatus, error) {
 	if err := issues.LoadPullRequests(); err != nil {
 		return nil, nil, err
 	}
@@ -788,7 +789,7 @@ func GetIssuesAllCommitStatus(ctx context.Context, issues models.IssueList) (map
 }
 
 // getAllCommitStatus get pr's commit statuses.
-func getAllCommitStatus(gitRepo *git.Repository, pr *models.PullRequest) (statuses []*git_model.CommitStatus, lastStatus *git_model.CommitStatus, err error) {
+func getAllCommitStatus(gitRepo *git.Repository, pr *issues_model.PullRequest) (statuses []*git_model.CommitStatus, lastStatus *git_model.CommitStatus, err error) {
 	sha, shaErr := gitRepo.GetRefCommitID(pr.GetGitRefName())
 	if shaErr != nil {
 		return nil, nil, shaErr
@@ -800,7 +801,7 @@ func getAllCommitStatus(gitRepo *git.Repository, pr *models.PullRequest) (status
 }
 
 // IsHeadEqualWithBranch returns if the commits of branchName are available in pull request head
-func IsHeadEqualWithBranch(ctx context.Context, pr *models.PullRequest, branchName string) (bool, error) {
+func IsHeadEqualWithBranch(ctx context.Context, pr *issues_model.PullRequest, branchName string) (bool, error) {
 	var err error
 	if err = pr.LoadBaseRepoCtx(ctx); err != nil {
 		return false, err
@@ -833,7 +834,7 @@ func IsHeadEqualWithBranch(ctx context.Context, pr *models.PullRequest, branchNa
 	}
 
 	var headCommit *git.Commit
-	if pr.Flow == models.PullRequestFlowGithub {
+	if pr.Flow == issues_model.PullRequestFlowGithub {
 		headCommit, err = headGitRepo.GetBranchCommit(pr.HeadBranch)
 		if err != nil {
 			return false, err
diff --git a/services/pull/pull_test.go b/services/pull/pull_test.go
index 09bae97780..9160c43460 100644
--- a/services/pull/pull_test.go
+++ b/services/pull/pull_test.go
@@ -8,7 +8,7 @@ package pull
 import (
 	"testing"
 
-	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/models/unittest"
@@ -38,7 +38,7 @@ func TestPullRequest_CommitMessageTrailersPattern(t *testing.T) {
 
 func TestPullRequest_GetDefaultMergeMessage_InternalTracker(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	pr := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2}).(*models.PullRequest)
+	pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}).(*issues_model.PullRequest)
 
 	assert.NoError(t, pr.LoadBaseRepo())
 	gitRepo, err := git.OpenRepository(git.DefaultContext, pr.BaseRepo.RepoPath())
@@ -68,7 +68,7 @@ func TestPullRequest_GetDefaultMergeMessage_ExternalTracker(t *testing.T) {
 	baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
 	baseRepo.Units = []*repo_model.RepoUnit{&externalTracker}
 
-	pr := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2, BaseRepo: baseRepo}).(*models.PullRequest)
+	pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2, BaseRepo: baseRepo}).(*issues_model.PullRequest)
 
 	assert.NoError(t, pr.LoadBaseRepo())
 	gitRepo, err := git.OpenRepository(git.DefaultContext, pr.BaseRepo.RepoPath())
diff --git a/services/pull/review.go b/services/pull/review.go
index eac7279f9b..9cb58fa3a1 100644
--- a/services/pull/review.go
+++ b/services/pull/review.go
@@ -12,8 +12,8 @@ import (
 	"regexp"
 	"strings"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
@@ -23,7 +23,7 @@ import (
 )
 
 // CreateCodeComment creates a comment on the code line
-func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *models.Issue, line int64, content, treePath string, isReview bool, replyReviewID int64, latestCommitID string) (*models.Comment, error) {
+func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, line int64, content, treePath string, isReview bool, replyReviewID int64, latestCommitID string) (*issues_model.Comment, error) {
 	var (
 		existsReview bool
 		err          error
@@ -37,7 +37,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
 	if !isReview && replyReviewID != 0 {
 		// It's not part of a review; maybe a reply to a review comment or a single comment.
 		// Check if there are reviews for that line already; if there are, this is a reply
-		if existsReview, err = models.ReviewExists(issue, treePath, line); err != nil {
+		if existsReview, err = issues_model.ReviewExists(issue, treePath, line); err != nil {
 			return nil, err
 		}
 	}
@@ -61,7 +61,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
 			return nil, err
 		}
 
-		mentions, err := models.FindAndUpdateIssueMentions(ctx, issue, doer, comment.Content)
+		mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, doer, comment.Content)
 		if err != nil {
 			return nil, err
 		}
@@ -71,14 +71,14 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
 		return comment, nil
 	}
 
-	review, err := models.GetCurrentReview(ctx, doer, issue)
+	review, err := issues_model.GetCurrentReview(ctx, doer, issue)
 	if err != nil {
-		if !models.IsErrReviewNotExist(err) {
+		if !issues_model.IsErrReviewNotExist(err) {
 			return nil, err
 		}
 
-		if review, err = models.CreateReview(ctx, models.CreateReviewOptions{
-			Type:     models.ReviewTypePending,
+		if review, err = issues_model.CreateReview(ctx, issues_model.CreateReviewOptions{
+			Type:     issues_model.ReviewTypePending,
 			Reviewer: doer,
 			Issue:    issue,
 			Official: false,
@@ -103,7 +103,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
 
 	if !isReview && !existsReview {
 		// Submit the review we've just created so the comment shows up in the issue view
-		if _, _, err = SubmitReview(ctx, doer, gitRepo, issue, models.ReviewTypeComment, "", latestCommitID, nil); err != nil {
+		if _, _, err = SubmitReview(ctx, doer, gitRepo, issue, issues_model.ReviewTypeComment, "", latestCommitID, nil); err != nil {
 			return nil, err
 		}
 	}
@@ -116,7 +116,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
 var notEnoughLines = regexp.MustCompile(`exit status 128 - fatal: file .* has only \d+ lines?`)
 
 // createCodeComment creates a plain code comment at the specified line / path
-func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *models.Issue, content, treePath string, line, reviewID int64) (*models.Comment, error) {
+func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content, treePath string, line, reviewID int64) (*issues_model.Comment, error) {
 	var commitID, patch string
 	if err := issue.LoadPullRequest(); err != nil {
 		return nil, fmt.Errorf("GetPullRequestByIssueID: %v", err)
@@ -135,11 +135,11 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo
 	head := pr.GetGitRefName()
 	if line > 0 {
 		if reviewID != 0 {
-			first, err := models.FindComments(ctx, &models.FindCommentsOptions{
+			first, err := issues_model.FindComments(ctx, &issues_model.FindCommentsOptions{
 				ReviewID: reviewID,
 				Line:     line,
 				TreePath: treePath,
-				Type:     models.CommentTypeCode,
+				Type:     issues_model.CommentTypeCode,
 				ListOptions: db.ListOptions{
 					PageSize: 1,
 					Page:     1,
@@ -149,13 +149,13 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo
 				commitID = first[0].CommitSHA
 				invalidated = first[0].Invalidated
 				patch = first[0].Patch
-			} else if err != nil && !models.IsErrCommentNotExist(err) {
+			} else if err != nil && !issues_model.IsErrCommentNotExist(err) {
 				return nil, fmt.Errorf("Find first comment for %d line %d path %s. Error: %v", reviewID, line, treePath, err)
 			} else {
-				review, err := models.GetReviewByID(ctx, reviewID)
+				review, err := issues_model.GetReviewByID(ctx, reviewID)
 				if err == nil && len(review.CommitID) > 0 {
 					head = review.CommitID
-				} else if err != nil && !models.IsErrReviewNotExist(err) {
+				} else if err != nil && !issues_model.IsErrReviewNotExist(err) {
 					return nil, fmt.Errorf("GetReviewByID %d. Error: %v", reviewID, err)
 				}
 			}
@@ -196,14 +196,14 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo
 			_ = writer.Close()
 		}()
 
-		patch, err = git.CutDiffAroundLine(reader, int64((&models.Comment{Line: line}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines)
+		patch, err = git.CutDiffAroundLine(reader, int64((&issues_model.Comment{Line: line}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines)
 		if err != nil {
 			log.Error("Error whilst generating patch: %v", err)
 			return nil, err
 		}
 	}
-	return models.CreateComment(&models.CreateCommentOptions{
-		Type:        models.CommentTypeCode,
+	return issues_model.CreateComment(&issues_model.CreateCommentOptions{
+		Type:        issues_model.CommentTypeCode,
 		Doer:        doer,
 		Repo:        repo,
 		Issue:       issue,
@@ -218,14 +218,14 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo
 }
 
 // SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
-func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *models.Issue, reviewType models.ReviewType, content, commitID string, attachmentUUIDs []string) (*models.Review, *models.Comment, error) {
+func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, reviewType issues_model.ReviewType, content, commitID string, attachmentUUIDs []string) (*issues_model.Review, *issues_model.Comment, error) {
 	pr, err := issue.GetPullRequest()
 	if err != nil {
 		return nil, nil, err
 	}
 
 	var stale bool
-	if reviewType != models.ReviewTypeApprove && reviewType != models.ReviewTypeReject {
+	if reviewType != issues_model.ReviewTypeApprove && reviewType != issues_model.ReviewTypeReject {
 		stale = false
 	} else {
 		headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName())
@@ -243,12 +243,12 @@ func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repos
 		}
 	}
 
-	review, comm, err := models.SubmitReview(doer, issue, reviewType, content, commitID, stale, attachmentUUIDs)
+	review, comm, err := issues_model.SubmitReview(doer, issue, reviewType, content, commitID, stale, attachmentUUIDs)
 	if err != nil {
 		return nil, nil, err
 	}
 
-	mentions, err := models.FindAndUpdateIssueMentions(ctx, issue, doer, comm.Content)
+	mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, doer, comm.Content)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -258,7 +258,7 @@ func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repos
 	for _, lines := range review.CodeComments {
 		for _, comments := range lines {
 			for _, codeComment := range comments {
-				mentions, err := models.FindAndUpdateIssueMentions(ctx, issue, doer, codeComment.Content)
+				mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, doer, codeComment.Content)
 				if err != nil {
 					return nil, nil, err
 				}
@@ -271,17 +271,17 @@ func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repos
 }
 
 // DismissReview dismissing stale review by repo admin
-func DismissReview(ctx context.Context, reviewID int64, message string, doer *user_model.User, isDismiss bool) (comment *models.Comment, err error) {
-	review, err := models.GetReviewByID(ctx, reviewID)
+func DismissReview(ctx context.Context, reviewID int64, message string, doer *user_model.User, isDismiss bool) (comment *issues_model.Comment, err error) {
+	review, err := issues_model.GetReviewByID(ctx, reviewID)
 	if err != nil {
 		return
 	}
 
-	if review.Type != models.ReviewTypeApprove && review.Type != models.ReviewTypeReject {
+	if review.Type != issues_model.ReviewTypeApprove && review.Type != issues_model.ReviewTypeReject {
 		return nil, fmt.Errorf("not need to dismiss this review because it's type is not Approve or change request")
 	}
 
-	if err = models.DismissReview(review, isDismiss); err != nil {
+	if err = issues_model.DismissReview(review, isDismiss); err != nil {
 		return
 	}
 
@@ -296,14 +296,14 @@ func DismissReview(ctx context.Context, reviewID int64, message string, doer *us
 	if err = review.Issue.LoadPullRequest(); err != nil {
 		return
 	}
-	if err = review.Issue.LoadAttributes(); err != nil {
+	if err = review.Issue.LoadAttributes(ctx); err != nil {
 		return
 	}
 
-	comment, err = models.CreateComment(&models.CreateCommentOptions{
+	comment, err = issues_model.CreateComment(&issues_model.CreateCommentOptions{
 		Doer:     doer,
 		Content:  message,
-		Type:     models.CommentTypeDismissReview,
+		Type:     issues_model.CommentTypeDismissReview,
 		ReviewID: review.ID,
 		Issue:    review.Issue,
 		Repo:     review.Issue.Repo,
diff --git a/services/pull/temp_repo.go b/services/pull/temp_repo.go
index 6b01809d49..c1456ef0a9 100644
--- a/services/pull/temp_repo.go
+++ b/services/pull/temp_repo.go
@@ -13,6 +13,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
@@ -21,7 +22,7 @@ import (
 
 // createTemporaryRepo creates a temporary repo with "base" for pr.BaseBranch and "tracking" for  pr.HeadBranch
 // it also create a second base branch called "original_base"
-func createTemporaryRepo(ctx context.Context, pr *models.PullRequest) (string, error) {
+func createTemporaryRepo(ctx context.Context, pr *issues_model.PullRequest) (string, error) {
 	if err := pr.LoadHeadRepoCtx(ctx); err != nil {
 		log.Error("LoadHeadRepo: %v", err)
 		return "", fmt.Errorf("LoadHeadRepo: %v", err)
@@ -164,7 +165,7 @@ func createTemporaryRepo(ctx context.Context, pr *models.PullRequest) (string, e
 	trackingBranch := "tracking"
 	// Fetch head branch
 	var headBranch string
-	if pr.Flow == models.PullRequestFlowGithub {
+	if pr.Flow == issues_model.PullRequestFlowGithub {
 		headBranch = git.BranchPrefix + pr.HeadBranch
 	} else if len(pr.HeadCommitID) == 40 { // for not created pull request
 		headBranch = pr.HeadCommitID
diff --git a/services/pull/update.go b/services/pull/update.go
index 0ab8ffcd7d..e5e26462e5 100644
--- a/services/pull/update.go
+++ b/services/pull/update.go
@@ -9,6 +9,7 @@ import (
 	"fmt"
 
 	"code.gitea.io/gitea/models"
+	issues_model "code.gitea.io/gitea/models/issues"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
@@ -19,9 +20,9 @@ import (
 )
 
 // Update updates pull request with base branch.
-func Update(ctx context.Context, pull *models.PullRequest, doer *user_model.User, message string, rebase bool) error {
+func Update(ctx context.Context, pull *issues_model.PullRequest, doer *user_model.User, message string, rebase bool) error {
 	var (
-		pr    *models.PullRequest
+		pr    *issues_model.PullRequest
 		style repo_model.MergeStyle
 	)
 
@@ -33,7 +34,7 @@ func Update(ctx context.Context, pull *models.PullRequest, doer *user_model.User
 		style = repo_model.MergeStyleRebaseUpdate
 	} else {
 		// use merge functions but switch repo's and branch's
-		pr = &models.PullRequest{
+		pr = &issues_model.PullRequest{
 			HeadRepoID: pull.BaseRepoID,
 			BaseRepoID: pull.HeadRepoID,
 			HeadBranch: pull.BaseBranch,
@@ -42,7 +43,7 @@ func Update(ctx context.Context, pull *models.PullRequest, doer *user_model.User
 		style = repo_model.MergeStyleMerge
 	}
 
-	if pull.Flow == models.PullRequestFlowAGit {
+	if pull.Flow == issues_model.PullRequestFlowAGit {
 		// TODO: Not support update agit flow pull request's head branch
 		return fmt.Errorf("Not support update agit flow pull request's head branch")
 	}
@@ -76,8 +77,8 @@ func Update(ctx context.Context, pull *models.PullRequest, doer *user_model.User
 }
 
 // IsUserAllowedToUpdate check if user is allowed to update PR with given permissions and branch protections
-func IsUserAllowedToUpdate(ctx context.Context, pull *models.PullRequest, user *user_model.User) (mergeAllowed, rebaseAllowed bool, err error) {
-	if pull.Flow == models.PullRequestFlowAGit {
+func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest, user *user_model.User) (mergeAllowed, rebaseAllowed bool, err error) {
+	if pull.Flow == issues_model.PullRequestFlowAGit {
 		return false, false, nil
 	}
 
@@ -89,7 +90,7 @@ func IsUserAllowedToUpdate(ctx context.Context, pull *models.PullRequest, user *
 		return false, false, err
 	}
 
-	pr := &models.PullRequest{
+	pr := &issues_model.PullRequest{
 		HeadRepoID: pull.BaseRepoID,
 		BaseRepoID: pull.HeadRepoID,
 		HeadBranch: pull.BaseBranch,
@@ -139,7 +140,7 @@ func IsUserAllowedToUpdate(ctx context.Context, pull *models.PullRequest, user *
 }
 
 // GetDiverging determines how many commits a PR is ahead or behind the PR base branch
-func GetDiverging(ctx context.Context, pr *models.PullRequest) (*git.DivergeObject, error) {
+func GetDiverging(ctx context.Context, pr *issues_model.PullRequest) (*git.DivergeObject, error) {
 	log.Trace("GetDiverging[%d]: compare commits", pr.ID)
 	if err := pr.LoadBaseRepoCtx(ctx); err != nil {
 		return nil, err
diff --git a/services/repository/repository.go b/services/repository/repository.go
index 6848eda101..4bde6879a6 100644
--- a/services/repository/repository.go
+++ b/services/repository/repository.go
@@ -11,6 +11,7 @@ import (
 	"code.gitea.io/gitea/models"
 	admin_model "code.gitea.io/gitea/models/admin"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
 	packages_model "code.gitea.io/gitea/models/packages"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -105,7 +106,7 @@ func UpdateRepository(repo *repo_model.Repository, visibilityChanged bool) (err
 // LinkedRepository returns the linked repo if any
 func LinkedRepository(a *repo_model.Attachment) (*repo_model.Repository, unit.Type, error) {
 	if a.IssueID != 0 {
-		iss, err := models.GetIssueByID(a.IssueID)
+		iss, err := issues_model.GetIssueByID(db.DefaultContext, a.IssueID)
 		if err != nil {
 			return nil, unit.TypeIssues, err
 		}
diff --git a/services/repository/template.go b/services/repository/template.go
index 6a1bfaff5b..d7e8145811 100644
--- a/services/repository/template.go
+++ b/services/repository/template.go
@@ -9,6 +9,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
@@ -18,14 +19,14 @@ import (
 
 // GenerateIssueLabels generates issue labels from a template repository
 func GenerateIssueLabels(ctx context.Context, templateRepo, generateRepo *repo_model.Repository) error {
-	templateLabels, err := models.GetLabelsByRepoID(ctx, templateRepo.ID, "", db.ListOptions{})
+	templateLabels, err := issues_model.GetLabelsByRepoID(ctx, templateRepo.ID, "", db.ListOptions{})
 	if err != nil {
 		return err
 	}
 
-	newLabels := make([]*models.Label, 0, len(templateLabels))
+	newLabels := make([]*issues_model.Label, 0, len(templateLabels))
 	for _, templateLabel := range templateLabels {
-		newLabels = append(newLabels, &models.Label{
+		newLabels = append(newLabels, &issues_model.Label{
 			RepoID:      generateRepo.ID,
 			Name:        templateLabel.Name,
 			Description: templateLabel.Description,

From 3c6c1507403b659a9ed790df19311a9f83175364 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 13 Jun 2022 20:55:08 +0800
Subject: [PATCH 4/7] Add deprecated log when using MySQL with utf8 charset
 (#19952)

---
 modules/setting/database.go | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/modules/setting/database.go b/modules/setting/database.go
index 5be2d8deea..a90ace5b4b 100644
--- a/modules/setting/database.go
+++ b/modules/setting/database.go
@@ -13,6 +13,8 @@ import (
 	"path/filepath"
 	"strings"
 	"time"
+
+	"code.gitea.io/gitea/modules/log"
 )
 
 var (
@@ -83,6 +85,10 @@ func InitDBConfig() {
 	Database.Schema = sec.Key("SCHEMA").String()
 	Database.SSLMode = sec.Key("SSL_MODE").MustString("disable")
 	Database.Charset = sec.Key("CHARSET").In(defaultCharset, []string{"utf8", "utf8mb4"})
+	if Database.UseMySQL && defaultCharset != "utf8mb4" {
+		log.Error("Deprecated database mysql charset utf8 support, please use utf8mb4 or convert utf8 to utf8mb4.")
+	}
+
 	Database.Path = sec.Key("PATH").MustString(filepath.Join(AppDataPath, "gitea.db"))
 	Database.Timeout = sec.Key("SQLITE_TIMEOUT").MustInt(500)
 	Database.MaxIdleConns = sec.Key("MAX_IDLE_CONNS").MustInt(2)

From ff82a1831521bb2a9d91884eb2bc426a6403b0ed Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 14 Jun 2022 00:12:59 +0800
Subject: [PATCH 5/7] Fix mirror template bug (#19959)

* Fix mirror template bug

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
---
 modules/context/repo.go    | 20 ++++++--------------
 templates/repo/header.tmpl |  7 ++++---
 2 files changed, 10 insertions(+), 17 deletions(-)

diff --git a/modules/context/repo.go b/modules/context/repo.go
index c2b8306b9d..8e75ad07d5 100644
--- a/modules/context/repo.go
+++ b/modules/context/repo.go
@@ -380,24 +380,16 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
 	ctx.Data["Permission"] = &ctx.Repo.Permission
 
 	if repo.IsMirror {
-
-		// Check if the mirror has finsihed migrationg, only then we can
-		// lookup the mirror informtation the database.
-		finishedMigrating, err := models.HasFinishedMigratingTask(repo.ID)
-		if err != nil {
-			ctx.ServerError("HasFinishedMigratingTask", err)
-			return
-		}
-		if finishedMigrating {
-			ctx.Repo.Mirror, err = repo_model.GetMirrorByRepoID(ctx, repo.ID)
-			if err != nil {
-				ctx.ServerError("GetMirrorByRepoID", err)
-				return
-			}
+		ctx.Repo.Mirror, err = repo_model.GetMirrorByRepoID(ctx, repo.ID)
+		if err == nil {
 			ctx.Repo.Mirror.Repo = repo
+			ctx.Data["IsPullMirror"] = true
 			ctx.Data["MirrorEnablePrune"] = ctx.Repo.Mirror.EnablePrune
 			ctx.Data["MirrorInterval"] = ctx.Repo.Mirror.Interval
 			ctx.Data["Mirror"] = ctx.Repo.Mirror
+		} else if err != repo_model.ErrMirrorNotExist {
+			ctx.ServerError("GetMirrorByRepoID", err)
+			return
 		}
 	}
 
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl
index cfac37cd11..029e9a186f 100644
--- a/templates/repo/header.tmpl
+++ b/templates/repo/header.tmpl
@@ -37,9 +37,10 @@
 						{{end}}
 					</div>
 				</div>
-				{{if .IsMirror}}
-				{{$address := MirrorRemoteAddress $.Context . $.Mirror.GetRemoteName}}
-				<div class="fork-flag">{{$.i18n.Tr "repo.mirror_from"}} <a target="_blank" rel="noopener noreferrer" href="{{$address.Address}}">{{$address.Address}}</a></div>{{end}}
+				{{if $.IsPullMirror}}
+					{{$address := MirrorRemoteAddress $.Context . $.Mirror.GetRemoteName}}
+					<div class="fork-flag">{{$.i18n.Tr "repo.mirror_from"}} <a target="_blank" rel="noopener noreferrer" href="{{$address.Address}}">{{$address.Address}}</a></div>
+				{{end}}
 				{{if .IsFork}}<div class="fork-flag">{{$.i18n.Tr "repo.forked_from"}} <a href="{{.BaseRepo.Link}}">{{.BaseRepo.FullName}}</a></div>{{end}}
 				{{if .IsGenerated}}<div class="fork-flag">{{$.i18n.Tr "repo.generated_from"}} <a href="{{.TemplateRepo.Link}}">{{.TemplateRepo.FullName}}</a></div>{{end}}
 			</div>

From 0d7eda511f57a5327706129f9fc25a3fa5c1a898 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 14 Jun 2022 02:46:39 +0800
Subject: [PATCH 6/7] Fix aria for logo (#19955)

Co-authored-by: 6543 <6543@obermui.de>
---
 templates/base/head_navbar.tmpl | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl
index 37a579142c..fab1d2d0b1 100644
--- a/templates/base/head_navbar.tmpl
+++ b/templates/base/head_navbar.tmpl
@@ -1,7 +1,7 @@
 <div class="ui container" id="navbar">
 	<div class="item brand" style="justify-content: space-between;">
-		<a href="{{AppSubUrl}}/" data-content="{{if .IsSigned}}{{.i18n.Tr "dashboard"}}{{else}}{{.i18n.Tr "home"}}{{end}}">
-			<img class="ui mini image" width="30" height="30" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{.i18n.Tr "logo"}}">
+		<a href="{{AppSubUrl}}/" aria-label="{{if .IsSigned}}{{.i18n.Tr "dashboard"}}{{else}}{{.i18n.Tr "home"}}{{end}}">
+			<img class="ui mini image" width="30" height="30" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{.i18n.Tr "logo"}}" aria-hidden="true">
 		</a>
 		<div class="ui basic icon button mobile-only" id="navbar-expand-toggle">
 			<i class="sidebar icon"></i>

From 1fef9a2d698150f86efd4c71c8f9f732b56a1fd1 Mon Sep 17 00:00:00 2001
From: singuliere <35190819+singuliere@users.noreply.github.com>
Date: Mon, 13 Jun 2022 22:05:41 +0200
Subject: [PATCH 7/7] Remove singuliere from MAINTAINERS (#19883)

---
 MAINTAINERS | 1 -
 1 file changed, 1 deletion(-)

diff --git a/MAINTAINERS b/MAINTAINERS
index 907cbb5c41..ec49f3f140 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -45,6 +45,5 @@ Steven Kriegler <sk.bunsenbrenner@gmail.com> (@justusbunsi)
 Jimmy Praet <jimmy.praet@telenet.be> (@jpraet)
 Leon Hofmeister <dev.lh@web.de> (@delvh) 
 Gusted <williamzijl7@hotmail.com) (@Gusted)
-singuliere <singuliere@autistici.org> (@singuliere)
 silentcode <silentcode@senga.org> (@silentcodeg)
 Wim <wim@42.be> (@42wim)