]> source.dussan.org Git - gitea.git/commitdiff
Refactor routers directory (#15800)
authorLunny Xiao <xiaolunwen@gmail.com>
Tue, 8 Jun 2021 23:33:54 +0000 (07:33 +0800)
committerGitHub <noreply@github.com>
Tue, 8 Jun 2021 23:33:54 +0000 (01:33 +0200)
* refactor routers directory

* move func used for web and api to common

* make corsHandler a function to prohibit side efects

* rm unused func

Co-authored-by: 6543 <6543@obermui.de>
195 files changed:
.golangci.yml
cmd/web.go
contrib/pr/checkout.go
integrations/create_no_session_test.go
integrations/integration_test.go
integrations/lfs_getobject_test.go
routers/admin/admin.go [deleted file]
routers/admin/admin_test.go [deleted file]
routers/admin/auths.go [deleted file]
routers/admin/emails.go [deleted file]
routers/admin/hooks.go [deleted file]
routers/admin/main_test.go [deleted file]
routers/admin/notice.go [deleted file]
routers/admin/orgs.go [deleted file]
routers/admin/repos.go [deleted file]
routers/admin/users.go [deleted file]
routers/admin/users_test.go [deleted file]
routers/api/v1/repo/file.go
routers/common/db.go [new file with mode: 0644]
routers/common/logger.go [new file with mode: 0644]
routers/common/middleware.go [new file with mode: 0644]
routers/common/repo.go [new file with mode: 0644]
routers/dev/template.go [deleted file]
routers/events/events.go [deleted file]
routers/home.go [deleted file]
routers/init.go
routers/install.go [deleted file]
routers/install/install.go [new file with mode: 0644]
routers/install/routes.go [new file with mode: 0644]
routers/install/setting.go [new file with mode: 0644]
routers/metrics.go [deleted file]
routers/org/home.go [deleted file]
routers/org/members.go [deleted file]
routers/org/org.go [deleted file]
routers/org/org_labels.go [deleted file]
routers/org/setting.go [deleted file]
routers/org/teams.go [deleted file]
routers/repo/activity.go [deleted file]
routers/repo/attachment.go [deleted file]
routers/repo/blame.go [deleted file]
routers/repo/branch.go [deleted file]
routers/repo/commit.go [deleted file]
routers/repo/compare.go [deleted file]
routers/repo/download.go [deleted file]
routers/repo/editor.go [deleted file]
routers/repo/editor_test.go [deleted file]
routers/repo/http.go [deleted file]
routers/repo/issue.go [deleted file]
routers/repo/issue_dependency.go [deleted file]
routers/repo/issue_label.go [deleted file]
routers/repo/issue_label_test.go [deleted file]
routers/repo/issue_lock.go [deleted file]
routers/repo/issue_stopwatch.go [deleted file]
routers/repo/issue_test.go [deleted file]
routers/repo/issue_timetrack.go [deleted file]
routers/repo/issue_watch.go [deleted file]
routers/repo/lfs.go [deleted file]
routers/repo/main_test.go [deleted file]
routers/repo/middlewares.go [deleted file]
routers/repo/migrate.go [deleted file]
routers/repo/milestone.go [deleted file]
routers/repo/projects.go [deleted file]
routers/repo/projects_test.go [deleted file]
routers/repo/pull.go [deleted file]
routers/repo/pull_review.go [deleted file]
routers/repo/release.go [deleted file]
routers/repo/release_test.go [deleted file]
routers/repo/repo.go [deleted file]
routers/repo/search.go [deleted file]
routers/repo/setting.go [deleted file]
routers/repo/setting_protected_branch.go [deleted file]
routers/repo/settings_test.go [deleted file]
routers/repo/topic.go [deleted file]
routers/repo/view.go [deleted file]
routers/repo/webhook.go [deleted file]
routers/repo/wiki.go [deleted file]
routers/repo/wiki_test.go [deleted file]
routers/routes/base.go [deleted file]
routers/routes/goget.go [deleted file]
routers/routes/install.go [deleted file]
routers/routes/web.go [deleted file]
routers/swagger_json.go [deleted file]
routers/user/auth.go [deleted file]
routers/user/auth_openid.go [deleted file]
routers/user/avatar.go [deleted file]
routers/user/home.go [deleted file]
routers/user/home_test.go [deleted file]
routers/user/main_test.go [deleted file]
routers/user/notification.go [deleted file]
routers/user/oauth.go [deleted file]
routers/user/profile.go [deleted file]
routers/user/setting/account.go [deleted file]
routers/user/setting/account_test.go [deleted file]
routers/user/setting/adopt.go [deleted file]
routers/user/setting/applications.go [deleted file]
routers/user/setting/keys.go [deleted file]
routers/user/setting/main_test.go [deleted file]
routers/user/setting/oauth2.go [deleted file]
routers/user/setting/profile.go [deleted file]
routers/user/setting/security.go [deleted file]
routers/user/setting/security_openid.go [deleted file]
routers/user/setting/security_twofa.go [deleted file]
routers/user/setting/security_u2f.go [deleted file]
routers/user/task.go [deleted file]
routers/web/admin/admin.go [new file with mode: 0644]
routers/web/admin/admin_test.go [new file with mode: 0644]
routers/web/admin/auths.go [new file with mode: 0644]
routers/web/admin/emails.go [new file with mode: 0644]
routers/web/admin/hooks.go [new file with mode: 0644]
routers/web/admin/main_test.go [new file with mode: 0644]
routers/web/admin/notice.go [new file with mode: 0644]
routers/web/admin/orgs.go [new file with mode: 0644]
routers/web/admin/repos.go [new file with mode: 0644]
routers/web/admin/users.go [new file with mode: 0644]
routers/web/admin/users_test.go [new file with mode: 0644]
routers/web/base.go [new file with mode: 0644]
routers/web/dev/template.go [new file with mode: 0644]
routers/web/events/events.go [new file with mode: 0644]
routers/web/explore/code.go [new file with mode: 0644]
routers/web/explore/org.go [new file with mode: 0644]
routers/web/explore/repo.go [new file with mode: 0644]
routers/web/explore/user.go [new file with mode: 0644]
routers/web/goget.go [new file with mode: 0644]
routers/web/home.go [new file with mode: 0644]
routers/web/metrics.go [new file with mode: 0644]
routers/web/org/home.go [new file with mode: 0644]
routers/web/org/members.go [new file with mode: 0644]
routers/web/org/org.go [new file with mode: 0644]
routers/web/org/org_labels.go [new file with mode: 0644]
routers/web/org/setting.go [new file with mode: 0644]
routers/web/org/teams.go [new file with mode: 0644]
routers/web/repo/activity.go [new file with mode: 0644]
routers/web/repo/attachment.go [new file with mode: 0644]
routers/web/repo/blame.go [new file with mode: 0644]
routers/web/repo/branch.go [new file with mode: 0644]
routers/web/repo/commit.go [new file with mode: 0644]
routers/web/repo/compare.go [new file with mode: 0644]
routers/web/repo/download.go [new file with mode: 0644]
routers/web/repo/editor.go [new file with mode: 0644]
routers/web/repo/editor_test.go [new file with mode: 0644]
routers/web/repo/http.go [new file with mode: 0644]
routers/web/repo/issue.go [new file with mode: 0644]
routers/web/repo/issue_dependency.go [new file with mode: 0644]
routers/web/repo/issue_label.go [new file with mode: 0644]
routers/web/repo/issue_label_test.go [new file with mode: 0644]
routers/web/repo/issue_lock.go [new file with mode: 0644]
routers/web/repo/issue_stopwatch.go [new file with mode: 0644]
routers/web/repo/issue_test.go [new file with mode: 0644]
routers/web/repo/issue_timetrack.go [new file with mode: 0644]
routers/web/repo/issue_watch.go [new file with mode: 0644]
routers/web/repo/lfs.go [new file with mode: 0644]
routers/web/repo/main_test.go [new file with mode: 0644]
routers/web/repo/middlewares.go [new file with mode: 0644]
routers/web/repo/migrate.go [new file with mode: 0644]
routers/web/repo/milestone.go [new file with mode: 0644]
routers/web/repo/projects.go [new file with mode: 0644]
routers/web/repo/projects_test.go [new file with mode: 0644]
routers/web/repo/pull.go [new file with mode: 0644]
routers/web/repo/pull_review.go [new file with mode: 0644]
routers/web/repo/release.go [new file with mode: 0644]
routers/web/repo/release_test.go [new file with mode: 0644]
routers/web/repo/repo.go [new file with mode: 0644]
routers/web/repo/search.go [new file with mode: 0644]
routers/web/repo/setting.go [new file with mode: 0644]
routers/web/repo/setting_protected_branch.go [new file with mode: 0644]
routers/web/repo/settings_test.go [new file with mode: 0644]
routers/web/repo/topic.go [new file with mode: 0644]
routers/web/repo/view.go [new file with mode: 0644]
routers/web/repo/webhook.go [new file with mode: 0644]
routers/web/repo/wiki.go [new file with mode: 0644]
routers/web/repo/wiki_test.go [new file with mode: 0644]
routers/web/swagger_json.go [new file with mode: 0644]
routers/web/user/auth.go [new file with mode: 0644]
routers/web/user/auth_openid.go [new file with mode: 0644]
routers/web/user/avatar.go [new file with mode: 0644]
routers/web/user/home.go [new file with mode: 0644]
routers/web/user/home_test.go [new file with mode: 0644]
routers/web/user/main_test.go [new file with mode: 0644]
routers/web/user/notification.go [new file with mode: 0644]
routers/web/user/oauth.go [new file with mode: 0644]
routers/web/user/profile.go [new file with mode: 0644]
routers/web/user/setting/account.go [new file with mode: 0644]
routers/web/user/setting/account_test.go [new file with mode: 0644]
routers/web/user/setting/adopt.go [new file with mode: 0644]
routers/web/user/setting/applications.go [new file with mode: 0644]
routers/web/user/setting/keys.go [new file with mode: 0644]
routers/web/user/setting/main_test.go [new file with mode: 0644]
routers/web/user/setting/oauth2.go [new file with mode: 0644]
routers/web/user/setting/profile.go [new file with mode: 0644]
routers/web/user/setting/security.go [new file with mode: 0644]
routers/web/user/setting/security_openid.go [new file with mode: 0644]
routers/web/user/setting/security_twofa.go [new file with mode: 0644]
routers/web/user/setting/security_u2f.go [new file with mode: 0644]
routers/web/user/task.go [new file with mode: 0644]
routers/web/web.go [new file with mode: 0644]

index 0d7f90e263c407876ddce3a2fa8bce7b2fd5c705..c3dd47ec29da67e8231149355f1f25149583c058 100644 (file)
@@ -70,9 +70,6 @@ issues:
     - path: modules/log/
       linters:
         - errcheck
-    - path: routers/routes/web.go
-      linters:
-        - dupl
     - path: routers/api/v1/repo/issue_subscription.go
       linters:
         - dupl
@@ -114,3 +111,4 @@ issues:
       linters:
         - staticcheck
       text: "svc.IsAnInteractiveSession is deprecated: Use IsWindowsService instead."
+
index 9c7d493339f04114bc81848c7cf2688c4f74d497..0ba14ae70642ca8e71a2ff926e1d483ae6208fe6 100644 (file)
@@ -17,7 +17,7 @@ import (
        "code.gitea.io/gitea/modules/log"
        "code.gitea.io/gitea/modules/setting"
        "code.gitea.io/gitea/routers"
-       "code.gitea.io/gitea/routers/routes"
+       "code.gitea.io/gitea/routers/install"
 
        context2 "github.com/gorilla/context"
        "github.com/urfave/cli"
@@ -88,7 +88,7 @@ func runWeb(ctx *cli.Context) error {
        }
 
        // Perform pre-initialization
-       needsInstall := routers.PreInstallInit(graceful.GetManager().HammerContext())
+       needsInstall := install.PreloadSettings(graceful.GetManager().HammerContext())
        if needsInstall {
                // Flag for port number in case first time run conflict
                if ctx.IsSet("port") {
@@ -101,7 +101,7 @@ func runWeb(ctx *cli.Context) error {
                                return err
                        }
                }
-               c := routes.InstallRoutes()
+               c := install.Routes()
                err := listen(c, false)
                select {
                case <-graceful.GetManager().IsShutdown():
@@ -134,7 +134,7 @@ func runWeb(ctx *cli.Context) error {
        }
 
        // Set up Chi routes
-       c := routes.NormalRoutes()
+       c := routers.NormalRoutes()
        err := listen(c, true)
        <-graceful.GetManager().Done()
        log.Info("PID: %d Gitea Web Finished", os.Getpid())
index 9ee692fd35b1a7191eeb9fd99afa9afa35a17caa..9ce84f762cecd809ab7b952ec288648fc0a54ae5 100644 (file)
@@ -31,7 +31,6 @@ import (
        "code.gitea.io/gitea/modules/setting"
        "code.gitea.io/gitea/modules/util"
        "code.gitea.io/gitea/routers"
-       "code.gitea.io/gitea/routers/routes"
 
        "github.com/go-git/go-git/v5"
        "github.com/go-git/go-git/v5/config"
@@ -116,7 +115,7 @@ func runPR() {
        //routers.GlobalInit()
        external.RegisterRenderers()
        markup.Init()
-       c := routes.NormalRoutes()
+       c := routers.NormalRoutes()
 
        log.Printf("[PR] Ready for testing !\n")
        log.Printf("[PR] Login with user1, user2, user3, ... with pass: password\n")
index c864b9c7ae125df65eab9eaab69c224398ab4ee0..46f111b6f7d8439f4a82f5ea782a558a7a271da4 100644 (file)
@@ -14,7 +14,7 @@ import (
 
        "code.gitea.io/gitea/modules/setting"
        "code.gitea.io/gitea/modules/util"
-       "code.gitea.io/gitea/routers/routes"
+       "code.gitea.io/gitea/routers"
 
        "gitea.com/go-chi/session"
        jsoniter "github.com/json-iterator/go"
@@ -58,7 +58,7 @@ func TestSessionFileCreation(t *testing.T) {
        oldSessionConfig := setting.SessionConfig.ProviderConfig
        defer func() {
                setting.SessionConfig.ProviderConfig = oldSessionConfig
-               c = routes.NormalRoutes()
+               c = routers.NormalRoutes()
        }()
 
        var config session.Options
@@ -84,7 +84,7 @@ func TestSessionFileCreation(t *testing.T) {
 
        setting.SessionConfig.ProviderConfig = string(newConfigBytes)
 
-       c = routes.NormalRoutes()
+       c = routers.NormalRoutes()
 
        t.Run("NoSessionOnViewIssue", func(t *testing.T) {
                defer PrintCurrentTest(t)()
index 74227416c4be5a2771ba460e05af5362ad4db292..d755977d1ab16a6be3d50607e7e641cf2b77d0dd 100644 (file)
@@ -34,7 +34,6 @@ import (
        "code.gitea.io/gitea/modules/util"
        "code.gitea.io/gitea/modules/web"
        "code.gitea.io/gitea/routers"
-       "code.gitea.io/gitea/routers/routes"
 
        "github.com/PuerkitoBio/goquery"
        jsoniter "github.com/json-iterator/go"
@@ -88,7 +87,7 @@ func TestMain(m *testing.M) {
        defer cancel()
 
        initIntegrationTest()
-       c = routes.NormalRoutes()
+       c = routers.NormalRoutes()
 
        // integration test settings...
        if setting.Cfg != nil {
index b7423a2dbe55fd6dc7c9a4098a2951882c74b402..337a93567a49f8c7a5258daa0197c01e684a3ddc 100644 (file)
@@ -15,7 +15,7 @@ import (
        "code.gitea.io/gitea/models"
        "code.gitea.io/gitea/modules/lfs"
        "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/routers/routes"
+       "code.gitea.io/gitea/routers/web"
 
        jsoniter "github.com/json-iterator/go"
        gzipp "github.com/klauspost/compress/gzip"
@@ -99,7 +99,7 @@ func TestGetLFSLarge(t *testing.T) {
                t.Skip()
                return
        }
-       content := make([]byte, routes.GzipMinSize*10)
+       content := make([]byte, web.GzipMinSize*10)
        for i := range content {
                content[i] = byte(i % 256)
        }
@@ -115,7 +115,7 @@ func TestGetLFSGzip(t *testing.T) {
                t.Skip()
                return
        }
-       b := make([]byte, routes.GzipMinSize*10)
+       b := make([]byte, web.GzipMinSize*10)
        for i := range b {
                b[i] = byte(i % 256)
        }
@@ -136,7 +136,7 @@ func TestGetLFSZip(t *testing.T) {
                t.Skip()
                return
        }
-       b := make([]byte, routes.GzipMinSize*10)
+       b := make([]byte, web.GzipMinSize*10)
        for i := range b {
                b[i] = byte(i % 256)
        }
diff --git a/routers/admin/admin.go b/routers/admin/admin.go
deleted file mode 100644 (file)
index c2d94ab..0000000
+++ /dev/null
@@ -1,485 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2019 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 admin
-
-import (
-       "fmt"
-       "net/http"
-       "net/url"
-       "os"
-       "runtime"
-       "strconv"
-       "strings"
-       "time"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/cron"
-       "code.gitea.io/gitea/modules/git"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/process"
-       "code.gitea.io/gitea/modules/queue"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/timeutil"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-       "code.gitea.io/gitea/services/mailer"
-       jsoniter "github.com/json-iterator/go"
-
-       "gitea.com/go-chi/session"
-)
-
-const (
-       tplDashboard base.TplName = "admin/dashboard"
-       tplConfig    base.TplName = "admin/config"
-       tplMonitor   base.TplName = "admin/monitor"
-       tplQueue     base.TplName = "admin/queue"
-)
-
-var sysStatus struct {
-       Uptime       string
-       NumGoroutine int
-
-       // General statistics.
-       MemAllocated string // bytes allocated and still in use
-       MemTotal     string // bytes allocated (even if freed)
-       MemSys       string // bytes obtained from system (sum of XxxSys below)
-       Lookups      uint64 // number of pointer lookups
-       MemMallocs   uint64 // number of mallocs
-       MemFrees     uint64 // number of frees
-
-       // Main allocation heap statistics.
-       HeapAlloc    string // bytes allocated and still in use
-       HeapSys      string // bytes obtained from system
-       HeapIdle     string // bytes in idle spans
-       HeapInuse    string // bytes in non-idle span
-       HeapReleased string // bytes released to the OS
-       HeapObjects  uint64 // total number of allocated objects
-
-       // Low-level fixed-size structure allocator statistics.
-       //      Inuse is bytes used now.
-       //      Sys is bytes obtained from system.
-       StackInuse  string // bootstrap stacks
-       StackSys    string
-       MSpanInuse  string // mspan structures
-       MSpanSys    string
-       MCacheInuse string // mcache structures
-       MCacheSys   string
-       BuckHashSys string // profiling bucket hash table
-       GCSys       string // GC metadata
-       OtherSys    string // other system allocations
-
-       // Garbage collector statistics.
-       NextGC       string // next run in HeapAlloc time (bytes)
-       LastGC       string // last run in absolute time (ns)
-       PauseTotalNs string
-       PauseNs      string // circular buffer of recent GC pause times, most recent at [(NumGC+255)%256]
-       NumGC        uint32
-}
-
-func updateSystemStatus() {
-       sysStatus.Uptime = timeutil.TimeSincePro(setting.AppStartTime, "en")
-
-       m := new(runtime.MemStats)
-       runtime.ReadMemStats(m)
-       sysStatus.NumGoroutine = runtime.NumGoroutine()
-
-       sysStatus.MemAllocated = base.FileSize(int64(m.Alloc))
-       sysStatus.MemTotal = base.FileSize(int64(m.TotalAlloc))
-       sysStatus.MemSys = base.FileSize(int64(m.Sys))
-       sysStatus.Lookups = m.Lookups
-       sysStatus.MemMallocs = m.Mallocs
-       sysStatus.MemFrees = m.Frees
-
-       sysStatus.HeapAlloc = base.FileSize(int64(m.HeapAlloc))
-       sysStatus.HeapSys = base.FileSize(int64(m.HeapSys))
-       sysStatus.HeapIdle = base.FileSize(int64(m.HeapIdle))
-       sysStatus.HeapInuse = base.FileSize(int64(m.HeapInuse))
-       sysStatus.HeapReleased = base.FileSize(int64(m.HeapReleased))
-       sysStatus.HeapObjects = m.HeapObjects
-
-       sysStatus.StackInuse = base.FileSize(int64(m.StackInuse))
-       sysStatus.StackSys = base.FileSize(int64(m.StackSys))
-       sysStatus.MSpanInuse = base.FileSize(int64(m.MSpanInuse))
-       sysStatus.MSpanSys = base.FileSize(int64(m.MSpanSys))
-       sysStatus.MCacheInuse = base.FileSize(int64(m.MCacheInuse))
-       sysStatus.MCacheSys = base.FileSize(int64(m.MCacheSys))
-       sysStatus.BuckHashSys = base.FileSize(int64(m.BuckHashSys))
-       sysStatus.GCSys = base.FileSize(int64(m.GCSys))
-       sysStatus.OtherSys = base.FileSize(int64(m.OtherSys))
-
-       sysStatus.NextGC = base.FileSize(int64(m.NextGC))
-       sysStatus.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000)
-       sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000)
-       sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000)
-       sysStatus.NumGC = m.NumGC
-}
-
-// Dashboard show admin panel dashboard
-func Dashboard(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("admin.dashboard")
-       ctx.Data["PageIsAdmin"] = true
-       ctx.Data["PageIsAdminDashboard"] = true
-       ctx.Data["Stats"] = models.GetStatistic()
-       // FIXME: update periodically
-       updateSystemStatus()
-       ctx.Data["SysStatus"] = sysStatus
-       ctx.Data["SSH"] = setting.SSH
-       ctx.HTML(http.StatusOK, tplDashboard)
-}
-
-// DashboardPost run an admin operation
-func DashboardPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.AdminDashboardForm)
-       ctx.Data["Title"] = ctx.Tr("admin.dashboard")
-       ctx.Data["PageIsAdmin"] = true
-       ctx.Data["PageIsAdminDashboard"] = true
-       ctx.Data["Stats"] = models.GetStatistic()
-       updateSystemStatus()
-       ctx.Data["SysStatus"] = sysStatus
-
-       // Run operation.
-       if form.Op != "" {
-               task := cron.GetTask(form.Op)
-               if task != nil {
-                       go task.RunWithUser(ctx.User, nil)
-                       ctx.Flash.Success(ctx.Tr("admin.dashboard.task.started", ctx.Tr("admin.dashboard."+form.Op)))
-               } else {
-                       ctx.Flash.Error(ctx.Tr("admin.dashboard.task.unknown", form.Op))
-               }
-       }
-       if form.From == "monitor" {
-               ctx.Redirect(setting.AppSubURL + "/admin/monitor")
-       } else {
-               ctx.Redirect(setting.AppSubURL + "/admin")
-       }
-}
-
-// SendTestMail send test mail to confirm mail service is OK
-func SendTestMail(ctx *context.Context) {
-       email := ctx.Query("email")
-       // Send a test email to the user's email address and redirect back to Config
-       if err := mailer.SendTestMail(email); err != nil {
-               ctx.Flash.Error(ctx.Tr("admin.config.test_mail_failed", email, err))
-       } else {
-               ctx.Flash.Info(ctx.Tr("admin.config.test_mail_sent", email))
-       }
-
-       ctx.Redirect(setting.AppSubURL + "/admin/config")
-}
-
-func shadowPasswordKV(cfgItem, splitter string) string {
-       fields := strings.Split(cfgItem, splitter)
-       for i := 0; i < len(fields); i++ {
-               if strings.HasPrefix(fields[i], "password=") {
-                       fields[i] = "password=******"
-                       break
-               }
-       }
-       return strings.Join(fields, splitter)
-}
-
-func shadowURL(provider, cfgItem string) string {
-       u, err := url.Parse(cfgItem)
-       if err != nil {
-               log.Error("Shadowing Password for %v failed: %v", provider, err)
-               return cfgItem
-       }
-       if u.User != nil {
-               atIdx := strings.Index(cfgItem, "@")
-               if atIdx > 0 {
-                       colonIdx := strings.LastIndex(cfgItem[:atIdx], ":")
-                       if colonIdx > 0 {
-                               return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:]
-                       }
-               }
-       }
-       return cfgItem
-}
-
-func shadowPassword(provider, cfgItem string) string {
-       switch provider {
-       case "redis":
-               return shadowPasswordKV(cfgItem, ",")
-       case "mysql":
-               //root:@tcp(localhost:3306)/macaron?charset=utf8
-               atIdx := strings.Index(cfgItem, "@")
-               if atIdx > 0 {
-                       colonIdx := strings.Index(cfgItem[:atIdx], ":")
-                       if colonIdx > 0 {
-                               return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:]
-                       }
-               }
-               return cfgItem
-       case "postgres":
-               // user=jiahuachen dbname=macaron port=5432 sslmode=disable
-               if !strings.HasPrefix(cfgItem, "postgres://") {
-                       return shadowPasswordKV(cfgItem, " ")
-               }
-               fallthrough
-       case "couchbase":
-               return shadowURL(provider, cfgItem)
-               // postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full
-               // Notice: use shadowURL
-       }
-       return cfgItem
-}
-
-// Config show admin config page
-func Config(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("admin.config")
-       ctx.Data["PageIsAdmin"] = true
-       ctx.Data["PageIsAdminConfig"] = true
-
-       ctx.Data["CustomConf"] = setting.CustomConf
-       ctx.Data["AppUrl"] = setting.AppURL
-       ctx.Data["Domain"] = setting.Domain
-       ctx.Data["OfflineMode"] = setting.OfflineMode
-       ctx.Data["DisableRouterLog"] = setting.DisableRouterLog
-       ctx.Data["RunUser"] = setting.RunUser
-       ctx.Data["RunMode"] = strings.Title(setting.RunMode)
-       if version, err := git.LocalVersion(); err == nil {
-               ctx.Data["GitVersion"] = version.Original()
-       }
-       ctx.Data["RepoRootPath"] = setting.RepoRootPath
-       ctx.Data["CustomRootPath"] = setting.CustomPath
-       ctx.Data["StaticRootPath"] = setting.StaticRootPath
-       ctx.Data["LogRootPath"] = setting.LogRootPath
-       ctx.Data["ScriptType"] = setting.ScriptType
-       ctx.Data["ReverseProxyAuthUser"] = setting.ReverseProxyAuthUser
-       ctx.Data["ReverseProxyAuthEmail"] = setting.ReverseProxyAuthEmail
-
-       ctx.Data["SSH"] = setting.SSH
-       ctx.Data["LFS"] = setting.LFS
-
-       ctx.Data["Service"] = setting.Service
-       ctx.Data["DbCfg"] = setting.Database
-       ctx.Data["Webhook"] = setting.Webhook
-
-       ctx.Data["MailerEnabled"] = false
-       if setting.MailService != nil {
-               ctx.Data["MailerEnabled"] = true
-               ctx.Data["Mailer"] = setting.MailService
-       }
-
-       ctx.Data["CacheAdapter"] = setting.CacheService.Adapter
-       ctx.Data["CacheInterval"] = setting.CacheService.Interval
-
-       ctx.Data["CacheConn"] = shadowPassword(setting.CacheService.Adapter, setting.CacheService.Conn)
-       ctx.Data["CacheItemTTL"] = setting.CacheService.TTL
-
-       sessionCfg := setting.SessionConfig
-       if sessionCfg.Provider == "VirtualSession" {
-               var realSession session.Options
-               json := jsoniter.ConfigCompatibleWithStandardLibrary
-               if err := json.Unmarshal([]byte(sessionCfg.ProviderConfig), &realSession); err != nil {
-                       log.Error("Unable to unmarshall session config for virtualed provider config: %s\nError: %v", sessionCfg.ProviderConfig, err)
-               }
-               sessionCfg.Provider = realSession.Provider
-               sessionCfg.ProviderConfig = realSession.ProviderConfig
-               sessionCfg.CookieName = realSession.CookieName
-               sessionCfg.CookiePath = realSession.CookiePath
-               sessionCfg.Gclifetime = realSession.Gclifetime
-               sessionCfg.Maxlifetime = realSession.Maxlifetime
-               sessionCfg.Secure = realSession.Secure
-               sessionCfg.Domain = realSession.Domain
-       }
-       sessionCfg.ProviderConfig = shadowPassword(sessionCfg.Provider, sessionCfg.ProviderConfig)
-       ctx.Data["SessionConfig"] = sessionCfg
-
-       ctx.Data["DisableGravatar"] = setting.DisableGravatar
-       ctx.Data["EnableFederatedAvatar"] = setting.EnableFederatedAvatar
-
-       ctx.Data["Git"] = setting.Git
-
-       type envVar struct {
-               Name, Value string
-       }
-
-       envVars := map[string]*envVar{}
-       if len(os.Getenv("GITEA_WORK_DIR")) > 0 {
-               envVars["GITEA_WORK_DIR"] = &envVar{"GITEA_WORK_DIR", os.Getenv("GITEA_WORK_DIR")}
-       }
-       if len(os.Getenv("GITEA_CUSTOM")) > 0 {
-               envVars["GITEA_CUSTOM"] = &envVar{"GITEA_CUSTOM", os.Getenv("GITEA_CUSTOM")}
-       }
-
-       ctx.Data["EnvVars"] = envVars
-       ctx.Data["Loggers"] = setting.GetLogDescriptions()
-       ctx.Data["EnableAccessLog"] = setting.EnableAccessLog
-       ctx.Data["AccessLogTemplate"] = setting.AccessLogTemplate
-       ctx.Data["DisableRouterLog"] = setting.DisableRouterLog
-       ctx.Data["EnableXORMLog"] = setting.EnableXORMLog
-       ctx.Data["LogSQL"] = setting.Database.LogSQL
-
-       ctx.HTML(http.StatusOK, tplConfig)
-}
-
-// Monitor show admin monitor page
-func Monitor(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("admin.monitor")
-       ctx.Data["PageIsAdmin"] = true
-       ctx.Data["PageIsAdminMonitor"] = true
-       ctx.Data["Processes"] = process.GetManager().Processes()
-       ctx.Data["Entries"] = cron.ListTasks()
-       ctx.Data["Queues"] = queue.GetManager().ManagedQueues()
-       ctx.HTML(http.StatusOK, tplMonitor)
-}
-
-// MonitorCancel cancels a process
-func MonitorCancel(ctx *context.Context) {
-       pid := ctx.ParamsInt64("pid")
-       process.GetManager().Cancel(pid)
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": setting.AppSubURL + "/admin/monitor",
-       })
-}
-
-// Queue shows details for a specific queue
-func Queue(ctx *context.Context) {
-       qid := ctx.ParamsInt64("qid")
-       mq := queue.GetManager().GetManagedQueue(qid)
-       if mq == nil {
-               ctx.Status(404)
-               return
-       }
-       ctx.Data["Title"] = ctx.Tr("admin.monitor.queue", mq.Name)
-       ctx.Data["PageIsAdmin"] = true
-       ctx.Data["PageIsAdminMonitor"] = true
-       ctx.Data["Queue"] = mq
-       ctx.HTML(http.StatusOK, tplQueue)
-}
-
-// WorkerCancel cancels a worker group
-func WorkerCancel(ctx *context.Context) {
-       qid := ctx.ParamsInt64("qid")
-       mq := queue.GetManager().GetManagedQueue(qid)
-       if mq == nil {
-               ctx.Status(404)
-               return
-       }
-       pid := ctx.ParamsInt64("pid")
-       mq.CancelWorkers(pid)
-       ctx.Flash.Info(ctx.Tr("admin.monitor.queue.pool.cancelling"))
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10),
-       })
-}
-
-// Flush flushes a queue
-func Flush(ctx *context.Context) {
-       qid := ctx.ParamsInt64("qid")
-       mq := queue.GetManager().GetManagedQueue(qid)
-       if mq == nil {
-               ctx.Status(404)
-               return
-       }
-       timeout, err := time.ParseDuration(ctx.Query("timeout"))
-       if err != nil {
-               timeout = -1
-       }
-       ctx.Flash.Info(ctx.Tr("admin.monitor.queue.pool.flush.added", mq.Name))
-       go func() {
-               err := mq.Flush(timeout)
-               if err != nil {
-                       log.Error("Flushing failure for %s: Error %v", mq.Name, err)
-               }
-       }()
-       ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
-}
-
-// AddWorkers adds workers to a worker group
-func AddWorkers(ctx *context.Context) {
-       qid := ctx.ParamsInt64("qid")
-       mq := queue.GetManager().GetManagedQueue(qid)
-       if mq == nil {
-               ctx.Status(404)
-               return
-       }
-       number := ctx.QueryInt("number")
-       if number < 1 {
-               ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.addworkers.mustnumbergreaterzero"))
-               ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
-               return
-       }
-       timeout, err := time.ParseDuration(ctx.Query("timeout"))
-       if err != nil {
-               ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.addworkers.musttimeoutduration"))
-               ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
-               return
-       }
-       if _, ok := mq.Managed.(queue.ManagedPool); !ok {
-               ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.none"))
-               ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
-               return
-       }
-       mq.AddWorkers(number, timeout)
-       ctx.Flash.Success(ctx.Tr("admin.monitor.queue.pool.added"))
-       ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
-}
-
-// SetQueueSettings sets the maximum number of workers and other settings for this queue
-func SetQueueSettings(ctx *context.Context) {
-       qid := ctx.ParamsInt64("qid")
-       mq := queue.GetManager().GetManagedQueue(qid)
-       if mq == nil {
-               ctx.Status(404)
-               return
-       }
-       if _, ok := mq.Managed.(queue.ManagedPool); !ok {
-               ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.none"))
-               ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
-               return
-       }
-
-       maxNumberStr := ctx.Query("max-number")
-       numberStr := ctx.Query("number")
-       timeoutStr := ctx.Query("timeout")
-
-       var err error
-       var maxNumber, number int
-       var timeout time.Duration
-       if len(maxNumberStr) > 0 {
-               maxNumber, err = strconv.Atoi(maxNumberStr)
-               if err != nil {
-                       ctx.Flash.Error(ctx.Tr("admin.monitor.queue.settings.maxnumberworkers.error"))
-                       ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
-                       return
-               }
-               if maxNumber < -1 {
-                       maxNumber = -1
-               }
-       } else {
-               maxNumber = mq.MaxNumberOfWorkers()
-       }
-
-       if len(numberStr) > 0 {
-               number, err = strconv.Atoi(numberStr)
-               if err != nil || number < 0 {
-                       ctx.Flash.Error(ctx.Tr("admin.monitor.queue.settings.numberworkers.error"))
-                       ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
-                       return
-               }
-       } else {
-               number = mq.BoostWorkers()
-       }
-
-       if len(timeoutStr) > 0 {
-               timeout, err = time.ParseDuration(timeoutStr)
-               if err != nil {
-                       ctx.Flash.Error(ctx.Tr("admin.monitor.queue.settings.timeout.error"))
-                       ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
-                       return
-               }
-       } else {
-               timeout = mq.BoostTimeout()
-       }
-
-       mq.SetPoolSettings(maxNumber, number, timeout)
-       ctx.Flash.Success(ctx.Tr("admin.monitor.queue.settings.changed"))
-       ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
-}
diff --git a/routers/admin/admin_test.go b/routers/admin/admin_test.go
deleted file mode 100644 (file)
index da404e5..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright 2019 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 admin
-
-import (
-       "testing"
-
-       "github.com/stretchr/testify/assert"
-)
-
-func TestShadowPassword(t *testing.T) {
-       var kases = []struct {
-               Provider string
-               CfgItem  string
-               Result   string
-       }{
-               {
-                       Provider: "redis",
-                       CfgItem:  "network=tcp,addr=:6379,password=gitea,db=0,pool_size=100,idle_timeout=180",
-                       Result:   "network=tcp,addr=:6379,password=******,db=0,pool_size=100,idle_timeout=180",
-               },
-               {
-                       Provider: "mysql",
-                       CfgItem:  "root:@tcp(localhost:3306)/gitea?charset=utf8",
-                       Result:   "root:******@tcp(localhost:3306)/gitea?charset=utf8",
-               },
-               {
-                       Provider: "mysql",
-                       CfgItem:  "/gitea?charset=utf8",
-                       Result:   "/gitea?charset=utf8",
-               },
-               {
-                       Provider: "mysql",
-                       CfgItem:  "user:mypassword@/dbname",
-                       Result:   "user:******@/dbname",
-               },
-               {
-                       Provider: "postgres",
-                       CfgItem:  "user=pqgotest dbname=pqgotest sslmode=verify-full",
-                       Result:   "user=pqgotest dbname=pqgotest sslmode=verify-full",
-               },
-               {
-                       Provider: "postgres",
-                       CfgItem:  "user=pqgotest password= dbname=pqgotest sslmode=verify-full",
-                       Result:   "user=pqgotest password=****** dbname=pqgotest sslmode=verify-full",
-               },
-               {
-                       Provider: "postgres",
-                       CfgItem:  "postgres://user:pass@hostname/dbname",
-                       Result:   "postgres://user:******@hostname/dbname",
-               },
-               {
-                       Provider: "couchbase",
-                       CfgItem:  "http://dev-couchbase.example.com:8091/",
-                       Result:   "http://dev-couchbase.example.com:8091/",
-               },
-               {
-                       Provider: "couchbase",
-                       CfgItem:  "http://user:the_password@dev-couchbase.example.com:8091/",
-                       Result:   "http://user:******@dev-couchbase.example.com:8091/",
-               },
-       }
-
-       for _, k := range kases {
-               assert.EqualValues(t, k.Result, shadowPassword(k.Provider, k.CfgItem))
-       }
-}
diff --git a/routers/admin/auths.go b/routers/admin/auths.go
deleted file mode 100644 (file)
index a2f9ab0..0000000
+++ /dev/null
@@ -1,410 +0,0 @@
-// Copyright 2014 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 admin
-
-import (
-       "errors"
-       "fmt"
-       "net/http"
-       "regexp"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/auth/ldap"
-       "code.gitea.io/gitea/modules/auth/oauth2"
-       "code.gitea.io/gitea/modules/auth/pam"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/util"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-
-       "xorm.io/xorm/convert"
-)
-
-const (
-       tplAuths    base.TplName = "admin/auth/list"
-       tplAuthNew  base.TplName = "admin/auth/new"
-       tplAuthEdit base.TplName = "admin/auth/edit"
-)
-
-var (
-       separatorAntiPattern = regexp.MustCompile(`[^\w-\.]`)
-       langCodePattern      = regexp.MustCompile(`^[a-z]{2}-[A-Z]{2}$`)
-)
-
-// Authentications show authentication config page
-func Authentications(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("admin.authentication")
-       ctx.Data["PageIsAdmin"] = true
-       ctx.Data["PageIsAdminAuthentications"] = true
-
-       var err error
-       ctx.Data["Sources"], err = models.LoginSources()
-       if err != nil {
-               ctx.ServerError("LoginSources", err)
-               return
-       }
-
-       ctx.Data["Total"] = models.CountLoginSources()
-       ctx.HTML(http.StatusOK, tplAuths)
-}
-
-type dropdownItem struct {
-       Name string
-       Type interface{}
-}
-
-var (
-       authSources = func() []dropdownItem {
-               items := []dropdownItem{
-                       {models.LoginNames[models.LoginLDAP], models.LoginLDAP},
-                       {models.LoginNames[models.LoginDLDAP], models.LoginDLDAP},
-                       {models.LoginNames[models.LoginSMTP], models.LoginSMTP},
-                       {models.LoginNames[models.LoginOAuth2], models.LoginOAuth2},
-                       {models.LoginNames[models.LoginSSPI], models.LoginSSPI},
-               }
-               if pam.Supported {
-                       items = append(items, dropdownItem{models.LoginNames[models.LoginPAM], models.LoginPAM})
-               }
-               return items
-       }()
-
-       securityProtocols = []dropdownItem{
-               {models.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted], ldap.SecurityProtocolUnencrypted},
-               {models.SecurityProtocolNames[ldap.SecurityProtocolLDAPS], ldap.SecurityProtocolLDAPS},
-               {models.SecurityProtocolNames[ldap.SecurityProtocolStartTLS], ldap.SecurityProtocolStartTLS},
-       }
-)
-
-// NewAuthSource render adding a new auth source page
-func NewAuthSource(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("admin.auths.new")
-       ctx.Data["PageIsAdmin"] = true
-       ctx.Data["PageIsAdminAuthentications"] = true
-
-       ctx.Data["type"] = models.LoginLDAP
-       ctx.Data["CurrentTypeName"] = models.LoginNames[models.LoginLDAP]
-       ctx.Data["CurrentSecurityProtocol"] = models.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted]
-       ctx.Data["smtp_auth"] = "PLAIN"
-       ctx.Data["is_active"] = true
-       ctx.Data["is_sync_enabled"] = true
-       ctx.Data["AuthSources"] = authSources
-       ctx.Data["SecurityProtocols"] = securityProtocols
-       ctx.Data["SMTPAuths"] = models.SMTPAuths
-       ctx.Data["OAuth2Providers"] = models.OAuth2Providers
-       ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
-
-       ctx.Data["SSPIAutoCreateUsers"] = true
-       ctx.Data["SSPIAutoActivateUsers"] = true
-       ctx.Data["SSPIStripDomainNames"] = true
-       ctx.Data["SSPISeparatorReplacement"] = "_"
-       ctx.Data["SSPIDefaultLanguage"] = ""
-
-       // only the first as default
-       for key := range models.OAuth2Providers {
-               ctx.Data["oauth2_provider"] = key
-               break
-       }
-
-       ctx.HTML(http.StatusOK, tplAuthNew)
-}
-
-func parseLDAPConfig(form forms.AuthenticationForm) *models.LDAPConfig {
-       var pageSize uint32
-       if form.UsePagedSearch {
-               pageSize = uint32(form.SearchPageSize)
-       }
-       return &models.LDAPConfig{
-               Source: &ldap.Source{
-                       Name:                  form.Name,
-                       Host:                  form.Host,
-                       Port:                  form.Port,
-                       SecurityProtocol:      ldap.SecurityProtocol(form.SecurityProtocol),
-                       SkipVerify:            form.SkipVerify,
-                       BindDN:                form.BindDN,
-                       UserDN:                form.UserDN,
-                       BindPassword:          form.BindPassword,
-                       UserBase:              form.UserBase,
-                       AttributeUsername:     form.AttributeUsername,
-                       AttributeName:         form.AttributeName,
-                       AttributeSurname:      form.AttributeSurname,
-                       AttributeMail:         form.AttributeMail,
-                       AttributesInBind:      form.AttributesInBind,
-                       AttributeSSHPublicKey: form.AttributeSSHPublicKey,
-                       SearchPageSize:        pageSize,
-                       Filter:                form.Filter,
-                       GroupsEnabled:         form.GroupsEnabled,
-                       GroupDN:               form.GroupDN,
-                       GroupFilter:           form.GroupFilter,
-                       GroupMemberUID:        form.GroupMemberUID,
-                       UserUID:               form.UserUID,
-                       AdminFilter:           form.AdminFilter,
-                       RestrictedFilter:      form.RestrictedFilter,
-                       AllowDeactivateAll:    form.AllowDeactivateAll,
-                       Enabled:               true,
-               },
-       }
-}
-
-func parseSMTPConfig(form forms.AuthenticationForm) *models.SMTPConfig {
-       return &models.SMTPConfig{
-               Auth:           form.SMTPAuth,
-               Host:           form.SMTPHost,
-               Port:           form.SMTPPort,
-               AllowedDomains: form.AllowedDomains,
-               TLS:            form.TLS,
-               SkipVerify:     form.SkipVerify,
-       }
-}
-
-func parseOAuth2Config(form forms.AuthenticationForm) *models.OAuth2Config {
-       var customURLMapping *oauth2.CustomURLMapping
-       if form.Oauth2UseCustomURL {
-               customURLMapping = &oauth2.CustomURLMapping{
-                       TokenURL:   form.Oauth2TokenURL,
-                       AuthURL:    form.Oauth2AuthURL,
-                       ProfileURL: form.Oauth2ProfileURL,
-                       EmailURL:   form.Oauth2EmailURL,
-               }
-       } else {
-               customURLMapping = nil
-       }
-       return &models.OAuth2Config{
-               Provider:                      form.Oauth2Provider,
-               ClientID:                      form.Oauth2Key,
-               ClientSecret:                  form.Oauth2Secret,
-               OpenIDConnectAutoDiscoveryURL: form.OpenIDConnectAutoDiscoveryURL,
-               CustomURLMapping:              customURLMapping,
-               IconURL:                       form.Oauth2IconURL,
-       }
-}
-
-func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*models.SSPIConfig, error) {
-       if util.IsEmptyString(form.SSPISeparatorReplacement) {
-               ctx.Data["Err_SSPISeparatorReplacement"] = true
-               return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.require_error"))
-       }
-       if separatorAntiPattern.MatchString(form.SSPISeparatorReplacement) {
-               ctx.Data["Err_SSPISeparatorReplacement"] = true
-               return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.alpha_dash_dot_error"))
-       }
-
-       if form.SSPIDefaultLanguage != "" && !langCodePattern.MatchString(form.SSPIDefaultLanguage) {
-               ctx.Data["Err_SSPIDefaultLanguage"] = true
-               return nil, errors.New(ctx.Tr("form.lang_select_error"))
-       }
-
-       return &models.SSPIConfig{
-               AutoCreateUsers:      form.SSPIAutoCreateUsers,
-               AutoActivateUsers:    form.SSPIAutoActivateUsers,
-               StripDomainNames:     form.SSPIStripDomainNames,
-               SeparatorReplacement: form.SSPISeparatorReplacement,
-               DefaultLanguage:      form.SSPIDefaultLanguage,
-       }, nil
-}
-
-// NewAuthSourcePost response for adding an auth source
-func NewAuthSourcePost(ctx *context.Context) {
-       form := *web.GetForm(ctx).(*forms.AuthenticationForm)
-       ctx.Data["Title"] = ctx.Tr("admin.auths.new")
-       ctx.Data["PageIsAdmin"] = true
-       ctx.Data["PageIsAdminAuthentications"] = true
-
-       ctx.Data["CurrentTypeName"] = models.LoginNames[models.LoginType(form.Type)]
-       ctx.Data["CurrentSecurityProtocol"] = models.SecurityProtocolNames[ldap.SecurityProtocol(form.SecurityProtocol)]
-       ctx.Data["AuthSources"] = authSources
-       ctx.Data["SecurityProtocols"] = securityProtocols
-       ctx.Data["SMTPAuths"] = models.SMTPAuths
-       ctx.Data["OAuth2Providers"] = models.OAuth2Providers
-       ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
-
-       ctx.Data["SSPIAutoCreateUsers"] = true
-       ctx.Data["SSPIAutoActivateUsers"] = true
-       ctx.Data["SSPIStripDomainNames"] = true
-       ctx.Data["SSPISeparatorReplacement"] = "_"
-       ctx.Data["SSPIDefaultLanguage"] = ""
-
-       hasTLS := false
-       var config convert.Conversion
-       switch models.LoginType(form.Type) {
-       case models.LoginLDAP, models.LoginDLDAP:
-               config = parseLDAPConfig(form)
-               hasTLS = ldap.SecurityProtocol(form.SecurityProtocol) > ldap.SecurityProtocolUnencrypted
-       case models.LoginSMTP:
-               config = parseSMTPConfig(form)
-               hasTLS = true
-       case models.LoginPAM:
-               config = &models.PAMConfig{
-                       ServiceName: form.PAMServiceName,
-                       EmailDomain: form.PAMEmailDomain,
-               }
-       case models.LoginOAuth2:
-               config = parseOAuth2Config(form)
-       case models.LoginSSPI:
-               var err error
-               config, err = parseSSPIConfig(ctx, form)
-               if err != nil {
-                       ctx.RenderWithErr(err.Error(), tplAuthNew, form)
-                       return
-               }
-               existing, err := models.LoginSourcesByType(models.LoginSSPI)
-               if err != nil || len(existing) > 0 {
-                       ctx.Data["Err_Type"] = true
-                       ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_of_type_exist"), tplAuthNew, form)
-                       return
-               }
-       default:
-               ctx.Error(http.StatusBadRequest)
-               return
-       }
-       ctx.Data["HasTLS"] = hasTLS
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplAuthNew)
-               return
-       }
-
-       if err := models.CreateLoginSource(&models.LoginSource{
-               Type:          models.LoginType(form.Type),
-               Name:          form.Name,
-               IsActived:     form.IsActive,
-               IsSyncEnabled: form.IsSyncEnabled,
-               Cfg:           config,
-       }); err != nil {
-               if models.IsErrLoginSourceAlreadyExist(err) {
-                       ctx.Data["Err_Name"] = true
-                       ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_exist", err.(models.ErrLoginSourceAlreadyExist).Name), tplAuthNew, form)
-               } else {
-                       ctx.ServerError("CreateSource", err)
-               }
-               return
-       }
-
-       log.Trace("Authentication created by admin(%s): %s", ctx.User.Name, form.Name)
-
-       ctx.Flash.Success(ctx.Tr("admin.auths.new_success", form.Name))
-       ctx.Redirect(setting.AppSubURL + "/admin/auths")
-}
-
-// EditAuthSource render editing auth source page
-func EditAuthSource(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("admin.auths.edit")
-       ctx.Data["PageIsAdmin"] = true
-       ctx.Data["PageIsAdminAuthentications"] = true
-
-       ctx.Data["SecurityProtocols"] = securityProtocols
-       ctx.Data["SMTPAuths"] = models.SMTPAuths
-       ctx.Data["OAuth2Providers"] = models.OAuth2Providers
-       ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
-
-       source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
-       if err != nil {
-               ctx.ServerError("GetLoginSourceByID", err)
-               return
-       }
-       ctx.Data["Source"] = source
-       ctx.Data["HasTLS"] = source.HasTLS()
-
-       if source.IsOAuth2() {
-               ctx.Data["CurrentOAuth2Provider"] = models.OAuth2Providers[source.OAuth2().Provider]
-       }
-       ctx.HTML(http.StatusOK, tplAuthEdit)
-}
-
-// EditAuthSourcePost response for editing auth source
-func EditAuthSourcePost(ctx *context.Context) {
-       form := *web.GetForm(ctx).(*forms.AuthenticationForm)
-       ctx.Data["Title"] = ctx.Tr("admin.auths.edit")
-       ctx.Data["PageIsAdmin"] = true
-       ctx.Data["PageIsAdminAuthentications"] = true
-
-       ctx.Data["SMTPAuths"] = models.SMTPAuths
-       ctx.Data["OAuth2Providers"] = models.OAuth2Providers
-       ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
-
-       source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
-       if err != nil {
-               ctx.ServerError("GetLoginSourceByID", err)
-               return
-       }
-       ctx.Data["Source"] = source
-       ctx.Data["HasTLS"] = source.HasTLS()
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplAuthEdit)
-               return
-       }
-
-       var config convert.Conversion
-       switch models.LoginType(form.Type) {
-       case models.LoginLDAP, models.LoginDLDAP:
-               config = parseLDAPConfig(form)
-       case models.LoginSMTP:
-               config = parseSMTPConfig(form)
-       case models.LoginPAM:
-               config = &models.PAMConfig{
-                       ServiceName: form.PAMServiceName,
-                       EmailDomain: form.PAMEmailDomain,
-               }
-       case models.LoginOAuth2:
-               config = parseOAuth2Config(form)
-       case models.LoginSSPI:
-               config, err = parseSSPIConfig(ctx, form)
-               if err != nil {
-                       ctx.RenderWithErr(err.Error(), tplAuthEdit, form)
-                       return
-               }
-       default:
-               ctx.Error(http.StatusBadRequest)
-               return
-       }
-
-       source.Name = form.Name
-       source.IsActived = form.IsActive
-       source.IsSyncEnabled = form.IsSyncEnabled
-       source.Cfg = config
-       if err := models.UpdateSource(source); err != nil {
-               if models.IsErrOpenIDConnectInitialize(err) {
-                       ctx.Flash.Error(err.Error(), true)
-                       ctx.HTML(http.StatusOK, tplAuthEdit)
-               } else {
-                       ctx.ServerError("UpdateSource", err)
-               }
-               return
-       }
-       log.Trace("Authentication changed by admin(%s): %d", ctx.User.Name, source.ID)
-
-       ctx.Flash.Success(ctx.Tr("admin.auths.update_success"))
-       ctx.Redirect(setting.AppSubURL + "/admin/auths/" + fmt.Sprint(form.ID))
-}
-
-// DeleteAuthSource response for deleting an auth source
-func DeleteAuthSource(ctx *context.Context) {
-       source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
-       if err != nil {
-               ctx.ServerError("GetLoginSourceByID", err)
-               return
-       }
-
-       if err = models.DeleteSource(source); err != nil {
-               if models.IsErrLoginSourceInUse(err) {
-                       ctx.Flash.Error(ctx.Tr("admin.auths.still_in_used"))
-               } else {
-                       ctx.Flash.Error(fmt.Sprintf("DeleteSource: %v", err))
-               }
-               ctx.JSON(http.StatusOK, map[string]interface{}{
-                       "redirect": setting.AppSubURL + "/admin/auths/" + ctx.Params(":authid"),
-               })
-               return
-       }
-       log.Trace("Authentication deleted by admin(%s): %d", ctx.User.Name, source.ID)
-
-       ctx.Flash.Success(ctx.Tr("admin.auths.deletion_success"))
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": setting.AppSubURL + "/admin/auths",
-       })
-}
diff --git a/routers/admin/emails.go b/routers/admin/emails.go
deleted file mode 100644 (file)
index f7e8c97..0000000
+++ /dev/null
@@ -1,156 +0,0 @@
-// Copyright 2020 The Gitea Authors.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-package admin
-
-import (
-       "bytes"
-       "net/http"
-       "net/url"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/util"
-)
-
-const (
-       tplEmails base.TplName = "admin/emails/list"
-)
-
-// Emails show all emails
-func Emails(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("admin.emails")
-       ctx.Data["PageIsAdmin"] = true
-       ctx.Data["PageIsAdminEmails"] = true
-
-       opts := &models.SearchEmailOptions{
-               ListOptions: models.ListOptions{
-                       PageSize: setting.UI.Admin.UserPagingNum,
-                       Page:     ctx.QueryInt("page"),
-               },
-       }
-
-       if opts.Page <= 1 {
-               opts.Page = 1
-       }
-
-       type ActiveEmail struct {
-               models.SearchEmailResult
-               CanChange bool
-       }
-
-       var (
-               baseEmails []*models.SearchEmailResult
-               emails     []ActiveEmail
-               count      int64
-               err        error
-               orderBy    models.SearchEmailOrderBy
-       )
-
-       ctx.Data["SortType"] = ctx.Query("sort")
-       switch ctx.Query("sort") {
-       case "email":
-               orderBy = models.SearchEmailOrderByEmail
-       case "reverseemail":
-               orderBy = models.SearchEmailOrderByEmailReverse
-       case "username":
-               orderBy = models.SearchEmailOrderByName
-       case "reverseusername":
-               orderBy = models.SearchEmailOrderByNameReverse
-       default:
-               ctx.Data["SortType"] = "email"
-               orderBy = models.SearchEmailOrderByEmail
-       }
-
-       opts.Keyword = ctx.QueryTrim("q")
-       opts.SortType = orderBy
-       if len(ctx.Query("is_activated")) != 0 {
-               opts.IsActivated = util.OptionalBoolOf(ctx.QueryBool("activated"))
-       }
-       if len(ctx.Query("is_primary")) != 0 {
-               opts.IsPrimary = util.OptionalBoolOf(ctx.QueryBool("primary"))
-       }
-
-       if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
-               baseEmails, count, err = models.SearchEmails(opts)
-               if err != nil {
-                       ctx.ServerError("SearchEmails", err)
-                       return
-               }
-               emails = make([]ActiveEmail, len(baseEmails))
-               for i := range baseEmails {
-                       emails[i].SearchEmailResult = *baseEmails[i]
-                       // Don't let the admin deactivate its own primary email address
-                       // We already know the user is admin
-                       emails[i].CanChange = ctx.User.ID != emails[i].UID || !emails[i].IsPrimary
-               }
-       }
-       ctx.Data["Keyword"] = opts.Keyword
-       ctx.Data["Total"] = count
-       ctx.Data["Emails"] = emails
-
-       pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
-       pager.SetDefaultParams(ctx)
-       ctx.Data["Page"] = pager
-
-       ctx.HTML(http.StatusOK, tplEmails)
-}
-
-var (
-       nullByte = []byte{0x00}
-)
-
-func isKeywordValid(keyword string) bool {
-       return !bytes.Contains([]byte(keyword), nullByte)
-}
-
-// ActivateEmail serves a POST request for activating/deactivating a user's email
-func ActivateEmail(ctx *context.Context) {
-
-       truefalse := map[string]bool{"1": true, "0": false}
-
-       uid := ctx.QueryInt64("uid")
-       email := ctx.Query("email")
-       primary, okp := truefalse[ctx.Query("primary")]
-       activate, oka := truefalse[ctx.Query("activate")]
-
-       if uid == 0 || len(email) == 0 || !okp || !oka {
-               ctx.Error(http.StatusBadRequest)
-               return
-       }
-
-       log.Info("Changing activation for User ID: %d, email: %s, primary: %v to %v", uid, email, primary, activate)
-
-       if err := models.ActivateUserEmail(uid, email, primary, activate); err != nil {
-               log.Error("ActivateUserEmail(%v,%v,%v,%v): %v", uid, email, primary, activate, err)
-               if models.IsErrEmailAlreadyUsed(err) {
-                       ctx.Flash.Error(ctx.Tr("admin.emails.duplicate_active"))
-               } else {
-                       ctx.Flash.Error(ctx.Tr("admin.emails.not_updated", err))
-               }
-       } else {
-               log.Info("Activation for User ID: %d, email: %s, primary: %v changed to %v", uid, email, primary, activate)
-               ctx.Flash.Info(ctx.Tr("admin.emails.updated"))
-       }
-
-       redirect, _ := url.Parse(setting.AppSubURL + "/admin/emails")
-       q := url.Values{}
-       if val := ctx.QueryTrim("q"); len(val) > 0 {
-               q.Set("q", val)
-       }
-       if val := ctx.QueryTrim("sort"); len(val) > 0 {
-               q.Set("sort", val)
-       }
-       if val := ctx.QueryTrim("is_primary"); len(val) > 0 {
-               q.Set("is_primary", val)
-       }
-       if val := ctx.QueryTrim("is_activated"); len(val) > 0 {
-               q.Set("is_activated", val)
-       }
-       redirect.RawQuery = q.Encode()
-       ctx.Redirect(redirect.String())
-}
diff --git a/routers/admin/hooks.go b/routers/admin/hooks.go
deleted file mode 100644 (file)
index ff32260..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-// Copyright 2018 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 admin
-
-import (
-       "net/http"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/setting"
-)
-
-const (
-       // tplAdminHooks template path to render hook settings
-       tplAdminHooks base.TplName = "admin/hooks"
-)
-
-// DefaultOrSystemWebhooks renders both admin default and system webhook list pages
-func DefaultOrSystemWebhooks(ctx *context.Context) {
-       var err error
-
-       ctx.Data["PageIsAdminSystemHooks"] = true
-       ctx.Data["PageIsAdminDefaultHooks"] = true
-
-       def := make(map[string]interface{}, len(ctx.Data))
-       sys := make(map[string]interface{}, len(ctx.Data))
-       for k, v := range ctx.Data {
-               def[k] = v
-               sys[k] = v
-       }
-
-       sys["Title"] = ctx.Tr("admin.systemhooks")
-       sys["Description"] = ctx.Tr("admin.systemhooks.desc")
-       sys["Webhooks"], err = models.GetSystemWebhooks()
-       sys["BaseLink"] = setting.AppSubURL + "/admin/hooks"
-       sys["BaseLinkNew"] = setting.AppSubURL + "/admin/system-hooks"
-       if err != nil {
-               ctx.ServerError("GetWebhooksAdmin", err)
-               return
-       }
-
-       def["Title"] = ctx.Tr("admin.defaulthooks")
-       def["Description"] = ctx.Tr("admin.defaulthooks.desc")
-       def["Webhooks"], err = models.GetDefaultWebhooks()
-       def["BaseLink"] = setting.AppSubURL + "/admin/hooks"
-       def["BaseLinkNew"] = setting.AppSubURL + "/admin/default-hooks"
-       if err != nil {
-               ctx.ServerError("GetWebhooksAdmin", err)
-               return
-       }
-
-       ctx.Data["DefaultWebhooks"] = def
-       ctx.Data["SystemWebhooks"] = sys
-
-       ctx.HTML(http.StatusOK, tplAdminHooks)
-}
-
-// DeleteDefaultOrSystemWebhook handler to delete an admin-defined system or default webhook
-func DeleteDefaultOrSystemWebhook(ctx *context.Context) {
-       if err := models.DeleteDefaultSystemWebhook(ctx.QueryInt64("id")); err != nil {
-               ctx.Flash.Error("DeleteDefaultWebhook: " + err.Error())
-       } else {
-               ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": setting.AppSubURL + "/admin/hooks",
-       })
-}
diff --git a/routers/admin/main_test.go b/routers/admin/main_test.go
deleted file mode 100644 (file)
index 9a7191d..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright 2018 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 admin
-
-import (
-       "path/filepath"
-       "testing"
-
-       "code.gitea.io/gitea/models"
-)
-
-func TestMain(m *testing.M) {
-       models.MainTest(m, filepath.Join("..", ".."))
-}
diff --git a/routers/admin/notice.go b/routers/admin/notice.go
deleted file mode 100644 (file)
index e2ebd0d..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2019 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 admin
-
-import (
-       "net/http"
-       "strconv"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-)
-
-const (
-       tplNotices base.TplName = "admin/notice"
-)
-
-// Notices show notices for admin
-func Notices(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("admin.notices")
-       ctx.Data["PageIsAdmin"] = true
-       ctx.Data["PageIsAdminNotices"] = true
-
-       total := models.CountNotices()
-       page := ctx.QueryInt("page")
-       if page <= 1 {
-               page = 1
-       }
-
-       notices, err := models.Notices(page, setting.UI.Admin.NoticePagingNum)
-       if err != nil {
-               ctx.ServerError("Notices", err)
-               return
-       }
-       ctx.Data["Notices"] = notices
-
-       ctx.Data["Total"] = total
-
-       ctx.Data["Page"] = context.NewPagination(int(total), setting.UI.Admin.NoticePagingNum, page, 5)
-
-       ctx.HTML(http.StatusOK, tplNotices)
-}
-
-// DeleteNotices delete the specific notices
-func DeleteNotices(ctx *context.Context) {
-       strs := ctx.QueryStrings("ids[]")
-       ids := make([]int64, 0, len(strs))
-       for i := range strs {
-               id, _ := strconv.ParseInt(strs[i], 10, 64)
-               if id > 0 {
-                       ids = append(ids, id)
-               }
-       }
-
-       if err := models.DeleteNoticesByIDs(ids); err != nil {
-               ctx.Flash.Error("DeleteNoticesByIDs: " + err.Error())
-               ctx.Status(500)
-       } else {
-               ctx.Flash.Success(ctx.Tr("admin.notices.delete_success"))
-               ctx.Status(200)
-       }
-}
-
-// EmptyNotices delete all the notices
-func EmptyNotices(ctx *context.Context) {
-       if err := models.DeleteNotices(0, 0); err != nil {
-               ctx.ServerError("DeleteNotices", err)
-               return
-       }
-
-       log.Trace("System notices deleted by admin (%s): [start: %d]", ctx.User.Name, 0)
-       ctx.Flash.Success(ctx.Tr("admin.notices.delete_success"))
-       ctx.Redirect(setting.AppSubURL + "/admin/notices")
-}
diff --git a/routers/admin/orgs.go b/routers/admin/orgs.go
deleted file mode 100644 (file)
index 627f56e..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2020 The Gitea Authors.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-package admin
-
-import (
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/structs"
-       "code.gitea.io/gitea/routers"
-)
-
-const (
-       tplOrgs base.TplName = "admin/org/list"
-)
-
-// Organizations show all the organizations
-func Organizations(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("admin.organizations")
-       ctx.Data["PageIsAdmin"] = true
-       ctx.Data["PageIsAdminOrganizations"] = true
-
-       routers.RenderUserSearch(ctx, &models.SearchUserOptions{
-               Type: models.UserTypeOrganization,
-               ListOptions: models.ListOptions{
-                       PageSize: setting.UI.Admin.OrgPagingNum,
-               },
-               Visible: []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate},
-       }, tplOrgs)
-}
diff --git a/routers/admin/repos.go b/routers/admin/repos.go
deleted file mode 100644 (file)
index d23f7c3..0000000
+++ /dev/null
@@ -1,166 +0,0 @@
-// Copyright 2014 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 admin
-
-import (
-       "net/http"
-       "net/url"
-       "strconv"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/repository"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/util"
-       "code.gitea.io/gitea/routers"
-       repo_service "code.gitea.io/gitea/services/repository"
-)
-
-const (
-       tplRepos          base.TplName = "admin/repo/list"
-       tplUnadoptedRepos base.TplName = "admin/repo/unadopted"
-)
-
-// Repos show all the repositories
-func Repos(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("admin.repositories")
-       ctx.Data["PageIsAdmin"] = true
-       ctx.Data["PageIsAdminRepositories"] = true
-
-       routers.RenderRepoSearch(ctx, &routers.RepoSearchOptions{
-               Private:  true,
-               PageSize: setting.UI.Admin.RepoPagingNum,
-               TplName:  tplRepos,
-       })
-}
-
-// DeleteRepo delete one repository
-func DeleteRepo(ctx *context.Context) {
-       repo, err := models.GetRepositoryByID(ctx.QueryInt64("id"))
-       if err != nil {
-               ctx.ServerError("GetRepositoryByID", err)
-               return
-       }
-
-       if ctx.Repo != nil && ctx.Repo.GitRepo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == repo.ID {
-               ctx.Repo.GitRepo.Close()
-       }
-
-       if err := repo_service.DeleteRepository(ctx.User, repo); err != nil {
-               ctx.ServerError("DeleteRepository", err)
-               return
-       }
-       log.Trace("Repository deleted: %s", repo.FullName())
-
-       ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success"))
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": setting.AppSubURL + "/admin/repos?page=" + ctx.Query("page") + "&sort=" + ctx.Query("sort"),
-       })
-}
-
-// UnadoptedRepos lists the unadopted repositories
-func UnadoptedRepos(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("admin.repositories")
-       ctx.Data["PageIsAdmin"] = true
-       ctx.Data["PageIsAdminRepositories"] = true
-
-       opts := models.ListOptions{
-               PageSize: setting.UI.Admin.UserPagingNum,
-               Page:     ctx.QueryInt("page"),
-       }
-
-       if opts.Page <= 0 {
-               opts.Page = 1
-       }
-
-       ctx.Data["CurrentPage"] = opts.Page
-
-       doSearch := ctx.QueryBool("search")
-
-       ctx.Data["search"] = doSearch
-       q := ctx.Query("q")
-
-       if !doSearch {
-               pager := context.NewPagination(0, opts.PageSize, opts.Page, 5)
-               pager.SetDefaultParams(ctx)
-               pager.AddParam(ctx, "search", "search")
-               ctx.Data["Page"] = pager
-               ctx.HTML(http.StatusOK, tplUnadoptedRepos)
-               return
-       }
-
-       ctx.Data["Keyword"] = q
-       repoNames, count, err := repository.ListUnadoptedRepositories(q, &opts)
-       if err != nil {
-               ctx.ServerError("ListUnadoptedRepositories", err)
-       }
-       ctx.Data["Dirs"] = repoNames
-       pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
-       pager.SetDefaultParams(ctx)
-       pager.AddParam(ctx, "search", "search")
-       ctx.Data["Page"] = pager
-       ctx.HTML(http.StatusOK, tplUnadoptedRepos)
-}
-
-// AdoptOrDeleteRepository adopts or deletes a repository
-func AdoptOrDeleteRepository(ctx *context.Context) {
-       dir := ctx.Query("id")
-       action := ctx.Query("action")
-       page := ctx.QueryInt("page")
-       q := ctx.Query("q")
-
-       dirSplit := strings.SplitN(dir, "/", 2)
-       if len(dirSplit) != 2 {
-               ctx.Redirect(setting.AppSubURL + "/admin/repos")
-               return
-       }
-
-       ctxUser, err := models.GetUserByName(dirSplit[0])
-       if err != nil {
-               if models.IsErrUserNotExist(err) {
-                       log.Debug("User does not exist: %s", dirSplit[0])
-                       ctx.Redirect(setting.AppSubURL + "/admin/repos")
-                       return
-               }
-               ctx.ServerError("GetUserByName", err)
-               return
-       }
-
-       repoName := dirSplit[1]
-
-       // check not a repo
-       has, err := models.IsRepositoryExist(ctxUser, repoName)
-       if err != nil {
-               ctx.ServerError("IsRepositoryExist", err)
-               return
-       }
-       isDir, err := util.IsDir(models.RepoPath(ctxUser.Name, repoName))
-       if err != nil {
-               ctx.ServerError("IsDir", err)
-               return
-       }
-       if has || !isDir {
-               // Fallthrough to failure mode
-       } else if action == "adopt" {
-               if _, err := repository.AdoptRepository(ctx.User, ctxUser, models.CreateRepoOptions{
-                       Name:      dirSplit[1],
-                       IsPrivate: true,
-               }); err != nil {
-                       ctx.ServerError("repository.AdoptRepository", err)
-                       return
-               }
-               ctx.Flash.Success(ctx.Tr("repo.adopt_preexisting_success", dir))
-       } else if action == "delete" {
-               if err := repository.DeleteUnadoptedRepository(ctx.User, ctxUser, dirSplit[1]); err != nil {
-                       ctx.ServerError("repository.AdoptRepository", err)
-                       return
-               }
-               ctx.Flash.Success(ctx.Tr("repo.delete_preexisting_success", dir))
-       }
-       ctx.Redirect(setting.AppSubURL + "/admin/repos/unadopted?search=true&q=" + url.QueryEscape(q) + "&page=" + strconv.Itoa(page))
-}
diff --git a/routers/admin/users.go b/routers/admin/users.go
deleted file mode 100644 (file)
index a71a11d..0000000
+++ /dev/null
@@ -1,371 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2020 The Gitea Authors.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-package admin
-
-import (
-       "fmt"
-       "net/http"
-       "strconv"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/password"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/routers"
-       router_user_setting "code.gitea.io/gitea/routers/user/setting"
-       "code.gitea.io/gitea/services/forms"
-       "code.gitea.io/gitea/services/mailer"
-)
-
-const (
-       tplUsers    base.TplName = "admin/user/list"
-       tplUserNew  base.TplName = "admin/user/new"
-       tplUserEdit base.TplName = "admin/user/edit"
-)
-
-// Users show all the users
-func Users(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("admin.users")
-       ctx.Data["PageIsAdmin"] = true
-       ctx.Data["PageIsAdminUsers"] = true
-
-       routers.RenderUserSearch(ctx, &models.SearchUserOptions{
-               Type: models.UserTypeIndividual,
-               ListOptions: models.ListOptions{
-                       PageSize: setting.UI.Admin.UserPagingNum,
-               },
-               SearchByEmail: true,
-       }, tplUsers)
-}
-
-// NewUser render adding a new user page
-func NewUser(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("admin.users.new_account")
-       ctx.Data["PageIsAdmin"] = true
-       ctx.Data["PageIsAdminUsers"] = true
-
-       ctx.Data["login_type"] = "0-0"
-
-       sources, err := models.LoginSources()
-       if err != nil {
-               ctx.ServerError("LoginSources", err)
-               return
-       }
-       ctx.Data["Sources"] = sources
-
-       ctx.Data["CanSendEmail"] = setting.MailService != nil
-       ctx.HTML(http.StatusOK, tplUserNew)
-}
-
-// NewUserPost response for adding a new user
-func NewUserPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.AdminCreateUserForm)
-       ctx.Data["Title"] = ctx.Tr("admin.users.new_account")
-       ctx.Data["PageIsAdmin"] = true
-       ctx.Data["PageIsAdminUsers"] = true
-
-       sources, err := models.LoginSources()
-       if err != nil {
-               ctx.ServerError("LoginSources", err)
-               return
-       }
-       ctx.Data["Sources"] = sources
-
-       ctx.Data["CanSendEmail"] = setting.MailService != nil
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplUserNew)
-               return
-       }
-
-       u := &models.User{
-               Name:      form.UserName,
-               Email:     form.Email,
-               Passwd:    form.Password,
-               IsActive:  true,
-               LoginType: models.LoginPlain,
-       }
-
-       if len(form.LoginType) > 0 {
-               fields := strings.Split(form.LoginType, "-")
-               if len(fields) == 2 {
-                       lType, _ := strconv.ParseInt(fields[0], 10, 0)
-                       u.LoginType = models.LoginType(lType)
-                       u.LoginSource, _ = strconv.ParseInt(fields[1], 10, 64)
-                       u.LoginName = form.LoginName
-               }
-       }
-       if u.LoginType == models.LoginNoType || u.LoginType == models.LoginPlain {
-               if len(form.Password) < setting.MinPasswordLength {
-                       ctx.Data["Err_Password"] = true
-                       ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserNew, &form)
-                       return
-               }
-               if !password.IsComplexEnough(form.Password) {
-                       ctx.Data["Err_Password"] = true
-                       ctx.RenderWithErr(password.BuildComplexityError(ctx), tplUserNew, &form)
-                       return
-               }
-               pwned, err := password.IsPwned(ctx, form.Password)
-               if pwned {
-                       ctx.Data["Err_Password"] = true
-                       errMsg := ctx.Tr("auth.password_pwned")
-                       if err != nil {
-                               log.Error(err.Error())
-                               errMsg = ctx.Tr("auth.password_pwned_err")
-                       }
-                       ctx.RenderWithErr(errMsg, tplUserNew, &form)
-                       return
-               }
-               u.MustChangePassword = form.MustChangePassword
-       }
-       if err := models.CreateUser(u); err != nil {
-               switch {
-               case models.IsErrUserAlreadyExist(err):
-                       ctx.Data["Err_UserName"] = true
-                       ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplUserNew, &form)
-               case models.IsErrEmailAlreadyUsed(err):
-                       ctx.Data["Err_Email"] = true
-                       ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserNew, &form)
-               case models.IsErrEmailInvalid(err):
-                       ctx.Data["Err_Email"] = true
-                       ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserNew, &form)
-               case models.IsErrNameReserved(err):
-                       ctx.Data["Err_UserName"] = true
-                       ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(models.ErrNameReserved).Name), tplUserNew, &form)
-               case models.IsErrNamePatternNotAllowed(err):
-                       ctx.Data["Err_UserName"] = true
-                       ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplUserNew, &form)
-               case models.IsErrNameCharsNotAllowed(err):
-                       ctx.Data["Err_UserName"] = true
-                       ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(models.ErrNameCharsNotAllowed).Name), tplUserNew, &form)
-               default:
-                       ctx.ServerError("CreateUser", err)
-               }
-               return
-       }
-       log.Trace("Account created by admin (%s): %s", ctx.User.Name, u.Name)
-
-       // Send email notification.
-       if form.SendNotify {
-               mailer.SendRegisterNotifyMail(u)
-       }
-
-       ctx.Flash.Success(ctx.Tr("admin.users.new_success", u.Name))
-       ctx.Redirect(setting.AppSubURL + "/admin/users/" + fmt.Sprint(u.ID))
-}
-
-func prepareUserInfo(ctx *context.Context) *models.User {
-       u, err := models.GetUserByID(ctx.ParamsInt64(":userid"))
-       if err != nil {
-               ctx.ServerError("GetUserByID", err)
-               return nil
-       }
-       ctx.Data["User"] = u
-
-       if u.LoginSource > 0 {
-               ctx.Data["LoginSource"], err = models.GetLoginSourceByID(u.LoginSource)
-               if err != nil {
-                       ctx.ServerError("GetLoginSourceByID", err)
-                       return nil
-               }
-       } else {
-               ctx.Data["LoginSource"] = &models.LoginSource{}
-       }
-
-       sources, err := models.LoginSources()
-       if err != nil {
-               ctx.ServerError("LoginSources", err)
-               return nil
-       }
-       ctx.Data["Sources"] = sources
-
-       ctx.Data["TwoFactorEnabled"] = true
-       _, err = models.GetTwoFactorByUID(u.ID)
-       if err != nil {
-               if !models.IsErrTwoFactorNotEnrolled(err) {
-                       ctx.ServerError("IsErrTwoFactorNotEnrolled", err)
-                       return nil
-               }
-               ctx.Data["TwoFactorEnabled"] = false
-       }
-
-       return u
-}
-
-// EditUser show editting user page
-func EditUser(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("admin.users.edit_account")
-       ctx.Data["PageIsAdmin"] = true
-       ctx.Data["PageIsAdminUsers"] = true
-       ctx.Data["DisableRegularOrgCreation"] = setting.Admin.DisableRegularOrgCreation
-       ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
-
-       prepareUserInfo(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       ctx.HTML(http.StatusOK, tplUserEdit)
-}
-
-// EditUserPost response for editting user
-func EditUserPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.AdminEditUserForm)
-       ctx.Data["Title"] = ctx.Tr("admin.users.edit_account")
-       ctx.Data["PageIsAdmin"] = true
-       ctx.Data["PageIsAdminUsers"] = true
-       ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
-
-       u := prepareUserInfo(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplUserEdit)
-               return
-       }
-
-       fields := strings.Split(form.LoginType, "-")
-       if len(fields) == 2 {
-               loginType, _ := strconv.ParseInt(fields[0], 10, 0)
-               loginSource, _ := strconv.ParseInt(fields[1], 10, 64)
-
-               if u.LoginSource != loginSource {
-                       u.LoginSource = loginSource
-                       u.LoginType = models.LoginType(loginType)
-               }
-       }
-
-       if len(form.Password) > 0 && (u.IsLocal() || u.IsOAuth2()) {
-               var err error
-               if len(form.Password) < setting.MinPasswordLength {
-                       ctx.Data["Err_Password"] = true
-                       ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserEdit, &form)
-                       return
-               }
-               if !password.IsComplexEnough(form.Password) {
-                       ctx.RenderWithErr(password.BuildComplexityError(ctx), tplUserEdit, &form)
-                       return
-               }
-               pwned, err := password.IsPwned(ctx, form.Password)
-               if pwned {
-                       ctx.Data["Err_Password"] = true
-                       errMsg := ctx.Tr("auth.password_pwned")
-                       if err != nil {
-                               log.Error(err.Error())
-                               errMsg = ctx.Tr("auth.password_pwned_err")
-                       }
-                       ctx.RenderWithErr(errMsg, tplUserNew, &form)
-                       return
-               }
-               if u.Salt, err = models.GetUserSalt(); err != nil {
-                       ctx.ServerError("UpdateUser", err)
-                       return
-               }
-               if err = u.SetPassword(form.Password); err != nil {
-                       ctx.ServerError("SetPassword", err)
-                       return
-               }
-       }
-
-       if len(form.UserName) != 0 && u.Name != form.UserName {
-               if err := router_user_setting.HandleUsernameChange(ctx, u, form.UserName); err != nil {
-                       ctx.Redirect(setting.AppSubURL + "/admin/users")
-                       return
-               }
-               u.Name = form.UserName
-               u.LowerName = strings.ToLower(form.UserName)
-       }
-
-       if form.Reset2FA {
-               tf, err := models.GetTwoFactorByUID(u.ID)
-               if err != nil && !models.IsErrTwoFactorNotEnrolled(err) {
-                       ctx.ServerError("GetTwoFactorByUID", err)
-                       return
-               }
-
-               if err = models.DeleteTwoFactorByID(tf.ID, u.ID); err != nil {
-                       ctx.ServerError("DeleteTwoFactorByID", err)
-                       return
-               }
-       }
-
-       u.LoginName = form.LoginName
-       u.FullName = form.FullName
-       u.Email = form.Email
-       u.Website = form.Website
-       u.Location = form.Location
-       u.MaxRepoCreation = form.MaxRepoCreation
-       u.IsActive = form.Active
-       u.IsAdmin = form.Admin
-       u.IsRestricted = form.Restricted
-       u.AllowGitHook = form.AllowGitHook
-       u.AllowImportLocal = form.AllowImportLocal
-       u.AllowCreateOrganization = form.AllowCreateOrganization
-
-       // skip self Prohibit Login
-       if ctx.User.ID == u.ID {
-               u.ProhibitLogin = false
-       } else {
-               u.ProhibitLogin = form.ProhibitLogin
-       }
-
-       if err := models.UpdateUser(u); err != nil {
-               if models.IsErrEmailAlreadyUsed(err) {
-                       ctx.Data["Err_Email"] = true
-                       ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserEdit, &form)
-               } else if models.IsErrEmailInvalid(err) {
-                       ctx.Data["Err_Email"] = true
-                       ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserEdit, &form)
-               } else {
-                       ctx.ServerError("UpdateUser", err)
-               }
-               return
-       }
-       log.Trace("Account profile updated by admin (%s): %s", ctx.User.Name, u.Name)
-
-       ctx.Flash.Success(ctx.Tr("admin.users.update_profile_success"))
-       ctx.Redirect(setting.AppSubURL + "/admin/users/" + ctx.Params(":userid"))
-}
-
-// DeleteUser response for deleting a user
-func DeleteUser(ctx *context.Context) {
-       u, err := models.GetUserByID(ctx.ParamsInt64(":userid"))
-       if err != nil {
-               ctx.ServerError("GetUserByID", err)
-               return
-       }
-
-       if err = models.DeleteUser(u); err != nil {
-               switch {
-               case models.IsErrUserOwnRepos(err):
-                       ctx.Flash.Error(ctx.Tr("admin.users.still_own_repo"))
-                       ctx.JSON(http.StatusOK, map[string]interface{}{
-                               "redirect": setting.AppSubURL + "/admin/users/" + ctx.Params(":userid"),
-                       })
-               case models.IsErrUserHasOrgs(err):
-                       ctx.Flash.Error(ctx.Tr("admin.users.still_has_org"))
-                       ctx.JSON(http.StatusOK, map[string]interface{}{
-                               "redirect": setting.AppSubURL + "/admin/users/" + ctx.Params(":userid"),
-                       })
-               default:
-                       ctx.ServerError("DeleteUser", err)
-               }
-               return
-       }
-       log.Trace("Account deleted by admin (%s): %s", ctx.User.Name, u.Name)
-
-       ctx.Flash.Success(ctx.Tr("admin.users.deletion_success"))
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": setting.AppSubURL + "/admin/users",
-       })
-}
diff --git a/routers/admin/users_test.go b/routers/admin/users_test.go
deleted file mode 100644 (file)
index b19dcb8..0000000
+++ /dev/null
@@ -1,123 +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 admin
-
-import (
-       "testing"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/test"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-
-       "github.com/stretchr/testify/assert"
-)
-
-func TestNewUserPost_MustChangePassword(t *testing.T) {
-
-       models.PrepareTestEnv(t)
-       ctx := test.MockContext(t, "admin/users/new")
-
-       u := models.AssertExistsAndLoadBean(t, &models.User{
-               IsAdmin: true,
-               ID:      2,
-       }).(*models.User)
-
-       ctx.User = u
-
-       username := "gitea"
-       email := "gitea@gitea.io"
-
-       form := forms.AdminCreateUserForm{
-               LoginType:          "local",
-               LoginName:          "local",
-               UserName:           username,
-               Email:              email,
-               Password:           "abc123ABC!=$",
-               SendNotify:         false,
-               MustChangePassword: true,
-       }
-
-       web.SetForm(ctx, &form)
-       NewUserPost(ctx)
-
-       assert.NotEmpty(t, ctx.Flash.SuccessMsg)
-
-       u, err := models.GetUserByName(username)
-
-       assert.NoError(t, err)
-       assert.Equal(t, username, u.Name)
-       assert.Equal(t, email, u.Email)
-       assert.True(t, u.MustChangePassword)
-}
-
-func TestNewUserPost_MustChangePasswordFalse(t *testing.T) {
-
-       models.PrepareTestEnv(t)
-       ctx := test.MockContext(t, "admin/users/new")
-
-       u := models.AssertExistsAndLoadBean(t, &models.User{
-               IsAdmin: true,
-               ID:      2,
-       }).(*models.User)
-
-       ctx.User = u
-
-       username := "gitea"
-       email := "gitea@gitea.io"
-
-       form := forms.AdminCreateUserForm{
-               LoginType:          "local",
-               LoginName:          "local",
-               UserName:           username,
-               Email:              email,
-               Password:           "abc123ABC!=$",
-               SendNotify:         false,
-               MustChangePassword: false,
-       }
-
-       web.SetForm(ctx, &form)
-       NewUserPost(ctx)
-
-       assert.NotEmpty(t, ctx.Flash.SuccessMsg)
-
-       u, err := models.GetUserByName(username)
-
-       assert.NoError(t, err)
-       assert.Equal(t, username, u.Name)
-       assert.Equal(t, email, u.Email)
-       assert.False(t, u.MustChangePassword)
-}
-
-func TestNewUserPost_InvalidEmail(t *testing.T) {
-
-       models.PrepareTestEnv(t)
-       ctx := test.MockContext(t, "admin/users/new")
-
-       u := models.AssertExistsAndLoadBean(t, &models.User{
-               IsAdmin: true,
-               ID:      2,
-       }).(*models.User)
-
-       ctx.User = u
-
-       username := "gitea"
-       email := "gitea@gitea.io\r\n"
-
-       form := forms.AdminCreateUserForm{
-               LoginType:          "local",
-               LoginName:          "local",
-               UserName:           username,
-               Email:              email,
-               Password:           "abc123ABC!=$",
-               SendNotify:         false,
-               MustChangePassword: false,
-       }
-
-       web.SetForm(ctx, &form)
-       NewUserPost(ctx)
-
-       assert.NotEmpty(t, ctx.Flash.ErrorMsg)
-}
index 37e02874b4ffdf3222f51fd84f8570df57742606..39a60df33f01bff93610534f18fab1b6b4f7c706 100644 (file)
@@ -17,7 +17,7 @@ import (
        "code.gitea.io/gitea/modules/repofiles"
        api "code.gitea.io/gitea/modules/structs"
        "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/routers/repo"
+       "code.gitea.io/gitea/routers/common"
 )
 
 // GetRawFile get a file by path on a repository
@@ -83,7 +83,7 @@ func GetRawFile(ctx *context.APIContext) {
                }
                return
        }
-       if err = repo.ServeBlob(ctx.Context, blob); err != nil {
+       if err = common.ServeBlob(ctx.Context, blob); err != nil {
                ctx.Error(http.StatusInternalServerError, "ServeBlob", err)
        }
 }
@@ -126,7 +126,7 @@ func GetArchive(ctx *context.APIContext) {
        ctx.Repo.GitRepo = gitRepo
        defer gitRepo.Close()
 
-       repo.Download(ctx.Context)
+       common.Download(ctx.Context)
 }
 
 // GetEditorconfig get editor config of a repository
diff --git a/routers/common/db.go b/routers/common/db.go
new file mode 100644 (file)
index 0000000..069a46f
--- /dev/null
@@ -0,0 +1,39 @@
+// Copyright 2021 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 common
+
+import (
+       "context"
+       "fmt"
+       "time"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/models/migrations"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+)
+
+// InitDBEngine In case of problems connecting to DB, retry connection. Eg, PGSQL in Docker Container on Synology
+func InitDBEngine(ctx context.Context) (err error) {
+       log.Info("Beginning ORM engine initialization.")
+       for i := 0; i < setting.Database.DBConnectRetries; i++ {
+               select {
+               case <-ctx.Done():
+                       return fmt.Errorf("Aborted due to shutdown:\nin retry ORM engine initialization")
+               default:
+               }
+               log.Info("ORM engine initialization attempt #%d/%d...", i+1, setting.Database.DBConnectRetries)
+               if err = models.NewEngine(ctx, migrations.Migrate); err == nil {
+                       break
+               } else if i == setting.Database.DBConnectRetries-1 {
+                       return err
+               }
+               log.Error("ORM engine initialization attempt #%d/%d failed. Error: %v", i+1, setting.Database.DBConnectRetries, err)
+               log.Info("Backing off for %d seconds", int64(setting.Database.DBConnectBackoff/time.Second))
+               time.Sleep(setting.Database.DBConnectBackoff)
+       }
+       models.HasEngine = true
+       return nil
+}
diff --git a/routers/common/logger.go b/routers/common/logger.go
new file mode 100644 (file)
index 0000000..bc11495
--- /dev/null
@@ -0,0 +1,33 @@
+// Copyright 2021 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 common
+
+import (
+       "net/http"
+       "time"
+
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+)
+
+// LoggerHandler is a handler that will log the routing to the default gitea log
+func LoggerHandler(level log.Level) func(next http.Handler) http.Handler {
+       return func(next http.Handler) http.Handler {
+               return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+                       start := time.Now()
+
+                       _ = log.GetLogger("router").Log(0, level, "Started %s %s for %s", log.ColoredMethod(req.Method), req.URL.RequestURI(), req.RemoteAddr)
+
+                       next.ServeHTTP(w, req)
+
+                       var status int
+                       if v, ok := w.(context.ResponseWriter); ok {
+                               status = v.Status()
+                       }
+
+                       _ = log.GetLogger("router").Log(0, level, "Completed %s %s %v %s in %v", log.ColoredMethod(req.Method), req.URL.RequestURI(), log.ColoredStatus(status), log.ColoredStatus(status, http.StatusText(status)), log.ColoredTime(time.Since(start)))
+               })
+       }
+}
diff --git a/routers/common/middleware.go b/routers/common/middleware.go
new file mode 100644 (file)
index 0000000..1d96522
--- /dev/null
@@ -0,0 +1,76 @@
+// Copyright 2021 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 common
+
+import (
+       "fmt"
+       "net/http"
+       "strings"
+
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+
+       "github.com/chi-middleware/proxy"
+       "github.com/go-chi/chi/middleware"
+)
+
+// Middlewares returns common middlewares
+func Middlewares() []func(http.Handler) http.Handler {
+       var handlers = []func(http.Handler) http.Handler{
+               func(next http.Handler) http.Handler {
+                       return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
+                               next.ServeHTTP(context.NewResponse(resp), req)
+                       })
+               },
+       }
+
+       if setting.ReverseProxyLimit > 0 {
+               opt := proxy.NewForwardedHeadersOptions().
+                       WithForwardLimit(setting.ReverseProxyLimit).
+                       ClearTrustedProxies()
+               for _, n := range setting.ReverseProxyTrustedProxies {
+                       if !strings.Contains(n, "/") {
+                               opt.AddTrustedProxy(n)
+                       } else {
+                               opt.AddTrustedNetwork(n)
+                       }
+               }
+               handlers = append(handlers, proxy.ForwardedHeaders(opt))
+       }
+
+       handlers = append(handlers, middleware.StripSlashes)
+
+       if !setting.DisableRouterLog && setting.RouterLogLevel != log.NONE {
+               if log.GetLogger("router").GetLevel() <= setting.RouterLogLevel {
+                       handlers = append(handlers, LoggerHandler(setting.RouterLogLevel))
+               }
+       }
+       if setting.EnableAccessLog {
+               handlers = append(handlers, context.AccessLogger())
+       }
+
+       handlers = append(handlers, func(next http.Handler) http.Handler {
+               return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
+                       // Why we need this? The Recovery() will try to render a beautiful
+                       // error page for user, but the process can still panic again, and other
+                       // middleware like session also may panic then we have to recover twice
+                       // and send a simple error page that should not panic any more.
+                       defer func() {
+                               if err := recover(); err != nil {
+                                       combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, string(log.Stack(2)))
+                                       log.Error("%v", combinedErr)
+                                       if setting.IsProd() {
+                                               http.Error(resp, http.StatusText(500), 500)
+                                       } else {
+                                               http.Error(resp, combinedErr, 500)
+                                       }
+                               }
+                       }()
+                       next.ServeHTTP(resp, req)
+               })
+       })
+       return handlers
+}
diff --git a/routers/common/repo.go b/routers/common/repo.go
new file mode 100644 (file)
index 0000000..c61b5ec
--- /dev/null
@@ -0,0 +1,127 @@
+// Copyright 2021 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 common
+
+import (
+       "fmt"
+       "io"
+       "net/http"
+       "path"
+       "path/filepath"
+       "strings"
+
+       "code.gitea.io/gitea/modules/charset"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/httpcache"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/typesniffer"
+       "code.gitea.io/gitea/services/archiver"
+)
+
+// ServeBlob download a git.Blob
+func ServeBlob(ctx *context.Context, blob *git.Blob) error {
+       if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`) {
+               return nil
+       }
+
+       dataRc, err := blob.DataAsync()
+       if err != nil {
+               return err
+       }
+       defer func() {
+               if err = dataRc.Close(); err != nil {
+                       log.Error("ServeBlob: Close: %v", err)
+               }
+       }()
+
+       return ServeData(ctx, ctx.Repo.TreePath, blob.Size(), dataRc)
+}
+
+// Download an archive of a repository
+func Download(ctx *context.Context) {
+       uri := ctx.Params("*")
+       aReq := archiver.DeriveRequestFrom(ctx, uri)
+
+       if aReq == nil {
+               ctx.Error(http.StatusNotFound)
+               return
+       }
+
+       downloadName := ctx.Repo.Repository.Name + "-" + aReq.GetArchiveName()
+       complete := aReq.IsComplete()
+       if !complete {
+               aReq = archiver.ArchiveRepository(aReq)
+               complete = aReq.WaitForCompletion(ctx)
+       }
+
+       if complete {
+               ctx.ServeFile(aReq.GetArchivePath(), downloadName)
+       } else {
+               ctx.Error(http.StatusNotFound)
+       }
+}
+
+// ServeData download file from io.Reader
+func ServeData(ctx *context.Context, name string, size int64, reader io.Reader) error {
+       buf := make([]byte, 1024)
+       n, err := reader.Read(buf)
+       if err != nil && err != io.EOF {
+               return err
+       }
+       if n >= 0 {
+               buf = buf[:n]
+       }
+
+       ctx.Resp.Header().Set("Cache-Control", "public,max-age=86400")
+
+       if size >= 0 {
+               ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", size))
+       } else {
+               log.Error("ServeData called to serve data: %s with size < 0: %d", name, size)
+       }
+       name = path.Base(name)
+
+       // Google Chrome dislike commas in filenames, so let's change it to a space
+       name = strings.ReplaceAll(name, ",", " ")
+
+       st := typesniffer.DetectContentType(buf)
+
+       if st.IsText() || ctx.QueryBool("render") {
+               cs, err := charset.DetectEncoding(buf)
+               if err != nil {
+                       log.Error("Detect raw file %s charset failed: %v, using by default utf-8", name, err)
+                       cs = "utf-8"
+               }
+               ctx.Resp.Header().Set("Content-Type", "text/plain; charset="+strings.ToLower(cs))
+       } else {
+               ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
+
+               if (st.IsImage() || st.IsPDF()) && (setting.UI.SVG.Enabled || !st.IsSvgImage()) {
+                       ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, name))
+                       if st.IsSvgImage() {
+                               ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
+                               ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
+                               ctx.Resp.Header().Set("Content-Type", typesniffer.SvgMimeType)
+                       }
+               } else {
+                       ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name))
+                       if setting.MimeTypeMap.Enabled {
+                               fileExtension := strings.ToLower(filepath.Ext(name))
+                               if mimetype, ok := setting.MimeTypeMap.Map[fileExtension]; ok {
+                                       ctx.Resp.Header().Set("Content-Type", mimetype)
+                               }
+                       }
+               }
+       }
+
+       _, err = ctx.Resp.Write(buf)
+       if err != nil {
+               return err
+       }
+       _, err = io.Copy(ctx.Resp, reader)
+       return err
+}
diff --git a/routers/dev/template.go b/routers/dev/template.go
deleted file mode 100644 (file)
index de334c4..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright 2014 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 dev
-
-import (
-       "net/http"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/timeutil"
-)
-
-// TemplatePreview render for previewing the indicated template
-func TemplatePreview(ctx *context.Context) {
-       ctx.Data["User"] = models.User{Name: "Unknown"}
-       ctx.Data["AppName"] = setting.AppName
-       ctx.Data["AppVer"] = setting.AppVer
-       ctx.Data["AppUrl"] = setting.AppURL
-       ctx.Data["Code"] = "2014031910370000009fff6782aadb2162b4a997acb69d4400888e0b9274657374"
-       ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())
-       ctx.Data["ResetPwdCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, ctx.Locale.Language())
-       ctx.Data["CurDbValue"] = ""
-
-       ctx.HTML(http.StatusOK, base.TplName(ctx.Params("*")))
-}
diff --git a/routers/events/events.go b/routers/events/events.go
deleted file mode 100644 (file)
index b140bf6..0000000
+++ /dev/null
@@ -1,156 +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 events
-
-import (
-       "net/http"
-       "time"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/convert"
-       "code.gitea.io/gitea/modules/eventsource"
-       "code.gitea.io/gitea/modules/graceful"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/routers/user"
-       jsoniter "github.com/json-iterator/go"
-)
-
-// Events listens for events
-func Events(ctx *context.Context) {
-       // FIXME: Need to check if resp is actually a http.Flusher! - how though?
-
-       // Set the headers related to event streaming.
-       ctx.Resp.Header().Set("Content-Type", "text/event-stream")
-       ctx.Resp.Header().Set("Cache-Control", "no-cache")
-       ctx.Resp.Header().Set("Connection", "keep-alive")
-       ctx.Resp.Header().Set("X-Accel-Buffering", "no")
-       ctx.Resp.WriteHeader(http.StatusOK)
-
-       if !ctx.IsSigned {
-               // Return unauthorized status event
-               event := &eventsource.Event{
-                       Name: "close",
-                       Data: "unauthorized",
-               }
-               _, _ = event.WriteTo(ctx)
-               ctx.Resp.Flush()
-               return
-       }
-
-       // Listen to connection close and un-register messageChan
-       notify := ctx.Done()
-       ctx.Resp.Flush()
-
-       shutdownCtx := graceful.GetManager().ShutdownContext()
-
-       uid := ctx.User.ID
-
-       messageChan := eventsource.GetManager().Register(uid)
-
-       unregister := func() {
-               eventsource.GetManager().Unregister(uid, messageChan)
-               // ensure the messageChan is closed
-               for {
-                       _, ok := <-messageChan
-                       if !ok {
-                               break
-                       }
-               }
-       }
-
-       if _, err := ctx.Resp.Write([]byte("\n")); err != nil {
-               log.Error("Unable to write to EventStream: %v", err)
-               unregister()
-               return
-       }
-
-       timer := time.NewTicker(30 * time.Second)
-
-       stopwatchTimer := time.NewTicker(setting.UI.Notification.MinTimeout)
-
-loop:
-       for {
-               select {
-               case <-timer.C:
-                       event := &eventsource.Event{
-                               Name: "ping",
-                       }
-                       _, err := event.WriteTo(ctx.Resp)
-                       if err != nil {
-                               log.Error("Unable to write to EventStream for user %s: %v", ctx.User.Name, err)
-                               go unregister()
-                               break loop
-                       }
-                       ctx.Resp.Flush()
-               case <-notify:
-                       go unregister()
-                       break loop
-               case <-shutdownCtx.Done():
-                       go unregister()
-                       break loop
-               case <-stopwatchTimer.C:
-                       sws, err := models.GetUserStopwatches(ctx.User.ID, models.ListOptions{})
-                       if err != nil {
-                               log.Error("Unable to GetUserStopwatches: %v", err)
-                               continue
-                       }
-                       apiSWs, err := convert.ToStopWatches(sws)
-                       if err != nil {
-                               log.Error("Unable to APIFormat stopwatches: %v", err)
-                               continue
-                       }
-                       json := jsoniter.ConfigCompatibleWithStandardLibrary
-                       dataBs, err := json.Marshal(apiSWs)
-                       if err != nil {
-                               log.Error("Unable to marshal stopwatches: %v", err)
-                               continue
-                       }
-                       _, err = (&eventsource.Event{
-                               Name: "stopwatches",
-                               Data: string(dataBs),
-                       }).WriteTo(ctx.Resp)
-                       if err != nil {
-                               log.Error("Unable to write to EventStream for user %s: %v", ctx.User.Name, err)
-                               go unregister()
-                               break loop
-                       }
-                       ctx.Resp.Flush()
-               case event, ok := <-messageChan:
-                       if !ok {
-                               break loop
-                       }
-
-                       // Handle logout
-                       if event.Name == "logout" {
-                               if ctx.Session.ID() == event.Data {
-                                       _, _ = (&eventsource.Event{
-                                               Name: "logout",
-                                               Data: "here",
-                                       }).WriteTo(ctx.Resp)
-                                       ctx.Resp.Flush()
-                                       go unregister()
-                                       user.HandleSignOut(ctx)
-                                       break loop
-                               }
-                               // Replace the event - we don't want to expose the session ID to the user
-                               event = &eventsource.Event{
-                                       Name: "logout",
-                                       Data: "elsewhere",
-                               }
-                       }
-
-                       _, err := event.WriteTo(ctx.Resp)
-                       if err != nil {
-                               log.Error("Unable to write to EventStream for user %s: %v", ctx.User.Name, err)
-                               go unregister()
-                               break loop
-                       }
-                       ctx.Resp.Flush()
-               }
-       }
-       timer.Stop()
-}
diff --git a/routers/home.go b/routers/home.go
deleted file mode 100644 (file)
index 7eaebc0..0000000
+++ /dev/null
@@ -1,413 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2019 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 routers
-
-import (
-       "bytes"
-       "net/http"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       code_indexer "code.gitea.io/gitea/modules/indexer/code"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/structs"
-       "code.gitea.io/gitea/modules/util"
-       "code.gitea.io/gitea/modules/web/middleware"
-       "code.gitea.io/gitea/routers/user"
-)
-
-const (
-       // tplHome home page template
-       tplHome base.TplName = "home"
-       // tplExploreRepos explore repositories page template
-       tplExploreRepos base.TplName = "explore/repos"
-       // tplExploreUsers explore users page template
-       tplExploreUsers base.TplName = "explore/users"
-       // tplExploreOrganizations explore organizations page template
-       tplExploreOrganizations base.TplName = "explore/organizations"
-       // tplExploreCode explore code page template
-       tplExploreCode base.TplName = "explore/code"
-)
-
-// Home render home page
-func Home(ctx *context.Context) {
-       if ctx.IsSigned {
-               if !ctx.User.IsActive && setting.Service.RegisterEmailConfirm {
-                       ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
-                       ctx.HTML(http.StatusOK, user.TplActivate)
-               } else if !ctx.User.IsActive || ctx.User.ProhibitLogin {
-                       log.Info("Failed authentication attempt for %s from %s", ctx.User.Name, ctx.RemoteAddr())
-                       ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
-                       ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
-               } else if ctx.User.MustChangePassword {
-                       ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
-                       ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/change_password"
-                       middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
-                       ctx.Redirect(setting.AppSubURL + "/user/settings/change_password")
-               } else {
-                       user.Dashboard(ctx)
-               }
-               return
-               // Check non-logged users landing page.
-       } else if setting.LandingPageURL != setting.LandingPageHome {
-               ctx.Redirect(setting.AppSubURL + string(setting.LandingPageURL))
-               return
-       }
-
-       // Check auto-login.
-       uname := ctx.GetCookie(setting.CookieUserName)
-       if len(uname) != 0 {
-               ctx.Redirect(setting.AppSubURL + "/user/login")
-               return
-       }
-
-       ctx.Data["PageIsHome"] = true
-       ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
-       ctx.HTML(http.StatusOK, tplHome)
-}
-
-// RepoSearchOptions when calling search repositories
-type RepoSearchOptions struct {
-       OwnerID    int64
-       Private    bool
-       Restricted bool
-       PageSize   int
-       TplName    base.TplName
-}
-
-var (
-       nullByte = []byte{0x00}
-)
-
-func isKeywordValid(keyword string) bool {
-       return !bytes.Contains([]byte(keyword), nullByte)
-}
-
-// RenderRepoSearch render repositories search page
-func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
-       page := ctx.QueryInt("page")
-       if page <= 0 {
-               page = 1
-       }
-
-       var (
-               repos   []*models.Repository
-               count   int64
-               err     error
-               orderBy models.SearchOrderBy
-       )
-
-       ctx.Data["SortType"] = ctx.Query("sort")
-       switch ctx.Query("sort") {
-       case "newest":
-               orderBy = models.SearchOrderByNewest
-       case "oldest":
-               orderBy = models.SearchOrderByOldest
-       case "recentupdate":
-               orderBy = models.SearchOrderByRecentUpdated
-       case "leastupdate":
-               orderBy = models.SearchOrderByLeastUpdated
-       case "reversealphabetically":
-               orderBy = models.SearchOrderByAlphabeticallyReverse
-       case "alphabetically":
-               orderBy = models.SearchOrderByAlphabetically
-       case "reversesize":
-               orderBy = models.SearchOrderBySizeReverse
-       case "size":
-               orderBy = models.SearchOrderBySize
-       case "moststars":
-               orderBy = models.SearchOrderByStarsReverse
-       case "feweststars":
-               orderBy = models.SearchOrderByStars
-       case "mostforks":
-               orderBy = models.SearchOrderByForksReverse
-       case "fewestforks":
-               orderBy = models.SearchOrderByForks
-       default:
-               ctx.Data["SortType"] = "recentupdate"
-               orderBy = models.SearchOrderByRecentUpdated
-       }
-
-       keyword := strings.Trim(ctx.Query("q"), " ")
-       topicOnly := ctx.QueryBool("topic")
-       ctx.Data["TopicOnly"] = topicOnly
-
-       repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
-               ListOptions: models.ListOptions{
-                       Page:     page,
-                       PageSize: opts.PageSize,
-               },
-               Actor:              ctx.User,
-               OrderBy:            orderBy,
-               Private:            opts.Private,
-               Keyword:            keyword,
-               OwnerID:            opts.OwnerID,
-               AllPublic:          true,
-               AllLimited:         true,
-               TopicOnly:          topicOnly,
-               IncludeDescription: setting.UI.SearchRepoDescription,
-       })
-       if err != nil {
-               ctx.ServerError("SearchRepository", err)
-               return
-       }
-       ctx.Data["Keyword"] = keyword
-       ctx.Data["Total"] = count
-       ctx.Data["Repos"] = repos
-       ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
-
-       pager := context.NewPagination(int(count), opts.PageSize, page, 5)
-       pager.SetDefaultParams(ctx)
-       pager.AddParam(ctx, "topic", "TopicOnly")
-       ctx.Data["Page"] = pager
-
-       ctx.HTML(http.StatusOK, opts.TplName)
-}
-
-// ExploreRepos render explore repositories page
-func ExploreRepos(ctx *context.Context) {
-       ctx.Data["UsersIsDisabled"] = setting.Service.Explore.DisableUsersPage
-       ctx.Data["Title"] = ctx.Tr("explore")
-       ctx.Data["PageIsExplore"] = true
-       ctx.Data["PageIsExploreRepositories"] = true
-       ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
-
-       var ownerID int64
-       if ctx.User != nil && !ctx.User.IsAdmin {
-               ownerID = ctx.User.ID
-       }
-
-       RenderRepoSearch(ctx, &RepoSearchOptions{
-               PageSize: setting.UI.ExplorePagingNum,
-               OwnerID:  ownerID,
-               Private:  ctx.User != nil,
-               TplName:  tplExploreRepos,
-       })
-}
-
-// RenderUserSearch render user search page
-func RenderUserSearch(ctx *context.Context, opts *models.SearchUserOptions, tplName base.TplName) {
-       opts.Page = ctx.QueryInt("page")
-       if opts.Page <= 1 {
-               opts.Page = 1
-       }
-
-       var (
-               users   []*models.User
-               count   int64
-               err     error
-               orderBy models.SearchOrderBy
-       )
-
-       ctx.Data["SortType"] = ctx.Query("sort")
-       switch ctx.Query("sort") {
-       case "newest":
-               orderBy = models.SearchOrderByIDReverse
-       case "oldest":
-               orderBy = models.SearchOrderByID
-       case "recentupdate":
-               orderBy = models.SearchOrderByRecentUpdated
-       case "leastupdate":
-               orderBy = models.SearchOrderByLeastUpdated
-       case "reversealphabetically":
-               orderBy = models.SearchOrderByAlphabeticallyReverse
-       case "alphabetically":
-               orderBy = models.SearchOrderByAlphabetically
-       default:
-               ctx.Data["SortType"] = "alphabetically"
-               orderBy = models.SearchOrderByAlphabetically
-       }
-
-       opts.Keyword = strings.Trim(ctx.Query("q"), " ")
-       opts.OrderBy = orderBy
-       if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
-               users, count, err = models.SearchUsers(opts)
-               if err != nil {
-                       ctx.ServerError("SearchUsers", err)
-                       return
-               }
-       }
-       ctx.Data["Keyword"] = opts.Keyword
-       ctx.Data["Total"] = count
-       ctx.Data["Users"] = users
-       ctx.Data["UsersTwoFaStatus"] = models.UserList(users).GetTwoFaStatus()
-       ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail
-       ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
-
-       pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
-       pager.SetDefaultParams(ctx)
-       ctx.Data["Page"] = pager
-
-       ctx.HTML(http.StatusOK, tplName)
-}
-
-// ExploreUsers render explore users page
-func ExploreUsers(ctx *context.Context) {
-       if setting.Service.Explore.DisableUsersPage {
-               ctx.Redirect(setting.AppSubURL + "/explore/repos")
-               return
-       }
-       ctx.Data["Title"] = ctx.Tr("explore")
-       ctx.Data["PageIsExplore"] = true
-       ctx.Data["PageIsExploreUsers"] = true
-       ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
-
-       RenderUserSearch(ctx, &models.SearchUserOptions{
-               Actor:       ctx.User,
-               Type:        models.UserTypeIndividual,
-               ListOptions: models.ListOptions{PageSize: setting.UI.ExplorePagingNum},
-               IsActive:    util.OptionalBoolTrue,
-               Visible:     []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate},
-       }, tplExploreUsers)
-}
-
-// ExploreOrganizations render explore organizations page
-func ExploreOrganizations(ctx *context.Context) {
-       ctx.Data["UsersIsDisabled"] = setting.Service.Explore.DisableUsersPage
-       ctx.Data["Title"] = ctx.Tr("explore")
-       ctx.Data["PageIsExplore"] = true
-       ctx.Data["PageIsExploreOrganizations"] = true
-       ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
-
-       visibleTypes := []structs.VisibleType{structs.VisibleTypePublic}
-       if ctx.User != nil {
-               visibleTypes = append(visibleTypes, structs.VisibleTypeLimited, structs.VisibleTypePrivate)
-       }
-
-       RenderUserSearch(ctx, &models.SearchUserOptions{
-               Actor:       ctx.User,
-               Type:        models.UserTypeOrganization,
-               ListOptions: models.ListOptions{PageSize: setting.UI.ExplorePagingNum},
-               Visible:     visibleTypes,
-       }, tplExploreOrganizations)
-}
-
-// ExploreCode render explore code page
-func ExploreCode(ctx *context.Context) {
-       if !setting.Indexer.RepoIndexerEnabled {
-               ctx.Redirect(setting.AppSubURL+"/explore", 302)
-               return
-       }
-
-       ctx.Data["UsersIsDisabled"] = setting.Service.Explore.DisableUsersPage
-       ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
-       ctx.Data["Title"] = ctx.Tr("explore")
-       ctx.Data["PageIsExplore"] = true
-       ctx.Data["PageIsExploreCode"] = true
-
-       language := strings.TrimSpace(ctx.Query("l"))
-       keyword := strings.TrimSpace(ctx.Query("q"))
-       page := ctx.QueryInt("page")
-       if page <= 0 {
-               page = 1
-       }
-
-       queryType := strings.TrimSpace(ctx.Query("t"))
-       isMatch := queryType == "match"
-
-       var (
-               repoIDs []int64
-               err     error
-               isAdmin bool
-       )
-       if ctx.User != nil {
-               isAdmin = ctx.User.IsAdmin
-       }
-
-       // guest user or non-admin user
-       if ctx.User == nil || !isAdmin {
-               repoIDs, err = models.FindUserAccessibleRepoIDs(ctx.User)
-               if err != nil {
-                       ctx.ServerError("SearchResults", err)
-                       return
-               }
-       }
-
-       var (
-               total                 int
-               searchResults         []*code_indexer.Result
-               searchResultLanguages []*code_indexer.SearchResultLanguages
-       )
-
-       // if non-admin login user, we need check UnitTypeCode at first
-       if ctx.User != nil && len(repoIDs) > 0 {
-               repoMaps, err := models.GetRepositoriesMapByIDs(repoIDs)
-               if err != nil {
-                       ctx.ServerError("SearchResults", err)
-                       return
-               }
-
-               var rightRepoMap = make(map[int64]*models.Repository, len(repoMaps))
-               repoIDs = make([]int64, 0, len(repoMaps))
-               for id, repo := range repoMaps {
-                       if repo.CheckUnitUser(ctx.User, models.UnitTypeCode) {
-                               rightRepoMap[id] = repo
-                               repoIDs = append(repoIDs, id)
-                       }
-               }
-
-               ctx.Data["RepoMaps"] = rightRepoMap
-
-               total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
-               if err != nil {
-                       ctx.ServerError("SearchResults", err)
-                       return
-               }
-               // if non-login user or isAdmin, no need to check UnitTypeCode
-       } else if (ctx.User == nil && len(repoIDs) > 0) || isAdmin {
-               total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
-               if err != nil {
-                       ctx.ServerError("SearchResults", err)
-                       return
-               }
-
-               var loadRepoIDs = make([]int64, 0, len(searchResults))
-               for _, result := range searchResults {
-                       var find bool
-                       for _, id := range loadRepoIDs {
-                               if id == result.RepoID {
-                                       find = true
-                                       break
-                               }
-                       }
-                       if !find {
-                               loadRepoIDs = append(loadRepoIDs, result.RepoID)
-                       }
-               }
-
-               repoMaps, err := models.GetRepositoriesMapByIDs(loadRepoIDs)
-               if err != nil {
-                       ctx.ServerError("SearchResults", err)
-                       return
-               }
-
-               ctx.Data["RepoMaps"] = repoMaps
-       }
-
-       ctx.Data["Keyword"] = keyword
-       ctx.Data["Language"] = language
-       ctx.Data["queryType"] = queryType
-       ctx.Data["SearchResults"] = searchResults
-       ctx.Data["SearchResultLanguages"] = searchResultLanguages
-       ctx.Data["RequireHighlightJS"] = true
-       ctx.Data["PageIsViewCode"] = true
-
-       pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
-       pager.SetDefaultParams(ctx)
-       pager.AddParam(ctx, "l", "Language")
-       ctx.Data["Page"] = pager
-
-       ctx.HTML(http.StatusOK, tplExploreCode)
-}
-
-// NotFound render 404 page
-func NotFound(ctx *context.Context) {
-       ctx.Data["Title"] = "Page Not Found"
-       ctx.NotFound("home.NotFound", nil)
-}
index 220d87a29da87f5c3312f565c37ea4cb749fedd4..5e2eca439eb68667e4714cb36ef09bea309902e2 100644 (file)
@@ -6,12 +6,9 @@ package routers
 
 import (
        "context"
-       "fmt"
        "strings"
-       "time"
 
        "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/models/migrations"
        "code.gitea.io/gitea/modules/auth/sso"
        "code.gitea.io/gitea/modules/cache"
        "code.gitea.io/gitea/modules/cron"
@@ -32,6 +29,11 @@ import (
        "code.gitea.io/gitea/modules/svg"
        "code.gitea.io/gitea/modules/task"
        "code.gitea.io/gitea/modules/translation"
+       "code.gitea.io/gitea/modules/web"
+       apiv1 "code.gitea.io/gitea/routers/api/v1"
+       "code.gitea.io/gitea/routers/common"
+       "code.gitea.io/gitea/routers/private"
+       web_routers "code.gitea.io/gitea/routers/web"
        "code.gitea.io/gitea/services/mailer"
        mirror_service "code.gitea.io/gitea/services/mirror"
        pull_service "code.gitea.io/gitea/services/pull"
@@ -63,63 +65,6 @@ func NewServices() {
        notification.NewContext()
 }
 
-// In case of problems connecting to DB, retry connection. Eg, PGSQL in Docker Container on Synology
-func initDBEngine(ctx context.Context) (err error) {
-       log.Info("Beginning ORM engine initialization.")
-       for i := 0; i < setting.Database.DBConnectRetries; i++ {
-               select {
-               case <-ctx.Done():
-                       return fmt.Errorf("Aborted due to shutdown:\nin retry ORM engine initialization")
-               default:
-               }
-               log.Info("ORM engine initialization attempt #%d/%d...", i+1, setting.Database.DBConnectRetries)
-               if err = models.NewEngine(ctx, migrations.Migrate); err == nil {
-                       break
-               } else if i == setting.Database.DBConnectRetries-1 {
-                       return err
-               }
-               log.Error("ORM engine initialization attempt #%d/%d failed. Error: %v", i+1, setting.Database.DBConnectRetries, err)
-               log.Info("Backing off for %d seconds", int64(setting.Database.DBConnectBackoff/time.Second))
-               time.Sleep(setting.Database.DBConnectBackoff)
-       }
-       models.HasEngine = true
-       return nil
-}
-
-// PreInstallInit preloads the configuration to check if we need to run install
-func PreInstallInit(ctx context.Context) bool {
-       setting.NewContext()
-       if !setting.InstallLock {
-               log.Trace("AppPath: %s", setting.AppPath)
-               log.Trace("AppWorkPath: %s", setting.AppWorkPath)
-               log.Trace("Custom path: %s", setting.CustomPath)
-               log.Trace("Log path: %s", setting.LogRootPath)
-               log.Trace("Preparing to run install page")
-               translation.InitLocales()
-               if setting.EnableSQLite3 {
-                       log.Info("SQLite3 Supported")
-               }
-               setting.InitDBConfig()
-               svg.Init()
-       }
-
-       return !setting.InstallLock
-}
-
-// PostInstallInit rereads the settings and starts up the database
-func PostInstallInit(ctx context.Context) {
-       setting.NewContext()
-       setting.InitDBConfig()
-       if setting.InstallLock {
-               if err := initDBEngine(ctx); err == nil {
-                       log.Info("ORM engine initialization successful!")
-               } else {
-                       log.Fatal("ORM engine initialization failed: %v", err)
-               }
-               svg.Init()
-       }
-}
-
 // GlobalInit is for global configuration reload-able.
 func GlobalInit(ctx context.Context) {
        setting.NewContext()
@@ -151,7 +96,7 @@ func GlobalInit(ctx context.Context) {
        } else if setting.Database.UseSQLite3 {
                log.Fatal("SQLite3 is set in settings but NOT Supported")
        }
-       if err := initDBEngine(ctx); err == nil {
+       if err := common.InitDBEngine(ctx); err == nil {
                log.Info("ORM engine initialization successful!")
        } else {
                log.Fatal("ORM engine initialization failed: %v", err)
@@ -193,3 +138,16 @@ func GlobalInit(ctx context.Context) {
 
        svg.Init()
 }
+
+// NormalRoutes represents non install routes
+func NormalRoutes() *web.Route {
+       r := web.NewRoute()
+       for _, middle := range common.Middlewares() {
+               r.Use(middle)
+       }
+
+       r.Mount("/", web_routers.Routes())
+       r.Mount("/api/v1", apiv1.Routes())
+       r.Mount("/api/internal", private.Routes())
+       return r
+}
diff --git a/routers/install.go b/routers/install.go
deleted file mode 100644 (file)
index 6c460a8..0000000
+++ /dev/null
@@ -1,472 +0,0 @@
-// Copyright 2014 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 routers
-
-import (
-       "fmt"
-       "net/http"
-       "os"
-       "os/exec"
-       "path/filepath"
-       "strings"
-       "time"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/generate"
-       "code.gitea.io/gitea/modules/graceful"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/templates"
-       "code.gitea.io/gitea/modules/translation"
-       "code.gitea.io/gitea/modules/user"
-       "code.gitea.io/gitea/modules/util"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/modules/web/middleware"
-       "code.gitea.io/gitea/services/forms"
-
-       "gitea.com/go-chi/session"
-       "gopkg.in/ini.v1"
-)
-
-const (
-       // tplInstall template for installation page
-       tplInstall     base.TplName = "install"
-       tplPostInstall base.TplName = "post-install"
-)
-
-// InstallInit prepare for rendering installation page
-func InstallInit(next http.Handler) http.Handler {
-       var rnd = templates.HTMLRenderer()
-
-       return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
-               if setting.InstallLock {
-                       resp.Header().Add("Refresh", "1; url="+setting.AppURL+"user/login")
-                       _ = rnd.HTML(resp, 200, string(tplPostInstall), nil)
-                       return
-               }
-               var locale = middleware.Locale(resp, req)
-               var startTime = time.Now()
-               var ctx = context.Context{
-                       Resp:    context.NewResponse(resp),
-                       Flash:   &middleware.Flash{},
-                       Locale:  locale,
-                       Render:  rnd,
-                       Session: session.GetSession(req),
-                       Data: map[string]interface{}{
-                               "Title":         locale.Tr("install.install"),
-                               "PageIsInstall": true,
-                               "DbOptions":     setting.SupportedDatabases,
-                               "i18n":          locale,
-                               "Language":      locale.Language(),
-                               "Lang":          locale.Language(),
-                               "AllLangs":      translation.AllLangs(),
-                               "CurrentURL":    setting.AppSubURL + req.URL.RequestURI(),
-                               "PageStartTime": startTime,
-                               "TmplLoadTimes": func() string {
-                                       return time.Since(startTime).String()
-                               },
-                               "PasswordHashAlgorithms": models.AvailableHashAlgorithms,
-                       },
-               }
-               for _, lang := range translation.AllLangs() {
-                       if lang.Lang == locale.Language() {
-                               ctx.Data["LangName"] = lang.Name
-                               break
-                       }
-               }
-               ctx.Req = context.WithContext(req, &ctx)
-               next.ServeHTTP(resp, ctx.Req)
-       })
-}
-
-// Install render installation page
-func Install(ctx *context.Context) {
-       form := forms.InstallForm{}
-
-       // Database settings
-       form.DbHost = setting.Database.Host
-       form.DbUser = setting.Database.User
-       form.DbPasswd = setting.Database.Passwd
-       form.DbName = setting.Database.Name
-       form.DbPath = setting.Database.Path
-       form.DbSchema = setting.Database.Schema
-       form.Charset = setting.Database.Charset
-
-       var curDBOption = "MySQL"
-       switch setting.Database.Type {
-       case "postgres":
-               curDBOption = "PostgreSQL"
-       case "mssql":
-               curDBOption = "MSSQL"
-       case "sqlite3":
-               if setting.EnableSQLite3 {
-                       curDBOption = "SQLite3"
-               }
-       }
-
-       ctx.Data["CurDbOption"] = curDBOption
-
-       // Application general settings
-       form.AppName = setting.AppName
-       form.RepoRootPath = setting.RepoRootPath
-       form.LFSRootPath = setting.LFS.Path
-
-       // Note(unknown): it's hard for Windows users change a running user,
-       //      so just use current one if config says default.
-       if setting.IsWindows && setting.RunUser == "git" {
-               form.RunUser = user.CurrentUsername()
-       } else {
-               form.RunUser = setting.RunUser
-       }
-
-       form.Domain = setting.Domain
-       form.SSHPort = setting.SSH.Port
-       form.HTTPPort = setting.HTTPPort
-       form.AppURL = setting.AppURL
-       form.LogRootPath = setting.LogRootPath
-
-       // E-mail service settings
-       if setting.MailService != nil {
-               form.SMTPHost = setting.MailService.Host
-               form.SMTPFrom = setting.MailService.From
-               form.SMTPUser = setting.MailService.User
-       }
-       form.RegisterConfirm = setting.Service.RegisterEmailConfirm
-       form.MailNotify = setting.Service.EnableNotifyMail
-
-       // Server and other services settings
-       form.OfflineMode = setting.OfflineMode
-       form.DisableGravatar = setting.DisableGravatar
-       form.EnableFederatedAvatar = setting.EnableFederatedAvatar
-       form.EnableOpenIDSignIn = setting.Service.EnableOpenIDSignIn
-       form.EnableOpenIDSignUp = setting.Service.EnableOpenIDSignUp
-       form.DisableRegistration = setting.Service.DisableRegistration
-       form.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration
-       form.EnableCaptcha = setting.Service.EnableCaptcha
-       form.RequireSignInView = setting.Service.RequireSignInView
-       form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
-       form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization
-       form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking
-       form.NoReplyAddress = setting.Service.NoReplyAddress
-       form.PasswordAlgorithm = setting.PasswordHashAlgo
-
-       middleware.AssignForm(form, ctx.Data)
-       ctx.HTML(http.StatusOK, tplInstall)
-}
-
-// InstallPost response for submit install items
-func InstallPost(ctx *context.Context) {
-       form := *web.GetForm(ctx).(*forms.InstallForm)
-       var err error
-       ctx.Data["CurDbOption"] = form.DbType
-
-       if ctx.HasError() {
-               if ctx.HasValue("Err_SMTPUser") {
-                       ctx.Data["Err_SMTP"] = true
-               }
-               if ctx.HasValue("Err_AdminName") ||
-                       ctx.HasValue("Err_AdminPasswd") ||
-                       ctx.HasValue("Err_AdminEmail") {
-                       ctx.Data["Err_Admin"] = true
-               }
-
-               ctx.HTML(http.StatusOK, tplInstall)
-               return
-       }
-
-       if _, err = exec.LookPath("git"); err != nil {
-               ctx.RenderWithErr(ctx.Tr("install.test_git_failed", err), tplInstall, &form)
-               return
-       }
-
-       // Pass basic check, now test configuration.
-       // Test database setting.
-
-       setting.Database.Type = setting.GetDBTypeByName(form.DbType)
-       setting.Database.Host = form.DbHost
-       setting.Database.User = form.DbUser
-       setting.Database.Passwd = form.DbPasswd
-       setting.Database.Name = form.DbName
-       setting.Database.Schema = form.DbSchema
-       setting.Database.SSLMode = form.SSLMode
-       setting.Database.Charset = form.Charset
-       setting.Database.Path = form.DbPath
-
-       setting.PasswordHashAlgo = form.PasswordAlgorithm
-
-       if (setting.Database.Type == "sqlite3") &&
-               len(setting.Database.Path) == 0 {
-               ctx.Data["Err_DbPath"] = true
-               ctx.RenderWithErr(ctx.Tr("install.err_empty_db_path"), tplInstall, &form)
-               return
-       }
-
-       // Set test engine.
-       if err = models.NewTestEngine(); err != nil {
-               if strings.Contains(err.Error(), `Unknown database type: sqlite3`) {
-                       ctx.Data["Err_DbType"] = true
-                       ctx.RenderWithErr(ctx.Tr("install.sqlite3_not_available", "https://docs.gitea.io/en-us/install-from-binary/"), tplInstall, &form)
-               } else {
-                       ctx.Data["Err_DbSetting"] = true
-                       ctx.RenderWithErr(ctx.Tr("install.invalid_db_setting", err), tplInstall, &form)
-               }
-               return
-       }
-
-       // Test repository root path.
-       form.RepoRootPath = strings.ReplaceAll(form.RepoRootPath, "\\", "/")
-       if err = os.MkdirAll(form.RepoRootPath, os.ModePerm); err != nil {
-               ctx.Data["Err_RepoRootPath"] = true
-               ctx.RenderWithErr(ctx.Tr("install.invalid_repo_path", err), tplInstall, &form)
-               return
-       }
-
-       // Test LFS root path if not empty, empty meaning disable LFS
-       if form.LFSRootPath != "" {
-               form.LFSRootPath = strings.ReplaceAll(form.LFSRootPath, "\\", "/")
-               if err := os.MkdirAll(form.LFSRootPath, os.ModePerm); err != nil {
-                       ctx.Data["Err_LFSRootPath"] = true
-                       ctx.RenderWithErr(ctx.Tr("install.invalid_lfs_path", err), tplInstall, &form)
-                       return
-               }
-       }
-
-       // Test log root path.
-       form.LogRootPath = strings.ReplaceAll(form.LogRootPath, "\\", "/")
-       if err = os.MkdirAll(form.LogRootPath, os.ModePerm); err != nil {
-               ctx.Data["Err_LogRootPath"] = true
-               ctx.RenderWithErr(ctx.Tr("install.invalid_log_root_path", err), tplInstall, &form)
-               return
-       }
-
-       currentUser, match := setting.IsRunUserMatchCurrentUser(form.RunUser)
-       if !match {
-               ctx.Data["Err_RunUser"] = true
-               ctx.RenderWithErr(ctx.Tr("install.run_user_not_match", form.RunUser, currentUser), tplInstall, &form)
-               return
-       }
-
-       // Check logic loophole between disable self-registration and no admin account.
-       if form.DisableRegistration && len(form.AdminName) == 0 {
-               ctx.Data["Err_Services"] = true
-               ctx.Data["Err_Admin"] = true
-               ctx.RenderWithErr(ctx.Tr("install.no_admin_and_disable_registration"), tplInstall, form)
-               return
-       }
-
-       // Check admin user creation
-       if len(form.AdminName) > 0 {
-               // Ensure AdminName is valid
-               if err := models.IsUsableUsername(form.AdminName); err != nil {
-                       ctx.Data["Err_Admin"] = true
-                       ctx.Data["Err_AdminName"] = true
-                       if models.IsErrNameReserved(err) {
-                               ctx.RenderWithErr(ctx.Tr("install.err_admin_name_is_reserved"), tplInstall, form)
-                               return
-                       } else if models.IsErrNamePatternNotAllowed(err) {
-                               ctx.RenderWithErr(ctx.Tr("install.err_admin_name_pattern_not_allowed"), tplInstall, form)
-                               return
-                       }
-                       ctx.RenderWithErr(ctx.Tr("install.err_admin_name_is_invalid"), tplInstall, form)
-                       return
-               }
-               // Check Admin email
-               if len(form.AdminEmail) == 0 {
-                       ctx.Data["Err_Admin"] = true
-                       ctx.Data["Err_AdminEmail"] = true
-                       ctx.RenderWithErr(ctx.Tr("install.err_empty_admin_email"), tplInstall, form)
-                       return
-               }
-               // Check admin password.
-               if len(form.AdminPasswd) == 0 {
-                       ctx.Data["Err_Admin"] = true
-                       ctx.Data["Err_AdminPasswd"] = true
-                       ctx.RenderWithErr(ctx.Tr("install.err_empty_admin_password"), tplInstall, form)
-                       return
-               }
-               if form.AdminPasswd != form.AdminConfirmPasswd {
-                       ctx.Data["Err_Admin"] = true
-                       ctx.Data["Err_AdminPasswd"] = true
-                       ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplInstall, form)
-                       return
-               }
-       }
-
-       if form.AppURL[len(form.AppURL)-1] != '/' {
-               form.AppURL += "/"
-       }
-
-       // Save settings.
-       cfg := ini.Empty()
-       isFile, err := util.IsFile(setting.CustomConf)
-       if err != nil {
-               log.Error("Unable to check if %s is a file. Error: %v", setting.CustomConf, err)
-       }
-       if isFile {
-               // Keeps custom settings if there is already something.
-               if err = cfg.Append(setting.CustomConf); err != nil {
-                       log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err)
-               }
-       }
-       cfg.Section("database").Key("DB_TYPE").SetValue(setting.Database.Type)
-       cfg.Section("database").Key("HOST").SetValue(setting.Database.Host)
-       cfg.Section("database").Key("NAME").SetValue(setting.Database.Name)
-       cfg.Section("database").Key("USER").SetValue(setting.Database.User)
-       cfg.Section("database").Key("PASSWD").SetValue(setting.Database.Passwd)
-       cfg.Section("database").Key("SCHEMA").SetValue(setting.Database.Schema)
-       cfg.Section("database").Key("SSL_MODE").SetValue(setting.Database.SSLMode)
-       cfg.Section("database").Key("CHARSET").SetValue(setting.Database.Charset)
-       cfg.Section("database").Key("PATH").SetValue(setting.Database.Path)
-       cfg.Section("database").Key("LOG_SQL").SetValue("false") // LOG_SQL is rarely helpful
-
-       cfg.Section("").Key("APP_NAME").SetValue(form.AppName)
-       cfg.Section("repository").Key("ROOT").SetValue(form.RepoRootPath)
-       cfg.Section("").Key("RUN_USER").SetValue(form.RunUser)
-       cfg.Section("server").Key("SSH_DOMAIN").SetValue(form.Domain)
-       cfg.Section("server").Key("DOMAIN").SetValue(form.Domain)
-       cfg.Section("server").Key("HTTP_PORT").SetValue(form.HTTPPort)
-       cfg.Section("server").Key("ROOT_URL").SetValue(form.AppURL)
-
-       if form.SSHPort == 0 {
-               cfg.Section("server").Key("DISABLE_SSH").SetValue("true")
-       } else {
-               cfg.Section("server").Key("DISABLE_SSH").SetValue("false")
-               cfg.Section("server").Key("SSH_PORT").SetValue(fmt.Sprint(form.SSHPort))
-       }
-
-       if form.LFSRootPath != "" {
-               cfg.Section("server").Key("LFS_START_SERVER").SetValue("true")
-               cfg.Section("server").Key("LFS_CONTENT_PATH").SetValue(form.LFSRootPath)
-               var secretKey string
-               if secretKey, err = generate.NewJwtSecret(); err != nil {
-                       ctx.RenderWithErr(ctx.Tr("install.lfs_jwt_secret_failed", err), tplInstall, &form)
-                       return
-               }
-               cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(secretKey)
-       } else {
-               cfg.Section("server").Key("LFS_START_SERVER").SetValue("false")
-       }
-
-       if len(strings.TrimSpace(form.SMTPHost)) > 0 {
-               cfg.Section("mailer").Key("ENABLED").SetValue("true")
-               cfg.Section("mailer").Key("HOST").SetValue(form.SMTPHost)
-               cfg.Section("mailer").Key("FROM").SetValue(form.SMTPFrom)
-               cfg.Section("mailer").Key("USER").SetValue(form.SMTPUser)
-               cfg.Section("mailer").Key("PASSWD").SetValue(form.SMTPPasswd)
-       } else {
-               cfg.Section("mailer").Key("ENABLED").SetValue("false")
-       }
-       cfg.Section("service").Key("REGISTER_EMAIL_CONFIRM").SetValue(fmt.Sprint(form.RegisterConfirm))
-       cfg.Section("service").Key("ENABLE_NOTIFY_MAIL").SetValue(fmt.Sprint(form.MailNotify))
-
-       cfg.Section("server").Key("OFFLINE_MODE").SetValue(fmt.Sprint(form.OfflineMode))
-       cfg.Section("picture").Key("DISABLE_GRAVATAR").SetValue(fmt.Sprint(form.DisableGravatar))
-       cfg.Section("picture").Key("ENABLE_FEDERATED_AVATAR").SetValue(fmt.Sprint(form.EnableFederatedAvatar))
-       cfg.Section("openid").Key("ENABLE_OPENID_SIGNIN").SetValue(fmt.Sprint(form.EnableOpenIDSignIn))
-       cfg.Section("openid").Key("ENABLE_OPENID_SIGNUP").SetValue(fmt.Sprint(form.EnableOpenIDSignUp))
-       cfg.Section("service").Key("DISABLE_REGISTRATION").SetValue(fmt.Sprint(form.DisableRegistration))
-       cfg.Section("service").Key("ALLOW_ONLY_EXTERNAL_REGISTRATION").SetValue(fmt.Sprint(form.AllowOnlyExternalRegistration))
-       cfg.Section("service").Key("ENABLE_CAPTCHA").SetValue(fmt.Sprint(form.EnableCaptcha))
-       cfg.Section("service").Key("REQUIRE_SIGNIN_VIEW").SetValue(fmt.Sprint(form.RequireSignInView))
-       cfg.Section("service").Key("DEFAULT_KEEP_EMAIL_PRIVATE").SetValue(fmt.Sprint(form.DefaultKeepEmailPrivate))
-       cfg.Section("service").Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").SetValue(fmt.Sprint(form.DefaultAllowCreateOrganization))
-       cfg.Section("service").Key("DEFAULT_ENABLE_TIMETRACKING").SetValue(fmt.Sprint(form.DefaultEnableTimetracking))
-       cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(fmt.Sprint(form.NoReplyAddress))
-
-       cfg.Section("").Key("RUN_MODE").SetValue("prod")
-
-       cfg.Section("session").Key("PROVIDER").SetValue("file")
-
-       cfg.Section("log").Key("MODE").SetValue("console")
-       cfg.Section("log").Key("LEVEL").SetValue(setting.LogLevel.String())
-       cfg.Section("log").Key("ROOT_PATH").SetValue(form.LogRootPath)
-       cfg.Section("log").Key("ROUTER").SetValue("console")
-
-       cfg.Section("security").Key("INSTALL_LOCK").SetValue("true")
-       var secretKey string
-       if secretKey, err = generate.NewSecretKey(); err != nil {
-               ctx.RenderWithErr(ctx.Tr("install.secret_key_failed", err), tplInstall, &form)
-               return
-       }
-       cfg.Section("security").Key("SECRET_KEY").SetValue(secretKey)
-       if len(form.PasswordAlgorithm) > 0 {
-               cfg.Section("security").Key("PASSWORD_HASH_ALGO").SetValue(form.PasswordAlgorithm)
-       }
-
-       err = os.MkdirAll(filepath.Dir(setting.CustomConf), os.ModePerm)
-       if err != nil {
-               ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
-               return
-       }
-
-       if err = cfg.SaveTo(setting.CustomConf); err != nil {
-               ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
-               return
-       }
-
-       // Re-read settings
-       PostInstallInit(ctx)
-
-       // Create admin account
-       if len(form.AdminName) > 0 {
-               u := &models.User{
-                       Name:     form.AdminName,
-                       Email:    form.AdminEmail,
-                       Passwd:   form.AdminPasswd,
-                       IsAdmin:  true,
-                       IsActive: true,
-               }
-               if err = models.CreateUser(u); err != nil {
-                       if !models.IsErrUserAlreadyExist(err) {
-                               setting.InstallLock = false
-                               ctx.Data["Err_AdminName"] = true
-                               ctx.Data["Err_AdminEmail"] = true
-                               ctx.RenderWithErr(ctx.Tr("install.invalid_admin_setting", err), tplInstall, &form)
-                               return
-                       }
-                       log.Info("Admin account already exist")
-                       u, _ = models.GetUserByName(u.Name)
-               }
-
-               days := 86400 * setting.LogInRememberDays
-               ctx.SetCookie(setting.CookieUserName, u.Name, days)
-
-               ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
-                       setting.CookieRememberName, u.Name, days)
-
-               // Auto-login for admin
-               if err = ctx.Session.Set("uid", u.ID); err != nil {
-                       ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
-                       return
-               }
-               if err = ctx.Session.Set("uname", u.Name); err != nil {
-                       ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
-                       return
-               }
-
-               if err = ctx.Session.Release(); err != nil {
-                       ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
-                       return
-               }
-       }
-
-       log.Info("First-time run install finished!")
-
-       ctx.Flash.Success(ctx.Tr("install.install_success"))
-
-       ctx.Header().Add("Refresh", "1; url="+setting.AppURL+"user/login")
-       ctx.HTML(http.StatusOK, tplPostInstall)
-
-       // Now get the http.Server from this request and shut it down
-       // NB: This is not our hammerable graceful shutdown this is http.Server.Shutdown
-       srv := ctx.Value(http.ServerContextKey).(*http.Server)
-       go func() {
-               if err := srv.Shutdown(graceful.GetManager().HammerContext()); err != nil {
-                       log.Error("Unable to shutdown the install server! Error: %v", err)
-               }
-       }()
-}
diff --git a/routers/install/install.go b/routers/install/install.go
new file mode 100644 (file)
index 0000000..a7040bc
--- /dev/null
@@ -0,0 +1,473 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2021 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 install
+
+import (
+       "fmt"
+       "net/http"
+       "os"
+       "os/exec"
+       "path/filepath"
+       "strings"
+       "time"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/generate"
+       "code.gitea.io/gitea/modules/graceful"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/templates"
+       "code.gitea.io/gitea/modules/translation"
+       "code.gitea.io/gitea/modules/user"
+       "code.gitea.io/gitea/modules/util"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/modules/web/middleware"
+       "code.gitea.io/gitea/services/forms"
+
+       "gitea.com/go-chi/session"
+       "gopkg.in/ini.v1"
+)
+
+const (
+       // tplInstall template for installation page
+       tplInstall     base.TplName = "install"
+       tplPostInstall base.TplName = "post-install"
+)
+
+// Init prepare for rendering installation page
+func Init(next http.Handler) http.Handler {
+       var rnd = templates.HTMLRenderer()
+
+       return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
+               if setting.InstallLock {
+                       resp.Header().Add("Refresh", "1; url="+setting.AppURL+"user/login")
+                       _ = rnd.HTML(resp, 200, string(tplPostInstall), nil)
+                       return
+               }
+               var locale = middleware.Locale(resp, req)
+               var startTime = time.Now()
+               var ctx = context.Context{
+                       Resp:    context.NewResponse(resp),
+                       Flash:   &middleware.Flash{},
+                       Locale:  locale,
+                       Render:  rnd,
+                       Session: session.GetSession(req),
+                       Data: map[string]interface{}{
+                               "Title":         locale.Tr("install.install"),
+                               "PageIsInstall": true,
+                               "DbOptions":     setting.SupportedDatabases,
+                               "i18n":          locale,
+                               "Language":      locale.Language(),
+                               "Lang":          locale.Language(),
+                               "AllLangs":      translation.AllLangs(),
+                               "CurrentURL":    setting.AppSubURL + req.URL.RequestURI(),
+                               "PageStartTime": startTime,
+                               "TmplLoadTimes": func() string {
+                                       return time.Since(startTime).String()
+                               },
+                               "PasswordHashAlgorithms": models.AvailableHashAlgorithms,
+                       },
+               }
+               for _, lang := range translation.AllLangs() {
+                       if lang.Lang == locale.Language() {
+                               ctx.Data["LangName"] = lang.Name
+                               break
+                       }
+               }
+               ctx.Req = context.WithContext(req, &ctx)
+               next.ServeHTTP(resp, ctx.Req)
+       })
+}
+
+// Install render installation page
+func Install(ctx *context.Context) {
+       form := forms.InstallForm{}
+
+       // Database settings
+       form.DbHost = setting.Database.Host
+       form.DbUser = setting.Database.User
+       form.DbPasswd = setting.Database.Passwd
+       form.DbName = setting.Database.Name
+       form.DbPath = setting.Database.Path
+       form.DbSchema = setting.Database.Schema
+       form.Charset = setting.Database.Charset
+
+       var curDBOption = "MySQL"
+       switch setting.Database.Type {
+       case "postgres":
+               curDBOption = "PostgreSQL"
+       case "mssql":
+               curDBOption = "MSSQL"
+       case "sqlite3":
+               if setting.EnableSQLite3 {
+                       curDBOption = "SQLite3"
+               }
+       }
+
+       ctx.Data["CurDbOption"] = curDBOption
+
+       // Application general settings
+       form.AppName = setting.AppName
+       form.RepoRootPath = setting.RepoRootPath
+       form.LFSRootPath = setting.LFS.Path
+
+       // Note(unknown): it's hard for Windows users change a running user,
+       //      so just use current one if config says default.
+       if setting.IsWindows && setting.RunUser == "git" {
+               form.RunUser = user.CurrentUsername()
+       } else {
+               form.RunUser = setting.RunUser
+       }
+
+       form.Domain = setting.Domain
+       form.SSHPort = setting.SSH.Port
+       form.HTTPPort = setting.HTTPPort
+       form.AppURL = setting.AppURL
+       form.LogRootPath = setting.LogRootPath
+
+       // E-mail service settings
+       if setting.MailService != nil {
+               form.SMTPHost = setting.MailService.Host
+               form.SMTPFrom = setting.MailService.From
+               form.SMTPUser = setting.MailService.User
+       }
+       form.RegisterConfirm = setting.Service.RegisterEmailConfirm
+       form.MailNotify = setting.Service.EnableNotifyMail
+
+       // Server and other services settings
+       form.OfflineMode = setting.OfflineMode
+       form.DisableGravatar = setting.DisableGravatar
+       form.EnableFederatedAvatar = setting.EnableFederatedAvatar
+       form.EnableOpenIDSignIn = setting.Service.EnableOpenIDSignIn
+       form.EnableOpenIDSignUp = setting.Service.EnableOpenIDSignUp
+       form.DisableRegistration = setting.Service.DisableRegistration
+       form.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration
+       form.EnableCaptcha = setting.Service.EnableCaptcha
+       form.RequireSignInView = setting.Service.RequireSignInView
+       form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
+       form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization
+       form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking
+       form.NoReplyAddress = setting.Service.NoReplyAddress
+       form.PasswordAlgorithm = setting.PasswordHashAlgo
+
+       middleware.AssignForm(form, ctx.Data)
+       ctx.HTML(http.StatusOK, tplInstall)
+}
+
+// SubmitInstall response for submit install items
+func SubmitInstall(ctx *context.Context) {
+       form := *web.GetForm(ctx).(*forms.InstallForm)
+       var err error
+       ctx.Data["CurDbOption"] = form.DbType
+
+       if ctx.HasError() {
+               if ctx.HasValue("Err_SMTPUser") {
+                       ctx.Data["Err_SMTP"] = true
+               }
+               if ctx.HasValue("Err_AdminName") ||
+                       ctx.HasValue("Err_AdminPasswd") ||
+                       ctx.HasValue("Err_AdminEmail") {
+                       ctx.Data["Err_Admin"] = true
+               }
+
+               ctx.HTML(http.StatusOK, tplInstall)
+               return
+       }
+
+       if _, err = exec.LookPath("git"); err != nil {
+               ctx.RenderWithErr(ctx.Tr("install.test_git_failed", err), tplInstall, &form)
+               return
+       }
+
+       // Pass basic check, now test configuration.
+       // Test database setting.
+
+       setting.Database.Type = setting.GetDBTypeByName(form.DbType)
+       setting.Database.Host = form.DbHost
+       setting.Database.User = form.DbUser
+       setting.Database.Passwd = form.DbPasswd
+       setting.Database.Name = form.DbName
+       setting.Database.Schema = form.DbSchema
+       setting.Database.SSLMode = form.SSLMode
+       setting.Database.Charset = form.Charset
+       setting.Database.Path = form.DbPath
+
+       setting.PasswordHashAlgo = form.PasswordAlgorithm
+
+       if (setting.Database.Type == "sqlite3") &&
+               len(setting.Database.Path) == 0 {
+               ctx.Data["Err_DbPath"] = true
+               ctx.RenderWithErr(ctx.Tr("install.err_empty_db_path"), tplInstall, &form)
+               return
+       }
+
+       // Set test engine.
+       if err = models.NewTestEngine(); err != nil {
+               if strings.Contains(err.Error(), `Unknown database type: sqlite3`) {
+                       ctx.Data["Err_DbType"] = true
+                       ctx.RenderWithErr(ctx.Tr("install.sqlite3_not_available", "https://docs.gitea.io/en-us/install-from-binary/"), tplInstall, &form)
+               } else {
+                       ctx.Data["Err_DbSetting"] = true
+                       ctx.RenderWithErr(ctx.Tr("install.invalid_db_setting", err), tplInstall, &form)
+               }
+               return
+       }
+
+       // Test repository root path.
+       form.RepoRootPath = strings.ReplaceAll(form.RepoRootPath, "\\", "/")
+       if err = os.MkdirAll(form.RepoRootPath, os.ModePerm); err != nil {
+               ctx.Data["Err_RepoRootPath"] = true
+               ctx.RenderWithErr(ctx.Tr("install.invalid_repo_path", err), tplInstall, &form)
+               return
+       }
+
+       // Test LFS root path if not empty, empty meaning disable LFS
+       if form.LFSRootPath != "" {
+               form.LFSRootPath = strings.ReplaceAll(form.LFSRootPath, "\\", "/")
+               if err := os.MkdirAll(form.LFSRootPath, os.ModePerm); err != nil {
+                       ctx.Data["Err_LFSRootPath"] = true
+                       ctx.RenderWithErr(ctx.Tr("install.invalid_lfs_path", err), tplInstall, &form)
+                       return
+               }
+       }
+
+       // Test log root path.
+       form.LogRootPath = strings.ReplaceAll(form.LogRootPath, "\\", "/")
+       if err = os.MkdirAll(form.LogRootPath, os.ModePerm); err != nil {
+               ctx.Data["Err_LogRootPath"] = true
+               ctx.RenderWithErr(ctx.Tr("install.invalid_log_root_path", err), tplInstall, &form)
+               return
+       }
+
+       currentUser, match := setting.IsRunUserMatchCurrentUser(form.RunUser)
+       if !match {
+               ctx.Data["Err_RunUser"] = true
+               ctx.RenderWithErr(ctx.Tr("install.run_user_not_match", form.RunUser, currentUser), tplInstall, &form)
+               return
+       }
+
+       // Check logic loophole between disable self-registration and no admin account.
+       if form.DisableRegistration && len(form.AdminName) == 0 {
+               ctx.Data["Err_Services"] = true
+               ctx.Data["Err_Admin"] = true
+               ctx.RenderWithErr(ctx.Tr("install.no_admin_and_disable_registration"), tplInstall, form)
+               return
+       }
+
+       // Check admin user creation
+       if len(form.AdminName) > 0 {
+               // Ensure AdminName is valid
+               if err := models.IsUsableUsername(form.AdminName); err != nil {
+                       ctx.Data["Err_Admin"] = true
+                       ctx.Data["Err_AdminName"] = true
+                       if models.IsErrNameReserved(err) {
+                               ctx.RenderWithErr(ctx.Tr("install.err_admin_name_is_reserved"), tplInstall, form)
+                               return
+                       } else if models.IsErrNamePatternNotAllowed(err) {
+                               ctx.RenderWithErr(ctx.Tr("install.err_admin_name_pattern_not_allowed"), tplInstall, form)
+                               return
+                       }
+                       ctx.RenderWithErr(ctx.Tr("install.err_admin_name_is_invalid"), tplInstall, form)
+                       return
+               }
+               // Check Admin email
+               if len(form.AdminEmail) == 0 {
+                       ctx.Data["Err_Admin"] = true
+                       ctx.Data["Err_AdminEmail"] = true
+                       ctx.RenderWithErr(ctx.Tr("install.err_empty_admin_email"), tplInstall, form)
+                       return
+               }
+               // Check admin password.
+               if len(form.AdminPasswd) == 0 {
+                       ctx.Data["Err_Admin"] = true
+                       ctx.Data["Err_AdminPasswd"] = true
+                       ctx.RenderWithErr(ctx.Tr("install.err_empty_admin_password"), tplInstall, form)
+                       return
+               }
+               if form.AdminPasswd != form.AdminConfirmPasswd {
+                       ctx.Data["Err_Admin"] = true
+                       ctx.Data["Err_AdminPasswd"] = true
+                       ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplInstall, form)
+                       return
+               }
+       }
+
+       if form.AppURL[len(form.AppURL)-1] != '/' {
+               form.AppURL += "/"
+       }
+
+       // Save settings.
+       cfg := ini.Empty()
+       isFile, err := util.IsFile(setting.CustomConf)
+       if err != nil {
+               log.Error("Unable to check if %s is a file. Error: %v", setting.CustomConf, err)
+       }
+       if isFile {
+               // Keeps custom settings if there is already something.
+               if err = cfg.Append(setting.CustomConf); err != nil {
+                       log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err)
+               }
+       }
+       cfg.Section("database").Key("DB_TYPE").SetValue(setting.Database.Type)
+       cfg.Section("database").Key("HOST").SetValue(setting.Database.Host)
+       cfg.Section("database").Key("NAME").SetValue(setting.Database.Name)
+       cfg.Section("database").Key("USER").SetValue(setting.Database.User)
+       cfg.Section("database").Key("PASSWD").SetValue(setting.Database.Passwd)
+       cfg.Section("database").Key("SCHEMA").SetValue(setting.Database.Schema)
+       cfg.Section("database").Key("SSL_MODE").SetValue(setting.Database.SSLMode)
+       cfg.Section("database").Key("CHARSET").SetValue(setting.Database.Charset)
+       cfg.Section("database").Key("PATH").SetValue(setting.Database.Path)
+       cfg.Section("database").Key("LOG_SQL").SetValue("false") // LOG_SQL is rarely helpful
+
+       cfg.Section("").Key("APP_NAME").SetValue(form.AppName)
+       cfg.Section("repository").Key("ROOT").SetValue(form.RepoRootPath)
+       cfg.Section("").Key("RUN_USER").SetValue(form.RunUser)
+       cfg.Section("server").Key("SSH_DOMAIN").SetValue(form.Domain)
+       cfg.Section("server").Key("DOMAIN").SetValue(form.Domain)
+       cfg.Section("server").Key("HTTP_PORT").SetValue(form.HTTPPort)
+       cfg.Section("server").Key("ROOT_URL").SetValue(form.AppURL)
+
+       if form.SSHPort == 0 {
+               cfg.Section("server").Key("DISABLE_SSH").SetValue("true")
+       } else {
+               cfg.Section("server").Key("DISABLE_SSH").SetValue("false")
+               cfg.Section("server").Key("SSH_PORT").SetValue(fmt.Sprint(form.SSHPort))
+       }
+
+       if form.LFSRootPath != "" {
+               cfg.Section("server").Key("LFS_START_SERVER").SetValue("true")
+               cfg.Section("server").Key("LFS_CONTENT_PATH").SetValue(form.LFSRootPath)
+               var secretKey string
+               if secretKey, err = generate.NewJwtSecret(); err != nil {
+                       ctx.RenderWithErr(ctx.Tr("install.lfs_jwt_secret_failed", err), tplInstall, &form)
+                       return
+               }
+               cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(secretKey)
+       } else {
+               cfg.Section("server").Key("LFS_START_SERVER").SetValue("false")
+       }
+
+       if len(strings.TrimSpace(form.SMTPHost)) > 0 {
+               cfg.Section("mailer").Key("ENABLED").SetValue("true")
+               cfg.Section("mailer").Key("HOST").SetValue(form.SMTPHost)
+               cfg.Section("mailer").Key("FROM").SetValue(form.SMTPFrom)
+               cfg.Section("mailer").Key("USER").SetValue(form.SMTPUser)
+               cfg.Section("mailer").Key("PASSWD").SetValue(form.SMTPPasswd)
+       } else {
+               cfg.Section("mailer").Key("ENABLED").SetValue("false")
+       }
+       cfg.Section("service").Key("REGISTER_EMAIL_CONFIRM").SetValue(fmt.Sprint(form.RegisterConfirm))
+       cfg.Section("service").Key("ENABLE_NOTIFY_MAIL").SetValue(fmt.Sprint(form.MailNotify))
+
+       cfg.Section("server").Key("OFFLINE_MODE").SetValue(fmt.Sprint(form.OfflineMode))
+       cfg.Section("picture").Key("DISABLE_GRAVATAR").SetValue(fmt.Sprint(form.DisableGravatar))
+       cfg.Section("picture").Key("ENABLE_FEDERATED_AVATAR").SetValue(fmt.Sprint(form.EnableFederatedAvatar))
+       cfg.Section("openid").Key("ENABLE_OPENID_SIGNIN").SetValue(fmt.Sprint(form.EnableOpenIDSignIn))
+       cfg.Section("openid").Key("ENABLE_OPENID_SIGNUP").SetValue(fmt.Sprint(form.EnableOpenIDSignUp))
+       cfg.Section("service").Key("DISABLE_REGISTRATION").SetValue(fmt.Sprint(form.DisableRegistration))
+       cfg.Section("service").Key("ALLOW_ONLY_EXTERNAL_REGISTRATION").SetValue(fmt.Sprint(form.AllowOnlyExternalRegistration))
+       cfg.Section("service").Key("ENABLE_CAPTCHA").SetValue(fmt.Sprint(form.EnableCaptcha))
+       cfg.Section("service").Key("REQUIRE_SIGNIN_VIEW").SetValue(fmt.Sprint(form.RequireSignInView))
+       cfg.Section("service").Key("DEFAULT_KEEP_EMAIL_PRIVATE").SetValue(fmt.Sprint(form.DefaultKeepEmailPrivate))
+       cfg.Section("service").Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").SetValue(fmt.Sprint(form.DefaultAllowCreateOrganization))
+       cfg.Section("service").Key("DEFAULT_ENABLE_TIMETRACKING").SetValue(fmt.Sprint(form.DefaultEnableTimetracking))
+       cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(fmt.Sprint(form.NoReplyAddress))
+
+       cfg.Section("").Key("RUN_MODE").SetValue("prod")
+
+       cfg.Section("session").Key("PROVIDER").SetValue("file")
+
+       cfg.Section("log").Key("MODE").SetValue("console")
+       cfg.Section("log").Key("LEVEL").SetValue(setting.LogLevel.String())
+       cfg.Section("log").Key("ROOT_PATH").SetValue(form.LogRootPath)
+       cfg.Section("log").Key("ROUTER").SetValue("console")
+
+       cfg.Section("security").Key("INSTALL_LOCK").SetValue("true")
+       var secretKey string
+       if secretKey, err = generate.NewSecretKey(); err != nil {
+               ctx.RenderWithErr(ctx.Tr("install.secret_key_failed", err), tplInstall, &form)
+               return
+       }
+       cfg.Section("security").Key("SECRET_KEY").SetValue(secretKey)
+       if len(form.PasswordAlgorithm) > 0 {
+               cfg.Section("security").Key("PASSWORD_HASH_ALGO").SetValue(form.PasswordAlgorithm)
+       }
+
+       err = os.MkdirAll(filepath.Dir(setting.CustomConf), os.ModePerm)
+       if err != nil {
+               ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
+               return
+       }
+
+       if err = cfg.SaveTo(setting.CustomConf); err != nil {
+               ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
+               return
+       }
+
+       // Re-read settings
+       ReloadSettings(ctx)
+
+       // Create admin account
+       if len(form.AdminName) > 0 {
+               u := &models.User{
+                       Name:     form.AdminName,
+                       Email:    form.AdminEmail,
+                       Passwd:   form.AdminPasswd,
+                       IsAdmin:  true,
+                       IsActive: true,
+               }
+               if err = models.CreateUser(u); err != nil {
+                       if !models.IsErrUserAlreadyExist(err) {
+                               setting.InstallLock = false
+                               ctx.Data["Err_AdminName"] = true
+                               ctx.Data["Err_AdminEmail"] = true
+                               ctx.RenderWithErr(ctx.Tr("install.invalid_admin_setting", err), tplInstall, &form)
+                               return
+                       }
+                       log.Info("Admin account already exist")
+                       u, _ = models.GetUserByName(u.Name)
+               }
+
+               days := 86400 * setting.LogInRememberDays
+               ctx.SetCookie(setting.CookieUserName, u.Name, days)
+
+               ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
+                       setting.CookieRememberName, u.Name, days)
+
+               // Auto-login for admin
+               if err = ctx.Session.Set("uid", u.ID); err != nil {
+                       ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
+                       return
+               }
+               if err = ctx.Session.Set("uname", u.Name); err != nil {
+                       ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
+                       return
+               }
+
+               if err = ctx.Session.Release(); err != nil {
+                       ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
+                       return
+               }
+       }
+
+       log.Info("First-time run install finished!")
+
+       ctx.Flash.Success(ctx.Tr("install.install_success"))
+
+       ctx.Header().Add("Refresh", "1; url="+setting.AppURL+"user/login")
+       ctx.HTML(http.StatusOK, tplPostInstall)
+
+       // Now get the http.Server from this request and shut it down
+       // NB: This is not our hammerable graceful shutdown this is http.Server.Shutdown
+       srv := ctx.Value(http.ServerContextKey).(*http.Server)
+       go func() {
+               if err := srv.Shutdown(graceful.GetManager().HammerContext()); err != nil {
+                       log.Error("Unable to shutdown the install server! Error: %v", err)
+               }
+       }()
+}
diff --git a/routers/install/routes.go b/routers/install/routes.go
new file mode 100644 (file)
index 0000000..36130d4
--- /dev/null
@@ -0,0 +1,113 @@
+// 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 install
+
+import (
+       "fmt"
+       "net/http"
+       "path"
+
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/public"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/templates"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/modules/web/middleware"
+       "code.gitea.io/gitea/routers/common"
+       "code.gitea.io/gitea/services/forms"
+
+       "gitea.com/go-chi/session"
+)
+
+type dataStore map[string]interface{}
+
+func (d *dataStore) GetData() map[string]interface{} {
+       return *d
+}
+
+func installRecovery() func(next http.Handler) http.Handler {
+       var rnd = templates.HTMLRenderer()
+       return func(next http.Handler) http.Handler {
+               return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+                       defer func() {
+                               // Why we need this? The first recover will try to render a beautiful
+                               // error page for user, but the process can still panic again, then
+                               // we have to just recover twice and send a simple error page that
+                               // should not panic any more.
+                               defer func() {
+                                       if err := recover(); err != nil {
+                                               combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, string(log.Stack(2)))
+                                               log.Error(combinedErr)
+                                               if setting.IsProd() {
+                                                       http.Error(w, http.StatusText(500), 500)
+                                               } else {
+                                                       http.Error(w, combinedErr, 500)
+                                               }
+                                       }
+                               }()
+
+                               if err := recover(); err != nil {
+                                       combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, string(log.Stack(2)))
+                                       log.Error("%v", combinedErr)
+
+                                       lc := middleware.Locale(w, req)
+                                       var store = dataStore{
+                                               "Language":       lc.Language(),
+                                               "CurrentURL":     setting.AppSubURL + req.URL.RequestURI(),
+                                               "i18n":           lc,
+                                               "SignedUserID":   int64(0),
+                                               "SignedUserName": "",
+                                       }
+
+                                       w.Header().Set(`X-Frame-Options`, `SAMEORIGIN`)
+
+                                       if !setting.IsProd() {
+                                               store["ErrorMsg"] = combinedErr
+                                       }
+                                       err = rnd.HTML(w, 500, "status/500", templates.BaseVars().Merge(store))
+                                       if err != nil {
+                                               log.Error("%v", err)
+                                       }
+                               }
+                       }()
+
+                       next.ServeHTTP(w, req)
+               })
+       }
+}
+
+// Routes registers the install routes
+func Routes() *web.Route {
+       r := web.NewRoute()
+       for _, middle := range common.Middlewares() {
+               r.Use(middle)
+       }
+
+       r.Use(public.AssetsHandler(&public.Options{
+               Directory: path.Join(setting.StaticRootPath, "public"),
+               Prefix:    "/assets",
+       }))
+
+       r.Use(session.Sessioner(session.Options{
+               Provider:       setting.SessionConfig.Provider,
+               ProviderConfig: setting.SessionConfig.ProviderConfig,
+               CookieName:     setting.SessionConfig.CookieName,
+               CookiePath:     setting.SessionConfig.CookiePath,
+               Gclifetime:     setting.SessionConfig.Gclifetime,
+               Maxlifetime:    setting.SessionConfig.Maxlifetime,
+               Secure:         setting.SessionConfig.Secure,
+               SameSite:       setting.SessionConfig.SameSite,
+               Domain:         setting.SessionConfig.Domain,
+       }))
+
+       r.Use(installRecovery())
+       r.Use(Init)
+       r.Get("/", Install)
+       r.Post("/", web.Bind(forms.InstallForm{}), SubmitInstall)
+       r.NotFound(func(w http.ResponseWriter, req *http.Request) {
+               http.Redirect(w, req, setting.AppURL, http.StatusFound)
+       })
+       return r
+}
diff --git a/routers/install/setting.go b/routers/install/setting.go
new file mode 100644 (file)
index 0000000..50bb6aa
--- /dev/null
@@ -0,0 +1,49 @@
+// Copyright 2021 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 install
+
+import (
+       "context"
+
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/svg"
+       "code.gitea.io/gitea/modules/translation"
+       "code.gitea.io/gitea/routers/common"
+)
+
+// PreloadSettings preloads the configuration to check if we need to run install
+func PreloadSettings(ctx context.Context) bool {
+       setting.NewContext()
+       if !setting.InstallLock {
+               log.Trace("AppPath: %s", setting.AppPath)
+               log.Trace("AppWorkPath: %s", setting.AppWorkPath)
+               log.Trace("Custom path: %s", setting.CustomPath)
+               log.Trace("Log path: %s", setting.LogRootPath)
+               log.Trace("Preparing to run install page")
+               translation.InitLocales()
+               if setting.EnableSQLite3 {
+                       log.Info("SQLite3 Supported")
+               }
+               setting.InitDBConfig()
+               svg.Init()
+       }
+
+       return !setting.InstallLock
+}
+
+// ReloadSettings rereads the settings and starts up the database
+func ReloadSettings(ctx context.Context) {
+       setting.NewContext()
+       setting.InitDBConfig()
+       if setting.InstallLock {
+               if err := common.InitDBEngine(ctx); err == nil {
+                       log.Info("ORM engine initialization successful!")
+               } else {
+                       log.Fatal("ORM engine initialization failed: %v", err)
+               }
+               svg.Init()
+       }
+}
diff --git a/routers/metrics.go b/routers/metrics.go
deleted file mode 100644 (file)
index db2fb8d..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright 2018 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 routers
-
-import (
-       "crypto/subtle"
-       "net/http"
-
-       "code.gitea.io/gitea/modules/setting"
-
-       "github.com/prometheus/client_golang/prometheus/promhttp"
-)
-
-// Metrics validate auth token and render prometheus metrics
-func Metrics(resp http.ResponseWriter, req *http.Request) {
-       if setting.Metrics.Token == "" {
-               promhttp.Handler().ServeHTTP(resp, req)
-               return
-       }
-       header := req.Header.Get("Authorization")
-       if header == "" {
-               http.Error(resp, "", 401)
-               return
-       }
-       got := []byte(header)
-       want := []byte("Bearer " + setting.Metrics.Token)
-       if subtle.ConstantTimeCompare(got, want) != 1 {
-               http.Error(resp, "", 401)
-               return
-       }
-       promhttp.Handler().ServeHTTP(resp, req)
-}
diff --git a/routers/org/home.go b/routers/org/home.go
deleted file mode 100644 (file)
index d84ae87..0000000
+++ /dev/null
@@ -1,151 +0,0 @@
-// Copyright 2019 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 org
-
-import (
-       "net/http"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/markup"
-       "code.gitea.io/gitea/modules/markup/markdown"
-       "code.gitea.io/gitea/modules/setting"
-)
-
-const (
-       tplOrgHome base.TplName = "org/home"
-)
-
-// Home show organization home page
-func Home(ctx *context.Context) {
-       ctx.SetParams(":org", ctx.Params(":username"))
-       context.HandleOrgAssignment(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       org := ctx.Org.Organization
-
-       if !models.HasOrgVisible(org, ctx.User) {
-               ctx.NotFound("HasOrgVisible", nil)
-               return
-       }
-
-       ctx.Data["PageIsUserProfile"] = true
-       ctx.Data["Title"] = org.DisplayName()
-       if len(org.Description) != 0 {
-               desc, err := markdown.RenderString(&markup.RenderContext{
-                       URLPrefix: ctx.Repo.RepoLink,
-                       Metas:     map[string]string{"mode": "document"},
-               }, org.Description)
-               if err != nil {
-                       ctx.ServerError("RenderString", err)
-                       return
-               }
-               ctx.Data["RenderedDescription"] = desc
-       }
-
-       var orderBy models.SearchOrderBy
-       ctx.Data["SortType"] = ctx.Query("sort")
-       switch ctx.Query("sort") {
-       case "newest":
-               orderBy = models.SearchOrderByNewest
-       case "oldest":
-               orderBy = models.SearchOrderByOldest
-       case "recentupdate":
-               orderBy = models.SearchOrderByRecentUpdated
-       case "leastupdate":
-               orderBy = models.SearchOrderByLeastUpdated
-       case "reversealphabetically":
-               orderBy = models.SearchOrderByAlphabeticallyReverse
-       case "alphabetically":
-               orderBy = models.SearchOrderByAlphabetically
-       case "moststars":
-               orderBy = models.SearchOrderByStarsReverse
-       case "feweststars":
-               orderBy = models.SearchOrderByStars
-       case "mostforks":
-               orderBy = models.SearchOrderByForksReverse
-       case "fewestforks":
-               orderBy = models.SearchOrderByForks
-       default:
-               ctx.Data["SortType"] = "recentupdate"
-               orderBy = models.SearchOrderByRecentUpdated
-       }
-
-       keyword := strings.Trim(ctx.Query("q"), " ")
-       ctx.Data["Keyword"] = keyword
-
-       page := ctx.QueryInt("page")
-       if page <= 0 {
-               page = 1
-       }
-
-       var (
-               repos []*models.Repository
-               count int64
-               err   error
-       )
-       repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
-               ListOptions: models.ListOptions{
-                       PageSize: setting.UI.User.RepoPagingNum,
-                       Page:     page,
-               },
-               Keyword:            keyword,
-               OwnerID:            org.ID,
-               OrderBy:            orderBy,
-               Private:            ctx.IsSigned,
-               Actor:              ctx.User,
-               IncludeDescription: setting.UI.SearchRepoDescription,
-       })
-       if err != nil {
-               ctx.ServerError("SearchRepository", err)
-               return
-       }
-
-       var opts = models.FindOrgMembersOpts{
-               OrgID:       org.ID,
-               PublicOnly:  true,
-               ListOptions: models.ListOptions{Page: 1, PageSize: 25},
-       }
-
-       if ctx.User != nil {
-               isMember, err := org.IsOrgMember(ctx.User.ID)
-               if err != nil {
-                       ctx.Error(http.StatusInternalServerError, "IsOrgMember")
-                       return
-               }
-               opts.PublicOnly = !isMember && !ctx.User.IsAdmin
-       }
-
-       members, _, err := models.FindOrgMembers(&opts)
-       if err != nil {
-               ctx.ServerError("FindOrgMembers", err)
-               return
-       }
-
-       membersCount, err := models.CountOrgMembers(opts)
-       if err != nil {
-               ctx.ServerError("CountOrgMembers", err)
-               return
-       }
-
-       ctx.Data["Owner"] = org
-       ctx.Data["Repos"] = repos
-       ctx.Data["Total"] = count
-       ctx.Data["MembersTotal"] = membersCount
-       ctx.Data["Members"] = members
-       ctx.Data["Teams"] = org.Teams
-
-       ctx.Data["DisabledMirrors"] = setting.Repository.DisableMirrors
-
-       pager := context.NewPagination(int(count), setting.UI.User.RepoPagingNum, page, 5)
-       pager.SetDefaultParams(ctx)
-       ctx.Data["Page"] = pager
-
-       ctx.HTML(http.StatusOK, tplOrgHome)
-}
diff --git a/routers/org/members.go b/routers/org/members.go
deleted file mode 100644 (file)
index 934529d..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2020 The Gitea Authors.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-package org
-
-import (
-       "net/http"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-)
-
-const (
-       // tplMembers template for organization members page
-       tplMembers base.TplName = "org/member/members"
-)
-
-// Members render organization users page
-func Members(ctx *context.Context) {
-       org := ctx.Org.Organization
-       ctx.Data["Title"] = org.FullName
-       ctx.Data["PageIsOrgMembers"] = true
-
-       page := ctx.QueryInt("page")
-       if page <= 1 {
-               page = 1
-       }
-
-       var opts = models.FindOrgMembersOpts{
-               OrgID:      org.ID,
-               PublicOnly: true,
-       }
-
-       if ctx.User != nil {
-               isMember, err := ctx.Org.Organization.IsOrgMember(ctx.User.ID)
-               if err != nil {
-                       ctx.Error(http.StatusInternalServerError, "IsOrgMember")
-                       return
-               }
-               opts.PublicOnly = !isMember && !ctx.User.IsAdmin
-       }
-
-       total, err := models.CountOrgMembers(opts)
-       if err != nil {
-               ctx.Error(http.StatusInternalServerError, "CountOrgMembers")
-               return
-       }
-
-       pager := context.NewPagination(int(total), setting.UI.MembersPagingNum, page, 5)
-       opts.ListOptions.Page = page
-       opts.ListOptions.PageSize = setting.UI.MembersPagingNum
-       members, membersIsPublic, err := models.FindOrgMembers(&opts)
-       if err != nil {
-               ctx.ServerError("GetMembers", err)
-               return
-       }
-       ctx.Data["Page"] = pager
-       ctx.Data["Members"] = members
-       ctx.Data["MembersIsPublicMember"] = membersIsPublic
-       ctx.Data["MembersIsUserOrgOwner"] = members.IsUserOrgOwner(org.ID)
-       ctx.Data["MembersTwoFaStatus"] = members.GetTwoFaStatus()
-
-       ctx.HTML(http.StatusOK, tplMembers)
-}
-
-// MembersAction response for operation to a member of organization
-func MembersAction(ctx *context.Context) {
-       uid := ctx.QueryInt64("uid")
-       if uid == 0 {
-               ctx.Redirect(ctx.Org.OrgLink + "/members")
-               return
-       }
-
-       org := ctx.Org.Organization
-       var err error
-       switch ctx.Params(":action") {
-       case "private":
-               if ctx.User.ID != uid && !ctx.Org.IsOwner {
-                       ctx.Error(http.StatusNotFound)
-                       return
-               }
-               err = models.ChangeOrgUserStatus(org.ID, uid, false)
-       case "public":
-               if ctx.User.ID != uid && !ctx.Org.IsOwner {
-                       ctx.Error(http.StatusNotFound)
-                       return
-               }
-               err = models.ChangeOrgUserStatus(org.ID, uid, true)
-       case "remove":
-               if !ctx.Org.IsOwner {
-                       ctx.Error(http.StatusNotFound)
-                       return
-               }
-               err = org.RemoveMember(uid)
-               if models.IsErrLastOrgOwner(err) {
-                       ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
-                       ctx.Redirect(ctx.Org.OrgLink + "/members")
-                       return
-               }
-       case "leave":
-               err = org.RemoveMember(ctx.User.ID)
-               if models.IsErrLastOrgOwner(err) {
-                       ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
-                       ctx.Redirect(ctx.Org.OrgLink + "/members")
-                       return
-               }
-       }
-
-       if err != nil {
-               log.Error("Action(%s): %v", ctx.Params(":action"), err)
-               ctx.JSON(http.StatusOK, map[string]interface{}{
-                       "ok":  false,
-                       "err": err.Error(),
-               })
-               return
-       }
-
-       if ctx.Params(":action") != "leave" {
-               ctx.Redirect(ctx.Org.OrgLink + "/members")
-       } else {
-               ctx.Redirect(setting.AppSubURL + "/")
-       }
-}
diff --git a/routers/org/org.go b/routers/org/org.go
deleted file mode 100644 (file)
index beba3da..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2018 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 org
-
-import (
-       "errors"
-       "net/http"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-)
-
-const (
-       // tplCreateOrg template path for create organization
-       tplCreateOrg base.TplName = "org/create"
-)
-
-// Create render the page for create organization
-func Create(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("new_org")
-       ctx.Data["DefaultOrgVisibilityMode"] = setting.Service.DefaultOrgVisibilityMode
-       if !ctx.User.CanCreateOrganization() {
-               ctx.ServerError("Not allowed", errors.New(ctx.Tr("org.form.create_org_not_allowed")))
-               return
-       }
-       ctx.HTML(http.StatusOK, tplCreateOrg)
-}
-
-// CreatePost response for create organization
-func CreatePost(ctx *context.Context) {
-       form := *web.GetForm(ctx).(*forms.CreateOrgForm)
-       ctx.Data["Title"] = ctx.Tr("new_org")
-
-       if !ctx.User.CanCreateOrganization() {
-               ctx.ServerError("Not allowed", errors.New(ctx.Tr("org.form.create_org_not_allowed")))
-               return
-       }
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplCreateOrg)
-               return
-       }
-
-       org := &models.User{
-               Name:                      form.OrgName,
-               IsActive:                  true,
-               Type:                      models.UserTypeOrganization,
-               Visibility:                form.Visibility,
-               RepoAdminChangeTeamAccess: form.RepoAdminChangeTeamAccess,
-       }
-
-       if err := models.CreateOrganization(org, ctx.User); err != nil {
-               ctx.Data["Err_OrgName"] = true
-               switch {
-               case models.IsErrUserAlreadyExist(err):
-                       ctx.RenderWithErr(ctx.Tr("form.org_name_been_taken"), tplCreateOrg, &form)
-               case models.IsErrNameReserved(err):
-                       ctx.RenderWithErr(ctx.Tr("org.form.name_reserved", err.(models.ErrNameReserved).Name), tplCreateOrg, &form)
-               case models.IsErrNamePatternNotAllowed(err):
-                       ctx.RenderWithErr(ctx.Tr("org.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplCreateOrg, &form)
-               case models.IsErrUserNotAllowedCreateOrg(err):
-                       ctx.RenderWithErr(ctx.Tr("org.form.create_org_not_allowed"), tplCreateOrg, &form)
-               default:
-                       ctx.ServerError("CreateOrganization", err)
-               }
-               return
-       }
-       log.Trace("Organization created: %s", org.Name)
-
-       ctx.Redirect(org.DashboardLink())
-}
diff --git a/routers/org/org_labels.go b/routers/org/org_labels.go
deleted file mode 100644 (file)
index 26e232b..0000000
+++ /dev/null
@@ -1,112 +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 org
-
-import (
-       "net/http"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-)
-
-// RetrieveLabels find all the labels of an organization
-func RetrieveLabels(ctx *context.Context) {
-       labels, err := models.GetLabelsByOrgID(ctx.Org.Organization.ID, ctx.Query("sort"), models.ListOptions{})
-       if err != nil {
-               ctx.ServerError("RetrieveLabels.GetLabels", err)
-               return
-       }
-       for _, l := range labels {
-               l.CalOpenIssues()
-       }
-       ctx.Data["Labels"] = labels
-       ctx.Data["NumLabels"] = len(labels)
-       ctx.Data["SortType"] = ctx.Query("sort")
-}
-
-// NewLabel create new label for organization
-func NewLabel(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.CreateLabelForm)
-       ctx.Data["Title"] = ctx.Tr("repo.labels")
-       ctx.Data["PageIsLabels"] = true
-
-       if ctx.HasError() {
-               ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
-               ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
-               return
-       }
-
-       l := &models.Label{
-               OrgID:       ctx.Org.Organization.ID,
-               Name:        form.Title,
-               Description: form.Description,
-               Color:       form.Color,
-       }
-       if err := models.NewLabel(l); err != nil {
-               ctx.ServerError("NewLabel", err)
-               return
-       }
-       ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
-}
-
-// UpdateLabel update a label's name and color
-func UpdateLabel(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.CreateLabelForm)
-       l, err := models.GetLabelInOrgByID(ctx.Org.Organization.ID, form.ID)
-       if err != nil {
-               switch {
-               case models.IsErrOrgLabelNotExist(err):
-                       ctx.Error(http.StatusNotFound)
-               default:
-                       ctx.ServerError("UpdateLabel", err)
-               }
-               return
-       }
-
-       l.Name = form.Title
-       l.Description = form.Description
-       l.Color = form.Color
-       if err := models.UpdateLabel(l); err != nil {
-               ctx.ServerError("UpdateLabel", err)
-               return
-       }
-       ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
-}
-
-// DeleteLabel delete a label
-func DeleteLabel(ctx *context.Context) {
-       if err := models.DeleteLabel(ctx.Org.Organization.ID, ctx.QueryInt64("id")); err != nil {
-               ctx.Flash.Error("DeleteLabel: " + err.Error())
-       } else {
-               ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": ctx.Org.OrgLink + "/settings/labels",
-       })
-}
-
-// InitializeLabels init labels for an organization
-func InitializeLabels(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.InitializeLabelsForm)
-       if ctx.HasError() {
-               ctx.Redirect(ctx.Repo.RepoLink + "/labels")
-               return
-       }
-
-       if err := models.InitializeLabels(models.DefaultDBContext(), ctx.Org.Organization.ID, form.TemplateName, true); err != nil {
-               if models.IsErrIssueLabelTemplateLoad(err) {
-                       originalErr := err.(models.ErrIssueLabelTemplateLoad).OriginalError
-                       ctx.Flash.Error(ctx.Tr("repo.issues.label_templates.fail_to_load_file", form.TemplateName, originalErr))
-                       ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
-                       return
-               }
-               ctx.ServerError("InitializeLabels", err)
-               return
-       }
-       ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
-}
diff --git a/routers/org/setting.go b/routers/org/setting.go
deleted file mode 100644 (file)
index 0e28a93..0000000
+++ /dev/null
@@ -1,219 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2019 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 org
-
-import (
-       "net/http"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/web"
-       userSetting "code.gitea.io/gitea/routers/user/setting"
-       "code.gitea.io/gitea/services/forms"
-)
-
-const (
-       // tplSettingsOptions template path for render settings
-       tplSettingsOptions base.TplName = "org/settings/options"
-       // tplSettingsDelete template path for render delete repository
-       tplSettingsDelete base.TplName = "org/settings/delete"
-       // tplSettingsHooks template path for render hook settings
-       tplSettingsHooks base.TplName = "org/settings/hooks"
-       // tplSettingsLabels template path for render labels settings
-       tplSettingsLabels base.TplName = "org/settings/labels"
-)
-
-// Settings render the main settings page
-func Settings(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("org.settings")
-       ctx.Data["PageIsSettingsOptions"] = true
-       ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility
-       ctx.Data["RepoAdminChangeTeamAccess"] = ctx.Org.Organization.RepoAdminChangeTeamAccess
-       ctx.HTML(http.StatusOK, tplSettingsOptions)
-}
-
-// SettingsPost response for settings change submited
-func SettingsPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.UpdateOrgSettingForm)
-       ctx.Data["Title"] = ctx.Tr("org.settings")
-       ctx.Data["PageIsSettingsOptions"] = true
-       ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplSettingsOptions)
-               return
-       }
-
-       org := ctx.Org.Organization
-       nameChanged := org.Name != form.Name
-
-       // Check if organization name has been changed.
-       if org.LowerName != strings.ToLower(form.Name) {
-               isExist, err := models.IsUserExist(org.ID, form.Name)
-               if err != nil {
-                       ctx.ServerError("IsUserExist", err)
-                       return
-               } else if isExist {
-                       ctx.Data["OrgName"] = true
-                       ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form)
-                       return
-               } else if err = models.ChangeUserName(org, form.Name); err != nil {
-                       if err == models.ErrUserNameIllegal {
-                               ctx.Data["OrgName"] = true
-                               ctx.RenderWithErr(ctx.Tr("form.illegal_username"), tplSettingsOptions, &form)
-                       } else {
-                               ctx.ServerError("ChangeUserName", err)
-                       }
-                       return
-               }
-               // reset ctx.org.OrgLink with new name
-               ctx.Org.OrgLink = setting.AppSubURL + "/org/" + form.Name
-               log.Trace("Organization name changed: %s -> %s", org.Name, form.Name)
-               nameChanged = false
-       }
-
-       // In case it's just a case change.
-       org.Name = form.Name
-       org.LowerName = strings.ToLower(form.Name)
-
-       if ctx.User.IsAdmin {
-               org.MaxRepoCreation = form.MaxRepoCreation
-       }
-
-       org.FullName = form.FullName
-       org.Description = form.Description
-       org.Website = form.Website
-       org.Location = form.Location
-       org.RepoAdminChangeTeamAccess = form.RepoAdminChangeTeamAccess
-
-       visibilityChanged := form.Visibility != org.Visibility
-       org.Visibility = form.Visibility
-
-       if err := models.UpdateUser(org); err != nil {
-               ctx.ServerError("UpdateUser", err)
-               return
-       }
-
-       // update forks visibility
-       if visibilityChanged {
-               if err := org.GetRepositories(models.ListOptions{Page: 1, PageSize: org.NumRepos}); err != nil {
-                       ctx.ServerError("GetRepositories", err)
-                       return
-               }
-               for _, repo := range org.Repos {
-                       repo.OwnerName = org.Name
-                       if err := models.UpdateRepository(repo, true); err != nil {
-                               ctx.ServerError("UpdateRepository", err)
-                               return
-                       }
-               }
-       } else if nameChanged {
-               if err := models.UpdateRepositoryOwnerNames(org.ID, org.Name); err != nil {
-                       ctx.ServerError("UpdateRepository", err)
-                       return
-               }
-       }
-
-       log.Trace("Organization setting updated: %s", org.Name)
-       ctx.Flash.Success(ctx.Tr("org.settings.update_setting_success"))
-       ctx.Redirect(ctx.Org.OrgLink + "/settings")
-}
-
-// SettingsAvatar response for change avatar on settings page
-func SettingsAvatar(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.AvatarForm)
-       form.Source = forms.AvatarLocal
-       if err := userSetting.UpdateAvatarSetting(ctx, form, ctx.Org.Organization); err != nil {
-               ctx.Flash.Error(err.Error())
-       } else {
-               ctx.Flash.Success(ctx.Tr("org.settings.update_avatar_success"))
-       }
-
-       ctx.Redirect(ctx.Org.OrgLink + "/settings")
-}
-
-// SettingsDeleteAvatar response for delete avatar on setings page
-func SettingsDeleteAvatar(ctx *context.Context) {
-       if err := ctx.Org.Organization.DeleteAvatar(); err != nil {
-               ctx.Flash.Error(err.Error())
-       }
-
-       ctx.Redirect(ctx.Org.OrgLink + "/settings")
-}
-
-// SettingsDelete response for deleting an organization
-func SettingsDelete(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("org.settings")
-       ctx.Data["PageIsSettingsDelete"] = true
-
-       org := ctx.Org.Organization
-       if ctx.Req.Method == "POST" {
-               if org.Name != ctx.Query("org_name") {
-                       ctx.Data["Err_OrgName"] = true
-                       ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_org_name"), tplSettingsDelete, nil)
-                       return
-               }
-
-               if err := models.DeleteOrganization(org); err != nil {
-                       if models.IsErrUserOwnRepos(err) {
-                               ctx.Flash.Error(ctx.Tr("form.org_still_own_repo"))
-                               ctx.Redirect(ctx.Org.OrgLink + "/settings/delete")
-                       } else {
-                               ctx.ServerError("DeleteOrganization", err)
-                       }
-               } else {
-                       log.Trace("Organization deleted: %s", org.Name)
-                       ctx.Redirect(setting.AppSubURL + "/")
-               }
-               return
-       }
-
-       ctx.HTML(http.StatusOK, tplSettingsDelete)
-}
-
-// Webhooks render webhook list page
-func Webhooks(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("org.settings")
-       ctx.Data["PageIsSettingsHooks"] = true
-       ctx.Data["BaseLink"] = ctx.Org.OrgLink + "/settings/hooks"
-       ctx.Data["BaseLinkNew"] = ctx.Org.OrgLink + "/settings/hooks"
-       ctx.Data["Description"] = ctx.Tr("org.settings.hooks_desc")
-
-       ws, err := models.GetWebhooksByOrgID(ctx.Org.Organization.ID, models.ListOptions{})
-       if err != nil {
-               ctx.ServerError("GetWebhooksByOrgId", err)
-               return
-       }
-
-       ctx.Data["Webhooks"] = ws
-       ctx.HTML(http.StatusOK, tplSettingsHooks)
-}
-
-// DeleteWebhook response for delete webhook
-func DeleteWebhook(ctx *context.Context) {
-       if err := models.DeleteWebhookByOrgID(ctx.Org.Organization.ID, ctx.QueryInt64("id")); err != nil {
-               ctx.Flash.Error("DeleteWebhookByOrgID: " + err.Error())
-       } else {
-               ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": ctx.Org.OrgLink + "/settings/hooks",
-       })
-}
-
-// Labels render organization labels page
-func Labels(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.labels")
-       ctx.Data["PageIsOrgSettingsLabels"] = true
-       ctx.Data["RequireTribute"] = true
-       ctx.Data["LabelTemplates"] = models.LabelTemplates
-       ctx.HTML(http.StatusOK, tplSettingsLabels)
-}
diff --git a/routers/org/teams.go b/routers/org/teams.go
deleted file mode 100644 (file)
index e612cd7..0000000
+++ /dev/null
@@ -1,357 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2019 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 org
-
-import (
-       "net/http"
-       "path"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/routers/utils"
-       "code.gitea.io/gitea/services/forms"
-)
-
-const (
-       // tplTeams template path for teams list page
-       tplTeams base.TplName = "org/team/teams"
-       // tplTeamNew template path for create new team page
-       tplTeamNew base.TplName = "org/team/new"
-       // tplTeamMembers template path for showing team members page
-       tplTeamMembers base.TplName = "org/team/members"
-       // tplTeamRepositories template path for showing team repositories page
-       tplTeamRepositories base.TplName = "org/team/repositories"
-)
-
-// Teams render teams list page
-func Teams(ctx *context.Context) {
-       org := ctx.Org.Organization
-       ctx.Data["Title"] = org.FullName
-       ctx.Data["PageIsOrgTeams"] = true
-
-       for _, t := range org.Teams {
-               if err := t.GetMembers(&models.SearchMembersOptions{}); err != nil {
-                       ctx.ServerError("GetMembers", err)
-                       return
-               }
-       }
-       ctx.Data["Teams"] = org.Teams
-
-       ctx.HTML(http.StatusOK, tplTeams)
-}
-
-// TeamsAction response for join, leave, remove, add operations to team
-func TeamsAction(ctx *context.Context) {
-       uid := ctx.QueryInt64("uid")
-       if uid == 0 {
-               ctx.Redirect(ctx.Org.OrgLink + "/teams")
-               return
-       }
-
-       page := ctx.Query("page")
-       var err error
-       switch ctx.Params(":action") {
-       case "join":
-               if !ctx.Org.IsOwner {
-                       ctx.Error(http.StatusNotFound)
-                       return
-               }
-               err = ctx.Org.Team.AddMember(ctx.User.ID)
-       case "leave":
-               err = ctx.Org.Team.RemoveMember(ctx.User.ID)
-       case "remove":
-               if !ctx.Org.IsOwner {
-                       ctx.Error(http.StatusNotFound)
-                       return
-               }
-               err = ctx.Org.Team.RemoveMember(uid)
-               page = "team"
-       case "add":
-               if !ctx.Org.IsOwner {
-                       ctx.Error(http.StatusNotFound)
-                       return
-               }
-               uname := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.Query("uname")))
-               var u *models.User
-               u, err = models.GetUserByName(uname)
-               if err != nil {
-                       if models.IsErrUserNotExist(err) {
-                               ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
-                               ctx.Redirect(ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName)
-                       } else {
-                               ctx.ServerError(" GetUserByName", err)
-                       }
-                       return
-               }
-
-               if u.IsOrganization() {
-                       ctx.Flash.Error(ctx.Tr("form.cannot_add_org_to_team"))
-                       ctx.Redirect(ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName)
-                       return
-               }
-
-               if ctx.Org.Team.IsMember(u.ID) {
-                       ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users"))
-               } else {
-                       err = ctx.Org.Team.AddMember(u.ID)
-               }
-
-               page = "team"
-       }
-
-       if err != nil {
-               if models.IsErrLastOrgOwner(err) {
-                       ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
-               } else {
-                       log.Error("Action(%s): %v", ctx.Params(":action"), err)
-                       ctx.JSON(http.StatusOK, map[string]interface{}{
-                               "ok":  false,
-                               "err": err.Error(),
-                       })
-                       return
-               }
-       }
-
-       switch page {
-       case "team":
-               ctx.Redirect(ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName)
-       case "home":
-               ctx.Redirect(ctx.Org.Organization.HomeLink())
-       default:
-               ctx.Redirect(ctx.Org.OrgLink + "/teams")
-       }
-}
-
-// TeamsRepoAction operate team's repository
-func TeamsRepoAction(ctx *context.Context) {
-       if !ctx.Org.IsOwner {
-               ctx.Error(http.StatusNotFound)
-               return
-       }
-
-       var err error
-       action := ctx.Params(":action")
-       switch action {
-       case "add":
-               repoName := path.Base(ctx.Query("repo_name"))
-               var repo *models.Repository
-               repo, err = models.GetRepositoryByName(ctx.Org.Organization.ID, repoName)
-               if err != nil {
-                       if models.IsErrRepoNotExist(err) {
-                               ctx.Flash.Error(ctx.Tr("org.teams.add_nonexistent_repo"))
-                               ctx.Redirect(ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName + "/repositories")
-                               return
-                       }
-                       ctx.ServerError("GetRepositoryByName", err)
-                       return
-               }
-               err = ctx.Org.Team.AddRepository(repo)
-       case "remove":
-               err = ctx.Org.Team.RemoveRepository(ctx.QueryInt64("repoid"))
-       case "addall":
-               err = ctx.Org.Team.AddAllRepositories()
-       case "removeall":
-               err = ctx.Org.Team.RemoveAllRepositories()
-       }
-
-       if err != nil {
-               log.Error("Action(%s): '%s' %v", ctx.Params(":action"), ctx.Org.Team.Name, err)
-               ctx.ServerError("TeamsRepoAction", err)
-               return
-       }
-
-       if action == "addall" || action == "removeall" {
-               ctx.JSON(http.StatusOK, map[string]interface{}{
-                       "redirect": ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName + "/repositories",
-               })
-               return
-       }
-       ctx.Redirect(ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName + "/repositories")
-}
-
-// NewTeam render create new team page
-func NewTeam(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Org.Organization.FullName
-       ctx.Data["PageIsOrgTeams"] = true
-       ctx.Data["PageIsOrgTeamsNew"] = true
-       ctx.Data["Team"] = &models.Team{}
-       ctx.Data["Units"] = models.Units
-       ctx.HTML(http.StatusOK, tplTeamNew)
-}
-
-// NewTeamPost response for create new team
-func NewTeamPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.CreateTeamForm)
-       ctx.Data["Title"] = ctx.Org.Organization.FullName
-       ctx.Data["PageIsOrgTeams"] = true
-       ctx.Data["PageIsOrgTeamsNew"] = true
-       ctx.Data["Units"] = models.Units
-       var includesAllRepositories = form.RepoAccess == "all"
-
-       t := &models.Team{
-               OrgID:                   ctx.Org.Organization.ID,
-               Name:                    form.TeamName,
-               Description:             form.Description,
-               Authorize:               models.ParseAccessMode(form.Permission),
-               IncludesAllRepositories: includesAllRepositories,
-               CanCreateOrgRepo:        form.CanCreateOrgRepo,
-       }
-
-       if t.Authorize < models.AccessModeOwner {
-               var units = make([]*models.TeamUnit, 0, len(form.Units))
-               for _, tp := range form.Units {
-                       units = append(units, &models.TeamUnit{
-                               OrgID: ctx.Org.Organization.ID,
-                               Type:  tp,
-                       })
-               }
-               t.Units = units
-       }
-
-       ctx.Data["Team"] = t
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplTeamNew)
-               return
-       }
-
-       if t.Authorize < models.AccessModeAdmin && len(form.Units) == 0 {
-               ctx.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form)
-               return
-       }
-
-       if err := models.NewTeam(t); err != nil {
-               ctx.Data["Err_TeamName"] = true
-               switch {
-               case models.IsErrTeamAlreadyExist(err):
-                       ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form)
-               default:
-                       ctx.ServerError("NewTeam", err)
-               }
-               return
-       }
-       log.Trace("Team created: %s/%s", ctx.Org.Organization.Name, t.Name)
-       ctx.Redirect(ctx.Org.OrgLink + "/teams/" + t.LowerName)
-}
-
-// TeamMembers render team members page
-func TeamMembers(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Org.Team.Name
-       ctx.Data["PageIsOrgTeams"] = true
-       ctx.Data["PageIsOrgTeamMembers"] = true
-       if err := ctx.Org.Team.GetMembers(&models.SearchMembersOptions{}); err != nil {
-               ctx.ServerError("GetMembers", err)
-               return
-       }
-       ctx.HTML(http.StatusOK, tplTeamMembers)
-}
-
-// TeamRepositories show the repositories of team
-func TeamRepositories(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Org.Team.Name
-       ctx.Data["PageIsOrgTeams"] = true
-       ctx.Data["PageIsOrgTeamRepos"] = true
-       if err := ctx.Org.Team.GetRepositories(&models.SearchTeamOptions{}); err != nil {
-               ctx.ServerError("GetRepositories", err)
-               return
-       }
-       ctx.HTML(http.StatusOK, tplTeamRepositories)
-}
-
-// EditTeam render team edit page
-func EditTeam(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Org.Organization.FullName
-       ctx.Data["PageIsOrgTeams"] = true
-       ctx.Data["team_name"] = ctx.Org.Team.Name
-       ctx.Data["desc"] = ctx.Org.Team.Description
-       ctx.Data["Units"] = models.Units
-       ctx.HTML(http.StatusOK, tplTeamNew)
-}
-
-// EditTeamPost response for modify team information
-func EditTeamPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.CreateTeamForm)
-       t := ctx.Org.Team
-       ctx.Data["Title"] = ctx.Org.Organization.FullName
-       ctx.Data["PageIsOrgTeams"] = true
-       ctx.Data["Team"] = t
-       ctx.Data["Units"] = models.Units
-
-       isAuthChanged := false
-       isIncludeAllChanged := false
-       var includesAllRepositories = form.RepoAccess == "all"
-       if !t.IsOwnerTeam() {
-               // Validate permission level.
-               auth := models.ParseAccessMode(form.Permission)
-
-               t.Name = form.TeamName
-               if t.Authorize != auth {
-                       isAuthChanged = true
-                       t.Authorize = auth
-               }
-
-               if t.IncludesAllRepositories != includesAllRepositories {
-                       isIncludeAllChanged = true
-                       t.IncludesAllRepositories = includesAllRepositories
-               }
-       }
-       t.Description = form.Description
-       if t.Authorize < models.AccessModeOwner {
-               var units = make([]models.TeamUnit, 0, len(form.Units))
-               for _, tp := range form.Units {
-                       units = append(units, models.TeamUnit{
-                               OrgID:  t.OrgID,
-                               TeamID: t.ID,
-                               Type:   tp,
-                       })
-               }
-               err := models.UpdateTeamUnits(t, units)
-               if err != nil {
-                       ctx.Error(http.StatusInternalServerError, "LoadIssue", err.Error())
-                       return
-               }
-       }
-       t.CanCreateOrgRepo = form.CanCreateOrgRepo
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplTeamNew)
-               return
-       }
-
-       if t.Authorize < models.AccessModeAdmin && len(form.Units) == 0 {
-               ctx.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form)
-               return
-       }
-
-       if err := models.UpdateTeam(t, isAuthChanged, isIncludeAllChanged); err != nil {
-               ctx.Data["Err_TeamName"] = true
-               switch {
-               case models.IsErrTeamAlreadyExist(err):
-                       ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form)
-               default:
-                       ctx.ServerError("UpdateTeam", err)
-               }
-               return
-       }
-       ctx.Redirect(ctx.Org.OrgLink + "/teams/" + t.LowerName)
-}
-
-// DeleteTeam response for the delete team request
-func DeleteTeam(ctx *context.Context) {
-       if err := models.DeleteTeam(ctx.Org.Team); err != nil {
-               ctx.Flash.Error("DeleteTeam: " + err.Error())
-       } else {
-               ctx.Flash.Success(ctx.Tr("org.teams.delete_team_success"))
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": ctx.Org.OrgLink + "/teams",
-       })
-}
diff --git a/routers/repo/activity.go b/routers/repo/activity.go
deleted file mode 100644 (file)
index dcb7bf5..0000000
+++ /dev/null
@@ -1,103 +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 repo
-
-import (
-       "net/http"
-       "time"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-)
-
-const (
-       tplActivity base.TplName = "repo/activity"
-)
-
-// Activity render the page to show repository latest changes
-func Activity(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.activity")
-       ctx.Data["PageIsActivity"] = true
-
-       ctx.Data["Period"] = ctx.Params("period")
-
-       timeUntil := time.Now()
-       var timeFrom time.Time
-
-       switch ctx.Data["Period"] {
-       case "daily":
-               timeFrom = timeUntil.Add(-time.Hour * 24)
-       case "halfweekly":
-               timeFrom = timeUntil.Add(-time.Hour * 72)
-       case "weekly":
-               timeFrom = timeUntil.Add(-time.Hour * 168)
-       case "monthly":
-               timeFrom = timeUntil.AddDate(0, -1, 0)
-       case "quarterly":
-               timeFrom = timeUntil.AddDate(0, -3, 0)
-       case "semiyearly":
-               timeFrom = timeUntil.AddDate(0, -6, 0)
-       case "yearly":
-               timeFrom = timeUntil.AddDate(-1, 0, 0)
-       default:
-               ctx.Data["Period"] = "weekly"
-               timeFrom = timeUntil.Add(-time.Hour * 168)
-       }
-       ctx.Data["DateFrom"] = timeFrom.Format("January 2, 2006")
-       ctx.Data["DateUntil"] = timeUntil.Format("January 2, 2006")
-       ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + ctx.Data["Period"].(string))
-
-       var err error
-       if ctx.Data["Activity"], err = models.GetActivityStats(ctx.Repo.Repository, timeFrom,
-               ctx.Repo.CanRead(models.UnitTypeReleases),
-               ctx.Repo.CanRead(models.UnitTypeIssues),
-               ctx.Repo.CanRead(models.UnitTypePullRequests),
-               ctx.Repo.CanRead(models.UnitTypeCode)); err != nil {
-               ctx.ServerError("GetActivityStats", err)
-               return
-       }
-
-       if ctx.Data["ActivityTopAuthors"], err = models.GetActivityStatsTopAuthors(ctx.Repo.Repository, timeFrom, 10); err != nil {
-               ctx.ServerError("GetActivityStatsTopAuthors", err)
-               return
-       }
-
-       ctx.HTML(http.StatusOK, tplActivity)
-}
-
-// ActivityAuthors renders JSON with top commit authors for given time period over all branches
-func ActivityAuthors(ctx *context.Context) {
-       timeUntil := time.Now()
-       var timeFrom time.Time
-
-       switch ctx.Params("period") {
-       case "daily":
-               timeFrom = timeUntil.Add(-time.Hour * 24)
-       case "halfweekly":
-               timeFrom = timeUntil.Add(-time.Hour * 72)
-       case "weekly":
-               timeFrom = timeUntil.Add(-time.Hour * 168)
-       case "monthly":
-               timeFrom = timeUntil.AddDate(0, -1, 0)
-       case "quarterly":
-               timeFrom = timeUntil.AddDate(0, -3, 0)
-       case "semiyearly":
-               timeFrom = timeUntil.AddDate(0, -6, 0)
-       case "yearly":
-               timeFrom = timeUntil.AddDate(-1, 0, 0)
-       default:
-               timeFrom = timeUntil.Add(-time.Hour * 168)
-       }
-
-       var err error
-       authors, err := models.GetActivityStatsTopAuthors(ctx.Repo.Repository, timeFrom, 10)
-       if err != nil {
-               ctx.ServerError("GetActivityStatsTopAuthors", err)
-               return
-       }
-
-       ctx.JSON(http.StatusOK, authors)
-}
diff --git a/routers/repo/attachment.go b/routers/repo/attachment.go
deleted file mode 100644 (file)
index f53e745..0000000
+++ /dev/null
@@ -1,159 +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 repo
-
-import (
-       "fmt"
-       "net/http"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/httpcache"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/storage"
-       "code.gitea.io/gitea/modules/upload"
-)
-
-// UploadIssueAttachment response for Issue/PR attachments
-func UploadIssueAttachment(ctx *context.Context) {
-       uploadAttachment(ctx, setting.Attachment.AllowedTypes)
-}
-
-// UploadReleaseAttachment response for uploading release attachments
-func UploadReleaseAttachment(ctx *context.Context) {
-       uploadAttachment(ctx, setting.Repository.Release.AllowedTypes)
-}
-
-// UploadAttachment response for uploading attachments
-func uploadAttachment(ctx *context.Context, allowedTypes string) {
-       if !setting.Attachment.Enabled {
-               ctx.Error(http.StatusNotFound, "attachment is not enabled")
-               return
-       }
-
-       file, header, err := ctx.Req.FormFile("file")
-       if err != nil {
-               ctx.Error(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err))
-               return
-       }
-       defer file.Close()
-
-       buf := make([]byte, 1024)
-       n, _ := file.Read(buf)
-       if n > 0 {
-               buf = buf[:n]
-       }
-
-       err = upload.Verify(buf, header.Filename, allowedTypes)
-       if err != nil {
-               ctx.Error(http.StatusBadRequest, err.Error())
-               return
-       }
-
-       attach, err := models.NewAttachment(&models.Attachment{
-               UploaderID: ctx.User.ID,
-               Name:       header.Filename,
-       }, buf, file)
-       if err != nil {
-               ctx.Error(http.StatusInternalServerError, fmt.Sprintf("NewAttachment: %v", err))
-               return
-       }
-
-       log.Trace("New attachment uploaded: %s", attach.UUID)
-       ctx.JSON(http.StatusOK, map[string]string{
-               "uuid": attach.UUID,
-       })
-}
-
-// DeleteAttachment response for deleting issue's attachment
-func DeleteAttachment(ctx *context.Context) {
-       file := ctx.Query("file")
-       attach, err := models.GetAttachmentByUUID(file)
-       if err != nil {
-               ctx.Error(http.StatusBadRequest, err.Error())
-               return
-       }
-       if !ctx.IsSigned || (ctx.User.ID != attach.UploaderID) {
-               ctx.Error(http.StatusForbidden)
-               return
-       }
-       err = models.DeleteAttachment(attach, true)
-       if err != nil {
-               ctx.Error(http.StatusInternalServerError, fmt.Sprintf("DeleteAttachment: %v", err))
-               return
-       }
-       ctx.JSON(http.StatusOK, map[string]string{
-               "uuid": attach.UUID,
-       })
-}
-
-// GetAttachment serve attachements
-func GetAttachment(ctx *context.Context) {
-       attach, err := models.GetAttachmentByUUID(ctx.Params(":uuid"))
-       if err != nil {
-               if models.IsErrAttachmentNotExist(err) {
-                       ctx.Error(http.StatusNotFound)
-               } else {
-                       ctx.ServerError("GetAttachmentByUUID", err)
-               }
-               return
-       }
-
-       repository, unitType, err := attach.LinkedRepository()
-       if err != nil {
-               ctx.ServerError("LinkedRepository", err)
-               return
-       }
-
-       if repository == nil { //If not linked
-               if !(ctx.IsSigned && attach.UploaderID == ctx.User.ID) { //We block if not the uploader
-                       ctx.Error(http.StatusNotFound)
-                       return
-               }
-       } else { //If we have the repository we check access
-               perm, err := models.GetUserRepoPermission(repository, ctx.User)
-               if err != nil {
-                       ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err.Error())
-                       return
-               }
-               if !perm.CanRead(unitType) {
-                       ctx.Error(http.StatusNotFound)
-                       return
-               }
-       }
-
-       if err := attach.IncreaseDownloadCount(); err != nil {
-               ctx.ServerError("IncreaseDownloadCount", err)
-               return
-       }
-
-       if setting.Attachment.ServeDirect {
-               //If we have a signed url (S3, object storage), redirect to this directly.
-               u, err := storage.Attachments.URL(attach.RelativePath(), attach.Name)
-
-               if u != nil && err == nil {
-                       ctx.Redirect(u.String())
-                       return
-               }
-       }
-
-       if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+attach.UUID+`"`) {
-               return
-       }
-
-       //If we have matched and access to release or issue
-       fr, err := storage.Attachments.Open(attach.RelativePath())
-       if err != nil {
-               ctx.ServerError("Open", err)
-               return
-       }
-       defer fr.Close()
-
-       if err = ServeData(ctx, attach.Name, attach.Size, fr); err != nil {
-               ctx.ServerError("ServeData", err)
-               return
-       }
-}
diff --git a/routers/repo/blame.go b/routers/repo/blame.go
deleted file mode 100644 (file)
index 1a3e1dc..0000000
+++ /dev/null
@@ -1,251 +0,0 @@
-// Copyright 2019 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 repo
-
-import (
-       "bytes"
-       "container/list"
-       "fmt"
-       "html"
-       gotemplate "html/template"
-       "net/http"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/git"
-       "code.gitea.io/gitea/modules/highlight"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/templates"
-       "code.gitea.io/gitea/modules/timeutil"
-)
-
-const (
-       tplBlame base.TplName = "repo/home"
-)
-
-// RefBlame render blame page
-func RefBlame(ctx *context.Context) {
-       fileName := ctx.Repo.TreePath
-       if len(fileName) == 0 {
-               ctx.NotFound("Blame FileName", nil)
-               return
-       }
-
-       userName := ctx.Repo.Owner.Name
-       repoName := ctx.Repo.Repository.Name
-       commitID := ctx.Repo.CommitID
-
-       commit, err := ctx.Repo.GitRepo.GetCommit(commitID)
-       if err != nil {
-               if git.IsErrNotExist(err) {
-                       ctx.NotFound("Repo.GitRepo.GetCommit", err)
-               } else {
-                       ctx.ServerError("Repo.GitRepo.GetCommit", err)
-               }
-               return
-       }
-       if len(commitID) != 40 {
-               commitID = commit.ID.String()
-       }
-
-       branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
-       treeLink := branchLink
-       rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL()
-
-       if len(ctx.Repo.TreePath) > 0 {
-               treeLink += "/" + ctx.Repo.TreePath
-       }
-
-       var treeNames []string
-       paths := make([]string, 0, 5)
-       if len(ctx.Repo.TreePath) > 0 {
-               treeNames = strings.Split(ctx.Repo.TreePath, "/")
-               for i := range treeNames {
-                       paths = append(paths, strings.Join(treeNames[:i+1], "/"))
-               }
-
-               ctx.Data["HasParentPath"] = true
-               if len(paths)-2 >= 0 {
-                       ctx.Data["ParentPath"] = "/" + paths[len(paths)-1]
-               }
-       }
-
-       // Show latest commit info of repository in table header,
-       // or of directory if not in root directory.
-       latestCommit := ctx.Repo.Commit
-       if len(ctx.Repo.TreePath) > 0 {
-               latestCommit, err = ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath)
-               if err != nil {
-                       ctx.ServerError("GetCommitByPath", err)
-                       return
-               }
-       }
-       ctx.Data["LatestCommit"] = latestCommit
-       ctx.Data["LatestCommitVerification"] = models.ParseCommitWithSignature(latestCommit)
-       ctx.Data["LatestCommitUser"] = models.ValidateCommitWithEmail(latestCommit)
-
-       statuses, err := models.GetLatestCommitStatus(ctx.Repo.Repository.ID, ctx.Repo.Commit.ID.String(), models.ListOptions{})
-       if err != nil {
-               log.Error("GetLatestCommitStatus: %v", err)
-       }
-
-       // Get current entry user currently looking at.
-       entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
-       if err != nil {
-               ctx.NotFoundOrServerError("Repo.Commit.GetTreeEntryByPath", git.IsErrNotExist, err)
-               return
-       }
-
-       blob := entry.Blob()
-
-       ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(statuses)
-       ctx.Data["LatestCommitStatuses"] = statuses
-
-       ctx.Data["Paths"] = paths
-       ctx.Data["TreeLink"] = treeLink
-       ctx.Data["TreeNames"] = treeNames
-       ctx.Data["BranchLink"] = branchLink
-
-       ctx.Data["RawFileLink"] = rawLink + "/" + ctx.Repo.TreePath
-       ctx.Data["PageIsViewCode"] = true
-
-       ctx.Data["IsBlame"] = true
-
-       ctx.Data["FileSize"] = blob.Size()
-       ctx.Data["FileName"] = blob.Name()
-
-       ctx.Data["NumLines"], err = blob.GetBlobLineCount()
-       if err != nil {
-               ctx.NotFound("GetBlobLineCount", err)
-               return
-       }
-
-       blameReader, err := git.CreateBlameReader(ctx, models.RepoPath(userName, repoName), commitID, fileName)
-       if err != nil {
-               ctx.NotFound("CreateBlameReader", err)
-               return
-       }
-       defer blameReader.Close()
-
-       blameParts := make([]git.BlamePart, 0)
-
-       for {
-               blamePart, err := blameReader.NextPart()
-               if err != nil {
-                       ctx.NotFound("NextPart", err)
-                       return
-               }
-               if blamePart == nil {
-                       break
-               }
-               blameParts = append(blameParts, *blamePart)
-       }
-
-       commitNames := make(map[string]models.UserCommit)
-       commits := list.New()
-
-       for _, part := range blameParts {
-               sha := part.Sha
-               if _, ok := commitNames[sha]; ok {
-                       continue
-               }
-
-               commit, err := ctx.Repo.GitRepo.GetCommit(sha)
-               if err != nil {
-                       if git.IsErrNotExist(err) {
-                               ctx.NotFound("Repo.GitRepo.GetCommit", err)
-                       } else {
-                               ctx.ServerError("Repo.GitRepo.GetCommit", err)
-                       }
-                       return
-               }
-
-               commits.PushBack(commit)
-
-               commitNames[commit.ID.String()] = models.UserCommit{}
-       }
-
-       commits = models.ValidateCommitsWithEmails(commits)
-
-       for e := commits.Front(); e != nil; e = e.Next() {
-               c := e.Value.(models.UserCommit)
-
-               commitNames[c.ID.String()] = c
-       }
-
-       // Get Topics of this repo
-       renderRepoTopics(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       renderBlame(ctx, blameParts, commitNames)
-
-       ctx.HTML(http.StatusOK, tplBlame)
-}
-
-func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames map[string]models.UserCommit) {
-       repoLink := ctx.Repo.RepoLink
-
-       var lines = make([]string, 0)
-
-       var commitInfo bytes.Buffer
-       var lineNumbers bytes.Buffer
-       var codeLines bytes.Buffer
-
-       var i = 0
-       for pi, part := range blameParts {
-               for index, line := range part.Lines {
-                       i++
-                       lines = append(lines, line)
-
-                       var attr = ""
-                       if len(part.Lines)-1 == index && len(blameParts)-1 != pi {
-                               attr = " bottom-line"
-                       }
-                       commit := commitNames[part.Sha]
-                       if index == 0 {
-                               // User avatar image
-                               commitSince := timeutil.TimeSinceUnix(timeutil.TimeStamp(commit.Author.When.Unix()), ctx.Data["Lang"].(string))
-
-                               var avatar string
-                               if commit.User != nil {
-                                       avatar = string(templates.Avatar(commit.User, 18, "mr-3"))
-                               } else {
-                                       avatar = string(templates.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18, "mr-3"))
-                               }
-
-                               commitInfo.WriteString(fmt.Sprintf(`<div class="blame-info%s"><div class="blame-data"><div class="blame-avatar">%s</div><div class="blame-message"><a href="%s/commit/%s" title="%[5]s">%[5]s</a></div><div class="blame-time">%s</div></div></div>`, attr, avatar, repoLink, part.Sha, html.EscapeString(commit.CommitMessage), commitSince))
-                       } else {
-                               commitInfo.WriteString(fmt.Sprintf(`<div class="blame-info%s">&#8203;</div>`, attr))
-                       }
-
-                       //Line number
-                       if len(part.Lines)-1 == index && len(blameParts)-1 != pi {
-                               lineNumbers.WriteString(fmt.Sprintf(`<span id="L%d" data-line-number="%d" class="bottom-line"></span>`, i, i))
-                       } else {
-                               lineNumbers.WriteString(fmt.Sprintf(`<span id="L%d" data-line-number="%d"></span>`, i, i))
-                       }
-
-                       if i != len(lines)-1 {
-                               line += "\n"
-                       }
-                       fileName := fmt.Sprintf("%v", ctx.Data["FileName"])
-                       line = highlight.Code(fileName, line)
-                       line = `<code class="code-inner">` + line + `</code>`
-                       if len(part.Lines)-1 == index && len(blameParts)-1 != pi {
-                               codeLines.WriteString(fmt.Sprintf(`<li class="L%d bottom-line" rel="L%d">%s</li>`, i, i, line))
-                       } else {
-                               codeLines.WriteString(fmt.Sprintf(`<li class="L%d" rel="L%d">%s</li>`, i, i, line))
-                       }
-               }
-       }
-
-       ctx.Data["BlameContent"] = gotemplate.HTML(codeLines.String())
-       ctx.Data["BlameCommitInfo"] = gotemplate.HTML(commitInfo.String())
-       ctx.Data["BlameLineNums"] = gotemplate.HTML(lineNumbers.String())
-}
diff --git a/routers/repo/branch.go b/routers/repo/branch.go
deleted file mode 100644 (file)
index 4625b1a..0000000
+++ /dev/null
@@ -1,407 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2018 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 repo
-
-import (
-       "errors"
-       "fmt"
-       "net/http"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/git"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/repofiles"
-       repo_module "code.gitea.io/gitea/modules/repository"
-       "code.gitea.io/gitea/modules/util"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/routers/utils"
-       "code.gitea.io/gitea/services/forms"
-       release_service "code.gitea.io/gitea/services/release"
-       repo_service "code.gitea.io/gitea/services/repository"
-)
-
-const (
-       tplBranch base.TplName = "repo/branch/list"
-)
-
-// Branch contains the branch information
-type Branch struct {
-       Name              string
-       Commit            *git.Commit
-       IsProtected       bool
-       IsDeleted         bool
-       IsIncluded        bool
-       DeletedBranch     *models.DeletedBranch
-       CommitsAhead      int
-       CommitsBehind     int
-       LatestPullRequest *models.PullRequest
-       MergeMovedOn      bool
-}
-
-// Branches render repository branch page
-func Branches(ctx *context.Context) {
-       ctx.Data["Title"] = "Branches"
-       ctx.Data["IsRepoToolbarBranches"] = true
-       ctx.Data["DefaultBranch"] = ctx.Repo.Repository.DefaultBranch
-       ctx.Data["AllowsPulls"] = ctx.Repo.Repository.AllowsPulls()
-       ctx.Data["IsWriter"] = ctx.Repo.CanWrite(models.UnitTypeCode)
-       ctx.Data["IsMirror"] = ctx.Repo.Repository.IsMirror
-       ctx.Data["CanPull"] = ctx.Repo.CanWrite(models.UnitTypeCode) || (ctx.IsSigned && ctx.User.HasForkedRepo(ctx.Repo.Repository.ID))
-       ctx.Data["PageIsViewCode"] = true
-       ctx.Data["PageIsBranches"] = true
-
-       page := ctx.QueryInt("page")
-       if page <= 1 {
-               page = 1
-       }
-
-       limit := ctx.QueryInt("limit")
-       if limit <= 0 || limit > git.BranchesRangeSize {
-               limit = git.BranchesRangeSize
-       }
-
-       skip := (page - 1) * limit
-       log.Debug("Branches: skip: %d limit: %d", skip, limit)
-       branches, branchesCount := loadBranches(ctx, skip, limit)
-       if ctx.Written() {
-               return
-       }
-       ctx.Data["Branches"] = branches
-       pager := context.NewPagination(int(branchesCount), git.BranchesRangeSize, page, 5)
-       pager.SetDefaultParams(ctx)
-       ctx.Data["Page"] = pager
-
-       ctx.HTML(http.StatusOK, tplBranch)
-}
-
-// DeleteBranchPost responses for delete merged branch
-func DeleteBranchPost(ctx *context.Context) {
-       defer redirect(ctx)
-       branchName := ctx.Query("name")
-
-       if err := repo_service.DeleteBranch(ctx.User, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil {
-               switch {
-               case git.IsErrBranchNotExist(err):
-                       log.Debug("DeleteBranch: Can't delete non existing branch '%s'", branchName)
-                       ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", branchName))
-               case errors.Is(err, repo_service.ErrBranchIsDefault):
-                       log.Debug("DeleteBranch: Can't delete default branch '%s'", branchName)
-                       ctx.Flash.Error(ctx.Tr("repo.branch.default_deletion_failed", branchName))
-               case errors.Is(err, repo_service.ErrBranchIsProtected):
-                       log.Debug("DeleteBranch: Can't delete protected branch '%s'", branchName)
-                       ctx.Flash.Error(ctx.Tr("repo.branch.protected_deletion_failed", branchName))
-               default:
-                       log.Error("DeleteBranch: %v", err)
-                       ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", branchName))
-               }
-
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.branch.deletion_success", branchName))
-}
-
-// RestoreBranchPost responses for delete merged branch
-func RestoreBranchPost(ctx *context.Context) {
-       defer redirect(ctx)
-
-       branchID := ctx.QueryInt64("branch_id")
-       branchName := ctx.Query("name")
-
-       deletedBranch, err := ctx.Repo.Repository.GetDeletedBranchByID(branchID)
-       if err != nil {
-               log.Error("GetDeletedBranchByID: %v", err)
-               ctx.Flash.Error(ctx.Tr("repo.branch.restore_failed", branchName))
-               return
-       }
-
-       if err := git.Push(ctx.Repo.Repository.RepoPath(), git.PushOptions{
-               Remote: ctx.Repo.Repository.RepoPath(),
-               Branch: fmt.Sprintf("%s:%s%s", deletedBranch.Commit, git.BranchPrefix, deletedBranch.Name),
-               Env:    models.PushingEnvironment(ctx.User, ctx.Repo.Repository),
-       }); err != nil {
-               if strings.Contains(err.Error(), "already exists") {
-                       log.Debug("RestoreBranch: Can't restore branch '%s', since one with same name already exist", deletedBranch.Name)
-                       ctx.Flash.Error(ctx.Tr("repo.branch.already_exists", deletedBranch.Name))
-                       return
-               }
-               log.Error("RestoreBranch: CreateBranch: %v", err)
-               ctx.Flash.Error(ctx.Tr("repo.branch.restore_failed", deletedBranch.Name))
-               return
-       }
-
-       // Don't return error below this
-       if err := repo_service.PushUpdate(
-               &repo_module.PushUpdateOptions{
-                       RefFullName:  git.BranchPrefix + deletedBranch.Name,
-                       OldCommitID:  git.EmptySHA,
-                       NewCommitID:  deletedBranch.Commit,
-                       PusherID:     ctx.User.ID,
-                       PusherName:   ctx.User.Name,
-                       RepoUserName: ctx.Repo.Owner.Name,
-                       RepoName:     ctx.Repo.Repository.Name,
-               }); err != nil {
-               log.Error("RestoreBranch: Update: %v", err)
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.branch.restore_success", deletedBranch.Name))
-}
-
-func redirect(ctx *context.Context) {
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": ctx.Repo.RepoLink + "/branches",
-       })
-}
-
-// loadBranches loads branches from the repository limited by page & pageSize.
-// NOTE: May write to context on error.
-func loadBranches(ctx *context.Context, skip, limit int) ([]*Branch, int) {
-       defaultBranch, err := repo_module.GetBranch(ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch)
-       if err != nil {
-               log.Error("loadBranches: get default branch: %v", err)
-               ctx.ServerError("GetDefaultBranch", err)
-               return nil, 0
-       }
-
-       rawBranches, totalNumOfBranches, err := repo_module.GetBranches(ctx.Repo.Repository, skip, limit)
-       if err != nil {
-               log.Error("GetBranches: %v", err)
-               ctx.ServerError("GetBranches", err)
-               return nil, 0
-       }
-
-       protectedBranches, err := ctx.Repo.Repository.GetProtectedBranches()
-       if err != nil {
-               ctx.ServerError("GetProtectedBranches", err)
-               return nil, 0
-       }
-
-       repoIDToRepo := map[int64]*models.Repository{}
-       repoIDToRepo[ctx.Repo.Repository.ID] = ctx.Repo.Repository
-
-       repoIDToGitRepo := map[int64]*git.Repository{}
-       repoIDToGitRepo[ctx.Repo.Repository.ID] = ctx.Repo.GitRepo
-
-       var branches []*Branch
-       for i := range rawBranches {
-               if rawBranches[i].Name == defaultBranch.Name {
-                       // Skip default branch
-                       continue
-               }
-
-               var branch = loadOneBranch(ctx, rawBranches[i], protectedBranches, repoIDToRepo, repoIDToGitRepo)
-               if branch == nil {
-                       return nil, 0
-               }
-
-               branches = append(branches, branch)
-       }
-
-       // Always add the default branch
-       log.Debug("loadOneBranch: load default: '%s'", defaultBranch.Name)
-       branches = append(branches, loadOneBranch(ctx, defaultBranch, protectedBranches, repoIDToRepo, repoIDToGitRepo))
-
-       if ctx.Repo.CanWrite(models.UnitTypeCode) {
-               deletedBranches, err := getDeletedBranches(ctx)
-               if err != nil {
-                       ctx.ServerError("getDeletedBranches", err)
-                       return nil, 0
-               }
-               branches = append(branches, deletedBranches...)
-       }
-
-       return branches, totalNumOfBranches - 1
-}
-
-func loadOneBranch(ctx *context.Context, rawBranch *git.Branch, protectedBranches []*models.ProtectedBranch,
-       repoIDToRepo map[int64]*models.Repository,
-       repoIDToGitRepo map[int64]*git.Repository) *Branch {
-       log.Trace("loadOneBranch: '%s'", rawBranch.Name)
-
-       commit, err := rawBranch.GetCommit()
-       if err != nil {
-               ctx.ServerError("GetCommit", err)
-               return nil
-       }
-
-       branchName := rawBranch.Name
-       var isProtected bool
-       for _, b := range protectedBranches {
-               if b.BranchName == branchName {
-                       isProtected = true
-                       break
-               }
-       }
-
-       divergence, divergenceError := repofiles.CountDivergingCommits(ctx.Repo.Repository, git.BranchPrefix+branchName)
-       if divergenceError != nil {
-               ctx.ServerError("CountDivergingCommits", divergenceError)
-               return nil
-       }
-
-       pr, err := models.GetLatestPullRequestByHeadInfo(ctx.Repo.Repository.ID, branchName)
-       if err != nil {
-               ctx.ServerError("GetLatestPullRequestByHeadInfo", err)
-               return nil
-       }
-       headCommit := commit.ID.String()
-
-       mergeMovedOn := false
-       if pr != nil {
-               pr.HeadRepo = ctx.Repo.Repository
-               if err := pr.LoadIssue(); err != nil {
-                       ctx.ServerError("pr.LoadIssue", err)
-                       return nil
-               }
-               if repo, ok := repoIDToRepo[pr.BaseRepoID]; ok {
-                       pr.BaseRepo = repo
-               } else if err := pr.LoadBaseRepo(); err != nil {
-                       ctx.ServerError("pr.LoadBaseRepo", err)
-                       return nil
-               } else {
-                       repoIDToRepo[pr.BaseRepoID] = pr.BaseRepo
-               }
-               pr.Issue.Repo = pr.BaseRepo
-
-               if pr.HasMerged {
-                       baseGitRepo, ok := repoIDToGitRepo[pr.BaseRepoID]
-                       if !ok {
-                               baseGitRepo, err = git.OpenRepository(pr.BaseRepo.RepoPath())
-                               if err != nil {
-                                       ctx.ServerError("OpenRepository", err)
-                                       return nil
-                               }
-                               defer baseGitRepo.Close()
-                               repoIDToGitRepo[pr.BaseRepoID] = baseGitRepo
-                       }
-                       pullCommit, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
-                       if err != nil && !git.IsErrNotExist(err) {
-                               ctx.ServerError("GetBranchCommitID", err)
-                               return nil
-                       }
-                       if err == nil && headCommit != pullCommit {
-                               // the head has moved on from the merge - we shouldn't delete
-                               mergeMovedOn = true
-                       }
-               }
-       }
-
-       isIncluded := divergence.Ahead == 0 && ctx.Repo.Repository.DefaultBranch != branchName
-       return &Branch{
-               Name:              branchName,
-               Commit:            commit,
-               IsProtected:       isProtected,
-               IsIncluded:        isIncluded,
-               CommitsAhead:      divergence.Ahead,
-               CommitsBehind:     divergence.Behind,
-               LatestPullRequest: pr,
-               MergeMovedOn:      mergeMovedOn,
-       }
-}
-
-func getDeletedBranches(ctx *context.Context) ([]*Branch, error) {
-       branches := []*Branch{}
-
-       deletedBranches, err := ctx.Repo.Repository.GetDeletedBranches()
-       if err != nil {
-               return branches, err
-       }
-
-       for i := range deletedBranches {
-               deletedBranches[i].LoadUser()
-               branches = append(branches, &Branch{
-                       Name:          deletedBranches[i].Name,
-                       IsDeleted:     true,
-                       DeletedBranch: deletedBranches[i],
-               })
-       }
-
-       return branches, nil
-}
-
-// CreateBranch creates new branch in repository
-func CreateBranch(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.NewBranchForm)
-       if !ctx.Repo.CanCreateBranch() {
-               ctx.NotFound("CreateBranch", nil)
-               return
-       }
-
-       if ctx.HasError() {
-               ctx.Flash.Error(ctx.GetErrMsg())
-               ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
-               return
-       }
-
-       var err error
-
-       if form.CreateTag {
-               if ctx.Repo.IsViewTag {
-                       err = release_service.CreateNewTag(ctx.User, ctx.Repo.Repository, ctx.Repo.CommitID, form.NewBranchName, "")
-               } else {
-                       err = release_service.CreateNewTag(ctx.User, ctx.Repo.Repository, ctx.Repo.BranchName, form.NewBranchName, "")
-               }
-       } else if ctx.Repo.IsViewBranch {
-               err = repo_module.CreateNewBranch(ctx.User, ctx.Repo.Repository, ctx.Repo.BranchName, form.NewBranchName)
-       } else if ctx.Repo.IsViewTag {
-               err = repo_module.CreateNewBranchFromCommit(ctx.User, ctx.Repo.Repository, ctx.Repo.CommitID, form.NewBranchName)
-       } else {
-               err = repo_module.CreateNewBranchFromCommit(ctx.User, ctx.Repo.Repository, ctx.Repo.BranchName, form.NewBranchName)
-       }
-       if err != nil {
-               if models.IsErrTagAlreadyExists(err) {
-                       e := err.(models.ErrTagAlreadyExists)
-                       ctx.Flash.Error(ctx.Tr("repo.branch.tag_collision", e.TagName))
-                       ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
-                       return
-               }
-               if models.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) {
-                       ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.NewBranchName))
-                       ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
-                       return
-               }
-               if models.IsErrBranchNameConflict(err) {
-                       e := err.(models.ErrBranchNameConflict)
-                       ctx.Flash.Error(ctx.Tr("repo.branch.branch_name_conflict", form.NewBranchName, e.BranchName))
-                       ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
-                       return
-               }
-               if git.IsErrPushRejected(err) {
-                       e := err.(*git.ErrPushRejected)
-                       if len(e.Message) == 0 {
-                               ctx.Flash.Error(ctx.Tr("repo.editor.push_rejected_no_message"))
-                       } else {
-                               flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
-                                       "Message": ctx.Tr("repo.editor.push_rejected"),
-                                       "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
-                                       "Details": utils.SanitizeFlashErrorString(e.Message),
-                               })
-                               if err != nil {
-                                       ctx.ServerError("UpdatePullRequest.HTMLString", err)
-                                       return
-                               }
-                               ctx.Flash.Error(flashError)
-                       }
-                       ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
-                       return
-               }
-
-               ctx.ServerError("CreateNewBranch", err)
-               return
-       }
-
-       if form.CreateTag {
-               ctx.Flash.Success(ctx.Tr("repo.tag.create_success", form.NewBranchName))
-               ctx.Redirect(ctx.Repo.RepoLink + "/src/tag/" + util.PathEscapeSegments(form.NewBranchName))
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.branch.create_success", form.NewBranchName))
-       ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(form.NewBranchName))
-}
diff --git a/routers/repo/commit.go b/routers/repo/commit.go
deleted file mode 100644 (file)
index 3e6148b..0000000
+++ /dev/null
@@ -1,401 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2019 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 repo
-
-import (
-       "errors"
-       "net/http"
-       "path"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/charset"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/git"
-       "code.gitea.io/gitea/modules/gitgraph"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/services/gitdiff"
-)
-
-const (
-       tplCommits    base.TplName = "repo/commits"
-       tplGraph      base.TplName = "repo/graph"
-       tplGraphDiv   base.TplName = "repo/graph/div"
-       tplCommitPage base.TplName = "repo/commit_page"
-)
-
-// RefCommits render commits page
-func RefCommits(ctx *context.Context) {
-       switch {
-       case len(ctx.Repo.TreePath) == 0:
-               Commits(ctx)
-       case ctx.Repo.TreePath == "search":
-               SearchCommits(ctx)
-       default:
-               FileHistory(ctx)
-       }
-}
-
-// Commits render branch's commits
-func Commits(ctx *context.Context) {
-       ctx.Data["PageIsCommits"] = true
-       if ctx.Repo.Commit == nil {
-               ctx.NotFound("Commit not found", nil)
-               return
-       }
-       ctx.Data["PageIsViewCode"] = true
-
-       commitsCount, err := ctx.Repo.GetCommitsCount()
-       if err != nil {
-               ctx.ServerError("GetCommitsCount", err)
-               return
-       }
-
-       page := ctx.QueryInt("page")
-       if page <= 1 {
-               page = 1
-       }
-
-       pageSize := ctx.QueryInt("limit")
-       if pageSize <= 0 {
-               pageSize = git.CommitsRangeSize
-       }
-
-       // Both `git log branchName` and `git log commitId` work.
-       commits, err := ctx.Repo.Commit.CommitsByRange(page, pageSize)
-       if err != nil {
-               ctx.ServerError("CommitsByRange", err)
-               return
-       }
-       commits = models.ValidateCommitsWithEmails(commits)
-       commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
-       commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
-       ctx.Data["Commits"] = commits
-
-       ctx.Data["Username"] = ctx.Repo.Owner.Name
-       ctx.Data["Reponame"] = ctx.Repo.Repository.Name
-       ctx.Data["CommitCount"] = commitsCount
-       ctx.Data["Branch"] = ctx.Repo.BranchName
-
-       pager := context.NewPagination(int(commitsCount), git.CommitsRangeSize, page, 5)
-       pager.SetDefaultParams(ctx)
-       ctx.Data["Page"] = pager
-
-       ctx.HTML(http.StatusOK, tplCommits)
-}
-
-// Graph render commit graph - show commits from all branches.
-func Graph(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.commit_graph")
-       ctx.Data["PageIsCommits"] = true
-       ctx.Data["PageIsViewCode"] = true
-       mode := strings.ToLower(ctx.QueryTrim("mode"))
-       if mode != "monochrome" {
-               mode = "color"
-       }
-       ctx.Data["Mode"] = mode
-       hidePRRefs := ctx.QueryBool("hide-pr-refs")
-       ctx.Data["HidePRRefs"] = hidePRRefs
-       branches := ctx.QueryStrings("branch")
-       realBranches := make([]string, len(branches))
-       copy(realBranches, branches)
-       for i, branch := range realBranches {
-               if strings.HasPrefix(branch, "--") {
-                       realBranches[i] = "refs/heads/" + branch
-               }
-       }
-       ctx.Data["SelectedBranches"] = realBranches
-       files := ctx.QueryStrings("file")
-
-       commitsCount, err := ctx.Repo.GetCommitsCount()
-       if err != nil {
-               ctx.ServerError("GetCommitsCount", err)
-               return
-       }
-
-       graphCommitsCount, err := ctx.Repo.GetCommitGraphsCount(hidePRRefs, realBranches, files)
-       if err != nil {
-               log.Warn("GetCommitGraphsCount error for generate graph exclude prs: %t branches: %s in %-v, Will Ignore branches and try again. Underlying Error: %v", hidePRRefs, branches, ctx.Repo.Repository, err)
-               realBranches = []string{}
-               branches = []string{}
-               graphCommitsCount, err = ctx.Repo.GetCommitGraphsCount(hidePRRefs, realBranches, files)
-               if err != nil {
-                       ctx.ServerError("GetCommitGraphsCount", err)
-                       return
-               }
-       }
-
-       page := ctx.QueryInt("page")
-
-       graph, err := gitgraph.GetCommitGraph(ctx.Repo.GitRepo, page, 0, hidePRRefs, realBranches, files)
-       if err != nil {
-               ctx.ServerError("GetCommitGraph", err)
-               return
-       }
-
-       if err := graph.LoadAndProcessCommits(ctx.Repo.Repository, ctx.Repo.GitRepo); err != nil {
-               ctx.ServerError("LoadAndProcessCommits", err)
-               return
-       }
-
-       ctx.Data["Graph"] = graph
-
-       gitRefs, err := ctx.Repo.GitRepo.GetRefs()
-       if err != nil {
-               ctx.ServerError("GitRepo.GetRefs", err)
-               return
-       }
-
-       ctx.Data["AllRefs"] = gitRefs
-
-       ctx.Data["Username"] = ctx.Repo.Owner.Name
-       ctx.Data["Reponame"] = ctx.Repo.Repository.Name
-       ctx.Data["CommitCount"] = commitsCount
-       ctx.Data["Branch"] = ctx.Repo.BranchName
-       paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5)
-       paginator.AddParam(ctx, "mode", "Mode")
-       paginator.AddParam(ctx, "hide-pr-refs", "HidePRRefs")
-       for _, branch := range branches {
-               paginator.AddParamString("branch", branch)
-       }
-       for _, file := range files {
-               paginator.AddParamString("file", file)
-       }
-       ctx.Data["Page"] = paginator
-       if ctx.QueryBool("div-only") {
-               ctx.HTML(http.StatusOK, tplGraphDiv)
-               return
-       }
-
-       ctx.HTML(http.StatusOK, tplGraph)
-}
-
-// SearchCommits render commits filtered by keyword
-func SearchCommits(ctx *context.Context) {
-       ctx.Data["PageIsCommits"] = true
-       ctx.Data["PageIsViewCode"] = true
-
-       query := strings.Trim(ctx.Query("q"), " ")
-       if len(query) == 0 {
-               ctx.Redirect(ctx.Repo.RepoLink + "/commits/" + ctx.Repo.BranchNameSubURL())
-               return
-       }
-
-       all := ctx.QueryBool("all")
-       opts := git.NewSearchCommitsOptions(query, all)
-       commits, err := ctx.Repo.Commit.SearchCommits(opts)
-       if err != nil {
-               ctx.ServerError("SearchCommits", err)
-               return
-       }
-       commits = models.ValidateCommitsWithEmails(commits)
-       commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
-       commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
-       ctx.Data["Commits"] = commits
-
-       ctx.Data["Keyword"] = query
-       if all {
-               ctx.Data["All"] = "checked"
-       }
-       ctx.Data["Username"] = ctx.Repo.Owner.Name
-       ctx.Data["Reponame"] = ctx.Repo.Repository.Name
-       ctx.Data["CommitCount"] = commits.Len()
-       ctx.Data["Branch"] = ctx.Repo.BranchName
-       ctx.HTML(http.StatusOK, tplCommits)
-}
-
-// FileHistory show a file's reversions
-func FileHistory(ctx *context.Context) {
-       ctx.Data["IsRepoToolbarCommits"] = true
-
-       fileName := ctx.Repo.TreePath
-       if len(fileName) == 0 {
-               Commits(ctx)
-               return
-       }
-
-       branchName := ctx.Repo.BranchName
-       commitsCount, err := ctx.Repo.GitRepo.FileCommitsCount(branchName, fileName)
-       if err != nil {
-               ctx.ServerError("FileCommitsCount", err)
-               return
-       } else if commitsCount == 0 {
-               ctx.NotFound("FileCommitsCount", nil)
-               return
-       }
-
-       page := ctx.QueryInt("page")
-       if page <= 1 {
-               page = 1
-       }
-
-       commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange(branchName, fileName, page)
-       if err != nil {
-               ctx.ServerError("CommitsByFileAndRange", err)
-               return
-       }
-       commits = models.ValidateCommitsWithEmails(commits)
-       commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
-       commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
-       ctx.Data["Commits"] = commits
-
-       ctx.Data["Username"] = ctx.Repo.Owner.Name
-       ctx.Data["Reponame"] = ctx.Repo.Repository.Name
-       ctx.Data["FileName"] = fileName
-       ctx.Data["CommitCount"] = commitsCount
-       ctx.Data["Branch"] = branchName
-
-       pager := context.NewPagination(int(commitsCount), git.CommitsRangeSize, page, 5)
-       pager.SetDefaultParams(ctx)
-       ctx.Data["Page"] = pager
-
-       ctx.HTML(http.StatusOK, tplCommits)
-}
-
-// Diff show different from current commit to previous commit
-func Diff(ctx *context.Context) {
-       ctx.Data["PageIsDiff"] = true
-       ctx.Data["RequireHighlightJS"] = true
-       ctx.Data["RequireSimpleMDE"] = true
-       ctx.Data["RequireTribute"] = true
-
-       userName := ctx.Repo.Owner.Name
-       repoName := ctx.Repo.Repository.Name
-       commitID := ctx.Params(":sha")
-       var (
-               gitRepo  *git.Repository
-               err      error
-               repoPath string
-       )
-
-       if ctx.Data["PageIsWiki"] != nil {
-               gitRepo, err = git.OpenRepository(ctx.Repo.Repository.WikiPath())
-               if err != nil {
-                       ctx.ServerError("Repo.GitRepo.GetCommit", err)
-                       return
-               }
-               repoPath = ctx.Repo.Repository.WikiPath()
-       } else {
-               gitRepo = ctx.Repo.GitRepo
-               repoPath = models.RepoPath(userName, repoName)
-       }
-
-       commit, err := gitRepo.GetCommit(commitID)
-       if err != nil {
-               if git.IsErrNotExist(err) {
-                       ctx.NotFound("Repo.GitRepo.GetCommit", err)
-               } else {
-                       ctx.ServerError("Repo.GitRepo.GetCommit", err)
-               }
-               return
-       }
-       if len(commitID) != 40 {
-               commitID = commit.ID.String()
-       }
-
-       statuses, err := models.GetLatestCommitStatus(ctx.Repo.Repository.ID, commitID, models.ListOptions{})
-       if err != nil {
-               log.Error("GetLatestCommitStatus: %v", err)
-       }
-
-       ctx.Data["CommitStatus"] = models.CalcCommitStatus(statuses)
-       ctx.Data["CommitStatuses"] = statuses
-
-       diff, err := gitdiff.GetDiffCommitWithWhitespaceBehavior(repoPath,
-               commitID, setting.Git.MaxGitDiffLines,
-               setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles,
-               gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
-       if err != nil {
-               ctx.NotFound("GetDiffCommitWithWhitespaceBehavior", err)
-               return
-       }
-
-       parents := make([]string, commit.ParentCount())
-       for i := 0; i < commit.ParentCount(); i++ {
-               sha, err := commit.ParentID(i)
-               if err != nil {
-                       ctx.NotFound("repo.Diff", err)
-                       return
-               }
-               parents[i] = sha.String()
-       }
-
-       ctx.Data["CommitID"] = commitID
-       ctx.Data["AfterCommitID"] = commitID
-       ctx.Data["Username"] = userName
-       ctx.Data["Reponame"] = repoName
-
-       var parentCommit *git.Commit
-       if commit.ParentCount() > 0 {
-               parentCommit, err = gitRepo.GetCommit(parents[0])
-               if err != nil {
-                       ctx.NotFound("GetParentCommit", err)
-                       return
-               }
-       }
-       headTarget := path.Join(userName, repoName)
-       setCompareContext(ctx, parentCommit, commit, headTarget)
-       ctx.Data["Title"] = commit.Summary() + " Â· " + base.ShortSha(commitID)
-       ctx.Data["Commit"] = commit
-       verification := models.ParseCommitWithSignature(commit)
-       ctx.Data["Verification"] = verification
-       ctx.Data["Author"] = models.ValidateCommitWithEmail(commit)
-       ctx.Data["Diff"] = diff
-       ctx.Data["Parents"] = parents
-       ctx.Data["DiffNotAvailable"] = diff.NumFiles == 0
-
-       if err := models.CalculateTrustStatus(verification, ctx.Repo.Repository, nil); err != nil {
-               ctx.ServerError("CalculateTrustStatus", err)
-               return
-       }
-
-       note := &git.Note{}
-       err = git.GetNote(ctx, ctx.Repo.GitRepo, commitID, note)
-       if err == nil {
-               ctx.Data["Note"] = string(charset.ToUTF8WithFallback(note.Message))
-               ctx.Data["NoteCommit"] = note.Commit
-               ctx.Data["NoteAuthor"] = models.ValidateCommitWithEmail(note.Commit)
-       }
-
-       ctx.Data["BranchName"], err = commit.GetBranchName()
-       if err != nil {
-               ctx.ServerError("commit.GetBranchName", err)
-               return
-       }
-
-       ctx.Data["TagName"], err = commit.GetTagName()
-       if err != nil {
-               ctx.ServerError("commit.GetTagName", err)
-               return
-       }
-       ctx.HTML(http.StatusOK, tplCommitPage)
-}
-
-// RawDiff dumps diff results of repository in given commit ID to io.Writer
-func RawDiff(ctx *context.Context) {
-       var repoPath string
-       if ctx.Data["PageIsWiki"] != nil {
-               repoPath = ctx.Repo.Repository.WikiPath()
-       } else {
-               repoPath = models.RepoPath(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
-       }
-       if err := git.GetRawDiff(
-               repoPath,
-               ctx.Params(":sha"),
-               git.RawDiffType(ctx.Params(":ext")),
-               ctx.Resp,
-       ); err != nil {
-               if git.IsErrNotExist(err) {
-                       ctx.NotFound("GetRawDiff",
-                               errors.New("commit "+ctx.Params(":sha")+" does not exist."))
-                       return
-               }
-               ctx.ServerError("GetRawDiff", err)
-               return
-       }
-}
diff --git a/routers/repo/compare.go b/routers/repo/compare.go
deleted file mode 100644 (file)
index f53a317..0000000
+++ /dev/null
@@ -1,787 +0,0 @@
-// Copyright 2019 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 repo
-
-import (
-       "bufio"
-       "encoding/csv"
-       "errors"
-       "fmt"
-       "html"
-       "net/http"
-       "path"
-       "path/filepath"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/charset"
-       "code.gitea.io/gitea/modules/context"
-       csv_module "code.gitea.io/gitea/modules/csv"
-       "code.gitea.io/gitea/modules/git"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/upload"
-       "code.gitea.io/gitea/services/gitdiff"
-)
-
-const (
-       tplCompare     base.TplName = "repo/diff/compare"
-       tplBlobExcerpt base.TplName = "repo/diff/blob_excerpt"
-)
-
-// setCompareContext sets context data.
-func setCompareContext(ctx *context.Context, base *git.Commit, head *git.Commit, headTarget string) {
-       ctx.Data["BaseCommit"] = base
-       ctx.Data["HeadCommit"] = head
-
-       ctx.Data["GetBlobByPathForCommit"] = func(commit *git.Commit, path string) *git.Blob {
-               if commit == nil {
-                       return nil
-               }
-
-               blob, err := commit.GetBlobByPath(path)
-               if err != nil {
-                       return nil
-               }
-               return blob
-       }
-
-       setPathsCompareContext(ctx, base, head, headTarget)
-       setImageCompareContext(ctx)
-       setCsvCompareContext(ctx)
-}
-
-// setPathsCompareContext sets context data for source and raw paths
-func setPathsCompareContext(ctx *context.Context, base *git.Commit, head *git.Commit, headTarget string) {
-       sourcePath := setting.AppSubURL + "/%s/src/commit/%s"
-       rawPath := setting.AppSubURL + "/%s/raw/commit/%s"
-
-       ctx.Data["SourcePath"] = fmt.Sprintf(sourcePath, headTarget, head.ID)
-       ctx.Data["RawPath"] = fmt.Sprintf(rawPath, headTarget, head.ID)
-       if base != nil {
-               baseTarget := path.Join(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
-               ctx.Data["BeforeSourcePath"] = fmt.Sprintf(sourcePath, baseTarget, base.ID)
-               ctx.Data["BeforeRawPath"] = fmt.Sprintf(rawPath, baseTarget, base.ID)
-       }
-}
-
-// setImageCompareContext sets context data that is required by image compare template
-func setImageCompareContext(ctx *context.Context) {
-       ctx.Data["IsBlobAnImage"] = func(blob *git.Blob) bool {
-               if blob == nil {
-                       return false
-               }
-
-               st, err := blob.GuessContentType()
-               if err != nil {
-                       log.Error("GuessContentType failed: %v", err)
-                       return false
-               }
-               return st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage())
-       }
-}
-
-// setCsvCompareContext sets context data that is required by the CSV compare template
-func setCsvCompareContext(ctx *context.Context) {
-       ctx.Data["IsCsvFile"] = func(diffFile *gitdiff.DiffFile) bool {
-               extension := strings.ToLower(filepath.Ext(diffFile.Name))
-               return extension == ".csv" || extension == ".tsv"
-       }
-
-       type CsvDiffResult struct {
-               Sections []*gitdiff.TableDiffSection
-               Error    string
-       }
-
-       ctx.Data["CreateCsvDiff"] = func(diffFile *gitdiff.DiffFile, baseCommit *git.Commit, headCommit *git.Commit) CsvDiffResult {
-               if diffFile == nil || baseCommit == nil || headCommit == nil {
-                       return CsvDiffResult{nil, ""}
-               }
-
-               errTooLarge := errors.New(ctx.Locale.Tr("repo.error.csv.too_large"))
-
-               csvReaderFromCommit := func(c *git.Commit) (*csv.Reader, error) {
-                       blob, err := c.GetBlobByPath(diffFile.Name)
-                       if err != nil {
-                               return nil, err
-                       }
-
-                       if setting.UI.CSV.MaxFileSize != 0 && setting.UI.CSV.MaxFileSize < blob.Size() {
-                               return nil, errTooLarge
-                       }
-
-                       reader, err := blob.DataAsync()
-                       if err != nil {
-                               return nil, err
-                       }
-                       defer reader.Close()
-
-                       return csv_module.CreateReaderAndGuessDelimiter(charset.ToUTF8WithFallbackReader(reader))
-               }
-
-               baseReader, err := csvReaderFromCommit(baseCommit)
-               if err == errTooLarge {
-                       return CsvDiffResult{nil, err.Error()}
-               }
-               headReader, err := csvReaderFromCommit(headCommit)
-               if err == errTooLarge {
-                       return CsvDiffResult{nil, err.Error()}
-               }
-
-               sections, err := gitdiff.CreateCsvDiff(diffFile, baseReader, headReader)
-               if err != nil {
-                       errMessage, err := csv_module.FormatError(err, ctx.Locale)
-                       if err != nil {
-                               log.Error("RenderCsvDiff failed: %v", err)
-                               return CsvDiffResult{nil, ""}
-                       }
-                       return CsvDiffResult{nil, errMessage}
-               }
-               return CsvDiffResult{sections, ""}
-       }
-}
-
-// ParseCompareInfo parse compare info between two commit for preparing comparing references
-func ParseCompareInfo(ctx *context.Context) (*models.User, *models.Repository, *git.Repository, *git.CompareInfo, string, string) {
-       baseRepo := ctx.Repo.Repository
-
-       // Get compared branches information
-       // A full compare url is of the form:
-       //
-       // 1. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headBranch}
-       // 2. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}:{:headBranch}
-       // 3. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}/{:headRepoName}:{:headBranch}
-       //
-       // Here we obtain the infoPath "{:baseBranch}...[{:headOwner}/{:headRepoName}:]{:headBranch}" as ctx.Params("*")
-       // with the :baseRepo in ctx.Repo.
-       //
-       // Note: Generally :headRepoName is not provided here - we are only passed :headOwner.
-       //
-       // How do we determine the :headRepo?
-       //
-       // 1. If :headOwner is not set then the :headRepo = :baseRepo
-       // 2. If :headOwner is set - then look for the fork of :baseRepo owned by :headOwner
-       // 3. But... :baseRepo could be a fork of :headOwner's repo - so check that
-       // 4. Now, :baseRepo and :headRepos could be forks of the same repo - so check that
-       //
-       // format: <base branch>...[<head repo>:]<head branch>
-       // base<-head: master...head:feature
-       // same repo: master...feature
-
-       var (
-               headUser   *models.User
-               headRepo   *models.Repository
-               headBranch string
-               isSameRepo bool
-               infoPath   string
-               err        error
-       )
-       infoPath = ctx.Params("*")
-       infos := strings.SplitN(infoPath, "...", 2)
-       if len(infos) != 2 {
-               log.Trace("ParseCompareInfo[%d]: not enough compared branches information %s", baseRepo.ID, infos)
-               ctx.NotFound("CompareAndPullRequest", nil)
-               return nil, nil, nil, nil, "", ""
-       }
-
-       ctx.Data["BaseName"] = baseRepo.OwnerName
-       baseBranch := infos[0]
-       ctx.Data["BaseBranch"] = baseBranch
-
-       // If there is no head repository, it means compare between same repository.
-       headInfos := strings.Split(infos[1], ":")
-       if len(headInfos) == 1 {
-               isSameRepo = true
-               headUser = ctx.Repo.Owner
-               headBranch = headInfos[0]
-
-       } else if len(headInfos) == 2 {
-               headInfosSplit := strings.Split(headInfos[0], "/")
-               if len(headInfosSplit) == 1 {
-                       headUser, err = models.GetUserByName(headInfos[0])
-                       if err != nil {
-                               if models.IsErrUserNotExist(err) {
-                                       ctx.NotFound("GetUserByName", nil)
-                               } else {
-                                       ctx.ServerError("GetUserByName", err)
-                               }
-                               return nil, nil, nil, nil, "", ""
-                       }
-                       headBranch = headInfos[1]
-                       isSameRepo = headUser.ID == ctx.Repo.Owner.ID
-                       if isSameRepo {
-                               headRepo = baseRepo
-                       }
-               } else {
-                       headRepo, err = models.GetRepositoryByOwnerAndName(headInfosSplit[0], headInfosSplit[1])
-                       if err != nil {
-                               if models.IsErrRepoNotExist(err) {
-                                       ctx.NotFound("GetRepositoryByOwnerAndName", nil)
-                               } else {
-                                       ctx.ServerError("GetRepositoryByOwnerAndName", err)
-                               }
-                               return nil, nil, nil, nil, "", ""
-                       }
-                       if err := headRepo.GetOwner(); err != nil {
-                               if models.IsErrUserNotExist(err) {
-                                       ctx.NotFound("GetUserByName", nil)
-                               } else {
-                                       ctx.ServerError("GetUserByName", err)
-                               }
-                               return nil, nil, nil, nil, "", ""
-                       }
-                       headBranch = headInfos[1]
-                       headUser = headRepo.Owner
-                       isSameRepo = headRepo.ID == ctx.Repo.Repository.ID
-               }
-       } else {
-               ctx.NotFound("CompareAndPullRequest", nil)
-               return nil, nil, nil, nil, "", ""
-       }
-       ctx.Data["HeadUser"] = headUser
-       ctx.Data["HeadBranch"] = headBranch
-       ctx.Repo.PullRequest.SameRepo = isSameRepo
-
-       // Check if base branch is valid.
-       baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(baseBranch)
-       baseIsBranch := ctx.Repo.GitRepo.IsBranchExist(baseBranch)
-       baseIsTag := ctx.Repo.GitRepo.IsTagExist(baseBranch)
-       if !baseIsCommit && !baseIsBranch && !baseIsTag {
-               // Check if baseBranch is short sha commit hash
-               if baseCommit, _ := ctx.Repo.GitRepo.GetCommit(baseBranch); baseCommit != nil {
-                       baseBranch = baseCommit.ID.String()
-                       ctx.Data["BaseBranch"] = baseBranch
-                       baseIsCommit = true
-               } else {
-                       ctx.NotFound("IsRefExist", nil)
-                       return nil, nil, nil, nil, "", ""
-               }
-       }
-       ctx.Data["BaseIsCommit"] = baseIsCommit
-       ctx.Data["BaseIsBranch"] = baseIsBranch
-       ctx.Data["BaseIsTag"] = baseIsTag
-       ctx.Data["IsPull"] = true
-
-       // Now we have the repository that represents the base
-
-       // The current base and head repositories and branches may not
-       // actually be the intended branches that the user wants to
-       // create a pull-request from - but also determining the head
-       // repo is difficult.
-
-       // We will want therefore to offer a few repositories to set as
-       // our base and head
-
-       // 1. First if the baseRepo is a fork get the "RootRepo" it was
-       // forked from
-       var rootRepo *models.Repository
-       if baseRepo.IsFork {
-               err = baseRepo.GetBaseRepo()
-               if err != nil {
-                       if !models.IsErrRepoNotExist(err) {
-                               ctx.ServerError("Unable to find root repo", err)
-                               return nil, nil, nil, nil, "", ""
-                       }
-               } else {
-                       rootRepo = baseRepo.BaseRepo
-               }
-       }
-
-       // 2. Now if the current user is not the owner of the baseRepo,
-       // check if they have a fork of the base repo and offer that as
-       // "OwnForkRepo"
-       var ownForkRepo *models.Repository
-       if ctx.User != nil && baseRepo.OwnerID != ctx.User.ID {
-               repo, has := models.HasForkedRepo(ctx.User.ID, baseRepo.ID)
-               if has {
-                       ownForkRepo = repo
-                       ctx.Data["OwnForkRepo"] = ownForkRepo
-               }
-       }
-
-       has := headRepo != nil
-       // 3. If the base is a forked from "RootRepo" and the owner of
-       // the "RootRepo" is the :headUser - set headRepo to that
-       if !has && rootRepo != nil && rootRepo.OwnerID == headUser.ID {
-               headRepo = rootRepo
-               has = true
-       }
-
-       // 4. If the ctx.User has their own fork of the baseRepo and the headUser is the ctx.User
-       // set the headRepo to the ownFork
-       if !has && ownForkRepo != nil && ownForkRepo.OwnerID == headUser.ID {
-               headRepo = ownForkRepo
-               has = true
-       }
-
-       // 5. If the headOwner has a fork of the baseRepo - use that
-       if !has {
-               headRepo, has = models.HasForkedRepo(headUser.ID, baseRepo.ID)
-       }
-
-       // 6. If the baseRepo is a fork and the headUser has a fork of that use that
-       if !has && baseRepo.IsFork {
-               headRepo, has = models.HasForkedRepo(headUser.ID, baseRepo.ForkID)
-       }
-
-       // 7. Otherwise if we're not the same repo and haven't found a repo give up
-       if !isSameRepo && !has {
-               ctx.Data["PageIsComparePull"] = false
-       }
-
-       // 8. Finally open the git repo
-       var headGitRepo *git.Repository
-       if isSameRepo {
-               headRepo = ctx.Repo.Repository
-               headGitRepo = ctx.Repo.GitRepo
-       } else if has {
-               headGitRepo, err = git.OpenRepository(headRepo.RepoPath())
-               if err != nil {
-                       ctx.ServerError("OpenRepository", err)
-                       return nil, nil, nil, nil, "", ""
-               }
-               defer headGitRepo.Close()
-       }
-
-       ctx.Data["HeadRepo"] = headRepo
-
-       // Now we need to assert that the ctx.User has permission to read
-       // the baseRepo's code and pulls
-       // (NOT headRepo's)
-       permBase, err := models.GetUserRepoPermission(baseRepo, ctx.User)
-       if err != nil {
-               ctx.ServerError("GetUserRepoPermission", err)
-               return nil, nil, nil, nil, "", ""
-       }
-       if !permBase.CanRead(models.UnitTypeCode) {
-               if log.IsTrace() {
-                       log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in baseRepo has Permissions: %-+v",
-                               ctx.User,
-                               baseRepo,
-                               permBase)
-               }
-               ctx.NotFound("ParseCompareInfo", nil)
-               return nil, nil, nil, nil, "", ""
-       }
-
-       // If we're not merging from the same repo:
-       if !isSameRepo {
-               // Assert ctx.User has permission to read headRepo's codes
-               permHead, err := models.GetUserRepoPermission(headRepo, ctx.User)
-               if err != nil {
-                       ctx.ServerError("GetUserRepoPermission", err)
-                       return nil, nil, nil, nil, "", ""
-               }
-               if !permHead.CanRead(models.UnitTypeCode) {
-                       if log.IsTrace() {
-                               log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v",
-                                       ctx.User,
-                                       headRepo,
-                                       permHead)
-                       }
-                       ctx.NotFound("ParseCompareInfo", nil)
-                       return nil, nil, nil, nil, "", ""
-               }
-       }
-
-       // If we have a rootRepo and it's different from:
-       // 1. the computed base
-       // 2. the computed head
-       // then get the branches of it
-       if rootRepo != nil &&
-               rootRepo.ID != headRepo.ID &&
-               rootRepo.ID != baseRepo.ID {
-               perm, branches, tags, err := getBranchesAndTagsForRepo(ctx.User, rootRepo)
-               if err != nil {
-                       ctx.ServerError("GetBranchesForRepo", err)
-                       return nil, nil, nil, nil, "", ""
-               }
-               if perm {
-                       ctx.Data["RootRepo"] = rootRepo
-                       ctx.Data["RootRepoBranches"] = branches
-                       ctx.Data["RootRepoTags"] = tags
-               }
-       }
-
-       // If we have a ownForkRepo and it's different from:
-       // 1. The computed base
-       // 2. The computed head
-       // 3. The rootRepo (if we have one)
-       // then get the branches from it.
-       if ownForkRepo != nil &&
-               ownForkRepo.ID != headRepo.ID &&
-               ownForkRepo.ID != baseRepo.ID &&
-               (rootRepo == nil || ownForkRepo.ID != rootRepo.ID) {
-               perm, branches, tags, err := getBranchesAndTagsForRepo(ctx.User, ownForkRepo)
-               if err != nil {
-                       ctx.ServerError("GetBranchesForRepo", err)
-                       return nil, nil, nil, nil, "", ""
-               }
-               if perm {
-                       ctx.Data["OwnForkRepo"] = ownForkRepo
-                       ctx.Data["OwnForkRepoBranches"] = branches
-                       ctx.Data["OwnForkRepoTags"] = tags
-               }
-       }
-
-       // Check if head branch is valid.
-       headIsCommit := headGitRepo.IsCommitExist(headBranch)
-       headIsBranch := headGitRepo.IsBranchExist(headBranch)
-       headIsTag := headGitRepo.IsTagExist(headBranch)
-       if !headIsCommit && !headIsBranch && !headIsTag {
-               // Check if headBranch is short sha commit hash
-               if headCommit, _ := headGitRepo.GetCommit(headBranch); headCommit != nil {
-                       headBranch = headCommit.ID.String()
-                       ctx.Data["HeadBranch"] = headBranch
-                       headIsCommit = true
-               } else {
-                       ctx.NotFound("IsRefExist", nil)
-                       return nil, nil, nil, nil, "", ""
-               }
-       }
-       ctx.Data["HeadIsCommit"] = headIsCommit
-       ctx.Data["HeadIsBranch"] = headIsBranch
-       ctx.Data["HeadIsTag"] = headIsTag
-
-       // Treat as pull request if both references are branches
-       if ctx.Data["PageIsComparePull"] == nil {
-               ctx.Data["PageIsComparePull"] = headIsBranch && baseIsBranch
-       }
-
-       if ctx.Data["PageIsComparePull"] == true && !permBase.CanReadIssuesOrPulls(true) {
-               if log.IsTrace() {
-                       log.Trace("Permission Denied: User: %-v cannot create/read pull requests in Repo: %-v\nUser in baseRepo has Permissions: %-+v",
-                               ctx.User,
-                               baseRepo,
-                               permBase)
-               }
-               ctx.NotFound("ParseCompareInfo", nil)
-               return nil, nil, nil, nil, "", ""
-       }
-
-       baseBranchRef := baseBranch
-       if baseIsBranch {
-               baseBranchRef = git.BranchPrefix + baseBranch
-       } else if baseIsTag {
-               baseBranchRef = git.TagPrefix + baseBranch
-       }
-       headBranchRef := headBranch
-       if headIsBranch {
-               headBranchRef = git.BranchPrefix + headBranch
-       } else if headIsTag {
-               headBranchRef = git.TagPrefix + headBranch
-       }
-
-       compareInfo, err := headGitRepo.GetCompareInfo(baseRepo.RepoPath(), baseBranchRef, headBranchRef)
-       if err != nil {
-               ctx.ServerError("GetCompareInfo", err)
-               return nil, nil, nil, nil, "", ""
-       }
-       ctx.Data["BeforeCommitID"] = compareInfo.MergeBase
-
-       return headUser, headRepo, headGitRepo, compareInfo, baseBranch, headBranch
-}
-
-// PrepareCompareDiff renders compare diff page
-func PrepareCompareDiff(
-       ctx *context.Context,
-       headUser *models.User,
-       headRepo *models.Repository,
-       headGitRepo *git.Repository,
-       compareInfo *git.CompareInfo,
-       baseBranch, headBranch string,
-       whitespaceBehavior string) bool {
-
-       var (
-               repo  = ctx.Repo.Repository
-               err   error
-               title string
-       )
-
-       // Get diff information.
-       ctx.Data["CommitRepoLink"] = headRepo.Link()
-
-       headCommitID := compareInfo.HeadCommitID
-
-       ctx.Data["AfterCommitID"] = headCommitID
-
-       if headCommitID == compareInfo.MergeBase {
-               ctx.Data["IsNothingToCompare"] = true
-               if unit, err := repo.GetUnit(models.UnitTypePullRequests); err == nil {
-                       config := unit.PullRequestsConfig()
-
-                       if !config.AutodetectManualMerge {
-                               allowEmptyPr := !(baseBranch == headBranch && ctx.Repo.Repository.Name == headRepo.Name)
-                               ctx.Data["AllowEmptyPr"] = allowEmptyPr
-
-                               return !allowEmptyPr
-                       }
-
-                       ctx.Data["AllowEmptyPr"] = false
-               }
-               return true
-       }
-
-       diff, err := gitdiff.GetDiffRangeWithWhitespaceBehavior(models.RepoPath(headUser.Name, headRepo.Name),
-               compareInfo.MergeBase, headCommitID, setting.Git.MaxGitDiffLines,
-               setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, whitespaceBehavior)
-       if err != nil {
-               ctx.ServerError("GetDiffRangeWithWhitespaceBehavior", err)
-               return false
-       }
-       ctx.Data["Diff"] = diff
-       ctx.Data["DiffNotAvailable"] = diff.NumFiles == 0
-
-       headCommit, err := headGitRepo.GetCommit(headCommitID)
-       if err != nil {
-               ctx.ServerError("GetCommit", err)
-               return false
-       }
-
-       baseGitRepo := ctx.Repo.GitRepo
-       baseCommitID := compareInfo.BaseCommitID
-
-       baseCommit, err := baseGitRepo.GetCommit(baseCommitID)
-       if err != nil {
-               ctx.ServerError("GetCommit", err)
-               return false
-       }
-
-       compareInfo.Commits = models.ValidateCommitsWithEmails(compareInfo.Commits)
-       compareInfo.Commits = models.ParseCommitsWithSignature(compareInfo.Commits, headRepo)
-       compareInfo.Commits = models.ParseCommitsWithStatus(compareInfo.Commits, headRepo)
-       ctx.Data["Commits"] = compareInfo.Commits
-       ctx.Data["CommitCount"] = compareInfo.Commits.Len()
-
-       if compareInfo.Commits.Len() == 1 {
-               c := compareInfo.Commits.Front().Value.(models.SignCommitWithStatuses)
-               title = strings.TrimSpace(c.UserCommit.Summary())
-
-               body := strings.Split(strings.TrimSpace(c.UserCommit.Message()), "\n")
-               if len(body) > 1 {
-                       ctx.Data["content"] = strings.Join(body[1:], "\n")
-               }
-       } else {
-               title = headBranch
-       }
-       ctx.Data["title"] = title
-       ctx.Data["Username"] = headUser.Name
-       ctx.Data["Reponame"] = headRepo.Name
-
-       headTarget := path.Join(headUser.Name, repo.Name)
-       setCompareContext(ctx, baseCommit, headCommit, headTarget)
-
-       return false
-}
-
-func getBranchesAndTagsForRepo(user *models.User, repo *models.Repository) (bool, []string, []string, error) {
-       perm, err := models.GetUserRepoPermission(repo, user)
-       if err != nil {
-               return false, nil, nil, err
-       }
-       if !perm.CanRead(models.UnitTypeCode) {
-               return false, nil, nil, nil
-       }
-       gitRepo, err := git.OpenRepository(repo.RepoPath())
-       if err != nil {
-               return false, nil, nil, err
-       }
-       defer gitRepo.Close()
-
-       branches, _, err := gitRepo.GetBranches(0, 0)
-       if err != nil {
-               return false, nil, nil, err
-       }
-       tags, err := gitRepo.GetTags()
-       if err != nil {
-               return false, nil, nil, err
-       }
-       return true, branches, tags, nil
-}
-
-// CompareDiff show different from one commit to another commit
-func CompareDiff(ctx *context.Context) {
-       headUser, headRepo, headGitRepo, compareInfo, baseBranch, headBranch := ParseCompareInfo(ctx)
-
-       if ctx.Written() {
-               return
-       }
-       defer headGitRepo.Close()
-
-       nothingToCompare := PrepareCompareDiff(ctx, headUser, headRepo, headGitRepo, compareInfo, baseBranch, headBranch,
-               gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
-       if ctx.Written() {
-               return
-       }
-
-       baseGitRepo := ctx.Repo.GitRepo
-       baseTags, err := baseGitRepo.GetTags()
-       if err != nil {
-               ctx.ServerError("GetTags", err)
-               return
-       }
-       ctx.Data["Tags"] = baseTags
-
-       headBranches, _, err := headGitRepo.GetBranches(0, 0)
-       if err != nil {
-               ctx.ServerError("GetBranches", err)
-               return
-       }
-       ctx.Data["HeadBranches"] = headBranches
-
-       headTags, err := headGitRepo.GetTags()
-       if err != nil {
-               ctx.ServerError("GetTags", err)
-               return
-       }
-       ctx.Data["HeadTags"] = headTags
-
-       if ctx.Data["PageIsComparePull"] == true {
-               pr, err := models.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch)
-               if err != nil {
-                       if !models.IsErrPullRequestNotExist(err) {
-                               ctx.ServerError("GetUnmergedPullRequest", err)
-                               return
-                       }
-               } else {
-                       ctx.Data["HasPullRequest"] = true
-                       ctx.Data["PullRequest"] = pr
-                       ctx.HTML(http.StatusOK, tplCompareDiff)
-                       return
-               }
-
-               if !nothingToCompare {
-                       // Setup information for new form.
-                       RetrieveRepoMetas(ctx, ctx.Repo.Repository, true)
-                       if ctx.Written() {
-                               return
-                       }
-               }
-       }
-       beforeCommitID := ctx.Data["BeforeCommitID"].(string)
-       afterCommitID := ctx.Data["AfterCommitID"].(string)
-
-       ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + "..." + base.ShortSha(afterCommitID)
-
-       ctx.Data["IsRepoToolbarCommits"] = true
-       ctx.Data["IsDiffCompare"] = true
-       ctx.Data["RequireTribute"] = true
-       ctx.Data["RequireSimpleMDE"] = true
-       ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
-       setTemplateIfExists(ctx, pullRequestTemplateKey, nil, pullRequestTemplateCandidates)
-       ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
-       upload.AddUploadContext(ctx, "comment")
-
-       ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(models.UnitTypePullRequests)
-
-       ctx.HTML(http.StatusOK, tplCompare)
-}
-
-// ExcerptBlob render blob excerpt contents
-func ExcerptBlob(ctx *context.Context) {
-       commitID := ctx.Params("sha")
-       lastLeft := ctx.QueryInt("last_left")
-       lastRight := ctx.QueryInt("last_right")
-       idxLeft := ctx.QueryInt("left")
-       idxRight := ctx.QueryInt("right")
-       leftHunkSize := ctx.QueryInt("left_hunk_size")
-       rightHunkSize := ctx.QueryInt("right_hunk_size")
-       anchor := ctx.Query("anchor")
-       direction := ctx.Query("direction")
-       filePath := ctx.Query("path")
-       gitRepo := ctx.Repo.GitRepo
-       chunkSize := gitdiff.BlobExcerptChunkSize
-       commit, err := gitRepo.GetCommit(commitID)
-       if err != nil {
-               ctx.Error(http.StatusInternalServerError, "GetCommit")
-               return
-       }
-       section := &gitdiff.DiffSection{
-               FileName: filePath,
-               Name:     filePath,
-       }
-       if direction == "up" && (idxLeft-lastLeft) > chunkSize {
-               idxLeft -= chunkSize
-               idxRight -= chunkSize
-               leftHunkSize += chunkSize
-               rightHunkSize += chunkSize
-               section.Lines, err = getExcerptLines(commit, filePath, idxLeft-1, idxRight-1, chunkSize)
-       } else if direction == "down" && (idxLeft-lastLeft) > chunkSize {
-               section.Lines, err = getExcerptLines(commit, filePath, lastLeft, lastRight, chunkSize)
-               lastLeft += chunkSize
-               lastRight += chunkSize
-       } else {
-               section.Lines, err = getExcerptLines(commit, filePath, lastLeft, lastRight, idxRight-lastRight-1)
-               leftHunkSize = 0
-               rightHunkSize = 0
-               idxLeft = lastLeft
-               idxRight = lastRight
-       }
-       if err != nil {
-               ctx.Error(http.StatusInternalServerError, "getExcerptLines")
-               return
-       }
-       if idxRight > lastRight {
-               lineText := " "
-               if rightHunkSize > 0 || leftHunkSize > 0 {
-                       lineText = fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", idxLeft, leftHunkSize, idxRight, rightHunkSize)
-               }
-               lineText = html.EscapeString(lineText)
-               lineSection := &gitdiff.DiffLine{
-                       Type:    gitdiff.DiffLineSection,
-                       Content: lineText,
-                       SectionInfo: &gitdiff.DiffLineSectionInfo{
-                               Path:          filePath,
-                               LastLeftIdx:   lastLeft,
-                               LastRightIdx:  lastRight,
-                               LeftIdx:       idxLeft,
-                               RightIdx:      idxRight,
-                               LeftHunkSize:  leftHunkSize,
-                               RightHunkSize: rightHunkSize,
-                       }}
-               if direction == "up" {
-                       section.Lines = append([]*gitdiff.DiffLine{lineSection}, section.Lines...)
-               } else if direction == "down" {
-                       section.Lines = append(section.Lines, lineSection)
-               }
-       }
-       ctx.Data["section"] = section
-       ctx.Data["fileName"] = filePath
-       ctx.Data["AfterCommitID"] = commitID
-       ctx.Data["Anchor"] = anchor
-       ctx.HTML(http.StatusOK, tplBlobExcerpt)
-}
-
-func getExcerptLines(commit *git.Commit, filePath string, idxLeft int, idxRight int, chunkSize int) ([]*gitdiff.DiffLine, error) {
-       blob, err := commit.Tree.GetBlobByPath(filePath)
-       if err != nil {
-               return nil, err
-       }
-       reader, err := blob.DataAsync()
-       if err != nil {
-               return nil, err
-       }
-       defer reader.Close()
-       scanner := bufio.NewScanner(reader)
-       var diffLines []*gitdiff.DiffLine
-       for line := 0; line < idxRight+chunkSize; line++ {
-               if ok := scanner.Scan(); !ok {
-                       break
-               }
-               if line < idxRight {
-                       continue
-               }
-               lineText := scanner.Text()
-               diffLine := &gitdiff.DiffLine{
-                       LeftIdx:  idxLeft + (line - idxRight) + 1,
-                       RightIdx: line + 1,
-                       Type:     gitdiff.DiffLinePlain,
-                       Content:  " " + lineText,
-               }
-               diffLines = append(diffLines, diffLine)
-       }
-       return diffLines, nil
-}
diff --git a/routers/repo/download.go b/routers/repo/download.go
deleted file mode 100644 (file)
index bbf4684..0000000
+++ /dev/null
@@ -1,219 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2018 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 repo
-
-import (
-       "fmt"
-       "io"
-       "path"
-       "path/filepath"
-       "strings"
-
-       "code.gitea.io/gitea/modules/charset"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/git"
-       "code.gitea.io/gitea/modules/httpcache"
-       "code.gitea.io/gitea/modules/lfs"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/typesniffer"
-)
-
-// ServeData download file from io.Reader
-func ServeData(ctx *context.Context, name string, size int64, reader io.Reader) error {
-       buf := make([]byte, 1024)
-       n, err := reader.Read(buf)
-       if err != nil && err != io.EOF {
-               return err
-       }
-       if n >= 0 {
-               buf = buf[:n]
-       }
-
-       ctx.Resp.Header().Set("Cache-Control", "public,max-age=86400")
-
-       if size >= 0 {
-               ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", size))
-       } else {
-               log.Error("ServeData called to serve data: %s with size < 0: %d", name, size)
-       }
-       name = path.Base(name)
-
-       // Google Chrome dislike commas in filenames, so let's change it to a space
-       name = strings.ReplaceAll(name, ",", " ")
-
-       st := typesniffer.DetectContentType(buf)
-
-       if st.IsText() || ctx.QueryBool("render") {
-               cs, err := charset.DetectEncoding(buf)
-               if err != nil {
-                       log.Error("Detect raw file %s charset failed: %v, using by default utf-8", name, err)
-                       cs = "utf-8"
-               }
-               ctx.Resp.Header().Set("Content-Type", "text/plain; charset="+strings.ToLower(cs))
-       } else {
-               ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
-
-               if (st.IsImage() || st.IsPDF()) && (setting.UI.SVG.Enabled || !st.IsSvgImage()) {
-                       ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, name))
-                       if st.IsSvgImage() {
-                               ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
-                               ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
-                               ctx.Resp.Header().Set("Content-Type", typesniffer.SvgMimeType)
-                       }
-               } else {
-                       ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name))
-                       if setting.MimeTypeMap.Enabled {
-                               fileExtension := strings.ToLower(filepath.Ext(name))
-                               if mimetype, ok := setting.MimeTypeMap.Map[fileExtension]; ok {
-                                       ctx.Resp.Header().Set("Content-Type", mimetype)
-                               }
-                       }
-               }
-       }
-
-       _, err = ctx.Resp.Write(buf)
-       if err != nil {
-               return err
-       }
-       _, err = io.Copy(ctx.Resp, reader)
-       return err
-}
-
-// ServeBlob download a git.Blob
-func ServeBlob(ctx *context.Context, blob *git.Blob) error {
-       if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`) {
-               return nil
-       }
-
-       dataRc, err := blob.DataAsync()
-       if err != nil {
-               return err
-       }
-       defer func() {
-               if err = dataRc.Close(); err != nil {
-                       log.Error("ServeBlob: Close: %v", err)
-               }
-       }()
-
-       return ServeData(ctx, ctx.Repo.TreePath, blob.Size(), dataRc)
-}
-
-// ServeBlobOrLFS download a git.Blob redirecting to LFS if necessary
-func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob) error {
-       if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`) {
-               return nil
-       }
-
-       dataRc, err := blob.DataAsync()
-       if err != nil {
-               return err
-       }
-       closed := false
-       defer func() {
-               if closed {
-                       return
-               }
-               if err = dataRc.Close(); err != nil {
-                       log.Error("ServeBlobOrLFS: Close: %v", err)
-               }
-       }()
-
-       pointer, _ := lfs.ReadPointer(dataRc)
-       if pointer.IsValid() {
-               meta, _ := ctx.Repo.Repository.GetLFSMetaObjectByOid(pointer.Oid)
-               if meta == nil {
-                       if err = dataRc.Close(); err != nil {
-                               log.Error("ServeBlobOrLFS: Close: %v", err)
-                       }
-                       closed = true
-                       return ServeBlob(ctx, blob)
-               }
-               if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`) {
-                       return nil
-               }
-               lfsDataRc, err := lfs.ReadMetaObject(meta.Pointer)
-               if err != nil {
-                       return err
-               }
-               defer func() {
-                       if err = lfsDataRc.Close(); err != nil {
-                               log.Error("ServeBlobOrLFS: Close: %v", err)
-                       }
-               }()
-               return ServeData(ctx, ctx.Repo.TreePath, meta.Size, lfsDataRc)
-       }
-       if err = dataRc.Close(); err != nil {
-               log.Error("ServeBlobOrLFS: Close: %v", err)
-       }
-       closed = true
-
-       return ServeBlob(ctx, blob)
-}
-
-// SingleDownload download a file by repos path
-func SingleDownload(ctx *context.Context) {
-       blob, err := ctx.Repo.Commit.GetBlobByPath(ctx.Repo.TreePath)
-       if err != nil {
-               if git.IsErrNotExist(err) {
-                       ctx.NotFound("GetBlobByPath", nil)
-               } else {
-                       ctx.ServerError("GetBlobByPath", err)
-               }
-               return
-       }
-       if err = ServeBlob(ctx, blob); err != nil {
-               ctx.ServerError("ServeBlob", err)
-       }
-}
-
-// SingleDownloadOrLFS download a file by repos path redirecting to LFS if necessary
-func SingleDownloadOrLFS(ctx *context.Context) {
-       blob, err := ctx.Repo.Commit.GetBlobByPath(ctx.Repo.TreePath)
-       if err != nil {
-               if git.IsErrNotExist(err) {
-                       ctx.NotFound("GetBlobByPath", nil)
-               } else {
-                       ctx.ServerError("GetBlobByPath", err)
-               }
-               return
-       }
-       if err = ServeBlobOrLFS(ctx, blob); err != nil {
-               ctx.ServerError("ServeBlobOrLFS", err)
-       }
-}
-
-// DownloadByID download a file by sha1 ID
-func DownloadByID(ctx *context.Context) {
-       blob, err := ctx.Repo.GitRepo.GetBlob(ctx.Params("sha"))
-       if err != nil {
-               if git.IsErrNotExist(err) {
-                       ctx.NotFound("GetBlob", nil)
-               } else {
-                       ctx.ServerError("GetBlob", err)
-               }
-               return
-       }
-       if err = ServeBlob(ctx, blob); err != nil {
-               ctx.ServerError("ServeBlob", err)
-       }
-}
-
-// DownloadByIDOrLFS download a file by sha1 ID taking account of LFS
-func DownloadByIDOrLFS(ctx *context.Context) {
-       blob, err := ctx.Repo.GitRepo.GetBlob(ctx.Params("sha"))
-       if err != nil {
-               if git.IsErrNotExist(err) {
-                       ctx.NotFound("GetBlob", nil)
-               } else {
-                       ctx.ServerError("GetBlob", err)
-               }
-               return
-       }
-       if err = ServeBlobOrLFS(ctx, blob); err != nil {
-               ctx.ServerError("ServeBlob", err)
-       }
-}
diff --git a/routers/repo/editor.go b/routers/repo/editor.go
deleted file mode 100644 (file)
index 0f978c7..0000000
+++ /dev/null
@@ -1,831 +0,0 @@
-// Copyright 2016 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 repo
-
-import (
-       "fmt"
-       "io/ioutil"
-       "net/http"
-       "path"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/charset"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/git"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/repofiles"
-       repo_module "code.gitea.io/gitea/modules/repository"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/typesniffer"
-       "code.gitea.io/gitea/modules/upload"
-       "code.gitea.io/gitea/modules/util"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/routers/utils"
-       "code.gitea.io/gitea/services/forms"
-       jsoniter "github.com/json-iterator/go"
-)
-
-const (
-       tplEditFile        base.TplName = "repo/editor/edit"
-       tplEditDiffPreview base.TplName = "repo/editor/diff_preview"
-       tplDeleteFile      base.TplName = "repo/editor/delete"
-       tplUploadFile      base.TplName = "repo/editor/upload"
-
-       frmCommitChoiceDirect    string = "direct"
-       frmCommitChoiceNewBranch string = "commit-to-new-branch"
-)
-
-func renderCommitRights(ctx *context.Context) bool {
-       canCommitToBranch, err := ctx.Repo.CanCommitToBranch(ctx.User)
-       if err != nil {
-               log.Error("CanCommitToBranch: %v", err)
-       }
-       ctx.Data["CanCommitToBranch"] = canCommitToBranch
-
-       return canCommitToBranch.CanCommitToBranch
-}
-
-// getParentTreeFields returns list of parent tree names and corresponding tree paths
-// based on given tree path.
-func getParentTreeFields(treePath string) (treeNames []string, treePaths []string) {
-       if len(treePath) == 0 {
-               return treeNames, treePaths
-       }
-
-       treeNames = strings.Split(treePath, "/")
-       treePaths = make([]string, len(treeNames))
-       for i := range treeNames {
-               treePaths[i] = strings.Join(treeNames[:i+1], "/")
-       }
-       return treeNames, treePaths
-}
-
-func editFile(ctx *context.Context, isNewFile bool) {
-       ctx.Data["PageIsEdit"] = true
-       ctx.Data["IsNewFile"] = isNewFile
-       ctx.Data["RequireHighlightJS"] = true
-       ctx.Data["RequireSimpleMDE"] = true
-       canCommit := renderCommitRights(ctx)
-
-       treePath := cleanUploadFileName(ctx.Repo.TreePath)
-       if treePath != ctx.Repo.TreePath {
-               if isNewFile {
-                       ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_new", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
-               } else {
-                       ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_edit", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
-               }
-               return
-       }
-
-       treeNames, treePaths := getParentTreeFields(ctx.Repo.TreePath)
-
-       if !isNewFile {
-               entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
-               if err != nil {
-                       ctx.NotFoundOrServerError("GetTreeEntryByPath", git.IsErrNotExist, err)
-                       return
-               }
-
-               // No way to edit a directory online.
-               if entry.IsDir() {
-                       ctx.NotFound("entry.IsDir", nil)
-                       return
-               }
-
-               blob := entry.Blob()
-               if blob.Size() >= setting.UI.MaxDisplayFileSize {
-                       ctx.NotFound("blob.Size", err)
-                       return
-               }
-
-               dataRc, err := blob.DataAsync()
-               if err != nil {
-                       ctx.NotFound("blob.Data", err)
-                       return
-               }
-
-               defer dataRc.Close()
-
-               ctx.Data["FileSize"] = blob.Size()
-               ctx.Data["FileName"] = blob.Name()
-
-               buf := make([]byte, 1024)
-               n, _ := dataRc.Read(buf)
-               buf = buf[:n]
-
-               // Only some file types are editable online as text.
-               if !typesniffer.DetectContentType(buf).IsRepresentableAsText() {
-                       ctx.NotFound("typesniffer.IsRepresentableAsText", nil)
-                       return
-               }
-
-               d, _ := ioutil.ReadAll(dataRc)
-               if err := dataRc.Close(); err != nil {
-                       log.Error("Error whilst closing blob data: %v", err)
-               }
-
-               buf = append(buf, d...)
-               if content, err := charset.ToUTF8WithErr(buf); err != nil {
-                       log.Error("ToUTF8WithErr: %v", err)
-                       ctx.Data["FileContent"] = string(buf)
-               } else {
-                       ctx.Data["FileContent"] = content
-               }
-       } else {
-               treeNames = append(treeNames, "") // Append empty string to allow user name the new file.
-       }
-
-       ctx.Data["TreeNames"] = treeNames
-       ctx.Data["TreePaths"] = treePaths
-       ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
-       ctx.Data["commit_summary"] = ""
-       ctx.Data["commit_message"] = ""
-       if canCommit {
-               ctx.Data["commit_choice"] = frmCommitChoiceDirect
-       } else {
-               ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
-       }
-       ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
-       ctx.Data["last_commit"] = ctx.Repo.CommitID
-       ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",")
-       ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
-       ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",")
-       ctx.Data["Editorconfig"] = GetEditorConfig(ctx, treePath)
-
-       ctx.HTML(http.StatusOK, tplEditFile)
-}
-
-// GetEditorConfig returns a editorconfig JSON string for given treePath or "null"
-func GetEditorConfig(ctx *context.Context, treePath string) string {
-       ec, err := ctx.Repo.GetEditorconfig()
-       if err == nil {
-               def, err := ec.GetDefinitionForFilename(treePath)
-               if err == nil {
-                       json := jsoniter.ConfigCompatibleWithStandardLibrary
-                       jsonStr, _ := json.Marshal(def)
-                       return string(jsonStr)
-               }
-       }
-       return "null"
-}
-
-// EditFile render edit file page
-func EditFile(ctx *context.Context) {
-       editFile(ctx, false)
-}
-
-// NewFile render create file page
-func NewFile(ctx *context.Context) {
-       editFile(ctx, true)
-}
-
-func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile bool) {
-       canCommit := renderCommitRights(ctx)
-       treeNames, treePaths := getParentTreeFields(form.TreePath)
-       branchName := ctx.Repo.BranchName
-       if form.CommitChoice == frmCommitChoiceNewBranch {
-               branchName = form.NewBranchName
-       }
-
-       ctx.Data["PageIsEdit"] = true
-       ctx.Data["PageHasPosted"] = true
-       ctx.Data["IsNewFile"] = isNewFile
-       ctx.Data["RequireHighlightJS"] = true
-       ctx.Data["RequireSimpleMDE"] = true
-       ctx.Data["TreePath"] = form.TreePath
-       ctx.Data["TreeNames"] = treeNames
-       ctx.Data["TreePaths"] = treePaths
-       ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + ctx.Repo.BranchName
-       ctx.Data["FileContent"] = form.Content
-       ctx.Data["commit_summary"] = form.CommitSummary
-       ctx.Data["commit_message"] = form.CommitMessage
-       ctx.Data["commit_choice"] = form.CommitChoice
-       ctx.Data["new_branch_name"] = form.NewBranchName
-       ctx.Data["last_commit"] = ctx.Repo.CommitID
-       ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",")
-       ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
-       ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",")
-       ctx.Data["Editorconfig"] = GetEditorConfig(ctx, form.TreePath)
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplEditFile)
-               return
-       }
-
-       // Cannot commit to a an existing branch if user doesn't have rights
-       if branchName == ctx.Repo.BranchName && !canCommit {
-               ctx.Data["Err_NewBranchName"] = true
-               ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
-               ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form)
-               return
-       }
-
-       // CommitSummary is optional in the web form, if empty, give it a default message based on add or update
-       // `message` will be both the summary and message combined
-       message := strings.TrimSpace(form.CommitSummary)
-       if len(message) == 0 {
-               if isNewFile {
-                       message = ctx.Tr("repo.editor.add", form.TreePath)
-               } else {
-                       message = ctx.Tr("repo.editor.update", form.TreePath)
-               }
-       }
-       form.CommitMessage = strings.TrimSpace(form.CommitMessage)
-       if len(form.CommitMessage) > 0 {
-               message += "\n\n" + form.CommitMessage
-       }
-
-       if _, err := repofiles.CreateOrUpdateRepoFile(ctx.Repo.Repository, ctx.User, &repofiles.UpdateRepoFileOptions{
-               LastCommitID: form.LastCommit,
-               OldBranch:    ctx.Repo.BranchName,
-               NewBranch:    branchName,
-               FromTreePath: ctx.Repo.TreePath,
-               TreePath:     form.TreePath,
-               Message:      message,
-               Content:      strings.ReplaceAll(form.Content, "\r", ""),
-               IsNewFile:    isNewFile,
-               Signoff:      form.Signoff,
-       }); err != nil {
-               // This is where we handle all the errors thrown by repofiles.CreateOrUpdateRepoFile
-               if git.IsErrNotExist(err) {
-                       ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form)
-               } else if models.IsErrLFSFileLocked(err) {
-                       ctx.Data["Err_TreePath"] = true
-                       ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(models.ErrLFSFileLocked).Path, err.(models.ErrLFSFileLocked).UserName), tplEditFile, &form)
-               } else if models.IsErrFilenameInvalid(err) {
-                       ctx.Data["Err_TreePath"] = true
-                       ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplEditFile, &form)
-               } else if models.IsErrFilePathInvalid(err) {
-                       ctx.Data["Err_TreePath"] = true
-                       if fileErr, ok := err.(models.ErrFilePathInvalid); ok {
-                               switch fileErr.Type {
-                               case git.EntryModeSymlink:
-                                       ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplEditFile, &form)
-                               case git.EntryModeTree:
-                                       ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplEditFile, &form)
-                               case git.EntryModeBlob:
-                                       ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplEditFile, &form)
-                               default:
-                                       ctx.Error(http.StatusInternalServerError, err.Error())
-                               }
-                       } else {
-                               ctx.Error(http.StatusInternalServerError, err.Error())
-                       }
-               } else if models.IsErrRepoFileAlreadyExists(err) {
-                       ctx.Data["Err_TreePath"] = true
-                       ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplEditFile, &form)
-               } else if git.IsErrBranchNotExist(err) {
-                       // For when a user adds/updates a file to a branch that no longer exists
-                       if branchErr, ok := err.(git.ErrBranchNotExist); ok {
-                               ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplEditFile, &form)
-                       } else {
-                               ctx.Error(http.StatusInternalServerError, err.Error())
-                       }
-               } else if models.IsErrBranchAlreadyExists(err) {
-                       // For when a user specifies a new branch that already exists
-                       ctx.Data["Err_NewBranchName"] = true
-                       if branchErr, ok := err.(models.ErrBranchAlreadyExists); ok {
-                               ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form)
-                       } else {
-                               ctx.Error(http.StatusInternalServerError, err.Error())
-                       }
-               } else if models.IsErrCommitIDDoesNotMatch(err) {
-                       ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplEditFile, &form)
-               } else if git.IsErrPushOutOfDate(err) {
-                       ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+util.PathEscapeSegments(form.NewBranchName)), tplEditFile, &form)
-               } else if git.IsErrPushRejected(err) {
-                       errPushRej := err.(*git.ErrPushRejected)
-                       if len(errPushRej.Message) == 0 {
-                               ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form)
-                       } else {
-                               flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
-                                       "Message": ctx.Tr("repo.editor.push_rejected"),
-                                       "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
-                                       "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
-                               })
-                               if err != nil {
-                                       ctx.ServerError("editFilePost.HTMLString", err)
-                                       return
-                               }
-                               ctx.RenderWithErr(flashError, tplEditFile, &form)
-                       }
-               } else {
-                       flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
-                               "Message": ctx.Tr("repo.editor.fail_to_update_file", form.TreePath),
-                               "Summary": ctx.Tr("repo.editor.fail_to_update_file_summary"),
-                               "Details": utils.SanitizeFlashErrorString(err.Error()),
-                       })
-                       if err != nil {
-                               ctx.ServerError("editFilePost.HTMLString", err)
-                               return
-                       }
-                       ctx.RenderWithErr(flashError, tplEditFile, &form)
-               }
-       }
-
-       if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(models.UnitTypePullRequests) {
-               ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
-       } else {
-               ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(form.TreePath))
-       }
-}
-
-// EditFilePost response for editing file
-func EditFilePost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.EditRepoFileForm)
-       editFilePost(ctx, *form, false)
-}
-
-// NewFilePost response for creating file
-func NewFilePost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.EditRepoFileForm)
-       editFilePost(ctx, *form, true)
-}
-
-// DiffPreviewPost render preview diff page
-func DiffPreviewPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.EditPreviewDiffForm)
-       treePath := cleanUploadFileName(ctx.Repo.TreePath)
-       if len(treePath) == 0 {
-               ctx.Error(http.StatusInternalServerError, "file name to diff is invalid")
-               return
-       }
-
-       entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
-       if err != nil {
-               ctx.Error(http.StatusInternalServerError, "GetTreeEntryByPath: "+err.Error())
-               return
-       } else if entry.IsDir() {
-               ctx.Error(http.StatusUnprocessableEntity)
-               return
-       }
-
-       diff, err := repofiles.GetDiffPreview(ctx.Repo.Repository, ctx.Repo.BranchName, treePath, form.Content)
-       if err != nil {
-               ctx.Error(http.StatusInternalServerError, "GetDiffPreview: "+err.Error())
-               return
-       }
-
-       if diff.NumFiles == 0 {
-               ctx.PlainText(200, []byte(ctx.Tr("repo.editor.no_changes_to_show")))
-               return
-       }
-       ctx.Data["File"] = diff.Files[0]
-
-       ctx.HTML(http.StatusOK, tplEditDiffPreview)
-}
-
-// DeleteFile render delete file page
-func DeleteFile(ctx *context.Context) {
-       ctx.Data["PageIsDelete"] = true
-       ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
-       treePath := cleanUploadFileName(ctx.Repo.TreePath)
-
-       if treePath != ctx.Repo.TreePath {
-               ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_delete", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
-               return
-       }
-
-       ctx.Data["TreePath"] = treePath
-       canCommit := renderCommitRights(ctx)
-
-       ctx.Data["commit_summary"] = ""
-       ctx.Data["commit_message"] = ""
-       ctx.Data["last_commit"] = ctx.Repo.CommitID
-       if canCommit {
-               ctx.Data["commit_choice"] = frmCommitChoiceDirect
-       } else {
-               ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
-       }
-       ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
-
-       ctx.HTML(http.StatusOK, tplDeleteFile)
-}
-
-// DeleteFilePost response for deleting file
-func DeleteFilePost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.DeleteRepoFileForm)
-       canCommit := renderCommitRights(ctx)
-       branchName := ctx.Repo.BranchName
-       if form.CommitChoice == frmCommitChoiceNewBranch {
-               branchName = form.NewBranchName
-       }
-
-       ctx.Data["PageIsDelete"] = true
-       ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
-       ctx.Data["TreePath"] = ctx.Repo.TreePath
-       ctx.Data["commit_summary"] = form.CommitSummary
-       ctx.Data["commit_message"] = form.CommitMessage
-       ctx.Data["commit_choice"] = form.CommitChoice
-       ctx.Data["new_branch_name"] = form.NewBranchName
-       ctx.Data["last_commit"] = ctx.Repo.CommitID
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplDeleteFile)
-               return
-       }
-
-       if branchName == ctx.Repo.BranchName && !canCommit {
-               ctx.Data["Err_NewBranchName"] = true
-               ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
-               ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplDeleteFile, &form)
-               return
-       }
-
-       message := strings.TrimSpace(form.CommitSummary)
-       if len(message) == 0 {
-               message = ctx.Tr("repo.editor.delete", ctx.Repo.TreePath)
-       }
-       form.CommitMessage = strings.TrimSpace(form.CommitMessage)
-       if len(form.CommitMessage) > 0 {
-               message += "\n\n" + form.CommitMessage
-       }
-
-       if _, err := repofiles.DeleteRepoFile(ctx.Repo.Repository, ctx.User, &repofiles.DeleteRepoFileOptions{
-               LastCommitID: form.LastCommit,
-               OldBranch:    ctx.Repo.BranchName,
-               NewBranch:    branchName,
-               TreePath:     ctx.Repo.TreePath,
-               Message:      message,
-               Signoff:      form.Signoff,
-       }); err != nil {
-               // This is where we handle all the errors thrown by repofiles.DeleteRepoFile
-               if git.IsErrNotExist(err) || models.IsErrRepoFileDoesNotExist(err) {
-                       ctx.RenderWithErr(ctx.Tr("repo.editor.file_deleting_no_longer_exists", ctx.Repo.TreePath), tplDeleteFile, &form)
-               } else if models.IsErrFilenameInvalid(err) {
-                       ctx.Data["Err_TreePath"] = true
-                       ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", ctx.Repo.TreePath), tplDeleteFile, &form)
-               } else if models.IsErrFilePathInvalid(err) {
-                       ctx.Data["Err_TreePath"] = true
-                       if fileErr, ok := err.(models.ErrFilePathInvalid); ok {
-                               switch fileErr.Type {
-                               case git.EntryModeSymlink:
-                                       ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplDeleteFile, &form)
-                               case git.EntryModeTree:
-                                       ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplDeleteFile, &form)
-                               case git.EntryModeBlob:
-                                       ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplDeleteFile, &form)
-                               default:
-                                       ctx.ServerError("DeleteRepoFile", err)
-                               }
-                       } else {
-                               ctx.ServerError("DeleteRepoFile", err)
-                       }
-               } else if git.IsErrBranchNotExist(err) {
-                       // For when a user deletes a file to a branch that no longer exists
-                       if branchErr, ok := err.(git.ErrBranchNotExist); ok {
-                               ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplDeleteFile, &form)
-                       } else {
-                               ctx.Error(http.StatusInternalServerError, err.Error())
-                       }
-               } else if models.IsErrBranchAlreadyExists(err) {
-                       // For when a user specifies a new branch that already exists
-                       if branchErr, ok := err.(models.ErrBranchAlreadyExists); ok {
-                               ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplDeleteFile, &form)
-                       } else {
-                               ctx.Error(http.StatusInternalServerError, err.Error())
-                       }
-               } else if models.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) {
-                       ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_deleting", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplDeleteFile, &form)
-               } else if git.IsErrPushRejected(err) {
-                       errPushRej := err.(*git.ErrPushRejected)
-                       if len(errPushRej.Message) == 0 {
-                               ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form)
-                       } else {
-                               flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
-                                       "Message": ctx.Tr("repo.editor.push_rejected"),
-                                       "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
-                                       "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
-                               })
-                               if err != nil {
-                                       ctx.ServerError("DeleteFilePost.HTMLString", err)
-                                       return
-                               }
-                               ctx.RenderWithErr(flashError, tplDeleteFile, &form)
-                       }
-               } else {
-                       ctx.ServerError("DeleteRepoFile", err)
-               }
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath))
-       if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(models.UnitTypePullRequests) {
-               ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
-       } else {
-               treePath := path.Dir(ctx.Repo.TreePath)
-               if treePath == "." {
-                       treePath = "" // the file deleted was in the root, so we return the user to the root directory
-               }
-               if len(treePath) > 0 {
-                       // Need to get the latest commit since it changed
-                       commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
-                       if err == nil && commit != nil {
-                               // We have the comment, now find what directory we can return the user to
-                               // (must have entries)
-                               treePath = GetClosestParentWithFiles(treePath, commit)
-                       } else {
-                               treePath = "" // otherwise return them to the root of the repo
-                       }
-               }
-               ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(treePath))
-       }
-}
-
-// UploadFile render upload file page
-func UploadFile(ctx *context.Context) {
-       ctx.Data["PageIsUpload"] = true
-       ctx.Data["RequireTribute"] = true
-       ctx.Data["RequireSimpleMDE"] = true
-       upload.AddUploadContext(ctx, "repo")
-       canCommit := renderCommitRights(ctx)
-       treePath := cleanUploadFileName(ctx.Repo.TreePath)
-       if treePath != ctx.Repo.TreePath {
-               ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_upload", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
-               return
-       }
-       ctx.Repo.TreePath = treePath
-
-       treeNames, treePaths := getParentTreeFields(ctx.Repo.TreePath)
-       if len(treeNames) == 0 {
-               // We must at least have one element for user to input.
-               treeNames = []string{""}
-       }
-
-       ctx.Data["TreeNames"] = treeNames
-       ctx.Data["TreePaths"] = treePaths
-       ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
-       ctx.Data["commit_summary"] = ""
-       ctx.Data["commit_message"] = ""
-       if canCommit {
-               ctx.Data["commit_choice"] = frmCommitChoiceDirect
-       } else {
-               ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
-       }
-       ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
-
-       ctx.HTML(http.StatusOK, tplUploadFile)
-}
-
-// UploadFilePost response for uploading file
-func UploadFilePost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.UploadRepoFileForm)
-       ctx.Data["PageIsUpload"] = true
-       ctx.Data["RequireTribute"] = true
-       ctx.Data["RequireSimpleMDE"] = true
-       upload.AddUploadContext(ctx, "repo")
-       canCommit := renderCommitRights(ctx)
-
-       oldBranchName := ctx.Repo.BranchName
-       branchName := oldBranchName
-
-       if form.CommitChoice == frmCommitChoiceNewBranch {
-               branchName = form.NewBranchName
-       }
-
-       form.TreePath = cleanUploadFileName(form.TreePath)
-
-       treeNames, treePaths := getParentTreeFields(form.TreePath)
-       if len(treeNames) == 0 {
-               // We must at least have one element for user to input.
-               treeNames = []string{""}
-       }
-
-       ctx.Data["TreePath"] = form.TreePath
-       ctx.Data["TreeNames"] = treeNames
-       ctx.Data["TreePaths"] = treePaths
-       ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + branchName
-       ctx.Data["commit_summary"] = form.CommitSummary
-       ctx.Data["commit_message"] = form.CommitMessage
-       ctx.Data["commit_choice"] = form.CommitChoice
-       ctx.Data["new_branch_name"] = branchName
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplUploadFile)
-               return
-       }
-
-       if oldBranchName != branchName {
-               if _, err := repo_module.GetBranch(ctx.Repo.Repository, branchName); err == nil {
-                       ctx.Data["Err_NewBranchName"] = true
-                       ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tplUploadFile, &form)
-                       return
-               }
-       } else if !canCommit {
-               ctx.Data["Err_NewBranchName"] = true
-               ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
-               ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplUploadFile, &form)
-               return
-       }
-
-       var newTreePath string
-       for _, part := range treeNames {
-               newTreePath = path.Join(newTreePath, part)
-               entry, err := ctx.Repo.Commit.GetTreeEntryByPath(newTreePath)
-               if err != nil {
-                       if git.IsErrNotExist(err) {
-                               // Means there is no item with that name, so we're good
-                               break
-                       }
-
-                       ctx.ServerError("Repo.Commit.GetTreeEntryByPath", err)
-                       return
-               }
-
-               // User can only upload files to a directory.
-               if !entry.IsDir() {
-                       ctx.Data["Err_TreePath"] = true
-                       ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", part), tplUploadFile, &form)
-                       return
-               }
-       }
-
-       message := strings.TrimSpace(form.CommitSummary)
-       if len(message) == 0 {
-               message = ctx.Tr("repo.editor.upload_files_to_dir", form.TreePath)
-       }
-
-       form.CommitMessage = strings.TrimSpace(form.CommitMessage)
-       if len(form.CommitMessage) > 0 {
-               message += "\n\n" + form.CommitMessage
-       }
-
-       if err := repofiles.UploadRepoFiles(ctx.Repo.Repository, ctx.User, &repofiles.UploadRepoFileOptions{
-               LastCommitID: ctx.Repo.CommitID,
-               OldBranch:    oldBranchName,
-               NewBranch:    branchName,
-               TreePath:     form.TreePath,
-               Message:      message,
-               Files:        form.Files,
-               Signoff:      form.Signoff,
-       }); err != nil {
-               if models.IsErrLFSFileLocked(err) {
-                       ctx.Data["Err_TreePath"] = true
-                       ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(models.ErrLFSFileLocked).Path, err.(models.ErrLFSFileLocked).UserName), tplUploadFile, &form)
-               } else if models.IsErrFilenameInvalid(err) {
-                       ctx.Data["Err_TreePath"] = true
-                       ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplUploadFile, &form)
-               } else if models.IsErrFilePathInvalid(err) {
-                       ctx.Data["Err_TreePath"] = true
-                       fileErr := err.(models.ErrFilePathInvalid)
-                       switch fileErr.Type {
-                       case git.EntryModeSymlink:
-                               ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplUploadFile, &form)
-                       case git.EntryModeTree:
-                               ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplUploadFile, &form)
-                       case git.EntryModeBlob:
-                               ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplUploadFile, &form)
-                       default:
-                               ctx.Error(http.StatusInternalServerError, err.Error())
-                       }
-               } else if models.IsErrRepoFileAlreadyExists(err) {
-                       ctx.Data["Err_TreePath"] = true
-                       ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplUploadFile, &form)
-               } else if git.IsErrBranchNotExist(err) {
-                       branchErr := err.(git.ErrBranchNotExist)
-                       ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplUploadFile, &form)
-               } else if models.IsErrBranchAlreadyExists(err) {
-                       // For when a user specifies a new branch that already exists
-                       ctx.Data["Err_NewBranchName"] = true
-                       branchErr := err.(models.ErrBranchAlreadyExists)
-                       ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplUploadFile, &form)
-               } else if git.IsErrPushOutOfDate(err) {
-                       ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+ctx.Repo.CommitID+"..."+util.PathEscapeSegments(form.NewBranchName)), tplUploadFile, &form)
-               } else if git.IsErrPushRejected(err) {
-                       errPushRej := err.(*git.ErrPushRejected)
-                       if len(errPushRej.Message) == 0 {
-                               ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplUploadFile, &form)
-                       } else {
-                               flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
-                                       "Message": ctx.Tr("repo.editor.push_rejected"),
-                                       "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
-                                       "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
-                               })
-                               if err != nil {
-                                       ctx.ServerError("UploadFilePost.HTMLString", err)
-                                       return
-                               }
-                               ctx.RenderWithErr(flashError, tplUploadFile, &form)
-                       }
-               } else {
-                       // os.ErrNotExist - upload file missing in the intervening time?!
-                       log.Error("Error during upload to repo: %-v to filepath: %s on %s from %s: %v", ctx.Repo.Repository, form.TreePath, oldBranchName, form.NewBranchName, err)
-                       ctx.RenderWithErr(ctx.Tr("repo.editor.unable_to_upload_files", form.TreePath, err), tplUploadFile, &form)
-               }
-               return
-       }
-
-       if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(models.UnitTypePullRequests) {
-               ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
-       } else {
-               ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(form.TreePath))
-       }
-}
-
-func cleanUploadFileName(name string) string {
-       // Rebase the filename
-       name = strings.Trim(path.Clean("/"+name), " /")
-       // Git disallows any filenames to have a .git directory in them.
-       for _, part := range strings.Split(name, "/") {
-               if strings.ToLower(part) == ".git" {
-                       return ""
-               }
-       }
-       return name
-}
-
-// UploadFileToServer upload file to server file dir not git
-func UploadFileToServer(ctx *context.Context) {
-       file, header, err := ctx.Req.FormFile("file")
-       if err != nil {
-               ctx.Error(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err))
-               return
-       }
-       defer file.Close()
-
-       buf := make([]byte, 1024)
-       n, _ := file.Read(buf)
-       if n > 0 {
-               buf = buf[:n]
-       }
-
-       err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes)
-       if err != nil {
-               ctx.Error(http.StatusBadRequest, err.Error())
-               return
-       }
-
-       name := cleanUploadFileName(header.Filename)
-       if len(name) == 0 {
-               ctx.Error(http.StatusInternalServerError, "Upload file name is invalid")
-               return
-       }
-
-       upload, err := models.NewUpload(name, buf, file)
-       if err != nil {
-               ctx.Error(http.StatusInternalServerError, fmt.Sprintf("NewUpload: %v", err))
-               return
-       }
-
-       log.Trace("New file uploaded: %s", upload.UUID)
-       ctx.JSON(http.StatusOK, map[string]string{
-               "uuid": upload.UUID,
-       })
-}
-
-// RemoveUploadFileFromServer remove file from server file dir
-func RemoveUploadFileFromServer(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.RemoveUploadFileForm)
-       if len(form.File) == 0 {
-               ctx.Status(204)
-               return
-       }
-
-       if err := models.DeleteUploadByUUID(form.File); err != nil {
-               ctx.Error(http.StatusInternalServerError, fmt.Sprintf("DeleteUploadByUUID: %v", err))
-               return
-       }
-
-       log.Trace("Upload file removed: %s", form.File)
-       ctx.Status(204)
-}
-
-// GetUniquePatchBranchName Gets a unique branch name for a new patch branch
-// It will be in the form of <username>-patch-<num> where <num> is the first branch of this format
-// that doesn't already exist. If we exceed 1000 tries or an error is thrown, we just return "" so the user has to
-// type in the branch name themselves (will be an empty field)
-func GetUniquePatchBranchName(ctx *context.Context) string {
-       prefix := ctx.User.LowerName + "-patch-"
-       for i := 1; i <= 1000; i++ {
-               branchName := fmt.Sprintf("%s%d", prefix, i)
-               if _, err := repo_module.GetBranch(ctx.Repo.Repository, branchName); err != nil {
-                       if git.IsErrBranchNotExist(err) {
-                               return branchName
-                       }
-                       log.Error("GetUniquePatchBranchName: %v", err)
-                       return ""
-               }
-       }
-       return ""
-}
-
-// GetClosestParentWithFiles Recursively gets the path of parent in a tree that has files (used when file in a tree is
-// deleted). Returns "" for the root if no parents other than the root have files. If the given treePath isn't a
-// SubTree or it has no entries, we go up one dir and see if we can return the user to that listing.
-func GetClosestParentWithFiles(treePath string, commit *git.Commit) string {
-       if len(treePath) == 0 || treePath == "." {
-               return ""
-       }
-       // see if the tree has entries
-       if tree, err := commit.SubTree(treePath); err != nil {
-               // failed to get tree, going up a dir
-               return GetClosestParentWithFiles(path.Dir(treePath), commit)
-       } else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 {
-               // no files in this dir, going up a dir
-               return GetClosestParentWithFiles(path.Dir(treePath), commit)
-       }
-       return treePath
-}
diff --git a/routers/repo/editor_test.go b/routers/repo/editor_test.go
deleted file mode 100644 (file)
index ec7aee1..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-// Copyright 2018 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 repo
-
-import (
-       "testing"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/git"
-       "code.gitea.io/gitea/modules/test"
-
-       "github.com/stretchr/testify/assert"
-)
-
-func TestCleanUploadName(t *testing.T) {
-       models.PrepareTestEnv(t)
-
-       var kases = map[string]string{
-               ".git/refs/master":               "",
-               "/root/abc":                      "root/abc",
-               "./../../abc":                    "abc",
-               "a/../.git":                      "",
-               "a/../../../abc":                 "abc",
-               "../../../acd":                   "acd",
-               "../../.git/abc":                 "",
-               "..\\..\\.git/abc":               "..\\..\\.git/abc",
-               "..\\../.git/abc":                "",
-               "..\\../.git":                    "",
-               "abc/../def":                     "def",
-               ".drone.yml":                     ".drone.yml",
-               ".abc/def/.drone.yml":            ".abc/def/.drone.yml",
-               "..drone.yml.":                   "..drone.yml.",
-               "..a.dotty...name...":            "..a.dotty...name...",
-               "..a.dotty../.folder../.name...": "..a.dotty../.folder../.name...",
-       }
-       for k, v := range kases {
-               assert.EqualValues(t, cleanUploadFileName(k), v)
-       }
-}
-
-func TestGetUniquePatchBranchName(t *testing.T) {
-       models.PrepareTestEnv(t)
-       ctx := test.MockContext(t, "user2/repo1")
-       ctx.SetParams(":id", "1")
-       test.LoadRepo(t, ctx, 1)
-       test.LoadRepoCommit(t, ctx)
-       test.LoadUser(t, ctx, 2)
-       test.LoadGitRepo(t, ctx)
-       defer ctx.Repo.GitRepo.Close()
-
-       expectedBranchName := "user2-patch-1"
-       branchName := GetUniquePatchBranchName(ctx)
-       assert.Equal(t, expectedBranchName, branchName)
-}
-
-func TestGetClosestParentWithFiles(t *testing.T) {
-       models.PrepareTestEnv(t)
-       ctx := test.MockContext(t, "user2/repo1")
-       ctx.SetParams(":id", "1")
-       test.LoadRepo(t, ctx, 1)
-       test.LoadRepoCommit(t, ctx)
-       test.LoadUser(t, ctx, 2)
-       test.LoadGitRepo(t, ctx)
-       defer ctx.Repo.GitRepo.Close()
-
-       repo := ctx.Repo.Repository
-       branch := repo.DefaultBranch
-       gitRepo, _ := git.OpenRepository(repo.RepoPath())
-       defer gitRepo.Close()
-       commit, _ := gitRepo.GetBranchCommit(branch)
-       expectedTreePath := ""
-
-       expectedTreePath = "" // Should return the root dir, empty string, since there are no subdirs in this repo
-       for _, deletedFile := range []string{
-               "dir1/dir2/dir3/file.txt",
-               "file.txt",
-       } {
-               treePath := GetClosestParentWithFiles(deletedFile, commit)
-               assert.Equal(t, expectedTreePath, treePath)
-       }
-}
diff --git a/routers/repo/http.go b/routers/repo/http.go
deleted file mode 100644 (file)
index 30d382b..0000000
+++ /dev/null
@@ -1,602 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2019 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 repo
-
-import (
-       "bytes"
-       "compress/gzip"
-       gocontext "context"
-       "fmt"
-       "io/ioutil"
-       "net/http"
-       "os"
-       "os/exec"
-       "path"
-       "regexp"
-       "strconv"
-       "strings"
-       "sync"
-       "time"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/git"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/process"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/structs"
-       "code.gitea.io/gitea/modules/util"
-       repo_service "code.gitea.io/gitea/services/repository"
-)
-
-// httpBase implmentation git smart HTTP protocol
-func httpBase(ctx *context.Context) (h *serviceHandler) {
-       if setting.Repository.DisableHTTPGit {
-               ctx.Resp.WriteHeader(http.StatusForbidden)
-               _, err := ctx.Resp.Write([]byte("Interacting with repositories by HTTP protocol is not allowed"))
-               if err != nil {
-                       log.Error(err.Error())
-               }
-               return
-       }
-
-       if len(setting.Repository.AccessControlAllowOrigin) > 0 {
-               allowedOrigin := setting.Repository.AccessControlAllowOrigin
-               // Set CORS headers for browser-based git clients
-               ctx.Resp.Header().Set("Access-Control-Allow-Origin", allowedOrigin)
-               ctx.Resp.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, User-Agent")
-
-               // Handle preflight OPTIONS request
-               if ctx.Req.Method == "OPTIONS" {
-                       if allowedOrigin == "*" {
-                               ctx.Status(http.StatusOK)
-                       } else if allowedOrigin == "null" {
-                               ctx.Status(http.StatusForbidden)
-                       } else {
-                               origin := ctx.Req.Header.Get("Origin")
-                               if len(origin) > 0 && origin == allowedOrigin {
-                                       ctx.Status(http.StatusOK)
-                               } else {
-                                       ctx.Status(http.StatusForbidden)
-                               }
-                       }
-                       return
-               }
-       }
-
-       username := ctx.Params(":username")
-       reponame := strings.TrimSuffix(ctx.Params(":reponame"), ".git")
-
-       if ctx.Query("go-get") == "1" {
-               context.EarlyResponseForGoGetMeta(ctx)
-               return
-       }
-
-       var isPull, receivePack bool
-       service := ctx.Query("service")
-       if service == "git-receive-pack" ||
-               strings.HasSuffix(ctx.Req.URL.Path, "git-receive-pack") {
-               isPull = false
-               receivePack = true
-       } else if service == "git-upload-pack" ||
-               strings.HasSuffix(ctx.Req.URL.Path, "git-upload-pack") {
-               isPull = true
-       } else if service == "git-upload-archive" ||
-               strings.HasSuffix(ctx.Req.URL.Path, "git-upload-archive") {
-               isPull = true
-       } else {
-               isPull = ctx.Req.Method == "GET"
-       }
-
-       var accessMode models.AccessMode
-       if isPull {
-               accessMode = models.AccessModeRead
-       } else {
-               accessMode = models.AccessModeWrite
-       }
-
-       isWiki := false
-       var unitType = models.UnitTypeCode
-       var wikiRepoName string
-       if strings.HasSuffix(reponame, ".wiki") {
-               isWiki = true
-               unitType = models.UnitTypeWiki
-               wikiRepoName = reponame
-               reponame = reponame[:len(reponame)-5]
-       }
-
-       owner, err := models.GetUserByName(username)
-       if err != nil {
-               if models.IsErrUserNotExist(err) {
-                       if redirectUserID, err := models.LookupUserRedirect(username); err == nil {
-                               context.RedirectToUser(ctx, username, redirectUserID)
-                       } else {
-                               ctx.NotFound(fmt.Sprintf("User %s does not exist", username), nil)
-                       }
-               } else {
-                       ctx.ServerError("GetUserByName", err)
-               }
-               return
-       }
-       if !owner.IsOrganization() && !owner.IsActive {
-               ctx.HandleText(http.StatusForbidden, "Repository cannot be accessed. You cannot push or open issues/pull-requests.")
-               return
-       }
-
-       repoExist := true
-       repo, err := models.GetRepositoryByName(owner.ID, reponame)
-       if err != nil {
-               if models.IsErrRepoNotExist(err) {
-                       if redirectRepoID, err := models.LookupRepoRedirect(owner.ID, reponame); err == nil {
-                               context.RedirectToRepo(ctx, redirectRepoID)
-                               return
-                       }
-                       repoExist = false
-               } else {
-                       ctx.ServerError("GetRepositoryByName", err)
-                       return
-               }
-       }
-
-       // Don't allow pushing if the repo is archived
-       if repoExist && repo.IsArchived && !isPull {
-               ctx.HandleText(http.StatusForbidden, "This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.")
-               return
-       }
-
-       // Only public pull don't need auth.
-       isPublicPull := repoExist && !repo.IsPrivate && isPull
-       var (
-               askAuth = !isPublicPull || setting.Service.RequireSignInView
-               environ []string
-       )
-
-       // don't allow anonymous pulls if organization is not public
-       if isPublicPull {
-               if err := repo.GetOwner(); err != nil {
-                       ctx.ServerError("GetOwner", err)
-                       return
-               }
-
-               askAuth = askAuth || (repo.Owner.Visibility != structs.VisibleTypePublic)
-       }
-
-       // check access
-       if askAuth {
-               // rely on the results of Contexter
-               if !ctx.IsSigned {
-                       // TODO: support digit auth - which would be Authorization header with digit
-                       ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=\".\"")
-                       ctx.Error(http.StatusUnauthorized)
-                       return
-               }
-
-               if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true {
-                       _, err = models.GetTwoFactorByUID(ctx.User.ID)
-                       if err == nil {
-                               // TODO: This response should be changed to "invalid credentials" for security reasons once the expectation behind it (creating an app token to authenticate) is properly documented
-                               ctx.HandleText(http.StatusUnauthorized, "Users with two-factor authentication enabled cannot perform HTTP/HTTPS operations via plain username and password. Please create and use a personal access token on the user settings page")
-                               return
-                       } else if !models.IsErrTwoFactorNotEnrolled(err) {
-                               ctx.ServerError("IsErrTwoFactorNotEnrolled", err)
-                               return
-                       }
-               }
-
-               if !ctx.User.IsActive || ctx.User.ProhibitLogin {
-                       ctx.HandleText(http.StatusForbidden, "Your account is disabled.")
-                       return
-               }
-
-               if repoExist {
-                       perm, err := models.GetUserRepoPermission(repo, ctx.User)
-                       if err != nil {
-                               ctx.ServerError("GetUserRepoPermission", err)
-                               return
-                       }
-
-                       if !perm.CanAccess(accessMode, unitType) {
-                               ctx.HandleText(http.StatusForbidden, "User permission denied")
-                               return
-                       }
-
-                       if !isPull && repo.IsMirror {
-                               ctx.HandleText(http.StatusForbidden, "mirror repository is read-only")
-                               return
-                       }
-               }
-
-               environ = []string{
-                       models.EnvRepoUsername + "=" + username,
-                       models.EnvRepoName + "=" + reponame,
-                       models.EnvPusherName + "=" + ctx.User.Name,
-                       models.EnvPusherID + fmt.Sprintf("=%d", ctx.User.ID),
-                       models.EnvIsDeployKey + "=false",
-                       models.EnvAppURL + "=" + setting.AppURL,
-               }
-
-               if !ctx.User.KeepEmailPrivate {
-                       environ = append(environ, models.EnvPusherEmail+"="+ctx.User.Email)
-               }
-
-               if isWiki {
-                       environ = append(environ, models.EnvRepoIsWiki+"=true")
-               } else {
-                       environ = append(environ, models.EnvRepoIsWiki+"=false")
-               }
-       }
-
-       if !repoExist {
-               if !receivePack {
-                       ctx.HandleText(http.StatusNotFound, "Repository not found")
-                       return
-               }
-
-               if isWiki { // you cannot send wiki operation before create the repository
-                       ctx.HandleText(http.StatusNotFound, "Repository not found")
-                       return
-               }
-
-               if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg {
-                       ctx.HandleText(http.StatusForbidden, "Push to create is not enabled for organizations.")
-                       return
-               }
-               if !owner.IsOrganization() && !setting.Repository.EnablePushCreateUser {
-                       ctx.HandleText(http.StatusForbidden, "Push to create is not enabled for users.")
-                       return
-               }
-
-               // Return dummy payload if GET receive-pack
-               if ctx.Req.Method == http.MethodGet {
-                       dummyInfoRefs(ctx)
-                       return
-               }
-
-               repo, err = repo_service.PushCreateRepo(ctx.User, owner, reponame)
-               if err != nil {
-                       log.Error("pushCreateRepo: %v", err)
-                       ctx.Status(http.StatusNotFound)
-                       return
-               }
-       }
-
-       if isWiki {
-               // Ensure the wiki is enabled before we allow access to it
-               if _, err := repo.GetUnit(models.UnitTypeWiki); err != nil {
-                       if models.IsErrUnitTypeNotExist(err) {
-                               ctx.HandleText(http.StatusForbidden, "repository wiki is disabled")
-                               return
-                       }
-                       log.Error("Failed to get the wiki unit in %-v Error: %v", repo, err)
-                       ctx.ServerError("GetUnit(UnitTypeWiki) for "+repo.FullName(), err)
-                       return
-               }
-       }
-
-       environ = append(environ, models.EnvRepoID+fmt.Sprintf("=%d", repo.ID))
-
-       w := ctx.Resp
-       r := ctx.Req
-       cfg := &serviceConfig{
-               UploadPack:  true,
-               ReceivePack: true,
-               Env:         environ,
-       }
-
-       r.URL.Path = strings.ToLower(r.URL.Path) // blue: In case some repo name has upper case name
-
-       dir := models.RepoPath(username, reponame)
-       if isWiki {
-               dir = models.RepoPath(username, wikiRepoName)
-       }
-
-       return &serviceHandler{cfg, w, r, dir, cfg.Env}
-}
-
-var (
-       infoRefsCache []byte
-       infoRefsOnce  sync.Once
-)
-
-func dummyInfoRefs(ctx *context.Context) {
-       infoRefsOnce.Do(func() {
-               tmpDir, err := ioutil.TempDir(os.TempDir(), "gitea-info-refs-cache")
-               if err != nil {
-                       log.Error("Failed to create temp dir for git-receive-pack cache: %v", err)
-                       return
-               }
-
-               defer func() {
-                       if err := util.RemoveAll(tmpDir); err != nil {
-                               log.Error("RemoveAll: %v", err)
-                       }
-               }()
-
-               if err := git.InitRepository(tmpDir, true); err != nil {
-                       log.Error("Failed to init bare repo for git-receive-pack cache: %v", err)
-                       return
-               }
-
-               refs, err := git.NewCommand("receive-pack", "--stateless-rpc", "--advertise-refs", ".").RunInDirBytes(tmpDir)
-               if err != nil {
-                       log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
-               }
-
-               log.Debug("populating infoRefsCache: \n%s", string(refs))
-               infoRefsCache = refs
-       })
-
-       ctx.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
-       ctx.Header().Set("Pragma", "no-cache")
-       ctx.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
-       ctx.Header().Set("Content-Type", "application/x-git-receive-pack-advertisement")
-       _, _ = ctx.Write(packetWrite("# service=git-receive-pack\n"))
-       _, _ = ctx.Write([]byte("0000"))
-       _, _ = ctx.Write(infoRefsCache)
-}
-
-type serviceConfig struct {
-       UploadPack  bool
-       ReceivePack bool
-       Env         []string
-}
-
-type serviceHandler struct {
-       cfg     *serviceConfig
-       w       http.ResponseWriter
-       r       *http.Request
-       dir     string
-       environ []string
-}
-
-func (h *serviceHandler) setHeaderNoCache() {
-       h.w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
-       h.w.Header().Set("Pragma", "no-cache")
-       h.w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
-}
-
-func (h *serviceHandler) setHeaderCacheForever() {
-       now := time.Now().Unix()
-       expires := now + 31536000
-       h.w.Header().Set("Date", fmt.Sprintf("%d", now))
-       h.w.Header().Set("Expires", fmt.Sprintf("%d", expires))
-       h.w.Header().Set("Cache-Control", "public, max-age=31536000")
-}
-
-func (h *serviceHandler) sendFile(contentType, file string) {
-       reqFile := path.Join(h.dir, file)
-
-       fi, err := os.Stat(reqFile)
-       if os.IsNotExist(err) {
-               h.w.WriteHeader(http.StatusNotFound)
-               return
-       }
-
-       h.w.Header().Set("Content-Type", contentType)
-       h.w.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size()))
-       h.w.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat))
-       http.ServeFile(h.w, h.r, reqFile)
-}
-
-// one or more key=value pairs separated by colons
-var safeGitProtocolHeader = regexp.MustCompile(`^[0-9a-zA-Z]+=[0-9a-zA-Z]+(:[0-9a-zA-Z]+=[0-9a-zA-Z]+)*$`)
-
-func getGitConfig(option, dir string) string {
-       out, err := git.NewCommand("config", option).RunInDir(dir)
-       if err != nil {
-               log.Error("%v - %s", err, out)
-       }
-       return out[0 : len(out)-1]
-}
-
-func getConfigSetting(service, dir string) bool {
-       service = strings.ReplaceAll(service, "-", "")
-       setting := getGitConfig("http."+service, dir)
-
-       if service == "uploadpack" {
-               return setting != "false"
-       }
-
-       return setting == "true"
-}
-
-func hasAccess(service string, h serviceHandler, checkContentType bool) bool {
-       if checkContentType {
-               if h.r.Header.Get("Content-Type") != fmt.Sprintf("application/x-git-%s-request", service) {
-                       return false
-               }
-       }
-
-       if !(service == "upload-pack" || service == "receive-pack") {
-               return false
-       }
-       if service == "receive-pack" {
-               return h.cfg.ReceivePack
-       }
-       if service == "upload-pack" {
-               return h.cfg.UploadPack
-       }
-
-       return getConfigSetting(service, h.dir)
-}
-
-func serviceRPC(h serviceHandler, service string) {
-       defer func() {
-               if err := h.r.Body.Close(); err != nil {
-                       log.Error("serviceRPC: Close: %v", err)
-               }
-
-       }()
-
-       if !hasAccess(service, h, true) {
-               h.w.WriteHeader(http.StatusUnauthorized)
-               return
-       }
-
-       h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service))
-
-       var err error
-       var reqBody = h.r.Body
-
-       // Handle GZIP.
-       if h.r.Header.Get("Content-Encoding") == "gzip" {
-               reqBody, err = gzip.NewReader(reqBody)
-               if err != nil {
-                       log.Error("Fail to create gzip reader: %v", err)
-                       h.w.WriteHeader(http.StatusInternalServerError)
-                       return
-               }
-       }
-
-       // set this for allow pre-receive and post-receive execute
-       h.environ = append(h.environ, "SSH_ORIGINAL_COMMAND="+service)
-
-       if protocol := h.r.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
-               h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
-       }
-
-       ctx, cancel := gocontext.WithCancel(git.DefaultContext)
-       defer cancel()
-       var stderr bytes.Buffer
-       cmd := exec.CommandContext(ctx, git.GitExecutable, service, "--stateless-rpc", h.dir)
-       cmd.Dir = h.dir
-       cmd.Env = append(os.Environ(), h.environ...)
-       cmd.Stdout = h.w
-       cmd.Stdin = reqBody
-       cmd.Stderr = &stderr
-
-       pid := process.GetManager().Add(fmt.Sprintf("%s %s %s [repo_path: %s]", git.GitExecutable, service, "--stateless-rpc", h.dir), cancel)
-       defer process.GetManager().Remove(pid)
-
-       if err := cmd.Run(); err != nil {
-               log.Error("Fail to serve RPC(%s) in %s: %v - %s", service, h.dir, err, stderr.String())
-               return
-       }
-}
-
-// ServiceUploadPack implements Git Smart HTTP protocol
-func ServiceUploadPack(ctx *context.Context) {
-       h := httpBase(ctx)
-       if h != nil {
-               serviceRPC(*h, "upload-pack")
-       }
-}
-
-// ServiceReceivePack implements Git Smart HTTP protocol
-func ServiceReceivePack(ctx *context.Context) {
-       h := httpBase(ctx)
-       if h != nil {
-               serviceRPC(*h, "receive-pack")
-       }
-}
-
-func getServiceType(r *http.Request) string {
-       serviceType := r.FormValue("service")
-       if !strings.HasPrefix(serviceType, "git-") {
-               return ""
-       }
-       return strings.Replace(serviceType, "git-", "", 1)
-}
-
-func updateServerInfo(dir string) []byte {
-       out, err := git.NewCommand("update-server-info").RunInDirBytes(dir)
-       if err != nil {
-               log.Error(fmt.Sprintf("%v - %s", err, string(out)))
-       }
-       return out
-}
-
-func packetWrite(str string) []byte {
-       s := strconv.FormatInt(int64(len(str)+4), 16)
-       if len(s)%4 != 0 {
-               s = strings.Repeat("0", 4-len(s)%4) + s
-       }
-       return []byte(s + str)
-}
-
-// GetInfoRefs implements Git dumb HTTP
-func GetInfoRefs(ctx *context.Context) {
-       h := httpBase(ctx)
-       if h == nil {
-               return
-       }
-       h.setHeaderNoCache()
-       if hasAccess(getServiceType(h.r), *h, false) {
-               service := getServiceType(h.r)
-
-               if protocol := h.r.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
-                       h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
-               }
-               h.environ = append(os.Environ(), h.environ...)
-
-               refs, err := git.NewCommand(service, "--stateless-rpc", "--advertise-refs", ".").RunInDirTimeoutEnv(h.environ, -1, h.dir)
-               if err != nil {
-                       log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
-               }
-
-               h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service))
-               h.w.WriteHeader(http.StatusOK)
-               _, _ = h.w.Write(packetWrite("# service=git-" + service + "\n"))
-               _, _ = h.w.Write([]byte("0000"))
-               _, _ = h.w.Write(refs)
-       } else {
-               updateServerInfo(h.dir)
-               h.sendFile("text/plain; charset=utf-8", "info/refs")
-       }
-}
-
-// GetTextFile implements Git dumb HTTP
-func GetTextFile(p string) func(*context.Context) {
-       return func(ctx *context.Context) {
-               h := httpBase(ctx)
-               if h != nil {
-                       h.setHeaderNoCache()
-                       file := ctx.Params("file")
-                       if file != "" {
-                               h.sendFile("text/plain", "objects/info/"+file)
-                       } else {
-                               h.sendFile("text/plain", p)
-                       }
-               }
-       }
-}
-
-// GetInfoPacks implements Git dumb HTTP
-func GetInfoPacks(ctx *context.Context) {
-       h := httpBase(ctx)
-       if h != nil {
-               h.setHeaderCacheForever()
-               h.sendFile("text/plain; charset=utf-8", "objects/info/packs")
-       }
-}
-
-// GetLooseObject implements Git dumb HTTP
-func GetLooseObject(ctx *context.Context) {
-       h := httpBase(ctx)
-       if h != nil {
-               h.setHeaderCacheForever()
-               h.sendFile("application/x-git-loose-object", fmt.Sprintf("objects/%s/%s",
-                       ctx.Params("head"), ctx.Params("hash")))
-       }
-}
-
-// GetPackFile implements Git dumb HTTP
-func GetPackFile(ctx *context.Context) {
-       h := httpBase(ctx)
-       if h != nil {
-               h.setHeaderCacheForever()
-               h.sendFile("application/x-git-packed-objects", "objects/pack/pack-"+ctx.Params("file")+".pack")
-       }
-}
-
-// GetIdxFile implements Git dumb HTTP
-func GetIdxFile(ctx *context.Context) {
-       h := httpBase(ctx)
-       if h != nil {
-               h.setHeaderCacheForever()
-               h.sendFile("application/x-git-packed-objects-toc", "objects/pack/pack-"+ctx.Params("file")+".idx")
-       }
-}
diff --git a/routers/repo/issue.go b/routers/repo/issue.go
deleted file mode 100644 (file)
index fd2877e..0000000
+++ /dev/null
@@ -1,2599 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2018 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 repo
-
-import (
-       "bytes"
-       "errors"
-       "fmt"
-       "io/ioutil"
-       "net/http"
-       "path"
-       "strconv"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/convert"
-       "code.gitea.io/gitea/modules/git"
-       issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/markup"
-       "code.gitea.io/gitea/modules/markup/markdown"
-       "code.gitea.io/gitea/modules/setting"
-       api "code.gitea.io/gitea/modules/structs"
-       "code.gitea.io/gitea/modules/upload"
-       "code.gitea.io/gitea/modules/util"
-       "code.gitea.io/gitea/modules/web"
-       comment_service "code.gitea.io/gitea/services/comments"
-       "code.gitea.io/gitea/services/forms"
-       issue_service "code.gitea.io/gitea/services/issue"
-       pull_service "code.gitea.io/gitea/services/pull"
-
-       "github.com/unknwon/com"
-)
-
-const (
-       tplAttachment base.TplName = "repo/issue/view_content/attachments"
-
-       tplIssues      base.TplName = "repo/issue/list"
-       tplIssueNew    base.TplName = "repo/issue/new"
-       tplIssueChoose base.TplName = "repo/issue/choose"
-       tplIssueView   base.TplName = "repo/issue/view"
-
-       tplReactions base.TplName = "repo/issue/view_content/reactions"
-
-       issueTemplateKey      = "IssueTemplate"
-       issueTemplateTitleKey = "IssueTemplateTitle"
-)
-
-var (
-       // IssueTemplateCandidates issue templates
-       IssueTemplateCandidates = []string{
-               "ISSUE_TEMPLATE.md",
-               "issue_template.md",
-               ".gitea/ISSUE_TEMPLATE.md",
-               ".gitea/issue_template.md",
-               ".github/ISSUE_TEMPLATE.md",
-               ".github/issue_template.md",
-       }
-)
-
-// MustAllowUserComment checks to make sure if an issue is locked.
-// If locked and user has permissions to write to the repository,
-// then the comment is allowed, else it is blocked
-func MustAllowUserComment(ctx *context.Context) {
-       issue := GetActionIssue(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.User.IsAdmin {
-               ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked"))
-               ctx.Redirect(issue.HTMLURL())
-               return
-       }
-}
-
-// MustEnableIssues check if repository enable internal issues
-func MustEnableIssues(ctx *context.Context) {
-       if !ctx.Repo.CanRead(models.UnitTypeIssues) &&
-               !ctx.Repo.CanRead(models.UnitTypeExternalTracker) {
-               ctx.NotFound("MustEnableIssues", nil)
-               return
-       }
-
-       unit, err := ctx.Repo.Repository.GetUnit(models.UnitTypeExternalTracker)
-       if err == nil {
-               ctx.Redirect(unit.ExternalTrackerConfig().ExternalTrackerURL)
-               return
-       }
-}
-
-// MustAllowPulls check if repository enable pull requests and user have right to do that
-func MustAllowPulls(ctx *context.Context) {
-       if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(models.UnitTypePullRequests) {
-               ctx.NotFound("MustAllowPulls", nil)
-               return
-       }
-
-       // User can send pull request if owns a forked repository.
-       if ctx.IsSigned && ctx.User.HasForkedRepo(ctx.Repo.Repository.ID) {
-               ctx.Repo.PullRequest.Allowed = true
-               ctx.Repo.PullRequest.HeadInfo = ctx.User.Name + ":" + ctx.Repo.BranchName
-       }
-}
-
-func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption util.OptionalBool) {
-       var err error
-       viewType := ctx.Query("type")
-       sortType := ctx.Query("sort")
-       types := []string{"all", "your_repositories", "assigned", "created_by", "mentioned", "review_requested"}
-       if !util.IsStringInSlice(viewType, types, true) {
-               viewType = "all"
-       }
-
-       var (
-               assigneeID        = ctx.QueryInt64("assignee")
-               posterID          int64
-               mentionedID       int64
-               reviewRequestedID int64
-               forceEmpty        bool
-       )
-
-       if ctx.IsSigned {
-               switch viewType {
-               case "created_by":
-                       posterID = ctx.User.ID
-               case "mentioned":
-                       mentionedID = ctx.User.ID
-               case "assigned":
-                       assigneeID = ctx.User.ID
-               case "review_requested":
-                       reviewRequestedID = ctx.User.ID
-               }
-       }
-
-       repo := ctx.Repo.Repository
-       var labelIDs []int64
-       selectLabels := ctx.Query("labels")
-       if len(selectLabels) > 0 && selectLabels != "0" {
-               labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
-               if err != nil {
-                       ctx.ServerError("StringsToInt64s", err)
-                       return
-               }
-       }
-
-       keyword := strings.Trim(ctx.Query("q"), " ")
-       if bytes.Contains([]byte(keyword), []byte{0x00}) {
-               keyword = ""
-       }
-
-       var issueIDs []int64
-       if len(keyword) > 0 {
-               issueIDs, err = issue_indexer.SearchIssuesByKeyword([]int64{repo.ID}, keyword)
-               if err != nil {
-                       ctx.ServerError("issueIndexer.Search", err)
-                       return
-               }
-               if len(issueIDs) == 0 {
-                       forceEmpty = true
-               }
-       }
-
-       var issueStats *models.IssueStats
-       if forceEmpty {
-               issueStats = &models.IssueStats{}
-       } else {
-               issueStats, err = models.GetIssueStats(&models.IssueStatsOptions{
-                       RepoID:            repo.ID,
-                       Labels:            selectLabels,
-                       MilestoneID:       milestoneID,
-                       AssigneeID:        assigneeID,
-                       MentionedID:       mentionedID,
-                       PosterID:          posterID,
-                       ReviewRequestedID: reviewRequestedID,
-                       IsPull:            isPullOption,
-                       IssueIDs:          issueIDs,
-               })
-               if err != nil {
-                       ctx.ServerError("GetIssueStats", err)
-                       return
-               }
-       }
-
-       isShowClosed := ctx.Query("state") == "closed"
-       // if open issues are zero and close don't, use closed as default
-       if len(ctx.Query("state")) == 0 && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 {
-               isShowClosed = true
-       }
-
-       page := ctx.QueryInt("page")
-       if page <= 1 {
-               page = 1
-       }
-
-       var total int
-       if !isShowClosed {
-               total = int(issueStats.OpenCount)
-       } else {
-               total = int(issueStats.ClosedCount)
-       }
-       pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
-
-       var mileIDs []int64
-       if milestoneID > 0 {
-               mileIDs = []int64{milestoneID}
-       }
-
-       var issues []*models.Issue
-       if forceEmpty {
-               issues = []*models.Issue{}
-       } else {
-               issues, err = models.Issues(&models.IssuesOptions{
-                       ListOptions: models.ListOptions{
-                               Page:     pager.Paginater.Current(),
-                               PageSize: setting.UI.IssuePagingNum,
-                       },
-                       RepoIDs:           []int64{repo.ID},
-                       AssigneeID:        assigneeID,
-                       PosterID:          posterID,
-                       MentionedID:       mentionedID,
-                       ReviewRequestedID: reviewRequestedID,
-                       MilestoneIDs:      mileIDs,
-                       ProjectID:         projectID,
-                       IsClosed:          util.OptionalBoolOf(isShowClosed),
-                       IsPull:            isPullOption,
-                       LabelIDs:          labelIDs,
-                       SortType:          sortType,
-                       IssueIDs:          issueIDs,
-               })
-               if err != nil {
-                       ctx.ServerError("Issues", err)
-                       return
-               }
-       }
-
-       var issueList = models.IssueList(issues)
-       approvalCounts, err := issueList.GetApprovalCounts()
-       if err != nil {
-               ctx.ServerError("ApprovalCounts", err)
-               return
-       }
-
-       // Get posters.
-       for i := range issues {
-               // Check read status
-               if !ctx.IsSigned {
-                       issues[i].IsRead = true
-               } else if err = issues[i].GetIsRead(ctx.User.ID); err != nil {
-                       ctx.ServerError("GetIsRead", err)
-                       return
-               }
-       }
-
-       commitStatus, err := pull_service.GetIssuesLastCommitStatus(issues)
-       if err != nil {
-               ctx.ServerError("GetIssuesLastCommitStatus", err)
-               return
-       }
-
-       ctx.Data["Issues"] = issues
-       ctx.Data["CommitStatus"] = commitStatus
-
-       // Get assignees.
-       ctx.Data["Assignees"], err = repo.GetAssignees()
-       if err != nil {
-               ctx.ServerError("GetAssignees", err)
-               return
-       }
-
-       handleTeamMentions(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       labels, err := models.GetLabelsByRepoID(repo.ID, "", models.ListOptions{})
-       if err != nil {
-               ctx.ServerError("GetLabelsByRepoID", err)
-               return
-       }
-
-       if repo.Owner.IsOrganization() {
-               orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.Query("sort"), models.ListOptions{})
-               if err != nil {
-                       ctx.ServerError("GetLabelsByOrgID", err)
-                       return
-               }
-
-               ctx.Data["OrgLabels"] = orgLabels
-               labels = append(labels, orgLabels...)
-       }
-
-       for _, l := range labels {
-               l.LoadSelectedLabelsAfterClick(labelIDs)
-       }
-       ctx.Data["Labels"] = labels
-       ctx.Data["NumLabels"] = len(labels)
-
-       if ctx.QueryInt64("assignee") == 0 {
-               assigneeID = 0 // Reset ID to prevent unexpected selection of assignee.
-       }
-
-       ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] =
-               issue_service.GetRefEndNamesAndURLs(issues, ctx.Repo.RepoLink)
-
-       ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 {
-               counts, ok := approvalCounts[issueID]
-               if !ok || len(counts) == 0 {
-                       return 0
-               }
-               reviewTyp := models.ReviewTypeApprove
-               if typ == "reject" {
-                       reviewTyp = models.ReviewTypeReject
-               } else if typ == "waiting" {
-                       reviewTyp = models.ReviewTypeRequest
-               }
-               for _, count := range counts {
-                       if count.Type == reviewTyp {
-                               return count.Count
-                       }
-               }
-               return 0
-       }
-       ctx.Data["IssueStats"] = issueStats
-       ctx.Data["SelLabelIDs"] = labelIDs
-       ctx.Data["SelectLabels"] = selectLabels
-       ctx.Data["ViewType"] = viewType
-       ctx.Data["SortType"] = sortType
-       ctx.Data["MilestoneID"] = milestoneID
-       ctx.Data["AssigneeID"] = assigneeID
-       ctx.Data["IsShowClosed"] = isShowClosed
-       ctx.Data["Keyword"] = keyword
-       if isShowClosed {
-               ctx.Data["State"] = "closed"
-       } else {
-               ctx.Data["State"] = "open"
-       }
-
-       pager.AddParam(ctx, "q", "Keyword")
-       pager.AddParam(ctx, "type", "ViewType")
-       pager.AddParam(ctx, "sort", "SortType")
-       pager.AddParam(ctx, "state", "State")
-       pager.AddParam(ctx, "labels", "SelectLabels")
-       pager.AddParam(ctx, "milestone", "MilestoneID")
-       pager.AddParam(ctx, "assignee", "AssigneeID")
-       ctx.Data["Page"] = pager
-}
-
-// Issues render issues page
-func Issues(ctx *context.Context) {
-       isPullList := ctx.Params(":type") == "pulls"
-       if isPullList {
-               MustAllowPulls(ctx)
-               if ctx.Written() {
-                       return
-               }
-               ctx.Data["Title"] = ctx.Tr("repo.pulls")
-               ctx.Data["PageIsPullList"] = true
-       } else {
-               MustEnableIssues(ctx)
-               if ctx.Written() {
-                       return
-               }
-               ctx.Data["Title"] = ctx.Tr("repo.issues")
-               ctx.Data["PageIsIssueList"] = true
-               ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
-       }
-
-       issues(ctx, ctx.QueryInt64("milestone"), ctx.QueryInt64("project"), util.OptionalBoolOf(isPullList))
-       if ctx.Written() {
-               return
-       }
-
-       var err error
-       // Get milestones
-       ctx.Data["Milestones"], err = models.GetMilestones(models.GetMilestonesOption{
-               RepoID: ctx.Repo.Repository.ID,
-               State:  api.StateType(ctx.Query("state")),
-       })
-       if err != nil {
-               ctx.ServerError("GetAllRepoMilestones", err)
-               return
-       }
-
-       ctx.Data["CanWriteIssuesOrPulls"] = ctx.Repo.CanWriteIssuesOrPulls(isPullList)
-
-       ctx.HTML(http.StatusOK, tplIssues)
-}
-
-// RetrieveRepoMilestonesAndAssignees find all the milestones and assignees of a repository
-func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *models.Repository) {
-       var err error
-       ctx.Data["OpenMilestones"], err = models.GetMilestones(models.GetMilestonesOption{
-               RepoID: repo.ID,
-               State:  api.StateOpen,
-       })
-       if err != nil {
-               ctx.ServerError("GetMilestones", err)
-               return
-       }
-       ctx.Data["ClosedMilestones"], err = models.GetMilestones(models.GetMilestonesOption{
-               RepoID: repo.ID,
-               State:  api.StateClosed,
-       })
-       if err != nil {
-               ctx.ServerError("GetMilestones", err)
-               return
-       }
-
-       ctx.Data["Assignees"], err = repo.GetAssignees()
-       if err != nil {
-               ctx.ServerError("GetAssignees", err)
-               return
-       }
-
-       handleTeamMentions(ctx)
-       if ctx.Written() {
-               return
-       }
-}
-
-func retrieveProjects(ctx *context.Context, repo *models.Repository) {
-
-       var err error
-
-       ctx.Data["OpenProjects"], _, err = models.GetProjects(models.ProjectSearchOptions{
-               RepoID:   repo.ID,
-               Page:     -1,
-               IsClosed: util.OptionalBoolFalse,
-               Type:     models.ProjectTypeRepository,
-       })
-       if err != nil {
-               ctx.ServerError("GetProjects", err)
-               return
-       }
-
-       ctx.Data["ClosedProjects"], _, err = models.GetProjects(models.ProjectSearchOptions{
-               RepoID:   repo.ID,
-               Page:     -1,
-               IsClosed: util.OptionalBoolTrue,
-               Type:     models.ProjectTypeRepository,
-       })
-       if err != nil {
-               ctx.ServerError("GetProjects", err)
-               return
-       }
-}
-
-// repoReviewerSelection items to bee shown
-type repoReviewerSelection struct {
-       IsTeam    bool
-       Team      *models.Team
-       User      *models.User
-       Review    *models.Review
-       CanChange bool
-       Checked   bool
-       ItemID    int64
-}
-
-// RetrieveRepoReviewers find all reviewers of a repository
-func RetrieveRepoReviewers(ctx *context.Context, repo *models.Repository, issue *models.Issue, canChooseReviewer bool) {
-       ctx.Data["CanChooseReviewer"] = canChooseReviewer
-
-       originalAuthorReviews, err := models.GetReviewersFromOriginalAuthorsByIssueID(issue.ID)
-       if err != nil {
-               ctx.ServerError("GetReviewersFromOriginalAuthorsByIssueID", err)
-               return
-       }
-       ctx.Data["OriginalReviews"] = originalAuthorReviews
-
-       reviews, err := models.GetReviewersByIssueID(issue.ID)
-       if err != nil {
-               ctx.ServerError("GetReviewersByIssueID", err)
-               return
-       }
-
-       if len(reviews) == 0 && !canChooseReviewer {
-               return
-       }
-
-       var (
-               pullReviews         []*repoReviewerSelection
-               reviewersResult     []*repoReviewerSelection
-               teamReviewersResult []*repoReviewerSelection
-               teamReviewers       []*models.Team
-               reviewers           []*models.User
-       )
-
-       if canChooseReviewer {
-               posterID := issue.PosterID
-               if issue.OriginalAuthorID > 0 {
-                       posterID = 0
-               }
-
-               reviewers, err = repo.GetReviewers(ctx.User.ID, posterID)
-               if err != nil {
-                       ctx.ServerError("GetReviewers", err)
-                       return
-               }
-
-               teamReviewers, err = repo.GetReviewerTeams()
-               if err != nil {
-                       ctx.ServerError("GetReviewerTeams", err)
-                       return
-               }
-
-               if len(reviewers) > 0 {
-                       reviewersResult = make([]*repoReviewerSelection, 0, len(reviewers))
-               }
-
-               if len(teamReviewers) > 0 {
-                       teamReviewersResult = make([]*repoReviewerSelection, 0, len(teamReviewers))
-               }
-       }
-
-       pullReviews = make([]*repoReviewerSelection, 0, len(reviews))
-
-       for _, review := range reviews {
-               tmp := &repoReviewerSelection{
-                       Checked: review.Type == models.ReviewTypeRequest,
-                       Review:  review,
-                       ItemID:  review.ReviewerID,
-               }
-               if review.ReviewerTeamID > 0 {
-                       tmp.IsTeam = true
-                       tmp.ItemID = -review.ReviewerTeamID
-               }
-
-               if ctx.Repo.IsAdmin() {
-                       // Admin can dismiss or re-request any review requests
-                       tmp.CanChange = true
-               } else if ctx.User != nil && ctx.User.ID == review.ReviewerID && review.Type == models.ReviewTypeRequest {
-                       // A user can refuse review requests
-                       tmp.CanChange = true
-               } else if (canChooseReviewer || (ctx.User != nil && ctx.User.ID == issue.PosterID)) && review.Type != models.ReviewTypeRequest &&
-                       ctx.User.ID != review.ReviewerID {
-                       // The poster of the PR, a manager, or official reviewers can re-request review from other reviewers
-                       tmp.CanChange = true
-               }
-
-               pullReviews = append(pullReviews, tmp)
-
-               if canChooseReviewer {
-                       if tmp.IsTeam {
-                               teamReviewersResult = append(teamReviewersResult, tmp)
-                       } else {
-                               reviewersResult = append(reviewersResult, tmp)
-                       }
-               }
-       }
-
-       if len(pullReviews) > 0 {
-               // Drop all non-existing users and teams from the reviews
-               currentPullReviewers := make([]*repoReviewerSelection, 0, len(pullReviews))
-               for _, item := range pullReviews {
-                       if item.Review.ReviewerID > 0 {
-                               if err = item.Review.LoadReviewer(); err != nil {
-                                       if models.IsErrUserNotExist(err) {
-                                               continue
-                                       }
-                                       ctx.ServerError("LoadReviewer", err)
-                                       return
-                               }
-                               item.User = item.Review.Reviewer
-                       } else if item.Review.ReviewerTeamID > 0 {
-                               if err = item.Review.LoadReviewerTeam(); err != nil {
-                                       if models.IsErrTeamNotExist(err) {
-                                               continue
-                                       }
-                                       ctx.ServerError("LoadReviewerTeam", err)
-                                       return
-                               }
-                               item.Team = item.Review.ReviewerTeam
-                       } else {
-                               continue
-                       }
-
-                       currentPullReviewers = append(currentPullReviewers, item)
-               }
-               ctx.Data["PullReviewers"] = currentPullReviewers
-       }
-
-       if canChooseReviewer && reviewersResult != nil {
-               preadded := len(reviewersResult)
-               for _, reviewer := range reviewers {
-                       found := false
-               reviewAddLoop:
-                       for _, tmp := range reviewersResult[:preadded] {
-                               if tmp.ItemID == reviewer.ID {
-                                       tmp.User = reviewer
-                                       found = true
-                                       break reviewAddLoop
-                               }
-                       }
-
-                       if found {
-                               continue
-                       }
-
-                       reviewersResult = append(reviewersResult, &repoReviewerSelection{
-                               IsTeam:    false,
-                               CanChange: true,
-                               User:      reviewer,
-                               ItemID:    reviewer.ID,
-                       })
-               }
-
-               ctx.Data["Reviewers"] = reviewersResult
-       }
-
-       if canChooseReviewer && teamReviewersResult != nil {
-               preadded := len(teamReviewersResult)
-               for _, team := range teamReviewers {
-                       found := false
-               teamReviewAddLoop:
-                       for _, tmp := range teamReviewersResult[:preadded] {
-                               if tmp.ItemID == -team.ID {
-                                       tmp.Team = team
-                                       found = true
-                                       break teamReviewAddLoop
-                               }
-                       }
-
-                       if found {
-                               continue
-                       }
-
-                       teamReviewersResult = append(teamReviewersResult, &repoReviewerSelection{
-                               IsTeam:    true,
-                               CanChange: true,
-                               Team:      team,
-                               ItemID:    -team.ID,
-                       })
-               }
-
-               ctx.Data["TeamReviewers"] = teamReviewersResult
-       }
-}
-
-// RetrieveRepoMetas find all the meta information of a repository
-func RetrieveRepoMetas(ctx *context.Context, repo *models.Repository, isPull bool) []*models.Label {
-       if !ctx.Repo.CanWriteIssuesOrPulls(isPull) {
-               return nil
-       }
-
-       labels, err := models.GetLabelsByRepoID(repo.ID, "", models.ListOptions{})
-       if err != nil {
-               ctx.ServerError("GetLabelsByRepoID", err)
-               return nil
-       }
-       ctx.Data["Labels"] = labels
-       if repo.Owner.IsOrganization() {
-               orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.Query("sort"), models.ListOptions{})
-               if err != nil {
-                       return nil
-               }
-
-               ctx.Data["OrgLabels"] = orgLabels
-               labels = append(labels, orgLabels...)
-       }
-
-       RetrieveRepoMilestonesAndAssignees(ctx, repo)
-       if ctx.Written() {
-               return nil
-       }
-
-       retrieveProjects(ctx, repo)
-       if ctx.Written() {
-               return nil
-       }
-
-       brs, _, err := ctx.Repo.GitRepo.GetBranches(0, 0)
-       if err != nil {
-               ctx.ServerError("GetBranches", err)
-               return nil
-       }
-       ctx.Data["Branches"] = brs
-
-       // Contains true if the user can create issue dependencies
-       ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.User, isPull)
-
-       return labels
-}
-
-func getFileContentFromDefaultBranch(ctx *context.Context, filename string) (string, bool) {
-       var bytes []byte
-
-       if ctx.Repo.Commit == nil {
-               var err error
-               ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
-               if err != nil {
-                       return "", false
-               }
-       }
-
-       entry, err := ctx.Repo.Commit.GetTreeEntryByPath(filename)
-       if err != nil {
-               return "", false
-       }
-       if entry.Blob().Size() >= setting.UI.MaxDisplayFileSize {
-               return "", false
-       }
-       r, err := entry.Blob().DataAsync()
-       if err != nil {
-               return "", false
-       }
-       defer r.Close()
-       bytes, err = ioutil.ReadAll(r)
-       if err != nil {
-               return "", false
-       }
-       return string(bytes), true
-}
-
-func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleDirs []string, possibleFiles []string) {
-       templateCandidates := make([]string, 0, len(possibleFiles))
-       if ctx.Query("template") != "" {
-               for _, dirName := range possibleDirs {
-                       templateCandidates = append(templateCandidates, path.Join(dirName, ctx.Query("template")))
-               }
-       }
-       templateCandidates = append(templateCandidates, possibleFiles...) // Append files to the end because they should be fallback
-       for _, filename := range templateCandidates {
-               templateContent, found := getFileContentFromDefaultBranch(ctx, filename)
-               if found {
-                       var meta api.IssueTemplate
-                       templateBody, err := markdown.ExtractMetadata(templateContent, &meta)
-                       if err != nil {
-                               log.Debug("could not extract metadata from %s [%s]: %v", filename, ctx.Repo.Repository.FullName(), err)
-                               ctx.Data[ctxDataKey] = templateContent
-                               return
-                       }
-                       ctx.Data[issueTemplateTitleKey] = meta.Title
-                       ctx.Data[ctxDataKey] = templateBody
-                       labelIDs := make([]string, 0, len(meta.Labels))
-                       if repoLabels, err := models.GetLabelsByRepoID(ctx.Repo.Repository.ID, "", models.ListOptions{}); err == nil {
-                               ctx.Data["Labels"] = repoLabels
-                               if ctx.Repo.Owner.IsOrganization() {
-                                       if orgLabels, err := models.GetLabelsByOrgID(ctx.Repo.Owner.ID, ctx.Query("sort"), models.ListOptions{}); err == nil {
-                                               ctx.Data["OrgLabels"] = orgLabels
-                                               repoLabels = append(repoLabels, orgLabels...)
-                                       }
-                               }
-
-                               for _, metaLabel := range meta.Labels {
-                                       for _, repoLabel := range repoLabels {
-                                               if strings.EqualFold(repoLabel.Name, metaLabel) {
-                                                       repoLabel.IsChecked = true
-                                                       labelIDs = append(labelIDs, fmt.Sprintf("%d", repoLabel.ID))
-                                                       break
-                                               }
-                                       }
-                               }
-                       }
-                       ctx.Data["HasSelectedLabel"] = len(labelIDs) > 0
-                       ctx.Data["label_ids"] = strings.Join(labelIDs, ",")
-                       return
-               }
-       }
-}
-
-// NewIssue render creating issue page
-func NewIssue(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.issues.new")
-       ctx.Data["PageIsIssueList"] = true
-       ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
-       ctx.Data["RequireHighlightJS"] = true
-       ctx.Data["RequireSimpleMDE"] = true
-       ctx.Data["RequireTribute"] = true
-       ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
-       title := ctx.Query("title")
-       ctx.Data["TitleQuery"] = title
-       body := ctx.Query("body")
-       ctx.Data["BodyQuery"] = body
-
-       ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(models.UnitTypeProjects)
-       ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
-       upload.AddUploadContext(ctx, "comment")
-
-       milestoneID := ctx.QueryInt64("milestone")
-       if milestoneID > 0 {
-               milestone, err := models.GetMilestoneByID(milestoneID)
-               if err != nil {
-                       log.Error("GetMilestoneByID: %d: %v", milestoneID, err)
-               } else {
-                       ctx.Data["milestone_id"] = milestoneID
-                       ctx.Data["Milestone"] = milestone
-               }
-       }
-
-       projectID := ctx.QueryInt64("project")
-       if projectID > 0 {
-               project, err := models.GetProjectByID(projectID)
-               if err != nil {
-                       log.Error("GetProjectByID: %d: %v", projectID, err)
-               } else if project.RepoID != ctx.Repo.Repository.ID {
-                       log.Error("GetProjectByID: %d: %v", projectID, fmt.Errorf("project[%d] not in repo [%d]", project.ID, ctx.Repo.Repository.ID))
-               } else {
-                       ctx.Data["project_id"] = projectID
-                       ctx.Data["Project"] = project
-               }
-
-       }
-
-       RetrieveRepoMetas(ctx, ctx.Repo.Repository, false)
-       setTemplateIfExists(ctx, issueTemplateKey, context.IssueTemplateDirCandidates, IssueTemplateCandidates)
-       if ctx.Written() {
-               return
-       }
-
-       ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(models.UnitTypeIssues)
-
-       ctx.HTML(http.StatusOK, tplIssueNew)
-}
-
-// NewIssueChooseTemplate render creating issue from template page
-func NewIssueChooseTemplate(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.issues.new")
-       ctx.Data["PageIsIssueList"] = true
-       ctx.Data["milestone"] = ctx.QueryInt64("milestone")
-
-       issueTemplates := ctx.IssueTemplatesFromDefaultBranch()
-       ctx.Data["NewIssueChooseTemplate"] = len(issueTemplates) > 0
-       ctx.Data["IssueTemplates"] = issueTemplates
-
-       ctx.HTML(http.StatusOK, tplIssueChoose)
-}
-
-// ValidateRepoMetas check and returns repository's meta informations
-func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull bool) ([]int64, []int64, int64, int64) {
-       var (
-               repo = ctx.Repo.Repository
-               err  error
-       )
-
-       labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository, isPull)
-       if ctx.Written() {
-               return nil, nil, 0, 0
-       }
-
-       var labelIDs []int64
-       hasSelected := false
-       // Check labels.
-       if len(form.LabelIDs) > 0 {
-               labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
-               if err != nil {
-                       return nil, nil, 0, 0
-               }
-               labelIDMark := base.Int64sToMap(labelIDs)
-
-               for i := range labels {
-                       if labelIDMark[labels[i].ID] {
-                               labels[i].IsChecked = true
-                               hasSelected = true
-                       }
-               }
-       }
-
-       ctx.Data["Labels"] = labels
-       ctx.Data["HasSelectedLabel"] = hasSelected
-       ctx.Data["label_ids"] = form.LabelIDs
-
-       // Check milestone.
-       milestoneID := form.MilestoneID
-       if milestoneID > 0 {
-               ctx.Data["Milestone"], err = repo.GetMilestoneByID(milestoneID)
-               if err != nil {
-                       ctx.ServerError("GetMilestoneByID", err)
-                       return nil, nil, 0, 0
-               }
-               ctx.Data["milestone_id"] = milestoneID
-       }
-
-       if form.ProjectID > 0 {
-               p, err := models.GetProjectByID(form.ProjectID)
-               if err != nil {
-                       ctx.ServerError("GetProjectByID", err)
-                       return nil, nil, 0, 0
-               }
-               if p.RepoID != ctx.Repo.Repository.ID {
-                       ctx.NotFound("", nil)
-                       return nil, nil, 0, 0
-               }
-
-               ctx.Data["Project"] = p
-               ctx.Data["project_id"] = form.ProjectID
-       }
-
-       // Check assignees
-       var assigneeIDs []int64
-       if len(form.AssigneeIDs) > 0 {
-               assigneeIDs, err = base.StringsToInt64s(strings.Split(form.AssigneeIDs, ","))
-               if err != nil {
-                       return nil, nil, 0, 0
-               }
-
-               // Check if the passed assignees actually exists and is assignable
-               for _, aID := range assigneeIDs {
-                       assignee, err := models.GetUserByID(aID)
-                       if err != nil {
-                               ctx.ServerError("GetUserByID", err)
-                               return nil, nil, 0, 0
-                       }
-
-                       valid, err := models.CanBeAssigned(assignee, repo, isPull)
-                       if err != nil {
-                               ctx.ServerError("CanBeAssigned", err)
-                               return nil, nil, 0, 0
-                       }
-
-                       if !valid {
-                               ctx.ServerError("canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
-                               return nil, nil, 0, 0
-                       }
-               }
-       }
-
-       // Keep the old assignee id thingy for compatibility reasons
-       if form.AssigneeID > 0 {
-               assigneeIDs = append(assigneeIDs, form.AssigneeID)
-       }
-
-       return labelIDs, assigneeIDs, milestoneID, form.ProjectID
-}
-
-// NewIssuePost response for creating new issue
-func NewIssuePost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.CreateIssueForm)
-       ctx.Data["Title"] = ctx.Tr("repo.issues.new")
-       ctx.Data["PageIsIssueList"] = true
-       ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
-       ctx.Data["RequireHighlightJS"] = true
-       ctx.Data["RequireSimpleMDE"] = true
-       ctx.Data["ReadOnly"] = false
-       ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
-       ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
-       upload.AddUploadContext(ctx, "comment")
-
-       var (
-               repo        = ctx.Repo.Repository
-               attachments []string
-       )
-
-       labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, false)
-       if ctx.Written() {
-               return
-       }
-
-       if setting.Attachment.Enabled {
-               attachments = form.Files
-       }
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplIssueNew)
-               return
-       }
-
-       if util.IsEmptyString(form.Title) {
-               ctx.RenderWithErr(ctx.Tr("repo.issues.new.title_empty"), tplIssueNew, form)
-               return
-       }
-
-       issue := &models.Issue{
-               RepoID:      repo.ID,
-               Title:       form.Title,
-               PosterID:    ctx.User.ID,
-               Poster:      ctx.User,
-               MilestoneID: milestoneID,
-               Content:     form.Content,
-               Ref:         form.Ref,
-       }
-
-       if err := issue_service.NewIssue(repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
-               if models.IsErrUserDoesNotHaveAccessToRepo(err) {
-                       ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
-                       return
-               }
-               ctx.ServerError("NewIssue", err)
-               return
-       }
-
-       if projectID > 0 {
-               if err := models.ChangeProjectAssign(issue, ctx.User, projectID); err != nil {
-                       ctx.ServerError("ChangeProjectAssign", err)
-                       return
-               }
-       }
-
-       log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
-       ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + fmt.Sprint(issue.Index))
-}
-
-// commentTag returns the CommentTag for a comment in/with the given repo, poster and issue
-func commentTag(repo *models.Repository, poster *models.User, issue *models.Issue) (models.CommentTag, error) {
-       perm, err := models.GetUserRepoPermission(repo, poster)
-       if err != nil {
-               return models.CommentTagNone, err
-       }
-       if perm.IsOwner() {
-               if !poster.IsAdmin {
-                       return models.CommentTagOwner, nil
-               }
-
-               ok, err := models.IsUserRealRepoAdmin(repo, poster)
-               if err != nil {
-                       return models.CommentTagNone, err
-               }
-
-               if ok {
-                       return models.CommentTagOwner, nil
-               }
-
-               if ok, err = repo.IsCollaborator(poster.ID); ok && err == nil {
-                       return models.CommentTagWriter, nil
-               }
-
-               return models.CommentTagNone, err
-       }
-
-       if perm.CanWrite(models.UnitTypeCode) {
-               return models.CommentTagWriter, nil
-       }
-
-       return models.CommentTagNone, nil
-}
-
-func getBranchData(ctx *context.Context, issue *models.Issue) {
-       ctx.Data["BaseBranch"] = nil
-       ctx.Data["HeadBranch"] = nil
-       ctx.Data["HeadUserName"] = nil
-       ctx.Data["BaseName"] = ctx.Repo.Repository.OwnerName
-       if issue.IsPull {
-               pull := issue.PullRequest
-               ctx.Data["BaseBranch"] = pull.BaseBranch
-               ctx.Data["HeadBranch"] = pull.HeadBranch
-               ctx.Data["HeadUserName"] = pull.MustHeadUserName()
-       }
-}
-
-// ViewIssue render issue view page
-func ViewIssue(ctx *context.Context) {
-       if ctx.Params(":type") == "issues" {
-               // If issue was requested we check if repo has external tracker and redirect
-               extIssueUnit, err := ctx.Repo.Repository.GetUnit(models.UnitTypeExternalTracker)
-               if err == nil && extIssueUnit != nil {
-                       if extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == markup.IssueNameStyleNumeric || extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == "" {
-                               metas := ctx.Repo.Repository.ComposeMetas()
-                               metas["index"] = ctx.Params(":index")
-                               ctx.Redirect(com.Expand(extIssueUnit.ExternalTrackerConfig().ExternalTrackerFormat, metas))
-                               return
-                       }
-               } else if err != nil && !models.IsErrUnitTypeNotExist(err) {
-                       ctx.ServerError("GetUnit", err)
-                       return
-               }
-       }
-
-       issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
-       if err != nil {
-               if models.IsErrIssueNotExist(err) {
-                       ctx.NotFound("GetIssueByIndex", err)
-               } else {
-                       ctx.ServerError("GetIssueByIndex", err)
-               }
-               return
-       }
-
-       // Make sure type and URL matches.
-       if ctx.Params(":type") == "issues" && issue.IsPull {
-               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
-               return
-       } else if ctx.Params(":type") == "pulls" && !issue.IsPull {
-               ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + fmt.Sprint(issue.Index))
-               return
-       }
-
-       if issue.IsPull {
-               MustAllowPulls(ctx)
-               if ctx.Written() {
-                       return
-               }
-               ctx.Data["PageIsPullList"] = true
-               ctx.Data["PageIsPullConversation"] = true
-       } else {
-               MustEnableIssues(ctx)
-               if ctx.Written() {
-                       return
-               }
-               ctx.Data["PageIsIssueList"] = true
-               ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
-       }
-
-       if issue.IsPull && !ctx.Repo.CanRead(models.UnitTypeIssues) {
-               ctx.Data["IssueType"] = "pulls"
-       } else if !issue.IsPull && !ctx.Repo.CanRead(models.UnitTypePullRequests) {
-               ctx.Data["IssueType"] = "issues"
-       } else {
-               ctx.Data["IssueType"] = "all"
-       }
-
-       ctx.Data["RequireHighlightJS"] = true
-       ctx.Data["RequireTribute"] = true
-       ctx.Data["RequireSimpleMDE"] = true
-       ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(models.UnitTypeProjects)
-       ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
-       upload.AddUploadContext(ctx, "comment")
-
-       if err = issue.LoadAttributes(); err != nil {
-               ctx.ServerError("LoadAttributes", err)
-               return
-       }
-
-       if err = filterXRefComments(ctx, issue); err != nil {
-               ctx.ServerError("filterXRefComments", err)
-               return
-       }
-
-       ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
-
-       iw := new(models.IssueWatch)
-       if ctx.User != nil {
-               iw.UserID = ctx.User.ID
-               iw.IssueID = issue.ID
-               iw.IsWatching, err = models.CheckIssueWatch(ctx.User, issue)
-               if err != nil {
-                       ctx.ServerError("CheckIssueWatch", err)
-                       return
-               }
-       }
-       ctx.Data["IssueWatch"] = iw
-
-       issue.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
-               URLPrefix: ctx.Repo.RepoLink,
-               Metas:     ctx.Repo.Repository.ComposeMetas(),
-       }, issue.Content)
-       if err != nil {
-               ctx.ServerError("RenderString", err)
-               return
-       }
-
-       repo := ctx.Repo.Repository
-
-       // Get more information if it's a pull request.
-       if issue.IsPull {
-               if issue.PullRequest.HasMerged {
-                       ctx.Data["DisableStatusChange"] = issue.PullRequest.HasMerged
-                       PrepareMergedViewPullInfo(ctx, issue)
-               } else {
-                       PrepareViewPullInfo(ctx, issue)
-                       ctx.Data["DisableStatusChange"] = ctx.Data["IsPullRequestBroken"] == true && issue.IsClosed
-               }
-               if ctx.Written() {
-                       return
-               }
-       }
-
-       // Metas.
-       // Check labels.
-       labelIDMark := make(map[int64]bool)
-       for i := range issue.Labels {
-               labelIDMark[issue.Labels[i].ID] = true
-       }
-       labels, err := models.GetLabelsByRepoID(repo.ID, "", models.ListOptions{})
-       if err != nil {
-               ctx.ServerError("GetLabelsByRepoID", err)
-               return
-       }
-       ctx.Data["Labels"] = labels
-
-       if repo.Owner.IsOrganization() {
-               orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.Query("sort"), models.ListOptions{})
-               if err != nil {
-                       ctx.ServerError("GetLabelsByOrgID", err)
-                       return
-               }
-               ctx.Data["OrgLabels"] = orgLabels
-
-               labels = append(labels, orgLabels...)
-       }
-
-       hasSelected := false
-       for i := range labels {
-               if labelIDMark[labels[i].ID] {
-                       labels[i].IsChecked = true
-                       hasSelected = true
-               }
-       }
-       ctx.Data["HasSelectedLabel"] = hasSelected
-
-       // Check milestone and assignee.
-       if ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
-               RetrieveRepoMilestonesAndAssignees(ctx, repo)
-               retrieveProjects(ctx, repo)
-
-               if ctx.Written() {
-                       return
-               }
-       }
-
-       if issue.IsPull {
-               canChooseReviewer := ctx.Repo.CanWrite(models.UnitTypePullRequests)
-               if !canChooseReviewer && ctx.User != nil && ctx.IsSigned {
-                       canChooseReviewer, err = models.IsOfficialReviewer(issue, ctx.User)
-                       if err != nil {
-                               ctx.ServerError("IsOfficialReviewer", err)
-                               return
-                       }
-               }
-
-               RetrieveRepoReviewers(ctx, repo, issue, canChooseReviewer)
-               if ctx.Written() {
-                       return
-               }
-       }
-
-       if ctx.IsSigned {
-               // Update issue-user.
-               if err = issue.ReadBy(ctx.User.ID); err != nil {
-                       ctx.ServerError("ReadBy", err)
-                       return
-               }
-       }
-
-       var (
-               tag          models.CommentTag
-               ok           bool
-               marked       = make(map[int64]models.CommentTag)
-               comment      *models.Comment
-               participants = make([]*models.User, 1, 10)
-       )
-       if ctx.Repo.Repository.IsTimetrackerEnabled() {
-               if ctx.IsSigned {
-                       // Deal with the stopwatch
-                       ctx.Data["IsStopwatchRunning"] = models.StopwatchExists(ctx.User.ID, issue.ID)
-                       if !ctx.Data["IsStopwatchRunning"].(bool) {
-                               var exists bool
-                               var sw *models.Stopwatch
-                               if exists, sw, err = models.HasUserStopwatch(ctx.User.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 {
-                                               ctx.ServerError("GetIssueByID", err)
-                                               return
-                                       }
-                                       if err = otherIssue.LoadRepo(); err != nil {
-                                               ctx.ServerError("LoadRepo", err)
-                                               return
-                                       }
-                                       // Add link to the issue of the already running stopwatch
-                                       ctx.Data["OtherStopwatchURL"] = otherIssue.HTMLURL()
-                               }
-                       }
-                       ctx.Data["CanUseTimetracker"] = ctx.Repo.CanUseTimetracker(issue, ctx.User)
-               } else {
-                       ctx.Data["CanUseTimetracker"] = false
-               }
-               if ctx.Data["WorkingUsers"], err = models.TotalTimes(models.FindTrackedTimesOptions{IssueID: issue.ID}); err != nil {
-                       ctx.ServerError("TotalTimes", err)
-                       return
-               }
-       }
-
-       // Check if the user can use the dependencies
-       ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.User, issue.IsPull)
-
-       // check if dependencies can be created across repositories
-       ctx.Data["AllowCrossRepositoryDependencies"] = setting.Service.AllowCrossRepositoryDependencies
-
-       if issue.ShowTag, err = commentTag(repo, issue.Poster, issue); err != nil {
-               ctx.ServerError("commentTag", err)
-               return
-       }
-       marked[issue.PosterID] = issue.ShowTag
-
-       // Render comments and and fetch participants.
-       participants[0] = issue.Poster
-       for _, comment = range issue.Comments {
-               comment.Issue = issue
-
-               if err := comment.LoadPoster(); err != nil {
-                       ctx.ServerError("LoadPoster", err)
-                       return
-               }
-
-               if comment.Type == models.CommentTypeComment {
-                       if err := comment.LoadAttachments(); err != nil {
-                               ctx.ServerError("LoadAttachments", err)
-                               return
-                       }
-
-                       comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
-                               URLPrefix: ctx.Repo.RepoLink,
-                               Metas:     ctx.Repo.Repository.ComposeMetas(),
-                       }, comment.Content)
-                       if err != nil {
-                               ctx.ServerError("RenderString", err)
-                               return
-                       }
-                       // Check tag.
-                       tag, ok = marked[comment.PosterID]
-                       if ok {
-                               comment.ShowTag = tag
-                               continue
-                       }
-
-                       comment.ShowTag, err = commentTag(repo, comment.Poster, issue)
-                       if err != nil {
-                               ctx.ServerError("commentTag", err)
-                               return
-                       }
-                       marked[comment.PosterID] = comment.ShowTag
-                       participants = addParticipant(comment.Poster, participants)
-               } else if comment.Type == models.CommentTypeLabel {
-                       if err = comment.LoadLabel(); err != nil {
-                               ctx.ServerError("LoadLabel", err)
-                               return
-                       }
-               } else if comment.Type == models.CommentTypeMilestone {
-                       if err = comment.LoadMilestone(); err != nil {
-                               ctx.ServerError("LoadMilestone", err)
-                               return
-                       }
-                       ghostMilestone := &models.Milestone{
-                               ID:   -1,
-                               Name: ctx.Tr("repo.issues.deleted_milestone"),
-                       }
-                       if comment.OldMilestoneID > 0 && comment.OldMilestone == nil {
-                               comment.OldMilestone = ghostMilestone
-                       }
-                       if comment.MilestoneID > 0 && comment.Milestone == nil {
-                               comment.Milestone = ghostMilestone
-                       }
-               } else if comment.Type == models.CommentTypeProject {
-
-                       if err = comment.LoadProject(); err != nil {
-                               ctx.ServerError("LoadProject", err)
-                               return
-                       }
-
-                       ghostProject := &models.Project{
-                               ID:    -1,
-                               Title: ctx.Tr("repo.issues.deleted_project"),
-                       }
-
-                       if comment.OldProjectID > 0 && comment.OldProject == nil {
-                               comment.OldProject = ghostProject
-                       }
-
-                       if comment.ProjectID > 0 && comment.Project == nil {
-                               comment.Project = ghostProject
-                       }
-
-               } else if comment.Type == models.CommentTypeAssignees || comment.Type == models.CommentTypeReviewRequest {
-                       if err = comment.LoadAssigneeUserAndTeam(); err != nil {
-                               ctx.ServerError("LoadAssigneeUserAndTeam", err)
-                               return
-                       }
-               } else if comment.Type == models.CommentTypeRemoveDependency || comment.Type == models.CommentTypeAddDependency {
-                       if err = comment.LoadDepIssueDetails(); err != nil {
-                               if !models.IsErrIssueNotExist(err) {
-                                       ctx.ServerError("LoadDepIssueDetails", err)
-                                       return
-                               }
-                       }
-               } else if comment.Type == models.CommentTypeCode || comment.Type == models.CommentTypeReview || comment.Type == models.CommentTypeDismissReview {
-                       comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
-                               URLPrefix: ctx.Repo.RepoLink,
-                               Metas:     ctx.Repo.Repository.ComposeMetas(),
-                       }, comment.Content)
-                       if err != nil {
-                               ctx.ServerError("RenderString", err)
-                               return
-                       }
-                       if err = comment.LoadReview(); err != nil && !models.IsErrReviewNotExist(err) {
-                               ctx.ServerError("LoadReview", err)
-                               return
-                       }
-                       participants = addParticipant(comment.Poster, participants)
-                       if comment.Review == nil {
-                               continue
-                       }
-                       if err = comment.Review.LoadAttributes(); err != nil {
-                               if !models.IsErrUserNotExist(err) {
-                                       ctx.ServerError("Review.LoadAttributes", err)
-                                       return
-                               }
-                               comment.Review.Reviewer = models.NewGhostUser()
-                       }
-                       if err = comment.Review.LoadCodeComments(); err != nil {
-                               ctx.ServerError("Review.LoadCodeComments", err)
-                               return
-                       }
-                       for _, codeComments := range comment.Review.CodeComments {
-                               for _, lineComments := range codeComments {
-                                       for _, c := range lineComments {
-                                               // Check tag.
-                                               tag, ok = marked[c.PosterID]
-                                               if ok {
-                                                       c.ShowTag = tag
-                                                       continue
-                                               }
-
-                                               c.ShowTag, err = commentTag(repo, c.Poster, issue)
-                                               if err != nil {
-                                                       ctx.ServerError("commentTag", err)
-                                                       return
-                                               }
-                                               marked[c.PosterID] = c.ShowTag
-                                               participants = addParticipant(c.Poster, participants)
-                                       }
-                               }
-                       }
-                       if err = comment.LoadResolveDoer(); err != nil {
-                               ctx.ServerError("LoadResolveDoer", err)
-                               return
-                       }
-               } else if comment.Type == models.CommentTypePullPush {
-                       participants = addParticipant(comment.Poster, participants)
-                       if err = comment.LoadPushCommits(); err != nil {
-                               ctx.ServerError("LoadPushCommits", err)
-                               return
-                       }
-               } else if comment.Type == models.CommentTypeAddTimeManual ||
-                       comment.Type == models.CommentTypeStopTracking {
-                       // drop error since times could be pruned from DB..
-                       _ = comment.LoadTime()
-               }
-       }
-
-       // Combine multiple label assignments into a single comment
-       combineLabelComments(issue)
-
-       getBranchData(ctx, issue)
-       if issue.IsPull {
-               pull := issue.PullRequest
-               pull.Issue = issue
-               canDelete := false
-               ctx.Data["AllowMerge"] = false
-
-               if ctx.IsSigned {
-                       if err := pull.LoadHeadRepo(); err != nil {
-                               log.Error("LoadHeadRepo: %v", err)
-                       } else if pull.HeadRepo != nil && pull.HeadBranch != pull.HeadRepo.DefaultBranch {
-                               perm, err := models.GetUserRepoPermission(pull.HeadRepo, ctx.User)
-                               if err != nil {
-                                       ctx.ServerError("GetUserRepoPermission", err)
-                                       return
-                               }
-                               if perm.CanWrite(models.UnitTypeCode) {
-                                       // Check if branch is not protected
-                                       if protected, err := pull.HeadRepo.IsProtectedBranch(pull.HeadBranch, ctx.User); err != nil {
-                                               log.Error("IsProtectedBranch: %v", err)
-                                       } else if !protected {
-                                               canDelete = true
-                                               ctx.Data["DeleteBranchLink"] = ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index) + "/cleanup"
-                                       }
-                               }
-                       }
-
-                       if err := pull.LoadBaseRepo(); err != nil {
-                               log.Error("LoadBaseRepo: %v", err)
-                       }
-                       perm, err := models.GetUserRepoPermission(pull.BaseRepo, ctx.User)
-                       if err != nil {
-                               ctx.ServerError("GetUserRepoPermission", err)
-                               return
-                       }
-                       ctx.Data["AllowMerge"], err = pull_service.IsUserAllowedToMerge(pull, perm, ctx.User)
-                       if err != nil {
-                               ctx.ServerError("IsUserAllowedToMerge", err)
-                               return
-                       }
-
-                       if ctx.Data["CanMarkConversation"], err = models.CanMarkConversation(issue, ctx.User); err != nil {
-                               ctx.ServerError("CanMarkConversation", err)
-                               return
-                       }
-               }
-
-               prUnit, err := repo.GetUnit(models.UnitTypePullRequests)
-               if err != nil {
-                       ctx.ServerError("GetUnit", err)
-                       return
-               }
-               prConfig := prUnit.PullRequestsConfig()
-
-               // Check correct values and select default
-               if ms, ok := ctx.Data["MergeStyle"].(models.MergeStyle); !ok ||
-                       !prConfig.IsMergeStyleAllowed(ms) {
-                       defaultMergeStyle := prConfig.GetDefaultMergeStyle()
-                       if prConfig.IsMergeStyleAllowed(defaultMergeStyle) && !ok {
-                               ctx.Data["MergeStyle"] = defaultMergeStyle
-                       } else if prConfig.AllowMerge {
-                               ctx.Data["MergeStyle"] = models.MergeStyleMerge
-                       } else if prConfig.AllowRebase {
-                               ctx.Data["MergeStyle"] = models.MergeStyleRebase
-                       } else if prConfig.AllowRebaseMerge {
-                               ctx.Data["MergeStyle"] = models.MergeStyleRebaseMerge
-                       } else if prConfig.AllowSquash {
-                               ctx.Data["MergeStyle"] = models.MergeStyleSquash
-                       } else if prConfig.AllowManualMerge {
-                               ctx.Data["MergeStyle"] = models.MergeStyleManuallyMerged
-                       } else {
-                               ctx.Data["MergeStyle"] = ""
-                       }
-               }
-               if err = pull.LoadProtectedBranch(); err != nil {
-                       ctx.ServerError("LoadProtectedBranch", err)
-                       return
-               }
-               if pull.ProtectedBranch != nil {
-                       cnt := pull.ProtectedBranch.GetGrantedApprovalsCount(pull)
-                       ctx.Data["IsBlockedByApprovals"] = !pull.ProtectedBranch.HasEnoughApprovals(pull)
-                       ctx.Data["IsBlockedByRejection"] = pull.ProtectedBranch.MergeBlockedByRejectedReview(pull)
-                       ctx.Data["IsBlockedByOfficialReviewRequests"] = pull.ProtectedBranch.MergeBlockedByOfficialReviewRequests(pull)
-                       ctx.Data["IsBlockedByOutdatedBranch"] = pull.ProtectedBranch.MergeBlockedByOutdatedBranch(pull)
-                       ctx.Data["GrantedApprovals"] = cnt
-                       ctx.Data["RequireSigned"] = pull.ProtectedBranch.RequireSignedCommits
-                       ctx.Data["ChangedProtectedFiles"] = pull.ChangedProtectedFiles
-                       ctx.Data["IsBlockedByChangedProtectedFiles"] = len(pull.ChangedProtectedFiles) != 0
-                       ctx.Data["ChangedProtectedFilesNum"] = len(pull.ChangedProtectedFiles)
-               }
-               ctx.Data["WillSign"] = false
-               if ctx.User != nil {
-                       sign, key, _, err := pull.SignMerge(ctx.User, pull.BaseRepo.RepoPath(), pull.BaseBranch, pull.GetGitRefName())
-                       ctx.Data["WillSign"] = sign
-                       ctx.Data["SigningKey"] = key
-                       if err != nil {
-                               if models.IsErrWontSign(err) {
-                                       ctx.Data["WontSignReason"] = err.(*models.ErrWontSign).Reason
-                               } else {
-                                       ctx.Data["WontSignReason"] = "error"
-                                       log.Error("Error whilst checking if could sign pr %d in repo %s. Error: %v", pull.ID, pull.BaseRepo.FullName(), err)
-                               }
-                       }
-               } else {
-                       ctx.Data["WontSignReason"] = "not_signed_in"
-               }
-               ctx.Data["IsPullBranchDeletable"] = canDelete &&
-                       pull.HeadRepo != nil &&
-                       git.IsBranchExist(pull.HeadRepo.RepoPath(), pull.HeadBranch) &&
-                       (!pull.HasMerged || ctx.Data["HeadBranchCommitID"] == ctx.Data["PullHeadCommitID"])
-
-               stillCanManualMerge := func() bool {
-                       if pull.HasMerged || issue.IsClosed || !ctx.IsSigned {
-                               return false
-                       }
-                       if pull.CanAutoMerge() || pull.IsWorkInProgress() || pull.IsChecking() {
-                               return false
-                       }
-                       if (ctx.User.IsAdmin || ctx.Repo.IsAdmin()) && prConfig.AllowManualMerge {
-                               return true
-                       }
-
-                       return false
-               }
-
-               ctx.Data["StillCanManualMerge"] = stillCanManualMerge()
-       }
-
-       // Get Dependencies
-       ctx.Data["BlockedByDependencies"], err = issue.BlockedByDependencies()
-       if err != nil {
-               ctx.ServerError("BlockedByDependencies", err)
-               return
-       }
-       ctx.Data["BlockingDependencies"], err = issue.BlockingDependencies()
-       if err != nil {
-               ctx.ServerError("BlockingDependencies", err)
-               return
-       }
-
-       ctx.Data["Participants"] = participants
-       ctx.Data["NumParticipants"] = len(participants)
-       ctx.Data["Issue"] = issue
-       ctx.Data["ReadOnly"] = false
-       ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + ctx.Data["Link"].(string)
-       ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.User.ID)
-       ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
-       ctx.Data["HasProjectsWritePermission"] = ctx.Repo.CanWrite(models.UnitTypeProjects)
-       ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.User.IsAdmin)
-       ctx.Data["LockReasons"] = setting.Repository.Issue.LockReasons
-       ctx.Data["RefEndName"] = git.RefEndName(issue.Ref)
-       ctx.HTML(http.StatusOK, tplIssueView)
-}
-
-// 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"))
-       if err != nil {
-               ctx.NotFoundOrServerError("GetIssueByIndex", models.IsErrIssueNotExist, err)
-               return nil
-       }
-       issue.Repo = ctx.Repo.Repository
-       checkIssueRights(ctx, issue)
-       if ctx.Written() {
-               return nil
-       }
-       if err = issue.LoadAttributes(); err != nil {
-               ctx.ServerError("LoadAttributes", nil)
-               return nil
-       }
-       return issue
-}
-
-func checkIssueRights(ctx *context.Context, issue *models.Issue) {
-       if issue.IsPull && !ctx.Repo.CanRead(models.UnitTypePullRequests) ||
-               !issue.IsPull && !ctx.Repo.CanRead(models.UnitTypeIssues) {
-               ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
-       }
-}
-
-func getActionIssues(ctx *context.Context) []*models.Issue {
-       commaSeparatedIssueIDs := ctx.Query("issue_ids")
-       if len(commaSeparatedIssueIDs) == 0 {
-               return nil
-       }
-       issueIDs := make([]int64, 0, 10)
-       for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") {
-               issueID, err := strconv.ParseInt(stringIssueID, 10, 64)
-               if err != nil {
-                       ctx.ServerError("ParseInt", err)
-                       return nil
-               }
-               issueIDs = append(issueIDs, issueID)
-       }
-       issues, err := models.GetIssuesByIDs(issueIDs)
-       if err != nil {
-               ctx.ServerError("GetIssuesByIDs", err)
-               return nil
-       }
-       // Check access rights for all issues
-       issueUnitEnabled := ctx.Repo.CanRead(models.UnitTypeIssues)
-       prUnitEnabled := ctx.Repo.CanRead(models.UnitTypePullRequests)
-       for _, issue := range issues {
-               if issue.IsPull && !prUnitEnabled || !issue.IsPull && !issueUnitEnabled {
-                       ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
-                       return nil
-               }
-               if err = issue.LoadAttributes(); err != nil {
-                       ctx.ServerError("LoadAttributes", err)
-                       return nil
-               }
-       }
-       return issues
-}
-
-// UpdateIssueTitle change issue's title
-func UpdateIssueTitle(ctx *context.Context) {
-       issue := GetActionIssue(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       if !ctx.IsSigned || (!issue.IsPoster(ctx.User.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
-               ctx.Error(http.StatusForbidden)
-               return
-       }
-
-       title := ctx.QueryTrim("title")
-       if len(title) == 0 {
-               ctx.Error(http.StatusNoContent)
-               return
-       }
-
-       if err := issue_service.ChangeTitle(issue, ctx.User, title); err != nil {
-               ctx.ServerError("ChangeTitle", err)
-               return
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "title": issue.Title,
-       })
-}
-
-// UpdateIssueRef change issue's ref (branch)
-func UpdateIssueRef(ctx *context.Context) {
-       issue := GetActionIssue(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       if !ctx.IsSigned || (!issue.IsPoster(ctx.User.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) || issue.IsPull {
-               ctx.Error(http.StatusForbidden)
-               return
-       }
-
-       ref := ctx.QueryTrim("ref")
-
-       if err := issue_service.ChangeIssueRef(issue, ctx.User, ref); err != nil {
-               ctx.ServerError("ChangeRef", err)
-               return
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "ref": ref,
-       })
-}
-
-// UpdateIssueContent change issue's content
-func UpdateIssueContent(ctx *context.Context) {
-       issue := GetActionIssue(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
-               ctx.Error(http.StatusForbidden)
-               return
-       }
-
-       content := ctx.Query("content")
-       if err := issue_service.ChangeContent(issue, ctx.User, content); err != nil {
-               ctx.ServerError("ChangeContent", err)
-               return
-       }
-
-       files := ctx.QueryStrings("files[]")
-       if err := updateAttachments(issue, files); err != nil {
-               ctx.ServerError("UpdateAttachments", err)
-               return
-       }
-
-       content, err := markdown.RenderString(&markup.RenderContext{
-               URLPrefix: ctx.Query("context"),
-               Metas:     ctx.Repo.Repository.ComposeMetas(),
-       }, issue.Content)
-       if err != nil {
-               ctx.ServerError("RenderString", err)
-               return
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "content":     content,
-               "attachments": attachmentsHTML(ctx, issue.Attachments, issue.Content),
-       })
-}
-
-// UpdateIssueMilestone change issue's milestone
-func UpdateIssueMilestone(ctx *context.Context) {
-       issues := getActionIssues(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       milestoneID := ctx.QueryInt64("id")
-       for _, issue := range issues {
-               oldMilestoneID := issue.MilestoneID
-               if oldMilestoneID == milestoneID {
-                       continue
-               }
-               issue.MilestoneID = milestoneID
-               if err := issue_service.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil {
-                       ctx.ServerError("ChangeMilestoneAssign", err)
-                       return
-               }
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "ok": true,
-       })
-}
-
-// UpdateIssueAssignee change issue's or pull's assignee
-func UpdateIssueAssignee(ctx *context.Context) {
-       issues := getActionIssues(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       assigneeID := ctx.QueryInt64("id")
-       action := ctx.Query("action")
-
-       for _, issue := range issues {
-               switch action {
-               case "clear":
-                       if err := issue_service.DeleteNotPassedAssignee(issue, ctx.User, []*models.User{}); err != nil {
-                               ctx.ServerError("ClearAssignees", err)
-                               return
-                       }
-               default:
-                       assignee, err := models.GetUserByID(assigneeID)
-                       if err != nil {
-                               ctx.ServerError("GetUserByID", err)
-                               return
-                       }
-
-                       valid, err := models.CanBeAssigned(assignee, issue.Repo, issue.IsPull)
-                       if err != nil {
-                               ctx.ServerError("canBeAssigned", err)
-                               return
-                       }
-                       if !valid {
-                               ctx.ServerError("canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name})
-                               return
-                       }
-
-                       _, _, err = issue_service.ToggleAssignee(issue, ctx.User, assigneeID)
-                       if err != nil {
-                               ctx.ServerError("ToggleAssignee", err)
-                               return
-                       }
-               }
-       }
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "ok": true,
-       })
-}
-
-// UpdatePullReviewRequest add or remove review request
-func UpdatePullReviewRequest(ctx *context.Context) {
-       issues := getActionIssues(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       reviewID := ctx.QueryInt64("id")
-       action := ctx.Query("action")
-
-       // TODO: Not support 'clear' now
-       if action != "attach" && action != "detach" {
-               ctx.Status(403)
-               return
-       }
-
-       for _, issue := range issues {
-               if err := issue.LoadRepo(); err != nil {
-                       ctx.ServerError("issue.LoadRepo", err)
-                       return
-               }
-
-               if !issue.IsPull {
-                       log.Warn(
-                               "UpdatePullReviewRequest: refusing to add review request for non-PR issue %-v#%d",
-                               issue.Repo, issue.Index,
-                       )
-                       ctx.Status(403)
-                       return
-               }
-               if reviewID < 0 {
-                       // negative reviewIDs represent team requests
-                       if err := issue.Repo.GetOwner(); err != nil {
-                               ctx.ServerError("issue.Repo.GetOwner", err)
-                               return
-                       }
-
-                       if !issue.Repo.Owner.IsOrganization() {
-                               log.Warn(
-                                       "UpdatePullReviewRequest: refusing to add team review request for %s#%d owned by non organization UID[%d]",
-                                       issue.Repo.FullName(), issue.Index, issue.Repo.ID,
-                               )
-                               ctx.Status(403)
-                               return
-                       }
-
-                       team, err := models.GetTeamByID(-reviewID)
-                       if err != nil {
-                               ctx.ServerError("models.GetTeamByID", err)
-                               return
-                       }
-
-                       if team.OrgID != issue.Repo.OwnerID {
-                               log.Warn(
-                                       "UpdatePullReviewRequest: refusing to add team review request for UID[%d] team %s to %s#%d owned by UID[%d]",
-                                       team.OrgID, team.Name, issue.Repo.FullName(), issue.Index, issue.Repo.ID)
-                               ctx.Status(403)
-                               return
-                       }
-
-                       err = issue_service.IsValidTeamReviewRequest(team, ctx.User, action == "attach", issue)
-                       if err != nil {
-                               if models.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,
-                                               err,
-                                       )
-                                       ctx.Status(403)
-                                       return
-                               }
-                               ctx.ServerError("IsValidTeamReviewRequest", err)
-                               return
-                       }
-
-                       _, err = issue_service.TeamReviewRequest(issue, ctx.User, team, action == "attach")
-                       if err != nil {
-                               ctx.ServerError("TeamReviewRequest", err)
-                               return
-                       }
-                       continue
-               }
-
-               reviewer, err := models.GetUserByID(reviewID)
-               if err != nil {
-                       if models.IsErrUserNotExist(err) {
-                               log.Warn(
-                                       "UpdatePullReviewRequest: requested reviewer [%d] for %-v to %-v#%d is not exist: Error: %v",
-                                       reviewID, issue.Repo, issue.Index,
-                                       err,
-                               )
-                               ctx.Status(403)
-                               return
-                       }
-                       ctx.ServerError("GetUserByID", err)
-                       return
-               }
-
-               err = issue_service.IsValidReviewRequest(reviewer, ctx.User, action == "attach", issue, nil)
-               if err != nil {
-                       if models.IsErrNotValidReviewRequest(err) {
-                               log.Warn(
-                                       "UpdatePullReviewRequest: refusing to add invalid review request for %-v to %-v#%d: Error: %v",
-                                       reviewer, issue.Repo, issue.Index,
-                                       err,
-                               )
-                               ctx.Status(403)
-                               return
-                       }
-                       ctx.ServerError("isValidReviewRequest", err)
-                       return
-               }
-
-               _, err = issue_service.ReviewRequest(issue, ctx.User, reviewer, action == "attach")
-               if err != nil {
-                       ctx.ServerError("ReviewRequest", err)
-                       return
-               }
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "ok": true,
-       })
-}
-
-// UpdateIssueStatus change issue's status
-func UpdateIssueStatus(ctx *context.Context) {
-       issues := getActionIssues(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       var isClosed bool
-       switch action := ctx.Query("action"); action {
-       case "open":
-               isClosed = false
-       case "close":
-               isClosed = true
-       default:
-               log.Warn("Unrecognized action: %s", action)
-       }
-
-       if _, err := models.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.User, isClosed); err != nil {
-                               if models.IsErrDependenciesLeft(err) {
-                                       ctx.JSON(http.StatusPreconditionFailed, map[string]interface{}{
-                                               "error": "cannot close this issue because it still has open dependencies",
-                                       })
-                                       return
-                               }
-                               ctx.ServerError("ChangeStatus", err)
-                               return
-                       }
-               }
-       }
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "ok": true,
-       })
-}
-
-// NewComment create a comment for issue
-func NewComment(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.CreateCommentForm)
-       issue := GetActionIssue(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
-               if log.IsTrace() {
-                       if ctx.IsSigned {
-                               issueType := "issues"
-                               if issue.IsPull {
-                                       issueType = "pulls"
-                               }
-                               log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
-                                       "User in Repo has Permissions: %-+v",
-                                       ctx.User,
-                                       log.NewColoredIDValue(issue.PosterID),
-                                       issueType,
-                                       ctx.Repo.Repository,
-                                       ctx.Repo.Permission)
-                       } else {
-                               log.Trace("Permission Denied: Not logged in")
-                       }
-               }
-
-               ctx.Error(http.StatusForbidden)
-               return
-       }
-
-       if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.User.IsAdmin {
-               ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked"))
-               ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
-               return
-       }
-
-       var attachments []string
-       if setting.Attachment.Enabled {
-               attachments = form.Files
-       }
-
-       if ctx.HasError() {
-               ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
-               ctx.Redirect(issue.HTMLURL())
-               return
-       }
-
-       var comment *models.Comment
-       defer func() {
-               // Check if issue admin/poster changes the status of issue.
-               if (ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) || (ctx.IsSigned && issue.IsPoster(ctx.User.ID))) &&
-                       (form.Status == "reopen" || form.Status == "close") &&
-                       !(issue.IsPull && issue.PullRequest.HasMerged) {
-
-                       // Duplication and conflict check should apply to reopen pull request.
-                       var pr *models.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)
-                               if err != nil {
-                                       if !models.IsErrPullRequestNotExist(err) {
-                                               ctx.ServerError("GetUnmergedPullRequest", err)
-                                               return
-                                       }
-                               }
-
-                               // Regenerate patch and test conflict.
-                               if pr == nil {
-                                       pull_service.AddToTaskQueue(issue.PullRequest)
-                               }
-                       }
-
-                       if pr != nil {
-                               ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index))
-                       } else {
-                               isClosed := form.Status == "close"
-                               if err := issue_service.ChangeStatus(issue, ctx.User, isClosed); err != nil {
-                                       log.Error("ChangeStatus: %v", err)
-
-                                       if models.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), http.StatusSeeOther)
-                                               } else {
-                                                       ctx.Flash.Error(ctx.Tr("repo.issues.dependency.issue_close_blocked"))
-                                                       ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index), http.StatusSeeOther)
-                                               }
-                                               return
-                                       }
-                               } else {
-                                       if err := stopTimerIfAvailable(ctx.User, issue); err != nil {
-                                               ctx.ServerError("CreateOrStopIssueStopwatch", err)
-                                               return
-                                       }
-
-                                       log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed)
-                               }
-                       }
-               }
-
-               // Redirect to comment hashtag if there is any actual content.
-               typeName := "issues"
-               if issue.IsPull {
-                       typeName = "pulls"
-               }
-               if comment != nil {
-                       ctx.Redirect(fmt.Sprintf("%s/%s/%d#%s", ctx.Repo.RepoLink, typeName, issue.Index, comment.HashTag()))
-               } else {
-                       ctx.Redirect(fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, typeName, issue.Index))
-               }
-       }()
-
-       // Fix #321: Allow empty comments, as long as we have attachments.
-       if len(form.Content) == 0 && len(attachments) == 0 {
-               return
-       }
-
-       comment, err := comment_service.CreateIssueComment(ctx.User, ctx.Repo.Repository, issue, form.Content, attachments)
-       if err != nil {
-               ctx.ServerError("CreateIssueComment", err)
-               return
-       }
-
-       log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
-}
-
-// UpdateCommentContent change comment of issue's content
-func UpdateCommentContent(ctx *context.Context) {
-       comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
-       if err != nil {
-               ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
-               return
-       }
-
-       if err := comment.LoadIssue(); err != nil {
-               ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
-               return
-       }
-
-       if comment.Type == models.CommentTypeComment {
-               if err := comment.LoadAttachments(); err != nil {
-                       ctx.ServerError("LoadAttachments", err)
-                       return
-               }
-       }
-
-       if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
-               ctx.Error(http.StatusForbidden)
-               return
-       } else if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode {
-               ctx.Error(http.StatusNoContent)
-               return
-       }
-
-       oldContent := comment.Content
-       comment.Content = ctx.Query("content")
-       if len(comment.Content) == 0 {
-               ctx.JSON(http.StatusOK, map[string]interface{}{
-                       "content": "",
-               })
-               return
-       }
-       if err = comment_service.UpdateComment(comment, ctx.User, oldContent); err != nil {
-               ctx.ServerError("UpdateComment", err)
-               return
-       }
-
-       files := ctx.QueryStrings("files[]")
-       if err := updateAttachments(comment, files); err != nil {
-               ctx.ServerError("UpdateAttachments", err)
-               return
-       }
-
-       content, err := markdown.RenderString(&markup.RenderContext{
-               URLPrefix: ctx.Query("context"),
-               Metas:     ctx.Repo.Repository.ComposeMetas(),
-       }, comment.Content)
-       if err != nil {
-               ctx.ServerError("RenderString", err)
-               return
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "content":     content,
-               "attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content),
-       })
-}
-
-// DeleteComment delete comment of issue
-func DeleteComment(ctx *context.Context) {
-       comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
-       if err != nil {
-               ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
-               return
-       }
-
-       if err := comment.LoadIssue(); err != nil {
-               ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
-               return
-       }
-
-       if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
-               ctx.Error(http.StatusForbidden)
-               return
-       } else if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode {
-               ctx.Error(http.StatusNoContent)
-               return
-       }
-
-       if err = comment_service.DeleteComment(ctx.User, comment); err != nil {
-               ctx.ServerError("DeleteCommentByID", err)
-               return
-       }
-
-       ctx.Status(200)
-}
-
-// ChangeIssueReaction create a reaction for issue
-func ChangeIssueReaction(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.ReactionForm)
-       issue := GetActionIssue(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
-               if log.IsTrace() {
-                       if ctx.IsSigned {
-                               issueType := "issues"
-                               if issue.IsPull {
-                                       issueType = "pulls"
-                               }
-                               log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
-                                       "User in Repo has Permissions: %-+v",
-                                       ctx.User,
-                                       log.NewColoredIDValue(issue.PosterID),
-                                       issueType,
-                                       ctx.Repo.Repository,
-                                       ctx.Repo.Permission)
-                       } else {
-                               log.Trace("Permission Denied: Not logged in")
-                       }
-               }
-
-               ctx.Error(http.StatusForbidden)
-               return
-       }
-
-       if ctx.HasError() {
-               ctx.ServerError("ChangeIssueReaction", errors.New(ctx.GetErrMsg()))
-               return
-       }
-
-       switch ctx.Params(":action") {
-       case "react":
-               reaction, err := models.CreateIssueReaction(ctx.User, issue, form.Content)
-               if err != nil {
-                       if models.IsErrForbiddenIssueReaction(err) {
-                               ctx.ServerError("ChangeIssueReaction", err)
-                               return
-                       }
-                       log.Info("CreateIssueReaction: %s", err)
-                       break
-               }
-               // Reload new reactions
-               issue.Reactions = nil
-               if err = issue.LoadAttributes(); err != nil {
-                       log.Info("issue.LoadAttributes: %s", err)
-                       break
-               }
-
-               log.Trace("Reaction for issue created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, reaction.ID)
-       case "unreact":
-               if err := models.DeleteIssueReaction(ctx.User, issue, form.Content); err != nil {
-                       ctx.ServerError("DeleteIssueReaction", err)
-                       return
-               }
-
-               // Reload new reactions
-               issue.Reactions = nil
-               if err := issue.LoadAttributes(); err != nil {
-                       log.Info("issue.LoadAttributes: %s", err)
-                       break
-               }
-
-               log.Trace("Reaction for issue removed: %d/%d", ctx.Repo.Repository.ID, issue.ID)
-       default:
-               ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.Params(":action")), nil)
-               return
-       }
-
-       if len(issue.Reactions) == 0 {
-               ctx.JSON(http.StatusOK, map[string]interface{}{
-                       "empty": true,
-                       "html":  "",
-               })
-               return
-       }
-
-       html, err := ctx.HTMLString(string(tplReactions), map[string]interface{}{
-               "ctx":       ctx.Data,
-               "ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index),
-               "Reactions": issue.Reactions.GroupByType(),
-       })
-       if err != nil {
-               ctx.ServerError("ChangeIssueReaction.HTMLString", err)
-               return
-       }
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "html": html,
-       })
-}
-
-// ChangeCommentReaction create a reaction for comment
-func ChangeCommentReaction(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.ReactionForm)
-       comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
-       if err != nil {
-               ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
-               return
-       }
-
-       if err := comment.LoadIssue(); err != nil {
-               ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
-               return
-       }
-
-       if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull)) {
-               if log.IsTrace() {
-                       if ctx.IsSigned {
-                               issueType := "issues"
-                               if comment.Issue.IsPull {
-                                       issueType = "pulls"
-                               }
-                               log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
-                                       "User in Repo has Permissions: %-+v",
-                                       ctx.User,
-                                       log.NewColoredIDValue(comment.Issue.PosterID),
-                                       issueType,
-                                       ctx.Repo.Repository,
-                                       ctx.Repo.Permission)
-                       } else {
-                               log.Trace("Permission Denied: Not logged in")
-                       }
-               }
-
-               ctx.Error(http.StatusForbidden)
-               return
-       } else if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode {
-               ctx.Error(http.StatusNoContent)
-               return
-       }
-
-       switch ctx.Params(":action") {
-       case "react":
-               reaction, err := models.CreateCommentReaction(ctx.User, comment.Issue, comment, form.Content)
-               if err != nil {
-                       if models.IsErrForbiddenIssueReaction(err) {
-                               ctx.ServerError("ChangeIssueReaction", err)
-                               return
-                       }
-                       log.Info("CreateCommentReaction: %s", err)
-                       break
-               }
-               // Reload new reactions
-               comment.Reactions = nil
-               if err = comment.LoadReactions(ctx.Repo.Repository); err != nil {
-                       log.Info("comment.LoadReactions: %s", err)
-                       break
-               }
-
-               log.Trace("Reaction for comment created: %d/%d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID, reaction.ID)
-       case "unreact":
-               if err := models.DeleteCommentReaction(ctx.User, comment.Issue, comment, form.Content); err != nil {
-                       ctx.ServerError("DeleteCommentReaction", err)
-                       return
-               }
-
-               // Reload new reactions
-               comment.Reactions = nil
-               if err = comment.LoadReactions(ctx.Repo.Repository); err != nil {
-                       log.Info("comment.LoadReactions: %s", err)
-                       break
-               }
-
-               log.Trace("Reaction for comment removed: %d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID)
-       default:
-               ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.Params(":action")), nil)
-               return
-       }
-
-       if len(comment.Reactions) == 0 {
-               ctx.JSON(http.StatusOK, map[string]interface{}{
-                       "empty": true,
-                       "html":  "",
-               })
-               return
-       }
-
-       html, err := ctx.HTMLString(string(tplReactions), map[string]interface{}{
-               "ctx":       ctx.Data,
-               "ActionURL": fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID),
-               "Reactions": comment.Reactions.GroupByType(),
-       })
-       if err != nil {
-               ctx.ServerError("ChangeCommentReaction.HTMLString", err)
-               return
-       }
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "html": html,
-       })
-}
-
-func addParticipant(poster *models.User, participants []*models.User) []*models.User {
-       for _, part := range participants {
-               if poster.ID == part.ID {
-                       return participants
-               }
-       }
-       return append(participants, poster)
-}
-
-func filterXRefComments(ctx *context.Context, issue *models.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 {
-                       var err error
-                       // Set RefRepo for description in template
-                       c.RefRepo, err = models.GetRepositoryByID(c.RefRepoID)
-                       if err != nil {
-                               return err
-                       }
-                       perm, err := models.GetUserRepoPermission(c.RefRepo, ctx.User)
-                       if err != nil {
-                               return err
-                       }
-                       if !perm.CanReadIssuesOrPulls(c.RefIsPull) {
-                               issue.Comments = append(issue.Comments[:i], issue.Comments[i+1:]...)
-                               continue
-                       }
-               }
-               i++
-       }
-       return nil
-}
-
-// GetIssueAttachments returns attachments for the issue
-func GetIssueAttachments(ctx *context.Context) {
-       issue := GetActionIssue(ctx)
-       var attachments = make([]*api.Attachment, len(issue.Attachments))
-       for i := 0; i < len(issue.Attachments); i++ {
-               attachments[i] = convert.ToReleaseAttachment(issue.Attachments[i])
-       }
-       ctx.JSON(http.StatusOK, attachments)
-}
-
-// GetCommentAttachments returns attachments for the comment
-func GetCommentAttachments(ctx *context.Context) {
-       comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
-       if err != nil {
-               ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
-               return
-       }
-       var attachments = make([]*api.Attachment, 0)
-       if comment.Type == models.CommentTypeComment {
-               if err := comment.LoadAttachments(); err != nil {
-                       ctx.ServerError("LoadAttachments", err)
-                       return
-               }
-               for i := 0; i < len(comment.Attachments); i++ {
-                       attachments = append(attachments, convert.ToReleaseAttachment(comment.Attachments[i]))
-               }
-       }
-       ctx.JSON(http.StatusOK, attachments)
-}
-
-func updateAttachments(item interface{}, files []string) error {
-       var attachments []*models.Attachment
-       switch content := item.(type) {
-       case *models.Issue:
-               attachments = content.Attachments
-       case *models.Comment:
-               attachments = content.Attachments
-       default:
-               return fmt.Errorf("Unknown Type: %T", content)
-       }
-       for i := 0; i < len(attachments); i++ {
-               if util.IsStringInSlice(attachments[i].UUID, files) {
-                       continue
-               }
-               if err := models.DeleteAttachment(attachments[i], true); err != nil {
-                       return err
-               }
-       }
-       var err error
-       if len(files) > 0 {
-               switch content := item.(type) {
-               case *models.Issue:
-                       err = content.UpdateAttachments(files)
-               case *models.Comment:
-                       err = content.UpdateAttachments(files)
-               default:
-                       return fmt.Errorf("Unknown Type: %T", content)
-               }
-               if err != nil {
-                       return err
-               }
-       }
-       switch content := item.(type) {
-       case *models.Issue:
-               content.Attachments, err = models.GetAttachmentsByIssueID(content.ID)
-       case *models.Comment:
-               content.Attachments, err = models.GetAttachmentsByCommentID(content.ID)
-       default:
-               return fmt.Errorf("Unknown Type: %T", content)
-       }
-       return err
-}
-
-func attachmentsHTML(ctx *context.Context, attachments []*models.Attachment, content string) string {
-       attachHTML, err := ctx.HTMLString(string(tplAttachment), map[string]interface{}{
-               "ctx":         ctx.Data,
-               "Attachments": attachments,
-               "Content":     content,
-       })
-       if err != nil {
-               ctx.ServerError("attachmentsHTML.HTMLString", err)
-               return ""
-       }
-       return attachHTML
-}
-
-// combineLabelComments combine the nearby label comments as one.
-func combineLabelComments(issue *models.Issue) {
-       var prev, cur *models.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 ||
-                       (prev != nil && prev.PosterID != cur.PosterID) ||
-                       (prev != nil && cur.CreatedUnix-prev.CreatedUnix >= 60) {
-                       if cur.Type == models.CommentTypeLabel && cur.Label != nil {
-                               if cur.Content != "1" {
-                                       cur.RemovedLabels = append(cur.RemovedLabels, cur.Label)
-                               } else {
-                                       cur.AddedLabels = append(cur.AddedLabels, cur.Label)
-                               }
-                       }
-                       continue
-               }
-
-               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 cur.Content != "1" {
-                                       prev.RemovedLabels = append(prev.RemovedLabels, cur.Label)
-                               } else {
-                                       prev.AddedLabels = append(prev.AddedLabels, cur.Label)
-                               }
-                               prev.CreatedUnix = cur.CreatedUnix
-                               // remove the current comment since it has been combined to prev comment
-                               issue.Comments = append(issue.Comments[:i], issue.Comments[i+1:]...)
-                               i--
-                       } else { // if prev is not a label comment, start a new group
-                               if cur.Content != "1" {
-                                       cur.RemovedLabels = append(cur.RemovedLabels, cur.Label)
-                               } else {
-                                       cur.AddedLabels = append(cur.AddedLabels, cur.Label)
-                               }
-                       }
-               }
-       }
-}
-
-// get all teams that current user can mention
-func handleTeamMentions(ctx *context.Context) {
-       if ctx.User == nil || !ctx.Repo.Owner.IsOrganization() {
-               return
-       }
-
-       isAdmin := false
-       var err error
-       // Admin has super access.
-       if ctx.User.IsAdmin {
-               isAdmin = true
-       } else {
-               isAdmin, err = ctx.Repo.Owner.IsOwnedBy(ctx.User.ID)
-               if err != nil {
-                       ctx.ServerError("IsOwnedBy", err)
-                       return
-               }
-       }
-
-       if isAdmin {
-               if err := ctx.Repo.Owner.GetTeams(&models.SearchTeamOptions{}); err != nil {
-                       ctx.ServerError("GetTeams", err)
-                       return
-               }
-       } else {
-               ctx.Repo.Owner.Teams, err = ctx.Repo.Owner.GetUserTeams(ctx.User.ID)
-               if err != nil {
-                       ctx.ServerError("GetUserTeams", err)
-                       return
-               }
-       }
-
-       ctx.Data["MentionableTeams"] = ctx.Repo.Owner.Teams
-       ctx.Data["MentionableTeamsOrg"] = ctx.Repo.Owner.Name
-       ctx.Data["MentionableTeamsOrgAvatar"] = ctx.Repo.Owner.RelAvatarLink()
-}
diff --git a/routers/repo/issue_dependency.go b/routers/repo/issue_dependency.go
deleted file mode 100644 (file)
index 8a83c7b..0000000
+++ /dev/null
@@ -1,129 +0,0 @@
-// Copyright 2018 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 repo
-
-import (
-       "net/http"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/setting"
-)
-
-// AddDependency adds new dependencies
-func AddDependency(ctx *context.Context) {
-       issueIndex := ctx.ParamsInt64("index")
-       issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex)
-       if err != nil {
-               ctx.ServerError("GetIssueByIndex", err)
-               return
-       }
-
-       // Check if the Repo is allowed to have dependencies
-       if !ctx.Repo.CanCreateIssueDependencies(ctx.User, issue.IsPull) {
-               ctx.Error(http.StatusForbidden, "CanCreateIssueDependencies")
-               return
-       }
-
-       depID := ctx.QueryInt64("newDependency")
-
-       if err = issue.LoadRepo(); err != nil {
-               ctx.ServerError("LoadRepo", err)
-               return
-       }
-
-       // Redirect
-       defer ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
-
-       // Dependency
-       dep, err := models.GetIssueByID(depID)
-       if err != nil {
-               ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_issue_not_exist"))
-               return
-       }
-
-       // Check if both issues are in the same repo if cross repository dependencies is not enabled
-       if issue.RepoID != dep.RepoID && !setting.Service.AllowCrossRepositoryDependencies {
-               ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_not_same_repo"))
-               return
-       }
-
-       // Check if issue and dependency is the same
-       if dep.ID == issue.ID {
-               ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_same_issue"))
-               return
-       }
-
-       err = models.CreateIssueDependency(ctx.User, issue, dep)
-       if err != nil {
-               if models.IsErrDependencyExists(err) {
-                       ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_exists"))
-                       return
-               } else if models.IsErrCircularDependency(err) {
-                       ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_cannot_create_circular"))
-                       return
-               } else {
-                       ctx.ServerError("CreateOrUpdateIssueDependency", err)
-                       return
-               }
-       }
-}
-
-// RemoveDependency removes the dependency
-func RemoveDependency(ctx *context.Context) {
-       issueIndex := ctx.ParamsInt64("index")
-       issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex)
-       if err != nil {
-               ctx.ServerError("GetIssueByIndex", err)
-               return
-       }
-
-       // Check if the Repo is allowed to have dependencies
-       if !ctx.Repo.CanCreateIssueDependencies(ctx.User, issue.IsPull) {
-               ctx.Error(http.StatusForbidden, "CanCreateIssueDependencies")
-               return
-       }
-
-       depID := ctx.QueryInt64("removeDependencyID")
-
-       if err = issue.LoadRepo(); err != nil {
-               ctx.ServerError("LoadRepo", err)
-               return
-       }
-
-       // Dependency Type
-       depTypeStr := ctx.Req.PostForm.Get("dependencyType")
-
-       var depType models.DependencyType
-
-       switch depTypeStr {
-       case "blockedBy":
-               depType = models.DependencyTypeBlockedBy
-       case "blocking":
-               depType = models.DependencyTypeBlocking
-       default:
-               ctx.Error(http.StatusBadRequest, "GetDependecyType")
-               return
-       }
-
-       // Dependency
-       dep, err := models.GetIssueByID(depID)
-       if err != nil {
-               ctx.ServerError("GetIssueByID", err)
-               return
-       }
-
-       if err = models.RemoveIssueDependency(ctx.User, issue, dep, depType); err != nil {
-               if models.IsErrDependencyNotExists(err) {
-                       ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_not_exist"))
-                       return
-               }
-               ctx.ServerError("RemoveIssueDependency", err)
-               return
-       }
-
-       // Redirect
-       ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
-}
diff --git a/routers/repo/issue_label.go b/routers/repo/issue_label.go
deleted file mode 100644 (file)
index 7361260..0000000
+++ /dev/null
@@ -1,222 +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 repo
-
-import (
-       "net/http"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-       issue_service "code.gitea.io/gitea/services/issue"
-)
-
-const (
-       tplLabels base.TplName = "repo/issue/labels"
-)
-
-// Labels render issue's labels page
-func Labels(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.labels")
-       ctx.Data["PageIsIssueList"] = true
-       ctx.Data["PageIsLabels"] = true
-       ctx.Data["RequireTribute"] = true
-       ctx.Data["LabelTemplates"] = models.LabelTemplates
-       ctx.HTML(http.StatusOK, tplLabels)
-}
-
-// InitializeLabels init labels for a repository
-func InitializeLabels(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.InitializeLabelsForm)
-       if ctx.HasError() {
-               ctx.Redirect(ctx.Repo.RepoLink + "/labels")
-               return
-       }
-
-       if err := models.InitializeLabels(models.DefaultDBContext(), ctx.Repo.Repository.ID, form.TemplateName, false); err != nil {
-               if models.IsErrIssueLabelTemplateLoad(err) {
-                       originalErr := err.(models.ErrIssueLabelTemplateLoad).OriginalError
-                       ctx.Flash.Error(ctx.Tr("repo.issues.label_templates.fail_to_load_file", form.TemplateName, originalErr))
-                       ctx.Redirect(ctx.Repo.RepoLink + "/labels")
-                       return
-               }
-               ctx.ServerError("InitializeLabels", err)
-               return
-       }
-       ctx.Redirect(ctx.Repo.RepoLink + "/labels")
-}
-
-// RetrieveLabels find all the labels of a repository and organization
-func RetrieveLabels(ctx *context.Context) {
-       labels, err := models.GetLabelsByRepoID(ctx.Repo.Repository.ID, ctx.Query("sort"), models.ListOptions{})
-       if err != nil {
-               ctx.ServerError("RetrieveLabels.GetLabels", err)
-               return
-       }
-
-       for _, l := range labels {
-               l.CalOpenIssues()
-       }
-
-       ctx.Data["Labels"] = labels
-
-       if ctx.Repo.Owner.IsOrganization() {
-               orgLabels, err := models.GetLabelsByOrgID(ctx.Repo.Owner.ID, ctx.Query("sort"), models.ListOptions{})
-               if err != nil {
-                       ctx.ServerError("GetLabelsByOrgID", err)
-                       return
-               }
-               for _, l := range orgLabels {
-                       l.CalOpenOrgIssues(ctx.Repo.Repository.ID, l.ID)
-               }
-               ctx.Data["OrgLabels"] = orgLabels
-
-               org, err := models.GetOrgByName(ctx.Repo.Owner.LowerName)
-               if err != nil {
-                       ctx.ServerError("GetOrgByName", err)
-                       return
-               }
-               if ctx.User != nil {
-                       ctx.Org.IsOwner, err = org.IsOwnedBy(ctx.User.ID)
-                       if err != nil {
-                               ctx.ServerError("org.IsOwnedBy", err)
-                               return
-                       }
-                       ctx.Org.OrgLink = org.OrganisationLink()
-                       ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner
-                       ctx.Data["OrganizationLink"] = ctx.Org.OrgLink
-               }
-       }
-       ctx.Data["NumLabels"] = len(labels)
-       ctx.Data["SortType"] = ctx.Query("sort")
-}
-
-// NewLabel create new label for repository
-func NewLabel(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.CreateLabelForm)
-       ctx.Data["Title"] = ctx.Tr("repo.labels")
-       ctx.Data["PageIsLabels"] = true
-
-       if ctx.HasError() {
-               ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
-               ctx.Redirect(ctx.Repo.RepoLink + "/labels")
-               return
-       }
-
-       l := &models.Label{
-               RepoID:      ctx.Repo.Repository.ID,
-               Name:        form.Title,
-               Description: form.Description,
-               Color:       form.Color,
-       }
-       if err := models.NewLabel(l); err != nil {
-               ctx.ServerError("NewLabel", err)
-               return
-       }
-       ctx.Redirect(ctx.Repo.RepoLink + "/labels")
-}
-
-// UpdateLabel update a label's name and color
-func UpdateLabel(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.CreateLabelForm)
-       l, err := models.GetLabelInRepoByID(ctx.Repo.Repository.ID, form.ID)
-       if err != nil {
-               switch {
-               case models.IsErrRepoLabelNotExist(err):
-                       ctx.Error(http.StatusNotFound)
-               default:
-                       ctx.ServerError("UpdateLabel", err)
-               }
-               return
-       }
-
-       l.Name = form.Title
-       l.Description = form.Description
-       l.Color = form.Color
-       if err := models.UpdateLabel(l); err != nil {
-               ctx.ServerError("UpdateLabel", err)
-               return
-       }
-       ctx.Redirect(ctx.Repo.RepoLink + "/labels")
-}
-
-// DeleteLabel delete a label
-func DeleteLabel(ctx *context.Context) {
-       if err := models.DeleteLabel(ctx.Repo.Repository.ID, ctx.QueryInt64("id")); err != nil {
-               ctx.Flash.Error("DeleteLabel: " + err.Error())
-       } else {
-               ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": ctx.Repo.RepoLink + "/labels",
-       })
-}
-
-// UpdateIssueLabel change issue's labels
-func UpdateIssueLabel(ctx *context.Context) {
-       issues := getActionIssues(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       switch action := ctx.Query("action"); action {
-       case "clear":
-               for _, issue := range issues {
-                       if err := issue_service.ClearLabels(issue, ctx.User); err != nil {
-                               ctx.ServerError("ClearLabels", err)
-                               return
-                       }
-               }
-       case "attach", "detach", "toggle":
-               label, err := models.GetLabelByID(ctx.QueryInt64("id"))
-               if err != nil {
-                       if models.IsErrRepoLabelNotExist(err) {
-                               ctx.Error(http.StatusNotFound, "GetLabelByID")
-                       } else {
-                               ctx.ServerError("GetLabelByID", err)
-                       }
-                       return
-               }
-
-               if action == "toggle" {
-                       // detach if any issues already have label, otherwise attach
-                       action = "attach"
-                       for _, issue := range issues {
-                               if issue.HasLabel(label.ID) {
-                                       action = "detach"
-                                       break
-                               }
-                       }
-               }
-
-               if action == "attach" {
-                       for _, issue := range issues {
-                               if err = issue_service.AddLabel(issue, ctx.User, label); err != nil {
-                                       ctx.ServerError("AddLabel", err)
-                                       return
-                               }
-                       }
-               } else {
-                       for _, issue := range issues {
-                               if err = issue_service.RemoveLabel(issue, ctx.User, label); err != nil {
-                                       ctx.ServerError("RemoveLabel", err)
-                                       return
-                               }
-                       }
-               }
-       default:
-               log.Warn("Unrecognized action: %s", action)
-               ctx.Error(http.StatusInternalServerError)
-               return
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "ok": true,
-       })
-}
diff --git a/routers/repo/issue_label_test.go b/routers/repo/issue_label_test.go
deleted file mode 100644 (file)
index bf9e72a..0000000
+++ /dev/null
@@ -1,168 +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 repo
-
-import (
-       "net/http"
-       "strconv"
-       "testing"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/test"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-
-       "github.com/stretchr/testify/assert"
-)
-
-func int64SliceToCommaSeparated(a []int64) string {
-       s := ""
-       for i, n := range a {
-               if i > 0 {
-                       s += ","
-               }
-               s += strconv.Itoa(int(n))
-       }
-       return s
-}
-
-func TestInitializeLabels(t *testing.T) {
-       models.PrepareTestEnv(t)
-       ctx := test.MockContext(t, "user2/repo1/labels/initialize")
-       test.LoadUser(t, ctx, 2)
-       test.LoadRepo(t, ctx, 2)
-       web.SetForm(ctx, &forms.InitializeLabelsForm{TemplateName: "Default"})
-       InitializeLabels(ctx)
-       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
-       models.AssertExistsAndLoadBean(t, &models.Label{
-               RepoID: 2,
-               Name:   "enhancement",
-               Color:  "#84b6eb",
-       })
-       assert.Equal(t, "/user2/repo2/labels", test.RedirectURL(ctx.Resp))
-}
-
-func TestRetrieveLabels(t *testing.T) {
-       models.PrepareTestEnv(t)
-       for _, testCase := range []struct {
-               RepoID           int64
-               Sort             string
-               ExpectedLabelIDs []int64
-       }{
-               {1, "", []int64{1, 2}},
-               {1, "leastissues", []int64{2, 1}},
-               {2, "", []int64{}},
-       } {
-               ctx := test.MockContext(t, "user/repo/issues")
-               test.LoadUser(t, ctx, 2)
-               test.LoadRepo(t, ctx, testCase.RepoID)
-               ctx.Req.Form.Set("sort", testCase.Sort)
-               RetrieveLabels(ctx)
-               assert.False(t, ctx.Written())
-               labels, ok := ctx.Data["Labels"].([]*models.Label)
-               assert.True(t, ok)
-               if assert.Len(t, labels, len(testCase.ExpectedLabelIDs)) {
-                       for i, label := range labels {
-                               assert.EqualValues(t, testCase.ExpectedLabelIDs[i], label.ID)
-                       }
-               }
-       }
-}
-
-func TestNewLabel(t *testing.T) {
-       models.PrepareTestEnv(t)
-       ctx := test.MockContext(t, "user2/repo1/labels/edit")
-       test.LoadUser(t, ctx, 2)
-       test.LoadRepo(t, ctx, 1)
-       web.SetForm(ctx, &forms.CreateLabelForm{
-               Title: "newlabel",
-               Color: "#abcdef",
-       })
-       NewLabel(ctx)
-       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
-       models.AssertExistsAndLoadBean(t, &models.Label{
-               Name:  "newlabel",
-               Color: "#abcdef",
-       })
-       assert.Equal(t, "/user2/repo1/labels", test.RedirectURL(ctx.Resp))
-}
-
-func TestUpdateLabel(t *testing.T) {
-       models.PrepareTestEnv(t)
-       ctx := test.MockContext(t, "user2/repo1/labels/edit")
-       test.LoadUser(t, ctx, 2)
-       test.LoadRepo(t, ctx, 1)
-       web.SetForm(ctx, &forms.CreateLabelForm{
-               ID:    2,
-               Title: "newnameforlabel",
-               Color: "#abcdef",
-       })
-       UpdateLabel(ctx)
-       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
-       models.AssertExistsAndLoadBean(t, &models.Label{
-               ID:    2,
-               Name:  "newnameforlabel",
-               Color: "#abcdef",
-       })
-       assert.Equal(t, "/user2/repo1/labels", test.RedirectURL(ctx.Resp))
-}
-
-func TestDeleteLabel(t *testing.T) {
-       models.PrepareTestEnv(t)
-       ctx := test.MockContext(t, "user2/repo1/labels/delete")
-       test.LoadUser(t, ctx, 2)
-       test.LoadRepo(t, ctx, 1)
-       ctx.Req.Form.Set("id", "2")
-       DeleteLabel(ctx)
-       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
-       models.AssertNotExistsBean(t, &models.Label{ID: 2})
-       models.AssertNotExistsBean(t, &models.IssueLabel{LabelID: 2})
-       assert.Equal(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg)
-}
-
-func TestUpdateIssueLabel_Clear(t *testing.T) {
-       models.PrepareTestEnv(t)
-       ctx := test.MockContext(t, "user2/repo1/issues/labels")
-       test.LoadUser(t, ctx, 2)
-       test.LoadRepo(t, ctx, 1)
-       ctx.Req.Form.Set("issue_ids", "1,3")
-       ctx.Req.Form.Set("action", "clear")
-       UpdateIssueLabel(ctx)
-       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
-       models.AssertNotExistsBean(t, &models.IssueLabel{IssueID: 1})
-       models.AssertNotExistsBean(t, &models.IssueLabel{IssueID: 3})
-       models.CheckConsistencyFor(t, &models.Label{})
-}
-
-func TestUpdateIssueLabel_Toggle(t *testing.T) {
-       for _, testCase := range []struct {
-               Action      string
-               IssueIDs    []int64
-               LabelID     int64
-               ExpectedAdd bool // whether we expect the label to be added to the issues
-       }{
-               {"attach", []int64{1, 3}, 1, true},
-               {"detach", []int64{1, 3}, 1, false},
-               {"toggle", []int64{1, 3}, 1, false},
-               {"toggle", []int64{1, 2}, 2, true},
-       } {
-               models.PrepareTestEnv(t)
-               ctx := test.MockContext(t, "user2/repo1/issues/labels")
-               test.LoadUser(t, ctx, 2)
-               test.LoadRepo(t, ctx, 1)
-               ctx.Req.Form.Set("issue_ids", int64SliceToCommaSeparated(testCase.IssueIDs))
-               ctx.Req.Form.Set("action", testCase.Action)
-               ctx.Req.Form.Set("id", strconv.Itoa(int(testCase.LabelID)))
-               UpdateIssueLabel(ctx)
-               assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
-               for _, issueID := range testCase.IssueIDs {
-                       models.AssertExistsIf(t, testCase.ExpectedAdd, &models.IssueLabel{
-                               IssueID: issueID,
-                               LabelID: testCase.LabelID,
-                       })
-               }
-               models.CheckConsistencyFor(t, &models.Label{})
-       }
-}
diff --git a/routers/repo/issue_lock.go b/routers/repo/issue_lock.go
deleted file mode 100644 (file)
index 36894b4..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-// Copyright 2019 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 repo
-
-import (
-       "net/http"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-)
-
-// LockIssue locks an issue. This would limit commenting abilities to
-// users with write access to the repo.
-func LockIssue(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.IssueLockForm)
-       issue := GetActionIssue(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       if issue.IsLocked {
-               ctx.Flash.Error(ctx.Tr("repo.issues.lock_duplicate"))
-               ctx.Redirect(issue.HTMLURL())
-               return
-       }
-
-       if !form.HasValidReason() {
-               ctx.Flash.Error(ctx.Tr("repo.issues.lock.unknown_reason"))
-               ctx.Redirect(issue.HTMLURL())
-               return
-       }
-
-       if err := models.LockIssue(&models.IssueLockOptions{
-               Doer:   ctx.User,
-               Issue:  issue,
-               Reason: form.Reason,
-       }); err != nil {
-               ctx.ServerError("LockIssue", err)
-               return
-       }
-
-       ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
-}
-
-// UnlockIssue unlocks a previously locked issue.
-func UnlockIssue(ctx *context.Context) {
-
-       issue := GetActionIssue(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       if !issue.IsLocked {
-               ctx.Flash.Error(ctx.Tr("repo.issues.unlock_error"))
-               ctx.Redirect(issue.HTMLURL())
-               return
-       }
-
-       if err := models.UnlockIssue(&models.IssueLockOptions{
-               Doer:  ctx.User,
-               Issue: issue,
-       }); err != nil {
-               ctx.ServerError("UnlockIssue", err)
-               return
-       }
-
-       ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
-}
diff --git a/routers/repo/issue_stopwatch.go b/routers/repo/issue_stopwatch.go
deleted file mode 100644 (file)
index b8efb3b..0000000
+++ /dev/null
@@ -1,108 +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 repo
-
-import (
-       "net/http"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/context"
-)
-
-// IssueStopwatch creates or stops a stopwatch for the given issue.
-func IssueStopwatch(c *context.Context) {
-       issue := GetActionIssue(c)
-       if c.Written() {
-               return
-       }
-
-       var showSuccessMessage bool
-
-       if !models.StopwatchExists(c.User.ID, issue.ID) {
-               showSuccessMessage = true
-       }
-
-       if !c.Repo.CanUseTimetracker(issue, c.User) {
-               c.NotFound("CanUseTimetracker", nil)
-               return
-       }
-
-       if err := models.CreateOrStopIssueStopwatch(c.User, issue); err != nil {
-               c.ServerError("CreateOrStopIssueStopwatch", err)
-               return
-       }
-
-       if showSuccessMessage {
-               c.Flash.Success(c.Tr("repo.issues.tracker_auto_close"))
-       }
-
-       url := issue.HTMLURL()
-       c.Redirect(url, http.StatusSeeOther)
-}
-
-// CancelStopwatch cancel the stopwatch
-func CancelStopwatch(c *context.Context) {
-       issue := GetActionIssue(c)
-       if c.Written() {
-               return
-       }
-       if !c.Repo.CanUseTimetracker(issue, c.User) {
-               c.NotFound("CanUseTimetracker", nil)
-               return
-       }
-
-       if err := models.CancelStopwatch(c.User, issue); err != nil {
-               c.ServerError("CancelStopwatch", err)
-               return
-       }
-
-       url := issue.HTMLURL()
-       c.Redirect(url, http.StatusSeeOther)
-}
-
-// GetActiveStopwatch is the middleware that sets .ActiveStopwatch on context
-func GetActiveStopwatch(c *context.Context) {
-       if strings.HasPrefix(c.Req.URL.Path, "/api") {
-               return
-       }
-
-       if !c.IsSigned {
-               return
-       }
-
-       _, sw, err := models.HasUserStopwatch(c.User.ID)
-       if err != nil {
-               c.ServerError("HasUserStopwatch", err)
-               return
-       }
-
-       if sw == nil || sw.ID == 0 {
-               return
-       }
-
-       issue, err := models.GetIssueByID(sw.IssueID)
-       if err != nil || issue == nil {
-               c.ServerError("GetIssueByID", err)
-               return
-       }
-       if err = issue.LoadRepo(); err != nil {
-               c.ServerError("LoadRepo", err)
-               return
-       }
-
-       c.Data["ActiveStopwatch"] = StopwatchTmplInfo{
-               issue.Repo.FullName(),
-               issue.Index,
-               sw.Seconds() + 1, // ensure time is never zero in ui
-       }
-}
-
-// StopwatchTmplInfo is a view on a stopwatch specifically for template rendering
-type StopwatchTmplInfo struct {
-       RepoSlug   string
-       IssueIndex int64
-       Seconds    int64
-}
diff --git a/routers/repo/issue_test.go b/routers/repo/issue_test.go
deleted file mode 100644 (file)
index 7fb837f..0000000
+++ /dev/null
@@ -1,324 +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 repo
-
-import (
-       "testing"
-
-       "code.gitea.io/gitea/models"
-       "github.com/stretchr/testify/assert"
-)
-
-func TestCombineLabelComments(t *testing.T) {
-       var kases = []struct {
-               name           string
-               beforeCombined []*models.Comment
-               afterCombined  []*models.Comment
-       }{
-               {
-                       name: "kase 1",
-                       beforeCombined: []*models.Comment{
-                               {
-                                       Type:     models.CommentTypeLabel,
-                                       PosterID: 1,
-                                       Content:  "1",
-                                       Label: &models.Label{
-                                               Name: "kind/bug",
-                                       },
-                                       CreatedUnix: 0,
-                               },
-                               {
-                                       Type:     models.CommentTypeLabel,
-                                       PosterID: 1,
-                                       Content:  "",
-                                       Label: &models.Label{
-                                               Name: "kind/bug",
-                                       },
-                                       CreatedUnix: 0,
-                               },
-                               {
-                                       Type:        models.CommentTypeComment,
-                                       PosterID:    1,
-                                       Content:     "test",
-                                       CreatedUnix: 0,
-                               },
-                       },
-                       afterCombined: []*models.Comment{
-                               {
-                                       Type:        models.CommentTypeLabel,
-                                       PosterID:    1,
-                                       Content:     "1",
-                                       CreatedUnix: 0,
-                                       AddedLabels: []*models.Label{
-                                               {
-                                                       Name: "kind/bug",
-                                               },
-                                       },
-                                       RemovedLabels: []*models.Label{
-                                               {
-                                                       Name: "kind/bug",
-                                               },
-                                       },
-                                       Label: &models.Label{
-                                               Name: "kind/bug",
-                                       },
-                               },
-                               {
-                                       Type:        models.CommentTypeComment,
-                                       PosterID:    1,
-                                       Content:     "test",
-                                       CreatedUnix: 0,
-                               },
-                       },
-               },
-               {
-                       name: "kase 2",
-                       beforeCombined: []*models.Comment{
-                               {
-                                       Type:     models.CommentTypeLabel,
-                                       PosterID: 1,
-                                       Content:  "1",
-                                       Label: &models.Label{
-                                               Name: "kind/bug",
-                                       },
-                                       CreatedUnix: 0,
-                               },
-                               {
-                                       Type:     models.CommentTypeLabel,
-                                       PosterID: 1,
-                                       Content:  "",
-                                       Label: &models.Label{
-                                               Name: "kind/bug",
-                                       },
-                                       CreatedUnix: 70,
-                               },
-                               {
-                                       Type:        models.CommentTypeComment,
-                                       PosterID:    1,
-                                       Content:     "test",
-                                       CreatedUnix: 0,
-                               },
-                       },
-                       afterCombined: []*models.Comment{
-                               {
-                                       Type:        models.CommentTypeLabel,
-                                       PosterID:    1,
-                                       Content:     "1",
-                                       CreatedUnix: 0,
-                                       AddedLabels: []*models.Label{
-                                               {
-                                                       Name: "kind/bug",
-                                               },
-                                       },
-                                       Label: &models.Label{
-                                               Name: "kind/bug",
-                                       },
-                               },
-                               {
-                                       Type:        models.CommentTypeLabel,
-                                       PosterID:    1,
-                                       Content:     "",
-                                       CreatedUnix: 70,
-                                       RemovedLabels: []*models.Label{
-                                               {
-                                                       Name: "kind/bug",
-                                               },
-                                       },
-                                       Label: &models.Label{
-                                               Name: "kind/bug",
-                                       },
-                               },
-                               {
-                                       Type:        models.CommentTypeComment,
-                                       PosterID:    1,
-                                       Content:     "test",
-                                       CreatedUnix: 0,
-                               },
-                       },
-               },
-               {
-                       name: "kase 3",
-                       beforeCombined: []*models.Comment{
-                               {
-                                       Type:     models.CommentTypeLabel,
-                                       PosterID: 1,
-                                       Content:  "1",
-                                       Label: &models.Label{
-                                               Name: "kind/bug",
-                                       },
-                                       CreatedUnix: 0,
-                               },
-                               {
-                                       Type:     models.CommentTypeLabel,
-                                       PosterID: 2,
-                                       Content:  "",
-                                       Label: &models.Label{
-                                               Name: "kind/bug",
-                                       },
-                                       CreatedUnix: 0,
-                               },
-                               {
-                                       Type:        models.CommentTypeComment,
-                                       PosterID:    1,
-                                       Content:     "test",
-                                       CreatedUnix: 0,
-                               },
-                       },
-                       afterCombined: []*models.Comment{
-                               {
-                                       Type:        models.CommentTypeLabel,
-                                       PosterID:    1,
-                                       Content:     "1",
-                                       CreatedUnix: 0,
-                                       AddedLabels: []*models.Label{
-                                               {
-                                                       Name: "kind/bug",
-                                               },
-                                       },
-                                       Label: &models.Label{
-                                               Name: "kind/bug",
-                                       },
-                               },
-                               {
-                                       Type:        models.CommentTypeLabel,
-                                       PosterID:    2,
-                                       Content:     "",
-                                       CreatedUnix: 0,
-                                       RemovedLabels: []*models.Label{
-                                               {
-                                                       Name: "kind/bug",
-                                               },
-                                       },
-                                       Label: &models.Label{
-                                               Name: "kind/bug",
-                                       },
-                               },
-                               {
-                                       Type:        models.CommentTypeComment,
-                                       PosterID:    1,
-                                       Content:     "test",
-                                       CreatedUnix: 0,
-                               },
-                       },
-               },
-               {
-                       name: "kase 4",
-                       beforeCombined: []*models.Comment{
-                               {
-                                       Type:     models.CommentTypeLabel,
-                                       PosterID: 1,
-                                       Content:  "1",
-                                       Label: &models.Label{
-                                               Name: "kind/bug",
-                                       },
-                                       CreatedUnix: 0,
-                               },
-                               {
-                                       Type:     models.CommentTypeLabel,
-                                       PosterID: 1,
-                                       Content:  "1",
-                                       Label: &models.Label{
-                                               Name: "kind/backport",
-                                       },
-                                       CreatedUnix: 10,
-                               },
-                       },
-                       afterCombined: []*models.Comment{
-                               {
-                                       Type:        models.CommentTypeLabel,
-                                       PosterID:    1,
-                                       Content:     "1",
-                                       CreatedUnix: 10,
-                                       AddedLabels: []*models.Label{
-                                               {
-                                                       Name: "kind/bug",
-                                               },
-                                               {
-                                                       Name: "kind/backport",
-                                               },
-                                       },
-                                       Label: &models.Label{
-                                               Name: "kind/bug",
-                                       },
-                               },
-                       },
-               },
-               {
-                       name: "kase 5",
-                       beforeCombined: []*models.Comment{
-                               {
-                                       Type:     models.CommentTypeLabel,
-                                       PosterID: 1,
-                                       Content:  "1",
-                                       Label: &models.Label{
-                                               Name: "kind/bug",
-                                       },
-                                       CreatedUnix: 0,
-                               },
-                               {
-                                       Type:        models.CommentTypeComment,
-                                       PosterID:    2,
-                                       Content:     "testtest",
-                                       CreatedUnix: 0,
-                               },
-                               {
-                                       Type:     models.CommentTypeLabel,
-                                       PosterID: 1,
-                                       Content:  "",
-                                       Label: &models.Label{
-                                               Name: "kind/bug",
-                                       },
-                                       CreatedUnix: 0,
-                               },
-                       },
-                       afterCombined: []*models.Comment{
-                               {
-                                       Type:     models.CommentTypeLabel,
-                                       PosterID: 1,
-                                       Content:  "1",
-                                       Label: &models.Label{
-                                               Name: "kind/bug",
-                                       },
-                                       AddedLabels: []*models.Label{
-                                               {
-                                                       Name: "kind/bug",
-                                               },
-                                       },
-                                       CreatedUnix: 0,
-                               },
-                               {
-                                       Type:        models.CommentTypeComment,
-                                       PosterID:    2,
-                                       Content:     "testtest",
-                                       CreatedUnix: 0,
-                               },
-                               {
-                                       Type:     models.CommentTypeLabel,
-                                       PosterID: 1,
-                                       Content:  "",
-                                       RemovedLabels: []*models.Label{
-                                               {
-                                                       Name: "kind/bug",
-                                               },
-                                       },
-                                       Label: &models.Label{
-                                               Name: "kind/bug",
-                                       },
-                                       CreatedUnix: 0,
-                               },
-                       },
-               },
-       }
-
-       for _, kase := range kases {
-               t.Run(kase.name, func(t *testing.T) {
-                       var issue = models.Issue{
-                               Comments: kase.beforeCombined,
-                       }
-                       combineLabelComments(&issue)
-                       assert.EqualValues(t, kase.afterCombined, issue.Comments)
-               })
-       }
-}
diff --git a/routers/repo/issue_timetrack.go b/routers/repo/issue_timetrack.go
deleted file mode 100644 (file)
index 3770cd7..0000000
+++ /dev/null
@@ -1,86 +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 repo
-
-import (
-       "net/http"
-       "time"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-)
-
-// AddTimeManually tracks time manually
-func AddTimeManually(c *context.Context) {
-       form := web.GetForm(c).(*forms.AddTimeManuallyForm)
-       issue := GetActionIssue(c)
-       if c.Written() {
-               return
-       }
-       if !c.Repo.CanUseTimetracker(issue, c.User) {
-               c.NotFound("CanUseTimetracker", nil)
-               return
-       }
-       url := issue.HTMLURL()
-
-       if c.HasError() {
-               c.Flash.Error(c.GetErrMsg())
-               c.Redirect(url)
-               return
-       }
-
-       total := time.Duration(form.Hours)*time.Hour + time.Duration(form.Minutes)*time.Minute
-
-       if total <= 0 {
-               c.Flash.Error(c.Tr("repo.issues.add_time_sum_to_small"))
-               c.Redirect(url, http.StatusSeeOther)
-               return
-       }
-
-       if _, err := models.AddTime(c.User, issue, int64(total.Seconds()), time.Now()); err != nil {
-               c.ServerError("AddTime", err)
-               return
-       }
-
-       c.Redirect(url, http.StatusSeeOther)
-}
-
-// DeleteTime deletes tracked time
-func DeleteTime(c *context.Context) {
-       issue := GetActionIssue(c)
-       if c.Written() {
-               return
-       }
-       if !c.Repo.CanUseTimetracker(issue, c.User) {
-               c.NotFound("CanUseTimetracker", nil)
-               return
-       }
-
-       t, err := models.GetTrackedTimeByID(c.ParamsInt64(":timeid"))
-       if err != nil {
-               if models.IsErrNotExist(err) {
-                       c.NotFound("time not found", err)
-                       return
-               }
-               c.Error(http.StatusInternalServerError, "GetTrackedTimeByID", err.Error())
-               return
-       }
-
-       // only OP or admin may delete
-       if !c.IsSigned || (!c.IsUserSiteAdmin() && c.User.ID != t.UserID) {
-               c.Error(http.StatusForbidden, "not allowed")
-               return
-       }
-
-       if err = models.DeleteTime(t); err != nil {
-               c.ServerError("DeleteTime", err)
-               return
-       }
-
-       c.Flash.Success(c.Tr("repo.issues.del_time_history", models.SecToTime(t.Time)))
-       c.Redirect(issue.HTMLURL())
-}
diff --git a/routers/repo/issue_watch.go b/routers/repo/issue_watch.go
deleted file mode 100644 (file)
index dabbff8..0000000
+++ /dev/null
@@ -1,57 +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 repo
-
-import (
-       "net/http"
-       "strconv"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/log"
-)
-
-// IssueWatch sets issue watching
-func IssueWatch(ctx *context.Context) {
-       issue := GetActionIssue(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
-               if log.IsTrace() {
-                       if ctx.IsSigned {
-                               issueType := "issues"
-                               if issue.IsPull {
-                                       issueType = "pulls"
-                               }
-                               log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
-                                       "User in Repo has Permissions: %-+v",
-                                       ctx.User,
-                                       log.NewColoredIDValue(issue.PosterID),
-                                       issueType,
-                                       ctx.Repo.Repository,
-                                       ctx.Repo.Permission)
-                       } else {
-                               log.Trace("Permission Denied: Not logged in")
-                       }
-               }
-               ctx.Error(http.StatusForbidden)
-               return
-       }
-
-       watch, err := strconv.ParseBool(ctx.Req.PostForm.Get("watch"))
-       if err != nil {
-               ctx.ServerError("watch is not bool", err)
-               return
-       }
-
-       if err := models.CreateOrUpdateIssueWatch(ctx.User.ID, issue.ID, watch); err != nil {
-               ctx.ServerError("CreateOrUpdateIssueWatch", err)
-               return
-       }
-
-       ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
-}
diff --git a/routers/repo/lfs.go b/routers/repo/lfs.go
deleted file mode 100644 (file)
index 173ffb7..0000000
+++ /dev/null
@@ -1,537 +0,0 @@
-// Copyright 2019 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 repo
-
-import (
-       "bytes"
-       "fmt"
-       gotemplate "html/template"
-       "io"
-       "io/ioutil"
-       "net/http"
-       "path"
-       "strconv"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/charset"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/git"
-       "code.gitea.io/gitea/modules/git/pipeline"
-       "code.gitea.io/gitea/modules/lfs"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/storage"
-       "code.gitea.io/gitea/modules/typesniffer"
-)
-
-const (
-       tplSettingsLFS         base.TplName = "repo/settings/lfs"
-       tplSettingsLFSLocks    base.TplName = "repo/settings/lfs_locks"
-       tplSettingsLFSFile     base.TplName = "repo/settings/lfs_file"
-       tplSettingsLFSFileFind base.TplName = "repo/settings/lfs_file_find"
-       tplSettingsLFSPointers base.TplName = "repo/settings/lfs_pointers"
-)
-
-// LFSFiles shows a repository's LFS files
-func LFSFiles(ctx *context.Context) {
-       if !setting.LFS.StartServer {
-               ctx.NotFound("LFSFiles", nil)
-               return
-       }
-       page := ctx.QueryInt("page")
-       if page <= 1 {
-               page = 1
-       }
-       total, err := ctx.Repo.Repository.CountLFSMetaObjects()
-       if err != nil {
-               ctx.ServerError("LFSFiles", err)
-               return
-       }
-       ctx.Data["Total"] = total
-
-       pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
-       ctx.Data["Title"] = ctx.Tr("repo.settings.lfs")
-       ctx.Data["PageIsSettingsLFS"] = true
-       lfsMetaObjects, err := ctx.Repo.Repository.GetLFSMetaObjects(pager.Paginater.Current(), setting.UI.ExplorePagingNum)
-       if err != nil {
-               ctx.ServerError("LFSFiles", err)
-               return
-       }
-       ctx.Data["LFSFiles"] = lfsMetaObjects
-       ctx.Data["Page"] = pager
-       ctx.HTML(http.StatusOK, tplSettingsLFS)
-}
-
-// LFSLocks shows a repository's LFS locks
-func LFSLocks(ctx *context.Context) {
-       if !setting.LFS.StartServer {
-               ctx.NotFound("LFSLocks", nil)
-               return
-       }
-       ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
-
-       page := ctx.QueryInt("page")
-       if page <= 1 {
-               page = 1
-       }
-       total, err := models.CountLFSLockByRepoID(ctx.Repo.Repository.ID)
-       if err != nil {
-               ctx.ServerError("LFSLocks", err)
-               return
-       }
-       ctx.Data["Total"] = total
-
-       pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
-       ctx.Data["Title"] = ctx.Tr("repo.settings.lfs_locks")
-       ctx.Data["PageIsSettingsLFS"] = true
-       lfsLocks, err := models.GetLFSLockByRepoID(ctx.Repo.Repository.ID, pager.Paginater.Current(), setting.UI.ExplorePagingNum)
-       if err != nil {
-               ctx.ServerError("LFSLocks", err)
-               return
-       }
-       ctx.Data["LFSLocks"] = lfsLocks
-
-       if len(lfsLocks) == 0 {
-               ctx.Data["Page"] = pager
-               ctx.HTML(http.StatusOK, tplSettingsLFSLocks)
-               return
-       }
-
-       // Clone base repo.
-       tmpBasePath, err := models.CreateTemporaryPath("locks")
-       if err != nil {
-               log.Error("Failed to create temporary path: %v", err)
-               ctx.ServerError("LFSLocks", err)
-               return
-       }
-       defer func() {
-               if err := models.RemoveTemporaryPath(tmpBasePath); err != nil {
-                       log.Error("LFSLocks: RemoveTemporaryPath: %v", err)
-               }
-       }()
-
-       if err := git.Clone(ctx.Repo.Repository.RepoPath(), tmpBasePath, git.CloneRepoOptions{
-               Bare:   true,
-               Shared: true,
-       }); err != nil {
-               log.Error("Failed to clone repository: %s (%v)", ctx.Repo.Repository.FullName(), err)
-               ctx.ServerError("LFSLocks", fmt.Errorf("Failed to clone repository: %s (%v)", ctx.Repo.Repository.FullName(), err))
-               return
-       }
-
-       gitRepo, err := git.OpenRepository(tmpBasePath)
-       if err != nil {
-               log.Error("Unable to open temporary repository: %s (%v)", tmpBasePath, err)
-               ctx.ServerError("LFSLocks", fmt.Errorf("Failed to open new temporary repository in: %s %v", tmpBasePath, err))
-               return
-       }
-       defer gitRepo.Close()
-
-       filenames := make([]string, len(lfsLocks))
-
-       for i, lock := range lfsLocks {
-               filenames[i] = lock.Path
-       }
-
-       if err := gitRepo.ReadTreeToIndex(ctx.Repo.Repository.DefaultBranch); err != nil {
-               log.Error("Unable to read the default branch to the index: %s (%v)", ctx.Repo.Repository.DefaultBranch, err)
-               ctx.ServerError("LFSLocks", fmt.Errorf("Unable to read the default branch to the index: %s (%v)", ctx.Repo.Repository.DefaultBranch, err))
-               return
-       }
-
-       name2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{
-               Attributes: []string{"lockable"},
-               Filenames:  filenames,
-               CachedOnly: true,
-       })
-       if err != nil {
-               log.Error("Unable to check attributes in %s (%v)", tmpBasePath, err)
-               ctx.ServerError("LFSLocks", err)
-               return
-       }
-
-       lockables := make([]bool, len(lfsLocks))
-       for i, lock := range lfsLocks {
-               attribute2info, has := name2attribute2info[lock.Path]
-               if !has {
-                       continue
-               }
-               if attribute2info["lockable"] != "set" {
-                       continue
-               }
-               lockables[i] = true
-       }
-       ctx.Data["Lockables"] = lockables
-
-       filelist, err := gitRepo.LsFiles(filenames...)
-       if err != nil {
-               log.Error("Unable to lsfiles in %s (%v)", tmpBasePath, err)
-               ctx.ServerError("LFSLocks", err)
-               return
-       }
-
-       filemap := make(map[string]bool, len(filelist))
-       for _, name := range filelist {
-               filemap[name] = true
-       }
-
-       linkable := make([]bool, len(lfsLocks))
-       for i, lock := range lfsLocks {
-               linkable[i] = filemap[lock.Path]
-       }
-       ctx.Data["Linkable"] = linkable
-
-       ctx.Data["Page"] = pager
-       ctx.HTML(http.StatusOK, tplSettingsLFSLocks)
-}
-
-// LFSLockFile locks a file
-func LFSLockFile(ctx *context.Context) {
-       if !setting.LFS.StartServer {
-               ctx.NotFound("LFSLocks", nil)
-               return
-       }
-       originalPath := ctx.Query("path")
-       lockPath := originalPath
-       if len(lockPath) == 0 {
-               ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_locking_path", originalPath))
-               ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
-               return
-       }
-       if lockPath[len(lockPath)-1] == '/' {
-               ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_lock_directory", originalPath))
-               ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
-               return
-       }
-       lockPath = path.Clean("/" + lockPath)[1:]
-       if len(lockPath) == 0 {
-               ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_locking_path", originalPath))
-               ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
-               return
-       }
-
-       _, err := models.CreateLFSLock(&models.LFSLock{
-               Repo:  ctx.Repo.Repository,
-               Path:  lockPath,
-               Owner: ctx.User,
-       })
-       if err != nil {
-               if models.IsErrLFSLockAlreadyExist(err) {
-                       ctx.Flash.Error(ctx.Tr("repo.settings.lfs_lock_already_exists", originalPath))
-                       ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
-                       return
-               }
-               ctx.ServerError("LFSLockFile", err)
-               return
-       }
-       ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
-}
-
-// LFSUnlock forcibly unlocks an LFS lock
-func LFSUnlock(ctx *context.Context) {
-       if !setting.LFS.StartServer {
-               ctx.NotFound("LFSUnlock", nil)
-               return
-       }
-       _, err := models.DeleteLFSLockByID(ctx.ParamsInt64("lid"), ctx.User, true)
-       if err != nil {
-               ctx.ServerError("LFSUnlock", err)
-               return
-       }
-       ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
-}
-
-// LFSFileGet serves a single LFS file
-func LFSFileGet(ctx *context.Context) {
-       if !setting.LFS.StartServer {
-               ctx.NotFound("LFSFileGet", nil)
-               return
-       }
-       ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
-       oid := ctx.Params("oid")
-       ctx.Data["Title"] = oid
-       ctx.Data["PageIsSettingsLFS"] = true
-       meta, err := ctx.Repo.Repository.GetLFSMetaObjectByOid(oid)
-       if err != nil {
-               if err == models.ErrLFSObjectNotExist {
-                       ctx.NotFound("LFSFileGet", nil)
-                       return
-               }
-               ctx.ServerError("LFSFileGet", err)
-               return
-       }
-       ctx.Data["LFSFile"] = meta
-       dataRc, err := lfs.ReadMetaObject(meta.Pointer)
-       if err != nil {
-               ctx.ServerError("LFSFileGet", err)
-               return
-       }
-       defer dataRc.Close()
-       buf := make([]byte, 1024)
-       n, err := dataRc.Read(buf)
-       if err != nil {
-               ctx.ServerError("Data", err)
-               return
-       }
-       buf = buf[:n]
-
-       st := typesniffer.DetectContentType(buf)
-       ctx.Data["IsTextFile"] = st.IsText()
-       isRepresentableAsText := st.IsRepresentableAsText()
-
-       fileSize := meta.Size
-       ctx.Data["FileSize"] = meta.Size
-       ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s.git/info/lfs/objects/%s/%s", setting.AppURL, ctx.Repo.Repository.FullName(), meta.Oid, "direct")
-       switch {
-       case isRepresentableAsText:
-               if st.IsSvgImage() {
-                       ctx.Data["IsImageFile"] = true
-               }
-
-               if fileSize >= setting.UI.MaxDisplayFileSize {
-                       ctx.Data["IsFileTooLarge"] = true
-                       break
-               }
-
-               buf := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc))
-
-               // Building code view blocks with line number on server side.
-               fileContent, _ := ioutil.ReadAll(buf)
-
-               var output bytes.Buffer
-               lines := strings.Split(string(fileContent), "\n")
-               //Remove blank line at the end of file
-               if len(lines) > 0 && lines[len(lines)-1] == "" {
-                       lines = lines[:len(lines)-1]
-               }
-               for index, line := range lines {
-                       line = gotemplate.HTMLEscapeString(line)
-                       if index != len(lines)-1 {
-                               line += "\n"
-                       }
-                       output.WriteString(fmt.Sprintf(`<li class="L%d" rel="L%d">%s</li>`, index+1, index+1, line))
-               }
-               ctx.Data["FileContent"] = gotemplate.HTML(output.String())
-
-               output.Reset()
-               for i := 0; i < len(lines); i++ {
-                       output.WriteString(fmt.Sprintf(`<span id="L%d">%d</span>`, i+1, i+1))
-               }
-               ctx.Data["LineNums"] = gotemplate.HTML(output.String())
-
-       case st.IsPDF():
-               ctx.Data["IsPDFFile"] = true
-       case st.IsVideo():
-               ctx.Data["IsVideoFile"] = true
-       case st.IsAudio():
-               ctx.Data["IsAudioFile"] = true
-       case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()):
-               ctx.Data["IsImageFile"] = true
-       }
-       ctx.HTML(http.StatusOK, tplSettingsLFSFile)
-}
-
-// LFSDelete disassociates the provided oid from the repository and if the lfs file is no longer associated with any repositories - deletes it
-func LFSDelete(ctx *context.Context) {
-       if !setting.LFS.StartServer {
-               ctx.NotFound("LFSDelete", nil)
-               return
-       }
-       oid := ctx.Params("oid")
-       count, err := ctx.Repo.Repository.RemoveLFSMetaObjectByOid(oid)
-       if err != nil {
-               ctx.ServerError("LFSDelete", err)
-               return
-       }
-       // FIXME: Warning: the LFS store is not locked - and can't be locked - there could be a race condition here
-       // Please note a similar condition happens in models/repo.go DeleteRepository
-       if count == 0 {
-               oidPath := path.Join(oid[0:2], oid[2:4], oid[4:])
-               err = storage.LFS.Delete(oidPath)
-               if err != nil {
-                       ctx.ServerError("LFSDelete", err)
-                       return
-               }
-       }
-       ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs")
-}
-
-// LFSFileFind guesses a sha for the provided oid (or uses the provided sha) and then finds the commits that contain this sha
-func LFSFileFind(ctx *context.Context) {
-       if !setting.LFS.StartServer {
-               ctx.NotFound("LFSFind", nil)
-               return
-       }
-       oid := ctx.Query("oid")
-       size := ctx.QueryInt64("size")
-       if len(oid) == 0 || size == 0 {
-               ctx.NotFound("LFSFind", nil)
-               return
-       }
-       sha := ctx.Query("sha")
-       ctx.Data["Title"] = oid
-       ctx.Data["PageIsSettingsLFS"] = true
-       var hash git.SHA1
-       if len(sha) == 0 {
-               pointer := lfs.Pointer{Oid: oid, Size: size}
-               hash = git.ComputeBlobHash([]byte(pointer.StringContent()))
-               sha = hash.String()
-       } else {
-               hash = git.MustIDFromString(sha)
-       }
-       ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
-       ctx.Data["Oid"] = oid
-       ctx.Data["Size"] = size
-       ctx.Data["SHA"] = sha
-
-       results, err := pipeline.FindLFSFile(ctx.Repo.GitRepo, hash)
-       if err != nil && err != io.EOF {
-               log.Error("Failure in FindLFSFile: %v", err)
-               ctx.ServerError("LFSFind: FindLFSFile.", err)
-               return
-       }
-
-       ctx.Data["Results"] = results
-       ctx.HTML(http.StatusOK, tplSettingsLFSFileFind)
-}
-
-// LFSPointerFiles will search the repository for pointer files and report which are missing LFS files in the content store
-func LFSPointerFiles(ctx *context.Context) {
-       if !setting.LFS.StartServer {
-               ctx.NotFound("LFSFileGet", nil)
-               return
-       }
-       ctx.Data["PageIsSettingsLFS"] = true
-       err := git.LoadGitVersion()
-       if err != nil {
-               log.Fatal("Error retrieving git version: %v", err)
-       }
-       ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
-
-       err = func() error {
-               pointerChan := make(chan lfs.PointerBlob)
-               errChan := make(chan error, 1)
-               go lfs.SearchPointerBlobs(ctx, ctx.Repo.GitRepo, pointerChan, errChan)
-
-               numPointers := 0
-               var numAssociated, numNoExist, numAssociatable int
-
-               type pointerResult struct {
-                       SHA        string
-                       Oid        string
-                       Size       int64
-                       InRepo     bool
-                       Exists     bool
-                       Accessible bool
-               }
-
-               results := []pointerResult{}
-
-               contentStore := lfs.NewContentStore()
-               repo := ctx.Repo.Repository
-
-               for pointerBlob := range pointerChan {
-                       numPointers++
-
-                       result := pointerResult{
-                               SHA:  pointerBlob.Hash,
-                               Oid:  pointerBlob.Oid,
-                               Size: pointerBlob.Size,
-                       }
-
-                       if _, err := repo.GetLFSMetaObjectByOid(pointerBlob.Oid); err != nil {
-                               if err != models.ErrLFSObjectNotExist {
-                                       return err
-                               }
-                       } else {
-                               result.InRepo = true
-                       }
-
-                       result.Exists, err = contentStore.Exists(pointerBlob.Pointer)
-                       if err != nil {
-                               return err
-                       }
-
-                       if result.Exists {
-                               if !result.InRepo {
-                                       // Can we fix?
-                                       // OK well that's "simple"
-                                       // - we need to check whether current user has access to a repo that has access to the file
-                                       result.Accessible, err = models.LFSObjectAccessible(ctx.User, pointerBlob.Oid)
-                                       if err != nil {
-                                               return err
-                                       }
-                               } else {
-                                       result.Accessible = true
-                               }
-                       }
-
-                       if result.InRepo {
-                               numAssociated++
-                       }
-                       if !result.Exists {
-                               numNoExist++
-                       }
-                       if !result.InRepo && result.Accessible {
-                               numAssociatable++
-                       }
-
-                       results = append(results, result)
-               }
-
-               err, has := <-errChan
-               if has {
-                       return err
-               }
-
-               ctx.Data["Pointers"] = results
-               ctx.Data["NumPointers"] = numPointers
-               ctx.Data["NumAssociated"] = numAssociated
-               ctx.Data["NumAssociatable"] = numAssociatable
-               ctx.Data["NumNoExist"] = numNoExist
-               ctx.Data["NumNotAssociated"] = numPointers - numAssociated
-
-               return nil
-       }()
-       if err != nil {
-               ctx.ServerError("LFSPointerFiles", err)
-               return
-       }
-
-       ctx.HTML(http.StatusOK, tplSettingsLFSPointers)
-}
-
-// LFSAutoAssociate auto associates accessible lfs files
-func LFSAutoAssociate(ctx *context.Context) {
-       if !setting.LFS.StartServer {
-               ctx.NotFound("LFSAutoAssociate", nil)
-               return
-       }
-       oids := ctx.QueryStrings("oid")
-       metas := make([]*models.LFSMetaObject, len(oids))
-       for i, oid := range oids {
-               idx := strings.IndexRune(oid, ' ')
-               if idx < 0 || idx+1 > len(oid) {
-                       ctx.ServerError("LFSAutoAssociate", fmt.Errorf("Illegal oid input: %s", oid))
-                       return
-               }
-               var err error
-               metas[i] = &models.LFSMetaObject{}
-               metas[i].Size, err = strconv.ParseInt(oid[idx+1:], 10, 64)
-               if err != nil {
-                       ctx.ServerError("LFSAutoAssociate", fmt.Errorf("Illegal oid input: %s %v", oid, err))
-                       return
-               }
-               metas[i].Oid = oid[:idx]
-               //metas[i].RepositoryID = ctx.Repo.Repository.ID
-       }
-       if err := models.LFSAutoAssociate(metas, ctx.User, ctx.Repo.Repository.ID); err != nil {
-               ctx.ServerError("LFSAutoAssociate", err)
-               return
-       }
-       ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs")
-}
diff --git a/routers/repo/main_test.go b/routers/repo/main_test.go
deleted file mode 100644 (file)
index 04bbeeb..0000000
+++ /dev/null
@@ -1,16 +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 repo
-
-import (
-       "path/filepath"
-       "testing"
-
-       "code.gitea.io/gitea/models"
-)
-
-func TestMain(m *testing.M) {
-       models.MainTest(m, filepath.Join("..", ".."))
-}
diff --git a/routers/repo/middlewares.go b/routers/repo/middlewares.go
deleted file mode 100644 (file)
index 1b95a13..0000000
+++ /dev/null
@@ -1,72 +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 repo
-
-import (
-       "fmt"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/git"
-)
-
-// SetEditorconfigIfExists set editor config as render variable
-func SetEditorconfigIfExists(ctx *context.Context) {
-       if ctx.Repo.Repository.IsEmpty {
-               ctx.Data["Editorconfig"] = nil
-               return
-       }
-
-       ec, err := ctx.Repo.GetEditorconfig()
-
-       if err != nil && !git.IsErrNotExist(err) {
-               description := fmt.Sprintf("Error while getting .editorconfig file: %v", err)
-               if err := models.CreateRepositoryNotice(description); err != nil {
-                       ctx.ServerError("ErrCreatingReporitoryNotice", err)
-               }
-               return
-       }
-
-       ctx.Data["Editorconfig"] = ec
-}
-
-// SetDiffViewStyle set diff style as render variable
-func SetDiffViewStyle(ctx *context.Context) {
-       queryStyle := ctx.Query("style")
-
-       if !ctx.IsSigned {
-               ctx.Data["IsSplitStyle"] = queryStyle == "split"
-               return
-       }
-
-       var (
-               userStyle = ctx.User.DiffViewStyle
-               style     string
-       )
-
-       if queryStyle == "unified" || queryStyle == "split" {
-               style = queryStyle
-       } else if userStyle == "unified" || userStyle == "split" {
-               style = userStyle
-       } else {
-               style = "unified"
-       }
-
-       ctx.Data["IsSplitStyle"] = style == "split"
-       if err := ctx.User.UpdateDiffViewStyle(style); err != nil {
-               ctx.ServerError("ErrUpdateDiffViewStyle", err)
-       }
-}
-
-// SetWhitespaceBehavior set whitespace behavior as render variable
-func SetWhitespaceBehavior(ctx *context.Context) {
-       whitespaceBehavior := ctx.Query("whitespace")
-       switch whitespaceBehavior {
-       case "ignore-all", "ignore-eol", "ignore-change":
-               ctx.Data["WhitespaceBehavior"] = whitespaceBehavior
-       default:
-               ctx.Data["WhitespaceBehavior"] = ""
-       }
-}
diff --git a/routers/repo/migrate.go b/routers/repo/migrate.go
deleted file mode 100644 (file)
index 24d4ef4..0000000
+++ /dev/null
@@ -1,254 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// 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 repo
-
-import (
-       "net/http"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/lfs"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/migrations"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/structs"
-       "code.gitea.io/gitea/modules/task"
-       "code.gitea.io/gitea/modules/util"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-)
-
-const (
-       tplMigrate base.TplName = "repo/migrate/migrate"
-)
-
-// Migrate render migration of repository page
-func Migrate(ctx *context.Context) {
-       if setting.Repository.DisableMigrations {
-               ctx.Error(http.StatusForbidden, "Migrate: the site administrator has disabled migrations")
-               return
-       }
-
-       serviceType := structs.GitServiceType(ctx.QueryInt("service_type"))
-
-       setMigrationContextData(ctx, serviceType)
-
-       if serviceType == 0 {
-               ctx.Data["Org"] = ctx.Query("org")
-               ctx.Data["Mirror"] = ctx.Query("mirror")
-
-               ctx.HTML(http.StatusOK, tplMigrate)
-               return
-       }
-
-       ctx.Data["private"] = getRepoPrivate(ctx)
-       ctx.Data["mirror"] = ctx.Query("mirror") == "1"
-       ctx.Data["lfs"] = ctx.Query("lfs") == "1"
-       ctx.Data["wiki"] = ctx.Query("wiki") == "1"
-       ctx.Data["milestones"] = ctx.Query("milestones") == "1"
-       ctx.Data["labels"] = ctx.Query("labels") == "1"
-       ctx.Data["issues"] = ctx.Query("issues") == "1"
-       ctx.Data["pull_requests"] = ctx.Query("pull_requests") == "1"
-       ctx.Data["releases"] = ctx.Query("releases") == "1"
-
-       ctxUser := checkContextUser(ctx, ctx.QueryInt64("org"))
-       if ctx.Written() {
-               return
-       }
-       ctx.Data["ContextUser"] = ctxUser
-
-       ctx.HTML(http.StatusOK, base.TplName("repo/migrate/"+serviceType.Name()))
-}
-
-func handleMigrateError(ctx *context.Context, owner *models.User, err error, name string, tpl base.TplName, form *forms.MigrateRepoForm) {
-       if setting.Repository.DisableMigrations {
-               ctx.Error(http.StatusForbidden, "MigrateError: the site administrator has disabled migrations")
-               return
-       }
-
-       switch {
-       case migrations.IsRateLimitError(err):
-               ctx.RenderWithErr(ctx.Tr("form.visit_rate_limit"), tpl, form)
-       case migrations.IsTwoFactorAuthError(err):
-               ctx.RenderWithErr(ctx.Tr("form.2fa_auth_required"), tpl, form)
-       case models.IsErrReachLimitOfRepo(err):
-               ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_creation", owner.MaxCreationLimit()), tpl, form)
-       case models.IsErrRepoAlreadyExist(err):
-               ctx.Data["Err_RepoName"] = true
-               ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form)
-       case models.IsErrRepoFilesAlreadyExist(err):
-               ctx.Data["Err_RepoName"] = true
-               switch {
-               case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
-                       ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form)
-               case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
-                       ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form)
-               case setting.Repository.AllowDeleteOfUnadoptedRepositories:
-                       ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form)
-               default:
-                       ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form)
-               }
-       case models.IsErrNameReserved(err):
-               ctx.Data["Err_RepoName"] = true
-               ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tpl, form)
-       case models.IsErrNamePatternNotAllowed(err):
-               ctx.Data["Err_RepoName"] = true
-               ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tpl, form)
-       default:
-               remoteAddr, _ := forms.ParseRemoteAddr(form.CloneAddr, form.AuthUsername, form.AuthPassword)
-               err = util.URLSanitizedError(err, remoteAddr)
-               if strings.Contains(err.Error(), "Authentication failed") ||
-                       strings.Contains(err.Error(), "Bad credentials") ||
-                       strings.Contains(err.Error(), "could not read Username") {
-                       ctx.Data["Err_Auth"] = true
-                       ctx.RenderWithErr(ctx.Tr("form.auth_failed", err.Error()), tpl, form)
-               } else if strings.Contains(err.Error(), "fatal:") {
-                       ctx.Data["Err_CloneAddr"] = true
-                       ctx.RenderWithErr(ctx.Tr("repo.migrate.failed", err.Error()), tpl, form)
-               } else {
-                       ctx.ServerError(name, err)
-               }
-       }
-}
-
-func handleMigrateRemoteAddrError(ctx *context.Context, err error, tpl base.TplName, form *forms.MigrateRepoForm) {
-       if models.IsErrInvalidCloneAddr(err) {
-               addrErr := err.(*models.ErrInvalidCloneAddr)
-               switch {
-               case addrErr.IsProtocolInvalid:
-                       ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tpl, form)
-               case addrErr.IsURLError:
-                       ctx.RenderWithErr(ctx.Tr("form.url_error"), tpl, form)
-               case addrErr.IsPermissionDenied:
-                       if addrErr.LocalPath {
-                               ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tpl, form)
-                       } else if len(addrErr.PrivateNet) == 0 {
-                               ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_blocked"), tpl, form)
-                       } else {
-                               ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_private_ip"), tpl, form)
-                       }
-               case addrErr.IsInvalidPath:
-                       ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tpl, form)
-               default:
-                       log.Error("Error whilst updating url: %v", err)
-                       ctx.RenderWithErr(ctx.Tr("form.url_error"), tpl, form)
-               }
-       } else {
-               log.Error("Error whilst updating url: %v", err)
-               ctx.RenderWithErr(ctx.Tr("form.url_error"), tpl, form)
-       }
-}
-
-// MigratePost response for migrating from external git repository
-func MigratePost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.MigrateRepoForm)
-       if setting.Repository.DisableMigrations {
-               ctx.Error(http.StatusForbidden, "MigratePost: the site administrator has disabled migrations")
-               return
-       }
-
-       serviceType := structs.GitServiceType(form.Service)
-
-       setMigrationContextData(ctx, serviceType)
-
-       ctxUser := checkContextUser(ctx, form.UID)
-       if ctx.Written() {
-               return
-       }
-       ctx.Data["ContextUser"] = ctxUser
-
-       tpl := base.TplName("repo/migrate/" + serviceType.Name())
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tpl)
-               return
-       }
-
-       remoteAddr, err := forms.ParseRemoteAddr(form.CloneAddr, form.AuthUsername, form.AuthPassword)
-       if err == nil {
-               err = migrations.IsMigrateURLAllowed(remoteAddr, ctx.User)
-       }
-       if err != nil {
-               ctx.Data["Err_CloneAddr"] = true
-               handleMigrateRemoteAddrError(ctx, err, tpl, form)
-               return
-       }
-
-       form.LFS = form.LFS && setting.LFS.StartServer
-
-       if form.LFS && len(form.LFSEndpoint) > 0 {
-               ep := lfs.DetermineEndpoint("", form.LFSEndpoint)
-               if ep == nil {
-                       ctx.Data["Err_LFSEndpoint"] = true
-                       ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tpl, &form)
-                       return
-               }
-               err = migrations.IsMigrateURLAllowed(ep.String(), ctx.User)
-               if err != nil {
-                       ctx.Data["Err_LFSEndpoint"] = true
-                       handleMigrateRemoteAddrError(ctx, err, tpl, form)
-                       return
-               }
-       }
-
-       var opts = migrations.MigrateOptions{
-               OriginalURL:    form.CloneAddr,
-               GitServiceType: serviceType,
-               CloneAddr:      remoteAddr,
-               RepoName:       form.RepoName,
-               Description:    form.Description,
-               Private:        form.Private || setting.Repository.ForcePrivate,
-               Mirror:         form.Mirror && !setting.Repository.DisableMirrors,
-               LFS:            form.LFS,
-               LFSEndpoint:    form.LFSEndpoint,
-               AuthUsername:   form.AuthUsername,
-               AuthPassword:   form.AuthPassword,
-               AuthToken:      form.AuthToken,
-               Wiki:           form.Wiki,
-               Issues:         form.Issues,
-               Milestones:     form.Milestones,
-               Labels:         form.Labels,
-               Comments:       form.Issues || form.PullRequests,
-               PullRequests:   form.PullRequests,
-               Releases:       form.Releases,
-       }
-       if opts.Mirror {
-               opts.Issues = false
-               opts.Milestones = false
-               opts.Labels = false
-               opts.Comments = false
-               opts.PullRequests = false
-               opts.Releases = false
-       }
-
-       err = models.CheckCreateRepository(ctx.User, ctxUser, opts.RepoName, false)
-       if err != nil {
-               handleMigrateError(ctx, ctxUser, err, "MigratePost", tpl, form)
-               return
-       }
-
-       err = task.MigrateRepository(ctx.User, ctxUser, opts)
-       if err == nil {
-               ctx.Redirect(ctxUser.HomeLink() + "/" + opts.RepoName)
-               return
-       }
-
-       handleMigrateError(ctx, ctxUser, err, "MigratePost", tpl, form)
-}
-
-func setMigrationContextData(ctx *context.Context, serviceType structs.GitServiceType) {
-       ctx.Data["Title"] = ctx.Tr("new_migrate")
-
-       ctx.Data["LFSActive"] = setting.LFS.StartServer
-       ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate
-       ctx.Data["DisableMirrors"] = setting.Repository.DisableMirrors
-
-       // Plain git should be first
-       ctx.Data["Services"] = append([]structs.GitServiceType{structs.PlainGitService}, structs.SupportedFullGitService...)
-       ctx.Data["service"] = serviceType
-}
diff --git a/routers/repo/milestone.go b/routers/repo/milestone.go
deleted file mode 100644 (file)
index bb6b310..0000000
+++ /dev/null
@@ -1,299 +0,0 @@
-// Copyright 2018 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 repo
-
-import (
-       "net/http"
-       "strings"
-       "time"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/markup"
-       "code.gitea.io/gitea/modules/markup/markdown"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/structs"
-       "code.gitea.io/gitea/modules/timeutil"
-       "code.gitea.io/gitea/modules/util"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-
-       "xorm.io/builder"
-)
-
-const (
-       tplMilestone       base.TplName = "repo/issue/milestones"
-       tplMilestoneNew    base.TplName = "repo/issue/milestone_new"
-       tplMilestoneIssues base.TplName = "repo/issue/milestone_issues"
-)
-
-// Milestones render milestones page
-func Milestones(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.milestones")
-       ctx.Data["PageIsIssueList"] = true
-       ctx.Data["PageIsMilestones"] = true
-
-       isShowClosed := ctx.Query("state") == "closed"
-       stats, err := models.GetMilestonesStatsByRepoCond(builder.And(builder.Eq{"id": ctx.Repo.Repository.ID}))
-       if err != nil {
-               ctx.ServerError("MilestoneStats", err)
-               return
-       }
-       ctx.Data["OpenCount"] = stats.OpenCount
-       ctx.Data["ClosedCount"] = stats.ClosedCount
-
-       sortType := ctx.Query("sort")
-
-       keyword := strings.Trim(ctx.Query("q"), " ")
-
-       page := ctx.QueryInt("page")
-       if page <= 1 {
-               page = 1
-       }
-
-       var total int
-       var state structs.StateType
-       if !isShowClosed {
-               total = int(stats.OpenCount)
-               state = structs.StateOpen
-       } else {
-               total = int(stats.ClosedCount)
-               state = structs.StateClosed
-       }
-
-       miles, err := models.GetMilestones(models.GetMilestonesOption{
-               ListOptions: models.ListOptions{
-                       Page:     page,
-                       PageSize: setting.UI.IssuePagingNum,
-               },
-               RepoID:   ctx.Repo.Repository.ID,
-               State:    state,
-               SortType: sortType,
-               Name:     keyword,
-       })
-       if err != nil {
-               ctx.ServerError("GetMilestones", err)
-               return
-       }
-       if ctx.Repo.Repository.IsTimetrackerEnabled() {
-               if err := miles.LoadTotalTrackedTimes(); err != nil {
-                       ctx.ServerError("LoadTotalTrackedTimes", err)
-                       return
-               }
-       }
-       for _, m := range miles {
-               m.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
-                       URLPrefix: ctx.Repo.RepoLink,
-                       Metas:     ctx.Repo.Repository.ComposeMetas(),
-               }, m.Content)
-               if err != nil {
-                       ctx.ServerError("RenderString", err)
-                       return
-               }
-       }
-       ctx.Data["Milestones"] = miles
-
-       if isShowClosed {
-               ctx.Data["State"] = "closed"
-       } else {
-               ctx.Data["State"] = "open"
-       }
-
-       ctx.Data["SortType"] = sortType
-       ctx.Data["Keyword"] = keyword
-       ctx.Data["IsShowClosed"] = isShowClosed
-
-       pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
-       pager.AddParam(ctx, "state", "State")
-       pager.AddParam(ctx, "q", "Keyword")
-       ctx.Data["Page"] = pager
-
-       ctx.HTML(http.StatusOK, tplMilestone)
-}
-
-// NewMilestone render creating milestone page
-func NewMilestone(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.milestones.new")
-       ctx.Data["PageIsIssueList"] = true
-       ctx.Data["PageIsMilestones"] = true
-       ctx.HTML(http.StatusOK, tplMilestoneNew)
-}
-
-// NewMilestonePost response for creating milestone
-func NewMilestonePost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.CreateMilestoneForm)
-       ctx.Data["Title"] = ctx.Tr("repo.milestones.new")
-       ctx.Data["PageIsIssueList"] = true
-       ctx.Data["PageIsMilestones"] = true
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplMilestoneNew)
-               return
-       }
-
-       if len(form.Deadline) == 0 {
-               form.Deadline = "9999-12-31"
-       }
-       deadline, err := time.ParseInLocation("2006-01-02", form.Deadline, time.Local)
-       if err != nil {
-               ctx.Data["Err_Deadline"] = true
-               ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form)
-               return
-       }
-
-       deadline = time.Date(deadline.Year(), deadline.Month(), deadline.Day(), 23, 59, 59, 0, deadline.Location())
-       if err = models.NewMilestone(&models.Milestone{
-               RepoID:       ctx.Repo.Repository.ID,
-               Name:         form.Title,
-               Content:      form.Content,
-               DeadlineUnix: timeutil.TimeStamp(deadline.Unix()),
-       }); err != nil {
-               ctx.ServerError("NewMilestone", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.milestones.create_success", form.Title))
-       ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
-}
-
-// EditMilestone render edting milestone page
-func EditMilestone(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.milestones.edit")
-       ctx.Data["PageIsMilestones"] = true
-       ctx.Data["PageIsEditMilestone"] = true
-
-       m, err := models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
-       if err != nil {
-               if models.IsErrMilestoneNotExist(err) {
-                       ctx.NotFound("", nil)
-               } else {
-                       ctx.ServerError("GetMilestoneByRepoID", err)
-               }
-               return
-       }
-       ctx.Data["title"] = m.Name
-       ctx.Data["content"] = m.Content
-       if len(m.DeadlineString) > 0 {
-               ctx.Data["deadline"] = m.DeadlineString
-       }
-       ctx.HTML(http.StatusOK, tplMilestoneNew)
-}
-
-// EditMilestonePost response for edting milestone
-func EditMilestonePost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.CreateMilestoneForm)
-       ctx.Data["Title"] = ctx.Tr("repo.milestones.edit")
-       ctx.Data["PageIsMilestones"] = true
-       ctx.Data["PageIsEditMilestone"] = true
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplMilestoneNew)
-               return
-       }
-
-       if len(form.Deadline) == 0 {
-               form.Deadline = "9999-12-31"
-       }
-       deadline, err := time.ParseInLocation("2006-01-02", form.Deadline, time.Local)
-       if err != nil {
-               ctx.Data["Err_Deadline"] = true
-               ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form)
-               return
-       }
-
-       deadline = time.Date(deadline.Year(), deadline.Month(), deadline.Day(), 23, 59, 59, 0, deadline.Location())
-       m, err := models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
-       if err != nil {
-               if models.IsErrMilestoneNotExist(err) {
-                       ctx.NotFound("", nil)
-               } else {
-                       ctx.ServerError("GetMilestoneByRepoID", err)
-               }
-               return
-       }
-       m.Name = form.Title
-       m.Content = form.Content
-       m.DeadlineUnix = timeutil.TimeStamp(deadline.Unix())
-       if err = models.UpdateMilestone(m, m.IsClosed); err != nil {
-               ctx.ServerError("UpdateMilestone", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.milestones.edit_success", m.Name))
-       ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
-}
-
-// ChangeMilestoneStatus response for change a milestone's status
-func ChangeMilestoneStatus(ctx *context.Context) {
-       toClose := false
-       switch ctx.Params(":action") {
-       case "open":
-               toClose = false
-       case "close":
-               toClose = true
-       default:
-               ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
-       }
-       id := ctx.ParamsInt64(":id")
-
-       if err := models.ChangeMilestoneStatusByRepoIDAndID(ctx.Repo.Repository.ID, id, toClose); err != nil {
-               if models.IsErrMilestoneNotExist(err) {
-                       ctx.NotFound("", err)
-               } else {
-                       ctx.ServerError("ChangeMilestoneStatusByIDAndRepoID", err)
-               }
-               return
-       }
-       ctx.Redirect(ctx.Repo.RepoLink + "/milestones?state=" + ctx.Params(":action"))
-}
-
-// DeleteMilestone delete a milestone
-func DeleteMilestone(ctx *context.Context) {
-       if err := models.DeleteMilestoneByRepoID(ctx.Repo.Repository.ID, ctx.QueryInt64("id")); err != nil {
-               ctx.Flash.Error("DeleteMilestoneByRepoID: " + err.Error())
-       } else {
-               ctx.Flash.Success(ctx.Tr("repo.milestones.deletion_success"))
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": ctx.Repo.RepoLink + "/milestones",
-       })
-}
-
-// MilestoneIssuesAndPulls lists all the issues and pull requests of the milestone
-func MilestoneIssuesAndPulls(ctx *context.Context) {
-       milestoneID := ctx.ParamsInt64(":id")
-       milestone, err := models.GetMilestoneByID(milestoneID)
-       if err != nil {
-               if models.IsErrMilestoneNotExist(err) {
-                       ctx.NotFound("GetMilestoneByID", err)
-                       return
-               }
-
-               ctx.ServerError("GetMilestoneByID", err)
-               return
-       }
-
-       milestone.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
-               URLPrefix: ctx.Repo.RepoLink,
-               Metas:     ctx.Repo.Repository.ComposeMetas(),
-       }, milestone.Content)
-       if err != nil {
-               ctx.ServerError("RenderString", err)
-               return
-       }
-
-       ctx.Data["Title"] = milestone.Name
-       ctx.Data["Milestone"] = milestone
-
-       issues(ctx, milestoneID, 0, util.OptionalBoolNone)
-       ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
-
-       ctx.Data["CanWriteIssues"] = ctx.Repo.CanWriteIssuesOrPulls(false)
-       ctx.Data["CanWritePulls"] = ctx.Repo.CanWriteIssuesOrPulls(true)
-
-       ctx.HTML(http.StatusOK, tplMilestoneIssues)
-}
diff --git a/routers/repo/projects.go b/routers/repo/projects.go
deleted file mode 100644 (file)
index eb07199..0000000
+++ /dev/null
@@ -1,665 +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 repo
-
-import (
-       "fmt"
-       "net/http"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/markup"
-       "code.gitea.io/gitea/modules/markup/markdown"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/util"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-)
-
-const (
-       tplProjects           base.TplName = "repo/projects/list"
-       tplProjectsNew        base.TplName = "repo/projects/new"
-       tplProjectsView       base.TplName = "repo/projects/view"
-       tplGenericProjectsNew base.TplName = "user/project"
-)
-
-// MustEnableProjects check if projects are enabled in settings
-func MustEnableProjects(ctx *context.Context) {
-       if models.UnitTypeProjects.UnitGlobalDisabled() {
-               ctx.NotFound("EnableKanbanBoard", nil)
-               return
-       }
-
-       if ctx.Repo.Repository != nil {
-               if !ctx.Repo.CanRead(models.UnitTypeProjects) {
-                       ctx.NotFound("MustEnableProjects", nil)
-                       return
-               }
-       }
-}
-
-// Projects renders the home page of projects
-func Projects(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.project_board")
-
-       sortType := ctx.QueryTrim("sort")
-
-       isShowClosed := strings.ToLower(ctx.QueryTrim("state")) == "closed"
-       repo := ctx.Repo.Repository
-       page := ctx.QueryInt("page")
-       if page <= 1 {
-               page = 1
-       }
-
-       ctx.Data["OpenCount"] = repo.NumOpenProjects
-       ctx.Data["ClosedCount"] = repo.NumClosedProjects
-
-       var total int
-       if !isShowClosed {
-               total = repo.NumOpenProjects
-       } else {
-               total = repo.NumClosedProjects
-       }
-
-       projects, count, err := models.GetProjects(models.ProjectSearchOptions{
-               RepoID:   repo.ID,
-               Page:     page,
-               IsClosed: util.OptionalBoolOf(isShowClosed),
-               SortType: sortType,
-               Type:     models.ProjectTypeRepository,
-       })
-       if err != nil {
-               ctx.ServerError("GetProjects", err)
-               return
-       }
-
-       for i := range projects {
-               projects[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{
-                       URLPrefix: ctx.Repo.RepoLink,
-                       Metas:     ctx.Repo.Repository.ComposeMetas(),
-               }, projects[i].Description)
-               if err != nil {
-                       ctx.ServerError("RenderString", err)
-                       return
-               }
-       }
-
-       ctx.Data["Projects"] = projects
-
-       if isShowClosed {
-               ctx.Data["State"] = "closed"
-       } else {
-               ctx.Data["State"] = "open"
-       }
-
-       numPages := 0
-       if count > 0 {
-               numPages = int((int(count) - 1) / setting.UI.IssuePagingNum)
-       }
-
-       pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, numPages)
-       pager.AddParam(ctx, "state", "State")
-       ctx.Data["Page"] = pager
-
-       ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
-       ctx.Data["IsShowClosed"] = isShowClosed
-       ctx.Data["IsProjectsPage"] = true
-       ctx.Data["SortType"] = sortType
-
-       ctx.HTML(http.StatusOK, tplProjects)
-}
-
-// NewProject render creating a project page
-func NewProject(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.projects.new")
-       ctx.Data["ProjectTypes"] = models.GetProjectsConfig()
-       ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
-       ctx.HTML(http.StatusOK, tplProjectsNew)
-}
-
-// NewProjectPost creates a new project
-func NewProjectPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.CreateProjectForm)
-       ctx.Data["Title"] = ctx.Tr("repo.projects.new")
-
-       if ctx.HasError() {
-               ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
-               ctx.Data["ProjectTypes"] = models.GetProjectsConfig()
-               ctx.HTML(http.StatusOK, tplProjectsNew)
-               return
-       }
-
-       if err := models.NewProject(&models.Project{
-               RepoID:      ctx.Repo.Repository.ID,
-               Title:       form.Title,
-               Description: form.Content,
-               CreatorID:   ctx.User.ID,
-               BoardType:   form.BoardType,
-               Type:        models.ProjectTypeRepository,
-       }); err != nil {
-               ctx.ServerError("NewProject", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title))
-       ctx.Redirect(ctx.Repo.RepoLink + "/projects")
-}
-
-// ChangeProjectStatus updates the status of a project between "open" and "close"
-func ChangeProjectStatus(ctx *context.Context) {
-       toClose := false
-       switch ctx.Params(":action") {
-       case "open":
-               toClose = false
-       case "close":
-               toClose = true
-       default:
-               ctx.Redirect(ctx.Repo.RepoLink + "/projects")
-       }
-       id := ctx.ParamsInt64(":id")
-
-       if err := models.ChangeProjectStatusByRepoIDAndID(ctx.Repo.Repository.ID, id, toClose); err != nil {
-               if models.IsErrProjectNotExist(err) {
-                       ctx.NotFound("", err)
-               } else {
-                       ctx.ServerError("ChangeProjectStatusByIDAndRepoID", err)
-               }
-               return
-       }
-       ctx.Redirect(ctx.Repo.RepoLink + "/projects?state=" + ctx.Params(":action"))
-}
-
-// DeleteProject delete a project
-func DeleteProject(ctx *context.Context) {
-       p, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
-       if err != nil {
-               if models.IsErrProjectNotExist(err) {
-                       ctx.NotFound("", nil)
-               } else {
-                       ctx.ServerError("GetProjectByID", err)
-               }
-               return
-       }
-       if p.RepoID != ctx.Repo.Repository.ID {
-               ctx.NotFound("", nil)
-               return
-       }
-
-       if err := models.DeleteProjectByID(p.ID); err != nil {
-               ctx.Flash.Error("DeleteProjectByID: " + err.Error())
-       } else {
-               ctx.Flash.Success(ctx.Tr("repo.projects.deletion_success"))
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": ctx.Repo.RepoLink + "/projects",
-       })
-}
-
-// EditProject allows a project to be edited
-func EditProject(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
-       ctx.Data["PageIsProjects"] = true
-       ctx.Data["PageIsEditProjects"] = true
-       ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
-
-       p, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
-       if err != nil {
-               if models.IsErrProjectNotExist(err) {
-                       ctx.NotFound("", nil)
-               } else {
-                       ctx.ServerError("GetProjectByID", err)
-               }
-               return
-       }
-       if p.RepoID != ctx.Repo.Repository.ID {
-               ctx.NotFound("", nil)
-               return
-       }
-
-       ctx.Data["title"] = p.Title
-       ctx.Data["content"] = p.Description
-
-       ctx.HTML(http.StatusOK, tplProjectsNew)
-}
-
-// EditProjectPost response for editing a project
-func EditProjectPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.CreateProjectForm)
-       ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
-       ctx.Data["PageIsProjects"] = true
-       ctx.Data["PageIsEditProjects"] = true
-       ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplProjectsNew)
-               return
-       }
-
-       p, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
-       if err != nil {
-               if models.IsErrProjectNotExist(err) {
-                       ctx.NotFound("", nil)
-               } else {
-                       ctx.ServerError("GetProjectByID", err)
-               }
-               return
-       }
-       if p.RepoID != ctx.Repo.Repository.ID {
-               ctx.NotFound("", nil)
-               return
-       }
-
-       p.Title = form.Title
-       p.Description = form.Content
-       if err = models.UpdateProject(p); err != nil {
-               ctx.ServerError("UpdateProjects", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.projects.edit_success", p.Title))
-       ctx.Redirect(ctx.Repo.RepoLink + "/projects")
-}
-
-// ViewProject renders the project board for a project
-func ViewProject(ctx *context.Context) {
-
-       project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
-       if err != nil {
-               if models.IsErrProjectNotExist(err) {
-                       ctx.NotFound("", nil)
-               } else {
-                       ctx.ServerError("GetProjectByID", err)
-               }
-               return
-       }
-       if project.RepoID != ctx.Repo.Repository.ID {
-               ctx.NotFound("", nil)
-               return
-       }
-
-       boards, err := models.GetProjectBoards(project.ID)
-       if err != nil {
-               ctx.ServerError("GetProjectBoards", err)
-               return
-       }
-
-       if boards[0].ID == 0 {
-               boards[0].Title = ctx.Tr("repo.projects.type.uncategorized")
-       }
-
-       issueList, err := boards.LoadIssues()
-       if err != nil {
-               ctx.ServerError("LoadIssuesOfBoards", err)
-               return
-       }
-       ctx.Data["Issues"] = issueList
-
-       linkedPrsMap := make(map[int64][]*models.Issue)
-       for _, issue := range issueList {
-               var referencedIds []int64
-               for _, comment := range issue.Comments {
-                       if comment.RefIssueID != 0 && comment.RefIsPull {
-                               referencedIds = append(referencedIds, comment.RefIssueID)
-                       }
-               }
-
-               if len(referencedIds) > 0 {
-                       if linkedPrs, err := models.Issues(&models.IssuesOptions{
-                               IssueIDs: referencedIds,
-                               IsPull:   util.OptionalBoolTrue,
-                       }); err == nil {
-                               linkedPrsMap[issue.ID] = linkedPrs
-                       }
-               }
-       }
-       ctx.Data["LinkedPRs"] = linkedPrsMap
-
-       project.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
-               URLPrefix: ctx.Repo.RepoLink,
-               Metas:     ctx.Repo.Repository.ComposeMetas(),
-       }, project.Description)
-       if err != nil {
-               ctx.ServerError("RenderString", err)
-               return
-       }
-
-       ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
-       ctx.Data["Project"] = project
-       ctx.Data["Boards"] = boards
-       ctx.Data["PageIsProjects"] = true
-       ctx.Data["RequiresDraggable"] = true
-
-       ctx.HTML(http.StatusOK, tplProjectsView)
-}
-
-// UpdateIssueProject change an issue's project
-func UpdateIssueProject(ctx *context.Context) {
-       issues := getActionIssues(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       projectID := ctx.QueryInt64("id")
-       for _, issue := range issues {
-               oldProjectID := issue.ProjectID()
-               if oldProjectID == projectID {
-                       continue
-               }
-
-               if err := models.ChangeProjectAssign(issue, ctx.User, projectID); err != nil {
-                       ctx.ServerError("ChangeProjectAssign", err)
-                       return
-               }
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "ok": true,
-       })
-}
-
-// DeleteProjectBoard allows for the deletion of a project board
-func DeleteProjectBoard(ctx *context.Context) {
-       if ctx.User == nil {
-               ctx.JSON(http.StatusForbidden, map[string]string{
-                       "message": "Only signed in users are allowed to perform this action.",
-               })
-               return
-       }
-
-       if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) {
-               ctx.JSON(http.StatusForbidden, map[string]string{
-                       "message": "Only authorized users are allowed to perform this action.",
-               })
-               return
-       }
-
-       project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
-       if err != nil {
-               if models.IsErrProjectNotExist(err) {
-                       ctx.NotFound("", nil)
-               } else {
-                       ctx.ServerError("GetProjectByID", err)
-               }
-               return
-       }
-
-       pb, err := models.GetProjectBoard(ctx.ParamsInt64(":boardID"))
-       if err != nil {
-               ctx.ServerError("GetProjectBoard", err)
-               return
-       }
-       if pb.ProjectID != ctx.ParamsInt64(":id") {
-               ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
-                       "message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", pb.ID, project.ID),
-               })
-               return
-       }
-
-       if project.RepoID != ctx.Repo.Repository.ID {
-               ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
-                       "message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", pb.ID, ctx.Repo.Repository.ID),
-               })
-               return
-       }
-
-       if err := models.DeleteProjectBoardByID(ctx.ParamsInt64(":boardID")); err != nil {
-               ctx.ServerError("DeleteProjectBoardByID", err)
-               return
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "ok": true,
-       })
-}
-
-// AddBoardToProjectPost allows a new board to be added to a project.
-func AddBoardToProjectPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
-       if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) {
-               ctx.JSON(http.StatusForbidden, map[string]string{
-                       "message": "Only authorized users are allowed to perform this action.",
-               })
-               return
-       }
-
-       project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
-       if err != nil {
-               if models.IsErrProjectNotExist(err) {
-                       ctx.NotFound("", nil)
-               } else {
-                       ctx.ServerError("GetProjectByID", err)
-               }
-               return
-       }
-
-       if err := models.NewProjectBoard(&models.ProjectBoard{
-               ProjectID: project.ID,
-               Title:     form.Title,
-               CreatorID: ctx.User.ID,
-       }); err != nil {
-               ctx.ServerError("NewProjectBoard", err)
-               return
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "ok": true,
-       })
-}
-
-func checkProjectBoardChangePermissions(ctx *context.Context) (*models.Project, *models.ProjectBoard) {
-       if ctx.User == nil {
-               ctx.JSON(http.StatusForbidden, map[string]string{
-                       "message": "Only signed in users are allowed to perform this action.",
-               })
-               return nil, nil
-       }
-
-       if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) {
-               ctx.JSON(http.StatusForbidden, map[string]string{
-                       "message": "Only authorized users are allowed to perform this action.",
-               })
-               return nil, nil
-       }
-
-       project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
-       if err != nil {
-               if models.IsErrProjectNotExist(err) {
-                       ctx.NotFound("", nil)
-               } else {
-                       ctx.ServerError("GetProjectByID", err)
-               }
-               return nil, nil
-       }
-
-       board, err := models.GetProjectBoard(ctx.ParamsInt64(":boardID"))
-       if err != nil {
-               ctx.ServerError("GetProjectBoard", err)
-               return nil, nil
-       }
-       if board.ProjectID != ctx.ParamsInt64(":id") {
-               ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
-                       "message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", board.ID, project.ID),
-               })
-               return nil, nil
-       }
-
-       if project.RepoID != ctx.Repo.Repository.ID {
-               ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
-                       "message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", board.ID, ctx.Repo.Repository.ID),
-               })
-               return nil, nil
-       }
-       return project, board
-}
-
-// EditProjectBoard allows a project board's to be updated
-func EditProjectBoard(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
-       _, board := checkProjectBoardChangePermissions(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       if form.Title != "" {
-               board.Title = form.Title
-       }
-
-       if form.Sorting != 0 {
-               board.Sorting = form.Sorting
-       }
-
-       if err := models.UpdateProjectBoard(board); err != nil {
-               ctx.ServerError("UpdateProjectBoard", err)
-               return
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "ok": true,
-       })
-}
-
-// SetDefaultProjectBoard set default board for uncategorized issues/pulls
-func SetDefaultProjectBoard(ctx *context.Context) {
-
-       project, board := checkProjectBoardChangePermissions(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       if err := models.SetDefaultBoard(project.ID, board.ID); err != nil {
-               ctx.ServerError("SetDefaultBoard", err)
-               return
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "ok": true,
-       })
-}
-
-// MoveIssueAcrossBoards move a card from one board to another in a project
-func MoveIssueAcrossBoards(ctx *context.Context) {
-
-       if ctx.User == nil {
-               ctx.JSON(http.StatusForbidden, map[string]string{
-                       "message": "Only signed in users are allowed to perform this action.",
-               })
-               return
-       }
-
-       if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) {
-               ctx.JSON(http.StatusForbidden, map[string]string{
-                       "message": "Only authorized users are allowed to perform this action.",
-               })
-               return
-       }
-
-       p, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
-       if err != nil {
-               if models.IsErrProjectNotExist(err) {
-                       ctx.NotFound("", nil)
-               } else {
-                       ctx.ServerError("GetProjectByID", err)
-               }
-               return
-       }
-       if p.RepoID != ctx.Repo.Repository.ID {
-               ctx.NotFound("", nil)
-               return
-       }
-
-       var board *models.ProjectBoard
-
-       if ctx.ParamsInt64(":boardID") == 0 {
-
-               board = &models.ProjectBoard{
-                       ID:        0,
-                       ProjectID: 0,
-                       Title:     ctx.Tr("repo.projects.type.uncategorized"),
-               }
-
-       } else {
-               board, err = models.GetProjectBoard(ctx.ParamsInt64(":boardID"))
-               if err != nil {
-                       if models.IsErrProjectBoardNotExist(err) {
-                               ctx.NotFound("", nil)
-                       } else {
-                               ctx.ServerError("GetProjectBoard", err)
-                       }
-                       return
-               }
-               if board.ProjectID != p.ID {
-                       ctx.NotFound("", nil)
-                       return
-               }
-       }
-
-       issue, err := models.GetIssueByID(ctx.ParamsInt64(":index"))
-       if err != nil {
-               if models.IsErrIssueNotExist(err) {
-                       ctx.NotFound("", nil)
-               } else {
-                       ctx.ServerError("GetIssueByID", err)
-               }
-
-               return
-       }
-
-       if err := models.MoveIssueAcrossProjectBoards(issue, board); err != nil {
-               ctx.ServerError("MoveIssueAcrossProjectBoards", err)
-               return
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "ok": true,
-       })
-}
-
-// CreateProject renders the generic project creation page
-func CreateProject(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.projects.new")
-       ctx.Data["ProjectTypes"] = models.GetProjectsConfig()
-       ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
-
-       ctx.HTML(http.StatusOK, tplGenericProjectsNew)
-}
-
-// CreateProjectPost creates an individual and/or organization project
-func CreateProjectPost(ctx *context.Context, form forms.UserCreateProjectForm) {
-
-       user := checkContextUser(ctx, form.UID)
-       if ctx.Written() {
-               return
-       }
-
-       ctx.Data["ContextUser"] = user
-
-       if ctx.HasError() {
-               ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
-               ctx.HTML(http.StatusOK, tplGenericProjectsNew)
-               return
-       }
-
-       var projectType = models.ProjectTypeIndividual
-       if user.IsOrganization() {
-               projectType = models.ProjectTypeOrganization
-       }
-
-       if err := models.NewProject(&models.Project{
-               Title:       form.Title,
-               Description: form.Content,
-               CreatorID:   user.ID,
-               BoardType:   form.BoardType,
-               Type:        projectType,
-       }); err != nil {
-               ctx.ServerError("NewProject", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title))
-       ctx.Redirect(setting.AppSubURL + "/")
-}
diff --git a/routers/repo/projects_test.go b/routers/repo/projects_test.go
deleted file mode 100644 (file)
index c43cf6d..0000000
+++ /dev/null
@@ -1,28 +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 repo
-
-import (
-       "testing"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/test"
-
-       "github.com/stretchr/testify/assert"
-)
-
-func TestCheckProjectBoardChangePermissions(t *testing.T) {
-       models.PrepareTestEnv(t)
-       ctx := test.MockContext(t, "user2/repo1/projects/1/2")
-       test.LoadUser(t, ctx, 2)
-       test.LoadRepo(t, ctx, 1)
-       ctx.SetParams(":id", "1")
-       ctx.SetParams(":boardID", "2")
-
-       project, board := checkProjectBoardChangePermissions(ctx)
-       assert.NotNil(t, project)
-       assert.NotNil(t, board)
-       assert.False(t, ctx.Written())
-}
diff --git a/routers/repo/pull.go b/routers/repo/pull.go
deleted file mode 100644 (file)
index 28f94c8..0000000
+++ /dev/null
@@ -1,1341 +0,0 @@
-// Copyright 2018 The Gitea Authors.
-// Copyright 2014 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 repo
-
-import (
-       "container/list"
-       "crypto/subtle"
-       "errors"
-       "fmt"
-       "net/http"
-       "path"
-       "strings"
-       "time"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/git"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/notification"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/structs"
-       "code.gitea.io/gitea/modules/upload"
-       "code.gitea.io/gitea/modules/util"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/modules/web/middleware"
-       "code.gitea.io/gitea/routers/utils"
-       "code.gitea.io/gitea/services/forms"
-       "code.gitea.io/gitea/services/gitdiff"
-       pull_service "code.gitea.io/gitea/services/pull"
-       repo_service "code.gitea.io/gitea/services/repository"
-       "github.com/unknwon/com"
-)
-
-const (
-       tplFork        base.TplName = "repo/pulls/fork"
-       tplCompareDiff base.TplName = "repo/diff/compare"
-       tplPullCommits base.TplName = "repo/pulls/commits"
-       tplPullFiles   base.TplName = "repo/pulls/files"
-
-       pullRequestTemplateKey = "PullRequestTemplate"
-)
-
-var (
-       pullRequestTemplateCandidates = []string{
-               "PULL_REQUEST_TEMPLATE.md",
-               "pull_request_template.md",
-               ".gitea/PULL_REQUEST_TEMPLATE.md",
-               ".gitea/pull_request_template.md",
-               ".github/PULL_REQUEST_TEMPLATE.md",
-               ".github/pull_request_template.md",
-       }
-)
-
-func getRepository(ctx *context.Context, repoID int64) *models.Repository {
-       repo, err := models.GetRepositoryByID(repoID)
-       if err != nil {
-               if models.IsErrRepoNotExist(err) {
-                       ctx.NotFound("GetRepositoryByID", nil)
-               } else {
-                       ctx.ServerError("GetRepositoryByID", err)
-               }
-               return nil
-       }
-
-       perm, err := models.GetUserRepoPermission(repo, ctx.User)
-       if err != nil {
-               ctx.ServerError("GetUserRepoPermission", err)
-               return nil
-       }
-
-       if !perm.CanRead(models.UnitTypeCode) {
-               log.Trace("Permission Denied: User %-v cannot read %-v of repo %-v\n"+
-                       "User in repo has Permissions: %-+v",
-                       ctx.User,
-                       models.UnitTypeCode,
-                       ctx.Repo,
-                       perm)
-               ctx.NotFound("getRepository", nil)
-               return nil
-       }
-       return repo
-}
-
-func getForkRepository(ctx *context.Context) *models.Repository {
-       forkRepo := getRepository(ctx, ctx.ParamsInt64(":repoid"))
-       if ctx.Written() {
-               return nil
-       }
-
-       if forkRepo.IsEmpty {
-               log.Trace("Empty repository %-v", forkRepo)
-               ctx.NotFound("getForkRepository", nil)
-               return nil
-       }
-
-       if err := forkRepo.GetOwner(); err != nil {
-               ctx.ServerError("GetOwner", err)
-               return nil
-       }
-
-       ctx.Data["repo_name"] = forkRepo.Name
-       ctx.Data["description"] = forkRepo.Description
-       ctx.Data["IsPrivate"] = forkRepo.IsPrivate || forkRepo.Owner.Visibility == structs.VisibleTypePrivate
-       canForkToUser := forkRepo.OwnerID != ctx.User.ID && !ctx.User.HasForkedRepo(forkRepo.ID)
-
-       ctx.Data["ForkFrom"] = forkRepo.Owner.Name + "/" + forkRepo.Name
-       ctx.Data["ForkFromOwnerID"] = forkRepo.Owner.ID
-
-       if err := ctx.User.GetOwnedOrganizations(); err != nil {
-               ctx.ServerError("GetOwnedOrganizations", err)
-               return nil
-       }
-       var orgs []*models.User
-       for _, org := range ctx.User.OwnedOrgs {
-               if forkRepo.OwnerID != org.ID && !org.HasForkedRepo(forkRepo.ID) {
-                       orgs = append(orgs, org)
-               }
-       }
-
-       var traverseParentRepo = forkRepo
-       var err error
-       for {
-               if ctx.User.ID == traverseParentRepo.OwnerID {
-                       canForkToUser = false
-               } else {
-                       for i, org := range orgs {
-                               if org.ID == traverseParentRepo.OwnerID {
-                                       orgs = append(orgs[:i], orgs[i+1:]...)
-                                       break
-                               }
-                       }
-               }
-
-               if !traverseParentRepo.IsFork {
-                       break
-               }
-               traverseParentRepo, err = models.GetRepositoryByID(traverseParentRepo.ForkID)
-               if err != nil {
-                       ctx.ServerError("GetRepositoryByID", err)
-                       return nil
-               }
-       }
-
-       ctx.Data["CanForkToUser"] = canForkToUser
-       ctx.Data["Orgs"] = orgs
-
-       if canForkToUser {
-               ctx.Data["ContextUser"] = ctx.User
-       } else if len(orgs) > 0 {
-               ctx.Data["ContextUser"] = orgs[0]
-       }
-
-       return forkRepo
-}
-
-// Fork render repository fork page
-func Fork(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("new_fork")
-
-       getForkRepository(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       ctx.HTML(http.StatusOK, tplFork)
-}
-
-// ForkPost response for forking a repository
-func ForkPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.CreateRepoForm)
-       ctx.Data["Title"] = ctx.Tr("new_fork")
-
-       ctxUser := checkContextUser(ctx, form.UID)
-       if ctx.Written() {
-               return
-       }
-
-       forkRepo := getForkRepository(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       ctx.Data["ContextUser"] = ctxUser
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplFork)
-               return
-       }
-
-       var err error
-       var traverseParentRepo = forkRepo
-       for {
-               if ctxUser.ID == traverseParentRepo.OwnerID {
-                       ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
-                       return
-               }
-               repo, has := models.HasForkedRepo(ctxUser.ID, traverseParentRepo.ID)
-               if has {
-                       ctx.Redirect(ctxUser.HomeLink() + "/" + repo.Name)
-                       return
-               }
-               if !traverseParentRepo.IsFork {
-                       break
-               }
-               traverseParentRepo, err = models.GetRepositoryByID(traverseParentRepo.ForkID)
-               if err != nil {
-                       ctx.ServerError("GetRepositoryByID", err)
-                       return
-               }
-       }
-
-       // Check ownership of organization.
-       if ctxUser.IsOrganization() {
-               isOwner, err := ctxUser.IsOwnedBy(ctx.User.ID)
-               if err != nil {
-                       ctx.ServerError("IsOwnedBy", err)
-                       return
-               } else if !isOwner {
-                       ctx.Error(http.StatusForbidden)
-                       return
-               }
-       }
-
-       repo, err := repo_service.ForkRepository(ctx.User, ctxUser, forkRepo, form.RepoName, form.Description)
-       if err != nil {
-               ctx.Data["Err_RepoName"] = true
-               switch {
-               case models.IsErrRepoAlreadyExist(err):
-                       ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
-               case models.IsErrNameReserved(err):
-                       ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tplFork, &form)
-               case models.IsErrNamePatternNotAllowed(err):
-                       ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplFork, &form)
-               default:
-                       ctx.ServerError("ForkPost", err)
-               }
-               return
-       }
-
-       log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
-       ctx.Redirect(ctxUser.HomeLink() + "/" + repo.Name)
-}
-
-func checkPullInfo(ctx *context.Context) *models.Issue {
-       issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
-       if err != nil {
-               if models.IsErrIssueNotExist(err) {
-                       ctx.NotFound("GetIssueByIndex", err)
-               } else {
-                       ctx.ServerError("GetIssueByIndex", err)
-               }
-               return nil
-       }
-       if err = issue.LoadPoster(); err != nil {
-               ctx.ServerError("LoadPoster", err)
-               return nil
-       }
-       if err := issue.LoadRepo(); err != nil {
-               ctx.ServerError("LoadRepo", err)
-               return nil
-       }
-       ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
-       ctx.Data["Issue"] = issue
-
-       if !issue.IsPull {
-               ctx.NotFound("ViewPullCommits", nil)
-               return nil
-       }
-
-       if err = issue.LoadPullRequest(); err != nil {
-               ctx.ServerError("LoadPullRequest", err)
-               return nil
-       }
-
-       if err = issue.PullRequest.LoadHeadRepo(); err != nil {
-               ctx.ServerError("LoadHeadRepo", err)
-               return nil
-       }
-
-       if ctx.IsSigned {
-               // Update issue-user.
-               if err = issue.ReadBy(ctx.User.ID); err != nil {
-                       ctx.ServerError("ReadBy", err)
-                       return nil
-               }
-       }
-
-       return issue
-}
-
-func setMergeTarget(ctx *context.Context, pull *models.PullRequest) {
-       if ctx.Repo.Owner.Name == pull.MustHeadUserName() {
-               ctx.Data["HeadTarget"] = pull.HeadBranch
-       } else if pull.HeadRepo == nil {
-               ctx.Data["HeadTarget"] = pull.MustHeadUserName() + ":" + pull.HeadBranch
-       } else {
-               ctx.Data["HeadTarget"] = pull.MustHeadUserName() + "/" + pull.HeadRepo.Name + ":" + pull.HeadBranch
-       }
-       ctx.Data["BaseTarget"] = pull.BaseBranch
-       ctx.Data["HeadBranchHTMLURL"] = pull.GetHeadBranchHTMLURL()
-       ctx.Data["BaseBranchHTMLURL"] = pull.GetBaseBranchHTMLURL()
-}
-
-// PrepareMergedViewPullInfo show meta information for a merged pull request view page
-func PrepareMergedViewPullInfo(ctx *context.Context, issue *models.Issue) *git.CompareInfo {
-       pull := issue.PullRequest
-
-       setMergeTarget(ctx, pull)
-       ctx.Data["HasMerged"] = true
-
-       compareInfo, err := ctx.Repo.GitRepo.GetCompareInfo(ctx.Repo.Repository.RepoPath(),
-               pull.MergeBase, pull.GetGitRefName())
-       if err != nil {
-               if strings.Contains(err.Error(), "fatal: Not a valid object name") || strings.Contains(err.Error(), "unknown revision or path not in the working tree") {
-                       ctx.Data["IsPullRequestBroken"] = true
-                       ctx.Data["BaseTarget"] = pull.BaseBranch
-                       ctx.Data["NumCommits"] = 0
-                       ctx.Data["NumFiles"] = 0
-                       return nil
-               }
-
-               ctx.ServerError("GetCompareInfo", err)
-               return nil
-       }
-       ctx.Data["NumCommits"] = compareInfo.Commits.Len()
-       ctx.Data["NumFiles"] = compareInfo.NumFiles
-
-       if compareInfo.Commits.Len() != 0 {
-               sha := compareInfo.Commits.Front().Value.(*git.Commit).ID.String()
-               commitStatuses, err := models.GetLatestCommitStatus(ctx.Repo.Repository.ID, sha, models.ListOptions{})
-               if err != nil {
-                       ctx.ServerError("GetLatestCommitStatus", err)
-                       return nil
-               }
-               if len(commitStatuses) != 0 {
-                       ctx.Data["LatestCommitStatuses"] = commitStatuses
-                       ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(commitStatuses)
-               }
-       }
-
-       return compareInfo
-}
-
-// PrepareViewPullInfo show meta information for a pull request preview page
-func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.CompareInfo {
-       repo := ctx.Repo.Repository
-       pull := issue.PullRequest
-
-       if err := pull.LoadHeadRepo(); err != nil {
-               ctx.ServerError("LoadHeadRepo", err)
-               return nil
-       }
-
-       if err := pull.LoadBaseRepo(); err != nil {
-               ctx.ServerError("LoadBaseRepo", err)
-               return nil
-       }
-
-       setMergeTarget(ctx, pull)
-
-       if err := pull.LoadProtectedBranch(); err != nil {
-               ctx.ServerError("LoadProtectedBranch", err)
-               return nil
-       }
-       ctx.Data["EnableStatusCheck"] = pull.ProtectedBranch != nil && pull.ProtectedBranch.EnableStatusCheck
-
-       baseGitRepo, err := git.OpenRepository(pull.BaseRepo.RepoPath())
-       if err != nil {
-               ctx.ServerError("OpenRepository", err)
-               return nil
-       }
-       defer baseGitRepo.Close()
-
-       if !baseGitRepo.IsBranchExist(pull.BaseBranch) {
-               ctx.Data["IsPullRequestBroken"] = true
-               ctx.Data["BaseTarget"] = pull.BaseBranch
-               ctx.Data["HeadTarget"] = pull.HeadBranch
-
-               sha, err := baseGitRepo.GetRefCommitID(pull.GetGitRefName())
-               if err != nil {
-                       ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err)
-                       return nil
-               }
-               commitStatuses, err := models.GetLatestCommitStatus(repo.ID, sha, models.ListOptions{})
-               if err != nil {
-                       ctx.ServerError("GetLatestCommitStatus", err)
-                       return nil
-               }
-               if len(commitStatuses) > 0 {
-                       ctx.Data["LatestCommitStatuses"] = commitStatuses
-                       ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(commitStatuses)
-               }
-
-               compareInfo, err := baseGitRepo.GetCompareInfo(pull.BaseRepo.RepoPath(),
-                       pull.MergeBase, pull.GetGitRefName())
-               if err != nil {
-                       if strings.Contains(err.Error(), "fatal: Not a valid object name") {
-                               ctx.Data["IsPullRequestBroken"] = true
-                               ctx.Data["BaseTarget"] = pull.BaseBranch
-                               ctx.Data["NumCommits"] = 0
-                               ctx.Data["NumFiles"] = 0
-                               return nil
-                       }
-
-                       ctx.ServerError("GetCompareInfo", err)
-                       return nil
-               }
-
-               ctx.Data["NumCommits"] = compareInfo.Commits.Len()
-               ctx.Data["NumFiles"] = compareInfo.NumFiles
-               return compareInfo
-       }
-
-       var headBranchExist bool
-       var headBranchSha string
-       // HeadRepo may be missing
-       if pull.HeadRepo != nil {
-               headGitRepo, err := git.OpenRepository(pull.HeadRepo.RepoPath())
-               if err != nil {
-                       ctx.ServerError("OpenRepository", err)
-                       return nil
-               }
-               defer headGitRepo.Close()
-
-               headBranchExist = headGitRepo.IsBranchExist(pull.HeadBranch)
-
-               if headBranchExist {
-                       headBranchSha, err = headGitRepo.GetBranchCommitID(pull.HeadBranch)
-                       if err != nil {
-                               ctx.ServerError("GetBranchCommitID", err)
-                               return nil
-                       }
-               }
-       }
-
-       if headBranchExist {
-               ctx.Data["UpdateAllowed"], err = pull_service.IsUserAllowedToUpdate(pull, ctx.User)
-               if err != nil {
-                       ctx.ServerError("IsUserAllowedToUpdate", err)
-                       return nil
-               }
-               ctx.Data["GetCommitMessages"] = pull_service.GetSquashMergeCommitMessages(pull)
-       }
-
-       sha, err := baseGitRepo.GetRefCommitID(pull.GetGitRefName())
-       if err != nil {
-               if git.IsErrNotExist(err) {
-                       ctx.Data["IsPullRequestBroken"] = true
-                       if pull.IsSameRepo() {
-                               ctx.Data["HeadTarget"] = pull.HeadBranch
-                       } else if pull.HeadRepo == nil {
-                               ctx.Data["HeadTarget"] = "<deleted>:" + pull.HeadBranch
-                       } else {
-                               ctx.Data["HeadTarget"] = pull.HeadRepo.OwnerName + ":" + pull.HeadBranch
-                       }
-                       ctx.Data["BaseTarget"] = pull.BaseBranch
-                       ctx.Data["NumCommits"] = 0
-                       ctx.Data["NumFiles"] = 0
-                       return nil
-               }
-               ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err)
-               return nil
-       }
-
-       commitStatuses, err := models.GetLatestCommitStatus(repo.ID, sha, models.ListOptions{})
-       if err != nil {
-               ctx.ServerError("GetLatestCommitStatus", err)
-               return nil
-       }
-       if len(commitStatuses) > 0 {
-               ctx.Data["LatestCommitStatuses"] = commitStatuses
-               ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(commitStatuses)
-       }
-
-       if pull.ProtectedBranch != nil && pull.ProtectedBranch.EnableStatusCheck {
-               ctx.Data["is_context_required"] = func(context string) bool {
-                       for _, c := range pull.ProtectedBranch.StatusCheckContexts {
-                               if c == context {
-                                       return true
-                               }
-                       }
-                       return false
-               }
-               ctx.Data["RequiredStatusCheckState"] = pull_service.MergeRequiredContextsCommitStatus(commitStatuses, pull.ProtectedBranch.StatusCheckContexts)
-       }
-
-       ctx.Data["HeadBranchMovedOn"] = headBranchSha != sha
-       ctx.Data["HeadBranchCommitID"] = headBranchSha
-       ctx.Data["PullHeadCommitID"] = sha
-
-       if pull.HeadRepo == nil || !headBranchExist || headBranchSha != sha {
-               ctx.Data["IsPullRequestBroken"] = true
-               if pull.IsSameRepo() {
-                       ctx.Data["HeadTarget"] = pull.HeadBranch
-               } else if pull.HeadRepo == nil {
-                       ctx.Data["HeadTarget"] = "<deleted>:" + pull.HeadBranch
-               } else {
-                       ctx.Data["HeadTarget"] = pull.HeadRepo.OwnerName + ":" + pull.HeadBranch
-               }
-       }
-
-       compareInfo, err := baseGitRepo.GetCompareInfo(pull.BaseRepo.RepoPath(),
-               git.BranchPrefix+pull.BaseBranch, pull.GetGitRefName())
-       if err != nil {
-               if strings.Contains(err.Error(), "fatal: Not a valid object name") {
-                       ctx.Data["IsPullRequestBroken"] = true
-                       ctx.Data["BaseTarget"] = pull.BaseBranch
-                       ctx.Data["NumCommits"] = 0
-                       ctx.Data["NumFiles"] = 0
-                       return nil
-               }
-
-               ctx.ServerError("GetCompareInfo", err)
-               return nil
-       }
-
-       ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
-
-       if pull.IsWorkInProgress() {
-               ctx.Data["IsPullWorkInProgress"] = true
-               ctx.Data["WorkInProgressPrefix"] = pull.GetWorkInProgressPrefix()
-       }
-
-       if pull.IsFilesConflicted() {
-               ctx.Data["IsPullFilesConflicted"] = true
-               ctx.Data["ConflictedFiles"] = pull.ConflictedFiles
-       }
-
-       ctx.Data["NumCommits"] = compareInfo.Commits.Len()
-       ctx.Data["NumFiles"] = compareInfo.NumFiles
-       return compareInfo
-}
-
-// ViewPullCommits show commits for a pull request
-func ViewPullCommits(ctx *context.Context) {
-       ctx.Data["PageIsPullList"] = true
-       ctx.Data["PageIsPullCommits"] = true
-
-       issue := checkPullInfo(ctx)
-       if ctx.Written() {
-               return
-       }
-       pull := issue.PullRequest
-
-       var commits *list.List
-       var prInfo *git.CompareInfo
-       if pull.HasMerged {
-               prInfo = PrepareMergedViewPullInfo(ctx, issue)
-       } else {
-               prInfo = PrepareViewPullInfo(ctx, issue)
-       }
-
-       if ctx.Written() {
-               return
-       } else if prInfo == nil {
-               ctx.NotFound("ViewPullCommits", nil)
-               return
-       }
-
-       ctx.Data["Username"] = ctx.Repo.Owner.Name
-       ctx.Data["Reponame"] = ctx.Repo.Repository.Name
-       commits = prInfo.Commits
-       commits = models.ValidateCommitsWithEmails(commits)
-       commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
-       commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
-       ctx.Data["Commits"] = commits
-       ctx.Data["CommitCount"] = commits.Len()
-
-       getBranchData(ctx, issue)
-       ctx.HTML(http.StatusOK, tplPullCommits)
-}
-
-// ViewPullFiles render pull request changed files list page
-func ViewPullFiles(ctx *context.Context) {
-       ctx.Data["PageIsPullList"] = true
-       ctx.Data["PageIsPullFiles"] = true
-
-       issue := checkPullInfo(ctx)
-       if ctx.Written() {
-               return
-       }
-       pull := issue.PullRequest
-
-       var (
-               diffRepoPath  string
-               startCommitID string
-               endCommitID   string
-               gitRepo       *git.Repository
-       )
-
-       var prInfo *git.CompareInfo
-       if pull.HasMerged {
-               prInfo = PrepareMergedViewPullInfo(ctx, issue)
-       } else {
-               prInfo = PrepareViewPullInfo(ctx, issue)
-       }
-
-       if ctx.Written() {
-               return
-       } else if prInfo == nil {
-               ctx.NotFound("ViewPullFiles", nil)
-               return
-       }
-
-       diffRepoPath = ctx.Repo.GitRepo.Path
-       gitRepo = ctx.Repo.GitRepo
-
-       headCommitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
-       if err != nil {
-               ctx.ServerError("GetRefCommitID", err)
-               return
-       }
-
-       startCommitID = prInfo.MergeBase
-       endCommitID = headCommitID
-
-       ctx.Data["Username"] = ctx.Repo.Owner.Name
-       ctx.Data["Reponame"] = ctx.Repo.Repository.Name
-       ctx.Data["AfterCommitID"] = endCommitID
-
-       diff, err := gitdiff.GetDiffRangeWithWhitespaceBehavior(diffRepoPath,
-               startCommitID, endCommitID, setting.Git.MaxGitDiffLines,
-               setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles,
-               gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
-       if err != nil {
-               ctx.ServerError("GetDiffRangeWithWhitespaceBehavior", err)
-               return
-       }
-
-       if err = diff.LoadComments(issue, ctx.User); err != nil {
-               ctx.ServerError("LoadComments", err)
-               return
-       }
-
-       if err = pull.LoadProtectedBranch(); err != nil {
-               ctx.ServerError("LoadProtectedBranch", err)
-               return
-       }
-
-       if pull.ProtectedBranch != nil {
-               glob := pull.ProtectedBranch.GetProtectedFilePatterns()
-               if len(glob) != 0 {
-                       for _, file := range diff.Files {
-                               file.IsProtected = pull.ProtectedBranch.IsProtectedFile(glob, file.Name)
-                       }
-               }
-       }
-
-       ctx.Data["Diff"] = diff
-       ctx.Data["DiffNotAvailable"] = diff.NumFiles == 0
-
-       baseCommit, err := ctx.Repo.GitRepo.GetCommit(startCommitID)
-       if err != nil {
-               ctx.ServerError("GetCommit", err)
-               return
-       }
-       commit, err := gitRepo.GetCommit(endCommitID)
-       if err != nil {
-               ctx.ServerError("GetCommit", err)
-               return
-       }
-
-       if ctx.IsSigned && ctx.User != nil {
-               if ctx.Data["CanMarkConversation"], err = models.CanMarkConversation(issue, ctx.User); err != nil {
-                       ctx.ServerError("CanMarkConversation", err)
-                       return
-               }
-       }
-
-       headTarget := path.Join(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
-       setCompareContext(ctx, baseCommit, commit, headTarget)
-
-       ctx.Data["RequireHighlightJS"] = true
-       ctx.Data["RequireSimpleMDE"] = true
-       ctx.Data["RequireTribute"] = true
-       if ctx.Data["Assignees"], err = ctx.Repo.Repository.GetAssignees(); err != nil {
-               ctx.ServerError("GetAssignees", err)
-               return
-       }
-       handleTeamMentions(ctx)
-       if ctx.Written() {
-               return
-       }
-       ctx.Data["CurrentReview"], err = models.GetCurrentReview(ctx.User, issue)
-       if err != nil && !models.IsErrReviewNotExist(err) {
-               ctx.ServerError("GetCurrentReview", err)
-               return
-       }
-       getBranchData(ctx, issue)
-       ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.User.ID)
-       ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
-       ctx.HTML(http.StatusOK, tplPullFiles)
-}
-
-// UpdatePullRequest merge PR's baseBranch into headBranch
-func UpdatePullRequest(ctx *context.Context) {
-       issue := checkPullInfo(ctx)
-       if ctx.Written() {
-               return
-       }
-       if issue.IsClosed {
-               ctx.NotFound("MergePullRequest", nil)
-               return
-       }
-       if issue.PullRequest.HasMerged {
-               ctx.NotFound("MergePullRequest", nil)
-               return
-       }
-
-       if err := issue.PullRequest.LoadBaseRepo(); err != nil {
-               ctx.ServerError("LoadBaseRepo", err)
-               return
-       }
-       if err := issue.PullRequest.LoadHeadRepo(); err != nil {
-               ctx.ServerError("LoadHeadRepo", err)
-               return
-       }
-
-       allowedUpdate, err := pull_service.IsUserAllowedToUpdate(issue.PullRequest, ctx.User)
-       if err != nil {
-               ctx.ServerError("IsUserAllowedToMerge", err)
-               return
-       }
-
-       // ToDo: add check if maintainers are allowed to change branch ... (need migration & co)
-       if !allowedUpdate {
-               ctx.Flash.Error(ctx.Tr("repo.pulls.update_not_allowed"))
-               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
-               return
-       }
-
-       // default merge commit message
-       message := fmt.Sprintf("Merge branch '%s' into %s", issue.PullRequest.BaseBranch, issue.PullRequest.HeadBranch)
-
-       if err = pull_service.Update(issue.PullRequest, ctx.User, message); err != nil {
-               if models.IsErrMergeConflicts(err) {
-                       conflictError := err.(models.ErrMergeConflicts)
-                       flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
-                               "Message": ctx.Tr("repo.pulls.merge_conflict"),
-                               "Summary": ctx.Tr("repo.pulls.merge_conflict_summary"),
-                               "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
-                       })
-                       if err != nil {
-                               ctx.ServerError("UpdatePullRequest.HTMLString", err)
-                               return
-                       }
-                       ctx.Flash.Error(flashError)
-                       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
-                       return
-               }
-               ctx.Flash.Error(err.Error())
-               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
-               return
-       }
-
-       time.Sleep(1 * time.Second)
-
-       ctx.Flash.Success(ctx.Tr("repo.pulls.update_branch_success"))
-       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
-}
-
-// MergePullRequest response for merging pull request
-func MergePullRequest(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.MergePullRequestForm)
-       issue := checkPullInfo(ctx)
-       if ctx.Written() {
-               return
-       }
-       if issue.IsClosed {
-               if issue.IsPull {
-                       ctx.Flash.Error(ctx.Tr("repo.pulls.is_closed"))
-                       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
-                       return
-               }
-               ctx.Flash.Error(ctx.Tr("repo.issues.closed_title"))
-               ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + fmt.Sprint(issue.Index))
-               return
-       }
-
-       pr := issue.PullRequest
-
-       allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, ctx.Repo.Permission, ctx.User)
-       if err != nil {
-               ctx.ServerError("IsUserAllowedToMerge", err)
-               return
-       }
-       if !allowedMerge {
-               ctx.Flash.Error(ctx.Tr("repo.pulls.update_not_allowed"))
-               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
-               return
-       }
-
-       if pr.HasMerged {
-               ctx.Flash.Error(ctx.Tr("repo.pulls.has_merged"))
-               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
-               return
-       }
-
-       // handle manually-merged mark
-       if models.MergeStyle(form.Do) == models.MergeStyleManuallyMerged {
-               if err = pull_service.MergedManually(pr, ctx.User, ctx.Repo.GitRepo, form.MergeCommitID); err != nil {
-                       if models.IsErrInvalidMergeStyle(err) {
-                               ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option"))
-                               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
-                               return
-                       } else if strings.Contains(err.Error(), "Wrong commit ID") {
-                               ctx.Flash.Error(ctx.Tr("repo.pulls.wrong_commit_id"))
-                               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
-                               return
-                       }
-
-                       ctx.ServerError("MergedManually", err)
-                       return
-               }
-
-               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
-               return
-       }
-
-       if !pr.CanAutoMerge() {
-               ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_not_ready"))
-               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
-               return
-       }
-
-       if pr.IsWorkInProgress() {
-               ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_wip"))
-               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
-               return
-       }
-
-       if err := pull_service.CheckPRReadyToMerge(pr, false); err != nil {
-               if !models.IsErrNotAllowedToMerge(err) {
-                       ctx.ServerError("Merge PR status", err)
-                       return
-               }
-               if isRepoAdmin, err := models.IsUserRepoAdmin(pr.BaseRepo, ctx.User); err != nil {
-                       ctx.ServerError("IsUserRepoAdmin", err)
-                       return
-               } else if !isRepoAdmin {
-                       ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_not_ready"))
-                       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
-                       return
-               }
-       }
-
-       if ctx.HasError() {
-               ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
-               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
-               return
-       }
-
-       message := strings.TrimSpace(form.MergeTitleField)
-       if len(message) == 0 {
-               if models.MergeStyle(form.Do) == models.MergeStyleMerge {
-                       message = pr.GetDefaultMergeMessage()
-               }
-               if models.MergeStyle(form.Do) == models.MergeStyleRebaseMerge {
-                       message = pr.GetDefaultMergeMessage()
-               }
-               if models.MergeStyle(form.Do) == models.MergeStyleSquash {
-                       message = pr.GetDefaultSquashMessage()
-               }
-       }
-
-       form.MergeMessageField = strings.TrimSpace(form.MergeMessageField)
-       if len(form.MergeMessageField) > 0 {
-               message += "\n\n" + form.MergeMessageField
-       }
-
-       pr.Issue = issue
-       pr.Issue.Repo = ctx.Repo.Repository
-
-       noDeps, err := models.IssueNoDependenciesLeft(issue)
-       if err != nil {
-               return
-       }
-
-       if !noDeps {
-               ctx.Flash.Error(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
-               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
-               return
-       }
-
-       if err = pull_service.Merge(pr, ctx.User, ctx.Repo.GitRepo, models.MergeStyle(form.Do), message); err != nil {
-               if models.IsErrInvalidMergeStyle(err) {
-                       ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option"))
-                       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
-                       return
-               } else if models.IsErrMergeConflicts(err) {
-                       conflictError := err.(models.ErrMergeConflicts)
-                       flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
-                               "Message": ctx.Tr("repo.editor.merge_conflict"),
-                               "Summary": ctx.Tr("repo.editor.merge_conflict_summary"),
-                               "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
-                       })
-                       if err != nil {
-                               ctx.ServerError("MergePullRequest.HTMLString", err)
-                               return
-                       }
-                       ctx.Flash.Error(flashError)
-                       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
-                       return
-               } else if models.IsErrRebaseConflicts(err) {
-                       conflictError := err.(models.ErrRebaseConflicts)
-                       flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
-                               "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)),
-                               "Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"),
-                               "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
-                       })
-                       if err != nil {
-                               ctx.ServerError("MergePullRequest.HTMLString", err)
-                               return
-                       }
-                       ctx.Flash.Error(flashError)
-                       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
-                       return
-               } else if models.IsErrMergeUnrelatedHistories(err) {
-                       log.Debug("MergeUnrelatedHistories error: %v", err)
-                       ctx.Flash.Error(ctx.Tr("repo.pulls.unrelated_histories"))
-                       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
-                       return
-               } else if git.IsErrPushOutOfDate(err) {
-                       log.Debug("MergePushOutOfDate error: %v", err)
-                       ctx.Flash.Error(ctx.Tr("repo.pulls.merge_out_of_date"))
-                       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
-                       return
-               } else if git.IsErrPushRejected(err) {
-                       log.Debug("MergePushRejected error: %v", err)
-                       pushrejErr := err.(*git.ErrPushRejected)
-                       message := pushrejErr.Message
-                       if len(message) == 0 {
-                               ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected_no_message"))
-                       } else {
-                               flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
-                                       "Message": ctx.Tr("repo.pulls.push_rejected"),
-                                       "Summary": ctx.Tr("repo.pulls.push_rejected_summary"),
-                                       "Details": utils.SanitizeFlashErrorString(pushrejErr.Message),
-                               })
-                               if err != nil {
-                                       ctx.ServerError("MergePullRequest.HTMLString", err)
-                                       return
-                               }
-                               ctx.Flash.Error(flashError)
-                       }
-                       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
-                       return
-               }
-               ctx.ServerError("Merge", err)
-               return
-       }
-
-       if err := stopTimerIfAvailable(ctx.User, issue); err != nil {
-               ctx.ServerError("CreateOrStopIssueStopwatch", err)
-               return
-       }
-
-       log.Trace("Pull request merged: %d", pr.ID)
-       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
-}
-
-func stopTimerIfAvailable(user *models.User, issue *models.Issue) error {
-
-       if models.StopwatchExists(user.ID, issue.ID) {
-               if err := models.CreateOrStopIssueStopwatch(user, issue); err != nil {
-                       return err
-               }
-       }
-
-       return nil
-}
-
-// CompareAndPullRequestPost response for creating pull request
-func CompareAndPullRequestPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.CreateIssueForm)
-       ctx.Data["Title"] = ctx.Tr("repo.pulls.compare_changes")
-       ctx.Data["PageIsComparePull"] = true
-       ctx.Data["IsDiffCompare"] = true
-       ctx.Data["RequireHighlightJS"] = true
-       ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
-       ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
-       upload.AddUploadContext(ctx, "comment")
-
-       var (
-               repo        = ctx.Repo.Repository
-               attachments []string
-       )
-
-       headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch := ParseCompareInfo(ctx)
-       if ctx.Written() {
-               return
-       }
-       defer headGitRepo.Close()
-
-       labelIDs, assigneeIDs, milestoneID, _ := ValidateRepoMetas(ctx, *form, true)
-       if ctx.Written() {
-               return
-       }
-
-       if setting.Attachment.Enabled {
-               attachments = form.Files
-       }
-
-       if ctx.HasError() {
-               middleware.AssignForm(form, ctx.Data)
-
-               // This stage is already stop creating new pull request, so it does not matter if it has
-               // something to compare or not.
-               PrepareCompareDiff(ctx, headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch,
-                       gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
-               if ctx.Written() {
-                       return
-               }
-
-               ctx.HTML(http.StatusOK, tplCompareDiff)
-               return
-       }
-
-       if util.IsEmptyString(form.Title) {
-               PrepareCompareDiff(ctx, headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch,
-                       gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
-               if ctx.Written() {
-                       return
-               }
-
-               ctx.RenderWithErr(ctx.Tr("repo.issues.new.title_empty"), tplCompareDiff, form)
-               return
-       }
-
-       pullIssue := &models.Issue{
-               RepoID:      repo.ID,
-               Title:       form.Title,
-               PosterID:    ctx.User.ID,
-               Poster:      ctx.User,
-               MilestoneID: milestoneID,
-               IsPull:      true,
-               Content:     form.Content,
-       }
-       pullRequest := &models.PullRequest{
-               HeadRepoID: headRepo.ID,
-               BaseRepoID: repo.ID,
-               HeadBranch: headBranch,
-               BaseBranch: baseBranch,
-               HeadRepo:   headRepo,
-               BaseRepo:   repo,
-               MergeBase:  prInfo.MergeBase,
-               Type:       models.PullRequestGitea,
-       }
-       // 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(repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
-               if models.IsErrUserDoesNotHaveAccessToRepo(err) {
-                       ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
-                       return
-               } else if git.IsErrPushRejected(err) {
-                       pushrejErr := err.(*git.ErrPushRejected)
-                       message := pushrejErr.Message
-                       if len(message) == 0 {
-                               ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected_no_message"))
-                       } else {
-                               flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
-                                       "Message": ctx.Tr("repo.pulls.push_rejected"),
-                                       "Summary": ctx.Tr("repo.pulls.push_rejected_summary"),
-                                       "Details": utils.SanitizeFlashErrorString(pushrejErr.Message),
-                               })
-                               if err != nil {
-                                       ctx.ServerError("CompareAndPullRequest.HTMLString", err)
-                                       return
-                               }
-                               ctx.Flash.Error(flashError)
-                       }
-                       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pullIssue.Index))
-                       return
-               }
-               ctx.ServerError("NewPullRequest", err)
-               return
-       }
-
-       log.Trace("Pull request created: %d/%d", repo.ID, pullIssue.ID)
-       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pullIssue.Index))
-}
-
-// TriggerTask response for a trigger task request
-func TriggerTask(ctx *context.Context) {
-       pusherID := ctx.QueryInt64("pusher")
-       branch := ctx.Query("branch")
-       secret := ctx.Query("secret")
-       if len(branch) == 0 || len(secret) == 0 || pusherID <= 0 {
-               ctx.Error(http.StatusNotFound)
-               log.Trace("TriggerTask: branch or secret is empty, or pusher ID is not valid")
-               return
-       }
-       owner, repo := parseOwnerAndRepo(ctx)
-       if ctx.Written() {
-               return
-       }
-       got := []byte(base.EncodeMD5(owner.Salt))
-       want := []byte(secret)
-       if subtle.ConstantTimeCompare(got, want) != 1 {
-               ctx.Error(http.StatusNotFound)
-               log.Trace("TriggerTask [%s/%s]: invalid secret", owner.Name, repo.Name)
-               return
-       }
-
-       pusher, err := models.GetUserByID(pusherID)
-       if err != nil {
-               if models.IsErrUserNotExist(err) {
-                       ctx.Error(http.StatusNotFound)
-               } else {
-                       ctx.ServerError("GetUserByID", err)
-               }
-               return
-       }
-
-       log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name)
-
-       go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true, "", "")
-       ctx.Status(202)
-}
-
-// CleanUpPullRequest responses for delete merged branch when PR has been merged
-func CleanUpPullRequest(ctx *context.Context) {
-       issue := checkPullInfo(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       pr := issue.PullRequest
-
-       // Don't cleanup unmerged and unclosed PRs
-       if !pr.HasMerged && !issue.IsClosed {
-               ctx.NotFound("CleanUpPullRequest", nil)
-               return
-       }
-
-       if err := pr.LoadHeadRepo(); err != nil {
-               ctx.ServerError("LoadHeadRepo", err)
-               return
-       } else if pr.HeadRepo == nil {
-               // Forked repository has already been deleted
-               ctx.NotFound("CleanUpPullRequest", nil)
-               return
-       } else if err = pr.LoadBaseRepo(); err != nil {
-               ctx.ServerError("LoadBaseRepo", err)
-               return
-       } else if err = pr.HeadRepo.GetOwner(); err != nil {
-               ctx.ServerError("HeadRepo.GetOwner", err)
-               return
-       }
-
-       perm, err := models.GetUserRepoPermission(pr.HeadRepo, ctx.User)
-       if err != nil {
-               ctx.ServerError("GetUserRepoPermission", err)
-               return
-       }
-       if !perm.CanWrite(models.UnitTypeCode) {
-               ctx.NotFound("CleanUpPullRequest", nil)
-               return
-       }
-
-       fullBranchName := pr.HeadRepo.Owner.Name + "/" + pr.HeadBranch
-
-       gitRepo, err := git.OpenRepository(pr.HeadRepo.RepoPath())
-       if err != nil {
-               ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.RepoPath()), err)
-               return
-       }
-       defer gitRepo.Close()
-
-       gitBaseRepo, err := git.OpenRepository(pr.BaseRepo.RepoPath())
-       if err != nil {
-               ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.BaseRepo.RepoPath()), err)
-               return
-       }
-       defer gitBaseRepo.Close()
-
-       defer func() {
-               ctx.JSON(http.StatusOK, map[string]interface{}{
-                       "redirect": pr.BaseRepo.Link() + "/pulls/" + fmt.Sprint(issue.Index),
-               })
-       }()
-
-       // Check if branch has no new commits
-       headCommitID, err := gitBaseRepo.GetRefCommitID(pr.GetGitRefName())
-       if err != nil {
-               log.Error("GetRefCommitID: %v", err)
-               ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
-               return
-       }
-       branchCommitID, err := gitRepo.GetBranchCommitID(pr.HeadBranch)
-       if err != nil {
-               log.Error("GetBranchCommitID: %v", err)
-               ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
-               return
-       }
-       if headCommitID != branchCommitID {
-               ctx.Flash.Error(ctx.Tr("repo.branch.delete_branch_has_new_commits", fullBranchName))
-               return
-       }
-
-       if err := repo_service.DeleteBranch(ctx.User, pr.HeadRepo, gitRepo, pr.HeadBranch); err != nil {
-               switch {
-               case git.IsErrBranchNotExist(err):
-                       ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
-               case errors.Is(err, repo_service.ErrBranchIsDefault):
-                       ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
-               case errors.Is(err, repo_service.ErrBranchIsProtected):
-                       ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
-               default:
-                       log.Error("DeleteBranch: %v", err)
-                       ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
-               }
-               return
-       }
-
-       if err := models.AddDeletePRBranchComment(ctx.User, pr.BaseRepo, issue.ID, pr.HeadBranch); err != nil {
-               // Do not fail here as branch has already been deleted
-               log.Error("DeleteBranch: %v", err)
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.branch.deletion_success", fullBranchName))
-}
-
-// DownloadPullDiff render a pull's raw diff
-func DownloadPullDiff(ctx *context.Context) {
-       DownloadPullDiffOrPatch(ctx, false)
-}
-
-// DownloadPullPatch render a pull's raw patch
-func DownloadPullPatch(ctx *context.Context) {
-       DownloadPullDiffOrPatch(ctx, true)
-}
-
-// DownloadPullDiffOrPatch render a pull's raw diff or patch
-func DownloadPullDiffOrPatch(ctx *context.Context, patch bool) {
-       issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
-       if err != nil {
-               if models.IsErrIssueNotExist(err) {
-                       ctx.NotFound("GetIssueByIndex", err)
-               } else {
-                       ctx.ServerError("GetIssueByIndex", err)
-               }
-               return
-       }
-
-       // Return not found if it's not a pull request
-       if !issue.IsPull {
-               ctx.NotFound("DownloadPullDiff",
-                       fmt.Errorf("Issue is not a pull request"))
-               return
-       }
-
-       if err = issue.LoadPullRequest(); err != nil {
-               ctx.ServerError("LoadPullRequest", err)
-               return
-       }
-
-       pr := issue.PullRequest
-
-       if err := pull_service.DownloadDiffOrPatch(pr, ctx, patch); err != nil {
-               ctx.ServerError("DownloadDiffOrPatch", err)
-               return
-       }
-}
-
-// UpdatePullRequestTarget change pull request's target branch
-func UpdatePullRequestTarget(ctx *context.Context) {
-       issue := GetActionIssue(ctx)
-       pr := issue.PullRequest
-       if ctx.Written() {
-               return
-       }
-       if !issue.IsPull {
-               ctx.Error(http.StatusNotFound)
-               return
-       }
-
-       if !ctx.IsSigned || (!issue.IsPoster(ctx.User.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
-               ctx.Error(http.StatusForbidden)
-               return
-       }
-
-       targetBranch := ctx.QueryTrim("target_branch")
-       if len(targetBranch) == 0 {
-               ctx.Error(http.StatusNoContent)
-               return
-       }
-
-       if err := pull_service.ChangeTargetBranch(pr, ctx.User, targetBranch); err != nil {
-               if models.IsErrPullRequestAlreadyExists(err) {
-                       err := err.(models.ErrPullRequestAlreadyExists)
-
-                       RepoRelPath := ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name
-                       errorMessage := ctx.Tr("repo.pulls.has_pull_request", ctx.Repo.RepoLink, RepoRelPath, err.IssueID)
-
-                       ctx.Flash.Error(errorMessage)
-                       ctx.JSON(http.StatusConflict, map[string]interface{}{
-                               "error":      err.Error(),
-                               "user_error": errorMessage,
-                       })
-               } else if models.IsErrIssueIsClosed(err) {
-                       errorMessage := ctx.Tr("repo.pulls.is_closed")
-
-                       ctx.Flash.Error(errorMessage)
-                       ctx.JSON(http.StatusConflict, map[string]interface{}{
-                               "error":      err.Error(),
-                               "user_error": errorMessage,
-                       })
-               } else if models.IsErrPullRequestHasMerged(err) {
-                       errorMessage := ctx.Tr("repo.pulls.has_merged")
-
-                       ctx.Flash.Error(errorMessage)
-                       ctx.JSON(http.StatusConflict, map[string]interface{}{
-                               "error":      err.Error(),
-                               "user_error": errorMessage,
-                       })
-               } else if models.IsErrBranchesEqual(err) {
-                       errorMessage := ctx.Tr("repo.pulls.nothing_to_compare")
-
-                       ctx.Flash.Error(errorMessage)
-                       ctx.JSON(http.StatusBadRequest, map[string]interface{}{
-                               "error":      err.Error(),
-                               "user_error": errorMessage,
-                       })
-               } else {
-                       ctx.ServerError("UpdatePullRequestTarget", err)
-               }
-               return
-       }
-       notification.NotifyPullRequestChangeTargetBranch(ctx.User, pr, targetBranch)
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "base_branch": pr.BaseBranch,
-       })
-}
diff --git a/routers/repo/pull_review.go b/routers/repo/pull_review.go
deleted file mode 100644 (file)
index 9e505c3..0000000
+++ /dev/null
@@ -1,238 +0,0 @@
-// Copyright 2018 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 repo
-
-import (
-       "fmt"
-       "net/http"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-       pull_service "code.gitea.io/gitea/services/pull"
-)
-
-const (
-       tplConversation base.TplName = "repo/diff/conversation"
-       tplNewComment   base.TplName = "repo/diff/new_comment"
-)
-
-// RenderNewCodeCommentForm will render the form for creating a new review comment
-func RenderNewCodeCommentForm(ctx *context.Context) {
-       issue := GetActionIssue(ctx)
-       if !issue.IsPull {
-               return
-       }
-       currentReview, err := models.GetCurrentReview(ctx.User, issue)
-       if err != nil && !models.IsErrReviewNotExist(err) {
-               ctx.ServerError("GetCurrentReview", err)
-               return
-       }
-       ctx.Data["PageIsPullFiles"] = true
-       ctx.Data["Issue"] = issue
-       ctx.Data["CurrentReview"] = currentReview
-       pullHeadCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(issue.PullRequest.GetGitRefName())
-       if err != nil {
-               ctx.ServerError("GetRefCommitID", err)
-               return
-       }
-       ctx.Data["AfterCommitID"] = pullHeadCommitID
-       ctx.HTML(http.StatusOK, tplNewComment)
-}
-
-// CreateCodeComment will create a code comment including an pending review if required
-func CreateCodeComment(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.CodeCommentForm)
-       issue := GetActionIssue(ctx)
-       if !issue.IsPull {
-               return
-       }
-       if ctx.Written() {
-               return
-       }
-
-       if ctx.HasError() {
-               ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
-               ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
-               return
-       }
-
-       signedLine := form.Line
-       if form.Side == "previous" {
-               signedLine *= -1
-       }
-
-       comment, err := pull_service.CreateCodeComment(
-               ctx.User,
-               ctx.Repo.GitRepo,
-               issue,
-               signedLine,
-               form.Content,
-               form.TreePath,
-               form.IsReview,
-               form.Reply,
-               form.LatestCommitID,
-       )
-       if err != nil {
-               ctx.ServerError("CreateCodeComment", err)
-               return
-       }
-
-       if comment == nil {
-               log.Trace("Comment not created: %-v #%d[%d]", ctx.Repo.Repository, issue.Index, issue.ID)
-               ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
-               return
-       }
-
-       log.Trace("Comment created: %-v #%d[%d] Comment[%d]", ctx.Repo.Repository, issue.Index, issue.ID, comment.ID)
-
-       if form.Origin == "diff" {
-               renderConversation(ctx, comment)
-               return
-       }
-       ctx.Redirect(comment.HTMLURL())
-}
-
-// UpdateResolveConversation add or remove an Conversation resolved mark
-func UpdateResolveConversation(ctx *context.Context) {
-       origin := ctx.Query("origin")
-       action := ctx.Query("action")
-       commentID := ctx.QueryInt64("comment_id")
-
-       comment, err := models.GetCommentByID(commentID)
-       if err != nil {
-               ctx.ServerError("GetIssueByID", err)
-               return
-       }
-
-       if err = comment.LoadIssue(); err != nil {
-               ctx.ServerError("comment.LoadIssue", err)
-               return
-       }
-
-       var permResult bool
-       if permResult, err = models.CanMarkConversation(comment.Issue, ctx.User); err != nil {
-               ctx.ServerError("CanMarkConversation", err)
-               return
-       }
-       if !permResult {
-               ctx.Error(http.StatusForbidden)
-               return
-       }
-
-       if !comment.Issue.IsPull {
-               ctx.Error(http.StatusBadRequest)
-               return
-       }
-
-       if action == "Resolve" || action == "UnResolve" {
-               err = models.MarkConversation(comment, ctx.User, action == "Resolve")
-               if err != nil {
-                       ctx.ServerError("MarkConversation", err)
-                       return
-               }
-       } else {
-               ctx.Error(http.StatusBadRequest)
-               return
-       }
-
-       if origin == "diff" {
-               renderConversation(ctx, comment)
-               return
-       }
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "ok": true,
-       })
-}
-
-func renderConversation(ctx *context.Context, comment *models.Comment) {
-       comments, err := models.FetchCodeCommentsByLine(comment.Issue, ctx.User, comment.TreePath, comment.Line)
-       if err != nil {
-               ctx.ServerError("FetchCodeCommentsByLine", err)
-               return
-       }
-       ctx.Data["PageIsPullFiles"] = true
-       ctx.Data["comments"] = comments
-       ctx.Data["CanMarkConversation"] = true
-       ctx.Data["Issue"] = comment.Issue
-       if err = comment.Issue.LoadPullRequest(); err != nil {
-               ctx.ServerError("comment.Issue.LoadPullRequest", err)
-               return
-       }
-       pullHeadCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(comment.Issue.PullRequest.GetGitRefName())
-       if err != nil {
-               ctx.ServerError("GetRefCommitID", err)
-               return
-       }
-       ctx.Data["AfterCommitID"] = pullHeadCommitID
-       ctx.HTML(http.StatusOK, tplConversation)
-}
-
-// 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) {
-       form := web.GetForm(ctx).(*forms.SubmitReviewForm)
-       issue := GetActionIssue(ctx)
-       if !issue.IsPull {
-               return
-       }
-       if ctx.Written() {
-               return
-       }
-       if ctx.HasError() {
-               ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
-               ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
-               return
-       }
-
-       reviewType := form.ReviewType()
-       switch reviewType {
-       case models.ReviewTypeUnknown:
-               ctx.ServerError("ReviewType", fmt.Errorf("unknown ReviewType: %s", form.Type))
-               return
-
-       // can not approve/reject your own PR
-       case models.ReviewTypeApprove, models.ReviewTypeReject:
-               if issue.IsPoster(ctx.User.ID) {
-                       var translated string
-                       if reviewType == models.ReviewTypeApprove {
-                               translated = ctx.Tr("repo.issues.review.self.approval")
-                       } else {
-                               translated = ctx.Tr("repo.issues.review.self.rejection")
-                       }
-
-                       ctx.Flash.Error(translated)
-                       ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
-                       return
-               }
-       }
-
-       _, comm, err := pull_service.SubmitReview(ctx.User, ctx.Repo.GitRepo, issue, reviewType, form.Content, form.CommitID)
-       if err != nil {
-               if models.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 {
-                       ctx.ServerError("SubmitReview", err)
-               }
-               return
-       }
-
-       ctx.Redirect(fmt.Sprintf("%s/pulls/%d#%s", ctx.Repo.RepoLink, issue.Index, comm.HashTag()))
-}
-
-// DismissReview dismissing stale review by repo admin
-func DismissReview(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.DismissReviewForm)
-       comm, err := pull_service.DismissReview(form.ReviewID, form.Message, ctx.User, true)
-       if err != nil {
-               ctx.ServerError("pull_service.DismissReview", err)
-               return
-       }
-
-       ctx.Redirect(fmt.Sprintf("%s/pulls/%d#%s", ctx.Repo.RepoLink, comm.Issue.Index, comm.HashTag()))
-}
diff --git a/routers/repo/release.go b/routers/repo/release.go
deleted file mode 100644 (file)
index b7730e4..0000000
+++ /dev/null
@@ -1,512 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2018 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 repo
-
-import (
-       "fmt"
-       "net/http"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/convert"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/markup"
-       "code.gitea.io/gitea/modules/markup/markdown"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/upload"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-       releaseservice "code.gitea.io/gitea/services/release"
-)
-
-const (
-       tplReleases   base.TplName = "repo/release/list"
-       tplReleaseNew base.TplName = "repo/release/new"
-)
-
-// calReleaseNumCommitsBehind calculates given release has how many commits behind release target.
-func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *models.Release, countCache map[string]int64) error {
-       // Fast return if release target is same as default branch.
-       if repoCtx.BranchName == release.Target {
-               release.NumCommitsBehind = repoCtx.CommitsCount - release.NumCommits
-               return nil
-       }
-
-       // Get count if not exists
-       if _, ok := countCache[release.Target]; !ok {
-               if repoCtx.GitRepo.IsBranchExist(release.Target) {
-                       commit, err := repoCtx.GitRepo.GetBranchCommit(release.Target)
-                       if err != nil {
-                               return fmt.Errorf("GetBranchCommit: %v", err)
-                       }
-                       countCache[release.Target], err = commit.CommitsCount()
-                       if err != nil {
-                               return fmt.Errorf("CommitsCount: %v", err)
-                       }
-               } else {
-                       // Use NumCommits of the newest release on that target
-                       countCache[release.Target] = release.NumCommits
-               }
-       }
-       release.NumCommitsBehind = countCache[release.Target] - release.NumCommits
-       return nil
-}
-
-// Releases render releases list page
-func Releases(ctx *context.Context) {
-       releasesOrTags(ctx, false)
-}
-
-// TagsList render tags list page
-func TagsList(ctx *context.Context) {
-       releasesOrTags(ctx, true)
-}
-
-func releasesOrTags(ctx *context.Context, isTagList bool) {
-       ctx.Data["PageIsReleaseList"] = true
-       ctx.Data["DefaultBranch"] = ctx.Repo.Repository.DefaultBranch
-       ctx.Data["IsViewBranch"] = false
-       ctx.Data["IsViewTag"] = true
-       // Disable the showCreateNewBranch form in the dropdown on this page.
-       ctx.Data["CanCreateBranch"] = false
-       ctx.Data["HideBranchesInDropdown"] = true
-
-       if isTagList {
-               ctx.Data["Title"] = ctx.Tr("repo.release.tags")
-               ctx.Data["PageIsTagList"] = true
-       } else {
-               ctx.Data["Title"] = ctx.Tr("repo.release.releases")
-               ctx.Data["PageIsTagList"] = false
-       }
-
-       tags, err := ctx.Repo.GitRepo.GetTags()
-       if err != nil {
-               ctx.ServerError("GetTags", err)
-               return
-       }
-       ctx.Data["Tags"] = tags
-
-       writeAccess := ctx.Repo.CanWrite(models.UnitTypeReleases)
-       ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
-
-       opts := models.FindReleasesOptions{
-               ListOptions: models.ListOptions{
-                       Page:     ctx.QueryInt("page"),
-                       PageSize: convert.ToCorrectPageSize(ctx.QueryInt("limit")),
-               },
-               IncludeDrafts: writeAccess && !isTagList,
-               IncludeTags:   isTagList,
-       }
-
-       releases, err := models.GetReleasesByRepoID(ctx.Repo.Repository.ID, opts)
-       if err != nil {
-               ctx.ServerError("GetReleasesByRepoID", err)
-               return
-       }
-
-       count, err := models.GetReleaseCountByRepoID(ctx.Repo.Repository.ID, opts)
-       if err != nil {
-               ctx.ServerError("GetReleaseCountByRepoID", err)
-               return
-       }
-
-       if err = models.GetReleaseAttachments(releases...); err != nil {
-               ctx.ServerError("GetReleaseAttachments", err)
-               return
-       }
-
-       // Temporary cache commits count of used branches to speed up.
-       countCache := make(map[string]int64)
-       cacheUsers := make(map[int64]*models.User)
-       if ctx.User != nil {
-               cacheUsers[ctx.User.ID] = ctx.User
-       }
-       var ok bool
-
-       for _, r := range releases {
-               if r.Publisher, ok = cacheUsers[r.PublisherID]; !ok {
-                       r.Publisher, err = models.GetUserByID(r.PublisherID)
-                       if err != nil {
-                               if models.IsErrUserNotExist(err) {
-                                       r.Publisher = models.NewGhostUser()
-                               } else {
-                                       ctx.ServerError("GetUserByID", err)
-                                       return
-                               }
-                       }
-                       cacheUsers[r.PublisherID] = r.Publisher
-               }
-
-               r.Note, err = markdown.RenderString(&markup.RenderContext{
-                       URLPrefix: ctx.Repo.RepoLink,
-                       Metas:     ctx.Repo.Repository.ComposeMetas(),
-               }, r.Note)
-               if err != nil {
-                       ctx.ServerError("RenderString", err)
-                       return
-               }
-
-               if r.IsDraft {
-                       continue
-               }
-
-               if err := calReleaseNumCommitsBehind(ctx.Repo, r, countCache); err != nil {
-                       ctx.ServerError("calReleaseNumCommitsBehind", err)
-                       return
-               }
-       }
-
-       ctx.Data["Releases"] = releases
-       ctx.Data["ReleasesNum"] = len(releases)
-
-       pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
-       pager.SetDefaultParams(ctx)
-       ctx.Data["Page"] = pager
-
-       ctx.HTML(http.StatusOK, tplReleases)
-}
-
-// SingleRelease renders a single release's page
-func SingleRelease(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.release.releases")
-       ctx.Data["PageIsReleaseList"] = true
-
-       writeAccess := ctx.Repo.CanWrite(models.UnitTypeReleases)
-       ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
-
-       release, err := models.GetRelease(ctx.Repo.Repository.ID, ctx.Params("*"))
-       if err != nil {
-               if models.IsErrReleaseNotExist(err) {
-                       ctx.NotFound("GetRelease", err)
-                       return
-               }
-               ctx.ServerError("GetReleasesByRepoID", err)
-               return
-       }
-
-       err = models.GetReleaseAttachments(release)
-       if err != nil {
-               ctx.ServerError("GetReleaseAttachments", err)
-               return
-       }
-
-       release.Publisher, err = models.GetUserByID(release.PublisherID)
-       if err != nil {
-               if models.IsErrUserNotExist(err) {
-                       release.Publisher = models.NewGhostUser()
-               } else {
-                       ctx.ServerError("GetUserByID", err)
-                       return
-               }
-       }
-       if !release.IsDraft {
-               if err := calReleaseNumCommitsBehind(ctx.Repo, release, make(map[string]int64)); err != nil {
-                       ctx.ServerError("calReleaseNumCommitsBehind", err)
-                       return
-               }
-       }
-       release.Note, err = markdown.RenderString(&markup.RenderContext{
-               URLPrefix: ctx.Repo.RepoLink,
-               Metas:     ctx.Repo.Repository.ComposeMetas(),
-       }, release.Note)
-       if err != nil {
-               ctx.ServerError("RenderString", err)
-               return
-       }
-
-       ctx.Data["Releases"] = []*models.Release{release}
-       ctx.HTML(http.StatusOK, tplReleases)
-}
-
-// LatestRelease redirects to the latest release
-func LatestRelease(ctx *context.Context) {
-       release, err := models.GetLatestReleaseByRepoID(ctx.Repo.Repository.ID)
-       if err != nil {
-               if models.IsErrReleaseNotExist(err) {
-                       ctx.NotFound("LatestRelease", err)
-                       return
-               }
-               ctx.ServerError("GetLatestReleaseByRepoID", err)
-               return
-       }
-
-       if err := release.LoadAttributes(); err != nil {
-               ctx.ServerError("LoadAttributes", err)
-               return
-       }
-
-       ctx.Redirect(release.HTMLURL())
-}
-
-// NewRelease render creating or edit release page
-func NewRelease(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.release.new_release")
-       ctx.Data["PageIsReleaseList"] = true
-       ctx.Data["RequireSimpleMDE"] = true
-       ctx.Data["RequireTribute"] = true
-       ctx.Data["tag_target"] = ctx.Repo.Repository.DefaultBranch
-       if tagName := ctx.Query("tag"); len(tagName) > 0 {
-               rel, err := models.GetRelease(ctx.Repo.Repository.ID, tagName)
-               if err != nil && !models.IsErrReleaseNotExist(err) {
-                       ctx.ServerError("GetRelease", err)
-                       return
-               }
-
-               if rel != nil {
-                       rel.Repo = ctx.Repo.Repository
-                       if err := rel.LoadAttributes(); err != nil {
-                               ctx.ServerError("LoadAttributes", err)
-                               return
-                       }
-
-                       ctx.Data["tag_name"] = rel.TagName
-                       ctx.Data["tag_target"] = rel.Target
-                       ctx.Data["title"] = rel.Title
-                       ctx.Data["content"] = rel.Note
-                       ctx.Data["attachments"] = rel.Attachments
-               }
-       }
-       ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
-       upload.AddUploadContext(ctx, "release")
-       ctx.HTML(http.StatusOK, tplReleaseNew)
-}
-
-// NewReleasePost response for creating a release
-func NewReleasePost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.NewReleaseForm)
-       ctx.Data["Title"] = ctx.Tr("repo.release.new_release")
-       ctx.Data["PageIsReleaseList"] = true
-       ctx.Data["RequireSimpleMDE"] = true
-       ctx.Data["RequireTribute"] = true
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplReleaseNew)
-               return
-       }
-
-       if !ctx.Repo.GitRepo.IsBranchExist(form.Target) {
-               ctx.RenderWithErr(ctx.Tr("form.target_branch_not_exist"), tplReleaseNew, &form)
-               return
-       }
-
-       var attachmentUUIDs []string
-       if setting.Attachment.Enabled {
-               attachmentUUIDs = form.Files
-       }
-
-       rel, err := models.GetRelease(ctx.Repo.Repository.ID, form.TagName)
-       if err != nil {
-               if !models.IsErrReleaseNotExist(err) {
-                       ctx.ServerError("GetRelease", err)
-                       return
-               }
-
-               msg := ""
-               if len(form.Title) > 0 && form.AddTagMsg {
-                       msg = form.Title + "\n\n" + form.Content
-               }
-
-               if len(form.TagOnly) > 0 {
-                       if err = releaseservice.CreateNewTag(ctx.User, ctx.Repo.Repository, form.Target, form.TagName, msg); err != nil {
-                               if models.IsErrTagAlreadyExists(err) {
-                                       e := err.(models.ErrTagAlreadyExists)
-                                       ctx.Flash.Error(ctx.Tr("repo.branch.tag_collision", e.TagName))
-                                       ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
-                                       return
-                               }
-
-                               ctx.ServerError("releaseservice.CreateNewTag", err)
-                               return
-                       }
-
-                       ctx.Flash.Success(ctx.Tr("repo.tag.create_success", form.TagName))
-                       ctx.Redirect(ctx.Repo.RepoLink + "/src/tag/" + form.TagName)
-                       return
-               }
-
-               rel = &models.Release{
-                       RepoID:       ctx.Repo.Repository.ID,
-                       PublisherID:  ctx.User.ID,
-                       Title:        form.Title,
-                       TagName:      form.TagName,
-                       Target:       form.Target,
-                       Note:         form.Content,
-                       IsDraft:      len(form.Draft) > 0,
-                       IsPrerelease: form.Prerelease,
-                       IsTag:        false,
-               }
-
-               if err = releaseservice.CreateRelease(ctx.Repo.GitRepo, rel, attachmentUUIDs, msg); err != nil {
-                       ctx.Data["Err_TagName"] = true
-                       switch {
-                       case models.IsErrReleaseAlreadyExist(err):
-                               ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
-                       case models.IsErrInvalidTagName(err):
-                               ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form)
-                       default:
-                               ctx.ServerError("CreateRelease", err)
-                       }
-                       return
-               }
-       } else {
-               if !rel.IsTag {
-                       ctx.Data["Err_TagName"] = true
-                       ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
-                       return
-               }
-
-               rel.Title = form.Title
-               rel.Note = form.Content
-               rel.Target = form.Target
-               rel.IsDraft = len(form.Draft) > 0
-               rel.IsPrerelease = form.Prerelease
-               rel.PublisherID = ctx.User.ID
-               rel.IsTag = false
-
-               if err = releaseservice.UpdateRelease(ctx.User, ctx.Repo.GitRepo, rel, attachmentUUIDs, nil, nil); err != nil {
-                       ctx.Data["Err_TagName"] = true
-                       ctx.ServerError("UpdateRelease", err)
-                       return
-               }
-       }
-       log.Trace("Release created: %s/%s:%s", ctx.User.LowerName, ctx.Repo.Repository.Name, form.TagName)
-
-       ctx.Redirect(ctx.Repo.RepoLink + "/releases")
-}
-
-// EditRelease render release edit page
-func EditRelease(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.release.edit_release")
-       ctx.Data["PageIsReleaseList"] = true
-       ctx.Data["PageIsEditRelease"] = true
-       ctx.Data["RequireSimpleMDE"] = true
-       ctx.Data["RequireTribute"] = true
-       ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
-       upload.AddUploadContext(ctx, "release")
-
-       tagName := ctx.Params("*")
-       rel, err := models.GetRelease(ctx.Repo.Repository.ID, tagName)
-       if err != nil {
-               if models.IsErrReleaseNotExist(err) {
-                       ctx.NotFound("GetRelease", err)
-               } else {
-                       ctx.ServerError("GetRelease", err)
-               }
-               return
-       }
-       ctx.Data["ID"] = rel.ID
-       ctx.Data["tag_name"] = rel.TagName
-       ctx.Data["tag_target"] = rel.Target
-       ctx.Data["title"] = rel.Title
-       ctx.Data["content"] = rel.Note
-       ctx.Data["prerelease"] = rel.IsPrerelease
-       ctx.Data["IsDraft"] = rel.IsDraft
-
-       rel.Repo = ctx.Repo.Repository
-       if err := rel.LoadAttributes(); err != nil {
-               ctx.ServerError("LoadAttributes", err)
-               return
-       }
-       ctx.Data["attachments"] = rel.Attachments
-
-       ctx.HTML(http.StatusOK, tplReleaseNew)
-}
-
-// EditReleasePost response for edit release
-func EditReleasePost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.EditReleaseForm)
-       ctx.Data["Title"] = ctx.Tr("repo.release.edit_release")
-       ctx.Data["PageIsReleaseList"] = true
-       ctx.Data["PageIsEditRelease"] = true
-       ctx.Data["RequireSimpleMDE"] = true
-       ctx.Data["RequireTribute"] = true
-
-       tagName := ctx.Params("*")
-       rel, err := models.GetRelease(ctx.Repo.Repository.ID, tagName)
-       if err != nil {
-               if models.IsErrReleaseNotExist(err) {
-                       ctx.NotFound("GetRelease", err)
-               } else {
-                       ctx.ServerError("GetRelease", err)
-               }
-               return
-       }
-       if rel.IsTag {
-               ctx.NotFound("GetRelease", err)
-               return
-       }
-       ctx.Data["tag_name"] = rel.TagName
-       ctx.Data["tag_target"] = rel.Target
-       ctx.Data["title"] = rel.Title
-       ctx.Data["content"] = rel.Note
-       ctx.Data["prerelease"] = rel.IsPrerelease
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplReleaseNew)
-               return
-       }
-
-       const delPrefix = "attachment-del-"
-       const editPrefix = "attachment-edit-"
-       var addAttachmentUUIDs, delAttachmentUUIDs []string
-       var editAttachments = make(map[string]string) // uuid -> new name
-       if setting.Attachment.Enabled {
-               addAttachmentUUIDs = form.Files
-               for k, v := range ctx.Req.Form {
-                       if strings.HasPrefix(k, delPrefix) && v[0] == "true" {
-                               delAttachmentUUIDs = append(delAttachmentUUIDs, k[len(delPrefix):])
-                       } else if strings.HasPrefix(k, editPrefix) {
-                               editAttachments[k[len(editPrefix):]] = v[0]
-                       }
-               }
-       }
-
-       rel.Title = form.Title
-       rel.Note = form.Content
-       rel.IsDraft = len(form.Draft) > 0
-       rel.IsPrerelease = form.Prerelease
-       if err = releaseservice.UpdateRelease(ctx.User, ctx.Repo.GitRepo,
-               rel, addAttachmentUUIDs, delAttachmentUUIDs, editAttachments); err != nil {
-               ctx.ServerError("UpdateRelease", err)
-               return
-       }
-       ctx.Redirect(ctx.Repo.RepoLink + "/releases")
-}
-
-// DeleteRelease delete a release
-func DeleteRelease(ctx *context.Context) {
-       deleteReleaseOrTag(ctx, false)
-}
-
-// DeleteTag delete a tag
-func DeleteTag(ctx *context.Context) {
-       deleteReleaseOrTag(ctx, true)
-}
-
-func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) {
-       if err := releaseservice.DeleteReleaseByID(ctx.QueryInt64("id"), ctx.User, isDelTag); err != nil {
-               ctx.Flash.Error("DeleteReleaseByID: " + err.Error())
-       } else {
-               if isDelTag {
-                       ctx.Flash.Success(ctx.Tr("repo.release.deletion_tag_success"))
-               } else {
-                       ctx.Flash.Success(ctx.Tr("repo.release.deletion_success"))
-               }
-       }
-
-       if isDelTag {
-               ctx.JSON(http.StatusOK, map[string]interface{}{
-                       "redirect": ctx.Repo.RepoLink + "/tags",
-               })
-               return
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": ctx.Repo.RepoLink + "/releases",
-       })
-}
diff --git a/routers/repo/release_test.go b/routers/repo/release_test.go
deleted file mode 100644 (file)
index 004a6ef..0000000
+++ /dev/null
@@ -1,64 +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 repo
-
-import (
-       "testing"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/test"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-)
-
-func TestNewReleasePost(t *testing.T) {
-       for _, testCase := range []struct {
-               RepoID  int64
-               UserID  int64
-               TagName string
-               Form    forms.NewReleaseForm
-       }{
-               {
-                       RepoID:  1,
-                       UserID:  2,
-                       TagName: "v1.1", // pre-existing tag
-                       Form: forms.NewReleaseForm{
-                               TagName: "newtag",
-                               Target:  "master",
-                               Title:   "title",
-                               Content: "content",
-                       },
-               },
-               {
-                       RepoID:  1,
-                       UserID:  2,
-                       TagName: "newtag",
-                       Form: forms.NewReleaseForm{
-                               TagName: "newtag",
-                               Target:  "master",
-                               Title:   "title",
-                               Content: "content",
-                       },
-               },
-       } {
-               models.PrepareTestEnv(t)
-
-               ctx := test.MockContext(t, "user2/repo1/releases/new")
-               test.LoadUser(t, ctx, 2)
-               test.LoadRepo(t, ctx, 1)
-               test.LoadGitRepo(t, ctx)
-               web.SetForm(ctx, &testCase.Form)
-               NewReleasePost(ctx)
-               models.AssertExistsAndLoadBean(t, &models.Release{
-                       RepoID:      1,
-                       PublisherID: 2,
-                       TagName:     testCase.Form.TagName,
-                       Target:      testCase.Form.Target,
-                       Title:       testCase.Form.Title,
-                       Note:        testCase.Form.Content,
-               }, models.Cond("is_draft=?", len(testCase.Form.Draft) > 0))
-               ctx.Repo.GitRepo.Close()
-       }
-}
diff --git a/routers/repo/repo.go b/routers/repo/repo.go
deleted file mode 100644 (file)
index 69471a8..0000000
+++ /dev/null
@@ -1,412 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// 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 repo
-
-import (
-       "errors"
-       "fmt"
-       "net/http"
-       "strings"
-       "time"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/web"
-       archiver_service "code.gitea.io/gitea/services/archiver"
-       "code.gitea.io/gitea/services/forms"
-       repo_service "code.gitea.io/gitea/services/repository"
-)
-
-const (
-       tplCreate       base.TplName = "repo/create"
-       tplAlertDetails base.TplName = "base/alert_details"
-)
-
-// MustBeNotEmpty render when a repo is a empty git dir
-func MustBeNotEmpty(ctx *context.Context) {
-       if ctx.Repo.Repository.IsEmpty {
-               ctx.NotFound("MustBeNotEmpty", nil)
-       }
-}
-
-// MustBeEditable check that repo can be edited
-func MustBeEditable(ctx *context.Context) {
-       if !ctx.Repo.Repository.CanEnableEditor() || ctx.Repo.IsViewCommit {
-               ctx.NotFound("", nil)
-               return
-       }
-}
-
-// MustBeAbleToUpload check that repo can be uploaded to
-func MustBeAbleToUpload(ctx *context.Context) {
-       if !setting.Repository.Upload.Enabled {
-               ctx.NotFound("", nil)
-       }
-}
-
-func checkContextUser(ctx *context.Context, uid int64) *models.User {
-       orgs, err := models.GetOrgsCanCreateRepoByUserID(ctx.User.ID)
-       if err != nil {
-               ctx.ServerError("GetOrgsCanCreateRepoByUserID", err)
-               return nil
-       }
-
-       if !ctx.User.IsAdmin {
-               orgsAvailable := []*models.User{}
-               for i := 0; i < len(orgs); i++ {
-                       if orgs[i].CanCreateRepo() {
-                               orgsAvailable = append(orgsAvailable, orgs[i])
-                       }
-               }
-               ctx.Data["Orgs"] = orgsAvailable
-       } else {
-               ctx.Data["Orgs"] = orgs
-       }
-
-       // Not equal means current user is an organization.
-       if uid == ctx.User.ID || uid == 0 {
-               return ctx.User
-       }
-
-       org, err := models.GetUserByID(uid)
-       if models.IsErrUserNotExist(err) {
-               return ctx.User
-       }
-
-       if err != nil {
-               ctx.ServerError("GetUserByID", fmt.Errorf("[%d]: %v", uid, err))
-               return nil
-       }
-
-       // Check ownership of organization.
-       if !org.IsOrganization() {
-               ctx.Error(http.StatusForbidden)
-               return nil
-       }
-       if !ctx.User.IsAdmin {
-               canCreate, err := org.CanCreateOrgRepo(ctx.User.ID)
-               if err != nil {
-                       ctx.ServerError("CanCreateOrgRepo", err)
-                       return nil
-               } else if !canCreate {
-                       ctx.Error(http.StatusForbidden)
-                       return nil
-               }
-       } else {
-               ctx.Data["Orgs"] = orgs
-       }
-       return org
-}
-
-func getRepoPrivate(ctx *context.Context) bool {
-       switch strings.ToLower(setting.Repository.DefaultPrivate) {
-       case setting.RepoCreatingLastUserVisibility:
-               return ctx.User.LastRepoVisibility
-       case setting.RepoCreatingPrivate:
-               return true
-       case setting.RepoCreatingPublic:
-               return false
-       default:
-               return ctx.User.LastRepoVisibility
-       }
-}
-
-// Create render creating repository page
-func Create(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("new_repo")
-
-       // Give default value for template to render.
-       ctx.Data["Gitignores"] = models.Gitignores
-       ctx.Data["LabelTemplates"] = models.LabelTemplates
-       ctx.Data["Licenses"] = models.Licenses
-       ctx.Data["Readmes"] = models.Readmes
-       ctx.Data["readme"] = "Default"
-       ctx.Data["private"] = getRepoPrivate(ctx)
-       ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate
-       ctx.Data["default_branch"] = setting.Repository.DefaultBranch
-
-       ctxUser := checkContextUser(ctx, ctx.QueryInt64("org"))
-       if ctx.Written() {
-               return
-       }
-       ctx.Data["ContextUser"] = ctxUser
-
-       ctx.Data["repo_template_name"] = ctx.Tr("repo.template_select")
-       templateID := ctx.QueryInt64("template_id")
-       if templateID > 0 {
-               templateRepo, err := models.GetRepositoryByID(templateID)
-               if err == nil && templateRepo.CheckUnitUser(ctxUser, models.UnitTypeCode) {
-                       ctx.Data["repo_template"] = templateID
-                       ctx.Data["repo_template_name"] = templateRepo.Name
-               }
-       }
-
-       ctx.Data["CanCreateRepo"] = ctx.User.CanCreateRepo()
-       ctx.Data["MaxCreationLimit"] = ctx.User.MaxCreationLimit()
-
-       ctx.HTML(http.StatusOK, tplCreate)
-}
-
-func handleCreateError(ctx *context.Context, owner *models.User, err error, name string, tpl base.TplName, form interface{}) {
-       switch {
-       case models.IsErrReachLimitOfRepo(err):
-               ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_creation", owner.MaxCreationLimit()), tpl, form)
-       case models.IsErrRepoAlreadyExist(err):
-               ctx.Data["Err_RepoName"] = true
-               ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form)
-       case models.IsErrRepoFilesAlreadyExist(err):
-               ctx.Data["Err_RepoName"] = true
-               switch {
-               case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
-                       ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form)
-               case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
-                       ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form)
-               case setting.Repository.AllowDeleteOfUnadoptedRepositories:
-                       ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form)
-               default:
-                       ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form)
-               }
-       case models.IsErrNameReserved(err):
-               ctx.Data["Err_RepoName"] = true
-               ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tpl, form)
-       case models.IsErrNamePatternNotAllowed(err):
-               ctx.Data["Err_RepoName"] = true
-               ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tpl, form)
-       default:
-               ctx.ServerError(name, err)
-       }
-}
-
-// CreatePost response for creating repository
-func CreatePost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.CreateRepoForm)
-       ctx.Data["Title"] = ctx.Tr("new_repo")
-
-       ctx.Data["Gitignores"] = models.Gitignores
-       ctx.Data["LabelTemplates"] = models.LabelTemplates
-       ctx.Data["Licenses"] = models.Licenses
-       ctx.Data["Readmes"] = models.Readmes
-
-       ctx.Data["CanCreateRepo"] = ctx.User.CanCreateRepo()
-       ctx.Data["MaxCreationLimit"] = ctx.User.MaxCreationLimit()
-
-       ctxUser := checkContextUser(ctx, form.UID)
-       if ctx.Written() {
-               return
-       }
-       ctx.Data["ContextUser"] = ctxUser
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplCreate)
-               return
-       }
-
-       var repo *models.Repository
-       var err error
-       if form.RepoTemplate > 0 {
-               opts := models.GenerateRepoOptions{
-                       Name:        form.RepoName,
-                       Description: form.Description,
-                       Private:     form.Private,
-                       GitContent:  form.GitContent,
-                       Topics:      form.Topics,
-                       GitHooks:    form.GitHooks,
-                       Webhooks:    form.Webhooks,
-                       Avatar:      form.Avatar,
-                       IssueLabels: form.Labels,
-               }
-
-               if !opts.IsValid() {
-                       ctx.RenderWithErr(ctx.Tr("repo.template.one_item"), tplCreate, form)
-                       return
-               }
-
-               templateRepo := getRepository(ctx, form.RepoTemplate)
-               if ctx.Written() {
-                       return
-               }
-
-               if !templateRepo.IsTemplate {
-                       ctx.RenderWithErr(ctx.Tr("repo.template.invalid"), tplCreate, form)
-                       return
-               }
-
-               repo, err = repo_service.GenerateRepository(ctx.User, ctxUser, templateRepo, opts)
-               if err == nil {
-                       log.Trace("Repository generated [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)
-                       ctx.Redirect(ctxUser.HomeLink() + "/" + repo.Name)
-                       return
-               }
-       } else {
-               repo, err = repo_service.CreateRepository(ctx.User, ctxUser, models.CreateRepoOptions{
-                       Name:          form.RepoName,
-                       Description:   form.Description,
-                       Gitignores:    form.Gitignores,
-                       IssueLabels:   form.IssueLabels,
-                       License:       form.License,
-                       Readme:        form.Readme,
-                       IsPrivate:     form.Private || setting.Repository.ForcePrivate,
-                       DefaultBranch: form.DefaultBranch,
-                       AutoInit:      form.AutoInit,
-                       IsTemplate:    form.Template,
-                       TrustModel:    models.ToTrustModel(form.TrustModel),
-               })
-               if err == nil {
-                       log.Trace("Repository created [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)
-                       ctx.Redirect(ctxUser.HomeLink() + "/" + repo.Name)
-                       return
-               }
-       }
-
-       handleCreateError(ctx, ctxUser, err, "CreatePost", tplCreate, &form)
-}
-
-// Action response for actions to a repository
-func Action(ctx *context.Context) {
-       var err error
-       switch ctx.Params(":action") {
-       case "watch":
-               err = models.WatchRepo(ctx.User.ID, ctx.Repo.Repository.ID, true)
-       case "unwatch":
-               err = models.WatchRepo(ctx.User.ID, ctx.Repo.Repository.ID, false)
-       case "star":
-               err = models.StarRepo(ctx.User.ID, ctx.Repo.Repository.ID, true)
-       case "unstar":
-               err = models.StarRepo(ctx.User.ID, ctx.Repo.Repository.ID, false)
-       case "accept_transfer":
-               err = acceptOrRejectRepoTransfer(ctx, true)
-       case "reject_transfer":
-               err = acceptOrRejectRepoTransfer(ctx, false)
-       case "desc": // FIXME: this is not used
-               if !ctx.Repo.IsOwner() {
-                       ctx.Error(http.StatusNotFound)
-                       return
-               }
-
-               ctx.Repo.Repository.Description = ctx.Query("desc")
-               ctx.Repo.Repository.Website = ctx.Query("site")
-               err = models.UpdateRepository(ctx.Repo.Repository, false)
-       }
-
-       if err != nil {
-               ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err)
-               return
-       }
-
-       ctx.RedirectToFirst(ctx.Query("redirect_to"), ctx.Repo.RepoLink)
-}
-
-func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error {
-       repoTransfer, err := models.GetPendingRepositoryTransfer(ctx.Repo.Repository)
-       if err != nil {
-               return err
-       }
-
-       if err := repoTransfer.LoadAttributes(); err != nil {
-               return err
-       }
-
-       if !repoTransfer.CanUserAcceptTransfer(ctx.User) {
-               return errors.New("user does not have enough permissions")
-       }
-
-       if accept {
-               if err := repo_service.TransferOwnership(repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams); err != nil {
-                       return err
-               }
-               ctx.Flash.Success(ctx.Tr("repo.settings.transfer.success"))
-       } else {
-               if err := models.CancelRepositoryTransfer(ctx.Repo.Repository); err != nil {
-                       return err
-               }
-               ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected"))
-       }
-
-       ctx.Redirect(ctx.Repo.Repository.HTMLURL())
-       return nil
-}
-
-// RedirectDownload return a file based on the following infos:
-func RedirectDownload(ctx *context.Context) {
-       var (
-               vTag     = ctx.Params("vTag")
-               fileName = ctx.Params("fileName")
-       )
-       tagNames := []string{vTag}
-       curRepo := ctx.Repo.Repository
-       releases, err := models.GetReleasesByRepoIDAndNames(models.DefaultDBContext(), curRepo.ID, tagNames)
-       if err != nil {
-               if models.IsErrAttachmentNotExist(err) {
-                       ctx.Error(http.StatusNotFound)
-                       return
-               }
-               ctx.ServerError("RedirectDownload", err)
-               return
-       }
-       if len(releases) == 1 {
-               release := releases[0]
-               att, err := models.GetAttachmentByReleaseIDFileName(release.ID, fileName)
-               if err != nil {
-                       ctx.Error(http.StatusNotFound)
-                       return
-               }
-               if att != nil {
-                       ctx.Redirect(att.DownloadURL())
-                       return
-               }
-       }
-       ctx.Error(http.StatusNotFound)
-}
-
-// Download an archive of a repository
-func Download(ctx *context.Context) {
-       uri := ctx.Params("*")
-       aReq := archiver_service.DeriveRequestFrom(ctx, uri)
-
-       if aReq == nil {
-               ctx.Error(http.StatusNotFound)
-               return
-       }
-
-       downloadName := ctx.Repo.Repository.Name + "-" + aReq.GetArchiveName()
-       complete := aReq.IsComplete()
-       if !complete {
-               aReq = archiver_service.ArchiveRepository(aReq)
-               complete = aReq.WaitForCompletion(ctx)
-       }
-
-       if complete {
-               ctx.ServeFile(aReq.GetArchivePath(), downloadName)
-       } else {
-               ctx.Error(http.StatusNotFound)
-       }
-}
-
-// InitiateDownload will enqueue an archival request, as needed.  It may submit
-// a request that's already in-progress, but the archiver service will just
-// kind of drop it on the floor if this is the case.
-func InitiateDownload(ctx *context.Context) {
-       uri := ctx.Params("*")
-       aReq := archiver_service.DeriveRequestFrom(ctx, uri)
-
-       if aReq == nil {
-               ctx.Error(http.StatusNotFound)
-               return
-       }
-
-       complete := aReq.IsComplete()
-       if !complete {
-               aReq = archiver_service.ArchiveRepository(aReq)
-               complete, _ = aReq.TimedWaitForCompletion(ctx, 2*time.Second)
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "complete": complete,
-       })
-}
diff --git a/routers/repo/search.go b/routers/repo/search.go
deleted file mode 100644 (file)
index d9604ba..0000000
+++ /dev/null
@@ -1,55 +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 repo
-
-import (
-       "net/http"
-       "strings"
-
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       code_indexer "code.gitea.io/gitea/modules/indexer/code"
-       "code.gitea.io/gitea/modules/setting"
-)
-
-const tplSearch base.TplName = "repo/search"
-
-// Search render repository search page
-func Search(ctx *context.Context) {
-       if !setting.Indexer.RepoIndexerEnabled {
-               ctx.Redirect(ctx.Repo.RepoLink, 302)
-               return
-       }
-       language := strings.TrimSpace(ctx.Query("l"))
-       keyword := strings.TrimSpace(ctx.Query("q"))
-       page := ctx.QueryInt("page")
-       if page <= 0 {
-               page = 1
-       }
-       queryType := strings.TrimSpace(ctx.Query("t"))
-       isMatch := queryType == "match"
-
-       total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch([]int64{ctx.Repo.Repository.ID},
-               language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
-       if err != nil {
-               ctx.ServerError("SearchResults", err)
-               return
-       }
-       ctx.Data["Keyword"] = keyword
-       ctx.Data["Language"] = language
-       ctx.Data["queryType"] = queryType
-       ctx.Data["SourcePath"] = ctx.Repo.Repository.HTMLURL()
-       ctx.Data["SearchResults"] = searchResults
-       ctx.Data["SearchResultLanguages"] = searchResultLanguages
-       ctx.Data["RequireHighlightJS"] = true
-       ctx.Data["PageIsViewCode"] = true
-
-       pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
-       pager.SetDefaultParams(ctx)
-       pager.AddParam(ctx, "l", "Language")
-       ctx.Data["Page"] = pager
-
-       ctx.HTML(http.StatusOK, tplSearch)
-}
diff --git a/routers/repo/setting.go b/routers/repo/setting.go
deleted file mode 100644 (file)
index 21a8249..0000000
+++ /dev/null
@@ -1,1053 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2018 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 repo
-
-import (
-       "errors"
-       "fmt"
-       "io/ioutil"
-       "net/http"
-       "strings"
-       "time"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/git"
-       "code.gitea.io/gitea/modules/lfs"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/migrations"
-       "code.gitea.io/gitea/modules/repository"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/structs"
-       "code.gitea.io/gitea/modules/timeutil"
-       "code.gitea.io/gitea/modules/typesniffer"
-       "code.gitea.io/gitea/modules/validation"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/routers/utils"
-       "code.gitea.io/gitea/services/forms"
-       "code.gitea.io/gitea/services/mailer"
-       mirror_service "code.gitea.io/gitea/services/mirror"
-       repo_service "code.gitea.io/gitea/services/repository"
-)
-
-const (
-       tplSettingsOptions base.TplName = "repo/settings/options"
-       tplCollaboration   base.TplName = "repo/settings/collaboration"
-       tplBranches        base.TplName = "repo/settings/branches"
-       tplGithooks        base.TplName = "repo/settings/githooks"
-       tplGithookEdit     base.TplName = "repo/settings/githook_edit"
-       tplDeployKeys      base.TplName = "repo/settings/deploy_keys"
-       tplProtectedBranch base.TplName = "repo/settings/protected_branch"
-)
-
-// Settings show a repository's settings page
-func Settings(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.settings")
-       ctx.Data["PageIsSettingsOptions"] = true
-       ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate
-
-       signing, _ := models.SigningKey(ctx.Repo.Repository.RepoPath())
-       ctx.Data["SigningKeyAvailable"] = len(signing) > 0
-       ctx.Data["SigningSettings"] = setting.Repository.Signing
-
-       ctx.HTML(http.StatusOK, tplSettingsOptions)
-}
-
-// SettingsPost response for changes of a repository
-func SettingsPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.RepoSettingForm)
-       ctx.Data["Title"] = ctx.Tr("repo.settings")
-       ctx.Data["PageIsSettingsOptions"] = true
-
-       repo := ctx.Repo.Repository
-
-       switch ctx.Query("action") {
-       case "update":
-               if ctx.HasError() {
-                       ctx.HTML(http.StatusOK, tplSettingsOptions)
-                       return
-               }
-
-               newRepoName := form.RepoName
-               // Check if repository name has been changed.
-               if repo.LowerName != strings.ToLower(newRepoName) {
-                       // Close the GitRepo if open
-                       if ctx.Repo.GitRepo != nil {
-                               ctx.Repo.GitRepo.Close()
-                               ctx.Repo.GitRepo = nil
-                       }
-                       if err := repo_service.ChangeRepositoryName(ctx.User, repo, newRepoName); err != nil {
-                               ctx.Data["Err_RepoName"] = true
-                               switch {
-                               case models.IsErrRepoAlreadyExist(err):
-                                       ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplSettingsOptions, &form)
-                               case models.IsErrNameReserved(err):
-                                       ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tplSettingsOptions, &form)
-                               case models.IsErrRepoFilesAlreadyExist(err):
-                                       ctx.Data["Err_RepoName"] = true
-                                       switch {
-                                       case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
-                                               ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplSettingsOptions, form)
-                                       case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
-                                               ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplSettingsOptions, form)
-                                       case setting.Repository.AllowDeleteOfUnadoptedRepositories:
-                                               ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form)
-                                       default:
-                                               ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form)
-                                       }
-                               case models.IsErrNamePatternNotAllowed(err):
-                                       ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
-                               default:
-                                       ctx.ServerError("ChangeRepositoryName", err)
-                               }
-                               return
-                       }
-
-                       log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName)
-               }
-               // In case it's just a case change.
-               repo.Name = newRepoName
-               repo.LowerName = strings.ToLower(newRepoName)
-               repo.Description = form.Description
-               repo.Website = form.Website
-               repo.IsTemplate = form.Template
-
-               // Visibility of forked repository is forced sync with base repository.
-               if repo.IsFork {
-                       form.Private = repo.BaseRepo.IsPrivate || repo.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate
-               }
-
-               visibilityChanged := repo.IsPrivate != form.Private
-               // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
-               if visibilityChanged && setting.Repository.ForcePrivate && !form.Private && !ctx.User.IsAdmin {
-                       ctx.ServerError("Force Private enabled", errors.New("cannot change private repository to public"))
-                       return
-               }
-
-               repo.IsPrivate = form.Private
-               if err := models.UpdateRepository(repo, visibilityChanged); err != nil {
-                       ctx.ServerError("UpdateRepository", err)
-                       return
-               }
-               log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
-
-               ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
-               ctx.Redirect(repo.Link() + "/settings")
-
-       case "mirror":
-               if !repo.IsMirror {
-                       ctx.NotFound("", nil)
-                       return
-               }
-
-               // This section doesn't require repo_name/RepoName to be set in the form, don't show it
-               // as an error on the UI for this action
-               ctx.Data["Err_RepoName"] = nil
-
-               interval, err := time.ParseDuration(form.Interval)
-               if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
-                       ctx.Data["Err_Interval"] = true
-                       ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
-               } else {
-                       ctx.Repo.Mirror.EnablePrune = form.EnablePrune
-                       ctx.Repo.Mirror.Interval = interval
-                       if interval != 0 {
-                               ctx.Repo.Mirror.NextUpdateUnix = timeutil.TimeStampNow().AddDuration(interval)
-                       } else {
-                               ctx.Repo.Mirror.NextUpdateUnix = 0
-                       }
-                       if err := models.UpdateMirror(ctx.Repo.Mirror); err != nil {
-                               ctx.Data["Err_Interval"] = true
-                               ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
-                               return
-                       }
-               }
-
-               oldUsername := mirror_service.Username(ctx.Repo.Mirror)
-               oldPassword := mirror_service.Password(ctx.Repo.Mirror)
-               if form.MirrorPassword == "" && form.MirrorUsername == oldUsername {
-                       form.MirrorPassword = oldPassword
-               }
-
-               address, err := forms.ParseRemoteAddr(form.MirrorAddress, form.MirrorUsername, form.MirrorPassword)
-               if err == nil {
-                       err = migrations.IsMigrateURLAllowed(address, ctx.User)
-               }
-               if err != nil {
-                       ctx.Data["Err_MirrorAddress"] = true
-                       handleSettingRemoteAddrError(ctx, err, form)
-                       return
-               }
-
-               if err := mirror_service.UpdateAddress(ctx.Repo.Mirror, address); err != nil {
-                       ctx.ServerError("UpdateAddress", err)
-                       return
-               }
-
-               form.LFS = form.LFS && setting.LFS.StartServer
-
-               if len(form.LFSEndpoint) > 0 {
-                       ep := lfs.DetermineEndpoint("", form.LFSEndpoint)
-                       if ep == nil {
-                               ctx.Data["Err_LFSEndpoint"] = true
-                               ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tplSettingsOptions, &form)
-                               return
-                       }
-                       err = migrations.IsMigrateURLAllowed(ep.String(), ctx.User)
-                       if err != nil {
-                               ctx.Data["Err_LFSEndpoint"] = true
-                               handleSettingRemoteAddrError(ctx, err, form)
-                               return
-                       }
-               }
-
-               ctx.Repo.Mirror.LFS = form.LFS
-               ctx.Repo.Mirror.LFSEndpoint = form.LFSEndpoint
-               if err := models.UpdateMirror(ctx.Repo.Mirror); err != nil {
-                       ctx.ServerError("UpdateMirror", err)
-                       return
-               }
-
-               ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
-               ctx.Redirect(repo.Link() + "/settings")
-
-       case "mirror-sync":
-               if !repo.IsMirror {
-                       ctx.NotFound("", nil)
-                       return
-               }
-
-               mirror_service.StartToMirror(repo.ID)
-
-               ctx.Flash.Info(ctx.Tr("repo.settings.mirror_sync_in_progress"))
-               ctx.Redirect(repo.Link() + "/settings")
-
-       case "advanced":
-               var repoChanged bool
-               var units []models.RepoUnit
-               var deleteUnitTypes []models.UnitType
-
-               // This section doesn't require repo_name/RepoName to be set in the form, don't show it
-               // as an error on the UI for this action
-               ctx.Data["Err_RepoName"] = nil
-
-               if repo.CloseIssuesViaCommitInAnyBranch != form.EnableCloseIssuesViaCommitInAnyBranch {
-                       repo.CloseIssuesViaCommitInAnyBranch = form.EnableCloseIssuesViaCommitInAnyBranch
-                       repoChanged = true
-               }
-
-               if form.EnableWiki && form.EnableExternalWiki && !models.UnitTypeExternalWiki.UnitGlobalDisabled() {
-                       if !validation.IsValidExternalURL(form.ExternalWikiURL) {
-                               ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error"))
-                               ctx.Redirect(repo.Link() + "/settings")
-                               return
-                       }
-
-                       units = append(units, models.RepoUnit{
-                               RepoID: repo.ID,
-                               Type:   models.UnitTypeExternalWiki,
-                               Config: &models.ExternalWikiConfig{
-                                       ExternalWikiURL: form.ExternalWikiURL,
-                               },
-                       })
-                       deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeWiki)
-               } else if form.EnableWiki && !form.EnableExternalWiki && !models.UnitTypeWiki.UnitGlobalDisabled() {
-                       units = append(units, models.RepoUnit{
-                               RepoID: repo.ID,
-                               Type:   models.UnitTypeWiki,
-                               Config: new(models.UnitConfig),
-                       })
-                       deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalWiki)
-               } else {
-                       if !models.UnitTypeExternalWiki.UnitGlobalDisabled() {
-                               deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalWiki)
-                       }
-                       if !models.UnitTypeWiki.UnitGlobalDisabled() {
-                               deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeWiki)
-                       }
-               }
-
-               if form.EnableIssues && form.EnableExternalTracker && !models.UnitTypeExternalTracker.UnitGlobalDisabled() {
-                       if !validation.IsValidExternalURL(form.ExternalTrackerURL) {
-                               ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error"))
-                               ctx.Redirect(repo.Link() + "/settings")
-                               return
-                       }
-                       if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(form.TrackerURLFormat) {
-                               ctx.Flash.Error(ctx.Tr("repo.settings.tracker_url_format_error"))
-                               ctx.Redirect(repo.Link() + "/settings")
-                               return
-                       }
-                       units = append(units, models.RepoUnit{
-                               RepoID: repo.ID,
-                               Type:   models.UnitTypeExternalTracker,
-                               Config: &models.ExternalTrackerConfig{
-                                       ExternalTrackerURL:    form.ExternalTrackerURL,
-                                       ExternalTrackerFormat: form.TrackerURLFormat,
-                                       ExternalTrackerStyle:  form.TrackerIssueStyle,
-                               },
-                       })
-                       deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeIssues)
-               } else if form.EnableIssues && !form.EnableExternalTracker && !models.UnitTypeIssues.UnitGlobalDisabled() {
-                       units = append(units, models.RepoUnit{
-                               RepoID: repo.ID,
-                               Type:   models.UnitTypeIssues,
-                               Config: &models.IssuesConfig{
-                                       EnableTimetracker:                form.EnableTimetracker,
-                                       AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
-                                       EnableDependencies:               form.EnableIssueDependencies,
-                               },
-                       })
-                       deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalTracker)
-               } else {
-                       if !models.UnitTypeExternalTracker.UnitGlobalDisabled() {
-                               deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalTracker)
-                       }
-                       if !models.UnitTypeIssues.UnitGlobalDisabled() {
-                               deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeIssues)
-                       }
-               }
-
-               if form.EnableProjects && !models.UnitTypeProjects.UnitGlobalDisabled() {
-                       units = append(units, models.RepoUnit{
-                               RepoID: repo.ID,
-                               Type:   models.UnitTypeProjects,
-                       })
-               } else if !models.UnitTypeProjects.UnitGlobalDisabled() {
-                       deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeProjects)
-               }
-
-               if form.EnablePulls && !models.UnitTypePullRequests.UnitGlobalDisabled() {
-                       units = append(units, models.RepoUnit{
-                               RepoID: repo.ID,
-                               Type:   models.UnitTypePullRequests,
-                               Config: &models.PullRequestsConfig{
-                                       IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace,
-                                       AllowMerge:                form.PullsAllowMerge,
-                                       AllowRebase:               form.PullsAllowRebase,
-                                       AllowRebaseMerge:          form.PullsAllowRebaseMerge,
-                                       AllowSquash:               form.PullsAllowSquash,
-                                       AllowManualMerge:          form.PullsAllowManualMerge,
-                                       AutodetectManualMerge:     form.EnableAutodetectManualMerge,
-                                       DefaultMergeStyle:         models.MergeStyle(form.PullsDefaultMergeStyle),
-                               },
-                       })
-               } else if !models.UnitTypePullRequests.UnitGlobalDisabled() {
-                       deleteUnitTypes = append(deleteUnitTypes, models.UnitTypePullRequests)
-               }
-
-               if err := models.UpdateRepositoryUnits(repo, units, deleteUnitTypes); err != nil {
-                       ctx.ServerError("UpdateRepositoryUnits", err)
-                       return
-               }
-               if repoChanged {
-                       if err := models.UpdateRepository(repo, false); err != nil {
-                               ctx.ServerError("UpdateRepository", err)
-                               return
-                       }
-               }
-               log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
-
-               ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
-               ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-
-       case "signing":
-               changed := false
-
-               trustModel := models.ToTrustModel(form.TrustModel)
-               if trustModel != repo.TrustModel {
-                       repo.TrustModel = trustModel
-                       changed = true
-               }
-
-               if changed {
-                       if err := models.UpdateRepository(repo, false); err != nil {
-                               ctx.ServerError("UpdateRepository", err)
-                               return
-                       }
-               }
-               log.Trace("Repository signing settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
-
-               ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
-               ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-
-       case "admin":
-               if !ctx.User.IsAdmin {
-                       ctx.Error(http.StatusForbidden)
-                       return
-               }
-
-               if repo.IsFsckEnabled != form.EnableHealthCheck {
-                       repo.IsFsckEnabled = form.EnableHealthCheck
-               }
-
-               if err := models.UpdateRepository(repo, false); err != nil {
-                       ctx.ServerError("UpdateRepository", err)
-                       return
-               }
-
-               log.Trace("Repository admin settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
-
-               ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
-               ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-
-       case "convert":
-               if !ctx.Repo.IsOwner() {
-                       ctx.Error(http.StatusNotFound)
-                       return
-               }
-               if repo.Name != form.RepoName {
-                       ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
-                       return
-               }
-
-               if !repo.IsMirror {
-                       ctx.Error(http.StatusNotFound)
-                       return
-               }
-               repo.IsMirror = false
-
-               if _, err := repository.CleanUpMigrateInfo(repo); err != nil {
-                       ctx.ServerError("CleanUpMigrateInfo", err)
-                       return
-               } else if err = models.DeleteMirrorByRepoID(ctx.Repo.Repository.ID); err != nil {
-                       ctx.ServerError("DeleteMirrorByRepoID", err)
-                       return
-               }
-               log.Trace("Repository converted from mirror to regular: %s", repo.FullName())
-               ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed"))
-               ctx.Redirect(repo.Link())
-
-       case "convert_fork":
-               if !ctx.Repo.IsOwner() {
-                       ctx.Error(http.StatusNotFound)
-                       return
-               }
-               if err := repo.GetOwner(); err != nil {
-                       ctx.ServerError("Convert Fork", err)
-                       return
-               }
-               if repo.Name != form.RepoName {
-                       ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
-                       return
-               }
-
-               if !repo.IsFork {
-                       ctx.Error(http.StatusNotFound)
-                       return
-               }
-
-               if !ctx.Repo.Owner.CanCreateRepo() {
-                       ctx.Flash.Error(ctx.Tr("repo.form.reach_limit_of_creation", ctx.User.MaxCreationLimit()))
-                       ctx.Redirect(repo.Link() + "/settings")
-                       return
-               }
-
-               repo.IsFork = false
-               repo.ForkID = 0
-               if err := models.UpdateRepository(repo, false); err != nil {
-                       log.Error("Unable to update repository %-v whilst converting from fork", repo)
-                       ctx.ServerError("Convert Fork", err)
-                       return
-               }
-
-               log.Trace("Repository converted from fork to regular: %s", repo.FullName())
-               ctx.Flash.Success(ctx.Tr("repo.settings.convert_fork_succeed"))
-               ctx.Redirect(repo.Link())
-
-       case "transfer":
-               if !ctx.Repo.IsOwner() {
-                       ctx.Error(http.StatusNotFound)
-                       return
-               }
-               if repo.Name != form.RepoName {
-                       ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
-                       return
-               }
-
-               newOwner, err := models.GetUserByName(ctx.Query("new_owner_name"))
-               if err != nil {
-                       if models.IsErrUserNotExist(err) {
-                               ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
-                               return
-                       }
-                       ctx.ServerError("IsUserExist", err)
-                       return
-               }
-
-               if newOwner.Type == models.UserTypeOrganization {
-                       if !ctx.User.IsAdmin && newOwner.Visibility == structs.VisibleTypePrivate && !newOwner.HasMemberWithUserID(ctx.User.ID) {
-                               // The user shouldn't know about this organization
-                               ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
-                               return
-                       }
-               }
-
-               // Close the GitRepo if open
-               if ctx.Repo.GitRepo != nil {
-                       ctx.Repo.GitRepo.Close()
-                       ctx.Repo.GitRepo = nil
-               }
-
-               if err := repo_service.StartRepositoryTransfer(ctx.User, newOwner, repo, nil); err != nil {
-                       if models.IsErrRepoAlreadyExist(err) {
-                               ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil)
-                       } else if models.IsErrRepoTransferInProgress(err) {
-                               ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil)
-                       } else {
-                               ctx.ServerError("TransferOwnership", err)
-                       }
-
-                       return
-               }
-
-               log.Trace("Repository transfer process was started: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner)
-               ctx.Flash.Success(ctx.Tr("repo.settings.transfer_started", newOwner.DisplayName()))
-               ctx.Redirect(ctx.Repo.Owner.HomeLink() + "/" + repo.Name + "/settings")
-
-       case "cancel_transfer":
-               if !ctx.Repo.IsOwner() {
-                       ctx.Error(http.StatusNotFound)
-                       return
-               }
-
-               repoTransfer, err := models.GetPendingRepositoryTransfer(ctx.Repo.Repository)
-               if err != nil {
-                       if models.IsErrNoPendingTransfer(err) {
-                               ctx.Flash.Error("repo.settings.transfer_abort_invalid")
-                               ctx.Redirect(ctx.User.HomeLink() + "/" + repo.Name + "/settings")
-                       } else {
-                               ctx.ServerError("GetPendingRepositoryTransfer", err)
-                       }
-
-                       return
-               }
-
-               if err := repoTransfer.LoadAttributes(); err != nil {
-                       ctx.ServerError("LoadRecipient", err)
-                       return
-               }
-
-               if err := models.CancelRepositoryTransfer(ctx.Repo.Repository); err != nil {
-                       ctx.ServerError("CancelRepositoryTransfer", err)
-                       return
-               }
-
-               log.Trace("Repository transfer process was cancelled: %s/%s ", ctx.Repo.Owner.Name, repo.Name)
-               ctx.Flash.Success(ctx.Tr("repo.settings.transfer_abort_success", repoTransfer.Recipient.Name))
-               ctx.Redirect(ctx.Repo.Owner.HomeLink() + "/" + repo.Name + "/settings")
-
-       case "delete":
-               if !ctx.Repo.IsOwner() {
-                       ctx.Error(http.StatusNotFound)
-                       return
-               }
-               if repo.Name != form.RepoName {
-                       ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
-                       return
-               }
-
-               // Close the gitrepository before doing this.
-               if ctx.Repo.GitRepo != nil {
-                       ctx.Repo.GitRepo.Close()
-               }
-
-               if err := repo_service.DeleteRepository(ctx.User, ctx.Repo.Repository); err != nil {
-                       ctx.ServerError("DeleteRepository", err)
-                       return
-               }
-               log.Trace("Repository deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
-
-               ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success"))
-               ctx.Redirect(ctx.Repo.Owner.DashboardLink())
-
-       case "delete-wiki":
-               if !ctx.Repo.IsOwner() {
-                       ctx.Error(http.StatusNotFound)
-                       return
-               }
-               if repo.Name != form.RepoName {
-                       ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
-                       return
-               }
-
-               err := repo.DeleteWiki()
-               if err != nil {
-                       log.Error("Delete Wiki: %v", err.Error())
-               }
-               log.Trace("Repository wiki deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
-
-               ctx.Flash.Success(ctx.Tr("repo.settings.wiki_deletion_success"))
-               ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-
-       case "archive":
-               if !ctx.Repo.IsOwner() {
-                       ctx.Error(http.StatusForbidden)
-                       return
-               }
-
-               if repo.IsMirror {
-                       ctx.Flash.Error(ctx.Tr("repo.settings.archive.error_ismirror"))
-                       ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-                       return
-               }
-
-               if err := repo.SetArchiveRepoState(true); err != nil {
-                       log.Error("Tried to archive a repo: %s", err)
-                       ctx.Flash.Error(ctx.Tr("repo.settings.archive.error"))
-                       ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-                       return
-               }
-
-               ctx.Flash.Success(ctx.Tr("repo.settings.archive.success"))
-
-               log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
-               ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-       case "unarchive":
-               if !ctx.Repo.IsOwner() {
-                       ctx.Error(http.StatusForbidden)
-                       return
-               }
-
-               if err := repo.SetArchiveRepoState(false); err != nil {
-                       log.Error("Tried to unarchive a repo: %s", err)
-                       ctx.Flash.Error(ctx.Tr("repo.settings.unarchive.error"))
-                       ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-                       return
-               }
-
-               ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success"))
-
-               log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
-               ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-
-       default:
-               ctx.NotFound("", nil)
-       }
-}
-
-func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.RepoSettingForm) {
-       if models.IsErrInvalidCloneAddr(err) {
-               addrErr := err.(*models.ErrInvalidCloneAddr)
-               switch {
-               case addrErr.IsProtocolInvalid:
-                       ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tplSettingsOptions, form)
-               case addrErr.IsURLError:
-                       ctx.RenderWithErr(ctx.Tr("form.url_error"), tplSettingsOptions, form)
-               case addrErr.IsPermissionDenied:
-                       if addrErr.LocalPath {
-                               ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tplSettingsOptions, form)
-                       } else if len(addrErr.PrivateNet) == 0 {
-                               ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_blocked"), tplSettingsOptions, form)
-                       } else {
-                               ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_private_ip"), tplSettingsOptions, form)
-                       }
-               case addrErr.IsInvalidPath:
-                       ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tplSettingsOptions, form)
-               default:
-                       ctx.ServerError("Unknown error", err)
-               }
-       }
-       ctx.RenderWithErr(ctx.Tr("repo.mirror_address_url_invalid"), tplSettingsOptions, form)
-}
-
-// Collaboration render a repository's collaboration page
-func Collaboration(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.settings")
-       ctx.Data["PageIsSettingsCollaboration"] = true
-
-       users, err := ctx.Repo.Repository.GetCollaborators(models.ListOptions{})
-       if err != nil {
-               ctx.ServerError("GetCollaborators", err)
-               return
-       }
-       ctx.Data["Collaborators"] = users
-
-       teams, err := ctx.Repo.Repository.GetRepoTeams()
-       if err != nil {
-               ctx.ServerError("GetRepoTeams", err)
-               return
-       }
-       ctx.Data["Teams"] = teams
-       ctx.Data["Repo"] = ctx.Repo.Repository
-       ctx.Data["OrgID"] = ctx.Repo.Repository.OwnerID
-       ctx.Data["OrgName"] = ctx.Repo.Repository.OwnerName
-       ctx.Data["Org"] = ctx.Repo.Repository.Owner
-       ctx.Data["Units"] = models.Units
-
-       ctx.HTML(http.StatusOK, tplCollaboration)
-}
-
-// CollaborationPost response for actions for a collaboration of a repository
-func CollaborationPost(ctx *context.Context) {
-       name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.Query("collaborator")))
-       if len(name) == 0 || ctx.Repo.Owner.LowerName == name {
-               ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
-               return
-       }
-
-       u, err := models.GetUserByName(name)
-       if err != nil {
-               if models.IsErrUserNotExist(err) {
-                       ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
-                       ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
-               } else {
-                       ctx.ServerError("GetUserByName", err)
-               }
-               return
-       }
-
-       if !u.IsActive {
-               ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator_inactive_user"))
-               ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
-               return
-       }
-
-       // Organization is not allowed to be added as a collaborator.
-       if u.IsOrganization() {
-               ctx.Flash.Error(ctx.Tr("repo.settings.org_not_allowed_to_be_collaborator"))
-               ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
-               return
-       }
-
-       if got, err := ctx.Repo.Repository.IsCollaborator(u.ID); err == nil && got {
-               ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator_duplicate"))
-               ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
-               return
-       }
-
-       if err = ctx.Repo.Repository.AddCollaborator(u); err != nil {
-               ctx.ServerError("AddCollaborator", err)
-               return
-       }
-
-       if setting.Service.EnableNotifyMail {
-               mailer.SendCollaboratorMail(u, ctx.User, ctx.Repo.Repository)
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.settings.add_collaborator_success"))
-       ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
-}
-
-// ChangeCollaborationAccessMode response for changing access of a collaboration
-func ChangeCollaborationAccessMode(ctx *context.Context) {
-       if err := ctx.Repo.Repository.ChangeCollaborationAccessMode(
-               ctx.QueryInt64("uid"),
-               models.AccessMode(ctx.QueryInt("mode"))); err != nil {
-               log.Error("ChangeCollaborationAccessMode: %v", err)
-       }
-}
-
-// DeleteCollaboration delete a collaboration for a repository
-func DeleteCollaboration(ctx *context.Context) {
-       if err := ctx.Repo.Repository.DeleteCollaboration(ctx.QueryInt64("id")); err != nil {
-               ctx.Flash.Error("DeleteCollaboration: " + err.Error())
-       } else {
-               ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": ctx.Repo.RepoLink + "/settings/collaboration",
-       })
-}
-
-// AddTeamPost response for adding a team to a repository
-func AddTeamPost(ctx *context.Context) {
-       if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() {
-               ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed"))
-               ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
-               return
-       }
-
-       name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.Query("team")))
-       if len(name) == 0 {
-               ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
-               return
-       }
-
-       team, err := ctx.Repo.Owner.GetTeam(name)
-       if err != nil {
-               if models.IsErrTeamNotExist(err) {
-                       ctx.Flash.Error(ctx.Tr("form.team_not_exist"))
-                       ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
-               } else {
-                       ctx.ServerError("GetTeam", err)
-               }
-               return
-       }
-
-       if team.OrgID != ctx.Repo.Repository.OwnerID {
-               ctx.Flash.Error(ctx.Tr("repo.settings.team_not_in_organization"))
-               ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
-               return
-       }
-
-       if models.HasTeamRepo(ctx.Repo.Repository.OwnerID, team.ID, ctx.Repo.Repository.ID) {
-               ctx.Flash.Error(ctx.Tr("repo.settings.add_team_duplicate"))
-               ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
-               return
-       }
-
-       if err = team.AddRepository(ctx.Repo.Repository); err != nil {
-               ctx.ServerError("team.AddRepository", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.settings.add_team_success"))
-       ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
-}
-
-// DeleteTeam response for deleting a team from a repository
-func DeleteTeam(ctx *context.Context) {
-       if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() {
-               ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed"))
-               ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
-               return
-       }
-
-       team, err := models.GetTeamByID(ctx.QueryInt64("id"))
-       if err != nil {
-               ctx.ServerError("GetTeamByID", err)
-               return
-       }
-
-       if err = team.RemoveRepository(ctx.Repo.Repository.ID); err != nil {
-               ctx.ServerError("team.RemoveRepositorys", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.settings.remove_team_success"))
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": ctx.Repo.RepoLink + "/settings/collaboration",
-       })
-}
-
-// parseOwnerAndRepo get repos by owner
-func parseOwnerAndRepo(ctx *context.Context) (*models.User, *models.Repository) {
-       owner, err := models.GetUserByName(ctx.Params(":username"))
-       if err != nil {
-               if models.IsErrUserNotExist(err) {
-                       ctx.NotFound("GetUserByName", err)
-               } else {
-                       ctx.ServerError("GetUserByName", err)
-               }
-               return nil, nil
-       }
-
-       repo, err := models.GetRepositoryByName(owner.ID, ctx.Params(":reponame"))
-       if err != nil {
-               if models.IsErrRepoNotExist(err) {
-                       ctx.NotFound("GetRepositoryByName", err)
-               } else {
-                       ctx.ServerError("GetRepositoryByName", err)
-               }
-               return nil, nil
-       }
-
-       return owner, repo
-}
-
-// GitHooks hooks of a repository
-func GitHooks(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.settings.githooks")
-       ctx.Data["PageIsSettingsGitHooks"] = true
-
-       hooks, err := ctx.Repo.GitRepo.Hooks()
-       if err != nil {
-               ctx.ServerError("Hooks", err)
-               return
-       }
-       ctx.Data["Hooks"] = hooks
-
-       ctx.HTML(http.StatusOK, tplGithooks)
-}
-
-// GitHooksEdit render for editing a hook of repository page
-func GitHooksEdit(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.settings.githooks")
-       ctx.Data["PageIsSettingsGitHooks"] = true
-
-       name := ctx.Params(":name")
-       hook, err := ctx.Repo.GitRepo.GetHook(name)
-       if err != nil {
-               if err == git.ErrNotValidHook {
-                       ctx.NotFound("GetHook", err)
-               } else {
-                       ctx.ServerError("GetHook", err)
-               }
-               return
-       }
-       ctx.Data["Hook"] = hook
-       ctx.HTML(http.StatusOK, tplGithookEdit)
-}
-
-// GitHooksEditPost response for editing a git hook of a repository
-func GitHooksEditPost(ctx *context.Context) {
-       name := ctx.Params(":name")
-       hook, err := ctx.Repo.GitRepo.GetHook(name)
-       if err != nil {
-               if err == git.ErrNotValidHook {
-                       ctx.NotFound("GetHook", err)
-               } else {
-                       ctx.ServerError("GetHook", err)
-               }
-               return
-       }
-       hook.Content = ctx.Query("content")
-       if err = hook.Update(); err != nil {
-               ctx.ServerError("hook.Update", err)
-               return
-       }
-       ctx.Redirect(ctx.Repo.RepoLink + "/settings/hooks/git")
-}
-
-// DeployKeys render the deploy keys list of a repository page
-func DeployKeys(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys")
-       ctx.Data["PageIsSettingsKeys"] = true
-       ctx.Data["DisableSSH"] = setting.SSH.Disabled
-
-       keys, err := models.ListDeployKeys(ctx.Repo.Repository.ID, models.ListOptions{})
-       if err != nil {
-               ctx.ServerError("ListDeployKeys", err)
-               return
-       }
-       ctx.Data["Deploykeys"] = keys
-
-       ctx.HTML(http.StatusOK, tplDeployKeys)
-}
-
-// DeployKeysPost response for adding a deploy key of a repository
-func DeployKeysPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.AddKeyForm)
-       ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys")
-       ctx.Data["PageIsSettingsKeys"] = true
-
-       keys, err := models.ListDeployKeys(ctx.Repo.Repository.ID, models.ListOptions{})
-       if err != nil {
-               ctx.ServerError("ListDeployKeys", err)
-               return
-       }
-       ctx.Data["Deploykeys"] = keys
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplDeployKeys)
-               return
-       }
-
-       content, err := models.CheckPublicKeyString(form.Content)
-       if err != nil {
-               if models.IsErrSSHDisabled(err) {
-                       ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
-               } else if models.IsErrKeyUnableVerify(err) {
-                       ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
-               } else {
-                       ctx.Data["HasError"] = true
-                       ctx.Data["Err_Content"] = true
-                       ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error()))
-               }
-               ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
-               return
-       }
-
-       key, err := models.AddDeployKey(ctx.Repo.Repository.ID, form.Title, content, !form.IsWritable)
-       if err != nil {
-               ctx.Data["HasError"] = true
-               switch {
-               case models.IsErrDeployKeyAlreadyExist(err):
-                       ctx.Data["Err_Content"] = true
-                       ctx.RenderWithErr(ctx.Tr("repo.settings.key_been_used"), tplDeployKeys, &form)
-               case models.IsErrKeyAlreadyExist(err):
-                       ctx.Data["Err_Content"] = true
-                       ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplDeployKeys, &form)
-               case models.IsErrKeyNameAlreadyUsed(err):
-                       ctx.Data["Err_Title"] = true
-                       ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
-               case models.IsErrDeployKeyNameAlreadyUsed(err):
-                       ctx.Data["Err_Title"] = true
-                       ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
-               default:
-                       ctx.ServerError("AddDeployKey", err)
-               }
-               return
-       }
-
-       log.Trace("Deploy key added: %d", ctx.Repo.Repository.ID)
-       ctx.Flash.Success(ctx.Tr("repo.settings.add_key_success", key.Name))
-       ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
-}
-
-// DeleteDeployKey response for deleting a deploy key
-func DeleteDeployKey(ctx *context.Context) {
-       if err := models.DeleteDeployKey(ctx.User, ctx.QueryInt64("id")); err != nil {
-               ctx.Flash.Error("DeleteDeployKey: " + err.Error())
-       } else {
-               ctx.Flash.Success(ctx.Tr("repo.settings.deploy_key_deletion_success"))
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": ctx.Repo.RepoLink + "/settings/keys",
-       })
-}
-
-// UpdateAvatarSetting update repo's avatar
-func UpdateAvatarSetting(ctx *context.Context, form forms.AvatarForm) error {
-       ctxRepo := ctx.Repo.Repository
-
-       if form.Avatar == nil {
-               // No avatar is uploaded and we not removing it here.
-               // No random avatar generated here.
-               // Just exit, no action.
-               if ctxRepo.CustomAvatarRelativePath() == "" {
-                       log.Trace("No avatar was uploaded for repo: %d. Default icon will appear instead.", ctxRepo.ID)
-               }
-               return nil
-       }
-
-       r, err := form.Avatar.Open()
-       if err != nil {
-               return fmt.Errorf("Avatar.Open: %v", err)
-       }
-       defer r.Close()
-
-       if form.Avatar.Size > setting.Avatar.MaxFileSize {
-               return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big"))
-       }
-
-       data, err := ioutil.ReadAll(r)
-       if err != nil {
-               return fmt.Errorf("ioutil.ReadAll: %v", err)
-       }
-       st := typesniffer.DetectContentType(data)
-       if !(st.IsImage() && !st.IsSvgImage()) {
-               return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image"))
-       }
-       if err = ctxRepo.UploadAvatar(data); err != nil {
-               return fmt.Errorf("UploadAvatar: %v", err)
-       }
-       return nil
-}
-
-// SettingsAvatar save new POSTed repository avatar
-func SettingsAvatar(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.AvatarForm)
-       form.Source = forms.AvatarLocal
-       if err := UpdateAvatarSetting(ctx, *form); err != nil {
-               ctx.Flash.Error(err.Error())
-       } else {
-               ctx.Flash.Success(ctx.Tr("repo.settings.update_avatar_success"))
-       }
-       ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-}
-
-// SettingsDeleteAvatar delete repository avatar
-func SettingsDeleteAvatar(ctx *context.Context) {
-       if err := ctx.Repo.Repository.DeleteAvatar(); err != nil {
-               ctx.Flash.Error(fmt.Sprintf("DeleteAvatar: %v", err))
-       }
-       ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-}
diff --git a/routers/repo/setting_protected_branch.go b/routers/repo/setting_protected_branch.go
deleted file mode 100644 (file)
index fba2c09..0000000
+++ /dev/null
@@ -1,286 +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 repo
-
-import (
-       "fmt"
-       "net/http"
-       "strings"
-       "time"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/git"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-       pull_service "code.gitea.io/gitea/services/pull"
-)
-
-// ProtectedBranch render the page to protect the repository
-func ProtectedBranch(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.settings")
-       ctx.Data["PageIsSettingsBranches"] = true
-
-       protectedBranches, err := ctx.Repo.Repository.GetProtectedBranches()
-       if err != nil {
-               ctx.ServerError("GetProtectedBranches", err)
-               return
-       }
-       ctx.Data["ProtectedBranches"] = protectedBranches
-
-       branches := ctx.Data["Branches"].([]string)
-       leftBranches := make([]string, 0, len(branches)-len(protectedBranches))
-       for _, b := range branches {
-               var protected bool
-               for _, pb := range protectedBranches {
-                       if b == pb.BranchName {
-                               protected = true
-                               break
-                       }
-               }
-               if !protected {
-                       leftBranches = append(leftBranches, b)
-               }
-       }
-
-       ctx.Data["LeftBranches"] = leftBranches
-
-       ctx.HTML(http.StatusOK, tplBranches)
-}
-
-// ProtectedBranchPost response for protect for a branch of a repository
-func ProtectedBranchPost(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.settings")
-       ctx.Data["PageIsSettingsBranches"] = true
-
-       repo := ctx.Repo.Repository
-
-       switch ctx.Query("action") {
-       case "default_branch":
-               if ctx.HasError() {
-                       ctx.HTML(http.StatusOK, tplBranches)
-                       return
-               }
-
-               branch := ctx.Query("branch")
-               if !ctx.Repo.GitRepo.IsBranchExist(branch) {
-                       ctx.Status(404)
-                       return
-               } else if repo.DefaultBranch != branch {
-                       repo.DefaultBranch = branch
-                       if err := ctx.Repo.GitRepo.SetDefaultBranch(branch); err != nil {
-                               if !git.IsErrUnsupportedVersion(err) {
-                                       ctx.ServerError("SetDefaultBranch", err)
-                                       return
-                               }
-                       }
-                       if err := repo.UpdateDefaultBranch(); err != nil {
-                               ctx.ServerError("SetDefaultBranch", err)
-                               return
-                       }
-               }
-
-               log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
-
-               ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
-               ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
-       default:
-               ctx.NotFound("", nil)
-       }
-}
-
-// SettingsProtectedBranch renders the protected branch setting page
-func SettingsProtectedBranch(c *context.Context) {
-       branch := c.Params("*")
-       if !c.Repo.GitRepo.IsBranchExist(branch) {
-               c.NotFound("IsBranchExist", nil)
-               return
-       }
-
-       c.Data["Title"] = c.Tr("repo.settings.protected_branch") + " - " + branch
-       c.Data["PageIsSettingsBranches"] = true
-
-       protectBranch, err := models.GetProtectedBranchBy(c.Repo.Repository.ID, branch)
-       if err != nil {
-               if !git.IsErrBranchNotExist(err) {
-                       c.ServerError("GetProtectBranchOfRepoByName", err)
-                       return
-               }
-       }
-
-       if protectBranch == nil {
-               // No options found, create defaults.
-               protectBranch = &models.ProtectedBranch{
-                       BranchName: branch,
-               }
-       }
-
-       users, err := c.Repo.Repository.GetReaders()
-       if err != nil {
-               c.ServerError("Repo.Repository.GetReaders", err)
-               return
-       }
-       c.Data["Users"] = users
-       c.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistUserIDs), ",")
-       c.Data["merge_whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.MergeWhitelistUserIDs), ",")
-       c.Data["approvals_whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.ApprovalsWhitelistUserIDs), ",")
-       contexts, _ := models.FindRepoRecentCommitStatusContexts(c.Repo.Repository.ID, 7*24*time.Hour) // Find last week status check contexts
-       for _, ctx := range protectBranch.StatusCheckContexts {
-               var found bool
-               for i := range contexts {
-                       if contexts[i] == ctx {
-                               found = true
-                               break
-                       }
-               }
-               if !found {
-                       contexts = append(contexts, ctx)
-               }
-       }
-
-       c.Data["branch_status_check_contexts"] = contexts
-       c.Data["is_context_required"] = func(context string) bool {
-               for _, c := range protectBranch.StatusCheckContexts {
-                       if c == context {
-                               return true
-                       }
-               }
-               return false
-       }
-
-       if c.Repo.Owner.IsOrganization() {
-               teams, err := c.Repo.Owner.TeamsWithAccessToRepo(c.Repo.Repository.ID, models.AccessModeRead)
-               if err != nil {
-                       c.ServerError("Repo.Owner.TeamsWithAccessToRepo", err)
-                       return
-               }
-               c.Data["Teams"] = teams
-               c.Data["whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistTeamIDs), ",")
-               c.Data["merge_whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.MergeWhitelistTeamIDs), ",")
-               c.Data["approvals_whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.ApprovalsWhitelistTeamIDs), ",")
-       }
-
-       c.Data["Branch"] = protectBranch
-       c.HTML(http.StatusOK, tplProtectedBranch)
-}
-
-// SettingsProtectedBranchPost updates the protected branch settings
-func SettingsProtectedBranchPost(ctx *context.Context) {
-       f := web.GetForm(ctx).(*forms.ProtectBranchForm)
-       branch := ctx.Params("*")
-       if !ctx.Repo.GitRepo.IsBranchExist(branch) {
-               ctx.NotFound("IsBranchExist", nil)
-               return
-       }
-
-       protectBranch, err := models.GetProtectedBranchBy(ctx.Repo.Repository.ID, branch)
-       if err != nil {
-               if !git.IsErrBranchNotExist(err) {
-                       ctx.ServerError("GetProtectBranchOfRepoByName", err)
-                       return
-               }
-       }
-
-       if f.Protected {
-               if protectBranch == nil {
-                       // No options found, create defaults.
-                       protectBranch = &models.ProtectedBranch{
-                               RepoID:     ctx.Repo.Repository.ID,
-                               BranchName: branch,
-                       }
-               }
-               if f.RequiredApprovals < 0 {
-                       ctx.Flash.Error(ctx.Tr("repo.settings.protected_branch_required_approvals_min"))
-                       ctx.Redirect(fmt.Sprintf("%s/settings/branches/%s", ctx.Repo.RepoLink, branch))
-               }
-
-               var whitelistUsers, whitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams []int64
-               switch f.EnablePush {
-               case "all":
-                       protectBranch.CanPush = true
-                       protectBranch.EnableWhitelist = false
-                       protectBranch.WhitelistDeployKeys = false
-               case "whitelist":
-                       protectBranch.CanPush = true
-                       protectBranch.EnableWhitelist = true
-                       protectBranch.WhitelistDeployKeys = f.WhitelistDeployKeys
-                       if strings.TrimSpace(f.WhitelistUsers) != "" {
-                               whitelistUsers, _ = base.StringsToInt64s(strings.Split(f.WhitelistUsers, ","))
-                       }
-                       if strings.TrimSpace(f.WhitelistTeams) != "" {
-                               whitelistTeams, _ = base.StringsToInt64s(strings.Split(f.WhitelistTeams, ","))
-                       }
-               default:
-                       protectBranch.CanPush = false
-                       protectBranch.EnableWhitelist = false
-                       protectBranch.WhitelistDeployKeys = false
-               }
-
-               protectBranch.EnableMergeWhitelist = f.EnableMergeWhitelist
-               if f.EnableMergeWhitelist {
-                       if strings.TrimSpace(f.MergeWhitelistUsers) != "" {
-                               mergeWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistUsers, ","))
-                       }
-                       if strings.TrimSpace(f.MergeWhitelistTeams) != "" {
-                               mergeWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistTeams, ","))
-                       }
-               }
-
-               protectBranch.EnableStatusCheck = f.EnableStatusCheck
-               if f.EnableStatusCheck {
-                       protectBranch.StatusCheckContexts = f.StatusCheckContexts
-               } else {
-                       protectBranch.StatusCheckContexts = nil
-               }
-
-               protectBranch.RequiredApprovals = f.RequiredApprovals
-               protectBranch.EnableApprovalsWhitelist = f.EnableApprovalsWhitelist
-               if f.EnableApprovalsWhitelist {
-                       if strings.TrimSpace(f.ApprovalsWhitelistUsers) != "" {
-                               approvalsWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistUsers, ","))
-                       }
-                       if strings.TrimSpace(f.ApprovalsWhitelistTeams) != "" {
-                               approvalsWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistTeams, ","))
-                       }
-               }
-               protectBranch.BlockOnRejectedReviews = f.BlockOnRejectedReviews
-               protectBranch.BlockOnOfficialReviewRequests = f.BlockOnOfficialReviewRequests
-               protectBranch.DismissStaleApprovals = f.DismissStaleApprovals
-               protectBranch.RequireSignedCommits = f.RequireSignedCommits
-               protectBranch.ProtectedFilePatterns = f.ProtectedFilePatterns
-               protectBranch.BlockOnOutdatedBranch = f.BlockOnOutdatedBranch
-
-               err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{
-                       UserIDs:          whitelistUsers,
-                       TeamIDs:          whitelistTeams,
-                       MergeUserIDs:     mergeWhitelistUsers,
-                       MergeTeamIDs:     mergeWhitelistTeams,
-                       ApprovalsUserIDs: approvalsWhitelistUsers,
-                       ApprovalsTeamIDs: approvalsWhitelistTeams,
-               })
-               if err != nil {
-                       ctx.ServerError("UpdateProtectBranch", err)
-                       return
-               }
-               if err = pull_service.CheckPrsForBaseBranch(ctx.Repo.Repository, protectBranch.BranchName); err != nil {
-                       ctx.ServerError("CheckPrsForBaseBranch", err)
-                       return
-               }
-               ctx.Flash.Success(ctx.Tr("repo.settings.update_protect_branch_success", branch))
-               ctx.Redirect(fmt.Sprintf("%s/settings/branches/%s", ctx.Repo.RepoLink, branch))
-       } else {
-               if protectBranch != nil {
-                       if err := ctx.Repo.Repository.DeleteProtectedBranch(protectBranch.ID); err != nil {
-                               ctx.ServerError("DeleteProtectedBranch", err)
-                               return
-                       }
-               }
-               ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success", branch))
-               ctx.Redirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
-       }
-}
diff --git a/routers/repo/settings_test.go b/routers/repo/settings_test.go
deleted file mode 100644 (file)
index 5190f12..0000000
+++ /dev/null
@@ -1,413 +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 repo
-
-import (
-       "io/ioutil"
-       "net/http"
-       "testing"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/test"
-       "code.gitea.io/gitea/modules/util"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-
-       "github.com/stretchr/testify/assert"
-)
-
-func createSSHAuthorizedKeysTmpPath(t *testing.T) func() {
-       tmpDir, err := ioutil.TempDir("", "tmp-ssh")
-       if err != nil {
-               assert.Fail(t, "Unable to create temporary directory: %v", err)
-               return nil
-       }
-
-       oldPath := setting.SSH.RootPath
-       setting.SSH.RootPath = tmpDir
-
-       return func() {
-               setting.SSH.RootPath = oldPath
-               util.RemoveAll(tmpDir)
-       }
-}
-
-func TestAddReadOnlyDeployKey(t *testing.T) {
-       if deferable := createSSHAuthorizedKeysTmpPath(t); deferable != nil {
-               defer deferable()
-       } else {
-               return
-       }
-       models.PrepareTestEnv(t)
-
-       ctx := test.MockContext(t, "user2/repo1/settings/keys")
-
-       test.LoadUser(t, ctx, 2)
-       test.LoadRepo(t, ctx, 2)
-
-       addKeyForm := forms.AddKeyForm{
-               Title:   "read-only",
-               Content: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment\n",
-       }
-       web.SetForm(ctx, &addKeyForm)
-       DeployKeysPost(ctx)
-       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
-
-       models.AssertExistsAndLoadBean(t, &models.DeployKey{
-               Name:    addKeyForm.Title,
-               Content: addKeyForm.Content,
-               Mode:    models.AccessModeRead,
-       })
-}
-
-func TestAddReadWriteOnlyDeployKey(t *testing.T) {
-       if deferable := createSSHAuthorizedKeysTmpPath(t); deferable != nil {
-               defer deferable()
-       } else {
-               return
-       }
-
-       models.PrepareTestEnv(t)
-
-       ctx := test.MockContext(t, "user2/repo1/settings/keys")
-
-       test.LoadUser(t, ctx, 2)
-       test.LoadRepo(t, ctx, 2)
-
-       addKeyForm := forms.AddKeyForm{
-               Title:      "read-write",
-               Content:    "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment\n",
-               IsWritable: true,
-       }
-       web.SetForm(ctx, &addKeyForm)
-       DeployKeysPost(ctx)
-       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
-
-       models.AssertExistsAndLoadBean(t, &models.DeployKey{
-               Name:    addKeyForm.Title,
-               Content: addKeyForm.Content,
-               Mode:    models.AccessModeWrite,
-       })
-}
-
-func TestCollaborationPost(t *testing.T) {
-
-       models.PrepareTestEnv(t)
-       ctx := test.MockContext(t, "user2/repo1/issues/labels")
-       test.LoadUser(t, ctx, 2)
-       test.LoadUser(t, ctx, 4)
-       test.LoadRepo(t, ctx, 1)
-
-       ctx.Req.Form.Set("collaborator", "user4")
-
-       u := &models.User{
-               LowerName: "user2",
-               Type:      models.UserTypeIndividual,
-       }
-
-       re := &models.Repository{
-               ID:    2,
-               Owner: u,
-       }
-
-       repo := &context.Repository{
-               Owner:      u,
-               Repository: re,
-       }
-
-       ctx.Repo = repo
-
-       CollaborationPost(ctx)
-
-       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
-
-       exists, err := re.IsCollaborator(4)
-       assert.NoError(t, err)
-       assert.True(t, exists)
-}
-
-func TestCollaborationPost_InactiveUser(t *testing.T) {
-
-       models.PrepareTestEnv(t)
-       ctx := test.MockContext(t, "user2/repo1/issues/labels")
-       test.LoadUser(t, ctx, 2)
-       test.LoadUser(t, ctx, 9)
-       test.LoadRepo(t, ctx, 1)
-
-       ctx.Req.Form.Set("collaborator", "user9")
-
-       repo := &context.Repository{
-               Owner: &models.User{
-                       LowerName: "user2",
-               },
-       }
-
-       ctx.Repo = repo
-
-       CollaborationPost(ctx)
-
-       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
-       assert.NotEmpty(t, ctx.Flash.ErrorMsg)
-}
-
-func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) {
-
-       models.PrepareTestEnv(t)
-       ctx := test.MockContext(t, "user2/repo1/issues/labels")
-       test.LoadUser(t, ctx, 2)
-       test.LoadUser(t, ctx, 4)
-       test.LoadRepo(t, ctx, 1)
-
-       ctx.Req.Form.Set("collaborator", "user4")
-
-       u := &models.User{
-               LowerName: "user2",
-               Type:      models.UserTypeIndividual,
-       }
-
-       re := &models.Repository{
-               ID:    2,
-               Owner: u,
-       }
-
-       repo := &context.Repository{
-               Owner:      u,
-               Repository: re,
-       }
-
-       ctx.Repo = repo
-
-       CollaborationPost(ctx)
-
-       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
-
-       exists, err := re.IsCollaborator(4)
-       assert.NoError(t, err)
-       assert.True(t, exists)
-
-       // Try adding the same collaborator again
-       CollaborationPost(ctx)
-
-       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
-       assert.NotEmpty(t, ctx.Flash.ErrorMsg)
-}
-
-func TestCollaborationPost_NonExistentUser(t *testing.T) {
-
-       models.PrepareTestEnv(t)
-       ctx := test.MockContext(t, "user2/repo1/issues/labels")
-       test.LoadUser(t, ctx, 2)
-       test.LoadRepo(t, ctx, 1)
-
-       ctx.Req.Form.Set("collaborator", "user34")
-
-       repo := &context.Repository{
-               Owner: &models.User{
-                       LowerName: "user2",
-               },
-       }
-
-       ctx.Repo = repo
-
-       CollaborationPost(ctx)
-
-       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
-       assert.NotEmpty(t, ctx.Flash.ErrorMsg)
-}
-
-func TestAddTeamPost(t *testing.T) {
-       models.PrepareTestEnv(t)
-       ctx := test.MockContext(t, "org26/repo43")
-
-       ctx.Req.Form.Set("team", "team11")
-
-       org := &models.User{
-               LowerName: "org26",
-               Type:      models.UserTypeOrganization,
-       }
-
-       team := &models.Team{
-               ID:    11,
-               OrgID: 26,
-       }
-
-       re := &models.Repository{
-               ID:      43,
-               Owner:   org,
-               OwnerID: 26,
-       }
-
-       repo := &context.Repository{
-               Owner: &models.User{
-                       ID:                        26,
-                       LowerName:                 "org26",
-                       RepoAdminChangeTeamAccess: true,
-               },
-               Repository: re,
-       }
-
-       ctx.Repo = repo
-
-       AddTeamPost(ctx)
-
-       assert.True(t, team.HasRepository(re.ID))
-       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
-       assert.Empty(t, ctx.Flash.ErrorMsg)
-}
-
-func TestAddTeamPost_NotAllowed(t *testing.T) {
-       models.PrepareTestEnv(t)
-       ctx := test.MockContext(t, "org26/repo43")
-
-       ctx.Req.Form.Set("team", "team11")
-
-       org := &models.User{
-               LowerName: "org26",
-               Type:      models.UserTypeOrganization,
-       }
-
-       team := &models.Team{
-               ID:    11,
-               OrgID: 26,
-       }
-
-       re := &models.Repository{
-               ID:      43,
-               Owner:   org,
-               OwnerID: 26,
-       }
-
-       repo := &context.Repository{
-               Owner: &models.User{
-                       ID:                        26,
-                       LowerName:                 "org26",
-                       RepoAdminChangeTeamAccess: false,
-               },
-               Repository: re,
-       }
-
-       ctx.Repo = repo
-
-       AddTeamPost(ctx)
-
-       assert.False(t, team.HasRepository(re.ID))
-       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
-       assert.NotEmpty(t, ctx.Flash.ErrorMsg)
-
-}
-
-func TestAddTeamPost_AddTeamTwice(t *testing.T) {
-       models.PrepareTestEnv(t)
-       ctx := test.MockContext(t, "org26/repo43")
-
-       ctx.Req.Form.Set("team", "team11")
-
-       org := &models.User{
-               LowerName: "org26",
-               Type:      models.UserTypeOrganization,
-       }
-
-       team := &models.Team{
-               ID:    11,
-               OrgID: 26,
-       }
-
-       re := &models.Repository{
-               ID:      43,
-               Owner:   org,
-               OwnerID: 26,
-       }
-
-       repo := &context.Repository{
-               Owner: &models.User{
-                       ID:                        26,
-                       LowerName:                 "org26",
-                       RepoAdminChangeTeamAccess: true,
-               },
-               Repository: re,
-       }
-
-       ctx.Repo = repo
-
-       AddTeamPost(ctx)
-
-       AddTeamPost(ctx)
-       assert.True(t, team.HasRepository(re.ID))
-       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
-       assert.NotEmpty(t, ctx.Flash.ErrorMsg)
-}
-
-func TestAddTeamPost_NonExistentTeam(t *testing.T) {
-       models.PrepareTestEnv(t)
-       ctx := test.MockContext(t, "org26/repo43")
-
-       ctx.Req.Form.Set("team", "team-non-existent")
-
-       org := &models.User{
-               LowerName: "org26",
-               Type:      models.UserTypeOrganization,
-       }
-
-       re := &models.Repository{
-               ID:      43,
-               Owner:   org,
-               OwnerID: 26,
-       }
-
-       repo := &context.Repository{
-               Owner: &models.User{
-                       ID:                        26,
-                       LowerName:                 "org26",
-                       RepoAdminChangeTeamAccess: true,
-               },
-               Repository: re,
-       }
-
-       ctx.Repo = repo
-
-       AddTeamPost(ctx)
-       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
-       assert.NotEmpty(t, ctx.Flash.ErrorMsg)
-}
-
-func TestDeleteTeam(t *testing.T) {
-       models.PrepareTestEnv(t)
-       ctx := test.MockContext(t, "org3/team1/repo3")
-
-       ctx.Req.Form.Set("id", "2")
-
-       org := &models.User{
-               LowerName: "org3",
-               Type:      models.UserTypeOrganization,
-       }
-
-       team := &models.Team{
-               ID:    2,
-               OrgID: 3,
-       }
-
-       re := &models.Repository{
-               ID:      3,
-               Owner:   org,
-               OwnerID: 3,
-       }
-
-       repo := &context.Repository{
-               Owner: &models.User{
-                       ID:                        3,
-                       LowerName:                 "org3",
-                       RepoAdminChangeTeamAccess: true,
-               },
-               Repository: re,
-       }
-
-       ctx.Repo = repo
-
-       DeleteTeam(ctx)
-
-       assert.False(t, team.HasRepository(re.ID))
-}
diff --git a/routers/repo/topic.go b/routers/repo/topic.go
deleted file mode 100644 (file)
index 1d99b65..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright 2018 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 repo
-
-import (
-       "net/http"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/log"
-)
-
-// TopicsPost response for creating repository
-func TopicsPost(ctx *context.Context) {
-       if ctx.User == nil {
-               ctx.JSON(http.StatusForbidden, map[string]interface{}{
-                       "message": "Only owners could change the topics.",
-               })
-               return
-       }
-
-       var topics = make([]string, 0)
-       var topicsStr = strings.TrimSpace(ctx.Query("topics"))
-       if len(topicsStr) > 0 {
-               topics = strings.Split(topicsStr, ",")
-       }
-
-       validTopics, invalidTopics := models.SanitizeAndValidateTopics(topics)
-
-       if len(validTopics) > 25 {
-               ctx.JSON(http.StatusUnprocessableEntity, map[string]interface{}{
-                       "invalidTopics": nil,
-                       "message":       ctx.Tr("repo.topic.count_prompt"),
-               })
-               return
-       }
-
-       if len(invalidTopics) > 0 {
-               ctx.JSON(http.StatusUnprocessableEntity, map[string]interface{}{
-                       "invalidTopics": invalidTopics,
-                       "message":       ctx.Tr("repo.topic.format_prompt"),
-               })
-               return
-       }
-
-       err := models.SaveTopics(ctx.Repo.Repository.ID, validTopics...)
-       if err != nil {
-               log.Error("SaveTopics failed: %v", err)
-               ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
-                       "message": "Save topics failed.",
-               })
-               return
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "status": "ok",
-       })
-}
diff --git a/routers/repo/view.go b/routers/repo/view.go
deleted file mode 100644 (file)
index cd5b0f4..0000000
+++ /dev/null
@@ -1,808 +0,0 @@
-// Copyright 2017 The Gitea Authors. All rights reserved.
-// Copyright 2014 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 repo
-
-import (
-       "bytes"
-       "encoding/base64"
-       "fmt"
-       gotemplate "html/template"
-       "io"
-       "io/ioutil"
-       "net/http"
-       "net/url"
-       "path"
-       "strconv"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/cache"
-       "code.gitea.io/gitea/modules/charset"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/git"
-       "code.gitea.io/gitea/modules/highlight"
-       "code.gitea.io/gitea/modules/lfs"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/markup"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/typesniffer"
-)
-
-const (
-       tplRepoEMPTY base.TplName = "repo/empty"
-       tplRepoHome  base.TplName = "repo/home"
-       tplWatchers  base.TplName = "repo/watchers"
-       tplForks     base.TplName = "repo/forks"
-       tplMigrating base.TplName = "repo/migrate/migrating"
-)
-
-type namedBlob struct {
-       name      string
-       isSymlink bool
-       blob      *git.Blob
-}
-
-func linesBytesCount(s []byte) int {
-       nl := []byte{'\n'}
-       n := bytes.Count(s, nl)
-       if len(s) > 0 && !bytes.HasSuffix(s, nl) {
-               n++
-       }
-       return n
-}
-
-// FIXME: There has to be a more efficient way of doing this
-func getReadmeFileFromPath(commit *git.Commit, treePath string) (*namedBlob, error) {
-       tree, err := commit.SubTree(treePath)
-       if err != nil {
-               return nil, err
-       }
-
-       entries, err := tree.ListEntries()
-       if err != nil {
-               return nil, err
-       }
-
-       var readmeFiles [4]*namedBlob
-       var exts = []string{".md", ".txt", ""} // sorted by priority
-       for _, entry := range entries {
-               if entry.IsDir() {
-                       continue
-               }
-               for i, ext := range exts {
-                       if markup.IsReadmeFile(entry.Name(), ext) {
-                               if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].name, entry.Blob().Name()) {
-                                       name := entry.Name()
-                                       isSymlink := entry.IsLink()
-                                       target := entry
-                                       if isSymlink {
-                                               target, err = entry.FollowLinks()
-                                               if err != nil && !git.IsErrBadLink(err) {
-                                                       return nil, err
-                                               }
-                                       }
-                                       if target != nil && (target.IsExecutable() || target.IsRegular()) {
-                                               readmeFiles[i] = &namedBlob{
-                                                       name,
-                                                       isSymlink,
-                                                       target.Blob(),
-                                               }
-                                       }
-                               }
-                       }
-               }
-
-               if markup.IsReadmeFile(entry.Name()) {
-                       if readmeFiles[3] == nil || base.NaturalSortLess(readmeFiles[3].name, entry.Blob().Name()) {
-                               name := entry.Name()
-                               isSymlink := entry.IsLink()
-                               if isSymlink {
-                                       entry, err = entry.FollowLinks()
-                                       if err != nil && !git.IsErrBadLink(err) {
-                                               return nil, err
-                                       }
-                               }
-                               if entry != nil && (entry.IsExecutable() || entry.IsRegular()) {
-                                       readmeFiles[3] = &namedBlob{
-                                               name,
-                                               isSymlink,
-                                               entry.Blob(),
-                                       }
-                               }
-                       }
-               }
-       }
-       var readmeFile *namedBlob
-       for _, f := range readmeFiles {
-               if f != nil {
-                       readmeFile = f
-                       break
-               }
-       }
-       return readmeFile, nil
-}
-
-func renderDirectory(ctx *context.Context, treeLink string) {
-       tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath)
-       if err != nil {
-               ctx.NotFoundOrServerError("Repo.Commit.SubTree", git.IsErrNotExist, err)
-               return
-       }
-
-       entries, err := tree.ListEntries()
-       if err != nil {
-               ctx.ServerError("ListEntries", err)
-               return
-       }
-       entries.CustomSort(base.NaturalSortLess)
-
-       var c *git.LastCommitCache
-       if setting.CacheService.LastCommit.Enabled && ctx.Repo.CommitsCount >= setting.CacheService.LastCommit.CommitsCount {
-               c = git.NewLastCommitCache(ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, setting.LastCommitCacheTTLSeconds, cache.GetCache())
-       }
-
-       var latestCommit *git.Commit
-       ctx.Data["Files"], latestCommit, err = entries.GetCommitsInfo(ctx, ctx.Repo.Commit, ctx.Repo.TreePath, c)
-       if err != nil {
-               ctx.ServerError("GetCommitsInfo", err)
-               return
-       }
-
-       // 3 for the extensions in exts[] in order
-       // the last one is for a readme that doesn't
-       // strictly match an extension
-       var readmeFiles [4]*namedBlob
-       var docsEntries [3]*git.TreeEntry
-       var exts = []string{".md", ".txt", ""} // sorted by priority
-       for _, entry := range entries {
-               if entry.IsDir() {
-                       lowerName := strings.ToLower(entry.Name())
-                       switch lowerName {
-                       case "docs":
-                               if entry.Name() == "docs" || docsEntries[0] == nil {
-                                       docsEntries[0] = entry
-                               }
-                       case ".gitea":
-                               if entry.Name() == ".gitea" || docsEntries[1] == nil {
-                                       docsEntries[1] = entry
-                               }
-                       case ".github":
-                               if entry.Name() == ".github" || docsEntries[2] == nil {
-                                       docsEntries[2] = entry
-                               }
-                       }
-                       continue
-               }
-
-               for i, ext := range exts {
-                       if markup.IsReadmeFile(entry.Name(), ext) {
-                               log.Debug("%s", entry.Name())
-                               name := entry.Name()
-                               isSymlink := entry.IsLink()
-                               target := entry
-                               if isSymlink {
-                                       target, err = entry.FollowLinks()
-                                       if err != nil && !git.IsErrBadLink(err) {
-                                               ctx.ServerError("FollowLinks", err)
-                                               return
-                                       }
-                               }
-                               log.Debug("%t", target == nil)
-                               if target != nil && (target.IsExecutable() || target.IsRegular()) {
-                                       readmeFiles[i] = &namedBlob{
-                                               name,
-                                               isSymlink,
-                                               target.Blob(),
-                                       }
-                               }
-                       }
-               }
-
-               if markup.IsReadmeFile(entry.Name()) {
-                       name := entry.Name()
-                       isSymlink := entry.IsLink()
-                       if isSymlink {
-                               entry, err = entry.FollowLinks()
-                               if err != nil && !git.IsErrBadLink(err) {
-                                       ctx.ServerError("FollowLinks", err)
-                                       return
-                               }
-                       }
-                       if entry != nil && (entry.IsExecutable() || entry.IsRegular()) {
-                               readmeFiles[3] = &namedBlob{
-                                       name,
-                                       isSymlink,
-                                       entry.Blob(),
-                               }
-                       }
-               }
-       }
-
-       var readmeFile *namedBlob
-       readmeTreelink := treeLink
-       for _, f := range readmeFiles {
-               if f != nil {
-                       readmeFile = f
-                       break
-               }
-       }
-
-       if ctx.Repo.TreePath == "" && readmeFile == nil {
-               for _, entry := range docsEntries {
-                       if entry == nil {
-                               continue
-                       }
-                       readmeFile, err = getReadmeFileFromPath(ctx.Repo.Commit, entry.GetSubJumpablePathName())
-                       if err != nil {
-                               ctx.ServerError("getReadmeFileFromPath", err)
-                               return
-                       }
-                       if readmeFile != nil {
-                               readmeFile.name = entry.Name() + "/" + readmeFile.name
-                               readmeTreelink = treeLink + "/" + entry.GetSubJumpablePathName()
-                               break
-                       }
-               }
-       }
-
-       if readmeFile != nil {
-               ctx.Data["RawFileLink"] = ""
-               ctx.Data["ReadmeInList"] = true
-               ctx.Data["ReadmeExist"] = true
-               ctx.Data["FileIsSymlink"] = readmeFile.isSymlink
-
-               dataRc, err := readmeFile.blob.DataAsync()
-               if err != nil {
-                       ctx.ServerError("Data", err)
-                       return
-               }
-               defer dataRc.Close()
-
-               buf := make([]byte, 1024)
-               n, _ := dataRc.Read(buf)
-               buf = buf[:n]
-
-               st := typesniffer.DetectContentType(buf)
-               isTextFile := st.IsText()
-
-               ctx.Data["FileIsText"] = isTextFile
-               ctx.Data["FileName"] = readmeFile.name
-               fileSize := int64(0)
-               isLFSFile := false
-               ctx.Data["IsLFSFile"] = false
-
-               // FIXME: what happens when README file is an image?
-               if isTextFile && setting.LFS.StartServer {
-                       pointer, _ := lfs.ReadPointerFromBuffer(buf)
-                       if pointer.IsValid() {
-                               meta, err := ctx.Repo.Repository.GetLFSMetaObjectByOid(pointer.Oid)
-                               if err != nil && err != models.ErrLFSObjectNotExist {
-                                       ctx.ServerError("GetLFSMetaObject", err)
-                                       return
-                               }
-                               if meta != nil {
-                                       ctx.Data["IsLFSFile"] = true
-                                       isLFSFile = true
-
-                                       // OK read the lfs object
-                                       var err error
-                                       dataRc, err = lfs.ReadMetaObject(pointer)
-                                       if err != nil {
-                                               ctx.ServerError("ReadMetaObject", err)
-                                               return
-                                       }
-                                       defer dataRc.Close()
-
-                                       buf = make([]byte, 1024)
-                                       n, err = dataRc.Read(buf)
-                                       if err != nil {
-                                               ctx.ServerError("Data", err)
-                                               return
-                                       }
-                                       buf = buf[:n]
-
-                                       st = typesniffer.DetectContentType(buf)
-                                       isTextFile = st.IsText()
-                                       ctx.Data["IsTextFile"] = isTextFile
-
-                                       fileSize = meta.Size
-                                       ctx.Data["FileSize"] = meta.Size
-                                       filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.name))
-                                       ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s.git/info/lfs/objects/%s/%s", setting.AppURL, ctx.Repo.Repository.FullName(), meta.Oid, filenameBase64)
-                               }
-                       }
-               }
-
-               if !isLFSFile {
-                       fileSize = readmeFile.blob.Size()
-               }
-
-               if isTextFile {
-                       if fileSize >= setting.UI.MaxDisplayFileSize {
-                               // Pretend that this is a normal text file to display 'This file is too large to be shown'
-                               ctx.Data["IsFileTooLarge"] = true
-                               ctx.Data["IsTextFile"] = true
-                               ctx.Data["FileSize"] = fileSize
-                       } else {
-                               rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc))
-
-                               if markupType := markup.Type(readmeFile.name); markupType != "" {
-                                       ctx.Data["IsMarkup"] = true
-                                       ctx.Data["MarkupType"] = string(markupType)
-                                       var result strings.Builder
-                                       err := markup.Render(&markup.RenderContext{
-                                               Filename:  readmeFile.name,
-                                               URLPrefix: readmeTreelink,
-                                               Metas:     ctx.Repo.Repository.ComposeDocumentMetas(),
-                                       }, rd, &result)
-                                       if err != nil {
-                                               log.Error("Render failed: %v then fallback", err)
-                                               bs, _ := ioutil.ReadAll(rd)
-                                               ctx.Data["FileContent"] = strings.ReplaceAll(
-                                                       gotemplate.HTMLEscapeString(string(bs)), "\n", `<br>`,
-                                               )
-                                       } else {
-                                               ctx.Data["FileContent"] = result.String()
-                                       }
-                               } else {
-                                       ctx.Data["IsRenderedHTML"] = true
-                                       ctx.Data["FileContent"] = strings.ReplaceAll(
-                                               gotemplate.HTMLEscapeString(string(buf)), "\n", `<br>`,
-                                       )
-                               }
-                       }
-               }
-       }
-
-       // Show latest commit info of repository in table header,
-       // or of directory if not in root directory.
-       ctx.Data["LatestCommit"] = latestCommit
-       verification := models.ParseCommitWithSignature(latestCommit)
-
-       if err := models.CalculateTrustStatus(verification, ctx.Repo.Repository, nil); err != nil {
-               ctx.ServerError("CalculateTrustStatus", err)
-               return
-       }
-       ctx.Data["LatestCommitVerification"] = verification
-
-       ctx.Data["LatestCommitUser"] = models.ValidateCommitWithEmail(latestCommit)
-
-       statuses, err := models.GetLatestCommitStatus(ctx.Repo.Repository.ID, ctx.Repo.Commit.ID.String(), models.ListOptions{})
-       if err != nil {
-               log.Error("GetLatestCommitStatus: %v", err)
-       }
-
-       ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(statuses)
-       ctx.Data["LatestCommitStatuses"] = statuses
-
-       // Check permission to add or upload new file.
-       if ctx.Repo.CanWrite(models.UnitTypeCode) && ctx.Repo.IsViewBranch {
-               ctx.Data["CanAddFile"] = !ctx.Repo.Repository.IsArchived
-               ctx.Data["CanUploadFile"] = setting.Repository.Upload.Enabled && !ctx.Repo.Repository.IsArchived
-       }
-
-       ctx.Data["SSHDomain"] = setting.SSH.Domain
-}
-
-func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink string) {
-       ctx.Data["IsViewFile"] = true
-       blob := entry.Blob()
-       dataRc, err := blob.DataAsync()
-       if err != nil {
-               ctx.ServerError("DataAsync", err)
-               return
-       }
-       defer dataRc.Close()
-
-       ctx.Data["Title"] = ctx.Data["Title"].(string) + " - " + ctx.Repo.TreePath + " at " + ctx.Repo.BranchName
-
-       fileSize := blob.Size()
-       ctx.Data["FileIsSymlink"] = entry.IsLink()
-       ctx.Data["FileName"] = blob.Name()
-       ctx.Data["RawFileLink"] = rawLink + "/" + ctx.Repo.TreePath
-
-       buf := make([]byte, 1024)
-       n, _ := dataRc.Read(buf)
-       buf = buf[:n]
-
-       st := typesniffer.DetectContentType(buf)
-       isTextFile := st.IsText()
-
-       isLFSFile := false
-       isDisplayingSource := ctx.Query("display") == "source"
-       isDisplayingRendered := !isDisplayingSource
-
-       //Check for LFS meta file
-       if isTextFile && setting.LFS.StartServer {
-               pointer, _ := lfs.ReadPointerFromBuffer(buf)
-               if pointer.IsValid() {
-                       meta, err := ctx.Repo.Repository.GetLFSMetaObjectByOid(pointer.Oid)
-                       if err != nil && err != models.ErrLFSObjectNotExist {
-                               ctx.ServerError("GetLFSMetaObject", err)
-                               return
-                       }
-                       if meta != nil {
-                               isLFSFile = true
-
-                               // OK read the lfs object
-                               var err error
-                               dataRc, err = lfs.ReadMetaObject(pointer)
-                               if err != nil {
-                                       ctx.ServerError("ReadMetaObject", err)
-                                       return
-                               }
-                               defer dataRc.Close()
-
-                               buf = make([]byte, 1024)
-                               n, err = dataRc.Read(buf)
-                               // Error EOF don't mean there is an error, it just means we read to
-                               // the end
-                               if err != nil && err != io.EOF {
-                                       ctx.ServerError("Data", err)
-                                       return
-                               }
-                               buf = buf[:n]
-
-                               st = typesniffer.DetectContentType(buf)
-                               isTextFile = st.IsText()
-
-                               fileSize = meta.Size
-                               ctx.Data["RawFileLink"] = fmt.Sprintf("%s/media/%s/%s", ctx.Repo.RepoLink, ctx.Repo.BranchNameSubURL(), ctx.Repo.TreePath)
-                       }
-               }
-       }
-
-       isRepresentableAsText := st.IsRepresentableAsText()
-       if !isRepresentableAsText {
-               // If we can't show plain text, always try to render.
-               isDisplayingSource = false
-               isDisplayingRendered = true
-       }
-       ctx.Data["IsLFSFile"] = isLFSFile
-       ctx.Data["FileSize"] = fileSize
-       ctx.Data["IsTextFile"] = isTextFile
-       ctx.Data["IsRepresentableAsText"] = isRepresentableAsText
-       ctx.Data["IsDisplayingSource"] = isDisplayingSource
-       ctx.Data["IsDisplayingRendered"] = isDisplayingRendered
-       ctx.Data["IsTextSource"] = isTextFile || isDisplayingSource
-
-       // Check LFS Lock
-       lfsLock, err := ctx.Repo.Repository.GetTreePathLock(ctx.Repo.TreePath)
-       ctx.Data["LFSLock"] = lfsLock
-       if err != nil {
-               ctx.ServerError("GetTreePathLock", err)
-               return
-       }
-       if lfsLock != nil {
-               ctx.Data["LFSLockOwner"] = lfsLock.Owner.DisplayName()
-               ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked")
-       }
-
-       // Assume file is not editable first.
-       if isLFSFile {
-               ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_lfs_files")
-       } else if !isRepresentableAsText {
-               ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files")
-       }
-
-       switch {
-       case isRepresentableAsText:
-               if st.IsSvgImage() {
-                       ctx.Data["IsImageFile"] = true
-                       ctx.Data["HasSourceRenderedToggle"] = true
-               }
-
-               if fileSize >= setting.UI.MaxDisplayFileSize {
-                       ctx.Data["IsFileTooLarge"] = true
-                       break
-               }
-
-               rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc))
-               readmeExist := markup.IsReadmeFile(blob.Name())
-               ctx.Data["ReadmeExist"] = readmeExist
-               if markupType := markup.Type(blob.Name()); markupType != "" {
-                       ctx.Data["IsMarkup"] = true
-                       ctx.Data["MarkupType"] = markupType
-                       var result strings.Builder
-                       err := markup.Render(&markup.RenderContext{
-                               Filename:  blob.Name(),
-                               URLPrefix: path.Dir(treeLink),
-                               Metas:     ctx.Repo.Repository.ComposeDocumentMetas(),
-                       }, rd, &result)
-                       if err != nil {
-                               ctx.ServerError("Render", err)
-                               return
-                       }
-                       ctx.Data["FileContent"] = result.String()
-               } else if readmeExist {
-                       buf, _ := ioutil.ReadAll(rd)
-                       ctx.Data["IsRenderedHTML"] = true
-                       ctx.Data["FileContent"] = strings.ReplaceAll(
-                               gotemplate.HTMLEscapeString(string(buf)), "\n", `<br>`,
-                       )
-               } else {
-                       buf, _ := ioutil.ReadAll(rd)
-                       lineNums := linesBytesCount(buf)
-                       ctx.Data["NumLines"] = strconv.Itoa(lineNums)
-                       ctx.Data["NumLinesSet"] = true
-                       ctx.Data["FileContent"] = highlight.File(lineNums, blob.Name(), buf)
-               }
-               if !isLFSFile {
-                       if ctx.Repo.CanEnableEditor() {
-                               if lfsLock != nil && lfsLock.OwnerID != ctx.User.ID {
-                                       ctx.Data["CanEditFile"] = false
-                                       ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
-                               } else {
-                                       ctx.Data["CanEditFile"] = true
-                                       ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file")
-                               }
-                       } else if !ctx.Repo.IsViewBranch {
-                               ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
-                       } else if !ctx.Repo.CanWrite(models.UnitTypeCode) {
-                               ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit")
-                       }
-               }
-
-       case st.IsPDF():
-               ctx.Data["IsPDFFile"] = true
-       case st.IsVideo():
-               ctx.Data["IsVideoFile"] = true
-       case st.IsAudio():
-               ctx.Data["IsAudioFile"] = true
-       case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()):
-               ctx.Data["IsImageFile"] = true
-       default:
-               if fileSize >= setting.UI.MaxDisplayFileSize {
-                       ctx.Data["IsFileTooLarge"] = true
-                       break
-               }
-
-               if markupType := markup.Type(blob.Name()); markupType != "" {
-                       rd := io.MultiReader(bytes.NewReader(buf), dataRc)
-                       ctx.Data["IsMarkup"] = true
-                       ctx.Data["MarkupType"] = markupType
-                       var result strings.Builder
-                       err := markup.Render(&markup.RenderContext{
-                               Filename:  blob.Name(),
-                               URLPrefix: path.Dir(treeLink),
-                               Metas:     ctx.Repo.Repository.ComposeDocumentMetas(),
-                       }, rd, &result)
-                       if err != nil {
-                               ctx.ServerError("Render", err)
-                               return
-                       }
-                       ctx.Data["FileContent"] = result.String()
-               }
-       }
-
-       if ctx.Repo.CanEnableEditor() {
-               if lfsLock != nil && lfsLock.OwnerID != ctx.User.ID {
-                       ctx.Data["CanDeleteFile"] = false
-                       ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
-               } else {
-                       ctx.Data["CanDeleteFile"] = true
-                       ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.delete_this_file")
-               }
-       } else if !ctx.Repo.IsViewBranch {
-               ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
-       } else if !ctx.Repo.CanWrite(models.UnitTypeCode) {
-               ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access")
-       }
-}
-
-func safeURL(address string) string {
-       u, err := url.Parse(address)
-       if err != nil {
-               return address
-       }
-       u.User = nil
-       return u.String()
-}
-
-// Home render repository home page
-func Home(ctx *context.Context) {
-       if len(ctx.Repo.Units) > 0 {
-               if ctx.Repo.Repository.IsBeingCreated() {
-                       task, err := models.GetMigratingTask(ctx.Repo.Repository.ID)
-                       if err != nil {
-                               ctx.ServerError("models.GetMigratingTask", err)
-                               return
-                       }
-                       cfg, err := task.MigrateConfig()
-                       if err != nil {
-                               ctx.ServerError("task.MigrateConfig", err)
-                               return
-                       }
-
-                       ctx.Data["Repo"] = ctx.Repo
-                       ctx.Data["MigrateTask"] = task
-                       ctx.Data["CloneAddr"] = safeURL(cfg.CloneAddr)
-                       ctx.HTML(http.StatusOK, tplMigrating)
-                       return
-               }
-
-               if ctx.IsSigned {
-                       // Set repo notification-status read if unread
-                       if err := ctx.Repo.Repository.ReadBy(ctx.User.ID); err != nil {
-                               ctx.ServerError("ReadBy", err)
-                               return
-                       }
-               }
-
-               var firstUnit *models.Unit
-               for _, repoUnit := range ctx.Repo.Units {
-                       if repoUnit.Type == models.UnitTypeCode {
-                               renderCode(ctx)
-                               return
-                       }
-
-                       unit, ok := models.Units[repoUnit.Type]
-                       if ok && (firstUnit == nil || !firstUnit.IsLessThan(unit)) {
-                               firstUnit = &unit
-                       }
-               }
-
-               if firstUnit != nil {
-                       ctx.Redirect(fmt.Sprintf("%s/%s%s", setting.AppSubURL, ctx.Repo.Repository.FullName(), firstUnit.URI))
-                       return
-               }
-       }
-
-       ctx.NotFound("Home", fmt.Errorf(ctx.Tr("units.error.no_unit_allowed_repo")))
-}
-
-func renderLanguageStats(ctx *context.Context) {
-       langs, err := ctx.Repo.Repository.GetTopLanguageStats(5)
-       if err != nil {
-               ctx.ServerError("Repo.GetTopLanguageStats", err)
-               return
-       }
-
-       ctx.Data["LanguageStats"] = langs
-}
-
-func renderRepoTopics(ctx *context.Context) {
-       topics, err := models.FindTopics(&models.FindTopicOptions{
-               RepoID: ctx.Repo.Repository.ID,
-       })
-       if err != nil {
-               ctx.ServerError("models.FindTopics", err)
-               return
-       }
-       ctx.Data["Topics"] = topics
-}
-
-func renderCode(ctx *context.Context) {
-       ctx.Data["PageIsViewCode"] = true
-
-       if ctx.Repo.Repository.IsEmpty {
-               ctx.HTML(http.StatusOK, tplRepoEMPTY)
-               return
-       }
-
-       title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name
-       if len(ctx.Repo.Repository.Description) > 0 {
-               title += ": " + ctx.Repo.Repository.Description
-       }
-       ctx.Data["Title"] = title
-
-       branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
-       treeLink := branchLink
-       rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL()
-
-       if len(ctx.Repo.TreePath) > 0 {
-               treeLink += "/" + ctx.Repo.TreePath
-       }
-
-       // Get Topics of this repo
-       renderRepoTopics(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       // Get current entry user currently looking at.
-       entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
-       if err != nil {
-               ctx.NotFoundOrServerError("Repo.Commit.GetTreeEntryByPath", git.IsErrNotExist, err)
-               return
-       }
-
-       renderLanguageStats(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       if entry.IsDir() {
-               renderDirectory(ctx, treeLink)
-       } else {
-               renderFile(ctx, entry, treeLink, rawLink)
-       }
-       if ctx.Written() {
-               return
-       }
-
-       var treeNames []string
-       paths := make([]string, 0, 5)
-       if len(ctx.Repo.TreePath) > 0 {
-               treeNames = strings.Split(ctx.Repo.TreePath, "/")
-               for i := range treeNames {
-                       paths = append(paths, strings.Join(treeNames[:i+1], "/"))
-               }
-
-               ctx.Data["HasParentPath"] = true
-               if len(paths)-2 >= 0 {
-                       ctx.Data["ParentPath"] = "/" + paths[len(paths)-2]
-               }
-       }
-
-       ctx.Data["Paths"] = paths
-       ctx.Data["TreeLink"] = treeLink
-       ctx.Data["TreeNames"] = treeNames
-       ctx.Data["BranchLink"] = branchLink
-       ctx.HTML(http.StatusOK, tplRepoHome)
-}
-
-// RenderUserCards render a page show users according the input templaet
-func RenderUserCards(ctx *context.Context, total int, getter func(opts models.ListOptions) ([]*models.User, error), tpl base.TplName) {
-       page := ctx.QueryInt("page")
-       if page <= 0 {
-               page = 1
-       }
-       pager := context.NewPagination(total, models.ItemsPerPage, page, 5)
-       ctx.Data["Page"] = pager
-
-       items, err := getter(models.ListOptions{
-               Page:     pager.Paginater.Current(),
-               PageSize: models.ItemsPerPage,
-       })
-       if err != nil {
-               ctx.ServerError("getter", err)
-               return
-       }
-       ctx.Data["Cards"] = items
-
-       ctx.HTML(http.StatusOK, tpl)
-}
-
-// Watchers render repository's watch users
-func Watchers(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.watchers")
-       ctx.Data["CardsTitle"] = ctx.Tr("repo.watchers")
-       ctx.Data["PageIsWatchers"] = true
-
-       RenderUserCards(ctx, ctx.Repo.Repository.NumWatches, ctx.Repo.Repository.GetWatchers, tplWatchers)
-}
-
-// Stars render repository's starred users
-func Stars(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.stargazers")
-       ctx.Data["CardsTitle"] = ctx.Tr("repo.stargazers")
-       ctx.Data["PageIsStargazers"] = true
-       RenderUserCards(ctx, ctx.Repo.Repository.NumStars, ctx.Repo.Repository.GetStargazers, tplWatchers)
-}
-
-// Forks render repository's forked users
-func Forks(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repos.forks")
-
-       // TODO: need pagination
-       forks, err := ctx.Repo.Repository.GetForks(models.ListOptions{})
-       if err != nil {
-               ctx.ServerError("GetForks", err)
-               return
-       }
-
-       for _, fork := range forks {
-               if err = fork.GetOwner(); err != nil {
-                       ctx.ServerError("GetOwner", err)
-                       return
-               }
-       }
-       ctx.Data["Forks"] = forks
-
-       ctx.HTML(http.StatusOK, tplForks)
-}
diff --git a/routers/repo/webhook.go b/routers/repo/webhook.go
deleted file mode 100644 (file)
index fe16d24..0000000
+++ /dev/null
@@ -1,1131 +0,0 @@
-// Copyright 2015 The Gogs Authors. All rights reserved.
-// 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 repo
-
-import (
-       "errors"
-       "fmt"
-       "net/http"
-       "path"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/convert"
-       "code.gitea.io/gitea/modules/git"
-       "code.gitea.io/gitea/modules/setting"
-       api "code.gitea.io/gitea/modules/structs"
-       "code.gitea.io/gitea/modules/util"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-       "code.gitea.io/gitea/services/webhook"
-       jsoniter "github.com/json-iterator/go"
-)
-
-const (
-       tplHooks        base.TplName = "repo/settings/webhook/base"
-       tplHookNew      base.TplName = "repo/settings/webhook/new"
-       tplOrgHookNew   base.TplName = "org/settings/hook_new"
-       tplAdminHookNew base.TplName = "admin/hook_new"
-)
-
-// Webhooks render web hooks list page
-func Webhooks(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.settings.hooks")
-       ctx.Data["PageIsSettingsHooks"] = true
-       ctx.Data["BaseLink"] = ctx.Repo.RepoLink + "/settings/hooks"
-       ctx.Data["BaseLinkNew"] = ctx.Repo.RepoLink + "/settings/hooks"
-       ctx.Data["Description"] = ctx.Tr("repo.settings.hooks_desc", "https://docs.gitea.io/en-us/webhooks/")
-
-       ws, err := models.GetWebhooksByRepoID(ctx.Repo.Repository.ID, models.ListOptions{})
-       if err != nil {
-               ctx.ServerError("GetWebhooksByRepoID", err)
-               return
-       }
-       ctx.Data["Webhooks"] = ws
-
-       ctx.HTML(http.StatusOK, tplHooks)
-}
-
-type orgRepoCtx struct {
-       OrgID           int64
-       RepoID          int64
-       IsAdmin         bool
-       IsSystemWebhook bool
-       Link            string
-       LinkNew         string
-       NewTemplate     base.TplName
-}
-
-// getOrgRepoCtx determines whether this is a repo, organization, or admin (both default and system) context.
-func getOrgRepoCtx(ctx *context.Context) (*orgRepoCtx, error) {
-       if len(ctx.Repo.RepoLink) > 0 {
-               return &orgRepoCtx{
-                       RepoID:      ctx.Repo.Repository.ID,
-                       Link:        path.Join(ctx.Repo.RepoLink, "settings/hooks"),
-                       LinkNew:     path.Join(ctx.Repo.RepoLink, "settings/hooks"),
-                       NewTemplate: tplHookNew,
-               }, nil
-       }
-
-       if len(ctx.Org.OrgLink) > 0 {
-               return &orgRepoCtx{
-                       OrgID:       ctx.Org.Organization.ID,
-                       Link:        path.Join(ctx.Org.OrgLink, "settings/hooks"),
-                       LinkNew:     path.Join(ctx.Org.OrgLink, "settings/hooks"),
-                       NewTemplate: tplOrgHookNew,
-               }, nil
-       }
-
-       if ctx.User.IsAdmin {
-               // Are we looking at default webhooks?
-               if ctx.Params(":configType") == "default-hooks" {
-                       return &orgRepoCtx{
-                               IsAdmin:     true,
-                               Link:        path.Join(setting.AppSubURL, "/admin/hooks"),
-                               LinkNew:     path.Join(setting.AppSubURL, "/admin/default-hooks"),
-                               NewTemplate: tplAdminHookNew,
-                       }, nil
-               }
-
-               // Must be system webhooks instead
-               return &orgRepoCtx{
-                       IsAdmin:         true,
-                       IsSystemWebhook: true,
-                       Link:            path.Join(setting.AppSubURL, "/admin/hooks"),
-                       LinkNew:         path.Join(setting.AppSubURL, "/admin/system-hooks"),
-                       NewTemplate:     tplAdminHookNew,
-               }, nil
-       }
-
-       return nil, errors.New("Unable to set OrgRepo context")
-}
-
-func checkHookType(ctx *context.Context) string {
-       hookType := strings.ToLower(ctx.Params(":type"))
-       if !util.IsStringInSlice(hookType, setting.Webhook.Types, true) {
-               ctx.NotFound("checkHookType", nil)
-               return ""
-       }
-       return hookType
-}
-
-// WebhooksNew render creating webhook page
-func WebhooksNew(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook")
-       ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
-
-       orCtx, err := getOrgRepoCtx(ctx)
-       if err != nil {
-               ctx.ServerError("getOrgRepoCtx", err)
-               return
-       }
-
-       if orCtx.IsAdmin && orCtx.IsSystemWebhook {
-               ctx.Data["PageIsAdminSystemHooks"] = true
-               ctx.Data["PageIsAdminSystemHooksNew"] = true
-       } else if orCtx.IsAdmin {
-               ctx.Data["PageIsAdminDefaultHooks"] = true
-               ctx.Data["PageIsAdminDefaultHooksNew"] = true
-       } else {
-               ctx.Data["PageIsSettingsHooks"] = true
-               ctx.Data["PageIsSettingsHooksNew"] = true
-       }
-
-       hookType := checkHookType(ctx)
-       ctx.Data["HookType"] = hookType
-       if ctx.Written() {
-               return
-       }
-       if hookType == "discord" {
-               ctx.Data["DiscordHook"] = map[string]interface{}{
-                       "Username": "Gitea",
-                       "IconURL":  setting.AppURL + "img/favicon.png",
-               }
-       }
-       ctx.Data["BaseLink"] = orCtx.LinkNew
-
-       ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-}
-
-// ParseHookEvent convert web form content to models.HookEvent
-func ParseHookEvent(form forms.WebhookForm) *models.HookEvent {
-       return &models.HookEvent{
-               PushOnly:       form.PushOnly(),
-               SendEverything: form.SendEverything(),
-               ChooseEvents:   form.ChooseEvents(),
-               HookEvents: models.HookEvents{
-                       Create:               form.Create,
-                       Delete:               form.Delete,
-                       Fork:                 form.Fork,
-                       Issues:               form.Issues,
-                       IssueAssign:          form.IssueAssign,
-                       IssueLabel:           form.IssueLabel,
-                       IssueMilestone:       form.IssueMilestone,
-                       IssueComment:         form.IssueComment,
-                       Release:              form.Release,
-                       Push:                 form.Push,
-                       PullRequest:          form.PullRequest,
-                       PullRequestAssign:    form.PullRequestAssign,
-                       PullRequestLabel:     form.PullRequestLabel,
-                       PullRequestMilestone: form.PullRequestMilestone,
-                       PullRequestComment:   form.PullRequestComment,
-                       PullRequestReview:    form.PullRequestReview,
-                       PullRequestSync:      form.PullRequestSync,
-                       Repository:           form.Repository,
-               },
-               BranchFilter: form.BranchFilter,
-       }
-}
-
-// GiteaHooksNewPost response for creating Gitea webhook
-func GiteaHooksNewPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.NewWebhookForm)
-       ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook")
-       ctx.Data["PageIsSettingsHooks"] = true
-       ctx.Data["PageIsSettingsHooksNew"] = true
-       ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
-       ctx.Data["HookType"] = models.GITEA
-
-       orCtx, err := getOrgRepoCtx(ctx)
-       if err != nil {
-               ctx.ServerError("getOrgRepoCtx", err)
-               return
-       }
-       ctx.Data["BaseLink"] = orCtx.LinkNew
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-               return
-       }
-
-       contentType := models.ContentTypeJSON
-       if models.HookContentType(form.ContentType) == models.ContentTypeForm {
-               contentType = models.ContentTypeForm
-       }
-
-       w := &models.Webhook{
-               RepoID:          orCtx.RepoID,
-               URL:             form.PayloadURL,
-               HTTPMethod:      form.HTTPMethod,
-               ContentType:     contentType,
-               Secret:          form.Secret,
-               HookEvent:       ParseHookEvent(form.WebhookForm),
-               IsActive:        form.Active,
-               Type:            models.GITEA,
-               OrgID:           orCtx.OrgID,
-               IsSystemWebhook: orCtx.IsSystemWebhook,
-       }
-       if err := w.UpdateEvent(); err != nil {
-               ctx.ServerError("UpdateEvent", err)
-               return
-       } else if err := models.CreateWebhook(w); err != nil {
-               ctx.ServerError("CreateWebhook", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
-       ctx.Redirect(orCtx.Link)
-}
-
-// GogsHooksNewPost response for creating webhook
-func GogsHooksNewPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.NewGogshookForm)
-       newGogsWebhookPost(ctx, *form, models.GOGS)
-}
-
-// newGogsWebhookPost response for creating gogs hook
-func newGogsWebhookPost(ctx *context.Context, form forms.NewGogshookForm, kind models.HookTaskType) {
-       ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook")
-       ctx.Data["PageIsSettingsHooks"] = true
-       ctx.Data["PageIsSettingsHooksNew"] = true
-       ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
-       ctx.Data["HookType"] = models.GOGS
-
-       orCtx, err := getOrgRepoCtx(ctx)
-       if err != nil {
-               ctx.ServerError("getOrgRepoCtx", err)
-               return
-       }
-       ctx.Data["BaseLink"] = orCtx.LinkNew
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-               return
-       }
-
-       contentType := models.ContentTypeJSON
-       if models.HookContentType(form.ContentType) == models.ContentTypeForm {
-               contentType = models.ContentTypeForm
-       }
-
-       w := &models.Webhook{
-               RepoID:          orCtx.RepoID,
-               URL:             form.PayloadURL,
-               ContentType:     contentType,
-               Secret:          form.Secret,
-               HookEvent:       ParseHookEvent(form.WebhookForm),
-               IsActive:        form.Active,
-               Type:            kind,
-               OrgID:           orCtx.OrgID,
-               IsSystemWebhook: orCtx.IsSystemWebhook,
-       }
-       if err := w.UpdateEvent(); err != nil {
-               ctx.ServerError("UpdateEvent", err)
-               return
-       } else if err := models.CreateWebhook(w); err != nil {
-               ctx.ServerError("CreateWebhook", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
-       ctx.Redirect(orCtx.Link)
-}
-
-// DiscordHooksNewPost response for creating discord hook
-func DiscordHooksNewPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.NewDiscordHookForm)
-       ctx.Data["Title"] = ctx.Tr("repo.settings")
-       ctx.Data["PageIsSettingsHooks"] = true
-       ctx.Data["PageIsSettingsHooksNew"] = true
-       ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
-       ctx.Data["HookType"] = models.DISCORD
-
-       orCtx, err := getOrgRepoCtx(ctx)
-       if err != nil {
-               ctx.ServerError("getOrgRepoCtx", err)
-               return
-       }
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-               return
-       }
-
-       json := jsoniter.ConfigCompatibleWithStandardLibrary
-       meta, err := json.Marshal(&webhook.DiscordMeta{
-               Username: form.Username,
-               IconURL:  form.IconURL,
-       })
-       if err != nil {
-               ctx.ServerError("Marshal", err)
-               return
-       }
-
-       w := &models.Webhook{
-               RepoID:          orCtx.RepoID,
-               URL:             form.PayloadURL,
-               ContentType:     models.ContentTypeJSON,
-               HookEvent:       ParseHookEvent(form.WebhookForm),
-               IsActive:        form.Active,
-               Type:            models.DISCORD,
-               Meta:            string(meta),
-               OrgID:           orCtx.OrgID,
-               IsSystemWebhook: orCtx.IsSystemWebhook,
-       }
-       if err := w.UpdateEvent(); err != nil {
-               ctx.ServerError("UpdateEvent", err)
-               return
-       } else if err := models.CreateWebhook(w); err != nil {
-               ctx.ServerError("CreateWebhook", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
-       ctx.Redirect(orCtx.Link)
-}
-
-// DingtalkHooksNewPost response for creating dingtalk hook
-func DingtalkHooksNewPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.NewDingtalkHookForm)
-       ctx.Data["Title"] = ctx.Tr("repo.settings")
-       ctx.Data["PageIsSettingsHooks"] = true
-       ctx.Data["PageIsSettingsHooksNew"] = true
-       ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
-       ctx.Data["HookType"] = models.DINGTALK
-
-       orCtx, err := getOrgRepoCtx(ctx)
-       if err != nil {
-               ctx.ServerError("getOrgRepoCtx", err)
-               return
-       }
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-               return
-       }
-
-       w := &models.Webhook{
-               RepoID:          orCtx.RepoID,
-               URL:             form.PayloadURL,
-               ContentType:     models.ContentTypeJSON,
-               HookEvent:       ParseHookEvent(form.WebhookForm),
-               IsActive:        form.Active,
-               Type:            models.DINGTALK,
-               Meta:            "",
-               OrgID:           orCtx.OrgID,
-               IsSystemWebhook: orCtx.IsSystemWebhook,
-       }
-       if err := w.UpdateEvent(); err != nil {
-               ctx.ServerError("UpdateEvent", err)
-               return
-       } else if err := models.CreateWebhook(w); err != nil {
-               ctx.ServerError("CreateWebhook", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
-       ctx.Redirect(orCtx.Link)
-}
-
-// TelegramHooksNewPost response for creating telegram hook
-func TelegramHooksNewPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.NewTelegramHookForm)
-       ctx.Data["Title"] = ctx.Tr("repo.settings")
-       ctx.Data["PageIsSettingsHooks"] = true
-       ctx.Data["PageIsSettingsHooksNew"] = true
-       ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
-       ctx.Data["HookType"] = models.TELEGRAM
-
-       orCtx, err := getOrgRepoCtx(ctx)
-       if err != nil {
-               ctx.ServerError("getOrgRepoCtx", err)
-               return
-       }
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-               return
-       }
-
-       json := jsoniter.ConfigCompatibleWithStandardLibrary
-       meta, err := json.Marshal(&webhook.TelegramMeta{
-               BotToken: form.BotToken,
-               ChatID:   form.ChatID,
-       })
-       if err != nil {
-               ctx.ServerError("Marshal", err)
-               return
-       }
-
-       w := &models.Webhook{
-               RepoID:          orCtx.RepoID,
-               URL:             fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s", form.BotToken, form.ChatID),
-               ContentType:     models.ContentTypeJSON,
-               HookEvent:       ParseHookEvent(form.WebhookForm),
-               IsActive:        form.Active,
-               Type:            models.TELEGRAM,
-               Meta:            string(meta),
-               OrgID:           orCtx.OrgID,
-               IsSystemWebhook: orCtx.IsSystemWebhook,
-       }
-       if err := w.UpdateEvent(); err != nil {
-               ctx.ServerError("UpdateEvent", err)
-               return
-       } else if err := models.CreateWebhook(w); err != nil {
-               ctx.ServerError("CreateWebhook", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
-       ctx.Redirect(orCtx.Link)
-}
-
-// MatrixHooksNewPost response for creating a Matrix hook
-func MatrixHooksNewPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.NewMatrixHookForm)
-       ctx.Data["Title"] = ctx.Tr("repo.settings")
-       ctx.Data["PageIsSettingsHooks"] = true
-       ctx.Data["PageIsSettingsHooksNew"] = true
-       ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
-       ctx.Data["HookType"] = models.MATRIX
-
-       orCtx, err := getOrgRepoCtx(ctx)
-       if err != nil {
-               ctx.ServerError("getOrgRepoCtx", err)
-               return
-       }
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-               return
-       }
-
-       json := jsoniter.ConfigCompatibleWithStandardLibrary
-       meta, err := json.Marshal(&webhook.MatrixMeta{
-               HomeserverURL: form.HomeserverURL,
-               Room:          form.RoomID,
-               AccessToken:   form.AccessToken,
-               MessageType:   form.MessageType,
-       })
-       if err != nil {
-               ctx.ServerError("Marshal", err)
-               return
-       }
-
-       w := &models.Webhook{
-               RepoID:          orCtx.RepoID,
-               URL:             fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, form.RoomID),
-               ContentType:     models.ContentTypeJSON,
-               HTTPMethod:      "PUT",
-               HookEvent:       ParseHookEvent(form.WebhookForm),
-               IsActive:        form.Active,
-               Type:            models.MATRIX,
-               Meta:            string(meta),
-               OrgID:           orCtx.OrgID,
-               IsSystemWebhook: orCtx.IsSystemWebhook,
-       }
-       if err := w.UpdateEvent(); err != nil {
-               ctx.ServerError("UpdateEvent", err)
-               return
-       } else if err := models.CreateWebhook(w); err != nil {
-               ctx.ServerError("CreateWebhook", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
-       ctx.Redirect(orCtx.Link)
-}
-
-// MSTeamsHooksNewPost response for creating MS Teams hook
-func MSTeamsHooksNewPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.NewMSTeamsHookForm)
-       ctx.Data["Title"] = ctx.Tr("repo.settings")
-       ctx.Data["PageIsSettingsHooks"] = true
-       ctx.Data["PageIsSettingsHooksNew"] = true
-       ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
-       ctx.Data["HookType"] = models.MSTEAMS
-
-       orCtx, err := getOrgRepoCtx(ctx)
-       if err != nil {
-               ctx.ServerError("getOrgRepoCtx", err)
-               return
-       }
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-               return
-       }
-
-       w := &models.Webhook{
-               RepoID:          orCtx.RepoID,
-               URL:             form.PayloadURL,
-               ContentType:     models.ContentTypeJSON,
-               HookEvent:       ParseHookEvent(form.WebhookForm),
-               IsActive:        form.Active,
-               Type:            models.MSTEAMS,
-               Meta:            "",
-               OrgID:           orCtx.OrgID,
-               IsSystemWebhook: orCtx.IsSystemWebhook,
-       }
-       if err := w.UpdateEvent(); err != nil {
-               ctx.ServerError("UpdateEvent", err)
-               return
-       } else if err := models.CreateWebhook(w); err != nil {
-               ctx.ServerError("CreateWebhook", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
-       ctx.Redirect(orCtx.Link)
-}
-
-// SlackHooksNewPost response for creating slack hook
-func SlackHooksNewPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.NewSlackHookForm)
-       ctx.Data["Title"] = ctx.Tr("repo.settings")
-       ctx.Data["PageIsSettingsHooks"] = true
-       ctx.Data["PageIsSettingsHooksNew"] = true
-       ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
-       ctx.Data["HookType"] = models.SLACK
-
-       orCtx, err := getOrgRepoCtx(ctx)
-       if err != nil {
-               ctx.ServerError("getOrgRepoCtx", err)
-               return
-       }
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-               return
-       }
-
-       if form.HasInvalidChannel() {
-               ctx.Flash.Error(ctx.Tr("repo.settings.add_webhook.invalid_channel_name"))
-               ctx.Redirect(orCtx.LinkNew + "/slack/new")
-               return
-       }
-
-       json := jsoniter.ConfigCompatibleWithStandardLibrary
-       meta, err := json.Marshal(&webhook.SlackMeta{
-               Channel:  strings.TrimSpace(form.Channel),
-               Username: form.Username,
-               IconURL:  form.IconURL,
-               Color:    form.Color,
-       })
-       if err != nil {
-               ctx.ServerError("Marshal", err)
-               return
-       }
-
-       w := &models.Webhook{
-               RepoID:          orCtx.RepoID,
-               URL:             form.PayloadURL,
-               ContentType:     models.ContentTypeJSON,
-               HookEvent:       ParseHookEvent(form.WebhookForm),
-               IsActive:        form.Active,
-               Type:            models.SLACK,
-               Meta:            string(meta),
-               OrgID:           orCtx.OrgID,
-               IsSystemWebhook: orCtx.IsSystemWebhook,
-       }
-       if err := w.UpdateEvent(); err != nil {
-               ctx.ServerError("UpdateEvent", err)
-               return
-       } else if err := models.CreateWebhook(w); err != nil {
-               ctx.ServerError("CreateWebhook", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
-       ctx.Redirect(orCtx.Link)
-}
-
-// FeishuHooksNewPost response for creating feishu hook
-func FeishuHooksNewPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.NewFeishuHookForm)
-       ctx.Data["Title"] = ctx.Tr("repo.settings")
-       ctx.Data["PageIsSettingsHooks"] = true
-       ctx.Data["PageIsSettingsHooksNew"] = true
-       ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
-       ctx.Data["HookType"] = models.FEISHU
-
-       orCtx, err := getOrgRepoCtx(ctx)
-       if err != nil {
-               ctx.ServerError("getOrgRepoCtx", err)
-               return
-       }
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-               return
-       }
-
-       w := &models.Webhook{
-               RepoID:          orCtx.RepoID,
-               URL:             form.PayloadURL,
-               ContentType:     models.ContentTypeJSON,
-               HookEvent:       ParseHookEvent(form.WebhookForm),
-               IsActive:        form.Active,
-               Type:            models.FEISHU,
-               Meta:            "",
-               OrgID:           orCtx.OrgID,
-               IsSystemWebhook: orCtx.IsSystemWebhook,
-       }
-       if err := w.UpdateEvent(); err != nil {
-               ctx.ServerError("UpdateEvent", err)
-               return
-       } else if err := models.CreateWebhook(w); err != nil {
-               ctx.ServerError("CreateWebhook", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
-       ctx.Redirect(orCtx.Link)
-}
-
-func checkWebhook(ctx *context.Context) (*orgRepoCtx, *models.Webhook) {
-       ctx.Data["RequireHighlightJS"] = true
-
-       orCtx, err := getOrgRepoCtx(ctx)
-       if err != nil {
-               ctx.ServerError("getOrgRepoCtx", err)
-               return nil, nil
-       }
-       ctx.Data["BaseLink"] = orCtx.Link
-
-       var w *models.Webhook
-       if orCtx.RepoID > 0 {
-               w, err = models.GetWebhookByRepoID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
-       } else if orCtx.OrgID > 0 {
-               w, err = models.GetWebhookByOrgID(ctx.Org.Organization.ID, ctx.ParamsInt64(":id"))
-       } else if orCtx.IsAdmin {
-               w, err = models.GetSystemOrDefaultWebhook(ctx.ParamsInt64(":id"))
-       }
-       if err != nil || w == nil {
-               if models.IsErrWebhookNotExist(err) {
-                       ctx.NotFound("GetWebhookByID", nil)
-               } else {
-                       ctx.ServerError("GetWebhookByID", err)
-               }
-               return nil, nil
-       }
-
-       ctx.Data["HookType"] = w.Type
-       switch w.Type {
-       case models.SLACK:
-               ctx.Data["SlackHook"] = webhook.GetSlackHook(w)
-       case models.DISCORD:
-               ctx.Data["DiscordHook"] = webhook.GetDiscordHook(w)
-       case models.TELEGRAM:
-               ctx.Data["TelegramHook"] = webhook.GetTelegramHook(w)
-       case models.MATRIX:
-               ctx.Data["MatrixHook"] = webhook.GetMatrixHook(w)
-       }
-
-       ctx.Data["History"], err = w.History(1)
-       if err != nil {
-               ctx.ServerError("History", err)
-       }
-       return orCtx, w
-}
-
-// WebHooksEdit render editing web hook page
-func WebHooksEdit(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.settings.update_webhook")
-       ctx.Data["PageIsSettingsHooks"] = true
-       ctx.Data["PageIsSettingsHooksEdit"] = true
-
-       orCtx, w := checkWebhook(ctx)
-       if ctx.Written() {
-               return
-       }
-       ctx.Data["Webhook"] = w
-
-       ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-}
-
-// WebHooksEditPost response for editing web hook
-func WebHooksEditPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.NewWebhookForm)
-       ctx.Data["Title"] = ctx.Tr("repo.settings.update_webhook")
-       ctx.Data["PageIsSettingsHooks"] = true
-       ctx.Data["PageIsSettingsHooksEdit"] = true
-
-       orCtx, w := checkWebhook(ctx)
-       if ctx.Written() {
-               return
-       }
-       ctx.Data["Webhook"] = w
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-               return
-       }
-
-       contentType := models.ContentTypeJSON
-       if models.HookContentType(form.ContentType) == models.ContentTypeForm {
-               contentType = models.ContentTypeForm
-       }
-
-       w.URL = form.PayloadURL
-       w.ContentType = contentType
-       w.Secret = form.Secret
-       w.HookEvent = ParseHookEvent(form.WebhookForm)
-       w.IsActive = form.Active
-       w.HTTPMethod = form.HTTPMethod
-       if err := w.UpdateEvent(); err != nil {
-               ctx.ServerError("UpdateEvent", err)
-               return
-       } else if err := models.UpdateWebhook(w); err != nil {
-               ctx.ServerError("WebHooksEditPost", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
-       ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-}
-
-// GogsHooksEditPost response for editing gogs hook
-func GogsHooksEditPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.NewGogshookForm)
-       ctx.Data["Title"] = ctx.Tr("repo.settings.update_webhook")
-       ctx.Data["PageIsSettingsHooks"] = true
-       ctx.Data["PageIsSettingsHooksEdit"] = true
-
-       orCtx, w := checkWebhook(ctx)
-       if ctx.Written() {
-               return
-       }
-       ctx.Data["Webhook"] = w
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-               return
-       }
-
-       contentType := models.ContentTypeJSON
-       if models.HookContentType(form.ContentType) == models.ContentTypeForm {
-               contentType = models.ContentTypeForm
-       }
-
-       w.URL = form.PayloadURL
-       w.ContentType = contentType
-       w.Secret = form.Secret
-       w.HookEvent = ParseHookEvent(form.WebhookForm)
-       w.IsActive = form.Active
-       if err := w.UpdateEvent(); err != nil {
-               ctx.ServerError("UpdateEvent", err)
-               return
-       } else if err := models.UpdateWebhook(w); err != nil {
-               ctx.ServerError("GogsHooksEditPost", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
-       ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-}
-
-// SlackHooksEditPost response for editing slack hook
-func SlackHooksEditPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.NewSlackHookForm)
-       ctx.Data["Title"] = ctx.Tr("repo.settings")
-       ctx.Data["PageIsSettingsHooks"] = true
-       ctx.Data["PageIsSettingsHooksEdit"] = true
-
-       orCtx, w := checkWebhook(ctx)
-       if ctx.Written() {
-               return
-       }
-       ctx.Data["Webhook"] = w
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-               return
-       }
-
-       if form.HasInvalidChannel() {
-               ctx.Flash.Error(ctx.Tr("repo.settings.add_webhook.invalid_channel_name"))
-               ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-               return
-       }
-
-       json := jsoniter.ConfigCompatibleWithStandardLibrary
-       meta, err := json.Marshal(&webhook.SlackMeta{
-               Channel:  strings.TrimSpace(form.Channel),
-               Username: form.Username,
-               IconURL:  form.IconURL,
-               Color:    form.Color,
-       })
-       if err != nil {
-               ctx.ServerError("Marshal", err)
-               return
-       }
-
-       w.URL = form.PayloadURL
-       w.Meta = string(meta)
-       w.HookEvent = ParseHookEvent(form.WebhookForm)
-       w.IsActive = form.Active
-       if err := w.UpdateEvent(); err != nil {
-               ctx.ServerError("UpdateEvent", err)
-               return
-       } else if err := models.UpdateWebhook(w); err != nil {
-               ctx.ServerError("UpdateWebhook", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
-       ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-}
-
-// DiscordHooksEditPost response for editing discord hook
-func DiscordHooksEditPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.NewDiscordHookForm)
-       ctx.Data["Title"] = ctx.Tr("repo.settings")
-       ctx.Data["PageIsSettingsHooks"] = true
-       ctx.Data["PageIsSettingsHooksEdit"] = true
-
-       orCtx, w := checkWebhook(ctx)
-       if ctx.Written() {
-               return
-       }
-       ctx.Data["Webhook"] = w
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-               return
-       }
-
-       json := jsoniter.ConfigCompatibleWithStandardLibrary
-       meta, err := json.Marshal(&webhook.DiscordMeta{
-               Username: form.Username,
-               IconURL:  form.IconURL,
-       })
-       if err != nil {
-               ctx.ServerError("Marshal", err)
-               return
-       }
-
-       w.URL = form.PayloadURL
-       w.Meta = string(meta)
-       w.HookEvent = ParseHookEvent(form.WebhookForm)
-       w.IsActive = form.Active
-       if err := w.UpdateEvent(); err != nil {
-               ctx.ServerError("UpdateEvent", err)
-               return
-       } else if err := models.UpdateWebhook(w); err != nil {
-               ctx.ServerError("UpdateWebhook", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
-       ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-}
-
-// DingtalkHooksEditPost response for editing discord hook
-func DingtalkHooksEditPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.NewDingtalkHookForm)
-       ctx.Data["Title"] = ctx.Tr("repo.settings")
-       ctx.Data["PageIsSettingsHooks"] = true
-       ctx.Data["PageIsSettingsHooksEdit"] = true
-
-       orCtx, w := checkWebhook(ctx)
-       if ctx.Written() {
-               return
-       }
-       ctx.Data["Webhook"] = w
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-               return
-       }
-
-       w.URL = form.PayloadURL
-       w.HookEvent = ParseHookEvent(form.WebhookForm)
-       w.IsActive = form.Active
-       if err := w.UpdateEvent(); err != nil {
-               ctx.ServerError("UpdateEvent", err)
-               return
-       } else if err := models.UpdateWebhook(w); err != nil {
-               ctx.ServerError("UpdateWebhook", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
-       ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-}
-
-// TelegramHooksEditPost response for editing discord hook
-func TelegramHooksEditPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.NewTelegramHookForm)
-       ctx.Data["Title"] = ctx.Tr("repo.settings")
-       ctx.Data["PageIsSettingsHooks"] = true
-       ctx.Data["PageIsSettingsHooksEdit"] = true
-
-       orCtx, w := checkWebhook(ctx)
-       if ctx.Written() {
-               return
-       }
-       ctx.Data["Webhook"] = w
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-               return
-       }
-       json := jsoniter.ConfigCompatibleWithStandardLibrary
-       meta, err := json.Marshal(&webhook.TelegramMeta{
-               BotToken: form.BotToken,
-               ChatID:   form.ChatID,
-       })
-       if err != nil {
-               ctx.ServerError("Marshal", err)
-               return
-       }
-       w.Meta = string(meta)
-       w.URL = fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s", form.BotToken, form.ChatID)
-       w.HookEvent = ParseHookEvent(form.WebhookForm)
-       w.IsActive = form.Active
-       if err := w.UpdateEvent(); err != nil {
-               ctx.ServerError("UpdateEvent", err)
-               return
-       } else if err := models.UpdateWebhook(w); err != nil {
-               ctx.ServerError("UpdateWebhook", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
-       ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-}
-
-// MatrixHooksEditPost response for editing a Matrix hook
-func MatrixHooksEditPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.NewMatrixHookForm)
-       ctx.Data["Title"] = ctx.Tr("repo.settings")
-       ctx.Data["PageIsSettingsHooks"] = true
-       ctx.Data["PageIsSettingsHooksEdit"] = true
-
-       orCtx, w := checkWebhook(ctx)
-       if ctx.Written() {
-               return
-       }
-       ctx.Data["Webhook"] = w
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-               return
-       }
-       json := jsoniter.ConfigCompatibleWithStandardLibrary
-       meta, err := json.Marshal(&webhook.MatrixMeta{
-               HomeserverURL: form.HomeserverURL,
-               Room:          form.RoomID,
-               AccessToken:   form.AccessToken,
-               MessageType:   form.MessageType,
-       })
-       if err != nil {
-               ctx.ServerError("Marshal", err)
-               return
-       }
-       w.Meta = string(meta)
-       w.URL = fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, form.RoomID)
-
-       w.HookEvent = ParseHookEvent(form.WebhookForm)
-       w.IsActive = form.Active
-       if err := w.UpdateEvent(); err != nil {
-               ctx.ServerError("UpdateEvent", err)
-               return
-       } else if err := models.UpdateWebhook(w); err != nil {
-               ctx.ServerError("UpdateWebhook", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
-       ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-}
-
-// MSTeamsHooksEditPost response for editing MS Teams hook
-func MSTeamsHooksEditPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.NewMSTeamsHookForm)
-       ctx.Data["Title"] = ctx.Tr("repo.settings")
-       ctx.Data["PageIsSettingsHooks"] = true
-       ctx.Data["PageIsSettingsHooksEdit"] = true
-
-       orCtx, w := checkWebhook(ctx)
-       if ctx.Written() {
-               return
-       }
-       ctx.Data["Webhook"] = w
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-               return
-       }
-
-       w.URL = form.PayloadURL
-       w.HookEvent = ParseHookEvent(form.WebhookForm)
-       w.IsActive = form.Active
-       if err := w.UpdateEvent(); err != nil {
-               ctx.ServerError("UpdateEvent", err)
-               return
-       } else if err := models.UpdateWebhook(w); err != nil {
-               ctx.ServerError("UpdateWebhook", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
-       ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-}
-
-// FeishuHooksEditPost response for editing feishu hook
-func FeishuHooksEditPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.NewFeishuHookForm)
-       ctx.Data["Title"] = ctx.Tr("repo.settings")
-       ctx.Data["PageIsSettingsHooks"] = true
-       ctx.Data["PageIsSettingsHooksEdit"] = true
-
-       orCtx, w := checkWebhook(ctx)
-       if ctx.Written() {
-               return
-       }
-       ctx.Data["Webhook"] = w
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-               return
-       }
-
-       w.URL = form.PayloadURL
-       w.HookEvent = ParseHookEvent(form.WebhookForm)
-       w.IsActive = form.Active
-       if err := w.UpdateEvent(); err != nil {
-               ctx.ServerError("UpdateEvent", err)
-               return
-       } else if err := models.UpdateWebhook(w); err != nil {
-               ctx.ServerError("UpdateWebhook", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
-       ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-}
-
-// TestWebhook test if web hook is work fine
-func TestWebhook(ctx *context.Context) {
-       hookID := ctx.ParamsInt64(":id")
-       w, err := models.GetWebhookByRepoID(ctx.Repo.Repository.ID, hookID)
-       if err != nil {
-               ctx.Flash.Error("GetWebhookByID: " + err.Error())
-               ctx.Status(500)
-               return
-       }
-
-       // Grab latest commit or fake one if it's empty repository.
-       commit := ctx.Repo.Commit
-       if commit == nil {
-               ghost := models.NewGhostUser()
-               commit = &git.Commit{
-                       ID:            git.MustIDFromString(git.EmptySHA),
-                       Author:        ghost.NewGitSig(),
-                       Committer:     ghost.NewGitSig(),
-                       CommitMessage: "This is a fake commit",
-               }
-       }
-
-       apiUser := convert.ToUserWithAccessMode(ctx.User, models.AccessModeNone)
-       p := &api.PushPayload{
-               Ref:    git.BranchPrefix + ctx.Repo.Repository.DefaultBranch,
-               Before: commit.ID.String(),
-               After:  commit.ID.String(),
-               Commits: []*api.PayloadCommit{
-                       {
-                               ID:      commit.ID.String(),
-                               Message: commit.Message(),
-                               URL:     ctx.Repo.Repository.HTMLURL() + "/commit/" + commit.ID.String(),
-                               Author: &api.PayloadUser{
-                                       Name:  commit.Author.Name,
-                                       Email: commit.Author.Email,
-                               },
-                               Committer: &api.PayloadUser{
-                                       Name:  commit.Committer.Name,
-                                       Email: commit.Committer.Email,
-                               },
-                       },
-               },
-               Repo:   convert.ToRepo(ctx.Repo.Repository, models.AccessModeNone),
-               Pusher: apiUser,
-               Sender: apiUser,
-       }
-       if err := webhook.PrepareWebhook(w, ctx.Repo.Repository, models.HookEventPush, p); err != nil {
-               ctx.Flash.Error("PrepareWebhook: " + err.Error())
-               ctx.Status(500)
-       } else {
-               ctx.Flash.Info(ctx.Tr("repo.settings.webhook.test_delivery_success"))
-               ctx.Status(200)
-       }
-}
-
-// DeleteWebhook delete a webhook
-func DeleteWebhook(ctx *context.Context) {
-       if err := models.DeleteWebhookByRepoID(ctx.Repo.Repository.ID, ctx.QueryInt64("id")); err != nil {
-               ctx.Flash.Error("DeleteWebhookByRepoID: " + err.Error())
-       } else {
-               ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": ctx.Repo.RepoLink + "/settings/hooks",
-       })
-}
diff --git a/routers/repo/wiki.go b/routers/repo/wiki.go
deleted file mode 100644 (file)
index 1bdd06d..0000000
+++ /dev/null
@@ -1,683 +0,0 @@
-// Copyright 2015 The Gogs Authors. All rights reserved.
-// Copyright 2018 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 repo
-
-import (
-       "bytes"
-       "fmt"
-       "io/ioutil"
-       "net/http"
-       "net/url"
-       "path/filepath"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/git"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/markup"
-       "code.gitea.io/gitea/modules/markup/markdown"
-       "code.gitea.io/gitea/modules/timeutil"
-       "code.gitea.io/gitea/modules/util"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-       wiki_service "code.gitea.io/gitea/services/wiki"
-)
-
-const (
-       tplWikiStart    base.TplName = "repo/wiki/start"
-       tplWikiView     base.TplName = "repo/wiki/view"
-       tplWikiRevision base.TplName = "repo/wiki/revision"
-       tplWikiNew      base.TplName = "repo/wiki/new"
-       tplWikiPages    base.TplName = "repo/wiki/pages"
-)
-
-// MustEnableWiki check if wiki is enabled, if external then redirect
-func MustEnableWiki(ctx *context.Context) {
-       if !ctx.Repo.CanRead(models.UnitTypeWiki) &&
-               !ctx.Repo.CanRead(models.UnitTypeExternalWiki) {
-               if log.IsTrace() {
-                       log.Trace("Permission Denied: User %-v cannot read %-v or %-v of repo %-v\n"+
-                               "User in repo has Permissions: %-+v",
-                               ctx.User,
-                               models.UnitTypeWiki,
-                               models.UnitTypeExternalWiki,
-                               ctx.Repo.Repository,
-                               ctx.Repo.Permission)
-               }
-               ctx.NotFound("MustEnableWiki", nil)
-               return
-       }
-
-       unit, err := ctx.Repo.Repository.GetUnit(models.UnitTypeExternalWiki)
-       if err == nil {
-               ctx.Redirect(unit.ExternalWikiConfig().ExternalWikiURL)
-               return
-       }
-}
-
-// PageMeta wiki page meta information
-type PageMeta struct {
-       Name        string
-       SubURL      string
-       UpdatedUnix timeutil.TimeStamp
-}
-
-// findEntryForFile finds the tree entry for a target filepath.
-func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) {
-       entry, err := commit.GetTreeEntryByPath(target)
-       if err != nil && !git.IsErrNotExist(err) {
-               return nil, err
-       }
-       if entry != nil {
-               return entry, nil
-       }
-
-       // Then the unescaped, shortest alternative
-       var unescapedTarget string
-       if unescapedTarget, err = url.QueryUnescape(target); err != nil {
-               return nil, err
-       }
-       return commit.GetTreeEntryByPath(unescapedTarget)
-}
-
-func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, error) {
-       wikiRepo, err := git.OpenRepository(ctx.Repo.Repository.WikiPath())
-       if err != nil {
-               ctx.ServerError("OpenRepository", err)
-               return nil, nil, err
-       }
-
-       commit, err := wikiRepo.GetBranchCommit("master")
-       if err != nil {
-               return wikiRepo, nil, err
-       }
-       return wikiRepo, commit, nil
-}
-
-// wikiContentsByEntry returns the contents of the wiki page referenced by the
-// given tree entry. Writes to ctx if an error occurs.
-func wikiContentsByEntry(ctx *context.Context, entry *git.TreeEntry) []byte {
-       reader, err := entry.Blob().DataAsync()
-       if err != nil {
-               ctx.ServerError("Blob.Data", err)
-               return nil
-       }
-       defer reader.Close()
-       content, err := ioutil.ReadAll(reader)
-       if err != nil {
-               ctx.ServerError("ReadAll", err)
-               return nil
-       }
-       return content
-}
-
-// wikiContentsByName returns the contents of a wiki page, along with a boolean
-// indicating whether the page exists. Writes to ctx if an error occurs.
-func wikiContentsByName(ctx *context.Context, commit *git.Commit, wikiName string) ([]byte, *git.TreeEntry, string, bool) {
-       pageFilename := wiki_service.NameToFilename(wikiName)
-       entry, err := findEntryForFile(commit, pageFilename)
-       if err != nil && !git.IsErrNotExist(err) {
-               ctx.ServerError("findEntryForFile", err)
-               return nil, nil, "", false
-       } else if entry == nil {
-               return nil, nil, "", true
-       }
-       return wikiContentsByEntry(ctx, entry), entry, pageFilename, false
-}
-
-func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
-       wikiRepo, commit, err := findWikiRepoCommit(ctx)
-       if err != nil {
-               if !git.IsErrNotExist(err) {
-                       ctx.ServerError("GetBranchCommit", err)
-               }
-               return nil, nil
-       }
-
-       // Get page list.
-       entries, err := commit.ListEntries()
-       if err != nil {
-               if wikiRepo != nil {
-                       wikiRepo.Close()
-               }
-               ctx.ServerError("ListEntries", err)
-               return nil, nil
-       }
-       pages := make([]PageMeta, 0, len(entries))
-       for _, entry := range entries {
-               if !entry.IsRegular() {
-                       continue
-               }
-               wikiName, err := wiki_service.FilenameToName(entry.Name())
-               if err != nil {
-                       if models.IsErrWikiInvalidFileName(err) {
-                               continue
-                       }
-                       if wikiRepo != nil {
-                               wikiRepo.Close()
-                       }
-                       ctx.ServerError("WikiFilenameToName", err)
-                       return nil, nil
-               } else if wikiName == "_Sidebar" || wikiName == "_Footer" {
-                       continue
-               }
-               pages = append(pages, PageMeta{
-                       Name:   wikiName,
-                       SubURL: wiki_service.NameToSubURL(wikiName),
-               })
-       }
-       ctx.Data["Pages"] = pages
-
-       // get requested pagename
-       pageName := wiki_service.NormalizeWikiName(ctx.Params(":page"))
-       if len(pageName) == 0 {
-               pageName = "Home"
-       }
-       ctx.Data["PageURL"] = wiki_service.NameToSubURL(pageName)
-       ctx.Data["old_title"] = pageName
-       ctx.Data["Title"] = pageName
-       ctx.Data["title"] = pageName
-       ctx.Data["RequireHighlightJS"] = true
-
-       //lookup filename in wiki - get filecontent, gitTree entry , real filename
-       data, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName)
-       if noEntry {
-               ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages")
-       }
-       if entry == nil || ctx.Written() {
-               if wikiRepo != nil {
-                       wikiRepo.Close()
-               }
-               return nil, nil
-       }
-
-       sidebarContent, _, _, _ := wikiContentsByName(ctx, commit, "_Sidebar")
-       if ctx.Written() {
-               if wikiRepo != nil {
-                       wikiRepo.Close()
-               }
-               return nil, nil
-       }
-
-       footerContent, _, _, _ := wikiContentsByName(ctx, commit, "_Footer")
-       if ctx.Written() {
-               if wikiRepo != nil {
-                       wikiRepo.Close()
-               }
-               return nil, nil
-       }
-
-       var rctx = &markup.RenderContext{
-               URLPrefix: ctx.Repo.RepoLink,
-               Metas:     ctx.Repo.Repository.ComposeDocumentMetas(),
-               IsWiki:    true,
-       }
-
-       var buf strings.Builder
-       if err := markdown.Render(rctx, bytes.NewReader(data), &buf); err != nil {
-               ctx.ServerError("Render", err)
-               return nil, nil
-       }
-       ctx.Data["content"] = buf.String()
-
-       buf.Reset()
-       if err := markdown.Render(rctx, bytes.NewReader(sidebarContent), &buf); err != nil {
-               ctx.ServerError("Render", err)
-               return nil, nil
-       }
-       ctx.Data["sidebarPresent"] = sidebarContent != nil
-       ctx.Data["sidebarContent"] = buf.String()
-
-       buf.Reset()
-       if err := markdown.Render(rctx, bytes.NewReader(footerContent), &buf); err != nil {
-               ctx.ServerError("Render", err)
-               return nil, nil
-       }
-       ctx.Data["footerPresent"] = footerContent != nil
-       ctx.Data["footerContent"] = buf.String()
-
-       // get commit count - wiki revisions
-       commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
-       ctx.Data["CommitCount"] = commitsCount
-
-       return wikiRepo, entry
-}
-
-func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
-       wikiRepo, commit, err := findWikiRepoCommit(ctx)
-       if err != nil {
-               if wikiRepo != nil {
-                       wikiRepo.Close()
-               }
-               if !git.IsErrNotExist(err) {
-                       ctx.ServerError("GetBranchCommit", err)
-               }
-               return nil, nil
-       }
-
-       // get requested pagename
-       pageName := wiki_service.NormalizeWikiName(ctx.Params(":page"))
-       if len(pageName) == 0 {
-               pageName = "Home"
-       }
-       ctx.Data["PageURL"] = wiki_service.NameToSubURL(pageName)
-       ctx.Data["old_title"] = pageName
-       ctx.Data["Title"] = pageName
-       ctx.Data["title"] = pageName
-       ctx.Data["RequireHighlightJS"] = true
-       ctx.Data["Username"] = ctx.Repo.Owner.Name
-       ctx.Data["Reponame"] = ctx.Repo.Repository.Name
-
-       //lookup filename in wiki - get filecontent, gitTree entry , real filename
-       data, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName)
-       if noEntry {
-               ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages")
-       }
-       if entry == nil || ctx.Written() {
-               if wikiRepo != nil {
-                       wikiRepo.Close()
-               }
-               return nil, nil
-       }
-
-       ctx.Data["content"] = string(data)
-       ctx.Data["sidebarPresent"] = false
-       ctx.Data["sidebarContent"] = ""
-       ctx.Data["footerPresent"] = false
-       ctx.Data["footerContent"] = ""
-
-       // get commit count - wiki revisions
-       commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
-       ctx.Data["CommitCount"] = commitsCount
-
-       // get page
-       page := ctx.QueryInt("page")
-       if page <= 1 {
-               page = 1
-       }
-
-       // get Commit Count
-       commitsHistory, err := wikiRepo.CommitsByFileAndRangeNoFollow("master", pageFilename, page)
-       if err != nil {
-               if wikiRepo != nil {
-                       wikiRepo.Close()
-               }
-               ctx.ServerError("CommitsByFileAndRangeNoFollow", err)
-               return nil, nil
-       }
-       commitsHistory = models.ValidateCommitsWithEmails(commitsHistory)
-       commitsHistory = models.ParseCommitsWithSignature(commitsHistory, ctx.Repo.Repository)
-
-       ctx.Data["Commits"] = commitsHistory
-
-       pager := context.NewPagination(int(commitsCount), git.CommitsRangeSize, page, 5)
-       pager.SetDefaultParams(ctx)
-       ctx.Data["Page"] = pager
-
-       return wikiRepo, entry
-}
-
-func renderEditPage(ctx *context.Context) {
-       wikiRepo, commit, err := findWikiRepoCommit(ctx)
-       if err != nil {
-               if wikiRepo != nil {
-                       wikiRepo.Close()
-               }
-               if !git.IsErrNotExist(err) {
-                       ctx.ServerError("GetBranchCommit", err)
-               }
-               return
-       }
-       defer func() {
-               if wikiRepo != nil {
-                       wikiRepo.Close()
-               }
-       }()
-
-       // get requested pagename
-       pageName := wiki_service.NormalizeWikiName(ctx.Params(":page"))
-       if len(pageName) == 0 {
-               pageName = "Home"
-       }
-       ctx.Data["PageURL"] = wiki_service.NameToSubURL(pageName)
-       ctx.Data["old_title"] = pageName
-       ctx.Data["Title"] = pageName
-       ctx.Data["title"] = pageName
-       ctx.Data["RequireHighlightJS"] = true
-
-       //lookup filename in wiki - get filecontent, gitTree entry , real filename
-       data, entry, _, noEntry := wikiContentsByName(ctx, commit, pageName)
-       if noEntry {
-               ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages")
-       }
-       if entry == nil || ctx.Written() {
-               return
-       }
-
-       ctx.Data["content"] = string(data)
-       ctx.Data["sidebarPresent"] = false
-       ctx.Data["sidebarContent"] = ""
-       ctx.Data["footerPresent"] = false
-       ctx.Data["footerContent"] = ""
-}
-
-// Wiki renders single wiki page
-func Wiki(ctx *context.Context) {
-       ctx.Data["PageIsWiki"] = true
-       ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(models.UnitTypeWiki) && !ctx.Repo.Repository.IsArchived
-
-       if !ctx.Repo.Repository.HasWiki() {
-               ctx.Data["Title"] = ctx.Tr("repo.wiki")
-               ctx.HTML(http.StatusOK, tplWikiStart)
-               return
-       }
-
-       wikiRepo, entry := renderViewPage(ctx)
-       if ctx.Written() {
-               if wikiRepo != nil {
-                       wikiRepo.Close()
-               }
-               return
-       }
-       defer func() {
-               if wikiRepo != nil {
-                       wikiRepo.Close()
-               }
-       }()
-       if entry == nil {
-               ctx.Data["Title"] = ctx.Tr("repo.wiki")
-               ctx.HTML(http.StatusOK, tplWikiStart)
-               return
-       }
-
-       wikiPath := entry.Name()
-       if markup.Type(wikiPath) != markdown.MarkupName {
-               ext := strings.ToUpper(filepath.Ext(wikiPath))
-               ctx.Data["FormatWarning"] = fmt.Sprintf("%s rendering is not supported at the moment. Rendered as Markdown.", ext)
-       }
-       // Get last change information.
-       lastCommit, err := wikiRepo.GetCommitByPath(wikiPath)
-       if err != nil {
-               ctx.ServerError("GetCommitByPath", err)
-               return
-       }
-       ctx.Data["Author"] = lastCommit.Author
-
-       ctx.HTML(http.StatusOK, tplWikiView)
-}
-
-// WikiRevision renders file revision list of wiki page
-func WikiRevision(ctx *context.Context) {
-       ctx.Data["PageIsWiki"] = true
-       ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(models.UnitTypeWiki) && !ctx.Repo.Repository.IsArchived
-
-       if !ctx.Repo.Repository.HasWiki() {
-               ctx.Data["Title"] = ctx.Tr("repo.wiki")
-               ctx.HTML(http.StatusOK, tplWikiStart)
-               return
-       }
-
-       wikiRepo, entry := renderRevisionPage(ctx)
-       if ctx.Written() {
-               if wikiRepo != nil {
-                       wikiRepo.Close()
-               }
-               return
-       }
-       defer func() {
-               if wikiRepo != nil {
-                       wikiRepo.Close()
-               }
-       }()
-       if entry == nil {
-               ctx.Data["Title"] = ctx.Tr("repo.wiki")
-               ctx.HTML(http.StatusOK, tplWikiStart)
-               return
-       }
-
-       // Get last change information.
-       wikiPath := entry.Name()
-       lastCommit, err := wikiRepo.GetCommitByPath(wikiPath)
-       if err != nil {
-               ctx.ServerError("GetCommitByPath", err)
-               return
-       }
-       ctx.Data["Author"] = lastCommit.Author
-
-       ctx.HTML(http.StatusOK, tplWikiRevision)
-}
-
-// WikiPages render wiki pages list page
-func WikiPages(ctx *context.Context) {
-       if !ctx.Repo.Repository.HasWiki() {
-               ctx.Redirect(ctx.Repo.RepoLink + "/wiki")
-               return
-       }
-
-       ctx.Data["Title"] = ctx.Tr("repo.wiki.pages")
-       ctx.Data["PageIsWiki"] = true
-       ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(models.UnitTypeWiki) && !ctx.Repo.Repository.IsArchived
-
-       wikiRepo, commit, err := findWikiRepoCommit(ctx)
-       if err != nil {
-               if wikiRepo != nil {
-                       wikiRepo.Close()
-               }
-               return
-       }
-
-       entries, err := commit.ListEntries()
-       if err != nil {
-               if wikiRepo != nil {
-                       wikiRepo.Close()
-               }
-
-               ctx.ServerError("ListEntries", err)
-               return
-       }
-       pages := make([]PageMeta, 0, len(entries))
-       for _, entry := range entries {
-               if !entry.IsRegular() {
-                       continue
-               }
-               c, err := wikiRepo.GetCommitByPath(entry.Name())
-               if err != nil {
-                       if wikiRepo != nil {
-                               wikiRepo.Close()
-                       }
-
-                       ctx.ServerError("GetCommit", err)
-                       return
-               }
-               wikiName, err := wiki_service.FilenameToName(entry.Name())
-               if err != nil {
-                       if models.IsErrWikiInvalidFileName(err) {
-                               continue
-                       }
-                       if wikiRepo != nil {
-                               wikiRepo.Close()
-                       }
-
-                       ctx.ServerError("WikiFilenameToName", err)
-                       return
-               }
-               pages = append(pages, PageMeta{
-                       Name:        wikiName,
-                       SubURL:      wiki_service.NameToSubURL(wikiName),
-                       UpdatedUnix: timeutil.TimeStamp(c.Author.When.Unix()),
-               })
-       }
-       ctx.Data["Pages"] = pages
-
-       defer func() {
-               if wikiRepo != nil {
-                       wikiRepo.Close()
-               }
-       }()
-       ctx.HTML(http.StatusOK, tplWikiPages)
-}
-
-// WikiRaw outputs raw blob requested by user (image for example)
-func WikiRaw(ctx *context.Context) {
-       wikiRepo, commit, err := findWikiRepoCommit(ctx)
-       if err != nil {
-               if wikiRepo != nil {
-                       return
-               }
-       }
-
-       providedPath := ctx.Params("*")
-
-       var entry *git.TreeEntry
-       if commit != nil {
-               // Try to find a file with that name
-               entry, err = findEntryForFile(commit, providedPath)
-               if err != nil && !git.IsErrNotExist(err) {
-                       ctx.ServerError("findFile", err)
-                       return
-               }
-
-               if entry == nil {
-                       // Try to find a wiki page with that name
-                       if strings.HasSuffix(providedPath, ".md") {
-                               providedPath = providedPath[:len(providedPath)-3]
-                       }
-
-                       wikiPath := wiki_service.NameToFilename(providedPath)
-                       entry, err = findEntryForFile(commit, wikiPath)
-                       if err != nil && !git.IsErrNotExist(err) {
-                               ctx.ServerError("findFile", err)
-                               return
-                       }
-               }
-       }
-
-       if entry != nil {
-               if err = ServeBlob(ctx, entry.Blob()); err != nil {
-                       ctx.ServerError("ServeBlob", err)
-               }
-               return
-       }
-
-       ctx.NotFound("findEntryForFile", nil)
-}
-
-// NewWiki render wiki create page
-func NewWiki(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page")
-       ctx.Data["PageIsWiki"] = true
-       ctx.Data["RequireSimpleMDE"] = true
-
-       if !ctx.Repo.Repository.HasWiki() {
-               ctx.Data["title"] = "Home"
-       }
-
-       ctx.HTML(http.StatusOK, tplWikiNew)
-}
-
-// NewWikiPost response for wiki create request
-func NewWikiPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.NewWikiForm)
-       ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page")
-       ctx.Data["PageIsWiki"] = true
-       ctx.Data["RequireSimpleMDE"] = true
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplWikiNew)
-               return
-       }
-
-       if util.IsEmptyString(form.Title) {
-               ctx.RenderWithErr(ctx.Tr("repo.issues.new.title_empty"), tplWikiNew, form)
-               return
-       }
-
-       wikiName := wiki_service.NormalizeWikiName(form.Title)
-
-       if len(form.Message) == 0 {
-               form.Message = ctx.Tr("repo.editor.add", form.Title)
-       }
-
-       if err := wiki_service.AddWikiPage(ctx.User, ctx.Repo.Repository, wikiName, form.Content, form.Message); err != nil {
-               if models.IsErrWikiReservedName(err) {
-                       ctx.Data["Err_Title"] = true
-                       ctx.RenderWithErr(ctx.Tr("repo.wiki.reserved_page", wikiName), tplWikiNew, &form)
-               } else if models.IsErrWikiAlreadyExist(err) {
-                       ctx.Data["Err_Title"] = true
-                       ctx.RenderWithErr(ctx.Tr("repo.wiki.page_already_exists"), tplWikiNew, &form)
-               } else {
-                       ctx.ServerError("AddWikiPage", err)
-               }
-               return
-       }
-
-       ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + wiki_service.NameToSubURL(wikiName))
-}
-
-// EditWiki render wiki modify page
-func EditWiki(ctx *context.Context) {
-       ctx.Data["PageIsWiki"] = true
-       ctx.Data["PageIsWikiEdit"] = true
-       ctx.Data["RequireSimpleMDE"] = true
-
-       if !ctx.Repo.Repository.HasWiki() {
-               ctx.Redirect(ctx.Repo.RepoLink + "/wiki")
-               return
-       }
-
-       renderEditPage(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       ctx.HTML(http.StatusOK, tplWikiNew)
-}
-
-// EditWikiPost response for wiki modify request
-func EditWikiPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.NewWikiForm)
-       ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page")
-       ctx.Data["PageIsWiki"] = true
-       ctx.Data["RequireSimpleMDE"] = true
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplWikiNew)
-               return
-       }
-
-       oldWikiName := wiki_service.NormalizeWikiName(ctx.Params(":page"))
-       newWikiName := wiki_service.NormalizeWikiName(form.Title)
-
-       if len(form.Message) == 0 {
-               form.Message = ctx.Tr("repo.editor.update", form.Title)
-       }
-
-       if err := wiki_service.EditWikiPage(ctx.User, ctx.Repo.Repository, oldWikiName, newWikiName, form.Content, form.Message); err != nil {
-               ctx.ServerError("EditWikiPage", err)
-               return
-       }
-
-       ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + wiki_service.NameToSubURL(newWikiName))
-}
-
-// DeleteWikiPagePost delete wiki page
-func DeleteWikiPagePost(ctx *context.Context) {
-       wikiName := wiki_service.NormalizeWikiName(ctx.Params(":page"))
-       if len(wikiName) == 0 {
-               wikiName = "Home"
-       }
-
-       if err := wiki_service.DeleteWikiPage(ctx.User, ctx.Repo.Repository, wikiName); err != nil {
-               ctx.ServerError("DeleteWikiPage", err)
-               return
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": ctx.Repo.RepoLink + "/wiki/",
-       })
-}
diff --git a/routers/repo/wiki_test.go b/routers/repo/wiki_test.go
deleted file mode 100644 (file)
index 8934a66..0000000
+++ /dev/null
@@ -1,215 +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 repo
-
-import (
-       "io/ioutil"
-       "net/http"
-       "testing"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/git"
-       "code.gitea.io/gitea/modules/test"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-       wiki_service "code.gitea.io/gitea/services/wiki"
-
-       "github.com/stretchr/testify/assert"
-)
-
-const content = "Wiki contents for unit tests"
-const message = "Wiki commit message for unit tests"
-
-func wikiEntry(t *testing.T, repo *models.Repository, wikiName string) *git.TreeEntry {
-       wikiRepo, err := git.OpenRepository(repo.WikiPath())
-       assert.NoError(t, err)
-       defer wikiRepo.Close()
-       commit, err := wikiRepo.GetBranchCommit("master")
-       assert.NoError(t, err)
-       entries, err := commit.ListEntries()
-       assert.NoError(t, err)
-       for _, entry := range entries {
-               if entry.Name() == wiki_service.NameToFilename(wikiName) {
-                       return entry
-               }
-       }
-       return nil
-}
-
-func wikiContent(t *testing.T, repo *models.Repository, wikiName string) string {
-       entry := wikiEntry(t, repo, wikiName)
-       if !assert.NotNil(t, entry) {
-               return ""
-       }
-       reader, err := entry.Blob().DataAsync()
-       assert.NoError(t, err)
-       defer reader.Close()
-       bytes, err := ioutil.ReadAll(reader)
-       assert.NoError(t, err)
-       return string(bytes)
-}
-
-func assertWikiExists(t *testing.T, repo *models.Repository, wikiName string) {
-       assert.NotNil(t, wikiEntry(t, repo, wikiName))
-}
-
-func assertWikiNotExists(t *testing.T, repo *models.Repository, wikiName string) {
-       assert.Nil(t, wikiEntry(t, repo, wikiName))
-}
-
-func assertPagesMetas(t *testing.T, expectedNames []string, metas interface{}) {
-       pageMetas, ok := metas.([]PageMeta)
-       if !assert.True(t, ok) {
-               return
-       }
-       if !assert.Len(t, pageMetas, len(expectedNames)) {
-               return
-       }
-       for i, pageMeta := range pageMetas {
-               assert.EqualValues(t, expectedNames[i], pageMeta.Name)
-       }
-}
-
-func TestWiki(t *testing.T) {
-       models.PrepareTestEnv(t)
-
-       ctx := test.MockContext(t, "user2/repo1/wiki/_pages")
-       ctx.SetParams(":page", "Home")
-       test.LoadRepo(t, ctx, 1)
-       Wiki(ctx)
-       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
-       assert.EqualValues(t, "Home", ctx.Data["Title"])
-       assertPagesMetas(t, []string{"Home", "Page With Image", "Page With Spaced Name"}, ctx.Data["Pages"])
-}
-
-func TestWikiPages(t *testing.T) {
-       models.PrepareTestEnv(t)
-
-       ctx := test.MockContext(t, "user2/repo1/wiki/_pages")
-       test.LoadRepo(t, ctx, 1)
-       WikiPages(ctx)
-       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
-       assertPagesMetas(t, []string{"Home", "Page With Image", "Page With Spaced Name"}, ctx.Data["Pages"])
-}
-
-func TestNewWiki(t *testing.T) {
-       models.PrepareTestEnv(t)
-
-       ctx := test.MockContext(t, "user2/repo1/wiki/_new")
-       test.LoadUser(t, ctx, 2)
-       test.LoadRepo(t, ctx, 1)
-       NewWiki(ctx)
-       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
-       assert.EqualValues(t, ctx.Tr("repo.wiki.new_page"), ctx.Data["Title"])
-}
-
-func TestNewWikiPost(t *testing.T) {
-       for _, title := range []string{
-               "New page",
-               "&&&&",
-       } {
-               models.PrepareTestEnv(t)
-
-               ctx := test.MockContext(t, "user2/repo1/wiki/_new")
-               test.LoadUser(t, ctx, 2)
-               test.LoadRepo(t, ctx, 1)
-               web.SetForm(ctx, &forms.NewWikiForm{
-                       Title:   title,
-                       Content: content,
-                       Message: message,
-               })
-               NewWikiPost(ctx)
-               assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
-               assertWikiExists(t, ctx.Repo.Repository, title)
-               assert.Equal(t, wikiContent(t, ctx.Repo.Repository, title), content)
-       }
-}
-
-func TestNewWikiPost_ReservedName(t *testing.T) {
-       models.PrepareTestEnv(t)
-
-       ctx := test.MockContext(t, "user2/repo1/wiki/_new")
-       test.LoadUser(t, ctx, 2)
-       test.LoadRepo(t, ctx, 1)
-       web.SetForm(ctx, &forms.NewWikiForm{
-               Title:   "_edit",
-               Content: content,
-               Message: message,
-       })
-       NewWikiPost(ctx)
-       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
-       assert.EqualValues(t, ctx.Tr("repo.wiki.reserved_page"), ctx.Flash.ErrorMsg)
-       assertWikiNotExists(t, ctx.Repo.Repository, "_edit")
-}
-
-func TestEditWiki(t *testing.T) {
-       models.PrepareTestEnv(t)
-
-       ctx := test.MockContext(t, "user2/repo1/wiki/_edit/Home")
-       ctx.SetParams(":page", "Home")
-       test.LoadUser(t, ctx, 2)
-       test.LoadRepo(t, ctx, 1)
-       EditWiki(ctx)
-       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
-       assert.EqualValues(t, "Home", ctx.Data["Title"])
-       assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["content"])
-}
-
-func TestEditWikiPost(t *testing.T) {
-       for _, title := range []string{
-               "Home",
-               "New/<page>",
-       } {
-               models.PrepareTestEnv(t)
-               ctx := test.MockContext(t, "user2/repo1/wiki/_new/Home")
-               ctx.SetParams(":page", "Home")
-               test.LoadUser(t, ctx, 2)
-               test.LoadRepo(t, ctx, 1)
-               web.SetForm(ctx, &forms.NewWikiForm{
-                       Title:   title,
-                       Content: content,
-                       Message: message,
-               })
-               EditWikiPost(ctx)
-               assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
-               assertWikiExists(t, ctx.Repo.Repository, title)
-               assert.Equal(t, wikiContent(t, ctx.Repo.Repository, title), content)
-               if title != "Home" {
-                       assertWikiNotExists(t, ctx.Repo.Repository, "Home")
-               }
-       }
-}
-
-func TestDeleteWikiPagePost(t *testing.T) {
-       models.PrepareTestEnv(t)
-
-       ctx := test.MockContext(t, "user2/repo1/wiki/Home/delete")
-       test.LoadUser(t, ctx, 2)
-       test.LoadRepo(t, ctx, 1)
-       DeleteWikiPagePost(ctx)
-       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
-       assertWikiNotExists(t, ctx.Repo.Repository, "Home")
-}
-
-func TestWikiRaw(t *testing.T) {
-       for filepath, filetype := range map[string]string{
-               "jpeg.jpg":                 "image/jpeg",
-               "images/jpeg.jpg":          "image/jpeg",
-               "Page With Spaced Name":    "text/plain; charset=utf-8",
-               "Page-With-Spaced-Name":    "text/plain; charset=utf-8",
-               "Page With Spaced Name.md": "text/plain; charset=utf-8",
-               "Page-With-Spaced-Name.md": "text/plain; charset=utf-8",
-       } {
-               models.PrepareTestEnv(t)
-
-               ctx := test.MockContext(t, "user2/repo1/wiki/raw/"+filepath)
-               ctx.SetParams("*", filepath)
-               test.LoadUser(t, ctx, 2)
-               test.LoadRepo(t, ctx, 1)
-               WikiRaw(ctx)
-               assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
-               assert.EqualValues(t, filetype, ctx.Resp.Header().Get("Content-Type"))
-       }
-}
diff --git a/routers/routes/base.go b/routers/routes/base.go
deleted file mode 100644 (file)
index 0b78450..0000000
+++ /dev/null
@@ -1,214 +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 routes
-
-import (
-       "errors"
-       "fmt"
-       "io"
-       "net/http"
-       "os"
-       "path"
-       "path/filepath"
-       "strings"
-       "time"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/auth/sso"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/httpcache"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/storage"
-       "code.gitea.io/gitea/modules/templates"
-       "code.gitea.io/gitea/modules/web/middleware"
-
-       "gitea.com/go-chi/session"
-)
-
-// LoggerHandler is a handler that will log the routing to the default gitea log
-func LoggerHandler(level log.Level) func(next http.Handler) http.Handler {
-       return func(next http.Handler) http.Handler {
-               return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
-                       start := time.Now()
-
-                       _ = log.GetLogger("router").Log(0, level, "Started %s %s for %s", log.ColoredMethod(req.Method), req.URL.RequestURI(), req.RemoteAddr)
-
-                       next.ServeHTTP(w, req)
-
-                       var status int
-                       if v, ok := w.(context.ResponseWriter); ok {
-                               status = v.Status()
-                       }
-
-                       _ = log.GetLogger("router").Log(0, level, "Completed %s %s %v %s in %v", log.ColoredMethod(req.Method), req.URL.RequestURI(), log.ColoredStatus(status), log.ColoredStatus(status, http.StatusText(status)), log.ColoredTime(time.Since(start)))
-               })
-       }
-}
-
-func storageHandler(storageSetting setting.Storage, prefix string, objStore storage.ObjectStorage) func(next http.Handler) http.Handler {
-       return func(next http.Handler) http.Handler {
-               if storageSetting.ServeDirect {
-                       return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
-                               if req.Method != "GET" && req.Method != "HEAD" {
-                                       next.ServeHTTP(w, req)
-                                       return
-                               }
-
-                               if !strings.HasPrefix(req.URL.RequestURI(), "/"+prefix) {
-                                       next.ServeHTTP(w, req)
-                                       return
-                               }
-
-                               rPath := strings.TrimPrefix(req.URL.RequestURI(), "/"+prefix)
-                               u, err := objStore.URL(rPath, path.Base(rPath))
-                               if err != nil {
-                                       if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
-                                               log.Warn("Unable to find %s %s", prefix, rPath)
-                                               http.Error(w, "file not found", 404)
-                                               return
-                                       }
-                                       log.Error("Error whilst getting URL for %s %s. Error: %v", prefix, rPath, err)
-                                       http.Error(w, fmt.Sprintf("Error whilst getting URL for %s %s", prefix, rPath), 500)
-                                       return
-                               }
-                               http.Redirect(
-                                       w,
-                                       req,
-                                       u.String(),
-                                       301,
-                               )
-                       })
-               }
-
-               return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
-                       if req.Method != "GET" && req.Method != "HEAD" {
-                               next.ServeHTTP(w, req)
-                               return
-                       }
-
-                       prefix := strings.Trim(prefix, "/")
-
-                       if !strings.HasPrefix(req.URL.EscapedPath(), "/"+prefix+"/") {
-                               next.ServeHTTP(w, req)
-                               return
-                       }
-
-                       rPath := strings.TrimPrefix(req.URL.EscapedPath(), "/"+prefix+"/")
-                       rPath = strings.TrimPrefix(rPath, "/")
-                       if rPath == "" {
-                               http.Error(w, "file not found", 404)
-                               return
-                       }
-                       rPath = path.Clean("/" + filepath.ToSlash(rPath))
-                       rPath = rPath[1:]
-
-                       fi, err := objStore.Stat(rPath)
-                       if err == nil && httpcache.HandleTimeCache(req, w, fi) {
-                               return
-                       }
-
-                       //If we have matched and access to release or issue
-                       fr, err := objStore.Open(rPath)
-                       if err != nil {
-                               if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
-                                       log.Warn("Unable to find %s %s", prefix, rPath)
-                                       http.Error(w, "file not found", 404)
-                                       return
-                               }
-                               log.Error("Error whilst opening %s %s. Error: %v", prefix, rPath, err)
-                               http.Error(w, fmt.Sprintf("Error whilst opening %s %s", prefix, rPath), 500)
-                               return
-                       }
-                       defer fr.Close()
-
-                       _, err = io.Copy(w, fr)
-                       if err != nil {
-                               log.Error("Error whilst rendering %s %s. Error: %v", prefix, rPath, err)
-                               http.Error(w, fmt.Sprintf("Error whilst rendering %s %s", prefix, rPath), 500)
-                               return
-                       }
-               })
-       }
-}
-
-type dataStore struct {
-       Data map[string]interface{}
-}
-
-func (d *dataStore) GetData() map[string]interface{} {
-       return d.Data
-}
-
-// Recovery returns a middleware that recovers from any panics and writes a 500 and a log if so.
-// This error will be created with the gitea 500 page.
-func Recovery() func(next http.Handler) http.Handler {
-       var rnd = templates.HTMLRenderer()
-       return func(next http.Handler) http.Handler {
-               return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
-                       defer func() {
-                               if err := recover(); err != nil {
-                                       combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, string(log.Stack(2)))
-                                       log.Error("%v", combinedErr)
-
-                                       sessionStore := session.GetSession(req)
-                                       if sessionStore == nil {
-                                               if setting.IsProd() {
-                                                       http.Error(w, http.StatusText(500), 500)
-                                               } else {
-                                                       http.Error(w, combinedErr, 500)
-                                               }
-                                               return
-                                       }
-
-                                       var lc = middleware.Locale(w, req)
-                                       var store = dataStore{
-                                               Data: templates.Vars{
-                                                       "Language":   lc.Language(),
-                                                       "CurrentURL": setting.AppSubURL + req.URL.RequestURI(),
-                                                       "i18n":       lc,
-                                               },
-                                       }
-
-                                       var user *models.User
-                                       if apiContext := context.GetAPIContext(req); apiContext != nil {
-                                               user = apiContext.User
-                                       }
-                                       if user == nil {
-                                               if ctx := context.GetContext(req); ctx != nil {
-                                                       user = ctx.User
-                                               }
-                                       }
-                                       if user == nil {
-                                               // Get user from session if logged in - do not attempt to sign-in
-                                               user = sso.SessionUser(sessionStore)
-                                       }
-                                       if user != nil {
-                                               store.Data["IsSigned"] = true
-                                               store.Data["SignedUser"] = user
-                                               store.Data["SignedUserID"] = user.ID
-                                               store.Data["SignedUserName"] = user.Name
-                                               store.Data["IsAdmin"] = user.IsAdmin
-                                       } else {
-                                               store.Data["SignedUserID"] = int64(0)
-                                               store.Data["SignedUserName"] = ""
-                                       }
-
-                                       w.Header().Set(`X-Frame-Options`, `SAMEORIGIN`)
-
-                                       if !setting.IsProd() {
-                                               store.Data["ErrorMsg"] = combinedErr
-                                       }
-                                       err = rnd.HTML(w, 500, "status/500", templates.BaseVars().Merge(store.Data))
-                                       if err != nil {
-                                               log.Error("%v", err)
-                                       }
-                               }
-                       }()
-
-                       next.ServeHTTP(w, req)
-               })
-       }
-}
diff --git a/routers/routes/goget.go b/routers/routes/goget.go
deleted file mode 100644 (file)
index 518f5e3..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright 2021 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 routes
-
-import (
-       "net/http"
-       "net/url"
-       "path"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/util"
-       "github.com/unknwon/com"
-)
-
-func goGet(ctx *context.Context) {
-       if ctx.Req.Method != "GET" || ctx.Query("go-get") != "1" || len(ctx.Req.URL.Query()) > 1 {
-               return
-       }
-
-       parts := strings.SplitN(ctx.Req.URL.EscapedPath(), "/", 4)
-
-       if len(parts) < 3 {
-               return
-       }
-
-       ownerName := parts[1]
-       repoName := parts[2]
-
-       // Quick responses appropriate go-get meta with status 200
-       // regardless of if user have access to the repository,
-       // or the repository does not exist at all.
-       // This is particular a workaround for "go get" command which does not respect
-       // .netrc file.
-
-       trimmedRepoName := strings.TrimSuffix(repoName, ".git")
-
-       if ownerName == "" || trimmedRepoName == "" {
-               _, _ = ctx.Write([]byte(`<!doctype html>
-<html>
-       <body>
-               invalid import path
-       </body>
-</html>
-`))
-               ctx.Status(400)
-               return
-       }
-       branchName := setting.Repository.DefaultBranch
-
-       repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName)
-       if err == nil && len(repo.DefaultBranch) > 0 {
-               branchName = repo.DefaultBranch
-       }
-       prefix := setting.AppURL + path.Join(url.PathEscape(ownerName), url.PathEscape(repoName), "src", "branch", util.PathEscapeSegments(branchName))
-
-       appURL, _ := url.Parse(setting.AppURL)
-
-       insecure := ""
-       if appURL.Scheme == string(setting.HTTP) {
-               insecure = "--insecure "
-       }
-       ctx.Header().Set("Content-Type", "text/html")
-       ctx.Status(http.StatusOK)
-       _, _ = ctx.Write([]byte(com.Expand(`<!doctype html>
-<html>
-       <head>
-               <meta name="go-import" content="{GoGetImport} git {CloneLink}">
-               <meta name="go-source" content="{GoGetImport} _ {GoDocDirectory} {GoDocFile}">
-       </head>
-       <body>
-               go get {Insecure}{GoGetImport}
-       </body>
-</html>
-`, map[string]string{
-               "GoGetImport":    context.ComposeGoGetImport(ownerName, trimmedRepoName),
-               "CloneLink":      models.ComposeHTTPSCloneURL(ownerName, repoName),
-               "GoDocDirectory": prefix + "{/dir}",
-               "GoDocFile":      prefix + "{/dir}/{file}#L{line}",
-               "Insecure":       insecure,
-       })))
-}
diff --git a/routers/routes/install.go b/routers/routes/install.go
deleted file mode 100644 (file)
index 0918da1..0000000
+++ /dev/null
@@ -1,109 +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 routes
-
-import (
-       "fmt"
-       "net/http"
-       "path"
-
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/public"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/templates"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/modules/web/middleware"
-       "code.gitea.io/gitea/routers"
-       "code.gitea.io/gitea/services/forms"
-
-       "gitea.com/go-chi/session"
-)
-
-func installRecovery() func(next http.Handler) http.Handler {
-       var rnd = templates.HTMLRenderer()
-       return func(next http.Handler) http.Handler {
-               return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
-                       defer func() {
-                               // Why we need this? The first recover will try to render a beautiful
-                               // error page for user, but the process can still panic again, then
-                               // we have to just recover twice and send a simple error page that
-                               // should not panic any more.
-                               defer func() {
-                                       if err := recover(); err != nil {
-                                               combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, string(log.Stack(2)))
-                                               log.Error(combinedErr)
-                                               if setting.IsProd() {
-                                                       http.Error(w, http.StatusText(500), 500)
-                                               } else {
-                                                       http.Error(w, combinedErr, 500)
-                                               }
-                                       }
-                               }()
-
-                               if err := recover(); err != nil {
-                                       combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, string(log.Stack(2)))
-                                       log.Error("%v", combinedErr)
-
-                                       lc := middleware.Locale(w, req)
-                                       var store = dataStore{
-                                               Data: templates.Vars{
-                                                       "Language":       lc.Language(),
-                                                       "CurrentURL":     setting.AppSubURL + req.URL.RequestURI(),
-                                                       "i18n":           lc,
-                                                       "SignedUserID":   int64(0),
-                                                       "SignedUserName": "",
-                                               },
-                                       }
-
-                                       w.Header().Set(`X-Frame-Options`, `SAMEORIGIN`)
-
-                                       if !setting.IsProd() {
-                                               store.Data["ErrorMsg"] = combinedErr
-                                       }
-                                       err = rnd.HTML(w, 500, "status/500", templates.BaseVars().Merge(store.Data))
-                                       if err != nil {
-                                               log.Error("%v", err)
-                                       }
-                               }
-                       }()
-
-                       next.ServeHTTP(w, req)
-               })
-       }
-}
-
-// InstallRoutes registers the install routes
-func InstallRoutes() *web.Route {
-       r := web.NewRoute()
-       for _, middle := range commonMiddlewares() {
-               r.Use(middle)
-       }
-
-       r.Use(public.AssetsHandler(&public.Options{
-               Directory: path.Join(setting.StaticRootPath, "public"),
-               Prefix:    "/assets",
-       }))
-
-       r.Use(session.Sessioner(session.Options{
-               Provider:       setting.SessionConfig.Provider,
-               ProviderConfig: setting.SessionConfig.ProviderConfig,
-               CookieName:     setting.SessionConfig.CookieName,
-               CookiePath:     setting.SessionConfig.CookiePath,
-               Gclifetime:     setting.SessionConfig.Gclifetime,
-               Maxlifetime:    setting.SessionConfig.Maxlifetime,
-               Secure:         setting.SessionConfig.Secure,
-               SameSite:       setting.SessionConfig.SameSite,
-               Domain:         setting.SessionConfig.Domain,
-       }))
-
-       r.Use(installRecovery())
-       r.Use(routers.InstallInit)
-       r.Get("/", routers.Install)
-       r.Post("/", web.Bind(forms.InstallForm{}), routers.InstallPost)
-       r.NotFound(func(w http.ResponseWriter, req *http.Request) {
-               http.Redirect(w, req, setting.AppURL, http.StatusFound)
-       })
-       return r
-}
diff --git a/routers/routes/web.go b/routers/routes/web.go
deleted file mode 100644 (file)
index fbc41d5..0000000
+++ /dev/null
@@ -1,1099 +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 routes
-
-import (
-       "encoding/gob"
-       "fmt"
-       "net/http"
-       "os"
-       "path"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/httpcache"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/metrics"
-       "code.gitea.io/gitea/modules/public"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/storage"
-       "code.gitea.io/gitea/modules/templates"
-       "code.gitea.io/gitea/modules/validation"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/routers"
-       "code.gitea.io/gitea/routers/admin"
-       apiv1 "code.gitea.io/gitea/routers/api/v1"
-       "code.gitea.io/gitea/routers/api/v1/misc"
-       "code.gitea.io/gitea/routers/dev"
-       "code.gitea.io/gitea/routers/events"
-       "code.gitea.io/gitea/routers/org"
-       "code.gitea.io/gitea/routers/private"
-       "code.gitea.io/gitea/routers/repo"
-       "code.gitea.io/gitea/routers/user"
-       userSetting "code.gitea.io/gitea/routers/user/setting"
-       "code.gitea.io/gitea/services/forms"
-       "code.gitea.io/gitea/services/lfs"
-       "code.gitea.io/gitea/services/mailer"
-
-       // to registers all internal adapters
-       _ "code.gitea.io/gitea/modules/session"
-
-       "gitea.com/go-chi/captcha"
-       "gitea.com/go-chi/session"
-       "github.com/NYTimes/gziphandler"
-       "github.com/chi-middleware/proxy"
-       "github.com/go-chi/chi/middleware"
-       "github.com/go-chi/cors"
-       "github.com/prometheus/client_golang/prometheus"
-       "github.com/tstranex/u2f"
-)
-
-const (
-       // GzipMinSize represents min size to compress for the body size of response
-       GzipMinSize = 1400
-)
-
-func commonMiddlewares() []func(http.Handler) http.Handler {
-       var handlers = []func(http.Handler) http.Handler{
-               func(next http.Handler) http.Handler {
-                       return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
-                               next.ServeHTTP(context.NewResponse(resp), req)
-                       })
-               },
-       }
-
-       if setting.ReverseProxyLimit > 0 {
-               opt := proxy.NewForwardedHeadersOptions().
-                       WithForwardLimit(setting.ReverseProxyLimit).
-                       ClearTrustedProxies()
-               for _, n := range setting.ReverseProxyTrustedProxies {
-                       if !strings.Contains(n, "/") {
-                               opt.AddTrustedProxy(n)
-                       } else {
-                               opt.AddTrustedNetwork(n)
-                       }
-               }
-               handlers = append(handlers, proxy.ForwardedHeaders(opt))
-       }
-
-       handlers = append(handlers, middleware.StripSlashes)
-
-       if !setting.DisableRouterLog && setting.RouterLogLevel != log.NONE {
-               if log.GetLogger("router").GetLevel() <= setting.RouterLogLevel {
-                       handlers = append(handlers, LoggerHandler(setting.RouterLogLevel))
-               }
-       }
-       if setting.EnableAccessLog {
-               handlers = append(handlers, context.AccessLogger())
-       }
-
-       handlers = append(handlers, func(next http.Handler) http.Handler {
-               return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
-                       // Why we need this? The Recovery() will try to render a beautiful
-                       // error page for user, but the process can still panic again, and other
-                       // middleware like session also may panic then we have to recover twice
-                       // and send a simple error page that should not panic any more.
-                       defer func() {
-                               if err := recover(); err != nil {
-                                       combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, string(log.Stack(2)))
-                                       log.Error("%v", combinedErr)
-                                       if setting.IsProd() {
-                                               http.Error(resp, http.StatusText(500), 500)
-                                       } else {
-                                               http.Error(resp, combinedErr, 500)
-                                       }
-                               }
-                       }()
-                       next.ServeHTTP(resp, req)
-               })
-       })
-       return handlers
-}
-
-var corsHandler func(http.Handler) http.Handler
-
-// NormalRoutes represents non install routes
-func NormalRoutes() *web.Route {
-       r := web.NewRoute()
-       for _, middle := range commonMiddlewares() {
-               r.Use(middle)
-       }
-
-       if setting.CORSConfig.Enabled {
-               corsHandler = cors.Handler(cors.Options{
-                       //Scheme:           setting.CORSConfig.Scheme, // FIXME: the cors middleware needs scheme option
-                       AllowedOrigins: setting.CORSConfig.AllowDomain,
-                       //setting.CORSConfig.AllowSubdomain // FIXME: the cors middleware needs allowSubdomain option
-                       AllowedMethods:   setting.CORSConfig.Methods,
-                       AllowCredentials: setting.CORSConfig.AllowCredentials,
-                       MaxAge:           int(setting.CORSConfig.MaxAge.Seconds()),
-               })
-       } else {
-               corsHandler = func(next http.Handler) http.Handler {
-                       return next
-               }
-       }
-
-       r.Mount("/", WebRoutes())
-       r.Mount("/api/v1", apiv1.Routes())
-       r.Mount("/api/internal", private.Routes())
-       return r
-}
-
-// WebRoutes returns all web routes
-func WebRoutes() *web.Route {
-       routes := web.NewRoute()
-
-       routes.Use(public.AssetsHandler(&public.Options{
-               Directory:   path.Join(setting.StaticRootPath, "public"),
-               Prefix:      "/assets",
-               CorsHandler: corsHandler,
-       }))
-
-       routes.Use(session.Sessioner(session.Options{
-               Provider:       setting.SessionConfig.Provider,
-               ProviderConfig: setting.SessionConfig.ProviderConfig,
-               CookieName:     setting.SessionConfig.CookieName,
-               CookiePath:     setting.SessionConfig.CookiePath,
-               Gclifetime:     setting.SessionConfig.Gclifetime,
-               Maxlifetime:    setting.SessionConfig.Maxlifetime,
-               Secure:         setting.SessionConfig.Secure,
-               SameSite:       setting.SessionConfig.SameSite,
-               Domain:         setting.SessionConfig.Domain,
-       }))
-
-       routes.Use(Recovery())
-
-       // We use r.Route here over r.Use because this prevents requests that are not for avatars having to go through this additional handler
-       routes.Route("/avatars/*", "GET, HEAD", storageHandler(setting.Avatar.Storage, "avatars", storage.Avatars))
-       routes.Route("/repo-avatars/*", "GET, HEAD", storageHandler(setting.RepoAvatar.Storage, "repo-avatars", storage.RepoAvatars))
-
-       // for health check - doeesn't need to be passed through gzip handler
-       routes.Head("/", func(w http.ResponseWriter, req *http.Request) {
-               w.WriteHeader(http.StatusOK)
-       })
-
-       // this png is very likely to always be below the limit for gzip so it doesn't need to pass through gzip
-       routes.Get("/apple-touch-icon.png", func(w http.ResponseWriter, req *http.Request) {
-               http.Redirect(w, req, path.Join(setting.StaticURLPrefix, "/assets/img/apple-touch-icon.png"), 301)
-       })
-
-       gob.Register(&u2f.Challenge{})
-
-       common := []interface{}{}
-
-       if setting.EnableGzip {
-               h, err := gziphandler.GzipHandlerWithOpts(gziphandler.MinSize(GzipMinSize))
-               if err != nil {
-                       log.Fatal("GzipHandlerWithOpts failed: %v", err)
-               }
-               common = append(common, h)
-       }
-
-       mailer.InitMailRender(templates.Mailer())
-
-       if setting.Service.EnableCaptcha {
-               // The captcha http.Handler should only fire on /captcha/* so we can just mount this on that url
-               routes.Route("/captcha/*", "GET,HEAD", append(common, captcha.Captchaer(context.GetImageCaptcha()))...)
-       }
-
-       if setting.HasRobotsTxt {
-               routes.Get("/robots.txt", append(common, func(w http.ResponseWriter, req *http.Request) {
-                       filePath := path.Join(setting.CustomPath, "robots.txt")
-                       fi, err := os.Stat(filePath)
-                       if err == nil && httpcache.HandleTimeCache(req, w, fi) {
-                               return
-                       }
-                       http.ServeFile(w, req, filePath)
-               })...)
-       }
-
-       // prometheus metrics endpoint - do not need to go through contexter
-       if setting.Metrics.Enabled {
-               c := metrics.NewCollector()
-               prometheus.MustRegister(c)
-
-               routes.Get("/metrics", append(common, routers.Metrics)...)
-       }
-
-       // Removed: toolbox.Toolboxer middleware will provide debug informations which seems unnecessary
-       common = append(common, context.Contexter())
-
-       // GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route
-       common = append(common, middleware.GetHead)
-
-       if setting.API.EnableSwagger {
-               // Note: The route moved from apiroutes because it's in fact want to render a web page
-               routes.Get("/api/swagger", append(common, misc.Swagger)...) // Render V1 by default
-       }
-
-       // TODO: These really seem like things that could be folded into Contexter or as helper functions
-       common = append(common, user.GetNotificationCount)
-       common = append(common, repo.GetActiveStopwatch)
-       common = append(common, goGet)
-
-       others := web.NewRoute()
-       for _, middle := range common {
-               others.Use(middle)
-       }
-
-       RegisterRoutes(others)
-       routes.Mount("", others)
-       return routes
-}
-
-// RegisterRoutes register routes
-func RegisterRoutes(m *web.Route) {
-       reqSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: true})
-       ignSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: setting.Service.RequireSignInView})
-       ignExploreSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView})
-       ignSignInAndCsrf := context.Toggle(&context.ToggleOptions{DisableCSRF: true})
-       reqSignOut := context.Toggle(&context.ToggleOptions{SignOutRequired: true})
-
-       //bindIgnErr := binding.BindIgnErr
-       bindIgnErr := web.Bind
-       validation.AddBindingRules()
-
-       openIDSignInEnabled := func(ctx *context.Context) {
-               if !setting.Service.EnableOpenIDSignIn {
-                       ctx.Error(http.StatusForbidden)
-                       return
-               }
-       }
-
-       openIDSignUpEnabled := func(ctx *context.Context) {
-               if !setting.Service.EnableOpenIDSignUp {
-                       ctx.Error(http.StatusForbidden)
-                       return
-               }
-       }
-
-       reqMilestonesDashboardPageEnabled := func(ctx *context.Context) {
-               if !setting.Service.ShowMilestonesDashboardPage {
-                       ctx.Error(http.StatusForbidden)
-                       return
-               }
-       }
-
-       // webhooksEnabled requires webhooks to be enabled by admin.
-       webhooksEnabled := func(ctx *context.Context) {
-               if setting.DisableWebhooks {
-                       ctx.Error(http.StatusForbidden)
-                       return
-               }
-       }
-
-       lfsServerEnabled := func(ctx *context.Context) {
-               if !setting.LFS.StartServer {
-                       ctx.Error(http.StatusNotFound)
-                       return
-               }
-       }
-
-       // FIXME: not all routes need go through same middleware.
-       // Especially some AJAX requests, we can reduce middleware number to improve performance.
-       // Routers.
-       // for health check
-       m.Get("/", routers.Home)
-       m.Get("/.well-known/openid-configuration", user.OIDCWellKnown)
-       m.Group("/explore", func() {
-               m.Get("", func(ctx *context.Context) {
-                       ctx.Redirect(setting.AppSubURL + "/explore/repos")
-               })
-               m.Get("/repos", routers.ExploreRepos)
-               m.Get("/users", routers.ExploreUsers)
-               m.Get("/organizations", routers.ExploreOrganizations)
-               m.Get("/code", routers.ExploreCode)
-       }, ignExploreSignIn)
-       m.Get("/issues", reqSignIn, user.Issues)
-       m.Get("/pulls", reqSignIn, user.Pulls)
-       m.Get("/milestones", reqSignIn, reqMilestonesDashboardPageEnabled, user.Milestones)
-
-       // ***** START: User *****
-       m.Group("/user", func() {
-               m.Get("/login", user.SignIn)
-               m.Post("/login", bindIgnErr(forms.SignInForm{}), user.SignInPost)
-               m.Group("", func() {
-                       m.Combo("/login/openid").
-                               Get(user.SignInOpenID).
-                               Post(bindIgnErr(forms.SignInOpenIDForm{}), user.SignInOpenIDPost)
-               }, openIDSignInEnabled)
-               m.Group("/openid", func() {
-                       m.Combo("/connect").
-                               Get(user.ConnectOpenID).
-                               Post(bindIgnErr(forms.ConnectOpenIDForm{}), user.ConnectOpenIDPost)
-                       m.Group("/register", func() {
-                               m.Combo("").
-                                       Get(user.RegisterOpenID, openIDSignUpEnabled).
-                                       Post(bindIgnErr(forms.SignUpOpenIDForm{}), user.RegisterOpenIDPost)
-                       }, openIDSignUpEnabled)
-               }, openIDSignInEnabled)
-               m.Get("/sign_up", user.SignUp)
-               m.Post("/sign_up", bindIgnErr(forms.RegisterForm{}), user.SignUpPost)
-               m.Group("/oauth2", func() {
-                       m.Get("/{provider}", user.SignInOAuth)
-                       m.Get("/{provider}/callback", user.SignInOAuthCallback)
-               })
-               m.Get("/link_account", user.LinkAccount)
-               m.Post("/link_account_signin", bindIgnErr(forms.SignInForm{}), user.LinkAccountPostSignIn)
-               m.Post("/link_account_signup", bindIgnErr(forms.RegisterForm{}), user.LinkAccountPostRegister)
-               m.Group("/two_factor", func() {
-                       m.Get("", user.TwoFactor)
-                       m.Post("", bindIgnErr(forms.TwoFactorAuthForm{}), user.TwoFactorPost)
-                       m.Get("/scratch", user.TwoFactorScratch)
-                       m.Post("/scratch", bindIgnErr(forms.TwoFactorScratchAuthForm{}), user.TwoFactorScratchPost)
-               })
-               m.Group("/u2f", func() {
-                       m.Get("", user.U2F)
-                       m.Get("/challenge", user.U2FChallenge)
-                       m.Post("/sign", bindIgnErr(u2f.SignResponse{}), user.U2FSign)
-
-               })
-       }, reqSignOut)
-
-       m.Any("/user/events", events.Events)
-
-       m.Group("/login/oauth", func() {
-               m.Get("/authorize", bindIgnErr(forms.AuthorizationForm{}), user.AuthorizeOAuth)
-               m.Post("/grant", bindIgnErr(forms.GrantApplicationForm{}), user.GrantApplicationOAuth)
-               // TODO manage redirection
-               m.Post("/authorize", bindIgnErr(forms.AuthorizationForm{}), user.AuthorizeOAuth)
-       }, ignSignInAndCsrf, reqSignIn)
-       m.Get("/login/oauth/userinfo", ignSignInAndCsrf, user.InfoOAuth)
-       m.Post("/login/oauth/access_token", corsHandler, bindIgnErr(forms.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth)
-
-       m.Group("/user/settings", func() {
-               m.Get("", userSetting.Profile)
-               m.Post("", bindIgnErr(forms.UpdateProfileForm{}), userSetting.ProfilePost)
-               m.Get("/change_password", user.MustChangePassword)
-               m.Post("/change_password", bindIgnErr(forms.MustChangePasswordForm{}), user.MustChangePasswordPost)
-               m.Post("/avatar", bindIgnErr(forms.AvatarForm{}), userSetting.AvatarPost)
-               m.Post("/avatar/delete", userSetting.DeleteAvatar)
-               m.Group("/account", func() {
-                       m.Combo("").Get(userSetting.Account).Post(bindIgnErr(forms.ChangePasswordForm{}), userSetting.AccountPost)
-                       m.Post("/email", bindIgnErr(forms.AddEmailForm{}), userSetting.EmailPost)
-                       m.Post("/email/delete", userSetting.DeleteEmail)
-                       m.Post("/delete", userSetting.DeleteAccount)
-                       m.Post("/theme", bindIgnErr(forms.UpdateThemeForm{}), userSetting.UpdateUIThemePost)
-               })
-               m.Group("/security", func() {
-                       m.Get("", userSetting.Security)
-                       m.Group("/two_factor", func() {
-                               m.Post("/regenerate_scratch", userSetting.RegenerateScratchTwoFactor)
-                               m.Post("/disable", userSetting.DisableTwoFactor)
-                               m.Get("/enroll", userSetting.EnrollTwoFactor)
-                               m.Post("/enroll", bindIgnErr(forms.TwoFactorAuthForm{}), userSetting.EnrollTwoFactorPost)
-                       })
-                       m.Group("/u2f", func() {
-                               m.Post("/request_register", bindIgnErr(forms.U2FRegistrationForm{}), userSetting.U2FRegister)
-                               m.Post("/register", bindIgnErr(u2f.RegisterResponse{}), userSetting.U2FRegisterPost)
-                               m.Post("/delete", bindIgnErr(forms.U2FDeleteForm{}), userSetting.U2FDelete)
-                       })
-                       m.Group("/openid", func() {
-                               m.Post("", bindIgnErr(forms.AddOpenIDForm{}), userSetting.OpenIDPost)
-                               m.Post("/delete", userSetting.DeleteOpenID)
-                               m.Post("/toggle_visibility", userSetting.ToggleOpenIDVisibility)
-                       }, openIDSignInEnabled)
-                       m.Post("/account_link", userSetting.DeleteAccountLink)
-               })
-               m.Group("/applications/oauth2", func() {
-                       m.Get("/{id}", userSetting.OAuth2ApplicationShow)
-                       m.Post("/{id}", bindIgnErr(forms.EditOAuth2ApplicationForm{}), userSetting.OAuthApplicationsEdit)
-                       m.Post("/{id}/regenerate_secret", userSetting.OAuthApplicationsRegenerateSecret)
-                       m.Post("", bindIgnErr(forms.EditOAuth2ApplicationForm{}), userSetting.OAuthApplicationsPost)
-                       m.Post("/delete", userSetting.DeleteOAuth2Application)
-                       m.Post("/revoke", userSetting.RevokeOAuth2Grant)
-               })
-               m.Combo("/applications").Get(userSetting.Applications).
-                       Post(bindIgnErr(forms.NewAccessTokenForm{}), userSetting.ApplicationsPost)
-               m.Post("/applications/delete", userSetting.DeleteApplication)
-               m.Combo("/keys").Get(userSetting.Keys).
-                       Post(bindIgnErr(forms.AddKeyForm{}), userSetting.KeysPost)
-               m.Post("/keys/delete", userSetting.DeleteKey)
-               m.Get("/organization", userSetting.Organization)
-               m.Get("/repos", userSetting.Repos)
-               m.Post("/repos/unadopted", userSetting.AdoptOrDeleteRepository)
-       }, reqSignIn, func(ctx *context.Context) {
-               ctx.Data["PageIsUserSettings"] = true
-               ctx.Data["AllThemes"] = setting.UI.Themes
-       })
-
-       m.Group("/user", func() {
-               // r.Get("/feeds", binding.Bind(auth.FeedsForm{}), user.Feeds)
-               m.Get("/activate", user.Activate, reqSignIn)
-               m.Post("/activate", user.ActivatePost, reqSignIn)
-               m.Any("/activate_email", user.ActivateEmail)
-               m.Get("/avatar/{username}/{size}", user.Avatar)
-               m.Get("/email2user", user.Email2User)
-               m.Get("/recover_account", user.ResetPasswd)
-               m.Post("/recover_account", user.ResetPasswdPost)
-               m.Get("/forgot_password", user.ForgotPasswd)
-               m.Post("/forgot_password", user.ForgotPasswdPost)
-               m.Post("/logout", user.SignOut)
-               m.Get("/task/{task}", user.TaskStatus)
-       })
-       // ***** END: User *****
-
-       m.Get("/avatar/{hash}", user.AvatarByEmailHash)
-
-       adminReq := context.Toggle(&context.ToggleOptions{SignInRequired: true, AdminRequired: true})
-
-       // ***** START: Admin *****
-       m.Group("/admin", func() {
-               m.Get("", adminReq, admin.Dashboard)
-               m.Post("", adminReq, bindIgnErr(forms.AdminDashboardForm{}), admin.DashboardPost)
-               m.Get("/config", admin.Config)
-               m.Post("/config/test_mail", admin.SendTestMail)
-               m.Group("/monitor", func() {
-                       m.Get("", admin.Monitor)
-                       m.Post("/cancel/{pid}", admin.MonitorCancel)
-                       m.Group("/queue/{qid}", func() {
-                               m.Get("", admin.Queue)
-                               m.Post("/set", admin.SetQueueSettings)
-                               m.Post("/add", admin.AddWorkers)
-                               m.Post("/cancel/{pid}", admin.WorkerCancel)
-                               m.Post("/flush", admin.Flush)
-                       })
-               })
-
-               m.Group("/users", func() {
-                       m.Get("", admin.Users)
-                       m.Combo("/new").Get(admin.NewUser).Post(bindIgnErr(forms.AdminCreateUserForm{}), admin.NewUserPost)
-                       m.Combo("/{userid}").Get(admin.EditUser).Post(bindIgnErr(forms.AdminEditUserForm{}), admin.EditUserPost)
-                       m.Post("/{userid}/delete", admin.DeleteUser)
-               })
-
-               m.Group("/emails", func() {
-                       m.Get("", admin.Emails)
-                       m.Post("/activate", admin.ActivateEmail)
-               })
-
-               m.Group("/orgs", func() {
-                       m.Get("", admin.Organizations)
-               })
-
-               m.Group("/repos", func() {
-                       m.Get("", admin.Repos)
-                       m.Combo("/unadopted").Get(admin.UnadoptedRepos).Post(admin.AdoptOrDeleteRepository)
-                       m.Post("/delete", admin.DeleteRepo)
-               })
-
-               m.Group("/hooks", func() {
-                       m.Get("", admin.DefaultOrSystemWebhooks)
-                       m.Post("/delete", admin.DeleteDefaultOrSystemWebhook)
-                       m.Get("/{id}", repo.WebHooksEdit)
-                       m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.WebHooksEditPost)
-                       m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost)
-                       m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost)
-                       m.Post("/discord/{id}", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
-                       m.Post("/dingtalk/{id}", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost)
-                       m.Post("/telegram/{id}", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost)
-                       m.Post("/matrix/{id}", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost)
-                       m.Post("/msteams/{id}", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost)
-                       m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost)
-               }, webhooksEnabled)
-
-               m.Group("/{configType:default-hooks|system-hooks}", func() {
-                       m.Get("/{type}/new", repo.WebhooksNew)
-                       m.Post("/gitea/new", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksNewPost)
-                       m.Post("/gogs/new", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksNewPost)
-                       m.Post("/slack/new", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksNewPost)
-                       m.Post("/discord/new", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
-                       m.Post("/dingtalk/new", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost)
-                       m.Post("/telegram/new", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost)
-                       m.Post("/matrix/new", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost)
-                       m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost)
-                       m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost)
-               })
-
-               m.Group("/auths", func() {
-                       m.Get("", admin.Authentications)
-                       m.Combo("/new").Get(admin.NewAuthSource).Post(bindIgnErr(forms.AuthenticationForm{}), admin.NewAuthSourcePost)
-                       m.Combo("/{authid}").Get(admin.EditAuthSource).
-                               Post(bindIgnErr(forms.AuthenticationForm{}), admin.EditAuthSourcePost)
-                       m.Post("/{authid}/delete", admin.DeleteAuthSource)
-               })
-
-               m.Group("/notices", func() {
-                       m.Get("", admin.Notices)
-                       m.Post("/delete", admin.DeleteNotices)
-                       m.Post("/empty", admin.EmptyNotices)
-               })
-       }, adminReq)
-       // ***** END: Admin *****
-
-       m.Group("", func() {
-               m.Get("/{username}", user.Profile)
-               m.Get("/attachments/{uuid}", repo.GetAttachment)
-       }, ignSignIn)
-
-       m.Group("/{username}", func() {
-               m.Post("/action/{action}", user.Action)
-       }, reqSignIn)
-
-       if !setting.IsProd() {
-               m.Get("/template/*", dev.TemplatePreview)
-       }
-
-       reqRepoAdmin := context.RequireRepoAdmin()
-       reqRepoCodeWriter := context.RequireRepoWriter(models.UnitTypeCode)
-       reqRepoCodeReader := context.RequireRepoReader(models.UnitTypeCode)
-       reqRepoReleaseWriter := context.RequireRepoWriter(models.UnitTypeReleases)
-       reqRepoReleaseReader := context.RequireRepoReader(models.UnitTypeReleases)
-       reqRepoWikiWriter := context.RequireRepoWriter(models.UnitTypeWiki)
-       reqRepoIssueWriter := context.RequireRepoWriter(models.UnitTypeIssues)
-       reqRepoIssueReader := context.RequireRepoReader(models.UnitTypeIssues)
-       reqRepoPullsReader := context.RequireRepoReader(models.UnitTypePullRequests)
-       reqRepoIssuesOrPullsWriter := context.RequireRepoWriterOr(models.UnitTypeIssues, models.UnitTypePullRequests)
-       reqRepoIssuesOrPullsReader := context.RequireRepoReaderOr(models.UnitTypeIssues, models.UnitTypePullRequests)
-       reqRepoProjectsReader := context.RequireRepoReader(models.UnitTypeProjects)
-       reqRepoProjectsWriter := context.RequireRepoWriter(models.UnitTypeProjects)
-
-       // ***** START: Organization *****
-       m.Group("/org", func() {
-               m.Group("", func() {
-                       m.Get("/create", org.Create)
-                       m.Post("/create", bindIgnErr(forms.CreateOrgForm{}), org.CreatePost)
-               })
-
-               m.Group("/{org}", func() {
-                       m.Get("/dashboard", user.Dashboard)
-                       m.Get("/dashboard/{team}", user.Dashboard)
-                       m.Get("/issues", user.Issues)
-                       m.Get("/issues/{team}", user.Issues)
-                       m.Get("/pulls", user.Pulls)
-                       m.Get("/pulls/{team}", user.Pulls)
-                       m.Get("/milestones", reqMilestonesDashboardPageEnabled, user.Milestones)
-                       m.Get("/milestones/{team}", reqMilestonesDashboardPageEnabled, user.Milestones)
-                       m.Get("/members", org.Members)
-                       m.Post("/members/action/{action}", org.MembersAction)
-                       m.Get("/teams", org.Teams)
-               }, context.OrgAssignment(true, false, true))
-
-               m.Group("/{org}", func() {
-                       m.Get("/teams/{team}", org.TeamMembers)
-                       m.Get("/teams/{team}/repositories", org.TeamRepositories)
-                       m.Post("/teams/{team}/action/{action}", org.TeamsAction)
-                       m.Post("/teams/{team}/action/repo/{action}", org.TeamsRepoAction)
-               }, context.OrgAssignment(true, false, true))
-
-               m.Group("/{org}", func() {
-                       m.Get("/teams/new", org.NewTeam)
-                       m.Post("/teams/new", bindIgnErr(forms.CreateTeamForm{}), org.NewTeamPost)
-                       m.Get("/teams/{team}/edit", org.EditTeam)
-                       m.Post("/teams/{team}/edit", bindIgnErr(forms.CreateTeamForm{}), org.EditTeamPost)
-                       m.Post("/teams/{team}/delete", org.DeleteTeam)
-
-                       m.Group("/settings", func() {
-                               m.Combo("").Get(org.Settings).
-                                       Post(bindIgnErr(forms.UpdateOrgSettingForm{}), org.SettingsPost)
-                               m.Post("/avatar", bindIgnErr(forms.AvatarForm{}), org.SettingsAvatar)
-                               m.Post("/avatar/delete", org.SettingsDeleteAvatar)
-
-                               m.Group("/hooks", func() {
-                                       m.Get("", org.Webhooks)
-                                       m.Post("/delete", org.DeleteWebhook)
-                                       m.Get("/{type}/new", repo.WebhooksNew)
-                                       m.Post("/gitea/new", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksNewPost)
-                                       m.Post("/gogs/new", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksNewPost)
-                                       m.Post("/slack/new", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksNewPost)
-                                       m.Post("/discord/new", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
-                                       m.Post("/dingtalk/new", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost)
-                                       m.Post("/telegram/new", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost)
-                                       m.Post("/matrix/new", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost)
-                                       m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost)
-                                       m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost)
-                                       m.Get("/{id}", repo.WebHooksEdit)
-                                       m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.WebHooksEditPost)
-                                       m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost)
-                                       m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost)
-                                       m.Post("/discord/{id}", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
-                                       m.Post("/dingtalk/{id}", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost)
-                                       m.Post("/telegram/{id}", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost)
-                                       m.Post("/matrix/{id}", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost)
-                                       m.Post("/msteams/{id}", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost)
-                                       m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost)
-                               }, webhooksEnabled)
-
-                               m.Group("/labels", func() {
-                                       m.Get("", org.RetrieveLabels, org.Labels)
-                                       m.Post("/new", bindIgnErr(forms.CreateLabelForm{}), org.NewLabel)
-                                       m.Post("/edit", bindIgnErr(forms.CreateLabelForm{}), org.UpdateLabel)
-                                       m.Post("/delete", org.DeleteLabel)
-                                       m.Post("/initialize", bindIgnErr(forms.InitializeLabelsForm{}), org.InitializeLabels)
-                               })
-
-                               m.Route("/delete", "GET,POST", org.SettingsDelete)
-                       })
-               }, context.OrgAssignment(true, true))
-       }, reqSignIn)
-       // ***** END: Organization *****
-
-       // ***** START: Repository *****
-       m.Group("/repo", func() {
-               m.Get("/create", repo.Create)
-               m.Post("/create", bindIgnErr(forms.CreateRepoForm{}), repo.CreatePost)
-               m.Get("/migrate", repo.Migrate)
-               m.Post("/migrate", bindIgnErr(forms.MigrateRepoForm{}), repo.MigratePost)
-               m.Group("/fork", func() {
-                       m.Combo("/{repoid}").Get(repo.Fork).
-                               Post(bindIgnErr(forms.CreateRepoForm{}), repo.ForkPost)
-               }, context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader)
-       }, reqSignIn)
-
-       // ***** Release Attachment Download without Signin
-       m.Get("/{username}/{reponame}/releases/download/{vTag}/{fileName}", ignSignIn, context.RepoAssignment, repo.MustBeNotEmpty, repo.RedirectDownload)
-
-       m.Group("/{username}/{reponame}", func() {
-               m.Group("/settings", func() {
-                       m.Combo("").Get(repo.Settings).
-                               Post(bindIgnErr(forms.RepoSettingForm{}), repo.SettingsPost)
-                       m.Post("/avatar", bindIgnErr(forms.AvatarForm{}), repo.SettingsAvatar)
-                       m.Post("/avatar/delete", repo.SettingsDeleteAvatar)
-
-                       m.Group("/collaboration", func() {
-                               m.Combo("").Get(repo.Collaboration).Post(repo.CollaborationPost)
-                               m.Post("/access_mode", repo.ChangeCollaborationAccessMode)
-                               m.Post("/delete", repo.DeleteCollaboration)
-                               m.Group("/team", func() {
-                                       m.Post("", repo.AddTeamPost)
-                                       m.Post("/delete", repo.DeleteTeam)
-                               })
-                       })
-                       m.Group("/branches", func() {
-                               m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost)
-                               m.Combo("/*").Get(repo.SettingsProtectedBranch).
-                                       Post(bindIgnErr(forms.ProtectBranchForm{}), context.RepoMustNotBeArchived(), repo.SettingsProtectedBranchPost)
-                       }, repo.MustBeNotEmpty)
-
-                       m.Group("/hooks/git", func() {
-                               m.Get("", repo.GitHooks)
-                               m.Combo("/{name}").Get(repo.GitHooksEdit).
-                                       Post(repo.GitHooksEditPost)
-                       }, context.GitHookService())
-
-                       m.Group("/hooks", func() {
-                               m.Get("", repo.Webhooks)
-                               m.Post("/delete", repo.DeleteWebhook)
-                               m.Get("/{type}/new", repo.WebhooksNew)
-                               m.Post("/gitea/new", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksNewPost)
-                               m.Post("/gogs/new", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksNewPost)
-                               m.Post("/slack/new", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksNewPost)
-                               m.Post("/discord/new", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
-                               m.Post("/dingtalk/new", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost)
-                               m.Post("/telegram/new", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost)
-                               m.Post("/matrix/new", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost)
-                               m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost)
-                               m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost)
-                               m.Get("/{id}", repo.WebHooksEdit)
-                               m.Post("/{id}/test", repo.TestWebhook)
-                               m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.WebHooksEditPost)
-                               m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost)
-                               m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost)
-                               m.Post("/discord/{id}", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
-                               m.Post("/dingtalk/{id}", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost)
-                               m.Post("/telegram/{id}", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost)
-                               m.Post("/matrix/{id}", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost)
-                               m.Post("/msteams/{id}", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost)
-                               m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost)
-                       }, webhooksEnabled)
-
-                       m.Group("/keys", func() {
-                               m.Combo("").Get(repo.DeployKeys).
-                                       Post(bindIgnErr(forms.AddKeyForm{}), repo.DeployKeysPost)
-                               m.Post("/delete", repo.DeleteDeployKey)
-                       })
-
-                       m.Group("/lfs", func() {
-                               m.Get("/", repo.LFSFiles)
-                               m.Get("/show/{oid}", repo.LFSFileGet)
-                               m.Post("/delete/{oid}", repo.LFSDelete)
-                               m.Get("/pointers", repo.LFSPointerFiles)
-                               m.Post("/pointers/associate", repo.LFSAutoAssociate)
-                               m.Get("/find", repo.LFSFileFind)
-                               m.Group("/locks", func() {
-                                       m.Get("/", repo.LFSLocks)
-                                       m.Post("/", repo.LFSLockFile)
-                                       m.Post("/{lid}/unlock", repo.LFSUnlock)
-                               })
-                       })
-
-               }, func(ctx *context.Context) {
-                       ctx.Data["PageIsSettings"] = true
-                       ctx.Data["LFSStartServer"] = setting.LFS.StartServer
-               })
-       }, reqSignIn, context.RepoAssignment, context.UnitTypes(), reqRepoAdmin, context.RepoRef())
-
-       m.Post("/{username}/{reponame}/action/{action}", reqSignIn, context.RepoAssignment, context.UnitTypes(), repo.Action)
-
-       // Grouping for those endpoints not requiring authentication
-       m.Group("/{username}/{reponame}", func() {
-               m.Group("/milestone", func() {
-                       m.Get("/{id}", repo.MilestoneIssuesAndPulls)
-               }, reqRepoIssuesOrPullsReader, context.RepoRef())
-               m.Combo("/compare/*", repo.MustBeNotEmpty, reqRepoCodeReader, repo.SetEditorconfigIfExists).
-                       Get(ignSignIn, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff).
-                       Post(reqSignIn, context.RepoMustNotBeArchived(), reqRepoPullsReader, repo.MustAllowPulls, bindIgnErr(forms.CreateIssueForm{}), repo.SetWhitespaceBehavior, repo.CompareAndPullRequestPost)
-       }, context.RepoAssignment, context.UnitTypes())
-
-       // Grouping for those endpoints that do require authentication
-       m.Group("/{username}/{reponame}", func() {
-               m.Group("/issues", func() {
-                       m.Group("/new", func() {
-                               m.Combo("").Get(context.RepoRef(), repo.NewIssue).
-                                       Post(bindIgnErr(forms.CreateIssueForm{}), repo.NewIssuePost)
-                               m.Get("/choose", context.RepoRef(), repo.NewIssueChooseTemplate)
-                       })
-               }, context.RepoMustNotBeArchived(), reqRepoIssueReader)
-               // FIXME: should use different URLs but mostly same logic for comments of issue and pull request.
-               // So they can apply their own enable/disable logic on routers.
-               m.Group("/issues", func() {
-                       m.Group("/{index}", func() {
-                               m.Post("/title", repo.UpdateIssueTitle)
-                               m.Post("/content", repo.UpdateIssueContent)
-                               m.Post("/watch", repo.IssueWatch)
-                               m.Post("/ref", repo.UpdateIssueRef)
-                               m.Group("/dependency", func() {
-                                       m.Post("/add", repo.AddDependency)
-                                       m.Post("/delete", repo.RemoveDependency)
-                               })
-                               m.Combo("/comments").Post(repo.MustAllowUserComment, bindIgnErr(forms.CreateCommentForm{}), repo.NewComment)
-                               m.Group("/times", func() {
-                                       m.Post("/add", bindIgnErr(forms.AddTimeManuallyForm{}), repo.AddTimeManually)
-                                       m.Post("/{timeid}/delete", repo.DeleteTime)
-                                       m.Group("/stopwatch", func() {
-                                               m.Post("/toggle", repo.IssueStopwatch)
-                                               m.Post("/cancel", repo.CancelStopwatch)
-                                       })
-                               })
-                               m.Post("/reactions/{action}", bindIgnErr(forms.ReactionForm{}), repo.ChangeIssueReaction)
-                               m.Post("/lock", reqRepoIssueWriter, bindIgnErr(forms.IssueLockForm{}), repo.LockIssue)
-                               m.Post("/unlock", reqRepoIssueWriter, repo.UnlockIssue)
-                       }, context.RepoMustNotBeArchived())
-                       m.Group("/{index}", func() {
-                               m.Get("/attachments", repo.GetIssueAttachments)
-                               m.Get("/attachments/{uuid}", repo.GetAttachment)
-                       })
-
-                       m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel)
-                       m.Post("/milestone", reqRepoIssuesOrPullsWriter, repo.UpdateIssueMilestone)
-                       m.Post("/projects", reqRepoIssuesOrPullsWriter, repo.UpdateIssueProject)
-                       m.Post("/assignee", reqRepoIssuesOrPullsWriter, repo.UpdateIssueAssignee)
-                       m.Post("/request_review", reqRepoIssuesOrPullsReader, repo.UpdatePullReviewRequest)
-                       m.Post("/dismiss_review", reqRepoAdmin, bindIgnErr(forms.DismissReviewForm{}), repo.DismissReview)
-                       m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus)
-                       m.Post("/resolve_conversation", reqRepoIssuesOrPullsReader, repo.UpdateResolveConversation)
-                       m.Post("/attachments", repo.UploadIssueAttachment)
-                       m.Post("/attachments/remove", repo.DeleteAttachment)
-               }, context.RepoMustNotBeArchived())
-               m.Group("/comments/{id}", func() {
-                       m.Post("", repo.UpdateCommentContent)
-                       m.Post("/delete", repo.DeleteComment)
-                       m.Post("/reactions/{action}", bindIgnErr(forms.ReactionForm{}), repo.ChangeCommentReaction)
-               }, context.RepoMustNotBeArchived())
-               m.Group("/comments/{id}", func() {
-                       m.Get("/attachments", repo.GetCommentAttachments)
-               })
-               m.Group("/labels", func() {
-                       m.Post("/new", bindIgnErr(forms.CreateLabelForm{}), repo.NewLabel)
-                       m.Post("/edit", bindIgnErr(forms.CreateLabelForm{}), repo.UpdateLabel)
-                       m.Post("/delete", repo.DeleteLabel)
-                       m.Post("/initialize", bindIgnErr(forms.InitializeLabelsForm{}), repo.InitializeLabels)
-               }, context.RepoMustNotBeArchived(), reqRepoIssuesOrPullsWriter, context.RepoRef())
-               m.Group("/milestones", func() {
-                       m.Combo("/new").Get(repo.NewMilestone).
-                               Post(bindIgnErr(forms.CreateMilestoneForm{}), repo.NewMilestonePost)
-                       m.Get("/{id}/edit", repo.EditMilestone)
-                       m.Post("/{id}/edit", bindIgnErr(forms.CreateMilestoneForm{}), repo.EditMilestonePost)
-                       m.Post("/{id}/{action}", repo.ChangeMilestoneStatus)
-                       m.Post("/delete", repo.DeleteMilestone)
-               }, context.RepoMustNotBeArchived(), reqRepoIssuesOrPullsWriter, context.RepoRef())
-               m.Group("/pull", func() {
-                       m.Post("/{index}/target_branch", repo.UpdatePullRequestTarget)
-               }, context.RepoMustNotBeArchived())
-
-               m.Group("", func() {
-                       m.Group("", func() {
-                               m.Combo("/_edit/*").Get(repo.EditFile).
-                                       Post(bindIgnErr(forms.EditRepoFileForm{}), repo.EditFilePost)
-                               m.Combo("/_new/*").Get(repo.NewFile).
-                                       Post(bindIgnErr(forms.EditRepoFileForm{}), repo.NewFilePost)
-                               m.Post("/_preview/*", bindIgnErr(forms.EditPreviewDiffForm{}), repo.DiffPreviewPost)
-                               m.Combo("/_delete/*").Get(repo.DeleteFile).
-                                       Post(bindIgnErr(forms.DeleteRepoFileForm{}), repo.DeleteFilePost)
-                               m.Combo("/_upload/*", repo.MustBeAbleToUpload).
-                                       Get(repo.UploadFile).
-                                       Post(bindIgnErr(forms.UploadRepoFileForm{}), repo.UploadFilePost)
-                       }, context.RepoRefByType(context.RepoRefBranch), repo.MustBeEditable)
-                       m.Group("", func() {
-                               m.Post("/upload-file", repo.UploadFileToServer)
-                               m.Post("/upload-remove", bindIgnErr(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer)
-                       }, context.RepoRef(), repo.MustBeEditable, repo.MustBeAbleToUpload)
-               }, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty)
-
-               m.Group("/branches", func() {
-                       m.Group("/_new", func() {
-                               m.Post("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.CreateBranch)
-                               m.Post("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.CreateBranch)
-                               m.Post("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.CreateBranch)
-                       }, bindIgnErr(forms.NewBranchForm{}))
-                       m.Post("/delete", repo.DeleteBranchPost)
-                       m.Post("/restore", repo.RestoreBranchPost)
-               }, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty)
-
-       }, reqSignIn, context.RepoAssignment, context.UnitTypes())
-
-       // Releases
-       m.Group("/{username}/{reponame}", func() {
-               m.Get("/tags", repo.TagsList, repo.MustBeNotEmpty,
-                       reqRepoCodeReader, context.RepoRefByType(context.RepoRefTag))
-               m.Group("/releases", func() {
-                       m.Get("/", repo.Releases)
-                       m.Get("/tag/*", repo.SingleRelease)
-                       m.Get("/latest", repo.LatestRelease)
-               }, repo.MustBeNotEmpty, reqRepoReleaseReader, context.RepoRefByType(context.RepoRefTag, true))
-               m.Get("/releases/attachments/{uuid}", repo.GetAttachment, repo.MustBeNotEmpty, reqRepoReleaseReader)
-               m.Group("/releases", func() {
-                       m.Get("/new", repo.NewRelease)
-                       m.Post("/new", bindIgnErr(forms.NewReleaseForm{}), repo.NewReleasePost)
-                       m.Post("/delete", repo.DeleteRelease)
-                       m.Post("/attachments", repo.UploadReleaseAttachment)
-                       m.Post("/attachments/remove", repo.DeleteAttachment)
-               }, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, context.RepoRef())
-               m.Post("/tags/delete", repo.DeleteTag, reqSignIn,
-                       repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoCodeWriter, context.RepoRef())
-               m.Group("/releases", func() {
-                       m.Get("/edit/*", repo.EditRelease)
-                       m.Post("/edit/*", bindIgnErr(forms.EditReleaseForm{}), repo.EditReleasePost)
-               }, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, func(ctx *context.Context) {
-                       var err error
-                       ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
-                       if err != nil {
-                               ctx.ServerError("GetBranchCommit", err)
-                               return
-                       }
-                       ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount()
-                       if err != nil {
-                               ctx.ServerError("GetCommitsCount", err)
-                               return
-                       }
-                       ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
-               })
-               m.Get("/attachments/{uuid}", repo.GetAttachment)
-       }, ignSignIn, context.RepoAssignment, context.UnitTypes(), reqRepoReleaseReader)
-
-       m.Group("/{username}/{reponame}", func() {
-               m.Post("/topics", repo.TopicsPost)
-       }, context.RepoAssignment, context.RepoMustNotBeArchived(), reqRepoAdmin)
-
-       m.Group("/{username}/{reponame}", func() {
-               m.Group("", func() {
-                       m.Get("/{type:issues|pulls}", repo.Issues)
-                       m.Get("/{type:issues|pulls}/{index}", repo.ViewIssue)
-                       m.Get("/labels", reqRepoIssuesOrPullsReader, repo.RetrieveLabels, repo.Labels)
-                       m.Get("/milestones", reqRepoIssuesOrPullsReader, repo.Milestones)
-               }, context.RepoRef())
-
-               m.Group("/projects", func() {
-                       m.Get("", repo.Projects)
-                       m.Get("/{id}", repo.ViewProject)
-                       m.Group("", func() {
-                               m.Get("/new", repo.NewProject)
-                               m.Post("/new", bindIgnErr(forms.CreateProjectForm{}), repo.NewProjectPost)
-                               m.Group("/{id}", func() {
-                                       m.Post("", bindIgnErr(forms.EditProjectBoardForm{}), repo.AddBoardToProjectPost)
-                                       m.Post("/delete", repo.DeleteProject)
-
-                                       m.Get("/edit", repo.EditProject)
-                                       m.Post("/edit", bindIgnErr(forms.CreateProjectForm{}), repo.EditProjectPost)
-                                       m.Post("/{action:open|close}", repo.ChangeProjectStatus)
-
-                                       m.Group("/{boardID}", func() {
-                                               m.Put("", bindIgnErr(forms.EditProjectBoardForm{}), repo.EditProjectBoard)
-                                               m.Delete("", repo.DeleteProjectBoard)
-                                               m.Post("/default", repo.SetDefaultProjectBoard)
-
-                                               m.Post("/{index}", repo.MoveIssueAcrossBoards)
-                                       })
-                               })
-                       }, reqRepoProjectsWriter, context.RepoMustNotBeArchived())
-               }, reqRepoProjectsReader, repo.MustEnableProjects)
-
-               m.Group("/wiki", func() {
-                       m.Get("/", repo.Wiki)
-                       m.Get("/{page}", repo.Wiki)
-                       m.Get("/_pages", repo.WikiPages)
-                       m.Get("/{page}/_revision", repo.WikiRevision)
-                       m.Get("/commit/{sha:[a-f0-9]{7,40}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
-                       m.Get("/commit/{sha:[a-f0-9]{7,40}}.{:patch|diff}", repo.RawDiff)
-
-                       m.Group("", func() {
-                               m.Combo("/_new").Get(repo.NewWiki).
-                                       Post(bindIgnErr(forms.NewWikiForm{}), repo.NewWikiPost)
-                               m.Combo("/{page}/_edit").Get(repo.EditWiki).
-                                       Post(bindIgnErr(forms.NewWikiForm{}), repo.EditWikiPost)
-                               m.Post("/{page}/delete", repo.DeleteWikiPagePost)
-                       }, context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter)
-               }, repo.MustEnableWiki, context.RepoRef(), func(ctx *context.Context) {
-                       ctx.Data["PageIsWiki"] = true
-               })
-
-               m.Group("/wiki", func() {
-                       m.Get("/raw/*", repo.WikiRaw)
-               }, repo.MustEnableWiki)
-
-               m.Group("/activity", func() {
-                       m.Get("", repo.Activity)
-                       m.Get("/{period}", repo.Activity)
-               }, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(models.UnitTypePullRequests, models.UnitTypeIssues, models.UnitTypeReleases))
-
-               m.Group("/activity_author_data", func() {
-                       m.Get("", repo.ActivityAuthors)
-                       m.Get("/{period}", repo.ActivityAuthors)
-               }, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(models.UnitTypeCode))
-
-               m.Group("/archive", func() {
-                       m.Get("/*", repo.Download)
-                       m.Post("/*", repo.InitiateDownload)
-               }, repo.MustBeNotEmpty, reqRepoCodeReader)
-
-               m.Group("/branches", func() {
-                       m.Get("", repo.Branches)
-               }, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)
-
-               m.Group("/blob_excerpt", func() {
-                       m.Get("/{sha}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ExcerptBlob)
-               }, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)
-
-               m.Group("/pulls/{index}", func() {
-                       m.Get(".diff", repo.DownloadPullDiff)
-                       m.Get(".patch", repo.DownloadPullPatch)
-                       m.Get("/commits", context.RepoRef(), repo.ViewPullCommits)
-                       m.Post("/merge", context.RepoMustNotBeArchived(), bindIgnErr(forms.MergePullRequestForm{}), repo.MergePullRequest)
-                       m.Post("/update", repo.UpdatePullRequest)
-                       m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest)
-                       m.Group("/files", func() {
-                               m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.ViewPullFiles)
-                               m.Group("/reviews", func() {
-                                       m.Get("/new_comment", repo.RenderNewCodeCommentForm)
-                                       m.Post("/comments", bindIgnErr(forms.CodeCommentForm{}), repo.CreateCodeComment)
-                                       m.Post("/submit", bindIgnErr(forms.SubmitReviewForm{}), repo.SubmitReview)
-                               }, context.RepoMustNotBeArchived())
-                       })
-               }, repo.MustAllowPulls)
-
-               m.Group("/media", func() {
-                       m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.SingleDownloadOrLFS)
-                       m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.SingleDownloadOrLFS)
-                       m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.SingleDownloadOrLFS)
-                       m.Get("/blob/{sha}", context.RepoRefByType(context.RepoRefBlob), repo.DownloadByIDOrLFS)
-                       // "/*" route is deprecated, and kept for backward compatibility
-                       m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.SingleDownloadOrLFS)
-               }, repo.MustBeNotEmpty, reqRepoCodeReader)
-
-               m.Group("/raw", func() {
-                       m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.SingleDownload)
-                       m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.SingleDownload)
-                       m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.SingleDownload)
-                       m.Get("/blob/{sha}", context.RepoRefByType(context.RepoRefBlob), repo.DownloadByID)
-                       // "/*" route is deprecated, and kept for backward compatibility
-                       m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.SingleDownload)
-               }, repo.MustBeNotEmpty, reqRepoCodeReader)
-
-               m.Group("/commits", func() {
-                       m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RefCommits)
-                       m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RefCommits)
-                       m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.RefCommits)
-                       // "/*" route is deprecated, and kept for backward compatibility
-                       m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.RefCommits)
-               }, repo.MustBeNotEmpty, reqRepoCodeReader)
-
-               m.Group("/blame", func() {
-                       m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RefBlame)
-                       m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RefBlame)
-                       m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.RefBlame)
-               }, repo.MustBeNotEmpty, reqRepoCodeReader)
-
-               m.Group("", func() {
-                       m.Get("/graph", repo.Graph)
-                       m.Get("/commit/{sha:([a-f0-9]{7,40})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
-               }, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)
-
-               m.Group("/src", func() {
-                       m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.Home)
-                       m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.Home)
-                       m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.Home)
-                       // "/*" route is deprecated, and kept for backward compatibility
-                       m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.Home)
-               }, repo.SetEditorconfigIfExists)
-
-               m.Group("", func() {
-                       m.Get("/forks", repo.Forks)
-               }, context.RepoRef(), reqRepoCodeReader)
-               m.Get("/commit/{sha:([a-f0-9]{7,40})}.{ext:patch|diff}",
-                       repo.MustBeNotEmpty, reqRepoCodeReader, repo.RawDiff)
-       }, ignSignIn, context.RepoAssignment, context.UnitTypes())
-       m.Group("/{username}/{reponame}", func() {
-               m.Get("/stars", repo.Stars)
-               m.Get("/watchers", repo.Watchers)
-               m.Get("/search", reqRepoCodeReader, repo.Search)
-       }, ignSignIn, context.RepoAssignment, context.RepoRef(), context.UnitTypes())
-
-       m.Group("/{username}", func() {
-               m.Group("/{reponame}", func() {
-                       m.Get("", repo.SetEditorconfigIfExists, repo.Home)
-               }, ignSignIn, context.RepoAssignment, context.RepoRef(), context.UnitTypes())
-
-               m.Group("/{reponame}", func() {
-                       m.Group("/info/lfs", func() {
-                               m.Post("/objects/batch", lfs.CheckAcceptMediaType, lfs.BatchHandler)
-                               m.Put("/objects/{oid}/{size}", lfs.UploadHandler)
-                               m.Get("/objects/{oid}/{filename}", lfs.DownloadHandler)
-                               m.Get("/objects/{oid}", lfs.DownloadHandler)
-                               m.Post("/verify", lfs.CheckAcceptMediaType, lfs.VerifyHandler)
-                               m.Group("/locks", func() {
-                                       m.Get("/", lfs.GetListLockHandler)
-                                       m.Post("/", lfs.PostLockHandler)
-                                       m.Post("/verify", lfs.VerifyLockHandler)
-                                       m.Post("/{lid}/unlock", lfs.UnLockHandler)
-                               }, lfs.CheckAcceptMediaType)
-                               m.Any("/*", func(ctx *context.Context) {
-                                       ctx.NotFound("", nil)
-                               })
-                       }, ignSignInAndCsrf, lfsServerEnabled)
-
-                       m.Group("", func() {
-                               m.Post("/git-upload-pack", repo.ServiceUploadPack)
-                               m.Post("/git-receive-pack", repo.ServiceReceivePack)
-                               m.Get("/info/refs", repo.GetInfoRefs)
-                               m.Get("/HEAD", repo.GetTextFile("HEAD"))
-                               m.Get("/objects/info/alternates", repo.GetTextFile("objects/info/alternates"))
-                               m.Get("/objects/info/http-alternates", repo.GetTextFile("objects/info/http-alternates"))
-                               m.Get("/objects/info/packs", repo.GetInfoPacks)
-                               m.Get("/objects/info/{file:[^/]*}", repo.GetTextFile(""))
-                               m.Get("/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38}}", repo.GetLooseObject)
-                               m.Get("/objects/pack/pack-{file:[0-9a-f]{40}}.pack", repo.GetPackFile)
-                               m.Get("/objects/pack/pack-{file:[0-9a-f]{40}}.idx", repo.GetIdxFile)
-                       }, ignSignInAndCsrf)
-
-                       m.Head("/tasks/trigger", repo.TriggerTask)
-               })
-       })
-       // ***** END: Repository *****
-
-       m.Group("/notifications", func() {
-               m.Get("", user.Notifications)
-               m.Post("/status", user.NotificationStatusPost)
-               m.Post("/purge", user.NotificationPurgePost)
-       }, reqSignIn)
-
-       if setting.API.EnableSwagger {
-               m.Get("/swagger.v1.json", routers.SwaggerV1Json)
-       }
-
-       // Not found handler.
-       m.NotFound(web.Wrap(routers.NotFound))
-}
diff --git a/routers/swagger_json.go b/routers/swagger_json.go
deleted file mode 100644 (file)
index 78c7fb1..0000000
+++ /dev/null
@@ -1,26 +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 routers
-
-import (
-       "net/http"
-
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/log"
-)
-
-// tplSwaggerV1Json swagger v1 json template
-const tplSwaggerV1Json base.TplName = "swagger/v1_json"
-
-// SwaggerV1Json render swagger v1 json
-func SwaggerV1Json(ctx *context.Context) {
-       t := ctx.Render.TemplateLookup(string(tplSwaggerV1Json))
-       ctx.Resp.Header().Set("Content-Type", "application/json")
-       if err := t.Execute(ctx.Resp, ctx.Data); err != nil {
-               log.Error("%v", err)
-               ctx.Error(http.StatusInternalServerError)
-       }
-}
diff --git a/routers/user/auth.go b/routers/user/auth.go
deleted file mode 100644 (file)
index 827b7cd..0000000
+++ /dev/null
@@ -1,1769 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2018 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 user
-
-import (
-       "errors"
-       "fmt"
-       "io"
-       "io/ioutil"
-       "net/http"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/auth/oauth2"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/eventsource"
-       "code.gitea.io/gitea/modules/hcaptcha"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/password"
-       "code.gitea.io/gitea/modules/recaptcha"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/timeutil"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/modules/web/middleware"
-       "code.gitea.io/gitea/routers/utils"
-       "code.gitea.io/gitea/services/externalaccount"
-       "code.gitea.io/gitea/services/forms"
-       "code.gitea.io/gitea/services/mailer"
-
-       "github.com/markbates/goth"
-       "github.com/tstranex/u2f"
-)
-
-const (
-       // tplMustChangePassword template for updating a user's password
-       tplMustChangePassword = "user/auth/change_passwd"
-       // tplSignIn template for sign in page
-       tplSignIn base.TplName = "user/auth/signin"
-       // tplSignUp template path for sign up page
-       tplSignUp base.TplName = "user/auth/signup"
-       // TplActivate template path for activate user
-       TplActivate       base.TplName = "user/auth/activate"
-       tplForgotPassword base.TplName = "user/auth/forgot_passwd"
-       tplResetPassword  base.TplName = "user/auth/reset_passwd"
-       tplTwofa          base.TplName = "user/auth/twofa"
-       tplTwofaScratch   base.TplName = "user/auth/twofa_scratch"
-       tplLinkAccount    base.TplName = "user/auth/link_account"
-       tplU2F            base.TplName = "user/auth/u2f"
-)
-
-// AutoSignIn reads cookie and try to auto-login.
-func AutoSignIn(ctx *context.Context) (bool, error) {
-       if !models.HasEngine {
-               return false, nil
-       }
-
-       uname := ctx.GetCookie(setting.CookieUserName)
-       if len(uname) == 0 {
-               return false, nil
-       }
-
-       isSucceed := false
-       defer func() {
-               if !isSucceed {
-                       log.Trace("auto-login cookie cleared: %s", uname)
-                       ctx.DeleteCookie(setting.CookieUserName)
-                       ctx.DeleteCookie(setting.CookieRememberName)
-               }
-       }()
-
-       u, err := models.GetUserByName(uname)
-       if err != nil {
-               if !models.IsErrUserNotExist(err) {
-                       return false, fmt.Errorf("GetUserByName: %v", err)
-               }
-               return false, nil
-       }
-
-       if val, ok := ctx.GetSuperSecureCookie(
-               base.EncodeMD5(u.Rands+u.Passwd), setting.CookieRememberName); !ok || val != u.Name {
-               return false, nil
-       }
-
-       isSucceed = true
-
-       // Set session IDs
-       if err := ctx.Session.Set("uid", u.ID); err != nil {
-               return false, err
-       }
-       if err := ctx.Session.Set("uname", u.Name); err != nil {
-               return false, err
-       }
-       if err := ctx.Session.Release(); err != nil {
-               return false, err
-       }
-
-       middleware.DeleteCSRFCookie(ctx.Resp)
-       return true, nil
-}
-
-func checkAutoLogin(ctx *context.Context) bool {
-       // Check auto-login.
-       isSucceed, err := AutoSignIn(ctx)
-       if err != nil {
-               ctx.ServerError("AutoSignIn", err)
-               return true
-       }
-
-       redirectTo := ctx.Query("redirect_to")
-       if len(redirectTo) > 0 {
-               middleware.SetRedirectToCookie(ctx.Resp, redirectTo)
-       } else {
-               redirectTo = ctx.GetCookie("redirect_to")
-       }
-
-       if isSucceed {
-               middleware.DeleteRedirectToCookie(ctx.Resp)
-               ctx.RedirectToFirst(redirectTo, setting.AppSubURL+string(setting.LandingPageURL))
-               return true
-       }
-
-       return false
-}
-
-// SignIn render sign in page
-func SignIn(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("sign_in")
-
-       // Check auto-login.
-       if checkAutoLogin(ctx) {
-               return
-       }
-
-       orderedOAuth2Names, oauth2Providers, err := models.GetActiveOAuth2Providers()
-       if err != nil {
-               ctx.ServerError("UserSignIn", err)
-               return
-       }
-       ctx.Data["OrderedOAuth2Names"] = orderedOAuth2Names
-       ctx.Data["OAuth2Providers"] = oauth2Providers
-       ctx.Data["Title"] = ctx.Tr("sign_in")
-       ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
-       ctx.Data["PageIsSignIn"] = true
-       ctx.Data["PageIsLogin"] = true
-       ctx.Data["EnableSSPI"] = models.IsSSPIEnabled()
-
-       ctx.HTML(http.StatusOK, tplSignIn)
-}
-
-// SignInPost response for sign in request
-func SignInPost(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("sign_in")
-
-       orderedOAuth2Names, oauth2Providers, err := models.GetActiveOAuth2Providers()
-       if err != nil {
-               ctx.ServerError("UserSignIn", err)
-               return
-       }
-       ctx.Data["OrderedOAuth2Names"] = orderedOAuth2Names
-       ctx.Data["OAuth2Providers"] = oauth2Providers
-       ctx.Data["Title"] = ctx.Tr("sign_in")
-       ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
-       ctx.Data["PageIsSignIn"] = true
-       ctx.Data["PageIsLogin"] = true
-       ctx.Data["EnableSSPI"] = models.IsSSPIEnabled()
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplSignIn)
-               return
-       }
-
-       form := web.GetForm(ctx).(*forms.SignInForm)
-       u, err := models.UserSignIn(form.UserName, form.Password)
-       if err != nil {
-               if models.IsErrUserNotExist(err) {
-                       ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form)
-                       log.Info("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
-               } else if models.IsErrEmailAlreadyUsed(err) {
-                       ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSignIn, &form)
-                       log.Info("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
-               } else if models.IsErrUserProhibitLogin(err) {
-                       log.Info("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
-                       ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
-                       ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
-               } else if models.IsErrUserInactive(err) {
-                       if setting.Service.RegisterEmailConfirm {
-                               ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
-                               ctx.HTML(http.StatusOK, TplActivate)
-                       } else {
-                               log.Info("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
-                               ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
-                               ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
-                       }
-               } else {
-                       ctx.ServerError("UserSignIn", err)
-               }
-               return
-       }
-       // If this user is enrolled in 2FA, we can't sign the user in just yet.
-       // Instead, redirect them to the 2FA authentication page.
-       _, err = models.GetTwoFactorByUID(u.ID)
-       if err != nil {
-               if models.IsErrTwoFactorNotEnrolled(err) {
-                       handleSignIn(ctx, u, form.Remember)
-               } else {
-                       ctx.ServerError("UserSignIn", err)
-               }
-               return
-       }
-
-       // User needs to use 2FA, save data and redirect to 2FA page.
-       if err := ctx.Session.Set("twofaUid", u.ID); err != nil {
-               ctx.ServerError("UserSignIn: Unable to set twofaUid in session", err)
-               return
-       }
-       if err := ctx.Session.Set("twofaRemember", form.Remember); err != nil {
-               ctx.ServerError("UserSignIn: Unable to set twofaRemember in session", err)
-               return
-       }
-       if err := ctx.Session.Release(); err != nil {
-               ctx.ServerError("UserSignIn: Unable to save session", err)
-               return
-       }
-
-       regs, err := models.GetU2FRegistrationsByUID(u.ID)
-       if err == nil && len(regs) > 0 {
-               ctx.Redirect(setting.AppSubURL + "/user/u2f")
-               return
-       }
-
-       ctx.Redirect(setting.AppSubURL + "/user/two_factor")
-}
-
-// TwoFactor shows the user a two-factor authentication page.
-func TwoFactor(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("twofa")
-
-       // Check auto-login.
-       if checkAutoLogin(ctx) {
-               return
-       }
-
-       // Ensure user is in a 2FA session.
-       if ctx.Session.Get("twofaUid") == nil {
-               ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
-               return
-       }
-
-       ctx.HTML(http.StatusOK, tplTwofa)
-}
-
-// TwoFactorPost validates a user's two-factor authentication token.
-func TwoFactorPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.TwoFactorAuthForm)
-       ctx.Data["Title"] = ctx.Tr("twofa")
-
-       // Ensure user is in a 2FA session.
-       idSess := ctx.Session.Get("twofaUid")
-       if idSess == nil {
-               ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
-               return
-       }
-
-       id := idSess.(int64)
-       twofa, err := models.GetTwoFactorByUID(id)
-       if err != nil {
-               ctx.ServerError("UserSignIn", err)
-               return
-       }
-
-       // Validate the passcode with the stored TOTP secret.
-       ok, err := twofa.ValidateTOTP(form.Passcode)
-       if err != nil {
-               ctx.ServerError("UserSignIn", err)
-               return
-       }
-
-       if ok && twofa.LastUsedPasscode != form.Passcode {
-               remember := ctx.Session.Get("twofaRemember").(bool)
-               u, err := models.GetUserByID(id)
-               if err != nil {
-                       ctx.ServerError("UserSignIn", err)
-                       return
-               }
-
-               if ctx.Session.Get("linkAccount") != nil {
-                       gothUser := ctx.Session.Get("linkAccountGothUser")
-                       if gothUser == nil {
-                               ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
-                               return
-                       }
-
-                       err = externalaccount.LinkAccountToUser(u, gothUser.(goth.User))
-                       if err != nil {
-                               ctx.ServerError("UserSignIn", err)
-                               return
-                       }
-               }
-
-               twofa.LastUsedPasscode = form.Passcode
-               if err = models.UpdateTwoFactor(twofa); err != nil {
-                       ctx.ServerError("UserSignIn", err)
-                       return
-               }
-
-               handleSignIn(ctx, u, remember)
-               return
-       }
-
-       ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplTwofa, forms.TwoFactorAuthForm{})
-}
-
-// TwoFactorScratch shows the scratch code form for two-factor authentication.
-func TwoFactorScratch(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("twofa_scratch")
-
-       // Check auto-login.
-       if checkAutoLogin(ctx) {
-               return
-       }
-
-       // Ensure user is in a 2FA session.
-       if ctx.Session.Get("twofaUid") == nil {
-               ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
-               return
-       }
-
-       ctx.HTML(http.StatusOK, tplTwofaScratch)
-}
-
-// TwoFactorScratchPost validates and invalidates a user's two-factor scratch token.
-func TwoFactorScratchPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.TwoFactorScratchAuthForm)
-       ctx.Data["Title"] = ctx.Tr("twofa_scratch")
-
-       // Ensure user is in a 2FA session.
-       idSess := ctx.Session.Get("twofaUid")
-       if idSess == nil {
-               ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
-               return
-       }
-
-       id := idSess.(int64)
-       twofa, err := models.GetTwoFactorByUID(id)
-       if err != nil {
-               ctx.ServerError("UserSignIn", err)
-               return
-       }
-
-       // Validate the passcode with the stored TOTP secret.
-       if twofa.VerifyScratchToken(form.Token) {
-               // Invalidate the scratch token.
-               _, err = twofa.GenerateScratchToken()
-               if err != nil {
-                       ctx.ServerError("UserSignIn", err)
-                       return
-               }
-               if err = models.UpdateTwoFactor(twofa); err != nil {
-                       ctx.ServerError("UserSignIn", err)
-                       return
-               }
-
-               remember := ctx.Session.Get("twofaRemember").(bool)
-               u, err := models.GetUserByID(id)
-               if err != nil {
-                       ctx.ServerError("UserSignIn", err)
-                       return
-               }
-
-               handleSignInFull(ctx, u, remember, false)
-               ctx.Flash.Info(ctx.Tr("auth.twofa_scratch_used"))
-               ctx.Redirect(setting.AppSubURL + "/user/settings/security")
-               return
-       }
-
-       ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplTwofaScratch, forms.TwoFactorScratchAuthForm{})
-}
-
-// U2F shows the U2F login page
-func U2F(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("twofa")
-       ctx.Data["RequireU2F"] = true
-       // Check auto-login.
-       if checkAutoLogin(ctx) {
-               return
-       }
-
-       // Ensure user is in a 2FA session.
-       if ctx.Session.Get("twofaUid") == nil {
-               ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
-               return
-       }
-
-       ctx.HTML(http.StatusOK, tplU2F)
-}
-
-// U2FChallenge submits a sign challenge to the browser
-func U2FChallenge(ctx *context.Context) {
-       // Ensure user is in a U2F session.
-       idSess := ctx.Session.Get("twofaUid")
-       if idSess == nil {
-               ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
-               return
-       }
-       id := idSess.(int64)
-       regs, err := models.GetU2FRegistrationsByUID(id)
-       if err != nil {
-               ctx.ServerError("UserSignIn", err)
-               return
-       }
-       if len(regs) == 0 {
-               ctx.ServerError("UserSignIn", errors.New("no device registered"))
-               return
-       }
-       challenge, err := u2f.NewChallenge(setting.U2F.AppID, setting.U2F.TrustedFacets)
-       if err != nil {
-               ctx.ServerError("u2f.NewChallenge", err)
-               return
-       }
-       if err := ctx.Session.Set("u2fChallenge", challenge); err != nil {
-               ctx.ServerError("UserSignIn: unable to set u2fChallenge in session", err)
-               return
-       }
-       if err := ctx.Session.Release(); err != nil {
-               ctx.ServerError("UserSignIn: unable to store session", err)
-       }
-
-       ctx.JSON(http.StatusOK, challenge.SignRequest(regs.ToRegistrations()))
-}
-
-// U2FSign authenticates the user by signResp
-func U2FSign(ctx *context.Context) {
-       signResp := web.GetForm(ctx).(*u2f.SignResponse)
-       challSess := ctx.Session.Get("u2fChallenge")
-       idSess := ctx.Session.Get("twofaUid")
-       if challSess == nil || idSess == nil {
-               ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
-               return
-       }
-       challenge := challSess.(*u2f.Challenge)
-       id := idSess.(int64)
-       regs, err := models.GetU2FRegistrationsByUID(id)
-       if err != nil {
-               ctx.ServerError("UserSignIn", err)
-               return
-       }
-       for _, reg := range regs {
-               r, err := reg.Parse()
-               if err != nil {
-                       log.Fatal("parsing u2f registration: %v", err)
-                       continue
-               }
-               newCounter, authErr := r.Authenticate(*signResp, *challenge, reg.Counter)
-               if authErr == nil {
-                       reg.Counter = newCounter
-                       user, err := models.GetUserByID(id)
-                       if err != nil {
-                               ctx.ServerError("UserSignIn", err)
-                               return
-                       }
-                       remember := ctx.Session.Get("twofaRemember").(bool)
-                       if err := reg.UpdateCounter(); err != nil {
-                               ctx.ServerError("UserSignIn", err)
-                               return
-                       }
-
-                       if ctx.Session.Get("linkAccount") != nil {
-                               gothUser := ctx.Session.Get("linkAccountGothUser")
-                               if gothUser == nil {
-                                       ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
-                                       return
-                               }
-
-                               err = externalaccount.LinkAccountToUser(user, gothUser.(goth.User))
-                               if err != nil {
-                                       ctx.ServerError("UserSignIn", err)
-                                       return
-                               }
-                       }
-                       redirect := handleSignInFull(ctx, user, remember, false)
-                       if redirect == "" {
-                               redirect = setting.AppSubURL + "/"
-                       }
-                       ctx.PlainText(200, []byte(redirect))
-                       return
-               }
-       }
-       ctx.Error(http.StatusUnauthorized)
-}
-
-// This handles the final part of the sign-in process of the user.
-func handleSignIn(ctx *context.Context, u *models.User, remember bool) {
-       handleSignInFull(ctx, u, remember, true)
-}
-
-func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyRedirect bool) string {
-       if remember {
-               days := 86400 * setting.LogInRememberDays
-               ctx.SetCookie(setting.CookieUserName, u.Name, days)
-               ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
-                       setting.CookieRememberName, u.Name, days)
-       }
-
-       _ = ctx.Session.Delete("openid_verified_uri")
-       _ = ctx.Session.Delete("openid_signin_remember")
-       _ = ctx.Session.Delete("openid_determined_email")
-       _ = ctx.Session.Delete("openid_determined_username")
-       _ = ctx.Session.Delete("twofaUid")
-       _ = ctx.Session.Delete("twofaRemember")
-       _ = ctx.Session.Delete("u2fChallenge")
-       _ = ctx.Session.Delete("linkAccount")
-       if err := ctx.Session.Set("uid", u.ID); err != nil {
-               log.Error("Error setting uid %d in session: %v", u.ID, err)
-       }
-       if err := ctx.Session.Set("uname", u.Name); err != nil {
-               log.Error("Error setting uname %s session: %v", u.Name, err)
-       }
-       if err := ctx.Session.Release(); err != nil {
-               log.Error("Unable to store session: %v", err)
-       }
-
-       // Language setting of the user overwrites the one previously set
-       // If the user does not have a locale set, we save the current one.
-       if len(u.Language) == 0 {
-               u.Language = ctx.Locale.Language()
-               if err := models.UpdateUserCols(u, "language"); err != nil {
-                       log.Error(fmt.Sprintf("Error updating user language [user: %d, locale: %s]", u.ID, u.Language))
-                       return setting.AppSubURL + "/"
-               }
-       }
-
-       middleware.SetLocaleCookie(ctx.Resp, u.Language, 0)
-
-       // Clear whatever CSRF has right now, force to generate a new one
-       middleware.DeleteCSRFCookie(ctx.Resp)
-
-       // Register last login
-       u.SetLastLogin()
-       if err := models.UpdateUserCols(u, "last_login_unix"); err != nil {
-               ctx.ServerError("UpdateUserCols", err)
-               return setting.AppSubURL + "/"
-       }
-
-       if redirectTo := ctx.GetCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
-               middleware.DeleteRedirectToCookie(ctx.Resp)
-               if obeyRedirect {
-                       ctx.RedirectToFirst(redirectTo)
-               }
-               return redirectTo
-       }
-
-       if obeyRedirect {
-               ctx.Redirect(setting.AppSubURL + "/")
-       }
-       return setting.AppSubURL + "/"
-}
-
-// SignInOAuth handles the OAuth2 login buttons
-func SignInOAuth(ctx *context.Context) {
-       provider := ctx.Params(":provider")
-
-       loginSource, err := models.GetActiveOAuth2LoginSourceByName(provider)
-       if err != nil {
-               ctx.ServerError("SignIn", err)
-               return
-       }
-
-       // try to do a direct callback flow, so we don't authenticate the user again but use the valid accesstoken to get the user
-       user, gothUser, err := oAuth2UserLoginCallback(loginSource, ctx.Req, ctx.Resp)
-       if err == nil && user != nil {
-               // we got the user without going through the whole OAuth2 authentication flow again
-               handleOAuth2SignIn(ctx, user, gothUser)
-               return
-       }
-
-       if err = oauth2.Auth(loginSource.Name, ctx.Req, ctx.Resp); err != nil {
-               if strings.Contains(err.Error(), "no provider for ") {
-                       if err = models.ResetOAuth2(); err != nil {
-                               ctx.ServerError("SignIn", err)
-                               return
-                       }
-                       if err = oauth2.Auth(loginSource.Name, ctx.Req, ctx.Resp); err != nil {
-                               ctx.ServerError("SignIn", err)
-                       }
-                       return
-               }
-               ctx.ServerError("SignIn", err)
-       }
-       // redirect is done in oauth2.Auth
-}
-
-// SignInOAuthCallback handles the callback from the given provider
-func SignInOAuthCallback(ctx *context.Context) {
-       provider := ctx.Params(":provider")
-
-       // first look if the provider is still active
-       loginSource, err := models.GetActiveOAuth2LoginSourceByName(provider)
-       if err != nil {
-               ctx.ServerError("SignIn", err)
-               return
-       }
-
-       if loginSource == nil {
-               ctx.ServerError("SignIn", errors.New("No valid provider found, check configured callback url in provider"))
-               return
-       }
-
-       u, gothUser, err := oAuth2UserLoginCallback(loginSource, ctx.Req, ctx.Resp)
-
-       if err != nil {
-               ctx.ServerError("UserSignIn", err)
-               return
-       }
-
-       if u == nil {
-               if !(setting.Service.DisableRegistration || setting.Service.AllowOnlyInternalRegistration) && setting.OAuth2Client.EnableAutoRegistration {
-                       // create new user with details from oauth2 provider
-                       var missingFields []string
-                       if gothUser.UserID == "" {
-                               missingFields = append(missingFields, "sub")
-                       }
-                       if gothUser.Email == "" {
-                               missingFields = append(missingFields, "email")
-                       }
-                       if setting.OAuth2Client.Username == setting.OAuth2UsernameNickname && gothUser.NickName == "" {
-                               missingFields = append(missingFields, "nickname")
-                       }
-                       if len(missingFields) > 0 {
-                               log.Error("OAuth2 Provider %s returned empty or missing fields: %s", loginSource.Name, missingFields)
-                               if loginSource.IsOAuth2() && loginSource.OAuth2().Provider == "openidConnect" {
-                                       log.Error("You may need to change the 'OPENID_CONNECT_SCOPES' setting to request all required fields")
-                               }
-                               err = fmt.Errorf("OAuth2 Provider %s returned empty or missing fields: %s", loginSource.Name, missingFields)
-                               ctx.ServerError("CreateUser", err)
-                               return
-                       }
-                       u = &models.User{
-                               Name:        getUserName(&gothUser),
-                               FullName:    gothUser.Name,
-                               Email:       gothUser.Email,
-                               IsActive:    !setting.OAuth2Client.RegisterEmailConfirm,
-                               LoginType:   models.LoginOAuth2,
-                               LoginSource: loginSource.ID,
-                               LoginName:   gothUser.UserID,
-                       }
-
-                       if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
-                               // error already handled
-                               return
-                       }
-               } else {
-                       // no existing user is found, request attach or new account
-                       showLinkingLogin(ctx, gothUser)
-                       return
-               }
-       }
-
-       handleOAuth2SignIn(ctx, u, gothUser)
-}
-
-func getUserName(gothUser *goth.User) string {
-       switch setting.OAuth2Client.Username {
-       case setting.OAuth2UsernameEmail:
-               return strings.Split(gothUser.Email, "@")[0]
-       case setting.OAuth2UsernameNickname:
-               return gothUser.NickName
-       default: // OAuth2UsernameUserid
-               return gothUser.UserID
-       }
-}
-
-func showLinkingLogin(ctx *context.Context, gothUser goth.User) {
-       if err := ctx.Session.Set("linkAccountGothUser", gothUser); err != nil {
-               log.Error("Error setting linkAccountGothUser in session: %v", err)
-       }
-       if err := ctx.Session.Release(); err != nil {
-               log.Error("Error storing session: %v", err)
-       }
-       ctx.Redirect(setting.AppSubURL + "/user/link_account")
-}
-
-func updateAvatarIfNeed(url string, u *models.User) {
-       if setting.OAuth2Client.UpdateAvatar && len(url) > 0 {
-               resp, err := http.Get(url)
-               if err == nil {
-                       defer func() {
-                               _ = resp.Body.Close()
-                       }()
-               }
-               // ignore any error
-               if err == nil && resp.StatusCode == http.StatusOK {
-                       data, err := ioutil.ReadAll(io.LimitReader(resp.Body, setting.Avatar.MaxFileSize+1))
-                       if err == nil && int64(len(data)) <= setting.Avatar.MaxFileSize {
-                               _ = u.UploadAvatar(data)
-                       }
-               }
-       }
-}
-
-func handleOAuth2SignIn(ctx *context.Context, u *models.User, gothUser goth.User) {
-       updateAvatarIfNeed(gothUser.AvatarURL, u)
-
-       // If this user is enrolled in 2FA, we can't sign the user in just yet.
-       // Instead, redirect them to the 2FA authentication page.
-       _, err := models.GetTwoFactorByUID(u.ID)
-       if err != nil {
-               if !models.IsErrTwoFactorNotEnrolled(err) {
-                       ctx.ServerError("UserSignIn", err)
-                       return
-               }
-
-               if err := ctx.Session.Set("uid", u.ID); err != nil {
-                       log.Error("Error setting uid in session: %v", err)
-               }
-               if err := ctx.Session.Set("uname", u.Name); err != nil {
-                       log.Error("Error setting uname in session: %v", err)
-               }
-               if err := ctx.Session.Release(); err != nil {
-                       log.Error("Error storing session: %v", err)
-               }
-
-               // Clear whatever CSRF has right now, force to generate a new one
-               middleware.DeleteCSRFCookie(ctx.Resp)
-
-               // Register last login
-               u.SetLastLogin()
-               if err := models.UpdateUserCols(u, "last_login_unix"); err != nil {
-                       ctx.ServerError("UpdateUserCols", err)
-                       return
-               }
-
-               // update external user information
-               if err := models.UpdateExternalUser(u, gothUser); err != nil {
-                       log.Error("UpdateExternalUser failed: %v", err)
-               }
-
-               if redirectTo := ctx.GetCookie("redirect_to"); len(redirectTo) > 0 {
-                       middleware.DeleteRedirectToCookie(ctx.Resp)
-                       ctx.RedirectToFirst(redirectTo)
-                       return
-               }
-
-               ctx.Redirect(setting.AppSubURL + "/")
-               return
-       }
-
-       // User needs to use 2FA, save data and redirect to 2FA page.
-       if err := ctx.Session.Set("twofaUid", u.ID); err != nil {
-               log.Error("Error setting twofaUid in session: %v", err)
-       }
-       if err := ctx.Session.Set("twofaRemember", false); err != nil {
-               log.Error("Error setting twofaRemember in session: %v", err)
-       }
-       if err := ctx.Session.Release(); err != nil {
-               log.Error("Error storing session: %v", err)
-       }
-
-       // If U2F is enrolled -> Redirect to U2F instead
-       regs, err := models.GetU2FRegistrationsByUID(u.ID)
-       if err == nil && len(regs) > 0 {
-               ctx.Redirect(setting.AppSubURL + "/user/u2f")
-               return
-       }
-
-       ctx.Redirect(setting.AppSubURL + "/user/two_factor")
-}
-
-// OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful
-// login the user
-func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Request, response http.ResponseWriter) (*models.User, goth.User, error) {
-       gothUser, err := oauth2.ProviderCallback(loginSource.Name, request, response)
-
-       if err != nil {
-               if err.Error() == "securecookie: the value is too long" {
-                       log.Error("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", loginSource.Name, setting.OAuth2.MaxTokenLength)
-                       err = fmt.Errorf("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", loginSource.Name, setting.OAuth2.MaxTokenLength)
-               }
-               return nil, goth.User{}, err
-       }
-
-       user := &models.User{
-               LoginName:   gothUser.UserID,
-               LoginType:   models.LoginOAuth2,
-               LoginSource: loginSource.ID,
-       }
-
-       hasUser, err := models.GetUser(user)
-       if err != nil {
-               return nil, goth.User{}, err
-       }
-
-       if hasUser {
-               return user, gothUser, nil
-       }
-
-       // search in external linked users
-       externalLoginUser := &models.ExternalLoginUser{
-               ExternalID:    gothUser.UserID,
-               LoginSourceID: loginSource.ID,
-       }
-       hasUser, err = models.GetExternalLogin(externalLoginUser)
-       if err != nil {
-               return nil, goth.User{}, err
-       }
-       if hasUser {
-               user, err = models.GetUserByID(externalLoginUser.UserID)
-               return user, gothUser, err
-       }
-
-       // no user found to login
-       return nil, gothUser, nil
-
-}
-
-// LinkAccount shows the page where the user can decide to login or create a new account
-func LinkAccount(ctx *context.Context) {
-       ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
-       ctx.Data["Title"] = ctx.Tr("link_account")
-       ctx.Data["LinkAccountMode"] = true
-       ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
-       ctx.Data["Captcha"] = context.GetImageCaptcha()
-       ctx.Data["CaptchaType"] = setting.Service.CaptchaType
-       ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
-       ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
-       ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
-       ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
-       ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
-       ctx.Data["ShowRegistrationButton"] = false
-
-       // use this to set the right link into the signIn and signUp templates in the link_account template
-       ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
-       ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
-
-       gothUser := ctx.Session.Get("linkAccountGothUser")
-       if gothUser == nil {
-               ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
-               return
-       }
-
-       gu, _ := gothUser.(goth.User)
-       uname := getUserName(&gu)
-       email := gu.Email
-       ctx.Data["user_name"] = uname
-       ctx.Data["email"] = email
-
-       if len(email) != 0 {
-               u, err := models.GetUserByEmail(email)
-               if err != nil && !models.IsErrUserNotExist(err) {
-                       ctx.ServerError("UserSignIn", err)
-                       return
-               }
-               if u != nil {
-                       ctx.Data["user_exists"] = true
-               }
-       } else if len(uname) != 0 {
-               u, err := models.GetUserByName(uname)
-               if err != nil && !models.IsErrUserNotExist(err) {
-                       ctx.ServerError("UserSignIn", err)
-                       return
-               }
-               if u != nil {
-                       ctx.Data["user_exists"] = true
-               }
-       }
-
-       ctx.HTML(http.StatusOK, tplLinkAccount)
-}
-
-// LinkAccountPostSignIn handle the coupling of external account with another account using signIn
-func LinkAccountPostSignIn(ctx *context.Context) {
-       signInForm := web.GetForm(ctx).(*forms.SignInForm)
-       ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
-       ctx.Data["Title"] = ctx.Tr("link_account")
-       ctx.Data["LinkAccountMode"] = true
-       ctx.Data["LinkAccountModeSignIn"] = true
-       ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
-       ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
-       ctx.Data["Captcha"] = context.GetImageCaptcha()
-       ctx.Data["CaptchaType"] = setting.Service.CaptchaType
-       ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
-       ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
-       ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
-       ctx.Data["ShowRegistrationButton"] = false
-
-       // use this to set the right link into the signIn and signUp templates in the link_account template
-       ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
-       ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
-
-       gothUser := ctx.Session.Get("linkAccountGothUser")
-       if gothUser == nil {
-               ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
-               return
-       }
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplLinkAccount)
-               return
-       }
-
-       u, err := models.UserSignIn(signInForm.UserName, signInForm.Password)
-       if err != nil {
-               if models.IsErrUserNotExist(err) {
-                       ctx.Data["user_exists"] = true
-                       ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplLinkAccount, &signInForm)
-               } else {
-                       ctx.ServerError("UserLinkAccount", err)
-               }
-               return
-       }
-
-       linkAccount(ctx, u, gothUser.(goth.User), signInForm.Remember)
-}
-
-func linkAccount(ctx *context.Context, u *models.User, gothUser goth.User, remember bool) {
-       updateAvatarIfNeed(gothUser.AvatarURL, u)
-
-       // If this user is enrolled in 2FA, we can't sign the user in just yet.
-       // Instead, redirect them to the 2FA authentication page.
-       _, err := models.GetTwoFactorByUID(u.ID)
-       if err != nil {
-               if !models.IsErrTwoFactorNotEnrolled(err) {
-                       ctx.ServerError("UserLinkAccount", err)
-                       return
-               }
-
-               err = externalaccount.LinkAccountToUser(u, gothUser)
-               if err != nil {
-                       ctx.ServerError("UserLinkAccount", err)
-                       return
-               }
-
-               handleSignIn(ctx, u, remember)
-               return
-       }
-
-       // User needs to use 2FA, save data and redirect to 2FA page.
-       if err := ctx.Session.Set("twofaUid", u.ID); err != nil {
-               log.Error("Error setting twofaUid in session: %v", err)
-       }
-       if err := ctx.Session.Set("twofaRemember", remember); err != nil {
-               log.Error("Error setting twofaRemember in session: %v", err)
-       }
-       if err := ctx.Session.Set("linkAccount", true); err != nil {
-               log.Error("Error setting linkAccount in session: %v", err)
-       }
-       if err := ctx.Session.Release(); err != nil {
-               log.Error("Error storing session: %v", err)
-       }
-
-       // If U2F is enrolled -> Redirect to U2F instead
-       regs, err := models.GetU2FRegistrationsByUID(u.ID)
-       if err == nil && len(regs) > 0 {
-               ctx.Redirect(setting.AppSubURL + "/user/u2f")
-               return
-       }
-
-       ctx.Redirect(setting.AppSubURL + "/user/two_factor")
-}
-
-// LinkAccountPostRegister handle the creation of a new account for an external account using signUp
-func LinkAccountPostRegister(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.RegisterForm)
-       // TODO Make insecure passwords optional for local accounts also,
-       //      once email-based Second-Factor Auth is available
-       ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
-       ctx.Data["Title"] = ctx.Tr("link_account")
-       ctx.Data["LinkAccountMode"] = true
-       ctx.Data["LinkAccountModeRegister"] = true
-       ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
-       ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
-       ctx.Data["Captcha"] = context.GetImageCaptcha()
-       ctx.Data["CaptchaType"] = setting.Service.CaptchaType
-       ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
-       ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
-       ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
-       ctx.Data["ShowRegistrationButton"] = false
-
-       // use this to set the right link into the signIn and signUp templates in the link_account template
-       ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
-       ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
-
-       gothUserInterface := ctx.Session.Get("linkAccountGothUser")
-       if gothUserInterface == nil {
-               ctx.ServerError("UserSignUp", errors.New("not in LinkAccount session"))
-               return
-       }
-       gothUser, ok := gothUserInterface.(goth.User)
-       if !ok {
-               ctx.ServerError("UserSignUp", fmt.Errorf("session linkAccountGothUser type is %t but not goth.User", gothUserInterface))
-               return
-       }
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplLinkAccount)
-               return
-       }
-
-       if setting.Service.DisableRegistration || setting.Service.AllowOnlyInternalRegistration {
-               ctx.Error(http.StatusForbidden)
-               return
-       }
-
-       if setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha {
-               var valid bool
-               var err error
-               switch setting.Service.CaptchaType {
-               case setting.ImageCaptcha:
-                       valid = context.GetImageCaptcha().VerifyReq(ctx.Req)
-               case setting.ReCaptcha:
-                       valid, err = recaptcha.Verify(ctx, form.GRecaptchaResponse)
-               case setting.HCaptcha:
-                       valid, err = hcaptcha.Verify(ctx, form.HcaptchaResponse)
-               default:
-                       ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
-                       return
-               }
-               if err != nil {
-                       log.Debug("%s", err.Error())
-               }
-
-               if !valid {
-                       ctx.Data["Err_Captcha"] = true
-                       ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplLinkAccount, &form)
-                       return
-               }
-       }
-
-       if !form.IsEmailDomainAllowed() {
-               ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplLinkAccount, &form)
-               return
-       }
-
-       if setting.Service.AllowOnlyExternalRegistration || !setting.Service.RequireExternalRegistrationPassword {
-               // In models.User an empty password is classed as not set, so we set form.Password to empty.
-               // Eventually the database should be changed to indicate "Second Factor"-enabled accounts
-               // (accounts that do not introduce the security vulnerabilities of a password).
-               // If a user decides to circumvent second-factor security, and purposefully create a password,
-               // they can still do so using the "Recover Account" option.
-               form.Password = ""
-       } else {
-               if (len(strings.TrimSpace(form.Password)) > 0 || len(strings.TrimSpace(form.Retype)) > 0) && form.Password != form.Retype {
-                       ctx.Data["Err_Password"] = true
-                       ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplLinkAccount, &form)
-                       return
-               }
-               if len(strings.TrimSpace(form.Password)) > 0 && len(form.Password) < setting.MinPasswordLength {
-                       ctx.Data["Err_Password"] = true
-                       ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplLinkAccount, &form)
-                       return
-               }
-       }
-
-       loginSource, err := models.GetActiveOAuth2LoginSourceByName(gothUser.Provider)
-       if err != nil {
-               ctx.ServerError("CreateUser", err)
-       }
-
-       u := &models.User{
-               Name:        form.UserName,
-               Email:       form.Email,
-               Passwd:      form.Password,
-               IsActive:    !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm),
-               LoginType:   models.LoginOAuth2,
-               LoginSource: loginSource.ID,
-               LoginName:   gothUser.UserID,
-       }
-
-       if !createAndHandleCreatedUser(ctx, tplLinkAccount, form, u, &gothUser, false) {
-               // error already handled
-               return
-       }
-
-       ctx.Redirect(setting.AppSubURL + "/user/login")
-}
-
-// HandleSignOut resets the session and sets the cookies
-func HandleSignOut(ctx *context.Context) {
-       _ = ctx.Session.Flush()
-       _ = ctx.Session.Destroy(ctx.Resp, ctx.Req)
-       ctx.DeleteCookie(setting.CookieUserName)
-       ctx.DeleteCookie(setting.CookieRememberName)
-       middleware.DeleteCSRFCookie(ctx.Resp)
-       middleware.DeleteLocaleCookie(ctx.Resp)
-       middleware.DeleteRedirectToCookie(ctx.Resp)
-}
-
-// SignOut sign out from login status
-func SignOut(ctx *context.Context) {
-       if ctx.User != nil {
-               eventsource.GetManager().SendMessageBlocking(ctx.User.ID, &eventsource.Event{
-                       Name: "logout",
-                       Data: ctx.Session.ID(),
-               })
-       }
-       HandleSignOut(ctx)
-       ctx.Redirect(setting.AppSubURL + "/")
-}
-
-// SignUp render the register page
-func SignUp(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("sign_up")
-
-       ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
-
-       ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
-       ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
-       ctx.Data["Captcha"] = context.GetImageCaptcha()
-       ctx.Data["CaptchaType"] = setting.Service.CaptchaType
-       ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
-       ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
-       ctx.Data["PageIsSignUp"] = true
-
-       //Show Disabled Registration message if DisableRegistration or AllowOnlyExternalRegistration options are true
-       ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration || setting.Service.AllowOnlyExternalRegistration
-
-       ctx.HTML(http.StatusOK, tplSignUp)
-}
-
-// SignUpPost response for sign up information submission
-func SignUpPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.RegisterForm)
-       ctx.Data["Title"] = ctx.Tr("sign_up")
-
-       ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
-
-       ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
-       ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
-       ctx.Data["Captcha"] = context.GetImageCaptcha()
-       ctx.Data["CaptchaType"] = setting.Service.CaptchaType
-       ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
-       ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
-       ctx.Data["PageIsSignUp"] = true
-
-       //Permission denied if DisableRegistration or AllowOnlyExternalRegistration options are true
-       if setting.Service.DisableRegistration || setting.Service.AllowOnlyExternalRegistration {
-               ctx.Error(http.StatusForbidden)
-               return
-       }
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplSignUp)
-               return
-       }
-
-       if setting.Service.EnableCaptcha {
-               var valid bool
-               var err error
-               switch setting.Service.CaptchaType {
-               case setting.ImageCaptcha:
-                       valid = context.GetImageCaptcha().VerifyReq(ctx.Req)
-               case setting.ReCaptcha:
-                       valid, err = recaptcha.Verify(ctx, form.GRecaptchaResponse)
-               case setting.HCaptcha:
-                       valid, err = hcaptcha.Verify(ctx, form.HcaptchaResponse)
-               default:
-                       ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
-                       return
-               }
-               if err != nil {
-                       log.Debug("%s", err.Error())
-               }
-
-               if !valid {
-                       ctx.Data["Err_Captcha"] = true
-                       ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplSignUp, &form)
-                       return
-               }
-       }
-
-       if !form.IsEmailDomainAllowed() {
-               ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplSignUp, &form)
-               return
-       }
-
-       if form.Password != form.Retype {
-               ctx.Data["Err_Password"] = true
-               ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplSignUp, &form)
-               return
-       }
-       if len(form.Password) < setting.MinPasswordLength {
-               ctx.Data["Err_Password"] = true
-               ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplSignUp, &form)
-               return
-       }
-       if !password.IsComplexEnough(form.Password) {
-               ctx.Data["Err_Password"] = true
-               ctx.RenderWithErr(password.BuildComplexityError(ctx), tplSignUp, &form)
-               return
-       }
-       pwned, err := password.IsPwned(ctx, form.Password)
-       if pwned {
-               errMsg := ctx.Tr("auth.password_pwned")
-               if err != nil {
-                       log.Error(err.Error())
-                       errMsg = ctx.Tr("auth.password_pwned_err")
-               }
-               ctx.Data["Err_Password"] = true
-               ctx.RenderWithErr(errMsg, tplSignUp, &form)
-               return
-       }
-
-       u := &models.User{
-               Name:     form.UserName,
-               Email:    form.Email,
-               Passwd:   form.Password,
-               IsActive: !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm),
-       }
-
-       if !createAndHandleCreatedUser(ctx, tplSignUp, form, u, nil, false) {
-               // error already handled
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("auth.sign_up_successful"))
-       handleSignInFull(ctx, u, false, true)
-}
-
-// createAndHandleCreatedUser calls createUserInContext and
-// then handleUserCreated.
-func createAndHandleCreatedUser(ctx *context.Context, tpl base.TplName, form interface{}, u *models.User, gothUser *goth.User, allowLink bool) bool {
-       if !createUserInContext(ctx, tpl, form, u, gothUser, allowLink) {
-               return false
-       }
-       return handleUserCreated(ctx, u, gothUser)
-}
-
-// createUserInContext creates a user and handles errors within a given context.
-// Optionally a template can be specified.
-func createUserInContext(ctx *context.Context, tpl base.TplName, form interface{}, u *models.User, gothUser *goth.User, allowLink bool) (ok bool) {
-       if err := models.CreateUser(u); err != nil {
-               if allowLink && (models.IsErrUserAlreadyExist(err) || models.IsErrEmailAlreadyUsed(err)) {
-                       if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingAuto {
-                               var user *models.User
-                               user = &models.User{Name: u.Name}
-                               hasUser, err := models.GetUser(user)
-                               if !hasUser || err != nil {
-                                       user = &models.User{Email: u.Email}
-                                       hasUser, err = models.GetUser(user)
-                                       if !hasUser || err != nil {
-                                               ctx.ServerError("UserLinkAccount", err)
-                                               return
-                                       }
-                               }
-
-                               // TODO: probably we should respect 'remember' user's choice...
-                               linkAccount(ctx, user, *gothUser, true)
-                               return // user is already created here, all redirects are handled
-                       } else if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingLogin {
-                               showLinkingLogin(ctx, *gothUser)
-                               return // user will be created only after linking login
-                       }
-               }
-
-               // handle error without template
-               if len(tpl) == 0 {
-                       ctx.ServerError("CreateUser", err)
-                       return
-               }
-
-               // handle error with template
-               switch {
-               case models.IsErrUserAlreadyExist(err):
-                       ctx.Data["Err_UserName"] = true
-                       ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tpl, form)
-               case models.IsErrEmailAlreadyUsed(err):
-                       ctx.Data["Err_Email"] = true
-                       ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tpl, form)
-               case models.IsErrEmailInvalid(err):
-                       ctx.Data["Err_Email"] = true
-                       ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form)
-               case models.IsErrNameReserved(err):
-                       ctx.Data["Err_UserName"] = true
-                       ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(models.ErrNameReserved).Name), tpl, form)
-               case models.IsErrNamePatternNotAllowed(err):
-                       ctx.Data["Err_UserName"] = true
-                       ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tpl, form)
-               case models.IsErrNameCharsNotAllowed(err):
-                       ctx.Data["Err_UserName"] = true
-                       ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(models.ErrNameCharsNotAllowed).Name), tpl, form)
-               default:
-                       ctx.ServerError("CreateUser", err)
-               }
-               return
-       }
-       log.Trace("Account created: %s", u.Name)
-       return true
-}
-
-// handleUserCreated does additional steps after a new user is created.
-// It auto-sets admin for the only user, updates the optional external user and
-// sends a confirmation email if required.
-func handleUserCreated(ctx *context.Context, u *models.User, gothUser *goth.User) (ok bool) {
-       // Auto-set admin for the only user.
-       if models.CountUsers() == 1 {
-               u.IsAdmin = true
-               u.IsActive = true
-               u.SetLastLogin()
-               if err := models.UpdateUserCols(u, "is_admin", "is_active", "last_login_unix"); err != nil {
-                       ctx.ServerError("UpdateUser", err)
-                       return
-               }
-       }
-
-       // update external user information
-       if gothUser != nil {
-               if err := models.UpdateExternalUser(u, *gothUser); err != nil {
-                       log.Error("UpdateExternalUser failed: %v", err)
-               }
-       }
-
-       // Send confirmation email
-       if !u.IsActive && u.ID > 1 {
-               mailer.SendActivateAccountMail(ctx.Locale, u)
-
-               ctx.Data["IsSendRegisterMail"] = true
-               ctx.Data["Email"] = u.Email
-               ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())
-               ctx.HTML(http.StatusOK, TplActivate)
-
-               if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
-                       log.Error("Set cache(MailResendLimit) fail: %v", err)
-               }
-               return
-       }
-
-       return true
-}
-
-// Activate render activate user page
-func Activate(ctx *context.Context) {
-       code := ctx.Query("code")
-
-       if len(code) == 0 {
-               ctx.Data["IsActivatePage"] = true
-               if ctx.User == nil || ctx.User.IsActive {
-                       ctx.NotFound("invalid user", nil)
-                       return
-               }
-               // Resend confirmation email.
-               if setting.Service.RegisterEmailConfirm {
-                       if ctx.Cache.IsExist("MailResendLimit_" + ctx.User.LowerName) {
-                               ctx.Data["ResendLimited"] = true
-                       } else {
-                               ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())
-                               mailer.SendActivateAccountMail(ctx.Locale, ctx.User)
-
-                               if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
-                                       log.Error("Set cache(MailResendLimit) fail: %v", err)
-                               }
-                       }
-               } else {
-                       ctx.Data["ServiceNotEnabled"] = true
-               }
-               ctx.HTML(http.StatusOK, TplActivate)
-               return
-       }
-
-       user := models.VerifyUserActiveCode(code)
-       // if code is wrong
-       if user == nil {
-               ctx.Data["IsActivateFailed"] = true
-               ctx.HTML(http.StatusOK, TplActivate)
-               return
-       }
-
-       // if account is local account, verify password
-       if user.LoginSource == 0 {
-               ctx.Data["Code"] = code
-               ctx.Data["NeedsPassword"] = true
-               ctx.HTML(http.StatusOK, TplActivate)
-               return
-       }
-
-       handleAccountActivation(ctx, user)
-}
-
-// ActivatePost handles account activation with password check
-func ActivatePost(ctx *context.Context) {
-       code := ctx.Query("code")
-       if len(code) == 0 {
-               ctx.Redirect(setting.AppSubURL + "/user/activate")
-               return
-       }
-
-       user := models.VerifyUserActiveCode(code)
-       // if code is wrong
-       if user == nil {
-               ctx.Data["IsActivateFailed"] = true
-               ctx.HTML(http.StatusOK, TplActivate)
-               return
-       }
-
-       // if account is local account, verify password
-       if user.LoginSource == 0 {
-               password := ctx.Query("password")
-               if len(password) == 0 {
-                       ctx.Data["Code"] = code
-                       ctx.Data["NeedsPassword"] = true
-                       ctx.HTML(http.StatusOK, TplActivate)
-                       return
-               }
-               if !user.ValidatePassword(password) {
-                       ctx.Data["IsActivateFailed"] = true
-                       ctx.HTML(http.StatusOK, TplActivate)
-                       return
-               }
-       }
-
-       handleAccountActivation(ctx, user)
-}
-
-func handleAccountActivation(ctx *context.Context, user *models.User) {
-       user.IsActive = true
-       var err error
-       if user.Rands, err = models.GetUserSalt(); err != nil {
-               ctx.ServerError("UpdateUser", err)
-               return
-       }
-       if err := models.UpdateUserCols(user, "is_active", "rands"); err != nil {
-               if models.IsErrUserNotExist(err) {
-                       ctx.NotFound("UpdateUserCols", err)
-               } else {
-                       ctx.ServerError("UpdateUser", err)
-               }
-               return
-       }
-
-       log.Trace("User activated: %s", user.Name)
-
-       if err := ctx.Session.Set("uid", user.ID); err != nil {
-               log.Error(fmt.Sprintf("Error setting uid in session: %v", err))
-       }
-       if err := ctx.Session.Set("uname", user.Name); err != nil {
-               log.Error(fmt.Sprintf("Error setting uname in session: %v", err))
-       }
-       if err := ctx.Session.Release(); err != nil {
-               log.Error("Error storing session: %v", err)
-       }
-
-       ctx.Flash.Success(ctx.Tr("auth.account_activated"))
-       ctx.Redirect(setting.AppSubURL + "/")
-}
-
-// ActivateEmail render the activate email page
-func ActivateEmail(ctx *context.Context) {
-       code := ctx.Query("code")
-       emailStr := ctx.Query("email")
-
-       // Verify code.
-       if email := models.VerifyActiveEmailCode(code, emailStr); email != nil {
-               if err := email.Activate(); err != nil {
-                       ctx.ServerError("ActivateEmail", err)
-               }
-
-               log.Trace("Email activated: %s", email.Email)
-               ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
-
-               if u, err := models.GetUserByID(email.UID); err != nil {
-                       log.Warn("GetUserByID: %d", email.UID)
-               } else {
-                       // Allow user to validate more emails
-                       _ = ctx.Cache.Delete("MailResendLimit_" + u.LowerName)
-               }
-       }
-
-       // FIXME: e-mail verification does not require the user to be logged in,
-       // so this could be redirecting to the login page.
-       // Should users be logged in automatically here? (consider 2FA requirements, etc.)
-       ctx.Redirect(setting.AppSubURL + "/user/settings/account")
-}
-
-// ForgotPasswd render the forget pasword page
-func ForgotPasswd(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title")
-
-       if setting.MailService == nil {
-               ctx.Data["IsResetDisable"] = true
-               ctx.HTML(http.StatusOK, tplForgotPassword)
-               return
-       }
-
-       email := ctx.Query("email")
-       ctx.Data["Email"] = email
-
-       ctx.Data["IsResetRequest"] = true
-       ctx.HTML(http.StatusOK, tplForgotPassword)
-}
-
-// ForgotPasswdPost response for forget password request
-func ForgotPasswdPost(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title")
-
-       if setting.MailService == nil {
-               ctx.NotFound("ForgotPasswdPost", nil)
-               return
-       }
-       ctx.Data["IsResetRequest"] = true
-
-       email := ctx.Query("email")
-       ctx.Data["Email"] = email
-
-       u, err := models.GetUserByEmail(email)
-       if err != nil {
-               if models.IsErrUserNotExist(err) {
-                       ctx.Data["ResetPwdCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, ctx.Locale.Language())
-                       ctx.Data["IsResetSent"] = true
-                       ctx.HTML(http.StatusOK, tplForgotPassword)
-                       return
-               }
-
-               ctx.ServerError("user.ResetPasswd(check existence)", err)
-               return
-       }
-
-       if !u.IsLocal() && !u.IsOAuth2() {
-               ctx.Data["Err_Email"] = true
-               ctx.RenderWithErr(ctx.Tr("auth.non_local_account"), tplForgotPassword, nil)
-               return
-       }
-
-       if ctx.Cache.IsExist("MailResendLimit_" + u.LowerName) {
-               ctx.Data["ResendLimited"] = true
-               ctx.HTML(http.StatusOK, tplForgotPassword)
-               return
-       }
-
-       mailer.SendResetPasswordMail(u)
-
-       if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
-               log.Error("Set cache(MailResendLimit) fail: %v", err)
-       }
-
-       ctx.Data["ResetPwdCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, ctx.Locale.Language())
-       ctx.Data["IsResetSent"] = true
-       ctx.HTML(http.StatusOK, tplForgotPassword)
-}
-
-func commonResetPassword(ctx *context.Context) (*models.User, *models.TwoFactor) {
-       code := ctx.Query("code")
-
-       ctx.Data["Title"] = ctx.Tr("auth.reset_password")
-       ctx.Data["Code"] = code
-
-       if nil != ctx.User {
-               ctx.Data["user_signed_in"] = true
-       }
-
-       if len(code) == 0 {
-               ctx.Flash.Error(ctx.Tr("auth.invalid_code"))
-               return nil, nil
-       }
-
-       // Fail early, don't frustrate the user
-       u := models.VerifyUserActiveCode(code)
-       if u == nil {
-               ctx.Flash.Error(ctx.Tr("auth.invalid_code"))
-               return nil, nil
-       }
-
-       twofa, err := models.GetTwoFactorByUID(u.ID)
-       if err != nil {
-               if !models.IsErrTwoFactorNotEnrolled(err) {
-                       ctx.Error(http.StatusInternalServerError, "CommonResetPassword", err.Error())
-                       return nil, nil
-               }
-       } else {
-               ctx.Data["has_two_factor"] = true
-               ctx.Data["scratch_code"] = ctx.QueryBool("scratch_code")
-       }
-
-       // Show the user that they are affecting the account that they intended to
-       ctx.Data["user_email"] = u.Email
-
-       if nil != ctx.User && u.ID != ctx.User.ID {
-               ctx.Flash.Error(ctx.Tr("auth.reset_password_wrong_user", ctx.User.Email, u.Email))
-               return nil, nil
-       }
-
-       return u, twofa
-}
-
-// ResetPasswd render the account recovery page
-func ResetPasswd(ctx *context.Context) {
-       ctx.Data["IsResetForm"] = true
-
-       commonResetPassword(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       ctx.HTML(http.StatusOK, tplResetPassword)
-}
-
-// ResetPasswdPost response from account recovery request
-func ResetPasswdPost(ctx *context.Context) {
-       u, twofa := commonResetPassword(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       if u == nil {
-               // Flash error has been set
-               ctx.HTML(http.StatusOK, tplResetPassword)
-               return
-       }
-
-       // Validate password length.
-       passwd := ctx.Query("password")
-       if len(passwd) < setting.MinPasswordLength {
-               ctx.Data["IsResetForm"] = true
-               ctx.Data["Err_Password"] = true
-               ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil)
-               return
-       } else if !password.IsComplexEnough(passwd) {
-               ctx.Data["IsResetForm"] = true
-               ctx.Data["Err_Password"] = true
-               ctx.RenderWithErr(password.BuildComplexityError(ctx), tplResetPassword, nil)
-               return
-       } else if pwned, err := password.IsPwned(ctx, passwd); pwned || err != nil {
-               errMsg := ctx.Tr("auth.password_pwned")
-               if err != nil {
-                       log.Error(err.Error())
-                       errMsg = ctx.Tr("auth.password_pwned_err")
-               }
-               ctx.Data["IsResetForm"] = true
-               ctx.Data["Err_Password"] = true
-               ctx.RenderWithErr(errMsg, tplResetPassword, nil)
-               return
-       }
-
-       // Handle two-factor
-       regenerateScratchToken := false
-       if twofa != nil {
-               if ctx.QueryBool("scratch_code") {
-                       if !twofa.VerifyScratchToken(ctx.Query("token")) {
-                               ctx.Data["IsResetForm"] = true
-                               ctx.Data["Err_Token"] = true
-                               ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplResetPassword, nil)
-                               return
-                       }
-                       regenerateScratchToken = true
-               } else {
-                       passcode := ctx.Query("passcode")
-                       ok, err := twofa.ValidateTOTP(passcode)
-                       if err != nil {
-                               ctx.Error(http.StatusInternalServerError, "ValidateTOTP", err.Error())
-                               return
-                       }
-                       if !ok || twofa.LastUsedPasscode == passcode {
-                               ctx.Data["IsResetForm"] = true
-                               ctx.Data["Err_Passcode"] = true
-                               ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplResetPassword, nil)
-                               return
-                       }
-
-                       twofa.LastUsedPasscode = passcode
-                       if err = models.UpdateTwoFactor(twofa); err != nil {
-                               ctx.ServerError("ResetPasswdPost: UpdateTwoFactor", err)
-                               return
-                       }
-               }
-       }
-       var err error
-       if u.Rands, err = models.GetUserSalt(); err != nil {
-               ctx.ServerError("UpdateUser", err)
-               return
-       }
-       if err = u.SetPassword(passwd); err != nil {
-               ctx.ServerError("UpdateUser", err)
-               return
-       }
-       u.MustChangePassword = false
-       if err := models.UpdateUserCols(u, "must_change_password", "passwd", "passwd_hash_algo", "rands", "salt"); err != nil {
-               ctx.ServerError("UpdateUser", err)
-               return
-       }
-
-       log.Trace("User password reset: %s", u.Name)
-       ctx.Data["IsResetFailed"] = true
-       remember := len(ctx.Query("remember")) != 0
-
-       if regenerateScratchToken {
-               // Invalidate the scratch token.
-               _, err = twofa.GenerateScratchToken()
-               if err != nil {
-                       ctx.ServerError("UserSignIn", err)
-                       return
-               }
-               if err = models.UpdateTwoFactor(twofa); err != nil {
-                       ctx.ServerError("UserSignIn", err)
-                       return
-               }
-
-               handleSignInFull(ctx, u, remember, false)
-               ctx.Flash.Info(ctx.Tr("auth.twofa_scratch_used"))
-               ctx.Redirect(setting.AppSubURL + "/user/settings/security")
-               return
-       }
-
-       handleSignInFull(ctx, u, remember, true)
-}
-
-// MustChangePassword renders the page to change a user's password
-func MustChangePassword(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
-       ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/settings/change_password"
-       ctx.Data["MustChangePassword"] = true
-       ctx.HTML(http.StatusOK, tplMustChangePassword)
-}
-
-// MustChangePasswordPost response for updating a user's password after his/her
-// account was created by an admin
-func MustChangePasswordPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.MustChangePasswordForm)
-       ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
-       ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/settings/change_password"
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplMustChangePassword)
-               return
-       }
-       u := ctx.User
-       // Make sure only requests for users who are eligible to change their password via
-       // this method passes through
-       if !u.MustChangePassword {
-               ctx.ServerError("MustUpdatePassword", errors.New("cannot update password.. Please visit the settings page"))
-               return
-       }
-
-       if form.Password != form.Retype {
-               ctx.Data["Err_Password"] = true
-               ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplMustChangePassword, &form)
-               return
-       }
-
-       if len(form.Password) < setting.MinPasswordLength {
-               ctx.Data["Err_Password"] = true
-               ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form)
-               return
-       }
-
-       var err error
-       if err = u.SetPassword(form.Password); err != nil {
-               ctx.ServerError("UpdateUser", err)
-               return
-       }
-
-       u.MustChangePassword = false
-
-       if err := models.UpdateUserCols(u, "must_change_password", "passwd", "passwd_hash_algo", "salt"); err != nil {
-               ctx.ServerError("UpdateUser", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("settings.change_password_success"))
-
-       log.Trace("User updated password: %s", u.Name)
-
-       if redirectTo := ctx.GetCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
-               middleware.DeleteRedirectToCookie(ctx.Resp)
-               ctx.RedirectToFirst(redirectTo)
-               return
-       }
-
-       ctx.Redirect(setting.AppSubURL + "/")
-}
diff --git a/routers/user/auth_openid.go b/routers/user/auth_openid.go
deleted file mode 100644 (file)
index 1a73a08..0000000
+++ /dev/null
@@ -1,450 +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 user
-
-import (
-       "fmt"
-       "net/http"
-       "net/url"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/auth/openid"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/hcaptcha"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/recaptcha"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/util"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/modules/web/middleware"
-       "code.gitea.io/gitea/services/forms"
-)
-
-const (
-       tplSignInOpenID base.TplName = "user/auth/signin_openid"
-       tplConnectOID   base.TplName = "user/auth/signup_openid_connect"
-       tplSignUpOID    base.TplName = "user/auth/signup_openid_register"
-)
-
-// SignInOpenID render sign in page
-func SignInOpenID(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("sign_in")
-
-       if ctx.Query("openid.return_to") != "" {
-               signInOpenIDVerify(ctx)
-               return
-       }
-
-       // Check auto-login.
-       isSucceed, err := AutoSignIn(ctx)
-       if err != nil {
-               ctx.ServerError("AutoSignIn", err)
-               return
-       }
-
-       redirectTo := ctx.Query("redirect_to")
-       if len(redirectTo) > 0 {
-               middleware.SetRedirectToCookie(ctx.Resp, redirectTo)
-       } else {
-               redirectTo = ctx.GetCookie("redirect_to")
-       }
-
-       if isSucceed {
-               middleware.DeleteRedirectToCookie(ctx.Resp)
-               ctx.RedirectToFirst(redirectTo)
-               return
-       }
-
-       ctx.Data["PageIsSignIn"] = true
-       ctx.Data["PageIsLoginOpenID"] = true
-       ctx.HTML(http.StatusOK, tplSignInOpenID)
-}
-
-// Check if the given OpenID URI is allowed by blacklist/whitelist
-func allowedOpenIDURI(uri string) (err error) {
-
-       // In case a Whitelist is present, URI must be in it
-       // in order to be accepted
-       if len(setting.Service.OpenIDWhitelist) != 0 {
-               for _, pat := range setting.Service.OpenIDWhitelist {
-                       if pat.MatchString(uri) {
-                               return nil // pass
-                       }
-               }
-               // must match one of this or be refused
-               return fmt.Errorf("URI not allowed by whitelist")
-       }
-
-       // A blacklist match expliclty forbids
-       for _, pat := range setting.Service.OpenIDBlacklist {
-               if pat.MatchString(uri) {
-                       return fmt.Errorf("URI forbidden by blacklist")
-               }
-       }
-
-       return nil
-}
-
-// SignInOpenIDPost response for openid sign in request
-func SignInOpenIDPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.SignInOpenIDForm)
-       ctx.Data["Title"] = ctx.Tr("sign_in")
-       ctx.Data["PageIsSignIn"] = true
-       ctx.Data["PageIsLoginOpenID"] = true
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplSignInOpenID)
-               return
-       }
-
-       id, err := openid.Normalize(form.Openid)
-       if err != nil {
-               ctx.RenderWithErr(err.Error(), tplSignInOpenID, &form)
-               return
-       }
-       form.Openid = id
-
-       log.Trace("OpenID uri: " + id)
-
-       err = allowedOpenIDURI(id)
-       if err != nil {
-               ctx.RenderWithErr(err.Error(), tplSignInOpenID, &form)
-               return
-       }
-
-       redirectTo := setting.AppURL + "user/login/openid"
-       url, err := openid.RedirectURL(id, redirectTo, setting.AppURL)
-       if err != nil {
-               log.Error("Error in OpenID redirect URL: %s, %v", redirectTo, err.Error())
-               ctx.RenderWithErr(fmt.Sprintf("Unable to find OpenID provider in %s", redirectTo), tplSignInOpenID, &form)
-               return
-       }
-
-       // Request optional nickname and email info
-       // NOTE: change to `openid.sreg.required` to require it
-       url += "&openid.ns.sreg=http%3A%2F%2Fopenid.net%2Fextensions%2Fsreg%2F1.1"
-       url += "&openid.sreg.optional=nickname%2Cemail"
-
-       log.Trace("Form-passed openid-remember: %t", form.Remember)
-
-       if err := ctx.Session.Set("openid_signin_remember", form.Remember); err != nil {
-               log.Error("SignInOpenIDPost: Could not set openid_signin_remember in session: %v", err)
-       }
-       if err := ctx.Session.Release(); err != nil {
-               log.Error("SignInOpenIDPost: Unable to save changes to the session: %v", err)
-       }
-
-       ctx.Redirect(url)
-}
-
-// signInOpenIDVerify handles response from OpenID provider
-func signInOpenIDVerify(ctx *context.Context) {
-
-       log.Trace("Incoming call to: " + ctx.Req.URL.String())
-
-       fullURL := setting.AppURL + ctx.Req.URL.String()[1:]
-       log.Trace("Full URL: " + fullURL)
-
-       var id, err = openid.Verify(fullURL)
-       if err != nil {
-               ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
-                       Openid: id,
-               })
-               return
-       }
-
-       log.Trace("Verified ID: " + id)
-
-       /* Now we should seek for the user and log him in, or prompt
-        * to register if not found */
-
-       u, err := models.GetUserByOpenID(id)
-       if err != nil {
-               if !models.IsErrUserNotExist(err) {
-                       ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
-                               Openid: id,
-                       })
-                       return
-               }
-               log.Error("signInOpenIDVerify: %v", err)
-       }
-       if u != nil {
-               log.Trace("User exists, logging in")
-               remember, _ := ctx.Session.Get("openid_signin_remember").(bool)
-               log.Trace("Session stored openid-remember: %t", remember)
-               handleSignIn(ctx, u, remember)
-               return
-       }
-
-       log.Trace("User with openid " + id + " does not exist, should connect or register")
-
-       parsedURL, err := url.Parse(fullURL)
-       if err != nil {
-               ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
-                       Openid: id,
-               })
-               return
-       }
-       values, err := url.ParseQuery(parsedURL.RawQuery)
-       if err != nil {
-               ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
-                       Openid: id,
-               })
-               return
-       }
-       email := values.Get("openid.sreg.email")
-       nickname := values.Get("openid.sreg.nickname")
-
-       log.Trace("User has email=" + email + " and nickname=" + nickname)
-
-       if email != "" {
-               u, err = models.GetUserByEmail(email)
-               if err != nil {
-                       if !models.IsErrUserNotExist(err) {
-                               ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
-                                       Openid: id,
-                               })
-                               return
-                       }
-                       log.Error("signInOpenIDVerify: %v", err)
-               }
-               if u != nil {
-                       log.Trace("Local user " + u.LowerName + " has OpenID provided email " + email)
-               }
-       }
-
-       if u == nil && nickname != "" {
-               u, _ = models.GetUserByName(nickname)
-               if err != nil {
-                       if !models.IsErrUserNotExist(err) {
-                               ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
-                                       Openid: id,
-                               })
-                               return
-                       }
-               }
-               if u != nil {
-                       log.Trace("Local user " + u.LowerName + " has OpenID provided nickname " + nickname)
-               }
-       }
-
-       if err := ctx.Session.Set("openid_verified_uri", id); err != nil {
-               log.Error("signInOpenIDVerify: Could not set openid_verified_uri in session: %v", err)
-       }
-       if err := ctx.Session.Set("openid_determined_email", email); err != nil {
-               log.Error("signInOpenIDVerify: Could not set openid_determined_email in session: %v", err)
-       }
-
-       if u != nil {
-               nickname = u.LowerName
-       }
-
-       if err := ctx.Session.Set("openid_determined_username", nickname); err != nil {
-               log.Error("signInOpenIDVerify: Could not set openid_determined_username in session: %v", err)
-       }
-       if err := ctx.Session.Release(); err != nil {
-               log.Error("signInOpenIDVerify: Unable to save changes to the session: %v", err)
-       }
-
-       if u != nil || !setting.Service.EnableOpenIDSignUp || setting.Service.AllowOnlyInternalRegistration {
-               ctx.Redirect(setting.AppSubURL + "/user/openid/connect")
-       } else {
-               ctx.Redirect(setting.AppSubURL + "/user/openid/register")
-       }
-}
-
-// ConnectOpenID shows a form to connect an OpenID URI to an existing account
-func ConnectOpenID(ctx *context.Context) {
-       oid, _ := ctx.Session.Get("openid_verified_uri").(string)
-       if oid == "" {
-               ctx.Redirect(setting.AppSubURL + "/user/login/openid")
-               return
-       }
-       ctx.Data["Title"] = "OpenID connect"
-       ctx.Data["PageIsSignIn"] = true
-       ctx.Data["PageIsOpenIDConnect"] = true
-       ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
-       ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
-       ctx.Data["OpenID"] = oid
-       userName, _ := ctx.Session.Get("openid_determined_username").(string)
-       if userName != "" {
-               ctx.Data["user_name"] = userName
-       }
-       ctx.HTML(http.StatusOK, tplConnectOID)
-}
-
-// ConnectOpenIDPost handles submission of a form to connect an OpenID URI to an existing account
-func ConnectOpenIDPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.ConnectOpenIDForm)
-       oid, _ := ctx.Session.Get("openid_verified_uri").(string)
-       if oid == "" {
-               ctx.Redirect(setting.AppSubURL + "/user/login/openid")
-               return
-       }
-       ctx.Data["Title"] = "OpenID connect"
-       ctx.Data["PageIsSignIn"] = true
-       ctx.Data["PageIsOpenIDConnect"] = true
-       ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
-       ctx.Data["OpenID"] = oid
-
-       u, err := models.UserSignIn(form.UserName, form.Password)
-       if err != nil {
-               if models.IsErrUserNotExist(err) {
-                       ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplConnectOID, &form)
-               } else {
-                       ctx.ServerError("ConnectOpenIDPost", err)
-               }
-               return
-       }
-
-       // add OpenID for the user
-       userOID := &models.UserOpenID{UID: u.ID, URI: oid}
-       if err = models.AddUserOpenID(userOID); err != nil {
-               if models.IsErrOpenIDAlreadyUsed(err) {
-                       ctx.RenderWithErr(ctx.Tr("form.openid_been_used", oid), tplConnectOID, &form)
-                       return
-               }
-               ctx.ServerError("AddUserOpenID", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("settings.add_openid_success"))
-
-       remember, _ := ctx.Session.Get("openid_signin_remember").(bool)
-       log.Trace("Session stored openid-remember: %t", remember)
-       handleSignIn(ctx, u, remember)
-}
-
-// RegisterOpenID shows a form to create a new user authenticated via an OpenID URI
-func RegisterOpenID(ctx *context.Context) {
-       oid, _ := ctx.Session.Get("openid_verified_uri").(string)
-       if oid == "" {
-               ctx.Redirect(setting.AppSubURL + "/user/login/openid")
-               return
-       }
-       ctx.Data["Title"] = "OpenID signup"
-       ctx.Data["PageIsSignIn"] = true
-       ctx.Data["PageIsOpenIDRegister"] = true
-       ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
-       ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
-       ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
-       ctx.Data["Captcha"] = context.GetImageCaptcha()
-       ctx.Data["CaptchaType"] = setting.Service.CaptchaType
-       ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
-       ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
-       ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
-       ctx.Data["OpenID"] = oid
-       userName, _ := ctx.Session.Get("openid_determined_username").(string)
-       if userName != "" {
-               ctx.Data["user_name"] = userName
-       }
-       email, _ := ctx.Session.Get("openid_determined_email").(string)
-       if email != "" {
-               ctx.Data["email"] = email
-       }
-       ctx.HTML(http.StatusOK, tplSignUpOID)
-}
-
-// RegisterOpenIDPost handles submission of a form to create a new user authenticated via an OpenID URI
-func RegisterOpenIDPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.SignUpOpenIDForm)
-       oid, _ := ctx.Session.Get("openid_verified_uri").(string)
-       if oid == "" {
-               ctx.Redirect(setting.AppSubURL + "/user/login/openid")
-               return
-       }
-
-       ctx.Data["Title"] = "OpenID signup"
-       ctx.Data["PageIsSignIn"] = true
-       ctx.Data["PageIsOpenIDRegister"] = true
-       ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
-       ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
-       ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
-       ctx.Data["Captcha"] = context.GetImageCaptcha()
-       ctx.Data["CaptchaType"] = setting.Service.CaptchaType
-       ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
-       ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
-       ctx.Data["OpenID"] = oid
-
-       if setting.Service.AllowOnlyInternalRegistration {
-               ctx.Error(http.StatusForbidden)
-               return
-       }
-
-       if setting.Service.EnableCaptcha {
-               var valid bool
-               var err error
-               switch setting.Service.CaptchaType {
-               case setting.ImageCaptcha:
-                       valid = context.GetImageCaptcha().VerifyReq(ctx.Req)
-               case setting.ReCaptcha:
-                       if err := ctx.Req.ParseForm(); err != nil {
-                               ctx.ServerError("", err)
-                               return
-                       }
-                       valid, err = recaptcha.Verify(ctx, form.GRecaptchaResponse)
-               case setting.HCaptcha:
-                       if err := ctx.Req.ParseForm(); err != nil {
-                               ctx.ServerError("", err)
-                               return
-                       }
-                       valid, err = hcaptcha.Verify(ctx, form.HcaptchaResponse)
-               default:
-                       ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
-                       return
-               }
-               if err != nil {
-                       log.Debug("%s", err.Error())
-               }
-
-               if !valid {
-                       ctx.Data["Err_Captcha"] = true
-                       ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplSignUpOID, &form)
-                       return
-               }
-       }
-
-       length := setting.MinPasswordLength
-       if length < 256 {
-               length = 256
-       }
-       password, err := util.RandomString(int64(length))
-       if err != nil {
-               ctx.RenderWithErr(err.Error(), tplSignUpOID, form)
-               return
-       }
-
-       u := &models.User{
-               Name:     form.UserName,
-               Email:    form.Email,
-               Passwd:   password,
-               IsActive: !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm),
-       }
-       if !createUserInContext(ctx, tplSignUpOID, form, u, nil, false) {
-               // error already handled
-               return
-       }
-
-       // add OpenID for the user
-       userOID := &models.UserOpenID{UID: u.ID, URI: oid}
-       if err = models.AddUserOpenID(userOID); err != nil {
-               if models.IsErrOpenIDAlreadyUsed(err) {
-                       ctx.RenderWithErr(ctx.Tr("form.openid_been_used", oid), tplSignUpOID, &form)
-                       return
-               }
-               ctx.ServerError("AddUserOpenID", err)
-               return
-       }
-
-       if !handleUserCreated(ctx, u, nil) {
-               // error already handled
-               return
-       }
-
-       remember, _ := ctx.Session.Get("openid_signin_remember").(bool)
-       log.Trace("Session stored openid-remember: %t", remember)
-       handleSignIn(ctx, u, remember)
-}
diff --git a/routers/user/avatar.go b/routers/user/avatar.go
deleted file mode 100644 (file)
index 4287589..0000000
+++ /dev/null
@@ -1,98 +0,0 @@
-// Copyright 2019 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 user
-
-import (
-       "errors"
-       "net/url"
-       "path"
-       "strconv"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-)
-
-// Avatar redirect browser to user avatar of requested size
-func Avatar(ctx *context.Context) {
-       userName := ctx.Params(":username")
-       size, err := strconv.Atoi(ctx.Params(":size"))
-       if err != nil {
-               ctx.ServerError("Invalid avatar size", err)
-               return
-       }
-
-       log.Debug("Asked avatar for user %v and size %v", userName, size)
-
-       var user *models.User
-       if strings.ToLower(userName) != "ghost" {
-               user, err = models.GetUserByName(userName)
-               if err != nil {
-                       if models.IsErrUserNotExist(err) {
-                               ctx.ServerError("Requested avatar for invalid user", err)
-                       } else {
-                               ctx.ServerError("Retrieving user by name", err)
-                       }
-                       return
-               }
-       } else {
-               user = models.NewGhostUser()
-       }
-
-       ctx.Redirect(user.RealSizedAvatarLink(size))
-}
-
-// AvatarByEmailHash redirects the browser to the appropriate Avatar link
-func AvatarByEmailHash(ctx *context.Context) {
-       var err error
-
-       hash := ctx.Params(":hash")
-       if len(hash) == 0 {
-               ctx.ServerError("invalid avatar hash", errors.New("hash cannot be empty"))
-               return
-       }
-
-       var email string
-       email, err = models.GetEmailForHash(hash)
-       if err != nil {
-               ctx.ServerError("invalid avatar hash", err)
-               return
-       }
-       if len(email) == 0 {
-               ctx.Redirect(models.DefaultAvatarLink())
-               return
-       }
-       size := ctx.QueryInt("size")
-       if size == 0 {
-               size = models.DefaultAvatarSize
-       }
-
-       var avatarURL *url.URL
-
-       if setting.EnableFederatedAvatar && setting.LibravatarService != nil {
-               avatarURL, err = models.LibravatarURL(email)
-               if err != nil {
-                       avatarURL, err = url.Parse(models.DefaultAvatarLink())
-                       if err != nil {
-                               ctx.ServerError("invalid default avatar url", err)
-                               return
-                       }
-               }
-       } else if !setting.DisableGravatar {
-               copyOfGravatarSourceURL := *setting.GravatarSourceURL
-               avatarURL = &copyOfGravatarSourceURL
-               avatarURL.Path = path.Join(avatarURL.Path, hash)
-       } else {
-               avatarURL, err = url.Parse(models.DefaultAvatarLink())
-               if err != nil {
-                       ctx.ServerError("invalid default avatar url", err)
-                       return
-               }
-       }
-
-       ctx.Redirect(models.MakeFinalAvatarURL(avatarURL, size))
-}
diff --git a/routers/user/home.go b/routers/user/home.go
deleted file mode 100644 (file)
index acf73f8..0000000
+++ /dev/null
@@ -1,913 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2019 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 user
-
-import (
-       "bytes"
-       "fmt"
-       "net/http"
-       "regexp"
-       "sort"
-       "strconv"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/markup"
-       "code.gitea.io/gitea/modules/markup/markdown"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/util"
-       issue_service "code.gitea.io/gitea/services/issue"
-       pull_service "code.gitea.io/gitea/services/pull"
-
-       jsoniter "github.com/json-iterator/go"
-       "github.com/keybase/go-crypto/openpgp"
-       "github.com/keybase/go-crypto/openpgp/armor"
-       "xorm.io/builder"
-)
-
-const (
-       tplDashboard  base.TplName = "user/dashboard/dashboard"
-       tplIssues     base.TplName = "user/dashboard/issues"
-       tplMilestones base.TplName = "user/dashboard/milestones"
-       tplProfile    base.TplName = "user/profile"
-)
-
-// getDashboardContextUser finds out which context user dashboard is being viewed as .
-func getDashboardContextUser(ctx *context.Context) *models.User {
-       ctxUser := ctx.User
-       orgName := ctx.Params(":org")
-       if len(orgName) > 0 {
-               ctxUser = ctx.Org.Organization
-               ctx.Data["Teams"] = ctx.Org.Organization.Teams
-       }
-       ctx.Data["ContextUser"] = ctxUser
-
-       if err := ctx.User.GetOrganizations(&models.SearchOrganizationsOptions{All: true}); err != nil {
-               ctx.ServerError("GetOrganizations", err)
-               return nil
-       }
-       ctx.Data["Orgs"] = ctx.User.Orgs
-
-       return ctxUser
-}
-
-// retrieveFeeds loads feeds for the specified user
-func retrieveFeeds(ctx *context.Context, options models.GetFeedsOptions) {
-       actions, err := models.GetFeeds(options)
-       if err != nil {
-               ctx.ServerError("GetFeeds", err)
-               return
-       }
-
-       userCache := map[int64]*models.User{options.RequestedUser.ID: options.RequestedUser}
-       if ctx.User != nil {
-               userCache[ctx.User.ID] = ctx.User
-       }
-       for _, act := range actions {
-               if act.ActUser != nil {
-                       userCache[act.ActUserID] = act.ActUser
-               }
-       }
-
-       for _, act := range actions {
-               repoOwner, ok := userCache[act.Repo.OwnerID]
-               if !ok {
-                       repoOwner, err = models.GetUserByID(act.Repo.OwnerID)
-                       if err != nil {
-                               if models.IsErrUserNotExist(err) {
-                                       continue
-                               }
-                               ctx.ServerError("GetUserByID", err)
-                               return
-                       }
-                       userCache[repoOwner.ID] = repoOwner
-               }
-               act.Repo.Owner = repoOwner
-       }
-       ctx.Data["Feeds"] = actions
-}
-
-// Dashboard render the dashboard page
-func Dashboard(ctx *context.Context) {
-       ctxUser := getDashboardContextUser(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       ctx.Data["Title"] = ctxUser.DisplayName() + " - " + ctx.Tr("dashboard")
-       ctx.Data["PageIsDashboard"] = true
-       ctx.Data["PageIsNews"] = true
-       ctx.Data["SearchLimit"] = setting.UI.User.RepoPagingNum
-
-       if setting.Service.EnableUserHeatmap {
-               data, err := models.GetUserHeatmapDataByUserTeam(ctxUser, ctx.Org.Team, ctx.User)
-               if err != nil {
-                       ctx.ServerError("GetUserHeatmapDataByUserTeam", err)
-                       return
-               }
-               ctx.Data["HeatmapData"] = data
-       }
-
-       var err error
-       var mirrors []*models.Repository
-       if ctxUser.IsOrganization() {
-               var env models.AccessibleReposEnvironment
-               if ctx.Org.Team != nil {
-                       env = ctxUser.AccessibleTeamReposEnv(ctx.Org.Team)
-               } else {
-                       env, err = ctxUser.AccessibleReposEnv(ctx.User.ID)
-                       if err != nil {
-                               ctx.ServerError("AccessibleReposEnv", err)
-                               return
-                       }
-               }
-               mirrors, err = env.MirrorRepos()
-               if err != nil {
-                       ctx.ServerError("env.MirrorRepos", err)
-                       return
-               }
-       } else {
-               mirrors, err = ctxUser.GetMirrorRepositories()
-               if err != nil {
-                       ctx.ServerError("GetMirrorRepositories", err)
-                       return
-               }
-       }
-       ctx.Data["MaxShowRepoNum"] = setting.UI.User.RepoPagingNum
-
-       if err := models.MirrorRepositoryList(mirrors).LoadAttributes(); err != nil {
-               ctx.ServerError("MirrorRepositoryList.LoadAttributes", err)
-               return
-       }
-       ctx.Data["MirrorCount"] = len(mirrors)
-       ctx.Data["Mirrors"] = mirrors
-
-       retrieveFeeds(ctx, models.GetFeedsOptions{
-               RequestedUser:   ctxUser,
-               RequestedTeam:   ctx.Org.Team,
-               Actor:           ctx.User,
-               IncludePrivate:  true,
-               OnlyPerformedBy: false,
-               IncludeDeleted:  false,
-               Date:            ctx.Query("date"),
-       })
-
-       if ctx.Written() {
-               return
-       }
-       ctx.HTML(http.StatusOK, tplDashboard)
-}
-
-// Milestones render the user milestones page
-func Milestones(ctx *context.Context) {
-       if models.UnitTypeIssues.UnitGlobalDisabled() && models.UnitTypePullRequests.UnitGlobalDisabled() {
-               log.Debug("Milestones overview page not available as both issues and pull requests are globally disabled")
-               ctx.Status(404)
-               return
-       }
-
-       ctx.Data["Title"] = ctx.Tr("milestones")
-       ctx.Data["PageIsMilestonesDashboard"] = true
-
-       ctxUser := getDashboardContextUser(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       repoOpts := models.SearchRepoOptions{
-               Actor:         ctxUser,
-               OwnerID:       ctxUser.ID,
-               Private:       true,
-               AllPublic:     false,                 // Include also all public repositories of users and public organisations
-               AllLimited:    false,                 // Include also all public repositories of limited organisations
-               HasMilestones: util.OptionalBoolTrue, // Just needs display repos has milestones
-       }
-
-       if ctxUser.IsOrganization() && ctx.Org.Team != nil {
-               repoOpts.TeamID = ctx.Org.Team.ID
-       }
-
-       var (
-               userRepoCond = models.SearchRepositoryCondition(&repoOpts) // all repo condition user could visit
-               repoCond     = userRepoCond
-               repoIDs      []int64
-
-               reposQuery   = ctx.Query("repos")
-               isShowClosed = ctx.Query("state") == "closed"
-               sortType     = ctx.Query("sort")
-               page         = ctx.QueryInt("page")
-               keyword      = strings.Trim(ctx.Query("q"), " ")
-       )
-
-       if page <= 1 {
-               page = 1
-       }
-
-       if len(reposQuery) != 0 {
-               if issueReposQueryPattern.MatchString(reposQuery) {
-                       // remove "[" and "]" from string
-                       reposQuery = reposQuery[1 : len(reposQuery)-1]
-                       //for each ID (delimiter ",") add to int to repoIDs
-
-                       for _, rID := range strings.Split(reposQuery, ",") {
-                               // Ensure nonempty string entries
-                               if rID != "" && rID != "0" {
-                                       rIDint64, err := strconv.ParseInt(rID, 10, 64)
-                                       // If the repo id specified by query is not parseable or not accessible by user, just ignore it.
-                                       if err == nil {
-                                               repoIDs = append(repoIDs, rIDint64)
-                                       }
-                               }
-                       }
-                       if len(repoIDs) > 0 {
-                               // Don't just let repoCond = builder.In("id", repoIDs) because user may has no permission on repoIDs
-                               // But the original repoCond has a limitation
-                               repoCond = repoCond.And(builder.In("id", repoIDs))
-                       }
-               } else {
-                       log.Warn("issueReposQueryPattern not match with query")
-               }
-       }
-
-       counts, err := models.CountMilestonesByRepoCondAndKw(userRepoCond, keyword, isShowClosed)
-       if err != nil {
-               ctx.ServerError("CountMilestonesByRepoIDs", err)
-               return
-       }
-
-       milestones, err := models.SearchMilestones(repoCond, page, isShowClosed, sortType, keyword)
-       if err != nil {
-               ctx.ServerError("SearchMilestones", err)
-               return
-       }
-
-       showRepos, _, err := models.SearchRepositoryByCondition(&repoOpts, userRepoCond, false)
-       if err != nil {
-               ctx.ServerError("SearchRepositoryByCondition", err)
-               return
-       }
-       sort.Sort(showRepos)
-
-       for i := 0; i < len(milestones); {
-               for _, repo := range showRepos {
-                       if milestones[i].RepoID == repo.ID {
-                               milestones[i].Repo = repo
-                               break
-                       }
-               }
-               if milestones[i].Repo == nil {
-                       log.Warn("Cannot find milestone %d 's repository %d", milestones[i].ID, milestones[i].RepoID)
-                       milestones = append(milestones[:i], milestones[i+1:]...)
-                       continue
-               }
-
-               milestones[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{
-                       URLPrefix: milestones[i].Repo.Link(),
-                       Metas:     milestones[i].Repo.ComposeMetas(),
-               }, milestones[i].Content)
-               if err != nil {
-                       ctx.ServerError("RenderString", err)
-                       return
-               }
-
-               if milestones[i].Repo.IsTimetrackerEnabled() {
-                       err := milestones[i].LoadTotalTrackedTime()
-                       if err != nil {
-                               ctx.ServerError("LoadTotalTrackedTime", err)
-                               return
-                       }
-               }
-               i++
-       }
-
-       milestoneStats, err := models.GetMilestonesStatsByRepoCondAndKw(repoCond, keyword)
-       if err != nil {
-               ctx.ServerError("GetMilestoneStats", err)
-               return
-       }
-
-       var totalMilestoneStats *models.MilestonesStats
-       if len(repoIDs) == 0 {
-               totalMilestoneStats = milestoneStats
-       } else {
-               totalMilestoneStats, err = models.GetMilestonesStatsByRepoCondAndKw(userRepoCond, keyword)
-               if err != nil {
-                       ctx.ServerError("GetMilestoneStats", err)
-                       return
-               }
-       }
-
-       var pagerCount int
-       if isShowClosed {
-               ctx.Data["State"] = "closed"
-               ctx.Data["Total"] = totalMilestoneStats.ClosedCount
-               pagerCount = int(milestoneStats.ClosedCount)
-       } else {
-               ctx.Data["State"] = "open"
-               ctx.Data["Total"] = totalMilestoneStats.OpenCount
-               pagerCount = int(milestoneStats.OpenCount)
-       }
-
-       ctx.Data["Milestones"] = milestones
-       ctx.Data["Repos"] = showRepos
-       ctx.Data["Counts"] = counts
-       ctx.Data["MilestoneStats"] = milestoneStats
-       ctx.Data["SortType"] = sortType
-       ctx.Data["Keyword"] = keyword
-       if milestoneStats.Total() != totalMilestoneStats.Total() {
-               ctx.Data["RepoIDs"] = repoIDs
-       }
-       ctx.Data["IsShowClosed"] = isShowClosed
-
-       pager := context.NewPagination(pagerCount, setting.UI.IssuePagingNum, page, 5)
-       pager.AddParam(ctx, "q", "Keyword")
-       pager.AddParam(ctx, "repos", "RepoIDs")
-       pager.AddParam(ctx, "sort", "SortType")
-       pager.AddParam(ctx, "state", "State")
-       ctx.Data["Page"] = pager
-
-       ctx.HTML(http.StatusOK, tplMilestones)
-}
-
-// Pulls renders the user's pull request overview page
-func Pulls(ctx *context.Context) {
-       if models.UnitTypePullRequests.UnitGlobalDisabled() {
-               log.Debug("Pull request overview page not available as it is globally disabled.")
-               ctx.Status(404)
-               return
-       }
-
-       ctx.Data["Title"] = ctx.Tr("pull_requests")
-       ctx.Data["PageIsPulls"] = true
-       buildIssueOverview(ctx, models.UnitTypePullRequests)
-}
-
-// Issues renders the user's issues overview page
-func Issues(ctx *context.Context) {
-       if models.UnitTypeIssues.UnitGlobalDisabled() {
-               log.Debug("Issues overview page not available as it is globally disabled.")
-               ctx.Status(404)
-               return
-       }
-
-       ctx.Data["Title"] = ctx.Tr("issues")
-       ctx.Data["PageIsIssues"] = true
-       buildIssueOverview(ctx, models.UnitTypeIssues)
-}
-
-// Regexp for repos query
-var issueReposQueryPattern = regexp.MustCompile(`^\[\d+(,\d+)*,?\]$`)
-
-func buildIssueOverview(ctx *context.Context, unitType models.UnitType) {
-
-       // ----------------------------------------------------
-       // Determine user; can be either user or organization.
-       // Return with NotFound or ServerError if unsuccessful.
-       // ----------------------------------------------------
-
-       ctxUser := getDashboardContextUser(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       var (
-               viewType   string
-               sortType   = ctx.Query("sort")
-               filterMode = models.FilterModeAll
-       )
-
-       // --------------------------------------------------------------------------------
-       // Distinguish User from Organization.
-       // Org:
-       // - Remember pre-determined viewType string for later. Will be posted to ctx.Data.
-       //   Organization does not have view type and filter mode.
-       // User:
-       // - Use ctx.Query("type") to determine filterMode.
-       //  The type is set when clicking for example "assigned to me" on the overview page.
-       // - Remember either this or a fallback. Will be posted to ctx.Data.
-       // --------------------------------------------------------------------------------
-
-       // TODO: distinguish during routing
-
-       viewType = ctx.Query("type")
-       switch viewType {
-       case "assigned":
-               filterMode = models.FilterModeAssign
-       case "created_by":
-               filterMode = models.FilterModeCreate
-       case "mentioned":
-               filterMode = models.FilterModeMention
-       case "review_requested":
-               filterMode = models.FilterModeReviewRequested
-       case "your_repositories": // filterMode already set to All
-       default:
-               viewType = "your_repositories"
-       }
-
-       // --------------------------------------------------------------------------
-       // Build opts (IssuesOptions), which contains filter information.
-       // Will eventually be used to retrieve issues relevant for the overview page.
-       // Note: Non-final states of opts are used in-between, namely for:
-       //       - Keyword search
-       //       - Count Issues by repo
-       // --------------------------------------------------------------------------
-
-       isPullList := unitType == models.UnitTypePullRequests
-       opts := &models.IssuesOptions{
-               IsPull:     util.OptionalBoolOf(isPullList),
-               SortType:   sortType,
-               IsArchived: util.OptionalBoolFalse,
-       }
-
-       // Get repository IDs where User/Org/Team has access.
-       var team *models.Team
-       if ctx.Org != nil {
-               team = ctx.Org.Team
-       }
-       userRepoIDs, err := getActiveUserRepoIDs(ctxUser, team, unitType)
-       if err != nil {
-               ctx.ServerError("userRepoIDs", err)
-               return
-       }
-
-       switch filterMode {
-       case models.FilterModeAll:
-               opts.RepoIDs = userRepoIDs
-       case models.FilterModeAssign:
-               opts.AssigneeID = ctx.User.ID
-       case models.FilterModeCreate:
-               opts.PosterID = ctx.User.ID
-       case models.FilterModeMention:
-               opts.MentionedID = ctx.User.ID
-       case models.FilterModeReviewRequested:
-               opts.ReviewRequestedID = ctx.User.ID
-       }
-
-       if ctxUser.IsOrganization() {
-               opts.RepoIDs = userRepoIDs
-       }
-
-       // keyword holds the search term entered into the search field.
-       keyword := strings.Trim(ctx.Query("q"), " ")
-       ctx.Data["Keyword"] = keyword
-
-       // Execute keyword search for issues.
-       // USING NON-FINAL STATE OF opts FOR A QUERY.
-       issueIDsFromSearch, err := issueIDsFromSearch(ctxUser, keyword, opts)
-       if err != nil {
-               ctx.ServerError("issueIDsFromSearch", err)
-               return
-       }
-
-       // Ensure no issues are returned if a keyword was provided that didn't match any issues.
-       var forceEmpty bool
-
-       if len(issueIDsFromSearch) > 0 {
-               opts.IssueIDs = issueIDsFromSearch
-       } else if len(keyword) > 0 {
-               forceEmpty = true
-       }
-
-       // Educated guess: Do or don't show closed issues.
-       isShowClosed := ctx.Query("state") == "closed"
-       opts.IsClosed = util.OptionalBoolOf(isShowClosed)
-
-       // Filter repos and count issues in them. Count will be used later.
-       // USING NON-FINAL STATE OF opts FOR A QUERY.
-       var issueCountByRepo map[int64]int64
-       if !forceEmpty {
-               issueCountByRepo, err = models.CountIssuesByRepo(opts)
-               if err != nil {
-                       ctx.ServerError("CountIssuesByRepo", err)
-                       return
-               }
-       }
-
-       // Make sure page number is at least 1. Will be posted to ctx.Data.
-       page := ctx.QueryInt("page")
-       if page <= 1 {
-               page = 1
-       }
-       opts.Page = page
-       opts.PageSize = setting.UI.IssuePagingNum
-
-       // Get IDs for labels (a filter option for issues/pulls).
-       // Required for IssuesOptions.
-       var labelIDs []int64
-       selectedLabels := ctx.Query("labels")
-       if len(selectedLabels) > 0 && selectedLabels != "0" {
-               labelIDs, err = base.StringsToInt64s(strings.Split(selectedLabels, ","))
-               if err != nil {
-                       ctx.ServerError("StringsToInt64s", err)
-                       return
-               }
-       }
-       opts.LabelIDs = labelIDs
-
-       // Parse ctx.Query("repos") and remember matched repo IDs for later.
-       // Gets set when clicking filters on the issues overview page.
-       repoIDs := getRepoIDs(ctx.Query("repos"))
-       if len(repoIDs) > 0 {
-               opts.RepoIDs = repoIDs
-       }
-
-       // ------------------------------
-       // Get issues as defined by opts.
-       // ------------------------------
-
-       // Slice of Issues that will be displayed on the overview page
-       // USING FINAL STATE OF opts FOR A QUERY.
-       var issues []*models.Issue
-       if !forceEmpty {
-               issues, err = models.Issues(opts)
-               if err != nil {
-                       ctx.ServerError("Issues", err)
-                       return
-               }
-       } else {
-               issues = []*models.Issue{}
-       }
-
-       // ----------------------------------
-       // Add repository pointers to Issues.
-       // ----------------------------------
-
-       // showReposMap maps repository IDs to their Repository pointers.
-       showReposMap, err := repoIDMap(ctxUser, issueCountByRepo, unitType)
-       if err != nil {
-               if models.IsErrRepoNotExist(err) {
-                       ctx.NotFound("GetRepositoryByID", err)
-                       return
-               }
-               ctx.ServerError("repoIDMap", err)
-               return
-       }
-
-       // a RepositoryList
-       showRepos := models.RepositoryListOfMap(showReposMap)
-       sort.Sort(showRepos)
-       if err = showRepos.LoadAttributes(); err != nil {
-               ctx.ServerError("LoadAttributes", err)
-               return
-       }
-
-       // maps pull request IDs to their CommitStatus. Will be posted to ctx.Data.
-       for _, issue := range issues {
-               issue.Repo = showReposMap[issue.RepoID]
-       }
-
-       commitStatus, err := pull_service.GetIssuesLastCommitStatus(issues)
-       if err != nil {
-               ctx.ServerError("GetIssuesLastCommitStatus", err)
-               return
-       }
-
-       // -------------------------------
-       // Fill stats to post to ctx.Data.
-       // -------------------------------
-
-       userIssueStatsOpts := models.UserIssueStatsOptions{
-               UserID:      ctx.User.ID,
-               UserRepoIDs: userRepoIDs,
-               FilterMode:  filterMode,
-               IsPull:      isPullList,
-               IsClosed:    isShowClosed,
-               IsArchived:  util.OptionalBoolFalse,
-               LabelIDs:    opts.LabelIDs,
-       }
-       if len(repoIDs) > 0 {
-               userIssueStatsOpts.UserRepoIDs = repoIDs
-       }
-       if ctxUser.IsOrganization() {
-               userIssueStatsOpts.RepoIDs = userRepoIDs
-       }
-       userIssueStats, err := models.GetUserIssueStats(userIssueStatsOpts)
-       if err != nil {
-               ctx.ServerError("GetUserIssueStats User", err)
-               return
-       }
-
-       var shownIssueStats *models.IssueStats
-       if !forceEmpty {
-               statsOpts := models.UserIssueStatsOptions{
-                       UserID:      ctx.User.ID,
-                       UserRepoIDs: userRepoIDs,
-                       FilterMode:  filterMode,
-                       IsPull:      isPullList,
-                       IsClosed:    isShowClosed,
-                       IssueIDs:    issueIDsFromSearch,
-                       IsArchived:  util.OptionalBoolFalse,
-                       LabelIDs:    opts.LabelIDs,
-               }
-               if len(repoIDs) > 0 {
-                       statsOpts.RepoIDs = repoIDs
-               } else if ctxUser.IsOrganization() {
-                       statsOpts.RepoIDs = userRepoIDs
-               }
-               shownIssueStats, err = models.GetUserIssueStats(statsOpts)
-               if err != nil {
-                       ctx.ServerError("GetUserIssueStats Shown", err)
-                       return
-               }
-       } else {
-               shownIssueStats = &models.IssueStats{}
-       }
-
-       var allIssueStats *models.IssueStats
-       if !forceEmpty {
-               allIssueStatsOpts := models.UserIssueStatsOptions{
-                       UserID:      ctx.User.ID,
-                       UserRepoIDs: userRepoIDs,
-                       FilterMode:  filterMode,
-                       IsPull:      isPullList,
-                       IsClosed:    isShowClosed,
-                       IssueIDs:    issueIDsFromSearch,
-                       IsArchived:  util.OptionalBoolFalse,
-                       LabelIDs:    opts.LabelIDs,
-               }
-               if ctxUser.IsOrganization() {
-                       allIssueStatsOpts.RepoIDs = userRepoIDs
-               }
-               allIssueStats, err = models.GetUserIssueStats(allIssueStatsOpts)
-               if err != nil {
-                       ctx.ServerError("GetUserIssueStats All", err)
-                       return
-               }
-       } else {
-               allIssueStats = &models.IssueStats{}
-       }
-
-       // Will be posted to ctx.Data.
-       var shownIssues int
-       if !isShowClosed {
-               shownIssues = int(shownIssueStats.OpenCount)
-               ctx.Data["TotalIssueCount"] = int(allIssueStats.OpenCount)
-       } else {
-               shownIssues = int(shownIssueStats.ClosedCount)
-               ctx.Data["TotalIssueCount"] = int(allIssueStats.ClosedCount)
-       }
-
-       ctx.Data["IsShowClosed"] = isShowClosed
-
-       ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] =
-               issue_service.GetRefEndNamesAndURLs(issues, ctx.Query("RepoLink"))
-
-       ctx.Data["Issues"] = issues
-
-       approvalCounts, err := models.IssueList(issues).GetApprovalCounts()
-       if err != nil {
-               ctx.ServerError("ApprovalCounts", err)
-               return
-       }
-       ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 {
-               counts, ok := approvalCounts[issueID]
-               if !ok || len(counts) == 0 {
-                       return 0
-               }
-               reviewTyp := models.ReviewTypeApprove
-               if typ == "reject" {
-                       reviewTyp = models.ReviewTypeReject
-               } else if typ == "waiting" {
-                       reviewTyp = models.ReviewTypeRequest
-               }
-               for _, count := range counts {
-                       if count.Type == reviewTyp {
-                               return count.Count
-                       }
-               }
-               return 0
-       }
-       ctx.Data["CommitStatus"] = commitStatus
-       ctx.Data["Repos"] = showRepos
-       ctx.Data["Counts"] = issueCountByRepo
-       ctx.Data["IssueStats"] = userIssueStats
-       ctx.Data["ShownIssueStats"] = shownIssueStats
-       ctx.Data["ViewType"] = viewType
-       ctx.Data["SortType"] = sortType
-       ctx.Data["RepoIDs"] = repoIDs
-       ctx.Data["IsShowClosed"] = isShowClosed
-       ctx.Data["SelectLabels"] = selectedLabels
-
-       if isShowClosed {
-               ctx.Data["State"] = "closed"
-       } else {
-               ctx.Data["State"] = "open"
-       }
-
-       // Convert []int64 to string
-       json := jsoniter.ConfigCompatibleWithStandardLibrary
-       reposParam, _ := json.Marshal(repoIDs)
-
-       ctx.Data["ReposParam"] = string(reposParam)
-
-       pager := context.NewPagination(shownIssues, setting.UI.IssuePagingNum, page, 5)
-       pager.AddParam(ctx, "q", "Keyword")
-       pager.AddParam(ctx, "type", "ViewType")
-       pager.AddParam(ctx, "repos", "ReposParam")
-       pager.AddParam(ctx, "sort", "SortType")
-       pager.AddParam(ctx, "state", "State")
-       pager.AddParam(ctx, "labels", "SelectLabels")
-       pager.AddParam(ctx, "milestone", "MilestoneID")
-       pager.AddParam(ctx, "assignee", "AssigneeID")
-       ctx.Data["Page"] = pager
-
-       ctx.HTML(http.StatusOK, tplIssues)
-}
-
-func getRepoIDs(reposQuery string) []int64 {
-       if len(reposQuery) == 0 || reposQuery == "[]" {
-               return []int64{}
-       }
-       if !issueReposQueryPattern.MatchString(reposQuery) {
-               log.Warn("issueReposQueryPattern does not match query")
-               return []int64{}
-       }
-
-       var repoIDs []int64
-       // remove "[" and "]" from string
-       reposQuery = reposQuery[1 : len(reposQuery)-1]
-       //for each ID (delimiter ",") add to int to repoIDs
-       for _, rID := range strings.Split(reposQuery, ",") {
-               // Ensure nonempty string entries
-               if rID != "" && rID != "0" {
-                       rIDint64, err := strconv.ParseInt(rID, 10, 64)
-                       if err == nil {
-                               repoIDs = append(repoIDs, rIDint64)
-                       }
-               }
-       }
-
-       return repoIDs
-}
-
-func getActiveUserRepoIDs(ctxUser *models.User, team *models.Team, unitType models.UnitType) ([]int64, error) {
-       var userRepoIDs []int64
-       var err error
-
-       if ctxUser.IsOrganization() {
-               userRepoIDs, err = getActiveTeamOrOrgRepoIds(ctxUser, team, unitType)
-               if err != nil {
-                       return nil, fmt.Errorf("orgRepoIds: %v", err)
-               }
-       } else {
-               userRepoIDs, err = ctxUser.GetActiveAccessRepoIDs(unitType)
-               if err != nil {
-                       return nil, fmt.Errorf("ctxUser.GetAccessRepoIDs: %v", err)
-               }
-       }
-
-       if len(userRepoIDs) == 0 {
-               userRepoIDs = []int64{-1}
-       }
-
-       return userRepoIDs, nil
-}
-
-// getActiveTeamOrOrgRepoIds gets RepoIDs for ctxUser as Organization.
-// Should be called if and only if ctxUser.IsOrganization == true.
-func getActiveTeamOrOrgRepoIds(ctxUser *models.User, team *models.Team, unitType models.UnitType) ([]int64, error) {
-       var orgRepoIDs []int64
-       var err error
-       var env models.AccessibleReposEnvironment
-
-       if team != nil {
-               env = ctxUser.AccessibleTeamReposEnv(team)
-       } else {
-               env, err = ctxUser.AccessibleReposEnv(ctxUser.ID)
-               if err != nil {
-                       return nil, fmt.Errorf("AccessibleReposEnv: %v", err)
-               }
-       }
-       orgRepoIDs, err = env.RepoIDs(1, ctxUser.NumRepos)
-       if err != nil {
-               return nil, fmt.Errorf("env.RepoIDs: %v", err)
-       }
-       orgRepoIDs, err = models.FilterOutRepoIdsWithoutUnitAccess(ctxUser, orgRepoIDs, unitType)
-       if err != nil {
-               return nil, fmt.Errorf("FilterOutRepoIdsWithoutUnitAccess: %v", err)
-       }
-
-       return orgRepoIDs, nil
-}
-
-func issueIDsFromSearch(ctxUser *models.User, keyword string, opts *models.IssuesOptions) ([]int64, error) {
-       if len(keyword) == 0 {
-               return []int64{}, nil
-       }
-
-       searchRepoIDs, err := models.GetRepoIDsForIssuesOptions(opts, ctxUser)
-       if err != nil {
-               return nil, fmt.Errorf("GetRepoIDsForIssuesOptions: %v", err)
-       }
-       issueIDsFromSearch, err := issue_indexer.SearchIssuesByKeyword(searchRepoIDs, keyword)
-       if err != nil {
-               return nil, fmt.Errorf("SearchIssuesByKeyword: %v", err)
-       }
-
-       return issueIDsFromSearch, nil
-}
-
-func repoIDMap(ctxUser *models.User, issueCountByRepo map[int64]int64, unitType models.UnitType) (map[int64]*models.Repository, error) {
-       repoByID := make(map[int64]*models.Repository, len(issueCountByRepo))
-       for id := range issueCountByRepo {
-               if id <= 0 {
-                       continue
-               }
-               if _, ok := repoByID[id]; !ok {
-                       repo, err := models.GetRepositoryByID(id)
-                       if models.IsErrRepoNotExist(err) {
-                               return nil, err
-                       } else if err != nil {
-                               return nil, fmt.Errorf("GetRepositoryByID: [%d]%v", id, err)
-                       }
-                       repoByID[id] = repo
-               }
-               repo := repoByID[id]
-
-               // Check if user has access to given repository.
-               perm, err := models.GetUserRepoPermission(repo, ctxUser)
-               if err != nil {
-                       return nil, fmt.Errorf("GetUserRepoPermission: [%d]%v", id, err)
-               }
-               if !perm.CanRead(unitType) {
-                       log.Debug("User created Issues in Repository which they no longer have access to: [%d]", id)
-               }
-       }
-       return repoByID, nil
-}
-
-// ShowSSHKeys output all the ssh keys of user by uid
-func ShowSSHKeys(ctx *context.Context, uid int64) {
-       keys, err := models.ListPublicKeys(uid, models.ListOptions{})
-       if err != nil {
-               ctx.ServerError("ListPublicKeys", err)
-               return
-       }
-
-       var buf bytes.Buffer
-       for i := range keys {
-               buf.WriteString(keys[i].OmitEmail())
-               buf.WriteString("\n")
-       }
-       ctx.PlainText(200, buf.Bytes())
-}
-
-// ShowGPGKeys output all the public GPG keys of user by uid
-func ShowGPGKeys(ctx *context.Context, uid int64) {
-       keys, err := models.ListGPGKeys(uid, models.ListOptions{})
-       if err != nil {
-               ctx.ServerError("ListGPGKeys", err)
-               return
-       }
-       entities := make([]*openpgp.Entity, 0)
-       failedEntitiesID := make([]string, 0)
-       for _, k := range keys {
-               e, err := models.GPGKeyToEntity(k)
-               if err != nil {
-                       if models.IsErrGPGKeyImportNotExist(err) {
-                               failedEntitiesID = append(failedEntitiesID, k.KeyID)
-                               continue //Skip previous import without backup of imported armored key
-                       }
-                       ctx.ServerError("ShowGPGKeys", err)
-                       return
-               }
-               entities = append(entities, e)
-       }
-       var buf bytes.Buffer
-
-       headers := make(map[string]string)
-       if len(failedEntitiesID) > 0 { //If some key need re-import to be exported
-               headers["Note"] = fmt.Sprintf("The keys with the following IDs couldn't be exported and need to be reuploaded %s", strings.Join(failedEntitiesID, ", "))
-       }
-       writer, _ := armor.Encode(&buf, "PGP PUBLIC KEY BLOCK", headers)
-       for _, e := range entities {
-               err = e.Serialize(writer) //TODO find why key are exported with a different cipherTypeByte as original (should not be blocking but strange)
-               if err != nil {
-                       ctx.ServerError("ShowGPGKeys", err)
-                       return
-               }
-       }
-       writer.Close()
-       ctx.PlainText(200, buf.Bytes())
-}
-
-// Email2User show user page via email
-func Email2User(ctx *context.Context) {
-       u, err := models.GetUserByEmail(ctx.Query("email"))
-       if err != nil {
-               if models.IsErrUserNotExist(err) {
-                       ctx.NotFound("GetUserByEmail", err)
-               } else {
-                       ctx.ServerError("GetUserByEmail", err)
-               }
-               return
-       }
-       ctx.Redirect(setting.AppSubURL + "/user/" + u.Name)
-}
diff --git a/routers/user/home_test.go b/routers/user/home_test.go
deleted file mode 100644 (file)
index b0109c3..0000000
+++ /dev/null
@@ -1,118 +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 user
-
-import (
-       "net/http"
-       "testing"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/test"
-
-       "github.com/stretchr/testify/assert"
-)
-
-func TestArchivedIssues(t *testing.T) {
-       // Arrange
-       setting.UI.IssuePagingNum = 1
-       assert.NoError(t, models.LoadFixtures())
-
-       ctx := test.MockContext(t, "issues")
-       test.LoadUser(t, ctx, 30)
-       ctx.Req.Form.Set("state", "open")
-
-       // Assume: User 30 has access to two Repos with Issues, one of the Repos being archived.
-       repos, _, _ := models.GetUserRepositories(&models.SearchRepoOptions{Actor: ctx.User})
-       assert.Len(t, repos, 2)
-       IsArchived := make(map[int64]bool)
-       NumIssues := make(map[int64]int)
-       for _, repo := range repos {
-               IsArchived[repo.ID] = repo.IsArchived
-               NumIssues[repo.ID] = repo.NumIssues
-       }
-       assert.False(t, IsArchived[50])
-       assert.EqualValues(t, 1, NumIssues[50])
-       assert.True(t, IsArchived[51])
-       assert.EqualValues(t, 1, NumIssues[51])
-
-       // Act
-       Issues(ctx)
-
-       // Assert: One Issue (ID 30) from one Repo (ID 50) is retrieved, while nothing from archived Repo 51 is retrieved
-       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
-
-       assert.EqualValues(t, map[int64]int64{50: 1}, ctx.Data["Counts"])
-       assert.Len(t, ctx.Data["Issues"], 1)
-       assert.Len(t, ctx.Data["Repos"], 1)
-}
-
-func TestIssues(t *testing.T) {
-       setting.UI.IssuePagingNum = 1
-       assert.NoError(t, models.LoadFixtures())
-
-       ctx := test.MockContext(t, "issues")
-       test.LoadUser(t, ctx, 2)
-       ctx.Req.Form.Set("state", "closed")
-       Issues(ctx)
-       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
-
-       assert.EqualValues(t, map[int64]int64{1: 1, 2: 1}, ctx.Data["Counts"])
-       assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
-       assert.Len(t, ctx.Data["Issues"], 1)
-       assert.Len(t, ctx.Data["Repos"], 2)
-}
-
-func TestPulls(t *testing.T) {
-       setting.UI.IssuePagingNum = 20
-       assert.NoError(t, models.LoadFixtures())
-
-       ctx := test.MockContext(t, "pulls")
-       test.LoadUser(t, ctx, 2)
-       ctx.Req.Form.Set("state", "open")
-       Pulls(ctx)
-       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
-
-       assert.Len(t, ctx.Data["Issues"], 3)
-}
-
-func TestMilestones(t *testing.T) {
-       setting.UI.IssuePagingNum = 1
-       assert.NoError(t, models.LoadFixtures())
-
-       ctx := test.MockContext(t, "milestones")
-       test.LoadUser(t, ctx, 2)
-       ctx.SetParams("sort", "issues")
-       ctx.Req.Form.Set("state", "closed")
-       ctx.Req.Form.Set("sort", "furthestduedate")
-       Milestones(ctx)
-       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
-       assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"])
-       assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
-       assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"])
-       assert.EqualValues(t, 1, ctx.Data["Total"])
-       assert.Len(t, ctx.Data["Milestones"], 1)
-       assert.Len(t, ctx.Data["Repos"], 2) // both repo 42 and 1 have milestones and both are owned by user 2
-}
-
-func TestMilestonesForSpecificRepo(t *testing.T) {
-       setting.UI.IssuePagingNum = 1
-       assert.NoError(t, models.LoadFixtures())
-
-       ctx := test.MockContext(t, "milestones")
-       test.LoadUser(t, ctx, 2)
-       ctx.SetParams("sort", "issues")
-       ctx.SetParams("repo", "1")
-       ctx.Req.Form.Set("state", "closed")
-       ctx.Req.Form.Set("sort", "furthestduedate")
-       Milestones(ctx)
-       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
-       assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"])
-       assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
-       assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"])
-       assert.EqualValues(t, 1, ctx.Data["Total"])
-       assert.Len(t, ctx.Data["Milestones"], 1)
-       assert.Len(t, ctx.Data["Repos"], 2) // both repo 42 and 1 have milestones and both are owned by user 2
-}
diff --git a/routers/user/main_test.go b/routers/user/main_test.go
deleted file mode 100644 (file)
index ed0724d..0000000
+++ /dev/null
@@ -1,16 +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 user
-
-import (
-       "path/filepath"
-       "testing"
-
-       "code.gitea.io/gitea/models"
-)
-
-func TestMain(m *testing.M) {
-       models.MainTest(m, filepath.Join("..", ".."))
-}
diff --git a/routers/user/notification.go b/routers/user/notification.go
deleted file mode 100644 (file)
index 523e945..0000000
+++ /dev/null
@@ -1,192 +0,0 @@
-// Copyright 2019 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 user
-
-import (
-       "errors"
-       "fmt"
-       "net/http"
-       "strconv"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/setting"
-)
-
-const (
-       tplNotification    base.TplName = "user/notification/notification"
-       tplNotificationDiv base.TplName = "user/notification/notification_div"
-)
-
-// GetNotificationCount is the middleware that sets the notification count in the context
-func GetNotificationCount(c *context.Context) {
-       if strings.HasPrefix(c.Req.URL.Path, "/api") {
-               return
-       }
-
-       if !c.IsSigned {
-               return
-       }
-
-       c.Data["NotificationUnreadCount"] = func() int64 {
-               count, err := models.GetNotificationCount(c.User, models.NotificationStatusUnread)
-               if err != nil {
-                       c.ServerError("GetNotificationCount", err)
-                       return -1
-               }
-
-               return count
-       }
-}
-
-// Notifications is the notifications page
-func Notifications(c *context.Context) {
-       getNotifications(c)
-       if c.Written() {
-               return
-       }
-       if c.QueryBool("div-only") {
-               c.HTML(http.StatusOK, tplNotificationDiv)
-               return
-       }
-       c.HTML(http.StatusOK, tplNotification)
-}
-
-func getNotifications(c *context.Context) {
-       var (
-               keyword = strings.Trim(c.Query("q"), " ")
-               status  models.NotificationStatus
-               page    = c.QueryInt("page")
-               perPage = c.QueryInt("perPage")
-       )
-       if page < 1 {
-               page = 1
-       }
-       if perPage < 1 {
-               perPage = 20
-       }
-
-       switch keyword {
-       case "read":
-               status = models.NotificationStatusRead
-       default:
-               status = models.NotificationStatusUnread
-       }
-
-       total, err := models.GetNotificationCount(c.User, status)
-       if err != nil {
-               c.ServerError("ErrGetNotificationCount", err)
-               return
-       }
-
-       // redirect to last page if request page is more than total pages
-       pager := context.NewPagination(int(total), perPage, page, 5)
-       if pager.Paginater.Current() < page {
-               c.Redirect(fmt.Sprintf("/notifications?q=%s&page=%d", c.Query("q"), pager.Paginater.Current()))
-               return
-       }
-
-       statuses := []models.NotificationStatus{status, models.NotificationStatusPinned}
-       notifications, err := models.NotificationsForUser(c.User, statuses, page, perPage)
-       if err != nil {
-               c.ServerError("ErrNotificationsForUser", err)
-               return
-       }
-
-       failCount := 0
-
-       repos, failures, err := notifications.LoadRepos()
-       if err != nil {
-               c.ServerError("LoadRepos", err)
-               return
-       }
-       notifications = notifications.Without(failures)
-       if err := repos.LoadAttributes(); err != nil {
-               c.ServerError("LoadAttributes", err)
-               return
-       }
-       failCount += len(failures)
-
-       failures, err = notifications.LoadIssues()
-       if err != nil {
-               c.ServerError("LoadIssues", err)
-               return
-       }
-       notifications = notifications.Without(failures)
-       failCount += len(failures)
-
-       failures, err = notifications.LoadComments()
-       if err != nil {
-               c.ServerError("LoadComments", err)
-               return
-       }
-       notifications = notifications.Without(failures)
-       failCount += len(failures)
-
-       if failCount > 0 {
-               c.Flash.Error(fmt.Sprintf("ERROR: %d notifications were removed due to missing parts - check the logs", failCount))
-       }
-
-       c.Data["Title"] = c.Tr("notifications")
-       c.Data["Keyword"] = keyword
-       c.Data["Status"] = status
-       c.Data["Notifications"] = notifications
-
-       pager.SetDefaultParams(c)
-       c.Data["Page"] = pager
-}
-
-// NotificationStatusPost is a route for changing the status of a notification
-func NotificationStatusPost(c *context.Context) {
-       var (
-               notificationID, _ = strconv.ParseInt(c.Req.PostFormValue("notification_id"), 10, 64)
-               statusStr         = c.Req.PostFormValue("status")
-               status            models.NotificationStatus
-       )
-
-       switch statusStr {
-       case "read":
-               status = models.NotificationStatusRead
-       case "unread":
-               status = models.NotificationStatusUnread
-       case "pinned":
-               status = models.NotificationStatusPinned
-       default:
-               c.ServerError("InvalidNotificationStatus", errors.New("Invalid notification status"))
-               return
-       }
-
-       if err := models.SetNotificationStatus(notificationID, c.User, status); err != nil {
-               c.ServerError("SetNotificationStatus", err)
-               return
-       }
-
-       if !c.QueryBool("noredirect") {
-               url := fmt.Sprintf("%s/notifications?page=%s", setting.AppSubURL, c.Query("page"))
-               c.Redirect(url, http.StatusSeeOther)
-       }
-
-       getNotifications(c)
-       if c.Written() {
-               return
-       }
-       c.Data["Link"] = setting.AppURL + "notifications"
-
-       c.HTML(http.StatusOK, tplNotificationDiv)
-}
-
-// NotificationPurgePost is a route for 'purging' the list of notifications - marking all unread as read
-func NotificationPurgePost(c *context.Context) {
-       err := models.UpdateNotificationStatuses(c.User, models.NotificationStatusUnread, models.NotificationStatusRead)
-       if err != nil {
-               c.ServerError("ErrUpdateNotificationStatuses", err)
-               return
-       }
-
-       url := fmt.Sprintf("%s/notifications", setting.AppSubURL)
-       c.Redirect(url, http.StatusSeeOther)
-}
diff --git a/routers/user/oauth.go b/routers/user/oauth.go
deleted file mode 100644 (file)
index 3ef5a56..0000000
+++ /dev/null
@@ -1,646 +0,0 @@
-// Copyright 2019 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 user
-
-import (
-       "encoding/base64"
-       "fmt"
-       "html"
-       "net/http"
-       "net/url"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/auth/sso"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/timeutil"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-
-       "gitea.com/go-chi/binding"
-       "github.com/dgrijalva/jwt-go"
-)
-
-const (
-       tplGrantAccess base.TplName = "user/auth/grant"
-       tplGrantError  base.TplName = "user/auth/grant_error"
-)
-
-// TODO move error and responses to SDK or models
-
-// AuthorizeErrorCode represents an error code specified in RFC 6749
-type AuthorizeErrorCode string
-
-const (
-       // ErrorCodeInvalidRequest represents the according error in RFC 6749
-       ErrorCodeInvalidRequest AuthorizeErrorCode = "invalid_request"
-       // ErrorCodeUnauthorizedClient represents the according error in RFC 6749
-       ErrorCodeUnauthorizedClient AuthorizeErrorCode = "unauthorized_client"
-       // ErrorCodeAccessDenied represents the according error in RFC 6749
-       ErrorCodeAccessDenied AuthorizeErrorCode = "access_denied"
-       // ErrorCodeUnsupportedResponseType represents the according error in RFC 6749
-       ErrorCodeUnsupportedResponseType AuthorizeErrorCode = "unsupported_response_type"
-       // ErrorCodeInvalidScope represents the according error in RFC 6749
-       ErrorCodeInvalidScope AuthorizeErrorCode = "invalid_scope"
-       // ErrorCodeServerError represents the according error in RFC 6749
-       ErrorCodeServerError AuthorizeErrorCode = "server_error"
-       // ErrorCodeTemporaryUnavailable represents the according error in RFC 6749
-       ErrorCodeTemporaryUnavailable AuthorizeErrorCode = "temporarily_unavailable"
-)
-
-// AuthorizeError represents an error type specified in RFC 6749
-type AuthorizeError struct {
-       ErrorCode        AuthorizeErrorCode `json:"error" form:"error"`
-       ErrorDescription string
-       State            string
-}
-
-// Error returns the error message
-func (err AuthorizeError) Error() string {
-       return fmt.Sprintf("%s: %s", err.ErrorCode, err.ErrorDescription)
-}
-
-// AccessTokenErrorCode represents an error code specified in RFC 6749
-type AccessTokenErrorCode string
-
-const (
-       // AccessTokenErrorCodeInvalidRequest represents an error code specified in RFC 6749
-       AccessTokenErrorCodeInvalidRequest AccessTokenErrorCode = "invalid_request"
-       // AccessTokenErrorCodeInvalidClient represents an error code specified in RFC 6749
-       AccessTokenErrorCodeInvalidClient = "invalid_client"
-       // AccessTokenErrorCodeInvalidGrant represents an error code specified in RFC 6749
-       AccessTokenErrorCodeInvalidGrant = "invalid_grant"
-       // AccessTokenErrorCodeUnauthorizedClient represents an error code specified in RFC 6749
-       AccessTokenErrorCodeUnauthorizedClient = "unauthorized_client"
-       // AccessTokenErrorCodeUnsupportedGrantType represents an error code specified in RFC 6749
-       AccessTokenErrorCodeUnsupportedGrantType = "unsupported_grant_type"
-       // AccessTokenErrorCodeInvalidScope represents an error code specified in RFC 6749
-       AccessTokenErrorCodeInvalidScope = "invalid_scope"
-)
-
-// AccessTokenError represents an error response specified in RFC 6749
-type AccessTokenError struct {
-       ErrorCode        AccessTokenErrorCode `json:"error" form:"error"`
-       ErrorDescription string               `json:"error_description"`
-}
-
-// Error returns the error message
-func (err AccessTokenError) Error() string {
-       return fmt.Sprintf("%s: %s", err.ErrorCode, err.ErrorDescription)
-}
-
-// BearerTokenErrorCode represents an error code specified in RFC 6750
-type BearerTokenErrorCode string
-
-const (
-       // BearerTokenErrorCodeInvalidRequest represents an error code specified in RFC 6750
-       BearerTokenErrorCodeInvalidRequest BearerTokenErrorCode = "invalid_request"
-       // BearerTokenErrorCodeInvalidToken represents an error code specified in RFC 6750
-       BearerTokenErrorCodeInvalidToken BearerTokenErrorCode = "invalid_token"
-       // BearerTokenErrorCodeInsufficientScope represents an error code specified in RFC 6750
-       BearerTokenErrorCodeInsufficientScope BearerTokenErrorCode = "insufficient_scope"
-)
-
-// BearerTokenError represents an error response specified in RFC 6750
-type BearerTokenError struct {
-       ErrorCode        BearerTokenErrorCode `json:"error" form:"error"`
-       ErrorDescription string               `json:"error_description"`
-}
-
-// TokenType specifies the kind of token
-type TokenType string
-
-const (
-       // TokenTypeBearer represents a token type specified in RFC 6749
-       TokenTypeBearer TokenType = "bearer"
-       // TokenTypeMAC represents a token type specified in RFC 6749
-       TokenTypeMAC = "mac"
-)
-
-// AccessTokenResponse represents a successful access token response
-type AccessTokenResponse struct {
-       AccessToken  string    `json:"access_token"`
-       TokenType    TokenType `json:"token_type"`
-       ExpiresIn    int64     `json:"expires_in"`
-       RefreshToken string    `json:"refresh_token"`
-       IDToken      string    `json:"id_token,omitempty"`
-}
-
-func newAccessTokenResponse(grant *models.OAuth2Grant, clientSecret string) (*AccessTokenResponse, *AccessTokenError) {
-       if setting.OAuth2.InvalidateRefreshTokens {
-               if err := grant.IncreaseCounter(); err != nil {
-                       return nil, &AccessTokenError{
-                               ErrorCode:        AccessTokenErrorCodeInvalidGrant,
-                               ErrorDescription: "cannot increase the grant counter",
-                       }
-               }
-       }
-       // generate access token to access the API
-       expirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.AccessTokenExpirationTime)
-       accessToken := &models.OAuth2Token{
-               GrantID: grant.ID,
-               Type:    models.TypeAccessToken,
-               StandardClaims: jwt.StandardClaims{
-                       ExpiresAt: expirationDate.AsTime().Unix(),
-               },
-       }
-       signedAccessToken, err := accessToken.SignToken()
-       if err != nil {
-               return nil, &AccessTokenError{
-                       ErrorCode:        AccessTokenErrorCodeInvalidRequest,
-                       ErrorDescription: "cannot sign token",
-               }
-       }
-
-       // generate refresh token to request an access token after it expired later
-       refreshExpirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.RefreshTokenExpirationTime * 60 * 60).AsTime().Unix()
-       refreshToken := &models.OAuth2Token{
-               GrantID: grant.ID,
-               Counter: grant.Counter,
-               Type:    models.TypeRefreshToken,
-               StandardClaims: jwt.StandardClaims{
-                       ExpiresAt: refreshExpirationDate,
-               },
-       }
-       signedRefreshToken, err := refreshToken.SignToken()
-       if err != nil {
-               return nil, &AccessTokenError{
-                       ErrorCode:        AccessTokenErrorCodeInvalidRequest,
-                       ErrorDescription: "cannot sign token",
-               }
-       }
-
-       // generate OpenID Connect id_token
-       signedIDToken := ""
-       if grant.ScopeContains("openid") {
-               app, err := models.GetOAuth2ApplicationByID(grant.ApplicationID)
-               if err != nil {
-                       return nil, &AccessTokenError{
-                               ErrorCode:        AccessTokenErrorCodeInvalidRequest,
-                               ErrorDescription: "cannot find application",
-                       }
-               }
-               idToken := &models.OIDCToken{
-                       StandardClaims: jwt.StandardClaims{
-                               ExpiresAt: expirationDate.AsTime().Unix(),
-                               Issuer:    setting.AppURL,
-                               Audience:  app.ClientID,
-                               Subject:   fmt.Sprint(grant.UserID),
-                       },
-                       Nonce: grant.Nonce,
-               }
-               signedIDToken, err = idToken.SignToken(clientSecret)
-               if err != nil {
-                       return nil, &AccessTokenError{
-                               ErrorCode:        AccessTokenErrorCodeInvalidRequest,
-                               ErrorDescription: "cannot sign token",
-                       }
-               }
-       }
-
-       return &AccessTokenResponse{
-               AccessToken:  signedAccessToken,
-               TokenType:    TokenTypeBearer,
-               ExpiresIn:    setting.OAuth2.AccessTokenExpirationTime,
-               RefreshToken: signedRefreshToken,
-               IDToken:      signedIDToken,
-       }, nil
-}
-
-type userInfoResponse struct {
-       Sub      string `json:"sub"`
-       Name     string `json:"name"`
-       Username string `json:"preferred_username"`
-       Email    string `json:"email"`
-       Picture  string `json:"picture"`
-}
-
-// InfoOAuth manages request for userinfo endpoint
-func InfoOAuth(ctx *context.Context) {
-       header := ctx.Req.Header.Get("Authorization")
-       auths := strings.Fields(header)
-       if len(auths) != 2 || auths[0] != "Bearer" {
-               ctx.HandleText(http.StatusUnauthorized, "no valid auth token authorization")
-               return
-       }
-       uid := sso.CheckOAuthAccessToken(auths[1])
-       if uid == 0 {
-               handleBearerTokenError(ctx, BearerTokenError{
-                       ErrorCode:        BearerTokenErrorCodeInvalidToken,
-                       ErrorDescription: "Access token not assigned to any user",
-               })
-               return
-       }
-       authUser, err := models.GetUserByID(uid)
-       if err != nil {
-               ctx.ServerError("GetUserByID", err)
-               return
-       }
-       response := &userInfoResponse{
-               Sub:      fmt.Sprint(authUser.ID),
-               Name:     authUser.FullName,
-               Username: authUser.Name,
-               Email:    authUser.Email,
-               Picture:  authUser.AvatarLink(),
-       }
-       ctx.JSON(http.StatusOK, response)
-}
-
-// AuthorizeOAuth manages authorize requests
-func AuthorizeOAuth(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.AuthorizationForm)
-       errs := binding.Errors{}
-       errs = form.Validate(ctx.Req, errs)
-       if len(errs) > 0 {
-               errstring := ""
-               for _, e := range errs {
-                       errstring += e.Error() + "\n"
-               }
-               ctx.ServerError("AuthorizeOAuth: Validate: ", fmt.Errorf("errors occurred during validation: %s", errstring))
-               return
-       }
-
-       app, err := models.GetOAuth2ApplicationByClientID(form.ClientID)
-       if err != nil {
-               if models.IsErrOauthClientIDInvalid(err) {
-                       handleAuthorizeError(ctx, AuthorizeError{
-                               ErrorCode:        ErrorCodeUnauthorizedClient,
-                               ErrorDescription: "Client ID not registered",
-                               State:            form.State,
-                       }, "")
-                       return
-               }
-               ctx.ServerError("GetOAuth2ApplicationByClientID", err)
-               return
-       }
-       if err := app.LoadUser(); err != nil {
-               ctx.ServerError("LoadUser", err)
-               return
-       }
-
-       if !app.ContainsRedirectURI(form.RedirectURI) {
-               handleAuthorizeError(ctx, AuthorizeError{
-                       ErrorCode:        ErrorCodeInvalidRequest,
-                       ErrorDescription: "Unregistered Redirect URI",
-                       State:            form.State,
-               }, "")
-               return
-       }
-
-       if form.ResponseType != "code" {
-               handleAuthorizeError(ctx, AuthorizeError{
-                       ErrorCode:        ErrorCodeUnsupportedResponseType,
-                       ErrorDescription: "Only code response type is supported.",
-                       State:            form.State,
-               }, form.RedirectURI)
-               return
-       }
-
-       // pkce support
-       switch form.CodeChallengeMethod {
-       case "S256":
-       case "plain":
-               if err := ctx.Session.Set("CodeChallengeMethod", form.CodeChallengeMethod); err != nil {
-                       handleAuthorizeError(ctx, AuthorizeError{
-                               ErrorCode:        ErrorCodeServerError,
-                               ErrorDescription: "cannot set code challenge method",
-                               State:            form.State,
-                       }, form.RedirectURI)
-                       return
-               }
-               if err := ctx.Session.Set("CodeChallengeMethod", form.CodeChallenge); err != nil {
-                       handleAuthorizeError(ctx, AuthorizeError{
-                               ErrorCode:        ErrorCodeServerError,
-                               ErrorDescription: "cannot set code challenge",
-                               State:            form.State,
-                       }, form.RedirectURI)
-                       return
-               }
-               // Here we're just going to try to release the session early
-               if err := ctx.Session.Release(); err != nil {
-                       // we'll tolerate errors here as they *should* get saved elsewhere
-                       log.Error("Unable to save changes to the session: %v", err)
-               }
-       case "":
-               break
-       default:
-               handleAuthorizeError(ctx, AuthorizeError{
-                       ErrorCode:        ErrorCodeInvalidRequest,
-                       ErrorDescription: "unsupported code challenge method",
-                       State:            form.State,
-               }, form.RedirectURI)
-               return
-       }
-
-       grant, err := app.GetGrantByUserID(ctx.User.ID)
-       if err != nil {
-               handleServerError(ctx, form.State, form.RedirectURI)
-               return
-       }
-
-       // Redirect if user already granted access
-       if grant != nil {
-               code, err := grant.GenerateNewAuthorizationCode(form.RedirectURI, form.CodeChallenge, form.CodeChallengeMethod)
-               if err != nil {
-                       handleServerError(ctx, form.State, form.RedirectURI)
-                       return
-               }
-               redirect, err := code.GenerateRedirectURI(form.State)
-               if err != nil {
-                       handleServerError(ctx, form.State, form.RedirectURI)
-                       return
-               }
-               // Update nonce to reflect the new session
-               if len(form.Nonce) > 0 {
-                       err := grant.SetNonce(form.Nonce)
-                       if err != nil {
-                               log.Error("Unable to update nonce: %v", err)
-                       }
-               }
-               ctx.Redirect(redirect.String(), 302)
-               return
-       }
-
-       // show authorize page to grant access
-       ctx.Data["Application"] = app
-       ctx.Data["RedirectURI"] = form.RedirectURI
-       ctx.Data["State"] = form.State
-       ctx.Data["Scope"] = form.Scope
-       ctx.Data["Nonce"] = form.Nonce
-       ctx.Data["ApplicationUserLink"] = "<a href=\"" + html.EscapeString(setting.AppURL) + html.EscapeString(url.PathEscape(app.User.LowerName)) + "\">@" + html.EscapeString(app.User.Name) + "</a>"
-       ctx.Data["ApplicationRedirectDomainHTML"] = "<strong>" + html.EscapeString(form.RedirectURI) + "</strong>"
-       // TODO document SESSION <=> FORM
-       err = ctx.Session.Set("client_id", app.ClientID)
-       if err != nil {
-               handleServerError(ctx, form.State, form.RedirectURI)
-               log.Error(err.Error())
-               return
-       }
-       err = ctx.Session.Set("redirect_uri", form.RedirectURI)
-       if err != nil {
-               handleServerError(ctx, form.State, form.RedirectURI)
-               log.Error(err.Error())
-               return
-       }
-       err = ctx.Session.Set("state", form.State)
-       if err != nil {
-               handleServerError(ctx, form.State, form.RedirectURI)
-               log.Error(err.Error())
-               return
-       }
-       // Here we're just going to try to release the session early
-       if err := ctx.Session.Release(); err != nil {
-               // we'll tolerate errors here as they *should* get saved elsewhere
-               log.Error("Unable to save changes to the session: %v", err)
-       }
-       ctx.HTML(http.StatusOK, tplGrantAccess)
-}
-
-// GrantApplicationOAuth manages the post request submitted when a user grants access to an application
-func GrantApplicationOAuth(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.GrantApplicationForm)
-       if ctx.Session.Get("client_id") != form.ClientID || ctx.Session.Get("state") != form.State ||
-               ctx.Session.Get("redirect_uri") != form.RedirectURI {
-               ctx.Error(http.StatusBadRequest)
-               return
-       }
-       app, err := models.GetOAuth2ApplicationByClientID(form.ClientID)
-       if err != nil {
-               ctx.ServerError("GetOAuth2ApplicationByClientID", err)
-               return
-       }
-       grant, err := app.CreateGrant(ctx.User.ID, form.Scope)
-       if err != nil {
-               handleAuthorizeError(ctx, AuthorizeError{
-                       State:            form.State,
-                       ErrorDescription: "cannot create grant for user",
-                       ErrorCode:        ErrorCodeServerError,
-               }, form.RedirectURI)
-               return
-       }
-       if len(form.Nonce) > 0 {
-               err := grant.SetNonce(form.Nonce)
-               if err != nil {
-                       log.Error("Unable to update nonce: %v", err)
-               }
-       }
-
-       var codeChallenge, codeChallengeMethod string
-       codeChallenge, _ = ctx.Session.Get("CodeChallenge").(string)
-       codeChallengeMethod, _ = ctx.Session.Get("CodeChallengeMethod").(string)
-
-       code, err := grant.GenerateNewAuthorizationCode(form.RedirectURI, codeChallenge, codeChallengeMethod)
-       if err != nil {
-               handleServerError(ctx, form.State, form.RedirectURI)
-               return
-       }
-       redirect, err := code.GenerateRedirectURI(form.State)
-       if err != nil {
-               handleServerError(ctx, form.State, form.RedirectURI)
-               return
-       }
-       ctx.Redirect(redirect.String(), 302)
-}
-
-// OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
-func OIDCWellKnown(ctx *context.Context) {
-       t := ctx.Render.TemplateLookup("user/auth/oidc_wellknown")
-       ctx.Resp.Header().Set("Content-Type", "application/json")
-       if err := t.Execute(ctx.Resp, ctx.Data); err != nil {
-               log.Error("%v", err)
-               ctx.Error(http.StatusInternalServerError)
-       }
-}
-
-// AccessTokenOAuth manages all access token requests by the client
-func AccessTokenOAuth(ctx *context.Context) {
-       form := *web.GetForm(ctx).(*forms.AccessTokenForm)
-       if form.ClientID == "" {
-               authHeader := ctx.Req.Header.Get("Authorization")
-               authContent := strings.SplitN(authHeader, " ", 2)
-               if len(authContent) == 2 && authContent[0] == "Basic" {
-                       payload, err := base64.StdEncoding.DecodeString(authContent[1])
-                       if err != nil {
-                               handleAccessTokenError(ctx, AccessTokenError{
-                                       ErrorCode:        AccessTokenErrorCodeInvalidRequest,
-                                       ErrorDescription: "cannot parse basic auth header",
-                               })
-                               return
-                       }
-                       pair := strings.SplitN(string(payload), ":", 2)
-                       if len(pair) != 2 {
-                               handleAccessTokenError(ctx, AccessTokenError{
-                                       ErrorCode:        AccessTokenErrorCodeInvalidRequest,
-                                       ErrorDescription: "cannot parse basic auth header",
-                               })
-                               return
-                       }
-                       form.ClientID = pair[0]
-                       form.ClientSecret = pair[1]
-               }
-       }
-       switch form.GrantType {
-       case "refresh_token":
-               handleRefreshToken(ctx, form)
-               return
-       case "authorization_code":
-               handleAuthorizationCode(ctx, form)
-               return
-       default:
-               handleAccessTokenError(ctx, AccessTokenError{
-                       ErrorCode:        AccessTokenErrorCodeUnsupportedGrantType,
-                       ErrorDescription: "Only refresh_token or authorization_code grant type is supported",
-               })
-       }
-}
-
-func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm) {
-       token, err := models.ParseOAuth2Token(form.RefreshToken)
-       if err != nil {
-               handleAccessTokenError(ctx, AccessTokenError{
-                       ErrorCode:        AccessTokenErrorCodeUnauthorizedClient,
-                       ErrorDescription: "client is not authorized",
-               })
-               return
-       }
-       // get grant before increasing counter
-       grant, err := models.GetOAuth2GrantByID(token.GrantID)
-       if err != nil || grant == nil {
-               handleAccessTokenError(ctx, AccessTokenError{
-                       ErrorCode:        AccessTokenErrorCodeInvalidGrant,
-                       ErrorDescription: "grant does not exist",
-               })
-               return
-       }
-
-       // check if token got already used
-       if setting.OAuth2.InvalidateRefreshTokens && (grant.Counter != token.Counter || token.Counter == 0) {
-               handleAccessTokenError(ctx, AccessTokenError{
-                       ErrorCode:        AccessTokenErrorCodeUnauthorizedClient,
-                       ErrorDescription: "token was already used",
-               })
-               log.Warn("A client tried to use a refresh token for grant_id = %d was used twice!", grant.ID)
-               return
-       }
-       accessToken, tokenErr := newAccessTokenResponse(grant, form.ClientSecret)
-       if tokenErr != nil {
-               handleAccessTokenError(ctx, *tokenErr)
-               return
-       }
-       ctx.JSON(http.StatusOK, accessToken)
-}
-
-func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm) {
-       app, err := models.GetOAuth2ApplicationByClientID(form.ClientID)
-       if err != nil {
-               handleAccessTokenError(ctx, AccessTokenError{
-                       ErrorCode:        AccessTokenErrorCodeInvalidClient,
-                       ErrorDescription: fmt.Sprintf("cannot load client with client id: '%s'", form.ClientID),
-               })
-               return
-       }
-       if !app.ValidateClientSecret([]byte(form.ClientSecret)) {
-               handleAccessTokenError(ctx, AccessTokenError{
-                       ErrorCode:        AccessTokenErrorCodeUnauthorizedClient,
-                       ErrorDescription: "client is not authorized",
-               })
-               return
-       }
-       if form.RedirectURI != "" && !app.ContainsRedirectURI(form.RedirectURI) {
-               handleAccessTokenError(ctx, AccessTokenError{
-                       ErrorCode:        AccessTokenErrorCodeUnauthorizedClient,
-                       ErrorDescription: "client is not authorized",
-               })
-               return
-       }
-       authorizationCode, err := models.GetOAuth2AuthorizationByCode(form.Code)
-       if err != nil || authorizationCode == nil {
-               handleAccessTokenError(ctx, AccessTokenError{
-                       ErrorCode:        AccessTokenErrorCodeUnauthorizedClient,
-                       ErrorDescription: "client is not authorized",
-               })
-               return
-       }
-       // check if code verifier authorizes the client, PKCE support
-       if !authorizationCode.ValidateCodeChallenge(form.CodeVerifier) {
-               handleAccessTokenError(ctx, AccessTokenError{
-                       ErrorCode:        AccessTokenErrorCodeUnauthorizedClient,
-                       ErrorDescription: "client is not authorized",
-               })
-               return
-       }
-       // check if granted for this application
-       if authorizationCode.Grant.ApplicationID != app.ID {
-               handleAccessTokenError(ctx, AccessTokenError{
-                       ErrorCode:        AccessTokenErrorCodeInvalidGrant,
-                       ErrorDescription: "invalid grant",
-               })
-               return
-       }
-       // remove token from database to deny duplicate usage
-       if err := authorizationCode.Invalidate(); err != nil {
-               handleAccessTokenError(ctx, AccessTokenError{
-                       ErrorCode:        AccessTokenErrorCodeInvalidRequest,
-                       ErrorDescription: "cannot proceed your request",
-               })
-       }
-       resp, tokenErr := newAccessTokenResponse(authorizationCode.Grant, form.ClientSecret)
-       if tokenErr != nil {
-               handleAccessTokenError(ctx, *tokenErr)
-               return
-       }
-       // send successful response
-       ctx.JSON(http.StatusOK, resp)
-}
-
-func handleAccessTokenError(ctx *context.Context, acErr AccessTokenError) {
-       ctx.JSON(http.StatusBadRequest, acErr)
-}
-
-func handleServerError(ctx *context.Context, state string, redirectURI string) {
-       handleAuthorizeError(ctx, AuthorizeError{
-               ErrorCode:        ErrorCodeServerError,
-               ErrorDescription: "A server error occurred",
-               State:            state,
-       }, redirectURI)
-}
-
-func handleAuthorizeError(ctx *context.Context, authErr AuthorizeError, redirectURI string) {
-       if redirectURI == "" {
-               log.Warn("Authorization failed: %v", authErr.ErrorDescription)
-               ctx.Data["Error"] = authErr
-               ctx.HTML(400, tplGrantError)
-               return
-       }
-       redirect, err := url.Parse(redirectURI)
-       if err != nil {
-               ctx.ServerError("url.Parse", err)
-               return
-       }
-       q := redirect.Query()
-       q.Set("error", string(authErr.ErrorCode))
-       q.Set("error_description", authErr.ErrorDescription)
-       q.Set("state", authErr.State)
-       redirect.RawQuery = q.Encode()
-       ctx.Redirect(redirect.String(), 302)
-}
-
-func handleBearerTokenError(ctx *context.Context, beErr BearerTokenError) {
-       ctx.Resp.Header().Set("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"\", error=\"%s\", error_description=\"%s\"", beErr.ErrorCode, beErr.ErrorDescription))
-       switch beErr.ErrorCode {
-       case BearerTokenErrorCodeInvalidRequest:
-               ctx.JSON(http.StatusBadRequest, beErr)
-       case BearerTokenErrorCodeInvalidToken:
-               ctx.JSON(http.StatusUnauthorized, beErr)
-       case BearerTokenErrorCodeInsufficientScope:
-               ctx.JSON(http.StatusForbidden, beErr)
-       default:
-               log.Error("Invalid BearerTokenErrorCode: %v", beErr.ErrorCode)
-               ctx.ServerError("Unhandled BearerTokenError", fmt.Errorf("BearerTokenError: error=\"%v\", error_description=\"%v\"", beErr.ErrorCode, beErr.ErrorDescription))
-       }
-}
diff --git a/routers/user/profile.go b/routers/user/profile.go
deleted file mode 100644 (file)
index 8ff1ee2..0000000
+++ /dev/null
@@ -1,329 +0,0 @@
-// Copyright 2015 The Gogs Authors. All rights reserved.
-// Copyright 2019 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 user
-
-import (
-       "fmt"
-       "net/http"
-       "path"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/markup"
-       "code.gitea.io/gitea/modules/markup/markdown"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/util"
-       "code.gitea.io/gitea/routers/org"
-)
-
-// GetUserByName get user by name
-func GetUserByName(ctx *context.Context, name string) *models.User {
-       user, err := models.GetUserByName(name)
-       if err != nil {
-               if models.IsErrUserNotExist(err) {
-                       if redirectUserID, err := models.LookupUserRedirect(name); err == nil {
-                               context.RedirectToUser(ctx, name, redirectUserID)
-                       } else {
-                               ctx.NotFound("GetUserByName", err)
-                       }
-               } else {
-                       ctx.ServerError("GetUserByName", err)
-               }
-               return nil
-       }
-       return user
-}
-
-// GetUserByParams returns user whose name is presented in URL paramenter.
-func GetUserByParams(ctx *context.Context) *models.User {
-       return GetUserByName(ctx, ctx.Params(":username"))
-}
-
-// Profile render user's profile page
-func Profile(ctx *context.Context) {
-       uname := ctx.Params(":username")
-
-       // Special handle for FireFox requests favicon.ico.
-       if uname == "favicon.ico" {
-               ctx.ServeFile(path.Join(setting.StaticRootPath, "public/img/favicon.png"))
-               return
-       }
-
-       if strings.HasSuffix(uname, ".png") {
-               ctx.Error(http.StatusNotFound)
-               return
-       }
-
-       isShowKeys := false
-       if strings.HasSuffix(uname, ".keys") {
-               isShowKeys = true
-               uname = strings.TrimSuffix(uname, ".keys")
-       }
-
-       isShowGPG := false
-       if strings.HasSuffix(uname, ".gpg") {
-               isShowGPG = true
-               uname = strings.TrimSuffix(uname, ".gpg")
-       }
-
-       ctxUser := GetUserByName(ctx, uname)
-       if ctx.Written() {
-               return
-       }
-
-       // Show SSH keys.
-       if isShowKeys {
-               ShowSSHKeys(ctx, ctxUser.ID)
-               return
-       }
-
-       // Show GPG keys.
-       if isShowGPG {
-               ShowGPGKeys(ctx, ctxUser.ID)
-               return
-       }
-
-       if ctxUser.IsOrganization() {
-               org.Home(ctx)
-               return
-       }
-
-       // Show OpenID URIs
-       openIDs, err := models.GetUserOpenIDs(ctxUser.ID)
-       if err != nil {
-               ctx.ServerError("GetUserOpenIDs", err)
-               return
-       }
-
-       ctx.Data["Title"] = ctxUser.DisplayName()
-       ctx.Data["PageIsUserProfile"] = true
-       ctx.Data["Owner"] = ctxUser
-       ctx.Data["OpenIDs"] = openIDs
-
-       if setting.Service.EnableUserHeatmap {
-               data, err := models.GetUserHeatmapDataByUser(ctxUser, ctx.User)
-               if err != nil {
-                       ctx.ServerError("GetUserHeatmapDataByUser", err)
-                       return
-               }
-               ctx.Data["HeatmapData"] = data
-       }
-
-       if len(ctxUser.Description) != 0 {
-               content, err := markdown.RenderString(&markup.RenderContext{
-                       URLPrefix: ctx.Repo.RepoLink,
-                       Metas:     map[string]string{"mode": "document"},
-               }, ctxUser.Description)
-               if err != nil {
-                       ctx.ServerError("RenderString", err)
-                       return
-               }
-               ctx.Data["RenderedDescription"] = content
-       }
-
-       showPrivate := ctx.IsSigned && (ctx.User.IsAdmin || ctx.User.ID == ctxUser.ID)
-
-       orgs, err := models.GetOrgsByUserID(ctxUser.ID, showPrivate)
-       if err != nil {
-               ctx.ServerError("GetOrgsByUserIDDesc", err)
-               return
-       }
-
-       ctx.Data["Orgs"] = orgs
-       ctx.Data["HasOrgsVisible"] = models.HasOrgsVisible(orgs, ctx.User)
-
-       tab := ctx.Query("tab")
-       ctx.Data["TabName"] = tab
-
-       page := ctx.QueryInt("page")
-       if page <= 0 {
-               page = 1
-       }
-
-       topicOnly := ctx.QueryBool("topic")
-
-       var (
-               repos   []*models.Repository
-               count   int64
-               total   int
-               orderBy models.SearchOrderBy
-       )
-
-       ctx.Data["SortType"] = ctx.Query("sort")
-       switch ctx.Query("sort") {
-       case "newest":
-               orderBy = models.SearchOrderByNewest
-       case "oldest":
-               orderBy = models.SearchOrderByOldest
-       case "recentupdate":
-               orderBy = models.SearchOrderByRecentUpdated
-       case "leastupdate":
-               orderBy = models.SearchOrderByLeastUpdated
-       case "reversealphabetically":
-               orderBy = models.SearchOrderByAlphabeticallyReverse
-       case "alphabetically":
-               orderBy = models.SearchOrderByAlphabetically
-       case "moststars":
-               orderBy = models.SearchOrderByStarsReverse
-       case "feweststars":
-               orderBy = models.SearchOrderByStars
-       case "mostforks":
-               orderBy = models.SearchOrderByForksReverse
-       case "fewestforks":
-               orderBy = models.SearchOrderByForks
-       default:
-               ctx.Data["SortType"] = "recentupdate"
-               orderBy = models.SearchOrderByRecentUpdated
-       }
-
-       keyword := strings.Trim(ctx.Query("q"), " ")
-       ctx.Data["Keyword"] = keyword
-       switch tab {
-       case "followers":
-               items, err := ctxUser.GetFollowers(models.ListOptions{
-                       PageSize: setting.UI.User.RepoPagingNum,
-                       Page:     page,
-               })
-               if err != nil {
-                       ctx.ServerError("GetFollowers", err)
-                       return
-               }
-               ctx.Data["Cards"] = items
-
-               total = ctxUser.NumFollowers
-       case "following":
-               items, err := ctxUser.GetFollowing(models.ListOptions{
-                       PageSize: setting.UI.User.RepoPagingNum,
-                       Page:     page,
-               })
-               if err != nil {
-                       ctx.ServerError("GetFollowing", err)
-                       return
-               }
-               ctx.Data["Cards"] = items
-
-               total = ctxUser.NumFollowing
-       case "activity":
-               retrieveFeeds(ctx, models.GetFeedsOptions{RequestedUser: ctxUser,
-                       Actor:           ctx.User,
-                       IncludePrivate:  showPrivate,
-                       OnlyPerformedBy: true,
-                       IncludeDeleted:  false,
-                       Date:            ctx.Query("date"),
-               })
-               if ctx.Written() {
-                       return
-               }
-       case "stars":
-               ctx.Data["PageIsProfileStarList"] = true
-               repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
-                       ListOptions: models.ListOptions{
-                               PageSize: setting.UI.User.RepoPagingNum,
-                               Page:     page,
-                       },
-                       Actor:              ctx.User,
-                       Keyword:            keyword,
-                       OrderBy:            orderBy,
-                       Private:            ctx.IsSigned,
-                       StarredByID:        ctxUser.ID,
-                       Collaborate:        util.OptionalBoolFalse,
-                       TopicOnly:          topicOnly,
-                       IncludeDescription: setting.UI.SearchRepoDescription,
-               })
-               if err != nil {
-                       ctx.ServerError("SearchRepository", err)
-                       return
-               }
-
-               total = int(count)
-       case "projects":
-               ctx.Data["OpenProjects"], _, err = models.GetProjects(models.ProjectSearchOptions{
-                       Page:     -1,
-                       IsClosed: util.OptionalBoolFalse,
-                       Type:     models.ProjectTypeIndividual,
-               })
-               if err != nil {
-                       ctx.ServerError("GetProjects", err)
-                       return
-               }
-       case "watching":
-               repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
-                       ListOptions: models.ListOptions{
-                               PageSize: setting.UI.User.RepoPagingNum,
-                               Page:     page,
-                       },
-                       Actor:              ctx.User,
-                       Keyword:            keyword,
-                       OrderBy:            orderBy,
-                       Private:            ctx.IsSigned,
-                       WatchedByID:        ctxUser.ID,
-                       Collaborate:        util.OptionalBoolFalse,
-                       TopicOnly:          topicOnly,
-                       IncludeDescription: setting.UI.SearchRepoDescription,
-               })
-               if err != nil {
-                       ctx.ServerError("SearchRepository", err)
-                       return
-               }
-
-               total = int(count)
-       default:
-               repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
-                       ListOptions: models.ListOptions{
-                               PageSize: setting.UI.User.RepoPagingNum,
-                               Page:     page,
-                       },
-                       Actor:              ctx.User,
-                       Keyword:            keyword,
-                       OwnerID:            ctxUser.ID,
-                       OrderBy:            orderBy,
-                       Private:            ctx.IsSigned,
-                       Collaborate:        util.OptionalBoolFalse,
-                       TopicOnly:          topicOnly,
-                       IncludeDescription: setting.UI.SearchRepoDescription,
-               })
-               if err != nil {
-                       ctx.ServerError("SearchRepository", err)
-                       return
-               }
-
-               total = int(count)
-       }
-       ctx.Data["Repos"] = repos
-       ctx.Data["Total"] = total
-
-       pager := context.NewPagination(total, setting.UI.User.RepoPagingNum, page, 5)
-       pager.SetDefaultParams(ctx)
-       ctx.Data["Page"] = pager
-
-       ctx.Data["ShowUserEmail"] = len(ctxUser.Email) > 0 && ctx.IsSigned && (!ctxUser.KeepEmailPrivate || ctxUser.ID == ctx.User.ID)
-
-       ctx.HTML(http.StatusOK, tplProfile)
-}
-
-// Action response for follow/unfollow user request
-func Action(ctx *context.Context) {
-       u := GetUserByParams(ctx)
-       if ctx.Written() {
-               return
-       }
-
-       var err error
-       switch ctx.Params(":action") {
-       case "follow":
-               err = models.FollowUser(ctx.User.ID, u.ID)
-       case "unfollow":
-               err = models.UnfollowUser(ctx.User.ID, u.ID)
-       }
-
-       if err != nil {
-               ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err)
-               return
-       }
-
-       ctx.RedirectToFirst(ctx.Query("redirect_to"), u.HomeLink())
-}
diff --git a/routers/user/setting/account.go b/routers/user/setting/account.go
deleted file mode 100644 (file)
index 48ab37d..0000000
+++ /dev/null
@@ -1,313 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2018 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 setting
-
-import (
-       "errors"
-       "net/http"
-       "time"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/password"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/timeutil"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-       "code.gitea.io/gitea/services/mailer"
-)
-
-const (
-       tplSettingsAccount base.TplName = "user/settings/account"
-)
-
-// Account renders change user's password, user's email and user suicide page
-func Account(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("settings")
-       ctx.Data["PageIsSettingsAccount"] = true
-       ctx.Data["Email"] = ctx.User.Email
-
-       loadAccountData(ctx)
-
-       ctx.HTML(http.StatusOK, tplSettingsAccount)
-}
-
-// AccountPost response for change user's password
-func AccountPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.ChangePasswordForm)
-       ctx.Data["Title"] = ctx.Tr("settings")
-       ctx.Data["PageIsSettingsAccount"] = true
-
-       if ctx.HasError() {
-               loadAccountData(ctx)
-
-               ctx.HTML(http.StatusOK, tplSettingsAccount)
-               return
-       }
-
-       if len(form.Password) < setting.MinPasswordLength {
-               ctx.Flash.Error(ctx.Tr("auth.password_too_short", setting.MinPasswordLength))
-       } else if ctx.User.IsPasswordSet() && !ctx.User.ValidatePassword(form.OldPassword) {
-               ctx.Flash.Error(ctx.Tr("settings.password_incorrect"))
-       } else if form.Password != form.Retype {
-               ctx.Flash.Error(ctx.Tr("form.password_not_match"))
-       } else if !password.IsComplexEnough(form.Password) {
-               ctx.Flash.Error(password.BuildComplexityError(ctx))
-       } else if pwned, err := password.IsPwned(ctx, form.Password); pwned || err != nil {
-               errMsg := ctx.Tr("auth.password_pwned")
-               if err != nil {
-                       log.Error(err.Error())
-                       errMsg = ctx.Tr("auth.password_pwned_err")
-               }
-               ctx.Flash.Error(errMsg)
-       } else {
-               var err error
-               if err = ctx.User.SetPassword(form.Password); err != nil {
-                       ctx.ServerError("UpdateUser", err)
-                       return
-               }
-               if err := models.UpdateUserCols(ctx.User, "salt", "passwd_hash_algo", "passwd"); err != nil {
-                       ctx.ServerError("UpdateUser", err)
-                       return
-               }
-               log.Trace("User password updated: %s", ctx.User.Name)
-               ctx.Flash.Success(ctx.Tr("settings.change_password_success"))
-       }
-
-       ctx.Redirect(setting.AppSubURL + "/user/settings/account")
-}
-
-// EmailPost response for change user's email
-func EmailPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.AddEmailForm)
-       ctx.Data["Title"] = ctx.Tr("settings")
-       ctx.Data["PageIsSettingsAccount"] = true
-
-       // Make emailaddress primary.
-       if ctx.Query("_method") == "PRIMARY" {
-               if err := models.MakeEmailPrimary(&models.EmailAddress{ID: ctx.QueryInt64("id")}); err != nil {
-                       ctx.ServerError("MakeEmailPrimary", err)
-                       return
-               }
-
-               log.Trace("Email made primary: %s", ctx.User.Name)
-               ctx.Redirect(setting.AppSubURL + "/user/settings/account")
-               return
-       }
-       // Send activation Email
-       if ctx.Query("_method") == "SENDACTIVATION" {
-               var address string
-               if ctx.Cache.IsExist("MailResendLimit_" + ctx.User.LowerName) {
-                       log.Error("Send activation: activation still pending")
-                       ctx.Redirect(setting.AppSubURL + "/user/settings/account")
-                       return
-               }
-               if ctx.Query("id") == "PRIMARY" {
-                       if ctx.User.IsActive {
-                               log.Error("Send activation: email not set for activation")
-                               ctx.Redirect(setting.AppSubURL + "/user/settings/account")
-                               return
-                       }
-                       mailer.SendActivateAccountMail(ctx.Locale, ctx.User)
-                       address = ctx.User.Email
-               } else {
-                       id := ctx.QueryInt64("id")
-                       email, err := models.GetEmailAddressByID(ctx.User.ID, id)
-                       if err != nil {
-                               log.Error("GetEmailAddressByID(%d,%d) error: %v", ctx.User.ID, id, err)
-                               ctx.Redirect(setting.AppSubURL + "/user/settings/account")
-                               return
-                       }
-                       if email == nil {
-                               log.Error("Send activation: EmailAddress not found; user:%d, id: %d", ctx.User.ID, id)
-                               ctx.Redirect(setting.AppSubURL + "/user/settings/account")
-                               return
-                       }
-                       if email.IsActivated {
-                               log.Error("Send activation: email not set for activation")
-                               ctx.Redirect(setting.AppSubURL + "/user/settings/account")
-                               return
-                       }
-                       mailer.SendActivateEmailMail(ctx.User, email)
-                       address = email.Email
-               }
-
-               if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
-                       log.Error("Set cache(MailResendLimit) fail: %v", err)
-               }
-               ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", address, timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())))
-               ctx.Redirect(setting.AppSubURL + "/user/settings/account")
-               return
-       }
-       // Set Email Notification Preference
-       if ctx.Query("_method") == "NOTIFICATION" {
-               preference := ctx.Query("preference")
-               if !(preference == models.EmailNotificationsEnabled ||
-                       preference == models.EmailNotificationsOnMention ||
-                       preference == models.EmailNotificationsDisabled) {
-                       log.Error("Email notifications preference change returned unrecognized option %s: %s", preference, ctx.User.Name)
-                       ctx.ServerError("SetEmailPreference", errors.New("option unrecognized"))
-                       return
-               }
-               if err := ctx.User.SetEmailNotifications(preference); err != nil {
-                       log.Error("Set Email Notifications failed: %v", err)
-                       ctx.ServerError("SetEmailNotifications", err)
-                       return
-               }
-               log.Trace("Email notifications preference made %s: %s", preference, ctx.User.Name)
-               ctx.Flash.Success(ctx.Tr("settings.email_preference_set_success"))
-               ctx.Redirect(setting.AppSubURL + "/user/settings/account")
-               return
-       }
-
-       if ctx.HasError() {
-               loadAccountData(ctx)
-
-               ctx.HTML(http.StatusOK, tplSettingsAccount)
-               return
-       }
-
-       email := &models.EmailAddress{
-               UID:         ctx.User.ID,
-               Email:       form.Email,
-               IsActivated: !setting.Service.RegisterEmailConfirm,
-       }
-       if err := models.AddEmailAddress(email); err != nil {
-               if models.IsErrEmailAlreadyUsed(err) {
-                       loadAccountData(ctx)
-
-                       ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form)
-                       return
-               } else if models.IsErrEmailInvalid(err) {
-                       loadAccountData(ctx)
-
-                       ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsAccount, &form)
-                       return
-               }
-               ctx.ServerError("AddEmailAddress", err)
-               return
-       }
-
-       // Send confirmation email
-       if setting.Service.RegisterEmailConfirm {
-               mailer.SendActivateEmailMail(ctx.User, email)
-               if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
-                       log.Error("Set cache(MailResendLimit) fail: %v", err)
-               }
-               ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", email.Email, timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())))
-       } else {
-               ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
-       }
-
-       log.Trace("Email address added: %s", email.Email)
-       ctx.Redirect(setting.AppSubURL + "/user/settings/account")
-}
-
-// DeleteEmail response for delete user's email
-func DeleteEmail(ctx *context.Context) {
-       if err := models.DeleteEmailAddress(&models.EmailAddress{ID: ctx.QueryInt64("id"), UID: ctx.User.ID}); err != nil {
-               ctx.ServerError("DeleteEmail", err)
-               return
-       }
-       log.Trace("Email address deleted: %s", ctx.User.Name)
-
-       ctx.Flash.Success(ctx.Tr("settings.email_deletion_success"))
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": setting.AppSubURL + "/user/settings/account",
-       })
-}
-
-// DeleteAccount render user suicide page and response for delete user himself
-func DeleteAccount(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("settings")
-       ctx.Data["PageIsSettingsAccount"] = true
-
-       if _, err := models.UserSignIn(ctx.User.Name, ctx.Query("password")); err != nil {
-               if models.IsErrUserNotExist(err) {
-                       loadAccountData(ctx)
-
-                       ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_password"), tplSettingsAccount, nil)
-               } else {
-                       ctx.ServerError("UserSignIn", err)
-               }
-               return
-       }
-
-       if err := models.DeleteUser(ctx.User); err != nil {
-               switch {
-               case models.IsErrUserOwnRepos(err):
-                       ctx.Flash.Error(ctx.Tr("form.still_own_repo"))
-                       ctx.Redirect(setting.AppSubURL + "/user/settings/account")
-               case models.IsErrUserHasOrgs(err):
-                       ctx.Flash.Error(ctx.Tr("form.still_has_org"))
-                       ctx.Redirect(setting.AppSubURL + "/user/settings/account")
-               default:
-                       ctx.ServerError("DeleteUser", err)
-               }
-       } else {
-               log.Trace("Account deleted: %s", ctx.User.Name)
-               ctx.Redirect(setting.AppSubURL + "/")
-       }
-}
-
-// UpdateUIThemePost is used to update users' specific theme
-func UpdateUIThemePost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.UpdateThemeForm)
-       ctx.Data["Title"] = ctx.Tr("settings")
-       ctx.Data["PageIsSettingsAccount"] = true
-
-       if ctx.HasError() {
-               ctx.Redirect(setting.AppSubURL + "/user/settings/account")
-               return
-       }
-
-       if !form.IsThemeExists() {
-               ctx.Flash.Error(ctx.Tr("settings.theme_update_error"))
-               ctx.Redirect(setting.AppSubURL + "/user/settings/account")
-               return
-       }
-
-       if err := ctx.User.UpdateTheme(form.Theme); err != nil {
-               ctx.Flash.Error(ctx.Tr("settings.theme_update_error"))
-               ctx.Redirect(setting.AppSubURL + "/user/settings/account")
-               return
-       }
-
-       log.Trace("Update user theme: %s", ctx.User.Name)
-       ctx.Flash.Success(ctx.Tr("settings.theme_update_success"))
-       ctx.Redirect(setting.AppSubURL + "/user/settings/account")
-}
-
-func loadAccountData(ctx *context.Context) {
-       emlist, err := models.GetEmailAddresses(ctx.User.ID)
-       if err != nil {
-               ctx.ServerError("GetEmailAddresses", err)
-               return
-       }
-       type UserEmail struct {
-               models.EmailAddress
-               CanBePrimary bool
-       }
-       pendingActivation := ctx.Cache.IsExist("MailResendLimit_" + ctx.User.LowerName)
-       emails := make([]*UserEmail, len(emlist))
-       for i, em := range emlist {
-               var email UserEmail
-               email.EmailAddress = *em
-               email.CanBePrimary = em.IsActivated
-               emails[i] = &email
-       }
-       ctx.Data["Emails"] = emails
-       ctx.Data["EmailNotificationsPreference"] = ctx.User.EmailNotifications()
-       ctx.Data["ActivationsPending"] = pendingActivation
-       ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm
-
-       if setting.Service.UserDeleteWithCommentsMaxTime != 0 {
-               ctx.Data["UserDeleteWithCommentsMaxTime"] = setting.Service.UserDeleteWithCommentsMaxTime.String()
-               ctx.Data["UserDeleteWithComments"] = ctx.User.CreatedUnix.AsTime().Add(setting.Service.UserDeleteWithCommentsMaxTime).After(time.Now())
-       }
-}
diff --git a/routers/user/setting/account_test.go b/routers/user/setting/account_test.go
deleted file mode 100644 (file)
index 25b68da..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-// Copyright 2018 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 setting
-
-import (
-       "net/http"
-       "testing"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/test"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-
-       "github.com/stretchr/testify/assert"
-)
-
-func TestChangePassword(t *testing.T) {
-       oldPassword := "password"
-       setting.MinPasswordLength = 6
-       var pcALL = []string{"lower", "upper", "digit", "spec"}
-       var pcLUN = []string{"lower", "upper", "digit"}
-       var pcLU = []string{"lower", "upper"}
-
-       for _, req := range []struct {
-               OldPassword        string
-               NewPassword        string
-               Retype             string
-               Message            string
-               PasswordComplexity []string
-       }{
-               {
-                       OldPassword:        oldPassword,
-                       NewPassword:        "Qwerty123456-",
-                       Retype:             "Qwerty123456-",
-                       Message:            "",
-                       PasswordComplexity: pcALL,
-               },
-               {
-                       OldPassword:        oldPassword,
-                       NewPassword:        "12345",
-                       Retype:             "12345",
-                       Message:            "auth.password_too_short",
-                       PasswordComplexity: pcALL,
-               },
-               {
-                       OldPassword:        "12334",
-                       NewPassword:        "123456",
-                       Retype:             "123456",
-                       Message:            "settings.password_incorrect",
-                       PasswordComplexity: pcALL,
-               },
-               {
-                       OldPassword:        oldPassword,
-                       NewPassword:        "123456",
-                       Retype:             "12345",
-                       Message:            "form.password_not_match",
-                       PasswordComplexity: pcALL,
-               },
-               {
-                       OldPassword:        oldPassword,
-                       NewPassword:        "Qwerty",
-                       Retype:             "Qwerty",
-                       Message:            "form.password_complexity",
-                       PasswordComplexity: pcALL,
-               },
-               {
-                       OldPassword:        oldPassword,
-                       NewPassword:        "Qwerty",
-                       Retype:             "Qwerty",
-                       Message:            "form.password_complexity",
-                       PasswordComplexity: pcLUN,
-               },
-               {
-                       OldPassword:        oldPassword,
-                       NewPassword:        "QWERTY",
-                       Retype:             "QWERTY",
-                       Message:            "form.password_complexity",
-                       PasswordComplexity: pcLU,
-               },
-       } {
-               models.PrepareTestEnv(t)
-               ctx := test.MockContext(t, "user/settings/security")
-               test.LoadUser(t, ctx, 2)
-               test.LoadRepo(t, ctx, 1)
-
-               web.SetForm(ctx, &forms.ChangePasswordForm{
-                       OldPassword: req.OldPassword,
-                       Password:    req.NewPassword,
-                       Retype:      req.Retype,
-               })
-               AccountPost(ctx)
-
-               assert.Contains(t, ctx.Flash.ErrorMsg, req.Message)
-               assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
-       }
-}
diff --git a/routers/user/setting/adopt.go b/routers/user/setting/adopt.go
deleted file mode 100644 (file)
index b2d9187..0000000
+++ /dev/null
@@ -1,64 +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 setting
-
-import (
-       "path/filepath"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/repository"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/util"
-)
-
-// AdoptOrDeleteRepository adopts or deletes a repository
-func AdoptOrDeleteRepository(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("settings")
-       ctx.Data["PageIsSettingsRepos"] = true
-       allowAdopt := ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories
-       ctx.Data["allowAdopt"] = allowAdopt
-       allowDelete := ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories
-       ctx.Data["allowDelete"] = allowDelete
-
-       dir := ctx.Query("id")
-       action := ctx.Query("action")
-
-       ctxUser := ctx.User
-       root := filepath.Join(models.UserPath(ctxUser.LowerName))
-
-       // check not a repo
-       has, err := models.IsRepositoryExist(ctxUser, dir)
-       if err != nil {
-               ctx.ServerError("IsRepositoryExist", err)
-               return
-       }
-
-       isDir, err := util.IsDir(filepath.Join(root, dir+".git"))
-       if err != nil {
-               ctx.ServerError("IsDir", err)
-               return
-       }
-       if has || !isDir {
-               // Fallthrough to failure mode
-       } else if action == "adopt" && allowAdopt {
-               if _, err := repository.AdoptRepository(ctxUser, ctxUser, models.CreateRepoOptions{
-                       Name:      dir,
-                       IsPrivate: true,
-               }); err != nil {
-                       ctx.ServerError("repository.AdoptRepository", err)
-                       return
-               }
-               ctx.Flash.Success(ctx.Tr("repo.adopt_preexisting_success", dir))
-       } else if action == "delete" && allowDelete {
-               if err := repository.DeleteUnadoptedRepository(ctxUser, ctxUser, dir); err != nil {
-                       ctx.ServerError("repository.AdoptRepository", err)
-                       return
-               }
-               ctx.Flash.Success(ctx.Tr("repo.delete_preexisting_success", dir))
-       }
-
-       ctx.Redirect(setting.AppSubURL + "/user/settings/repos")
-}
diff --git a/routers/user/setting/applications.go b/routers/user/setting/applications.go
deleted file mode 100644 (file)
index 4161efd..0000000
+++ /dev/null
@@ -1,106 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2018 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 setting
-
-import (
-       "net/http"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-)
-
-const (
-       tplSettingsApplications base.TplName = "user/settings/applications"
-)
-
-// Applications render manage access token page
-func Applications(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("settings")
-       ctx.Data["PageIsSettingsApplications"] = true
-
-       loadApplicationsData(ctx)
-
-       ctx.HTML(http.StatusOK, tplSettingsApplications)
-}
-
-// ApplicationsPost response for add user's access token
-func ApplicationsPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.NewAccessTokenForm)
-       ctx.Data["Title"] = ctx.Tr("settings")
-       ctx.Data["PageIsSettingsApplications"] = true
-
-       if ctx.HasError() {
-               loadApplicationsData(ctx)
-
-               ctx.HTML(http.StatusOK, tplSettingsApplications)
-               return
-       }
-
-       t := &models.AccessToken{
-               UID:  ctx.User.ID,
-               Name: form.Name,
-       }
-
-       exist, err := models.AccessTokenByNameExists(t)
-       if err != nil {
-               ctx.ServerError("AccessTokenByNameExists", err)
-               return
-       }
-       if exist {
-               ctx.Flash.Error(ctx.Tr("settings.generate_token_name_duplicate", t.Name))
-               ctx.Redirect(setting.AppSubURL + "/user/settings/applications")
-               return
-       }
-
-       if err := models.NewAccessToken(t); err != nil {
-               ctx.ServerError("NewAccessToken", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("settings.generate_token_success"))
-       ctx.Flash.Info(t.Token)
-
-       ctx.Redirect(setting.AppSubURL + "/user/settings/applications")
-}
-
-// DeleteApplication response for delete user access token
-func DeleteApplication(ctx *context.Context) {
-       if err := models.DeleteAccessTokenByID(ctx.QueryInt64("id"), ctx.User.ID); err != nil {
-               ctx.Flash.Error("DeleteAccessTokenByID: " + err.Error())
-       } else {
-               ctx.Flash.Success(ctx.Tr("settings.delete_token_success"))
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": setting.AppSubURL + "/user/settings/applications",
-       })
-}
-
-func loadApplicationsData(ctx *context.Context) {
-       tokens, err := models.ListAccessTokens(models.ListAccessTokensOptions{UserID: ctx.User.ID})
-       if err != nil {
-               ctx.ServerError("ListAccessTokens", err)
-               return
-       }
-       ctx.Data["Tokens"] = tokens
-       ctx.Data["EnableOAuth2"] = setting.OAuth2.Enable
-       if setting.OAuth2.Enable {
-               ctx.Data["Applications"], err = models.GetOAuth2ApplicationsByUserID(ctx.User.ID)
-               if err != nil {
-                       ctx.ServerError("GetOAuth2ApplicationsByUserID", err)
-                       return
-               }
-               ctx.Data["Grants"], err = models.GetOAuth2GrantsByUserID(ctx.User.ID)
-               if err != nil {
-                       ctx.ServerError("GetOAuth2GrantsByUserID", err)
-                       return
-               }
-       }
-}
diff --git a/routers/user/setting/keys.go b/routers/user/setting/keys.go
deleted file mode 100644 (file)
index e56a33a..0000000
+++ /dev/null
@@ -1,226 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2018 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 setting
-
-import (
-       "net/http"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-)
-
-const (
-       tplSettingsKeys base.TplName = "user/settings/keys"
-)
-
-// Keys render user's SSH/GPG public keys page
-func Keys(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("settings")
-       ctx.Data["PageIsSettingsKeys"] = true
-       ctx.Data["DisableSSH"] = setting.SSH.Disabled
-       ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer
-       ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled
-
-       loadKeysData(ctx)
-
-       ctx.HTML(http.StatusOK, tplSettingsKeys)
-}
-
-// KeysPost response for change user's SSH/GPG keys
-func KeysPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.AddKeyForm)
-       ctx.Data["Title"] = ctx.Tr("settings")
-       ctx.Data["PageIsSettingsKeys"] = true
-       ctx.Data["DisableSSH"] = setting.SSH.Disabled
-       ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer
-       ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled
-
-       if ctx.HasError() {
-               loadKeysData(ctx)
-
-               ctx.HTML(http.StatusOK, tplSettingsKeys)
-               return
-       }
-       switch form.Type {
-       case "principal":
-               content, err := models.CheckPrincipalKeyString(ctx.User, form.Content)
-               if err != nil {
-                       if models.IsErrSSHDisabled(err) {
-                               ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
-                       } else {
-                               ctx.Flash.Error(ctx.Tr("form.invalid_ssh_principal", err.Error()))
-                       }
-                       ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
-                       return
-               }
-               if _, err = models.AddPrincipalKey(ctx.User.ID, content, 0); err != nil {
-                       ctx.Data["HasPrincipalError"] = true
-                       switch {
-                       case models.IsErrKeyAlreadyExist(err), models.IsErrKeyNameAlreadyUsed(err):
-                               loadKeysData(ctx)
-
-                               ctx.Data["Err_Content"] = true
-                               ctx.RenderWithErr(ctx.Tr("settings.ssh_principal_been_used"), tplSettingsKeys, &form)
-                       default:
-                               ctx.ServerError("AddPrincipalKey", err)
-                       }
-                       return
-               }
-               ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content))
-               ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
-       case "gpg":
-               keys, err := models.AddGPGKey(ctx.User.ID, form.Content)
-               if err != nil {
-                       ctx.Data["HasGPGError"] = true
-                       switch {
-                       case models.IsErrGPGKeyParsing(err):
-                               ctx.Flash.Error(ctx.Tr("form.invalid_gpg_key", err.Error()))
-                               ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
-                       case models.IsErrGPGKeyIDAlreadyUsed(err):
-                               loadKeysData(ctx)
-
-                               ctx.Data["Err_Content"] = true
-                               ctx.RenderWithErr(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form)
-                       case models.IsErrGPGNoEmailFound(err):
-                               loadKeysData(ctx)
-
-                               ctx.Data["Err_Content"] = true
-                               ctx.RenderWithErr(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form)
-                       default:
-                               ctx.ServerError("AddPublicKey", err)
-                       }
-                       return
-               }
-               keyIDs := ""
-               for _, key := range keys {
-                       keyIDs += key.KeyID
-                       keyIDs += ", "
-               }
-               if len(keyIDs) > 0 {
-                       keyIDs = keyIDs[:len(keyIDs)-2]
-               }
-               ctx.Flash.Success(ctx.Tr("settings.add_gpg_key_success", keyIDs))
-               ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
-       case "ssh":
-               content, err := models.CheckPublicKeyString(form.Content)
-               if err != nil {
-                       if models.IsErrSSHDisabled(err) {
-                               ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
-                       } else if models.IsErrKeyUnableVerify(err) {
-                               ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
-                       } else {
-                               ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error()))
-                       }
-                       ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
-                       return
-               }
-
-               if _, err = models.AddPublicKey(ctx.User.ID, form.Title, content, 0); err != nil {
-                       ctx.Data["HasSSHError"] = true
-                       switch {
-                       case models.IsErrKeyAlreadyExist(err):
-                               loadKeysData(ctx)
-
-                               ctx.Data["Err_Content"] = true
-                               ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplSettingsKeys, &form)
-                       case models.IsErrKeyNameAlreadyUsed(err):
-                               loadKeysData(ctx)
-
-                               ctx.Data["Err_Title"] = true
-                               ctx.RenderWithErr(ctx.Tr("settings.ssh_key_name_used"), tplSettingsKeys, &form)
-                       case models.IsErrKeyUnableVerify(err):
-                               ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
-                               ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
-                       default:
-                               ctx.ServerError("AddPublicKey", err)
-                       }
-                       return
-               }
-               ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title))
-               ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
-
-       default:
-               ctx.Flash.Warning("Function not implemented")
-               ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
-       }
-
-}
-
-// DeleteKey response for delete user's SSH/GPG key
-func DeleteKey(ctx *context.Context) {
-
-       switch ctx.Query("type") {
-       case "gpg":
-               if err := models.DeleteGPGKey(ctx.User, ctx.QueryInt64("id")); err != nil {
-                       ctx.Flash.Error("DeleteGPGKey: " + err.Error())
-               } else {
-                       ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success"))
-               }
-       case "ssh":
-               keyID := ctx.QueryInt64("id")
-               external, err := models.PublicKeyIsExternallyManaged(keyID)
-               if err != nil {
-                       ctx.ServerError("sshKeysExternalManaged", err)
-                       return
-               }
-               if external {
-                       ctx.Flash.Error(ctx.Tr("setting.ssh_externally_managed"))
-                       ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
-                       return
-               }
-               if err := models.DeletePublicKey(ctx.User, keyID); err != nil {
-                       ctx.Flash.Error("DeletePublicKey: " + err.Error())
-               } else {
-                       ctx.Flash.Success(ctx.Tr("settings.ssh_key_deletion_success"))
-               }
-       case "principal":
-               if err := models.DeletePublicKey(ctx.User, ctx.QueryInt64("id")); err != nil {
-                       ctx.Flash.Error("DeletePublicKey: " + err.Error())
-               } else {
-                       ctx.Flash.Success(ctx.Tr("settings.ssh_principal_deletion_success"))
-               }
-       default:
-               ctx.Flash.Warning("Function not implemented")
-               ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
-       }
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": setting.AppSubURL + "/user/settings/keys",
-       })
-}
-
-func loadKeysData(ctx *context.Context) {
-       keys, err := models.ListPublicKeys(ctx.User.ID, models.ListOptions{})
-       if err != nil {
-               ctx.ServerError("ListPublicKeys", err)
-               return
-       }
-       ctx.Data["Keys"] = keys
-
-       externalKeys, err := models.PublicKeysAreExternallyManaged(keys)
-       if err != nil {
-               ctx.ServerError("ListPublicKeys", err)
-               return
-       }
-       ctx.Data["ExternalKeys"] = externalKeys
-
-       gpgkeys, err := models.ListGPGKeys(ctx.User.ID, models.ListOptions{})
-       if err != nil {
-               ctx.ServerError("ListGPGKeys", err)
-               return
-       }
-       ctx.Data["GPGKeys"] = gpgkeys
-
-       principals, err := models.ListPrincipalKeys(ctx.User.ID, models.ListOptions{})
-       if err != nil {
-               ctx.ServerError("ListPrincipalKeys", err)
-               return
-       }
-       ctx.Data["Principals"] = principals
-}
diff --git a/routers/user/setting/main_test.go b/routers/user/setting/main_test.go
deleted file mode 100644 (file)
index d343c02..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright 2018 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 setting
-
-import (
-       "path/filepath"
-       "testing"
-
-       "code.gitea.io/gitea/models"
-)
-
-func TestMain(m *testing.M) {
-       models.MainTest(m, filepath.Join("..", "..", ".."))
-}
diff --git a/routers/user/setting/oauth2.go b/routers/user/setting/oauth2.go
deleted file mode 100644 (file)
index c8db6e8..0000000
+++ /dev/null
@@ -1,159 +0,0 @@
-// Copyright 2019 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 setting
-
-import (
-       "fmt"
-       "net/http"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-)
-
-const (
-       tplSettingsOAuthApplications base.TplName = "user/settings/applications_oauth2_edit"
-)
-
-// OAuthApplicationsPost response for adding a oauth2 application
-func OAuthApplicationsPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.EditOAuth2ApplicationForm)
-       ctx.Data["Title"] = ctx.Tr("settings")
-       ctx.Data["PageIsSettingsApplications"] = true
-
-       if ctx.HasError() {
-               loadApplicationsData(ctx)
-
-               ctx.HTML(http.StatusOK, tplSettingsApplications)
-               return
-       }
-       // TODO validate redirect URI
-       app, err := models.CreateOAuth2Application(models.CreateOAuth2ApplicationOptions{
-               Name:         form.Name,
-               RedirectURIs: []string{form.RedirectURI},
-               UserID:       ctx.User.ID,
-       })
-       if err != nil {
-               ctx.ServerError("CreateOAuth2Application", err)
-               return
-       }
-       ctx.Flash.Success(ctx.Tr("settings.create_oauth2_application_success"))
-       ctx.Data["App"] = app
-       ctx.Data["ClientSecret"], err = app.GenerateClientSecret()
-       if err != nil {
-               ctx.ServerError("GenerateClientSecret", err)
-               return
-       }
-       ctx.HTML(http.StatusOK, tplSettingsOAuthApplications)
-}
-
-// OAuthApplicationsEdit response for editing oauth2 application
-func OAuthApplicationsEdit(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.EditOAuth2ApplicationForm)
-       ctx.Data["Title"] = ctx.Tr("settings")
-       ctx.Data["PageIsSettingsApplications"] = true
-
-       if ctx.HasError() {
-               loadApplicationsData(ctx)
-
-               ctx.HTML(http.StatusOK, tplSettingsApplications)
-               return
-       }
-       // TODO validate redirect URI
-       var err error
-       if ctx.Data["App"], err = models.UpdateOAuth2Application(models.UpdateOAuth2ApplicationOptions{
-               ID:           ctx.ParamsInt64("id"),
-               Name:         form.Name,
-               RedirectURIs: []string{form.RedirectURI},
-               UserID:       ctx.User.ID,
-       }); err != nil {
-               ctx.ServerError("UpdateOAuth2Application", err)
-               return
-       }
-       ctx.Flash.Success(ctx.Tr("settings.update_oauth2_application_success"))
-       ctx.HTML(http.StatusOK, tplSettingsOAuthApplications)
-}
-
-// OAuthApplicationsRegenerateSecret handles the post request for regenerating the secret
-func OAuthApplicationsRegenerateSecret(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("settings")
-       ctx.Data["PageIsSettingsApplications"] = true
-
-       app, err := models.GetOAuth2ApplicationByID(ctx.ParamsInt64("id"))
-       if err != nil {
-               if models.IsErrOAuthApplicationNotFound(err) {
-                       ctx.NotFound("Application not found", err)
-                       return
-               }
-               ctx.ServerError("GetOAuth2ApplicationByID", err)
-               return
-       }
-       if app.UID != ctx.User.ID {
-               ctx.NotFound("Application not found", nil)
-               return
-       }
-       ctx.Data["App"] = app
-       ctx.Data["ClientSecret"], err = app.GenerateClientSecret()
-       if err != nil {
-               ctx.ServerError("GenerateClientSecret", err)
-               return
-       }
-       ctx.Flash.Success(ctx.Tr("settings.update_oauth2_application_success"))
-       ctx.HTML(http.StatusOK, tplSettingsOAuthApplications)
-}
-
-// OAuth2ApplicationShow displays the given application
-func OAuth2ApplicationShow(ctx *context.Context) {
-       app, err := models.GetOAuth2ApplicationByID(ctx.ParamsInt64("id"))
-       if err != nil {
-               if models.IsErrOAuthApplicationNotFound(err) {
-                       ctx.NotFound("Application not found", err)
-                       return
-               }
-               ctx.ServerError("GetOAuth2ApplicationByID", err)
-               return
-       }
-       if app.UID != ctx.User.ID {
-               ctx.NotFound("Application not found", nil)
-               return
-       }
-       ctx.Data["App"] = app
-       ctx.HTML(http.StatusOK, tplSettingsOAuthApplications)
-}
-
-// DeleteOAuth2Application deletes the given oauth2 application
-func DeleteOAuth2Application(ctx *context.Context) {
-       if err := models.DeleteOAuth2Application(ctx.QueryInt64("id"), ctx.User.ID); err != nil {
-               ctx.ServerError("DeleteOAuth2Application", err)
-               return
-       }
-       log.Trace("OAuth2 Application deleted: %s", ctx.User.Name)
-
-       ctx.Flash.Success(ctx.Tr("settings.remove_oauth2_application_success"))
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": setting.AppSubURL + "/user/settings/applications",
-       })
-}
-
-// RevokeOAuth2Grant revokes the grant with the given id
-func RevokeOAuth2Grant(ctx *context.Context) {
-       if ctx.User.ID == 0 || ctx.QueryInt64("id") == 0 {
-               ctx.ServerError("RevokeOAuth2Grant", fmt.Errorf("user id or grant id is zero"))
-               return
-       }
-       if err := models.RevokeOAuth2Grant(ctx.QueryInt64("id"), ctx.User.ID); err != nil {
-               ctx.ServerError("RevokeOAuth2Grant", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("settings.revoke_oauth2_grant_success"))
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": setting.AppSubURL + "/user/settings/applications",
-       })
-}
diff --git a/routers/user/setting/profile.go b/routers/user/setting/profile.go
deleted file mode 100644 (file)
index 20042ca..0000000
+++ /dev/null
@@ -1,319 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2018 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 setting
-
-import (
-       "errors"
-       "fmt"
-       "io/ioutil"
-       "net/http"
-       "os"
-       "path/filepath"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/typesniffer"
-       "code.gitea.io/gitea/modules/util"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/modules/web/middleware"
-       "code.gitea.io/gitea/services/forms"
-
-       "github.com/unknwon/i18n"
-)
-
-const (
-       tplSettingsProfile      base.TplName = "user/settings/profile"
-       tplSettingsOrganization base.TplName = "user/settings/organization"
-       tplSettingsRepositories base.TplName = "user/settings/repos"
-)
-
-// Profile render user's profile page
-func Profile(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("settings")
-       ctx.Data["PageIsSettingsProfile"] = true
-
-       ctx.HTML(http.StatusOK, tplSettingsProfile)
-}
-
-// HandleUsernameChange handle username changes from user settings and admin interface
-func HandleUsernameChange(ctx *context.Context, user *models.User, newName string) error {
-       // Non-local users are not allowed to change their username.
-       if !user.IsLocal() {
-               ctx.Flash.Error(ctx.Tr("form.username_change_not_local_user"))
-               return fmt.Errorf(ctx.Tr("form.username_change_not_local_user"))
-       }
-
-       // Check if user name has been changed
-       if user.LowerName != strings.ToLower(newName) {
-               if err := models.ChangeUserName(user, newName); err != nil {
-                       switch {
-                       case models.IsErrUserAlreadyExist(err):
-                               ctx.Flash.Error(ctx.Tr("form.username_been_taken"))
-                       case models.IsErrEmailAlreadyUsed(err):
-                               ctx.Flash.Error(ctx.Tr("form.email_been_used"))
-                       case models.IsErrNameReserved(err):
-                               ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName))
-                       case models.IsErrNamePatternNotAllowed(err):
-                               ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName))
-                       case models.IsErrNameCharsNotAllowed(err):
-                               ctx.Flash.Error(ctx.Tr("user.form.name_chars_not_allowed", newName))
-                       default:
-                               ctx.ServerError("ChangeUserName", err)
-                       }
-                       return err
-               }
-       } else {
-               if err := models.UpdateRepositoryOwnerNames(user.ID, newName); err != nil {
-                       ctx.ServerError("UpdateRepository", err)
-                       return err
-               }
-       }
-       log.Trace("User name changed: %s -> %s", user.Name, newName)
-       return nil
-}
-
-// ProfilePost response for change user's profile
-func ProfilePost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.UpdateProfileForm)
-       ctx.Data["Title"] = ctx.Tr("settings")
-       ctx.Data["PageIsSettingsProfile"] = true
-
-       if ctx.HasError() {
-               ctx.HTML(http.StatusOK, tplSettingsProfile)
-               return
-       }
-
-       if len(form.Name) != 0 && ctx.User.Name != form.Name {
-               log.Debug("Changing name for %s to %s", ctx.User.Name, form.Name)
-               if err := HandleUsernameChange(ctx, ctx.User, form.Name); err != nil {
-                       ctx.Redirect(setting.AppSubURL + "/user/settings")
-                       return
-               }
-               ctx.User.Name = form.Name
-               ctx.User.LowerName = strings.ToLower(form.Name)
-       }
-
-       ctx.User.FullName = form.FullName
-       ctx.User.KeepEmailPrivate = form.KeepEmailPrivate
-       ctx.User.Website = form.Website
-       ctx.User.Location = form.Location
-       if len(form.Language) != 0 {
-               if !util.IsStringInSlice(form.Language, setting.Langs) {
-                       ctx.Flash.Error(ctx.Tr("settings.update_language_not_found", form.Language))
-                       ctx.Redirect(setting.AppSubURL + "/user/settings")
-                       return
-               }
-               ctx.User.Language = form.Language
-       }
-       ctx.User.Description = form.Description
-       ctx.User.KeepActivityPrivate = form.KeepActivityPrivate
-       if err := models.UpdateUserSetting(ctx.User); err != nil {
-               if _, ok := err.(models.ErrEmailAlreadyUsed); ok {
-                       ctx.Flash.Error(ctx.Tr("form.email_been_used"))
-                       ctx.Redirect(setting.AppSubURL + "/user/settings")
-                       return
-               }
-               ctx.ServerError("UpdateUser", err)
-               return
-       }
-
-       // Update the language to the one we just set
-       middleware.SetLocaleCookie(ctx.Resp, ctx.User.Language, 0)
-
-       log.Trace("User settings updated: %s", ctx.User.Name)
-       ctx.Flash.Success(i18n.Tr(ctx.User.Language, "settings.update_profile_success"))
-       ctx.Redirect(setting.AppSubURL + "/user/settings")
-}
-
-// UpdateAvatarSetting update user's avatar
-// FIXME: limit size.
-func UpdateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, ctxUser *models.User) error {
-       ctxUser.UseCustomAvatar = form.Source == forms.AvatarLocal
-       if len(form.Gravatar) > 0 {
-               if form.Avatar != nil {
-                       ctxUser.Avatar = base.EncodeMD5(form.Gravatar)
-               } else {
-                       ctxUser.Avatar = ""
-               }
-               ctxUser.AvatarEmail = form.Gravatar
-       }
-
-       if form.Avatar != nil && form.Avatar.Filename != "" {
-               fr, err := form.Avatar.Open()
-               if err != nil {
-                       return fmt.Errorf("Avatar.Open: %v", err)
-               }
-               defer fr.Close()
-
-               if form.Avatar.Size > setting.Avatar.MaxFileSize {
-                       return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big"))
-               }
-
-               data, err := ioutil.ReadAll(fr)
-               if err != nil {
-                       return fmt.Errorf("ioutil.ReadAll: %v", err)
-               }
-
-               st := typesniffer.DetectContentType(data)
-               if !(st.IsImage() && !st.IsSvgImage()) {
-                       return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image"))
-               }
-               if err = ctxUser.UploadAvatar(data); err != nil {
-                       return fmt.Errorf("UploadAvatar: %v", err)
-               }
-       } else if ctxUser.UseCustomAvatar && ctxUser.Avatar == "" {
-               // No avatar is uploaded but setting has been changed to enable,
-               // generate a random one when needed.
-               if err := ctxUser.GenerateRandomAvatar(); err != nil {
-                       log.Error("GenerateRandomAvatar[%d]: %v", ctxUser.ID, err)
-               }
-       }
-
-       if err := models.UpdateUserCols(ctxUser, "avatar", "avatar_email", "use_custom_avatar"); err != nil {
-               return fmt.Errorf("UpdateUser: %v", err)
-       }
-
-       return nil
-}
-
-// AvatarPost response for change user's avatar request
-func AvatarPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.AvatarForm)
-       if err := UpdateAvatarSetting(ctx, form, ctx.User); err != nil {
-               ctx.Flash.Error(err.Error())
-       } else {
-               ctx.Flash.Success(ctx.Tr("settings.update_avatar_success"))
-       }
-
-       ctx.Redirect(setting.AppSubURL + "/user/settings")
-}
-
-// DeleteAvatar render delete avatar page
-func DeleteAvatar(ctx *context.Context) {
-       if err := ctx.User.DeleteAvatar(); err != nil {
-               ctx.Flash.Error(err.Error())
-       }
-
-       ctx.Redirect(setting.AppSubURL + "/user/settings")
-}
-
-// Organization render all the organization of the user
-func Organization(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("settings")
-       ctx.Data["PageIsSettingsOrganization"] = true
-       orgs, err := models.GetOrgsByUserID(ctx.User.ID, ctx.IsSigned)
-       if err != nil {
-               ctx.ServerError("GetOrgsByUserID", err)
-               return
-       }
-       ctx.Data["Orgs"] = orgs
-       ctx.HTML(http.StatusOK, tplSettingsOrganization)
-}
-
-// Repos display a list of all repositories of the user
-func Repos(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("settings")
-       ctx.Data["PageIsSettingsRepos"] = true
-       ctx.Data["allowAdopt"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories
-       ctx.Data["allowDelete"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories
-
-       opts := models.ListOptions{
-               PageSize: setting.UI.Admin.UserPagingNum,
-               Page:     ctx.QueryInt("page"),
-       }
-
-       if opts.Page <= 0 {
-               opts.Page = 1
-       }
-       start := (opts.Page - 1) * opts.PageSize
-       end := start + opts.PageSize
-
-       adoptOrDelete := ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories)
-
-       ctxUser := ctx.User
-       count := 0
-
-       if adoptOrDelete {
-               repoNames := make([]string, 0, setting.UI.Admin.UserPagingNum)
-               repos := map[string]*models.Repository{}
-               // We're going to iterate by pagesize.
-               root := filepath.Join(models.UserPath(ctxUser.Name))
-               if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
-                       if err != nil {
-                               if os.IsNotExist(err) {
-                                       return nil
-                               }
-                               return err
-                       }
-                       if !info.IsDir() || path == root {
-                               return nil
-                       }
-                       name := info.Name()
-                       if !strings.HasSuffix(name, ".git") {
-                               return filepath.SkipDir
-                       }
-                       name = name[:len(name)-4]
-                       if models.IsUsableRepoName(name) != nil || strings.ToLower(name) != name {
-                               return filepath.SkipDir
-                       }
-                       if count >= start && count < end {
-                               repoNames = append(repoNames, name)
-                       }
-                       count++
-                       return filepath.SkipDir
-               }); err != nil {
-                       ctx.ServerError("filepath.Walk", err)
-                       return
-               }
-
-               if err := ctxUser.GetRepositories(models.ListOptions{Page: 1, PageSize: setting.UI.Admin.UserPagingNum}, repoNames...); err != nil {
-                       ctx.ServerError("GetRepositories", err)
-                       return
-               }
-               for _, repo := range ctxUser.Repos {
-                       if repo.IsFork {
-                               if err := repo.GetBaseRepo(); err != nil {
-                                       ctx.ServerError("GetBaseRepo", err)
-                                       return
-                               }
-                       }
-                       repos[repo.LowerName] = repo
-               }
-               ctx.Data["Dirs"] = repoNames
-               ctx.Data["ReposMap"] = repos
-       } else {
-               var err error
-               var count64 int64
-               ctxUser.Repos, count64, err = models.GetUserRepositories(&models.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts})
-
-               if err != nil {
-                       ctx.ServerError("GetRepositories", err)
-                       return
-               }
-               count = int(count64)
-               repos := ctxUser.Repos
-
-               for i := range repos {
-                       if repos[i].IsFork {
-                               if err := repos[i].GetBaseRepo(); err != nil {
-                                       ctx.ServerError("GetBaseRepo", err)
-                                       return
-                               }
-                       }
-               }
-
-               ctx.Data["Repos"] = repos
-       }
-       ctx.Data["Owner"] = ctxUser
-       pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
-       pager.SetDefaultParams(ctx)
-       ctx.Data["Page"] = pager
-       ctx.HTML(http.StatusOK, tplSettingsRepositories)
-}
diff --git a/routers/user/setting/security.go b/routers/user/setting/security.go
deleted file mode 100644 (file)
index 7753c5c..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2018 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 setting
-
-import (
-       "net/http"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/base"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/setting"
-)
-
-const (
-       tplSettingsSecurity    base.TplName = "user/settings/security"
-       tplSettingsTwofaEnroll base.TplName = "user/settings/twofa_enroll"
-)
-
-// Security render change user's password page and 2FA
-func Security(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("settings")
-       ctx.Data["PageIsSettingsSecurity"] = true
-       ctx.Data["RequireU2F"] = true
-
-       if ctx.Query("openid.return_to") != "" {
-               settingsOpenIDVerify(ctx)
-               return
-       }
-
-       loadSecurityData(ctx)
-
-       ctx.HTML(http.StatusOK, tplSettingsSecurity)
-}
-
-// DeleteAccountLink delete a single account link
-func DeleteAccountLink(ctx *context.Context) {
-       id := ctx.QueryInt64("id")
-       if id <= 0 {
-               ctx.Flash.Error("Account link id is not given")
-       } else {
-               if _, err := models.RemoveAccountLink(ctx.User, id); err != nil {
-                       ctx.Flash.Error("RemoveAccountLink: " + err.Error())
-               } else {
-                       ctx.Flash.Success(ctx.Tr("settings.remove_account_link_success"))
-               }
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": setting.AppSubURL + "/user/settings/security",
-       })
-}
-
-func loadSecurityData(ctx *context.Context) {
-       enrolled := true
-       _, err := models.GetTwoFactorByUID(ctx.User.ID)
-       if err != nil {
-               if models.IsErrTwoFactorNotEnrolled(err) {
-                       enrolled = false
-               } else {
-                       ctx.ServerError("SettingsTwoFactor", err)
-                       return
-               }
-       }
-       ctx.Data["TwofaEnrolled"] = enrolled
-       if enrolled {
-               ctx.Data["U2FRegistrations"], err = models.GetU2FRegistrationsByUID(ctx.User.ID)
-               if err != nil {
-                       ctx.ServerError("GetU2FRegistrationsByUID", err)
-                       return
-               }
-       }
-
-       tokens, err := models.ListAccessTokens(models.ListAccessTokensOptions{UserID: ctx.User.ID})
-       if err != nil {
-               ctx.ServerError("ListAccessTokens", err)
-               return
-       }
-       ctx.Data["Tokens"] = tokens
-
-       accountLinks, err := models.ListAccountLinks(ctx.User)
-       if err != nil {
-               ctx.ServerError("ListAccountLinks", err)
-               return
-       }
-
-       // map the provider display name with the LoginSource
-       sources := make(map[*models.LoginSource]string)
-       for _, externalAccount := range accountLinks {
-               if loginSource, err := models.GetLoginSourceByID(externalAccount.LoginSourceID); err == nil {
-                       var providerDisplayName string
-                       if loginSource.IsOAuth2() {
-                               providerTechnicalName := loginSource.OAuth2().Provider
-                               providerDisplayName = models.OAuth2Providers[providerTechnicalName].DisplayName
-                       } else {
-                               providerDisplayName = loginSource.Name
-                       }
-                       sources[loginSource] = providerDisplayName
-               }
-       }
-       ctx.Data["AccountLinks"] = sources
-
-       openid, err := models.GetUserOpenIDs(ctx.User.ID)
-       if err != nil {
-               ctx.ServerError("GetUserOpenIDs", err)
-               return
-       }
-       ctx.Data["OpenIDs"] = openid
-}
diff --git a/routers/user/setting/security_openid.go b/routers/user/setting/security_openid.go
deleted file mode 100644 (file)
index 74dba12..0000000
+++ /dev/null
@@ -1,129 +0,0 @@
-// Copyright 2018 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 setting
-
-import (
-       "net/http"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/auth/openid"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-)
-
-// OpenIDPost response for change user's openid
-func OpenIDPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.AddOpenIDForm)
-       ctx.Data["Title"] = ctx.Tr("settings")
-       ctx.Data["PageIsSettingsSecurity"] = true
-
-       if ctx.HasError() {
-               loadSecurityData(ctx)
-
-               ctx.HTML(http.StatusOK, tplSettingsSecurity)
-               return
-       }
-
-       // WARNING: specifying a wrong OpenID here could lock
-       // a user out of her account, would be better to
-       // verify/confirm the new OpenID before storing it
-
-       // Also, consider allowing for multiple OpenID URIs
-
-       id, err := openid.Normalize(form.Openid)
-       if err != nil {
-               loadSecurityData(ctx)
-
-               ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &form)
-               return
-       }
-       form.Openid = id
-       log.Trace("Normalized id: " + id)
-
-       oids, err := models.GetUserOpenIDs(ctx.User.ID)
-       if err != nil {
-               ctx.ServerError("GetUserOpenIDs", err)
-               return
-       }
-       ctx.Data["OpenIDs"] = oids
-
-       // Check that the OpenID is not already used
-       for _, obj := range oids {
-               if obj.URI == id {
-                       loadSecurityData(ctx)
-
-                       ctx.RenderWithErr(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &form)
-                       return
-               }
-       }
-
-       redirectTo := setting.AppURL + "user/settings/security"
-       url, err := openid.RedirectURL(id, redirectTo, setting.AppURL)
-       if err != nil {
-               loadSecurityData(ctx)
-
-               ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &form)
-               return
-       }
-       ctx.Redirect(url)
-}
-
-func settingsOpenIDVerify(ctx *context.Context) {
-       log.Trace("Incoming call to: " + ctx.Req.URL.String())
-
-       fullURL := setting.AppURL + ctx.Req.URL.String()[1:]
-       log.Trace("Full URL: " + fullURL)
-
-       id, err := openid.Verify(fullURL)
-       if err != nil {
-               ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &forms.AddOpenIDForm{
-                       Openid: id,
-               })
-               return
-       }
-
-       log.Trace("Verified ID: " + id)
-
-       oid := &models.UserOpenID{UID: ctx.User.ID, URI: id}
-       if err = models.AddUserOpenID(oid); err != nil {
-               if models.IsErrOpenIDAlreadyUsed(err) {
-                       ctx.RenderWithErr(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &forms.AddOpenIDForm{Openid: id})
-                       return
-               }
-               ctx.ServerError("AddUserOpenID", err)
-               return
-       }
-       log.Trace("Associated OpenID %s to user %s", id, ctx.User.Name)
-       ctx.Flash.Success(ctx.Tr("settings.add_openid_success"))
-
-       ctx.Redirect(setting.AppSubURL + "/user/settings/security")
-}
-
-// DeleteOpenID response for delete user's openid
-func DeleteOpenID(ctx *context.Context) {
-       if err := models.DeleteUserOpenID(&models.UserOpenID{ID: ctx.QueryInt64("id"), UID: ctx.User.ID}); err != nil {
-               ctx.ServerError("DeleteUserOpenID", err)
-               return
-       }
-       log.Trace("OpenID address deleted: %s", ctx.User.Name)
-
-       ctx.Flash.Success(ctx.Tr("settings.openid_deletion_success"))
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": setting.AppSubURL + "/user/settings/security",
-       })
-}
-
-// ToggleOpenIDVisibility response for toggle visibility of user's openid
-func ToggleOpenIDVisibility(ctx *context.Context) {
-       if err := models.ToggleUserOpenIDVisibility(ctx.QueryInt64("id")); err != nil {
-               ctx.ServerError("ToggleUserOpenIDVisibility", err)
-               return
-       }
-
-       ctx.Redirect(setting.AppSubURL + "/user/settings/security")
-}
diff --git a/routers/user/setting/security_twofa.go b/routers/user/setting/security_twofa.go
deleted file mode 100644 (file)
index 7b08a05..0000000
+++ /dev/null
@@ -1,250 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2018 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 setting
-
-import (
-       "bytes"
-       "encoding/base64"
-       "html/template"
-       "image/png"
-       "net/http"
-       "strings"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-
-       "github.com/pquerna/otp"
-       "github.com/pquerna/otp/totp"
-)
-
-// RegenerateScratchTwoFactor regenerates the user's 2FA scratch code.
-func RegenerateScratchTwoFactor(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("settings")
-       ctx.Data["PageIsSettingsSecurity"] = true
-
-       t, err := models.GetTwoFactorByUID(ctx.User.ID)
-       if err != nil {
-               if models.IsErrTwoFactorNotEnrolled(err) {
-                       ctx.Flash.Error(ctx.Tr("setting.twofa_not_enrolled"))
-                       ctx.Redirect(setting.AppSubURL + "/user/settings/security")
-               }
-               ctx.ServerError("SettingsTwoFactor: Failed to GetTwoFactorByUID", err)
-               return
-       }
-
-       token, err := t.GenerateScratchToken()
-       if err != nil {
-               ctx.ServerError("SettingsTwoFactor: Failed to GenerateScratchToken", err)
-               return
-       }
-
-       if err = models.UpdateTwoFactor(t); err != nil {
-               ctx.ServerError("SettingsTwoFactor: Failed to UpdateTwoFactor", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("settings.twofa_scratch_token_regenerated", token))
-       ctx.Redirect(setting.AppSubURL + "/user/settings/security")
-}
-
-// DisableTwoFactor deletes the user's 2FA settings.
-func DisableTwoFactor(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("settings")
-       ctx.Data["PageIsSettingsSecurity"] = true
-
-       t, err := models.GetTwoFactorByUID(ctx.User.ID)
-       if err != nil {
-               if models.IsErrTwoFactorNotEnrolled(err) {
-                       ctx.Flash.Error(ctx.Tr("setting.twofa_not_enrolled"))
-                       ctx.Redirect(setting.AppSubURL + "/user/settings/security")
-               }
-               ctx.ServerError("SettingsTwoFactor: Failed to GetTwoFactorByUID", err)
-               return
-       }
-
-       if err = models.DeleteTwoFactorByID(t.ID, ctx.User.ID); err != nil {
-               if models.IsErrTwoFactorNotEnrolled(err) {
-                       // There is a potential DB race here - we must have been disabled by another request in the intervening period
-                       ctx.Flash.Success(ctx.Tr("settings.twofa_disabled"))
-                       ctx.Redirect(setting.AppSubURL + "/user/settings/security")
-               }
-               ctx.ServerError("SettingsTwoFactor: Failed to DeleteTwoFactorByID", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("settings.twofa_disabled"))
-       ctx.Redirect(setting.AppSubURL + "/user/settings/security")
-}
-
-func twofaGenerateSecretAndQr(ctx *context.Context) bool {
-       var otpKey *otp.Key
-       var err error
-       uri := ctx.Session.Get("twofaUri")
-       if uri != nil {
-               otpKey, err = otp.NewKeyFromURL(uri.(string))
-               if err != nil {
-                       ctx.ServerError("SettingsTwoFactor: Failed NewKeyFromURL: ", err)
-                       return false
-               }
-       }
-       // Filter unsafe character ':' in issuer
-       issuer := strings.ReplaceAll(setting.AppName+" ("+setting.Domain+")", ":", "")
-       if otpKey == nil {
-               otpKey, err = totp.Generate(totp.GenerateOpts{
-                       SecretSize:  40,
-                       Issuer:      issuer,
-                       AccountName: ctx.User.Name,
-               })
-               if err != nil {
-                       ctx.ServerError("SettingsTwoFactor: totpGenerate Failed", err)
-                       return false
-               }
-       }
-
-       ctx.Data["TwofaSecret"] = otpKey.Secret()
-       img, err := otpKey.Image(320, 240)
-       if err != nil {
-               ctx.ServerError("SettingsTwoFactor: otpKey image generation failed", err)
-               return false
-       }
-
-       var imgBytes bytes.Buffer
-       if err = png.Encode(&imgBytes, img); err != nil {
-               ctx.ServerError("SettingsTwoFactor: otpKey png encoding failed", err)
-               return false
-       }
-
-       ctx.Data["QrUri"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(imgBytes.Bytes()))
-
-       if err := ctx.Session.Set("twofaSecret", otpKey.Secret()); err != nil {
-               ctx.ServerError("SettingsTwoFactor: Failed to set session for twofaSecret", err)
-               return false
-       }
-
-       if err := ctx.Session.Set("twofaUri", otpKey.String()); err != nil {
-               ctx.ServerError("SettingsTwoFactor: Failed to set session for twofaUri", err)
-               return false
-       }
-
-       // Here we're just going to try to release the session early
-       if err := ctx.Session.Release(); err != nil {
-               // we'll tolerate errors here as they *should* get saved elsewhere
-               log.Error("Unable to save changes to the session: %v", err)
-       }
-       return true
-}
-
-// EnrollTwoFactor shows the page where the user can enroll into 2FA.
-func EnrollTwoFactor(ctx *context.Context) {
-       ctx.Data["Title"] = ctx.Tr("settings")
-       ctx.Data["PageIsSettingsSecurity"] = true
-
-       t, err := models.GetTwoFactorByUID(ctx.User.ID)
-       if t != nil {
-               // already enrolled - we should redirect back!
-               log.Warn("Trying to re-enroll %-v in twofa when already enrolled", ctx.User)
-               ctx.Flash.Error(ctx.Tr("setting.twofa_is_enrolled"))
-               ctx.Redirect(setting.AppSubURL + "/user/settings/security")
-               return
-       }
-       if err != nil && !models.IsErrTwoFactorNotEnrolled(err) {
-               ctx.ServerError("SettingsTwoFactor: GetTwoFactorByUID", err)
-               return
-       }
-
-       if !twofaGenerateSecretAndQr(ctx) {
-               return
-       }
-
-       ctx.HTML(http.StatusOK, tplSettingsTwofaEnroll)
-}
-
-// EnrollTwoFactorPost handles enrolling the user into 2FA.
-func EnrollTwoFactorPost(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.TwoFactorAuthForm)
-       ctx.Data["Title"] = ctx.Tr("settings")
-       ctx.Data["PageIsSettingsSecurity"] = true
-
-       t, err := models.GetTwoFactorByUID(ctx.User.ID)
-       if t != nil {
-               // already enrolled
-               ctx.Flash.Error(ctx.Tr("setting.twofa_is_enrolled"))
-               ctx.Redirect(setting.AppSubURL + "/user/settings/security")
-               return
-       }
-       if err != nil && !models.IsErrTwoFactorNotEnrolled(err) {
-               ctx.ServerError("SettingsTwoFactor: Failed to check if already enrolled with GetTwoFactorByUID", err)
-               return
-       }
-
-       if ctx.HasError() {
-               if !twofaGenerateSecretAndQr(ctx) {
-                       return
-               }
-               ctx.HTML(http.StatusOK, tplSettingsTwofaEnroll)
-               return
-       }
-
-       secretRaw := ctx.Session.Get("twofaSecret")
-       if secretRaw == nil {
-               ctx.Flash.Error(ctx.Tr("settings.twofa_failed_get_secret"))
-               ctx.Redirect(setting.AppSubURL + "/user/settings/security/two_factor/enroll")
-               return
-       }
-
-       secret := secretRaw.(string)
-       if !totp.Validate(form.Passcode, secret) {
-               if !twofaGenerateSecretAndQr(ctx) {
-                       return
-               }
-               ctx.Flash.Error(ctx.Tr("settings.passcode_invalid"))
-               ctx.Redirect(setting.AppSubURL + "/user/settings/security/two_factor/enroll")
-               return
-       }
-
-       t = &models.TwoFactor{
-               UID: ctx.User.ID,
-       }
-       err = t.SetSecret(secret)
-       if err != nil {
-               ctx.ServerError("SettingsTwoFactor: Failed to set secret", err)
-               return
-       }
-       token, err := t.GenerateScratchToken()
-       if err != nil {
-               ctx.ServerError("SettingsTwoFactor: Failed to generate scratch token", err)
-               return
-       }
-
-       // Now we have to delete the secrets - because if we fail to insert then it's highly likely that they have already been used
-       // If we can detect the unique constraint failure below we can move this to after the NewTwoFactor
-       if err := ctx.Session.Delete("twofaSecret"); err != nil {
-               // tolerate this failure - it's more important to continue
-               log.Error("Unable to delete twofaSecret from the session: Error: %v", err)
-       }
-       if err := ctx.Session.Delete("twofaUri"); err != nil {
-               // tolerate this failure - it's more important to continue
-               log.Error("Unable to delete twofaUri from the session: Error: %v", err)
-       }
-       if err := ctx.Session.Release(); err != nil {
-               // tolerate this failure - it's more important to continue
-               log.Error("Unable to save changes to the session: %v", err)
-       }
-
-       if err = models.NewTwoFactor(t); err != nil {
-               // FIXME: We need to handle a unique constraint fail here it's entirely possible that another request has beaten us.
-               // If there is a unique constraint fail we should just tolerate the error
-               ctx.ServerError("SettingsTwoFactor: Failed to save two factor", err)
-               return
-       }
-
-       ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", token))
-       ctx.Redirect(setting.AppSubURL + "/user/settings/security")
-}
diff --git a/routers/user/setting/security_u2f.go b/routers/user/setting/security_u2f.go
deleted file mode 100644 (file)
index f9e3554..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-// Copyright 2018 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 setting
-
-import (
-       "errors"
-       "net/http"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/context"
-       "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/web"
-       "code.gitea.io/gitea/services/forms"
-
-       "github.com/tstranex/u2f"
-)
-
-// U2FRegister initializes the u2f registration procedure
-func U2FRegister(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.U2FRegistrationForm)
-       if form.Name == "" {
-               ctx.Error(http.StatusConflict)
-               return
-       }
-       challenge, err := u2f.NewChallenge(setting.U2F.AppID, setting.U2F.TrustedFacets)
-       if err != nil {
-               ctx.ServerError("NewChallenge", err)
-               return
-       }
-       if err := ctx.Session.Set("u2fChallenge", challenge); err != nil {
-               ctx.ServerError("Unable to set session key for u2fChallenge", err)
-               return
-       }
-       regs, err := models.GetU2FRegistrationsByUID(ctx.User.ID)
-       if err != nil {
-               ctx.ServerError("GetU2FRegistrationsByUID", err)
-               return
-       }
-       for _, reg := range regs {
-               if reg.Name == form.Name {
-                       ctx.Error(http.StatusConflict, "Name already taken")
-                       return
-               }
-       }
-       if err := ctx.Session.Set("u2fName", form.Name); err != nil {
-               ctx.ServerError("Unable to set session key for u2fName", err)
-               return
-       }
-       // Here we're just going to try to release the session early
-       if err := ctx.Session.Release(); err != nil {
-               // we'll tolerate errors here as they *should* get saved elsewhere
-               log.Error("Unable to save changes to the session: %v", err)
-       }
-       ctx.JSON(http.StatusOK, u2f.NewWebRegisterRequest(challenge, regs.ToRegistrations()))
-}
-
-// U2FRegisterPost receives the response of the security key
-func U2FRegisterPost(ctx *context.Context) {
-       response := web.GetForm(ctx).(*u2f.RegisterResponse)
-       challSess := ctx.Session.Get("u2fChallenge")
-       u2fName := ctx.Session.Get("u2fName")
-       if challSess == nil || u2fName == nil {
-               ctx.ServerError("U2FRegisterPost", errors.New("not in U2F session"))
-               return
-       }
-       challenge := challSess.(*u2f.Challenge)
-       name := u2fName.(string)
-       config := &u2f.Config{
-               // Chrome 66+ doesn't return the device's attestation
-               // certificate by default.
-               SkipAttestationVerify: true,
-       }
-       reg, err := u2f.Register(*response, *challenge, config)
-       if err != nil {
-               ctx.ServerError("u2f.Register", err)
-               return
-       }
-       if _, err = models.CreateRegistration(ctx.User, name, reg); err != nil {
-               ctx.ServerError("u2f.Register", err)
-               return
-       }
-       ctx.Status(200)
-}
-
-// U2FDelete deletes an security key by id
-func U2FDelete(ctx *context.Context) {
-       form := web.GetForm(ctx).(*forms.U2FDeleteForm)
-       reg, err := models.GetU2FRegistrationByID(form.ID)
-       if err != nil {
-               if models.IsErrU2FRegistrationNotExist(err) {
-                       ctx.Status(200)
-                       return
-               }
-               ctx.ServerError("GetU2FRegistrationByID", err)
-               return
-       }
-       if reg.UserID != ctx.User.ID {
-               ctx.Status(401)
-               return
-       }
-       if err := models.DeleteRegistration(reg); err != nil {
-               ctx.ServerError("DeleteRegistration", err)
-               return
-       }
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "redirect": setting.AppSubURL + "/user/settings/security",
-       })
-}
diff --git a/routers/user/task.go b/routers/user/task.go
deleted file mode 100644 (file)
index b8df5d9..0000000
+++ /dev/null
@@ -1,32 +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 user
-
-import (
-       "net/http"
-
-       "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/context"
-)
-
-// TaskStatus returns task's status
-func TaskStatus(ctx *context.Context) {
-       task, opts, err := models.GetMigratingTaskByID(ctx.ParamsInt64("task"), ctx.User.ID)
-       if err != nil {
-               ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
-                       "err": err,
-               })
-               return
-       }
-
-       ctx.JSON(http.StatusOK, map[string]interface{}{
-               "status":    task.Status,
-               "err":       task.Errors,
-               "repo-id":   task.RepoID,
-               "repo-name": opts.RepoName,
-               "start":     task.StartTime,
-               "end":       task.EndTime,
-       })
-}
diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go
new file mode 100644 (file)
index 0000000..c2d94ab
--- /dev/null
@@ -0,0 +1,485 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2019 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 admin
+
+import (
+       "fmt"
+       "net/http"
+       "net/url"
+       "os"
+       "runtime"
+       "strconv"
+       "strings"
+       "time"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/cron"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/process"
+       "code.gitea.io/gitea/modules/queue"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/timeutil"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+       "code.gitea.io/gitea/services/mailer"
+       jsoniter "github.com/json-iterator/go"
+
+       "gitea.com/go-chi/session"
+)
+
+const (
+       tplDashboard base.TplName = "admin/dashboard"
+       tplConfig    base.TplName = "admin/config"
+       tplMonitor   base.TplName = "admin/monitor"
+       tplQueue     base.TplName = "admin/queue"
+)
+
+var sysStatus struct {
+       Uptime       string
+       NumGoroutine int
+
+       // General statistics.
+       MemAllocated string // bytes allocated and still in use
+       MemTotal     string // bytes allocated (even if freed)
+       MemSys       string // bytes obtained from system (sum of XxxSys below)
+       Lookups      uint64 // number of pointer lookups
+       MemMallocs   uint64 // number of mallocs
+       MemFrees     uint64 // number of frees
+
+       // Main allocation heap statistics.
+       HeapAlloc    string // bytes allocated and still in use
+       HeapSys      string // bytes obtained from system
+       HeapIdle     string // bytes in idle spans
+       HeapInuse    string // bytes in non-idle span
+       HeapReleased string // bytes released to the OS
+       HeapObjects  uint64 // total number of allocated objects
+
+       // Low-level fixed-size structure allocator statistics.
+       //      Inuse is bytes used now.
+       //      Sys is bytes obtained from system.
+       StackInuse  string // bootstrap stacks
+       StackSys    string
+       MSpanInuse  string // mspan structures
+       MSpanSys    string
+       MCacheInuse string // mcache structures
+       MCacheSys   string
+       BuckHashSys string // profiling bucket hash table
+       GCSys       string // GC metadata
+       OtherSys    string // other system allocations
+
+       // Garbage collector statistics.
+       NextGC       string // next run in HeapAlloc time (bytes)
+       LastGC       string // last run in absolute time (ns)
+       PauseTotalNs string
+       PauseNs      string // circular buffer of recent GC pause times, most recent at [(NumGC+255)%256]
+       NumGC        uint32
+}
+
+func updateSystemStatus() {
+       sysStatus.Uptime = timeutil.TimeSincePro(setting.AppStartTime, "en")
+
+       m := new(runtime.MemStats)
+       runtime.ReadMemStats(m)
+       sysStatus.NumGoroutine = runtime.NumGoroutine()
+
+       sysStatus.MemAllocated = base.FileSize(int64(m.Alloc))
+       sysStatus.MemTotal = base.FileSize(int64(m.TotalAlloc))
+       sysStatus.MemSys = base.FileSize(int64(m.Sys))
+       sysStatus.Lookups = m.Lookups
+       sysStatus.MemMallocs = m.Mallocs
+       sysStatus.MemFrees = m.Frees
+
+       sysStatus.HeapAlloc = base.FileSize(int64(m.HeapAlloc))
+       sysStatus.HeapSys = base.FileSize(int64(m.HeapSys))
+       sysStatus.HeapIdle = base.FileSize(int64(m.HeapIdle))
+       sysStatus.HeapInuse = base.FileSize(int64(m.HeapInuse))
+       sysStatus.HeapReleased = base.FileSize(int64(m.HeapReleased))
+       sysStatus.HeapObjects = m.HeapObjects
+
+       sysStatus.StackInuse = base.FileSize(int64(m.StackInuse))
+       sysStatus.StackSys = base.FileSize(int64(m.StackSys))
+       sysStatus.MSpanInuse = base.FileSize(int64(m.MSpanInuse))
+       sysStatus.MSpanSys = base.FileSize(int64(m.MSpanSys))
+       sysStatus.MCacheInuse = base.FileSize(int64(m.MCacheInuse))
+       sysStatus.MCacheSys = base.FileSize(int64(m.MCacheSys))
+       sysStatus.BuckHashSys = base.FileSize(int64(m.BuckHashSys))
+       sysStatus.GCSys = base.FileSize(int64(m.GCSys))
+       sysStatus.OtherSys = base.FileSize(int64(m.OtherSys))
+
+       sysStatus.NextGC = base.FileSize(int64(m.NextGC))
+       sysStatus.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000)
+       sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000)
+       sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000)
+       sysStatus.NumGC = m.NumGC
+}
+
+// Dashboard show admin panel dashboard
+func Dashboard(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("admin.dashboard")
+       ctx.Data["PageIsAdmin"] = true
+       ctx.Data["PageIsAdminDashboard"] = true
+       ctx.Data["Stats"] = models.GetStatistic()
+       // FIXME: update periodically
+       updateSystemStatus()
+       ctx.Data["SysStatus"] = sysStatus
+       ctx.Data["SSH"] = setting.SSH
+       ctx.HTML(http.StatusOK, tplDashboard)
+}
+
+// DashboardPost run an admin operation
+func DashboardPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.AdminDashboardForm)
+       ctx.Data["Title"] = ctx.Tr("admin.dashboard")
+       ctx.Data["PageIsAdmin"] = true
+       ctx.Data["PageIsAdminDashboard"] = true
+       ctx.Data["Stats"] = models.GetStatistic()
+       updateSystemStatus()
+       ctx.Data["SysStatus"] = sysStatus
+
+       // Run operation.
+       if form.Op != "" {
+               task := cron.GetTask(form.Op)
+               if task != nil {
+                       go task.RunWithUser(ctx.User, nil)
+                       ctx.Flash.Success(ctx.Tr("admin.dashboard.task.started", ctx.Tr("admin.dashboard."+form.Op)))
+               } else {
+                       ctx.Flash.Error(ctx.Tr("admin.dashboard.task.unknown", form.Op))
+               }
+       }
+       if form.From == "monitor" {
+               ctx.Redirect(setting.AppSubURL + "/admin/monitor")
+       } else {
+               ctx.Redirect(setting.AppSubURL + "/admin")
+       }
+}
+
+// SendTestMail send test mail to confirm mail service is OK
+func SendTestMail(ctx *context.Context) {
+       email := ctx.Query("email")
+       // Send a test email to the user's email address and redirect back to Config
+       if err := mailer.SendTestMail(email); err != nil {
+               ctx.Flash.Error(ctx.Tr("admin.config.test_mail_failed", email, err))
+       } else {
+               ctx.Flash.Info(ctx.Tr("admin.config.test_mail_sent", email))
+       }
+
+       ctx.Redirect(setting.AppSubURL + "/admin/config")
+}
+
+func shadowPasswordKV(cfgItem, splitter string) string {
+       fields := strings.Split(cfgItem, splitter)
+       for i := 0; i < len(fields); i++ {
+               if strings.HasPrefix(fields[i], "password=") {
+                       fields[i] = "password=******"
+                       break
+               }
+       }
+       return strings.Join(fields, splitter)
+}
+
+func shadowURL(provider, cfgItem string) string {
+       u, err := url.Parse(cfgItem)
+       if err != nil {
+               log.Error("Shadowing Password for %v failed: %v", provider, err)
+               return cfgItem
+       }
+       if u.User != nil {
+               atIdx := strings.Index(cfgItem, "@")
+               if atIdx > 0 {
+                       colonIdx := strings.LastIndex(cfgItem[:atIdx], ":")
+                       if colonIdx > 0 {
+                               return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:]
+                       }
+               }
+       }
+       return cfgItem
+}
+
+func shadowPassword(provider, cfgItem string) string {
+       switch provider {
+       case "redis":
+               return shadowPasswordKV(cfgItem, ",")
+       case "mysql":
+               //root:@tcp(localhost:3306)/macaron?charset=utf8
+               atIdx := strings.Index(cfgItem, "@")
+               if atIdx > 0 {
+                       colonIdx := strings.Index(cfgItem[:atIdx], ":")
+                       if colonIdx > 0 {
+                               return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:]
+                       }
+               }
+               return cfgItem
+       case "postgres":
+               // user=jiahuachen dbname=macaron port=5432 sslmode=disable
+               if !strings.HasPrefix(cfgItem, "postgres://") {
+                       return shadowPasswordKV(cfgItem, " ")
+               }
+               fallthrough
+       case "couchbase":
+               return shadowURL(provider, cfgItem)
+               // postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full
+               // Notice: use shadowURL
+       }
+       return cfgItem
+}
+
+// Config show admin config page
+func Config(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("admin.config")
+       ctx.Data["PageIsAdmin"] = true
+       ctx.Data["PageIsAdminConfig"] = true
+
+       ctx.Data["CustomConf"] = setting.CustomConf
+       ctx.Data["AppUrl"] = setting.AppURL
+       ctx.Data["Domain"] = setting.Domain
+       ctx.Data["OfflineMode"] = setting.OfflineMode
+       ctx.Data["DisableRouterLog"] = setting.DisableRouterLog
+       ctx.Data["RunUser"] = setting.RunUser
+       ctx.Data["RunMode"] = strings.Title(setting.RunMode)
+       if version, err := git.LocalVersion(); err == nil {
+               ctx.Data["GitVersion"] = version.Original()
+       }
+       ctx.Data["RepoRootPath"] = setting.RepoRootPath
+       ctx.Data["CustomRootPath"] = setting.CustomPath
+       ctx.Data["StaticRootPath"] = setting.StaticRootPath
+       ctx.Data["LogRootPath"] = setting.LogRootPath
+       ctx.Data["ScriptType"] = setting.ScriptType
+       ctx.Data["ReverseProxyAuthUser"] = setting.ReverseProxyAuthUser
+       ctx.Data["ReverseProxyAuthEmail"] = setting.ReverseProxyAuthEmail
+
+       ctx.Data["SSH"] = setting.SSH
+       ctx.Data["LFS"] = setting.LFS
+
+       ctx.Data["Service"] = setting.Service
+       ctx.Data["DbCfg"] = setting.Database
+       ctx.Data["Webhook"] = setting.Webhook
+
+       ctx.Data["MailerEnabled"] = false
+       if setting.MailService != nil {
+               ctx.Data["MailerEnabled"] = true
+               ctx.Data["Mailer"] = setting.MailService
+       }
+
+       ctx.Data["CacheAdapter"] = setting.CacheService.Adapter
+       ctx.Data["CacheInterval"] = setting.CacheService.Interval
+
+       ctx.Data["CacheConn"] = shadowPassword(setting.CacheService.Adapter, setting.CacheService.Conn)
+       ctx.Data["CacheItemTTL"] = setting.CacheService.TTL
+
+       sessionCfg := setting.SessionConfig
+       if sessionCfg.Provider == "VirtualSession" {
+               var realSession session.Options
+               json := jsoniter.ConfigCompatibleWithStandardLibrary
+               if err := json.Unmarshal([]byte(sessionCfg.ProviderConfig), &realSession); err != nil {
+                       log.Error("Unable to unmarshall session config for virtualed provider config: %s\nError: %v", sessionCfg.ProviderConfig, err)
+               }
+               sessionCfg.Provider = realSession.Provider
+               sessionCfg.ProviderConfig = realSession.ProviderConfig
+               sessionCfg.CookieName = realSession.CookieName
+               sessionCfg.CookiePath = realSession.CookiePath
+               sessionCfg.Gclifetime = realSession.Gclifetime
+               sessionCfg.Maxlifetime = realSession.Maxlifetime
+               sessionCfg.Secure = realSession.Secure
+               sessionCfg.Domain = realSession.Domain
+       }
+       sessionCfg.ProviderConfig = shadowPassword(sessionCfg.Provider, sessionCfg.ProviderConfig)
+       ctx.Data["SessionConfig"] = sessionCfg
+
+       ctx.Data["DisableGravatar"] = setting.DisableGravatar
+       ctx.Data["EnableFederatedAvatar"] = setting.EnableFederatedAvatar
+
+       ctx.Data["Git"] = setting.Git
+
+       type envVar struct {
+               Name, Value string
+       }
+
+       envVars := map[string]*envVar{}
+       if len(os.Getenv("GITEA_WORK_DIR")) > 0 {
+               envVars["GITEA_WORK_DIR"] = &envVar{"GITEA_WORK_DIR", os.Getenv("GITEA_WORK_DIR")}
+       }
+       if len(os.Getenv("GITEA_CUSTOM")) > 0 {
+               envVars["GITEA_CUSTOM"] = &envVar{"GITEA_CUSTOM", os.Getenv("GITEA_CUSTOM")}
+       }
+
+       ctx.Data["EnvVars"] = envVars
+       ctx.Data["Loggers"] = setting.GetLogDescriptions()
+       ctx.Data["EnableAccessLog"] = setting.EnableAccessLog
+       ctx.Data["AccessLogTemplate"] = setting.AccessLogTemplate
+       ctx.Data["DisableRouterLog"] = setting.DisableRouterLog
+       ctx.Data["EnableXORMLog"] = setting.EnableXORMLog
+       ctx.Data["LogSQL"] = setting.Database.LogSQL
+
+       ctx.HTML(http.StatusOK, tplConfig)
+}
+
+// Monitor show admin monitor page
+func Monitor(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("admin.monitor")
+       ctx.Data["PageIsAdmin"] = true
+       ctx.Data["PageIsAdminMonitor"] = true
+       ctx.Data["Processes"] = process.GetManager().Processes()
+       ctx.Data["Entries"] = cron.ListTasks()
+       ctx.Data["Queues"] = queue.GetManager().ManagedQueues()
+       ctx.HTML(http.StatusOK, tplMonitor)
+}
+
+// MonitorCancel cancels a process
+func MonitorCancel(ctx *context.Context) {
+       pid := ctx.ParamsInt64("pid")
+       process.GetManager().Cancel(pid)
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": setting.AppSubURL + "/admin/monitor",
+       })
+}
+
+// Queue shows details for a specific queue
+func Queue(ctx *context.Context) {
+       qid := ctx.ParamsInt64("qid")
+       mq := queue.GetManager().GetManagedQueue(qid)
+       if mq == nil {
+               ctx.Status(404)
+               return
+       }
+       ctx.Data["Title"] = ctx.Tr("admin.monitor.queue", mq.Name)
+       ctx.Data["PageIsAdmin"] = true
+       ctx.Data["PageIsAdminMonitor"] = true
+       ctx.Data["Queue"] = mq
+       ctx.HTML(http.StatusOK, tplQueue)
+}
+
+// WorkerCancel cancels a worker group
+func WorkerCancel(ctx *context.Context) {
+       qid := ctx.ParamsInt64("qid")
+       mq := queue.GetManager().GetManagedQueue(qid)
+       if mq == nil {
+               ctx.Status(404)
+               return
+       }
+       pid := ctx.ParamsInt64("pid")
+       mq.CancelWorkers(pid)
+       ctx.Flash.Info(ctx.Tr("admin.monitor.queue.pool.cancelling"))
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10),
+       })
+}
+
+// Flush flushes a queue
+func Flush(ctx *context.Context) {
+       qid := ctx.ParamsInt64("qid")
+       mq := queue.GetManager().GetManagedQueue(qid)
+       if mq == nil {
+               ctx.Status(404)
+               return
+       }
+       timeout, err := time.ParseDuration(ctx.Query("timeout"))
+       if err != nil {
+               timeout = -1
+       }
+       ctx.Flash.Info(ctx.Tr("admin.monitor.queue.pool.flush.added", mq.Name))
+       go func() {
+               err := mq.Flush(timeout)
+               if err != nil {
+                       log.Error("Flushing failure for %s: Error %v", mq.Name, err)
+               }
+       }()
+       ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
+}
+
+// AddWorkers adds workers to a worker group
+func AddWorkers(ctx *context.Context) {
+       qid := ctx.ParamsInt64("qid")
+       mq := queue.GetManager().GetManagedQueue(qid)
+       if mq == nil {
+               ctx.Status(404)
+               return
+       }
+       number := ctx.QueryInt("number")
+       if number < 1 {
+               ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.addworkers.mustnumbergreaterzero"))
+               ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
+               return
+       }
+       timeout, err := time.ParseDuration(ctx.Query("timeout"))
+       if err != nil {
+               ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.addworkers.musttimeoutduration"))
+               ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
+               return
+       }
+       if _, ok := mq.Managed.(queue.ManagedPool); !ok {
+               ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.none"))
+               ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
+               return
+       }
+       mq.AddWorkers(number, timeout)
+       ctx.Flash.Success(ctx.Tr("admin.monitor.queue.pool.added"))
+       ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
+}
+
+// SetQueueSettings sets the maximum number of workers and other settings for this queue
+func SetQueueSettings(ctx *context.Context) {
+       qid := ctx.ParamsInt64("qid")
+       mq := queue.GetManager().GetManagedQueue(qid)
+       if mq == nil {
+               ctx.Status(404)
+               return
+       }
+       if _, ok := mq.Managed.(queue.ManagedPool); !ok {
+               ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.none"))
+               ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
+               return
+       }
+
+       maxNumberStr := ctx.Query("max-number")
+       numberStr := ctx.Query("number")
+       timeoutStr := ctx.Query("timeout")
+
+       var err error
+       var maxNumber, number int
+       var timeout time.Duration
+       if len(maxNumberStr) > 0 {
+               maxNumber, err = strconv.Atoi(maxNumberStr)
+               if err != nil {
+                       ctx.Flash.Error(ctx.Tr("admin.monitor.queue.settings.maxnumberworkers.error"))
+                       ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
+                       return
+               }
+               if maxNumber < -1 {
+                       maxNumber = -1
+               }
+       } else {
+               maxNumber = mq.MaxNumberOfWorkers()
+       }
+
+       if len(numberStr) > 0 {
+               number, err = strconv.Atoi(numberStr)
+               if err != nil || number < 0 {
+                       ctx.Flash.Error(ctx.Tr("admin.monitor.queue.settings.numberworkers.error"))
+                       ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
+                       return
+               }
+       } else {
+               number = mq.BoostWorkers()
+       }
+
+       if len(timeoutStr) > 0 {
+               timeout, err = time.ParseDuration(timeoutStr)
+               if err != nil {
+                       ctx.Flash.Error(ctx.Tr("admin.monitor.queue.settings.timeout.error"))
+                       ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
+                       return
+               }
+       } else {
+               timeout = mq.BoostTimeout()
+       }
+
+       mq.SetPoolSettings(maxNumber, number, timeout)
+       ctx.Flash.Success(ctx.Tr("admin.monitor.queue.settings.changed"))
+       ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
+}
diff --git a/routers/web/admin/admin_test.go b/routers/web/admin/admin_test.go
new file mode 100644 (file)
index 0000000..da404e5
--- /dev/null
@@ -0,0 +1,69 @@
+// Copyright 2019 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 admin
+
+import (
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestShadowPassword(t *testing.T) {
+       var kases = []struct {
+               Provider string
+               CfgItem  string
+               Result   string
+       }{
+               {
+                       Provider: "redis",
+                       CfgItem:  "network=tcp,addr=:6379,password=gitea,db=0,pool_size=100,idle_timeout=180",
+                       Result:   "network=tcp,addr=:6379,password=******,db=0,pool_size=100,idle_timeout=180",
+               },
+               {
+                       Provider: "mysql",
+                       CfgItem:  "root:@tcp(localhost:3306)/gitea?charset=utf8",
+                       Result:   "root:******@tcp(localhost:3306)/gitea?charset=utf8",
+               },
+               {
+                       Provider: "mysql",
+                       CfgItem:  "/gitea?charset=utf8",
+                       Result:   "/gitea?charset=utf8",
+               },
+               {
+                       Provider: "mysql",
+                       CfgItem:  "user:mypassword@/dbname",
+                       Result:   "user:******@/dbname",
+               },
+               {
+                       Provider: "postgres",
+                       CfgItem:  "user=pqgotest dbname=pqgotest sslmode=verify-full",
+                       Result:   "user=pqgotest dbname=pqgotest sslmode=verify-full",
+               },
+               {
+                       Provider: "postgres",
+                       CfgItem:  "user=pqgotest password= dbname=pqgotest sslmode=verify-full",
+                       Result:   "user=pqgotest password=****** dbname=pqgotest sslmode=verify-full",
+               },
+               {
+                       Provider: "postgres",
+                       CfgItem:  "postgres://user:pass@hostname/dbname",
+                       Result:   "postgres://user:******@hostname/dbname",
+               },
+               {
+                       Provider: "couchbase",
+                       CfgItem:  "http://dev-couchbase.example.com:8091/",
+                       Result:   "http://dev-couchbase.example.com:8091/",
+               },
+               {
+                       Provider: "couchbase",
+                       CfgItem:  "http://user:the_password@dev-couchbase.example.com:8091/",
+                       Result:   "http://user:******@dev-couchbase.example.com:8091/",
+               },
+       }
+
+       for _, k := range kases {
+               assert.EqualValues(t, k.Result, shadowPassword(k.Provider, k.CfgItem))
+       }
+}
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go
new file mode 100644 (file)
index 0000000..a2f9ab0
--- /dev/null
@@ -0,0 +1,410 @@
+// Copyright 2014 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 admin
+
+import (
+       "errors"
+       "fmt"
+       "net/http"
+       "regexp"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/auth/ldap"
+       "code.gitea.io/gitea/modules/auth/oauth2"
+       "code.gitea.io/gitea/modules/auth/pam"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/util"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+
+       "xorm.io/xorm/convert"
+)
+
+const (
+       tplAuths    base.TplName = "admin/auth/list"
+       tplAuthNew  base.TplName = "admin/auth/new"
+       tplAuthEdit base.TplName = "admin/auth/edit"
+)
+
+var (
+       separatorAntiPattern = regexp.MustCompile(`[^\w-\.]`)
+       langCodePattern      = regexp.MustCompile(`^[a-z]{2}-[A-Z]{2}$`)
+)
+
+// Authentications show authentication config page
+func Authentications(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("admin.authentication")
+       ctx.Data["PageIsAdmin"] = true
+       ctx.Data["PageIsAdminAuthentications"] = true
+
+       var err error
+       ctx.Data["Sources"], err = models.LoginSources()
+       if err != nil {
+               ctx.ServerError("LoginSources", err)
+               return
+       }
+
+       ctx.Data["Total"] = models.CountLoginSources()
+       ctx.HTML(http.StatusOK, tplAuths)
+}
+
+type dropdownItem struct {
+       Name string
+       Type interface{}
+}
+
+var (
+       authSources = func() []dropdownItem {
+               items := []dropdownItem{
+                       {models.LoginNames[models.LoginLDAP], models.LoginLDAP},
+                       {models.LoginNames[models.LoginDLDAP], models.LoginDLDAP},
+                       {models.LoginNames[models.LoginSMTP], models.LoginSMTP},
+                       {models.LoginNames[models.LoginOAuth2], models.LoginOAuth2},
+                       {models.LoginNames[models.LoginSSPI], models.LoginSSPI},
+               }
+               if pam.Supported {
+                       items = append(items, dropdownItem{models.LoginNames[models.LoginPAM], models.LoginPAM})
+               }
+               return items
+       }()
+
+       securityProtocols = []dropdownItem{
+               {models.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted], ldap.SecurityProtocolUnencrypted},
+               {models.SecurityProtocolNames[ldap.SecurityProtocolLDAPS], ldap.SecurityProtocolLDAPS},
+               {models.SecurityProtocolNames[ldap.SecurityProtocolStartTLS], ldap.SecurityProtocolStartTLS},
+       }
+)
+
+// NewAuthSource render adding a new auth source page
+func NewAuthSource(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("admin.auths.new")
+       ctx.Data["PageIsAdmin"] = true
+       ctx.Data["PageIsAdminAuthentications"] = true
+
+       ctx.Data["type"] = models.LoginLDAP
+       ctx.Data["CurrentTypeName"] = models.LoginNames[models.LoginLDAP]
+       ctx.Data["CurrentSecurityProtocol"] = models.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted]
+       ctx.Data["smtp_auth"] = "PLAIN"
+       ctx.Data["is_active"] = true
+       ctx.Data["is_sync_enabled"] = true
+       ctx.Data["AuthSources"] = authSources
+       ctx.Data["SecurityProtocols"] = securityProtocols
+       ctx.Data["SMTPAuths"] = models.SMTPAuths
+       ctx.Data["OAuth2Providers"] = models.OAuth2Providers
+       ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
+
+       ctx.Data["SSPIAutoCreateUsers"] = true
+       ctx.Data["SSPIAutoActivateUsers"] = true
+       ctx.Data["SSPIStripDomainNames"] = true
+       ctx.Data["SSPISeparatorReplacement"] = "_"
+       ctx.Data["SSPIDefaultLanguage"] = ""
+
+       // only the first as default
+       for key := range models.OAuth2Providers {
+               ctx.Data["oauth2_provider"] = key
+               break
+       }
+
+       ctx.HTML(http.StatusOK, tplAuthNew)
+}
+
+func parseLDAPConfig(form forms.AuthenticationForm) *models.LDAPConfig {
+       var pageSize uint32
+       if form.UsePagedSearch {
+               pageSize = uint32(form.SearchPageSize)
+       }
+       return &models.LDAPConfig{
+               Source: &ldap.Source{
+                       Name:                  form.Name,
+                       Host:                  form.Host,
+                       Port:                  form.Port,
+                       SecurityProtocol:      ldap.SecurityProtocol(form.SecurityProtocol),
+                       SkipVerify:            form.SkipVerify,
+                       BindDN:                form.BindDN,
+                       UserDN:                form.UserDN,
+                       BindPassword:          form.BindPassword,
+                       UserBase:              form.UserBase,
+                       AttributeUsername:     form.AttributeUsername,
+                       AttributeName:         form.AttributeName,
+                       AttributeSurname:      form.AttributeSurname,
+                       AttributeMail:         form.AttributeMail,
+                       AttributesInBind:      form.AttributesInBind,
+                       AttributeSSHPublicKey: form.AttributeSSHPublicKey,
+                       SearchPageSize:        pageSize,
+                       Filter:                form.Filter,
+                       GroupsEnabled:         form.GroupsEnabled,
+                       GroupDN:               form.GroupDN,
+                       GroupFilter:           form.GroupFilter,
+                       GroupMemberUID:        form.GroupMemberUID,
+                       UserUID:               form.UserUID,
+                       AdminFilter:           form.AdminFilter,
+                       RestrictedFilter:      form.RestrictedFilter,
+                       AllowDeactivateAll:    form.AllowDeactivateAll,
+                       Enabled:               true,
+               },
+       }
+}
+
+func parseSMTPConfig(form forms.AuthenticationForm) *models.SMTPConfig {
+       return &models.SMTPConfig{
+               Auth:           form.SMTPAuth,
+               Host:           form.SMTPHost,
+               Port:           form.SMTPPort,
+               AllowedDomains: form.AllowedDomains,
+               TLS:            form.TLS,
+               SkipVerify:     form.SkipVerify,
+       }
+}
+
+func parseOAuth2Config(form forms.AuthenticationForm) *models.OAuth2Config {
+       var customURLMapping *oauth2.CustomURLMapping
+       if form.Oauth2UseCustomURL {
+               customURLMapping = &oauth2.CustomURLMapping{
+                       TokenURL:   form.Oauth2TokenURL,
+                       AuthURL:    form.Oauth2AuthURL,
+                       ProfileURL: form.Oauth2ProfileURL,
+                       EmailURL:   form.Oauth2EmailURL,
+               }
+       } else {
+               customURLMapping = nil
+       }
+       return &models.OAuth2Config{
+               Provider:                      form.Oauth2Provider,
+               ClientID:                      form.Oauth2Key,
+               ClientSecret:                  form.Oauth2Secret,
+               OpenIDConnectAutoDiscoveryURL: form.OpenIDConnectAutoDiscoveryURL,
+               CustomURLMapping:              customURLMapping,
+               IconURL:                       form.Oauth2IconURL,
+       }
+}
+
+func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*models.SSPIConfig, error) {
+       if util.IsEmptyString(form.SSPISeparatorReplacement) {
+               ctx.Data["Err_SSPISeparatorReplacement"] = true
+               return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.require_error"))
+       }
+       if separatorAntiPattern.MatchString(form.SSPISeparatorReplacement) {
+               ctx.Data["Err_SSPISeparatorReplacement"] = true
+               return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.alpha_dash_dot_error"))
+       }
+
+       if form.SSPIDefaultLanguage != "" && !langCodePattern.MatchString(form.SSPIDefaultLanguage) {
+               ctx.Data["Err_SSPIDefaultLanguage"] = true
+               return nil, errors.New(ctx.Tr("form.lang_select_error"))
+       }
+
+       return &models.SSPIConfig{
+               AutoCreateUsers:      form.SSPIAutoCreateUsers,
+               AutoActivateUsers:    form.SSPIAutoActivateUsers,
+               StripDomainNames:     form.SSPIStripDomainNames,
+               SeparatorReplacement: form.SSPISeparatorReplacement,
+               DefaultLanguage:      form.SSPIDefaultLanguage,
+       }, nil
+}
+
+// NewAuthSourcePost response for adding an auth source
+func NewAuthSourcePost(ctx *context.Context) {
+       form := *web.GetForm(ctx).(*forms.AuthenticationForm)
+       ctx.Data["Title"] = ctx.Tr("admin.auths.new")
+       ctx.Data["PageIsAdmin"] = true
+       ctx.Data["PageIsAdminAuthentications"] = true
+
+       ctx.Data["CurrentTypeName"] = models.LoginNames[models.LoginType(form.Type)]
+       ctx.Data["CurrentSecurityProtocol"] = models.SecurityProtocolNames[ldap.SecurityProtocol(form.SecurityProtocol)]
+       ctx.Data["AuthSources"] = authSources
+       ctx.Data["SecurityProtocols"] = securityProtocols
+       ctx.Data["SMTPAuths"] = models.SMTPAuths
+       ctx.Data["OAuth2Providers"] = models.OAuth2Providers
+       ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
+
+       ctx.Data["SSPIAutoCreateUsers"] = true
+       ctx.Data["SSPIAutoActivateUsers"] = true
+       ctx.Data["SSPIStripDomainNames"] = true
+       ctx.Data["SSPISeparatorReplacement"] = "_"
+       ctx.Data["SSPIDefaultLanguage"] = ""
+
+       hasTLS := false
+       var config convert.Conversion
+       switch models.LoginType(form.Type) {
+       case models.LoginLDAP, models.LoginDLDAP:
+               config = parseLDAPConfig(form)
+               hasTLS = ldap.SecurityProtocol(form.SecurityProtocol) > ldap.SecurityProtocolUnencrypted
+       case models.LoginSMTP:
+               config = parseSMTPConfig(form)
+               hasTLS = true
+       case models.LoginPAM:
+               config = &models.PAMConfig{
+                       ServiceName: form.PAMServiceName,
+                       EmailDomain: form.PAMEmailDomain,
+               }
+       case models.LoginOAuth2:
+               config = parseOAuth2Config(form)
+       case models.LoginSSPI:
+               var err error
+               config, err = parseSSPIConfig(ctx, form)
+               if err != nil {
+                       ctx.RenderWithErr(err.Error(), tplAuthNew, form)
+                       return
+               }
+               existing, err := models.LoginSourcesByType(models.LoginSSPI)
+               if err != nil || len(existing) > 0 {
+                       ctx.Data["Err_Type"] = true
+                       ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_of_type_exist"), tplAuthNew, form)
+                       return
+               }
+       default:
+               ctx.Error(http.StatusBadRequest)
+               return
+       }
+       ctx.Data["HasTLS"] = hasTLS
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplAuthNew)
+               return
+       }
+
+       if err := models.CreateLoginSource(&models.LoginSource{
+               Type:          models.LoginType(form.Type),
+               Name:          form.Name,
+               IsActived:     form.IsActive,
+               IsSyncEnabled: form.IsSyncEnabled,
+               Cfg:           config,
+       }); err != nil {
+               if models.IsErrLoginSourceAlreadyExist(err) {
+                       ctx.Data["Err_Name"] = true
+                       ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_exist", err.(models.ErrLoginSourceAlreadyExist).Name), tplAuthNew, form)
+               } else {
+                       ctx.ServerError("CreateSource", err)
+               }
+               return
+       }
+
+       log.Trace("Authentication created by admin(%s): %s", ctx.User.Name, form.Name)
+
+       ctx.Flash.Success(ctx.Tr("admin.auths.new_success", form.Name))
+       ctx.Redirect(setting.AppSubURL + "/admin/auths")
+}
+
+// EditAuthSource render editing auth source page
+func EditAuthSource(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("admin.auths.edit")
+       ctx.Data["PageIsAdmin"] = true
+       ctx.Data["PageIsAdminAuthentications"] = true
+
+       ctx.Data["SecurityProtocols"] = securityProtocols
+       ctx.Data["SMTPAuths"] = models.SMTPAuths
+       ctx.Data["OAuth2Providers"] = models.OAuth2Providers
+       ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
+
+       source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
+       if err != nil {
+               ctx.ServerError("GetLoginSourceByID", err)
+               return
+       }
+       ctx.Data["Source"] = source
+       ctx.Data["HasTLS"] = source.HasTLS()
+
+       if source.IsOAuth2() {
+               ctx.Data["CurrentOAuth2Provider"] = models.OAuth2Providers[source.OAuth2().Provider]
+       }
+       ctx.HTML(http.StatusOK, tplAuthEdit)
+}
+
+// EditAuthSourcePost response for editing auth source
+func EditAuthSourcePost(ctx *context.Context) {
+       form := *web.GetForm(ctx).(*forms.AuthenticationForm)
+       ctx.Data["Title"] = ctx.Tr("admin.auths.edit")
+       ctx.Data["PageIsAdmin"] = true
+       ctx.Data["PageIsAdminAuthentications"] = true
+
+       ctx.Data["SMTPAuths"] = models.SMTPAuths
+       ctx.Data["OAuth2Providers"] = models.OAuth2Providers
+       ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
+
+       source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
+       if err != nil {
+               ctx.ServerError("GetLoginSourceByID", err)
+               return
+       }
+       ctx.Data["Source"] = source
+       ctx.Data["HasTLS"] = source.HasTLS()
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplAuthEdit)
+               return
+       }
+
+       var config convert.Conversion
+       switch models.LoginType(form.Type) {
+       case models.LoginLDAP, models.LoginDLDAP:
+               config = parseLDAPConfig(form)
+       case models.LoginSMTP:
+               config = parseSMTPConfig(form)
+       case models.LoginPAM:
+               config = &models.PAMConfig{
+                       ServiceName: form.PAMServiceName,
+                       EmailDomain: form.PAMEmailDomain,
+               }
+       case models.LoginOAuth2:
+               config = parseOAuth2Config(form)
+       case models.LoginSSPI:
+               config, err = parseSSPIConfig(ctx, form)
+               if err != nil {
+                       ctx.RenderWithErr(err.Error(), tplAuthEdit, form)
+                       return
+               }
+       default:
+               ctx.Error(http.StatusBadRequest)
+               return
+       }
+
+       source.Name = form.Name
+       source.IsActived = form.IsActive
+       source.IsSyncEnabled = form.IsSyncEnabled
+       source.Cfg = config
+       if err := models.UpdateSource(source); err != nil {
+               if models.IsErrOpenIDConnectInitialize(err) {
+                       ctx.Flash.Error(err.Error(), true)
+                       ctx.HTML(http.StatusOK, tplAuthEdit)
+               } else {
+                       ctx.ServerError("UpdateSource", err)
+               }
+               return
+       }
+       log.Trace("Authentication changed by admin(%s): %d", ctx.User.Name, source.ID)
+
+       ctx.Flash.Success(ctx.Tr("admin.auths.update_success"))
+       ctx.Redirect(setting.AppSubURL + "/admin/auths/" + fmt.Sprint(form.ID))
+}
+
+// DeleteAuthSource response for deleting an auth source
+func DeleteAuthSource(ctx *context.Context) {
+       source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
+       if err != nil {
+               ctx.ServerError("GetLoginSourceByID", err)
+               return
+       }
+
+       if err = models.DeleteSource(source); err != nil {
+               if models.IsErrLoginSourceInUse(err) {
+                       ctx.Flash.Error(ctx.Tr("admin.auths.still_in_used"))
+               } else {
+                       ctx.Flash.Error(fmt.Sprintf("DeleteSource: %v", err))
+               }
+               ctx.JSON(http.StatusOK, map[string]interface{}{
+                       "redirect": setting.AppSubURL + "/admin/auths/" + ctx.Params(":authid"),
+               })
+               return
+       }
+       log.Trace("Authentication deleted by admin(%s): %d", ctx.User.Name, source.ID)
+
+       ctx.Flash.Success(ctx.Tr("admin.auths.deletion_success"))
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": setting.AppSubURL + "/admin/auths",
+       })
+}
diff --git a/routers/web/admin/emails.go b/routers/web/admin/emails.go
new file mode 100644 (file)
index 0000000..f7e8c97
--- /dev/null
@@ -0,0 +1,156 @@
+// Copyright 2020 The Gitea Authors.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package admin
+
+import (
+       "bytes"
+       "net/http"
+       "net/url"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/util"
+)
+
+const (
+       tplEmails base.TplName = "admin/emails/list"
+)
+
+// Emails show all emails
+func Emails(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("admin.emails")
+       ctx.Data["PageIsAdmin"] = true
+       ctx.Data["PageIsAdminEmails"] = true
+
+       opts := &models.SearchEmailOptions{
+               ListOptions: models.ListOptions{
+                       PageSize: setting.UI.Admin.UserPagingNum,
+                       Page:     ctx.QueryInt("page"),
+               },
+       }
+
+       if opts.Page <= 1 {
+               opts.Page = 1
+       }
+
+       type ActiveEmail struct {
+               models.SearchEmailResult
+               CanChange bool
+       }
+
+       var (
+               baseEmails []*models.SearchEmailResult
+               emails     []ActiveEmail
+               count      int64
+               err        error
+               orderBy    models.SearchEmailOrderBy
+       )
+
+       ctx.Data["SortType"] = ctx.Query("sort")
+       switch ctx.Query("sort") {
+       case "email":
+               orderBy = models.SearchEmailOrderByEmail
+       case "reverseemail":
+               orderBy = models.SearchEmailOrderByEmailReverse
+       case "username":
+               orderBy = models.SearchEmailOrderByName
+       case "reverseusername":
+               orderBy = models.SearchEmailOrderByNameReverse
+       default:
+               ctx.Data["SortType"] = "email"
+               orderBy = models.SearchEmailOrderByEmail
+       }
+
+       opts.Keyword = ctx.QueryTrim("q")
+       opts.SortType = orderBy
+       if len(ctx.Query("is_activated")) != 0 {
+               opts.IsActivated = util.OptionalBoolOf(ctx.QueryBool("activated"))
+       }
+       if len(ctx.Query("is_primary")) != 0 {
+               opts.IsPrimary = util.OptionalBoolOf(ctx.QueryBool("primary"))
+       }
+
+       if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
+               baseEmails, count, err = models.SearchEmails(opts)
+               if err != nil {
+                       ctx.ServerError("SearchEmails", err)
+                       return
+               }
+               emails = make([]ActiveEmail, len(baseEmails))
+               for i := range baseEmails {
+                       emails[i].SearchEmailResult = *baseEmails[i]
+                       // Don't let the admin deactivate its own primary email address
+                       // We already know the user is admin
+                       emails[i].CanChange = ctx.User.ID != emails[i].UID || !emails[i].IsPrimary
+               }
+       }
+       ctx.Data["Keyword"] = opts.Keyword
+       ctx.Data["Total"] = count
+       ctx.Data["Emails"] = emails
+
+       pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
+       pager.SetDefaultParams(ctx)
+       ctx.Data["Page"] = pager
+
+       ctx.HTML(http.StatusOK, tplEmails)
+}
+
+var (
+       nullByte = []byte{0x00}
+)
+
+func isKeywordValid(keyword string) bool {
+       return !bytes.Contains([]byte(keyword), nullByte)
+}
+
+// ActivateEmail serves a POST request for activating/deactivating a user's email
+func ActivateEmail(ctx *context.Context) {
+
+       truefalse := map[string]bool{"1": true, "0": false}
+
+       uid := ctx.QueryInt64("uid")
+       email := ctx.Query("email")
+       primary, okp := truefalse[ctx.Query("primary")]
+       activate, oka := truefalse[ctx.Query("activate")]
+
+       if uid == 0 || len(email) == 0 || !okp || !oka {
+               ctx.Error(http.StatusBadRequest)
+               return
+       }
+
+       log.Info("Changing activation for User ID: %d, email: %s, primary: %v to %v", uid, email, primary, activate)
+
+       if err := models.ActivateUserEmail(uid, email, primary, activate); err != nil {
+               log.Error("ActivateUserEmail(%v,%v,%v,%v): %v", uid, email, primary, activate, err)
+               if models.IsErrEmailAlreadyUsed(err) {
+                       ctx.Flash.Error(ctx.Tr("admin.emails.duplicate_active"))
+               } else {
+                       ctx.Flash.Error(ctx.Tr("admin.emails.not_updated", err))
+               }
+       } else {
+               log.Info("Activation for User ID: %d, email: %s, primary: %v changed to %v", uid, email, primary, activate)
+               ctx.Flash.Info(ctx.Tr("admin.emails.updated"))
+       }
+
+       redirect, _ := url.Parse(setting.AppSubURL + "/admin/emails")
+       q := url.Values{}
+       if val := ctx.QueryTrim("q"); len(val) > 0 {
+               q.Set("q", val)
+       }
+       if val := ctx.QueryTrim("sort"); len(val) > 0 {
+               q.Set("sort", val)
+       }
+       if val := ctx.QueryTrim("is_primary"); len(val) > 0 {
+               q.Set("is_primary", val)
+       }
+       if val := ctx.QueryTrim("is_activated"); len(val) > 0 {
+               q.Set("is_activated", val)
+       }
+       redirect.RawQuery = q.Encode()
+       ctx.Redirect(redirect.String())
+}
diff --git a/routers/web/admin/hooks.go b/routers/web/admin/hooks.go
new file mode 100644 (file)
index 0000000..ff32260
--- /dev/null
@@ -0,0 +1,72 @@
+// Copyright 2018 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 admin
+
+import (
+       "net/http"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/setting"
+)
+
+const (
+       // tplAdminHooks template path to render hook settings
+       tplAdminHooks base.TplName = "admin/hooks"
+)
+
+// DefaultOrSystemWebhooks renders both admin default and system webhook list pages
+func DefaultOrSystemWebhooks(ctx *context.Context) {
+       var err error
+
+       ctx.Data["PageIsAdminSystemHooks"] = true
+       ctx.Data["PageIsAdminDefaultHooks"] = true
+
+       def := make(map[string]interface{}, len(ctx.Data))
+       sys := make(map[string]interface{}, len(ctx.Data))
+       for k, v := range ctx.Data {
+               def[k] = v
+               sys[k] = v
+       }
+
+       sys["Title"] = ctx.Tr("admin.systemhooks")
+       sys["Description"] = ctx.Tr("admin.systemhooks.desc")
+       sys["Webhooks"], err = models.GetSystemWebhooks()
+       sys["BaseLink"] = setting.AppSubURL + "/admin/hooks"
+       sys["BaseLinkNew"] = setting.AppSubURL + "/admin/system-hooks"
+       if err != nil {
+               ctx.ServerError("GetWebhooksAdmin", err)
+               return
+       }
+
+       def["Title"] = ctx.Tr("admin.defaulthooks")
+       def["Description"] = ctx.Tr("admin.defaulthooks.desc")
+       def["Webhooks"], err = models.GetDefaultWebhooks()
+       def["BaseLink"] = setting.AppSubURL + "/admin/hooks"
+       def["BaseLinkNew"] = setting.AppSubURL + "/admin/default-hooks"
+       if err != nil {
+               ctx.ServerError("GetWebhooksAdmin", err)
+               return
+       }
+
+       ctx.Data["DefaultWebhooks"] = def
+       ctx.Data["SystemWebhooks"] = sys
+
+       ctx.HTML(http.StatusOK, tplAdminHooks)
+}
+
+// DeleteDefaultOrSystemWebhook handler to delete an admin-defined system or default webhook
+func DeleteDefaultOrSystemWebhook(ctx *context.Context) {
+       if err := models.DeleteDefaultSystemWebhook(ctx.QueryInt64("id")); err != nil {
+               ctx.Flash.Error("DeleteDefaultWebhook: " + err.Error())
+       } else {
+               ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": setting.AppSubURL + "/admin/hooks",
+       })
+}
diff --git a/routers/web/admin/main_test.go b/routers/web/admin/main_test.go
new file mode 100644 (file)
index 0000000..352907c
--- /dev/null
@@ -0,0 +1,16 @@
+// Copyright 2018 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 admin
+
+import (
+       "path/filepath"
+       "testing"
+
+       "code.gitea.io/gitea/models"
+)
+
+func TestMain(m *testing.M) {
+       models.MainTest(m, filepath.Join("..", "..", ".."))
+}
diff --git a/routers/web/admin/notice.go b/routers/web/admin/notice.go
new file mode 100644 (file)
index 0000000..e2ebd0d
--- /dev/null
@@ -0,0 +1,79 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2019 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 admin
+
+import (
+       "net/http"
+       "strconv"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+)
+
+const (
+       tplNotices base.TplName = "admin/notice"
+)
+
+// Notices show notices for admin
+func Notices(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("admin.notices")
+       ctx.Data["PageIsAdmin"] = true
+       ctx.Data["PageIsAdminNotices"] = true
+
+       total := models.CountNotices()
+       page := ctx.QueryInt("page")
+       if page <= 1 {
+               page = 1
+       }
+
+       notices, err := models.Notices(page, setting.UI.Admin.NoticePagingNum)
+       if err != nil {
+               ctx.ServerError("Notices", err)
+               return
+       }
+       ctx.Data["Notices"] = notices
+
+       ctx.Data["Total"] = total
+
+       ctx.Data["Page"] = context.NewPagination(int(total), setting.UI.Admin.NoticePagingNum, page, 5)
+
+       ctx.HTML(http.StatusOK, tplNotices)
+}
+
+// DeleteNotices delete the specific notices
+func DeleteNotices(ctx *context.Context) {
+       strs := ctx.QueryStrings("ids[]")
+       ids := make([]int64, 0, len(strs))
+       for i := range strs {
+               id, _ := strconv.ParseInt(strs[i], 10, 64)
+               if id > 0 {
+                       ids = append(ids, id)
+               }
+       }
+
+       if err := models.DeleteNoticesByIDs(ids); err != nil {
+               ctx.Flash.Error("DeleteNoticesByIDs: " + err.Error())
+               ctx.Status(500)
+       } else {
+               ctx.Flash.Success(ctx.Tr("admin.notices.delete_success"))
+               ctx.Status(200)
+       }
+}
+
+// EmptyNotices delete all the notices
+func EmptyNotices(ctx *context.Context) {
+       if err := models.DeleteNotices(0, 0); err != nil {
+               ctx.ServerError("DeleteNotices", err)
+               return
+       }
+
+       log.Trace("System notices deleted by admin (%s): [start: %d]", ctx.User.Name, 0)
+       ctx.Flash.Success(ctx.Tr("admin.notices.delete_success"))
+       ctx.Redirect(setting.AppSubURL + "/admin/notices")
+}
diff --git a/routers/web/admin/orgs.go b/routers/web/admin/orgs.go
new file mode 100644 (file)
index 0000000..618f945
--- /dev/null
@@ -0,0 +1,34 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2020 The Gitea Authors.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package admin
+
+import (
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/structs"
+       "code.gitea.io/gitea/routers/web/explore"
+)
+
+const (
+       tplOrgs base.TplName = "admin/org/list"
+)
+
+// Organizations show all the organizations
+func Organizations(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("admin.organizations")
+       ctx.Data["PageIsAdmin"] = true
+       ctx.Data["PageIsAdminOrganizations"] = true
+
+       explore.RenderUserSearch(ctx, &models.SearchUserOptions{
+               Type: models.UserTypeOrganization,
+               ListOptions: models.ListOptions{
+                       PageSize: setting.UI.Admin.OrgPagingNum,
+               },
+               Visible: []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate},
+       }, tplOrgs)
+}
diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go
new file mode 100644 (file)
index 0000000..6128992
--- /dev/null
@@ -0,0 +1,166 @@
+// Copyright 2014 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 admin
+
+import (
+       "net/http"
+       "net/url"
+       "strconv"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/repository"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/util"
+       "code.gitea.io/gitea/routers/web/explore"
+       repo_service "code.gitea.io/gitea/services/repository"
+)
+
+const (
+       tplRepos          base.TplName = "admin/repo/list"
+       tplUnadoptedRepos base.TplName = "admin/repo/unadopted"
+)
+
+// Repos show all the repositories
+func Repos(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("admin.repositories")
+       ctx.Data["PageIsAdmin"] = true
+       ctx.Data["PageIsAdminRepositories"] = true
+
+       explore.RenderRepoSearch(ctx, &explore.RepoSearchOptions{
+               Private:  true,
+               PageSize: setting.UI.Admin.RepoPagingNum,
+               TplName:  tplRepos,
+       })
+}
+
+// DeleteRepo delete one repository
+func DeleteRepo(ctx *context.Context) {
+       repo, err := models.GetRepositoryByID(ctx.QueryInt64("id"))
+       if err != nil {
+               ctx.ServerError("GetRepositoryByID", err)
+               return
+       }
+
+       if ctx.Repo != nil && ctx.Repo.GitRepo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == repo.ID {
+               ctx.Repo.GitRepo.Close()
+       }
+
+       if err := repo_service.DeleteRepository(ctx.User, repo); err != nil {
+               ctx.ServerError("DeleteRepository", err)
+               return
+       }
+       log.Trace("Repository deleted: %s", repo.FullName())
+
+       ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success"))
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": setting.AppSubURL + "/admin/repos?page=" + ctx.Query("page") + "&sort=" + ctx.Query("sort"),
+       })
+}
+
+// UnadoptedRepos lists the unadopted repositories
+func UnadoptedRepos(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("admin.repositories")
+       ctx.Data["PageIsAdmin"] = true
+       ctx.Data["PageIsAdminRepositories"] = true
+
+       opts := models.ListOptions{
+               PageSize: setting.UI.Admin.UserPagingNum,
+               Page:     ctx.QueryInt("page"),
+       }
+
+       if opts.Page <= 0 {
+               opts.Page = 1
+       }
+
+       ctx.Data["CurrentPage"] = opts.Page
+
+       doSearch := ctx.QueryBool("search")
+
+       ctx.Data["search"] = doSearch
+       q := ctx.Query("q")
+
+       if !doSearch {
+               pager := context.NewPagination(0, opts.PageSize, opts.Page, 5)
+               pager.SetDefaultParams(ctx)
+               pager.AddParam(ctx, "search", "search")
+               ctx.Data["Page"] = pager
+               ctx.HTML(http.StatusOK, tplUnadoptedRepos)
+               return
+       }
+
+       ctx.Data["Keyword"] = q
+       repoNames, count, err := repository.ListUnadoptedRepositories(q, &opts)
+       if err != nil {
+               ctx.ServerError("ListUnadoptedRepositories", err)
+       }
+       ctx.Data["Dirs"] = repoNames
+       pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
+       pager.SetDefaultParams(ctx)
+       pager.AddParam(ctx, "search", "search")
+       ctx.Data["Page"] = pager
+       ctx.HTML(http.StatusOK, tplUnadoptedRepos)
+}
+
+// AdoptOrDeleteRepository adopts or deletes a repository
+func AdoptOrDeleteRepository(ctx *context.Context) {
+       dir := ctx.Query("id")
+       action := ctx.Query("action")
+       page := ctx.QueryInt("page")
+       q := ctx.Query("q")
+
+       dirSplit := strings.SplitN(dir, "/", 2)
+       if len(dirSplit) != 2 {
+               ctx.Redirect(setting.AppSubURL + "/admin/repos")
+               return
+       }
+
+       ctxUser, err := models.GetUserByName(dirSplit[0])
+       if err != nil {
+               if models.IsErrUserNotExist(err) {
+                       log.Debug("User does not exist: %s", dirSplit[0])
+                       ctx.Redirect(setting.AppSubURL + "/admin/repos")
+                       return
+               }
+               ctx.ServerError("GetUserByName", err)
+               return
+       }
+
+       repoName := dirSplit[1]
+
+       // check not a repo
+       has, err := models.IsRepositoryExist(ctxUser, repoName)
+       if err != nil {
+               ctx.ServerError("IsRepositoryExist", err)
+               return
+       }
+       isDir, err := util.IsDir(models.RepoPath(ctxUser.Name, repoName))
+       if err != nil {
+               ctx.ServerError("IsDir", err)
+               return
+       }
+       if has || !isDir {
+               // Fallthrough to failure mode
+       } else if action == "adopt" {
+               if _, err := repository.AdoptRepository(ctx.User, ctxUser, models.CreateRepoOptions{
+                       Name:      dirSplit[1],
+                       IsPrivate: true,
+               }); err != nil {
+                       ctx.ServerError("repository.AdoptRepository", err)
+                       return
+               }
+               ctx.Flash.Success(ctx.Tr("repo.adopt_preexisting_success", dir))
+       } else if action == "delete" {
+               if err := repository.DeleteUnadoptedRepository(ctx.User, ctxUser, dirSplit[1]); err != nil {
+                       ctx.ServerError("repository.AdoptRepository", err)
+                       return
+               }
+               ctx.Flash.Success(ctx.Tr("repo.delete_preexisting_success", dir))
+       }
+       ctx.Redirect(setting.AppSubURL + "/admin/repos/unadopted?search=true&q=" + url.QueryEscape(q) + "&page=" + strconv.Itoa(page))
+}
diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go
new file mode 100644 (file)
index 0000000..1b65795
--- /dev/null
@@ -0,0 +1,371 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2020 The Gitea Authors.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package admin
+
+import (
+       "fmt"
+       "net/http"
+       "strconv"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/password"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/routers/web/explore"
+       router_user_setting "code.gitea.io/gitea/routers/web/user/setting"
+       "code.gitea.io/gitea/services/forms"
+       "code.gitea.io/gitea/services/mailer"
+)
+
+const (
+       tplUsers    base.TplName = "admin/user/list"
+       tplUserNew  base.TplName = "admin/user/new"
+       tplUserEdit base.TplName = "admin/user/edit"
+)
+
+// Users show all the users
+func Users(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("admin.users")
+       ctx.Data["PageIsAdmin"] = true
+       ctx.Data["PageIsAdminUsers"] = true
+
+       explore.RenderUserSearch(ctx, &models.SearchUserOptions{
+               Type: models.UserTypeIndividual,
+               ListOptions: models.ListOptions{
+                       PageSize: setting.UI.Admin.UserPagingNum,
+               },
+               SearchByEmail: true,
+       }, tplUsers)
+}
+
+// NewUser render adding a new user page
+func NewUser(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("admin.users.new_account")
+       ctx.Data["PageIsAdmin"] = true
+       ctx.Data["PageIsAdminUsers"] = true
+
+       ctx.Data["login_type"] = "0-0"
+
+       sources, err := models.LoginSources()
+       if err != nil {
+               ctx.ServerError("LoginSources", err)
+               return
+       }
+       ctx.Data["Sources"] = sources
+
+       ctx.Data["CanSendEmail"] = setting.MailService != nil
+       ctx.HTML(http.StatusOK, tplUserNew)
+}
+
+// NewUserPost response for adding a new user
+func NewUserPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.AdminCreateUserForm)
+       ctx.Data["Title"] = ctx.Tr("admin.users.new_account")
+       ctx.Data["PageIsAdmin"] = true
+       ctx.Data["PageIsAdminUsers"] = true
+
+       sources, err := models.LoginSources()
+       if err != nil {
+               ctx.ServerError("LoginSources", err)
+               return
+       }
+       ctx.Data["Sources"] = sources
+
+       ctx.Data["CanSendEmail"] = setting.MailService != nil
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplUserNew)
+               return
+       }
+
+       u := &models.User{
+               Name:      form.UserName,
+               Email:     form.Email,
+               Passwd:    form.Password,
+               IsActive:  true,
+               LoginType: models.LoginPlain,
+       }
+
+       if len(form.LoginType) > 0 {
+               fields := strings.Split(form.LoginType, "-")
+               if len(fields) == 2 {
+                       lType, _ := strconv.ParseInt(fields[0], 10, 0)
+                       u.LoginType = models.LoginType(lType)
+                       u.LoginSource, _ = strconv.ParseInt(fields[1], 10, 64)
+                       u.LoginName = form.LoginName
+               }
+       }
+       if u.LoginType == models.LoginNoType || u.LoginType == models.LoginPlain {
+               if len(form.Password) < setting.MinPasswordLength {
+                       ctx.Data["Err_Password"] = true
+                       ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserNew, &form)
+                       return
+               }
+               if !password.IsComplexEnough(form.Password) {
+                       ctx.Data["Err_Password"] = true
+                       ctx.RenderWithErr(password.BuildComplexityError(ctx), tplUserNew, &form)
+                       return
+               }
+               pwned, err := password.IsPwned(ctx, form.Password)
+               if pwned {
+                       ctx.Data["Err_Password"] = true
+                       errMsg := ctx.Tr("auth.password_pwned")
+                       if err != nil {
+                               log.Error(err.Error())
+                               errMsg = ctx.Tr("auth.password_pwned_err")
+                       }
+                       ctx.RenderWithErr(errMsg, tplUserNew, &form)
+                       return
+               }
+               u.MustChangePassword = form.MustChangePassword
+       }
+       if err := models.CreateUser(u); err != nil {
+               switch {
+               case models.IsErrUserAlreadyExist(err):
+                       ctx.Data["Err_UserName"] = true
+                       ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplUserNew, &form)
+               case models.IsErrEmailAlreadyUsed(err):
+                       ctx.Data["Err_Email"] = true
+                       ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserNew, &form)
+               case models.IsErrEmailInvalid(err):
+                       ctx.Data["Err_Email"] = true
+                       ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserNew, &form)
+               case models.IsErrNameReserved(err):
+                       ctx.Data["Err_UserName"] = true
+                       ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(models.ErrNameReserved).Name), tplUserNew, &form)
+               case models.IsErrNamePatternNotAllowed(err):
+                       ctx.Data["Err_UserName"] = true
+                       ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplUserNew, &form)
+               case models.IsErrNameCharsNotAllowed(err):
+                       ctx.Data["Err_UserName"] = true
+                       ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(models.ErrNameCharsNotAllowed).Name), tplUserNew, &form)
+               default:
+                       ctx.ServerError("CreateUser", err)
+               }
+               return
+       }
+       log.Trace("Account created by admin (%s): %s", ctx.User.Name, u.Name)
+
+       // Send email notification.
+       if form.SendNotify {
+               mailer.SendRegisterNotifyMail(u)
+       }
+
+       ctx.Flash.Success(ctx.Tr("admin.users.new_success", u.Name))
+       ctx.Redirect(setting.AppSubURL + "/admin/users/" + fmt.Sprint(u.ID))
+}
+
+func prepareUserInfo(ctx *context.Context) *models.User {
+       u, err := models.GetUserByID(ctx.ParamsInt64(":userid"))
+       if err != nil {
+               ctx.ServerError("GetUserByID", err)
+               return nil
+       }
+       ctx.Data["User"] = u
+
+       if u.LoginSource > 0 {
+               ctx.Data["LoginSource"], err = models.GetLoginSourceByID(u.LoginSource)
+               if err != nil {
+                       ctx.ServerError("GetLoginSourceByID", err)
+                       return nil
+               }
+       } else {
+               ctx.Data["LoginSource"] = &models.LoginSource{}
+       }
+
+       sources, err := models.LoginSources()
+       if err != nil {
+               ctx.ServerError("LoginSources", err)
+               return nil
+       }
+       ctx.Data["Sources"] = sources
+
+       ctx.Data["TwoFactorEnabled"] = true
+       _, err = models.GetTwoFactorByUID(u.ID)
+       if err != nil {
+               if !models.IsErrTwoFactorNotEnrolled(err) {
+                       ctx.ServerError("IsErrTwoFactorNotEnrolled", err)
+                       return nil
+               }
+               ctx.Data["TwoFactorEnabled"] = false
+       }
+
+       return u
+}
+
+// EditUser show editting user page
+func EditUser(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("admin.users.edit_account")
+       ctx.Data["PageIsAdmin"] = true
+       ctx.Data["PageIsAdminUsers"] = true
+       ctx.Data["DisableRegularOrgCreation"] = setting.Admin.DisableRegularOrgCreation
+       ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
+
+       prepareUserInfo(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       ctx.HTML(http.StatusOK, tplUserEdit)
+}
+
+// EditUserPost response for editting user
+func EditUserPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.AdminEditUserForm)
+       ctx.Data["Title"] = ctx.Tr("admin.users.edit_account")
+       ctx.Data["PageIsAdmin"] = true
+       ctx.Data["PageIsAdminUsers"] = true
+       ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
+
+       u := prepareUserInfo(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplUserEdit)
+               return
+       }
+
+       fields := strings.Split(form.LoginType, "-")
+       if len(fields) == 2 {
+               loginType, _ := strconv.ParseInt(fields[0], 10, 0)
+               loginSource, _ := strconv.ParseInt(fields[1], 10, 64)
+
+               if u.LoginSource != loginSource {
+                       u.LoginSource = loginSource
+                       u.LoginType = models.LoginType(loginType)
+               }
+       }
+
+       if len(form.Password) > 0 && (u.IsLocal() || u.IsOAuth2()) {
+               var err error
+               if len(form.Password) < setting.MinPasswordLength {
+                       ctx.Data["Err_Password"] = true
+                       ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserEdit, &form)
+                       return
+               }
+               if !password.IsComplexEnough(form.Password) {
+                       ctx.RenderWithErr(password.BuildComplexityError(ctx), tplUserEdit, &form)
+                       return
+               }
+               pwned, err := password.IsPwned(ctx, form.Password)
+               if pwned {
+                       ctx.Data["Err_Password"] = true
+                       errMsg := ctx.Tr("auth.password_pwned")
+                       if err != nil {
+                               log.Error(err.Error())
+                               errMsg = ctx.Tr("auth.password_pwned_err")
+                       }
+                       ctx.RenderWithErr(errMsg, tplUserNew, &form)
+                       return
+               }
+               if u.Salt, err = models.GetUserSalt(); err != nil {
+                       ctx.ServerError("UpdateUser", err)
+                       return
+               }
+               if err = u.SetPassword(form.Password); err != nil {
+                       ctx.ServerError("SetPassword", err)
+                       return
+               }
+       }
+
+       if len(form.UserName) != 0 && u.Name != form.UserName {
+               if err := router_user_setting.HandleUsernameChange(ctx, u, form.UserName); err != nil {
+                       ctx.Redirect(setting.AppSubURL + "/admin/users")
+                       return
+               }
+               u.Name = form.UserName
+               u.LowerName = strings.ToLower(form.UserName)
+       }
+
+       if form.Reset2FA {
+               tf, err := models.GetTwoFactorByUID(u.ID)
+               if err != nil && !models.IsErrTwoFactorNotEnrolled(err) {
+                       ctx.ServerError("GetTwoFactorByUID", err)
+                       return
+               }
+
+               if err = models.DeleteTwoFactorByID(tf.ID, u.ID); err != nil {
+                       ctx.ServerError("DeleteTwoFactorByID", err)
+                       return
+               }
+       }
+
+       u.LoginName = form.LoginName
+       u.FullName = form.FullName
+       u.Email = form.Email
+       u.Website = form.Website
+       u.Location = form.Location
+       u.MaxRepoCreation = form.MaxRepoCreation
+       u.IsActive = form.Active
+       u.IsAdmin = form.Admin
+       u.IsRestricted = form.Restricted
+       u.AllowGitHook = form.AllowGitHook
+       u.AllowImportLocal = form.AllowImportLocal
+       u.AllowCreateOrganization = form.AllowCreateOrganization
+
+       // skip self Prohibit Login
+       if ctx.User.ID == u.ID {
+               u.ProhibitLogin = false
+       } else {
+               u.ProhibitLogin = form.ProhibitLogin
+       }
+
+       if err := models.UpdateUser(u); err != nil {
+               if models.IsErrEmailAlreadyUsed(err) {
+                       ctx.Data["Err_Email"] = true
+                       ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserEdit, &form)
+               } else if models.IsErrEmailInvalid(err) {
+                       ctx.Data["Err_Email"] = true
+                       ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserEdit, &form)
+               } else {
+                       ctx.ServerError("UpdateUser", err)
+               }
+               return
+       }
+       log.Trace("Account profile updated by admin (%s): %s", ctx.User.Name, u.Name)
+
+       ctx.Flash.Success(ctx.Tr("admin.users.update_profile_success"))
+       ctx.Redirect(setting.AppSubURL + "/admin/users/" + ctx.Params(":userid"))
+}
+
+// DeleteUser response for deleting a user
+func DeleteUser(ctx *context.Context) {
+       u, err := models.GetUserByID(ctx.ParamsInt64(":userid"))
+       if err != nil {
+               ctx.ServerError("GetUserByID", err)
+               return
+       }
+
+       if err = models.DeleteUser(u); err != nil {
+               switch {
+               case models.IsErrUserOwnRepos(err):
+                       ctx.Flash.Error(ctx.Tr("admin.users.still_own_repo"))
+                       ctx.JSON(http.StatusOK, map[string]interface{}{
+                               "redirect": setting.AppSubURL + "/admin/users/" + ctx.Params(":userid"),
+                       })
+               case models.IsErrUserHasOrgs(err):
+                       ctx.Flash.Error(ctx.Tr("admin.users.still_has_org"))
+                       ctx.JSON(http.StatusOK, map[string]interface{}{
+                               "redirect": setting.AppSubURL + "/admin/users/" + ctx.Params(":userid"),
+                       })
+               default:
+                       ctx.ServerError("DeleteUser", err)
+               }
+               return
+       }
+       log.Trace("Account deleted by admin (%s): %s", ctx.User.Name, u.Name)
+
+       ctx.Flash.Success(ctx.Tr("admin.users.deletion_success"))
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": setting.AppSubURL + "/admin/users",
+       })
+}
diff --git a/routers/web/admin/users_test.go b/routers/web/admin/users_test.go
new file mode 100644 (file)
index 0000000..b19dcb8
--- /dev/null
@@ -0,0 +1,123 @@
+// 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 admin
+
+import (
+       "testing"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/test"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestNewUserPost_MustChangePassword(t *testing.T) {
+
+       models.PrepareTestEnv(t)
+       ctx := test.MockContext(t, "admin/users/new")
+
+       u := models.AssertExistsAndLoadBean(t, &models.User{
+               IsAdmin: true,
+               ID:      2,
+       }).(*models.User)
+
+       ctx.User = u
+
+       username := "gitea"
+       email := "gitea@gitea.io"
+
+       form := forms.AdminCreateUserForm{
+               LoginType:          "local",
+               LoginName:          "local",
+               UserName:           username,
+               Email:              email,
+               Password:           "abc123ABC!=$",
+               SendNotify:         false,
+               MustChangePassword: true,
+       }
+
+       web.SetForm(ctx, &form)
+       NewUserPost(ctx)
+
+       assert.NotEmpty(t, ctx.Flash.SuccessMsg)
+
+       u, err := models.GetUserByName(username)
+
+       assert.NoError(t, err)
+       assert.Equal(t, username, u.Name)
+       assert.Equal(t, email, u.Email)
+       assert.True(t, u.MustChangePassword)
+}
+
+func TestNewUserPost_MustChangePasswordFalse(t *testing.T) {
+
+       models.PrepareTestEnv(t)
+       ctx := test.MockContext(t, "admin/users/new")
+
+       u := models.AssertExistsAndLoadBean(t, &models.User{
+               IsAdmin: true,
+               ID:      2,
+       }).(*models.User)
+
+       ctx.User = u
+
+       username := "gitea"
+       email := "gitea@gitea.io"
+
+       form := forms.AdminCreateUserForm{
+               LoginType:          "local",
+               LoginName:          "local",
+               UserName:           username,
+               Email:              email,
+               Password:           "abc123ABC!=$",
+               SendNotify:         false,
+               MustChangePassword: false,
+       }
+
+       web.SetForm(ctx, &form)
+       NewUserPost(ctx)
+
+       assert.NotEmpty(t, ctx.Flash.SuccessMsg)
+
+       u, err := models.GetUserByName(username)
+
+       assert.NoError(t, err)
+       assert.Equal(t, username, u.Name)
+       assert.Equal(t, email, u.Email)
+       assert.False(t, u.MustChangePassword)
+}
+
+func TestNewUserPost_InvalidEmail(t *testing.T) {
+
+       models.PrepareTestEnv(t)
+       ctx := test.MockContext(t, "admin/users/new")
+
+       u := models.AssertExistsAndLoadBean(t, &models.User{
+               IsAdmin: true,
+               ID:      2,
+       }).(*models.User)
+
+       ctx.User = u
+
+       username := "gitea"
+       email := "gitea@gitea.io\r\n"
+
+       form := forms.AdminCreateUserForm{
+               LoginType:          "local",
+               LoginName:          "local",
+               UserName:           username,
+               Email:              email,
+               Password:           "abc123ABC!=$",
+               SendNotify:         false,
+               MustChangePassword: false,
+       }
+
+       web.SetForm(ctx, &form)
+       NewUserPost(ctx)
+
+       assert.NotEmpty(t, ctx.Flash.ErrorMsg)
+}
diff --git a/routers/web/base.go b/routers/web/base.go
new file mode 100644 (file)
index 0000000..8a44736
--- /dev/null
@@ -0,0 +1,189 @@
+// 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 web
+
+import (
+       "errors"
+       "fmt"
+       "io"
+       "net/http"
+       "os"
+       "path"
+       "path/filepath"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/auth/sso"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/httpcache"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/storage"
+       "code.gitea.io/gitea/modules/templates"
+       "code.gitea.io/gitea/modules/web/middleware"
+
+       "gitea.com/go-chi/session"
+)
+
+func storageHandler(storageSetting setting.Storage, prefix string, objStore storage.ObjectStorage) func(next http.Handler) http.Handler {
+       return func(next http.Handler) http.Handler {
+               if storageSetting.ServeDirect {
+                       return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+                               if req.Method != "GET" && req.Method != "HEAD" {
+                                       next.ServeHTTP(w, req)
+                                       return
+                               }
+
+                               if !strings.HasPrefix(req.URL.RequestURI(), "/"+prefix) {
+                                       next.ServeHTTP(w, req)
+                                       return
+                               }
+
+                               rPath := strings.TrimPrefix(req.URL.RequestURI(), "/"+prefix)
+                               u, err := objStore.URL(rPath, path.Base(rPath))
+                               if err != nil {
+                                       if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
+                                               log.Warn("Unable to find %s %s", prefix, rPath)
+                                               http.Error(w, "file not found", 404)
+                                               return
+                                       }
+                                       log.Error("Error whilst getting URL for %s %s. Error: %v", prefix, rPath, err)
+                                       http.Error(w, fmt.Sprintf("Error whilst getting URL for %s %s", prefix, rPath), 500)
+                                       return
+                               }
+                               http.Redirect(
+                                       w,
+                                       req,
+                                       u.String(),
+                                       301,
+                               )
+                       })
+               }
+
+               return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+                       if req.Method != "GET" && req.Method != "HEAD" {
+                               next.ServeHTTP(w, req)
+                               return
+                       }
+
+                       prefix := strings.Trim(prefix, "/")
+
+                       if !strings.HasPrefix(req.URL.EscapedPath(), "/"+prefix+"/") {
+                               next.ServeHTTP(w, req)
+                               return
+                       }
+
+                       rPath := strings.TrimPrefix(req.URL.EscapedPath(), "/"+prefix+"/")
+                       rPath = strings.TrimPrefix(rPath, "/")
+                       if rPath == "" {
+                               http.Error(w, "file not found", 404)
+                               return
+                       }
+                       rPath = path.Clean("/" + filepath.ToSlash(rPath))
+                       rPath = rPath[1:]
+
+                       fi, err := objStore.Stat(rPath)
+                       if err == nil && httpcache.HandleTimeCache(req, w, fi) {
+                               return
+                       }
+
+                       //If we have matched and access to release or issue
+                       fr, err := objStore.Open(rPath)
+                       if err != nil {
+                               if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
+                                       log.Warn("Unable to find %s %s", prefix, rPath)
+                                       http.Error(w, "file not found", 404)
+                                       return
+                               }
+                               log.Error("Error whilst opening %s %s. Error: %v", prefix, rPath, err)
+                               http.Error(w, fmt.Sprintf("Error whilst opening %s %s", prefix, rPath), 500)
+                               return
+                       }
+                       defer fr.Close()
+
+                       _, err = io.Copy(w, fr)
+                       if err != nil {
+                               log.Error("Error whilst rendering %s %s. Error: %v", prefix, rPath, err)
+                               http.Error(w, fmt.Sprintf("Error whilst rendering %s %s", prefix, rPath), 500)
+                               return
+                       }
+               })
+       }
+}
+
+type dataStore map[string]interface{}
+
+func (d *dataStore) GetData() map[string]interface{} {
+       return *d
+}
+
+// Recovery returns a middleware that recovers from any panics and writes a 500 and a log if so.
+// This error will be created with the gitea 500 page.
+func Recovery() func(next http.Handler) http.Handler {
+       var rnd = templates.HTMLRenderer()
+       return func(next http.Handler) http.Handler {
+               return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+                       defer func() {
+                               if err := recover(); err != nil {
+                                       combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, string(log.Stack(2)))
+                                       log.Error("%v", combinedErr)
+
+                                       sessionStore := session.GetSession(req)
+                                       if sessionStore == nil {
+                                               if setting.IsProd() {
+                                                       http.Error(w, http.StatusText(500), 500)
+                                               } else {
+                                                       http.Error(w, combinedErr, 500)
+                                               }
+                                               return
+                                       }
+
+                                       var lc = middleware.Locale(w, req)
+                                       var store = dataStore{
+                                               "Language":   lc.Language(),
+                                               "CurrentURL": setting.AppSubURL + req.URL.RequestURI(),
+                                               "i18n":       lc,
+                                       }
+
+                                       var user *models.User
+                                       if apiContext := context.GetAPIContext(req); apiContext != nil {
+                                               user = apiContext.User
+                                       }
+                                       if user == nil {
+                                               if ctx := context.GetContext(req); ctx != nil {
+                                                       user = ctx.User
+                                               }
+                                       }
+                                       if user == nil {
+                                               // Get user from session if logged in - do not attempt to sign-in
+                                               user = sso.SessionUser(sessionStore)
+                                       }
+                                       if user != nil {
+                                               store["IsSigned"] = true
+                                               store["SignedUser"] = user
+                                               store["SignedUserID"] = user.ID
+                                               store["SignedUserName"] = user.Name
+                                               store["IsAdmin"] = user.IsAdmin
+                                       } else {
+                                               store["SignedUserID"] = int64(0)
+                                               store["SignedUserName"] = ""
+                                       }
+
+                                       w.Header().Set(`X-Frame-Options`, `SAMEORIGIN`)
+
+                                       if !setting.IsProd() {
+                                               store["ErrorMsg"] = combinedErr
+                                       }
+                                       err = rnd.HTML(w, 500, "status/500", templates.BaseVars().Merge(store))
+                                       if err != nil {
+                                               log.Error("%v", err)
+                                       }
+                               }
+                       }()
+
+                       next.ServeHTTP(w, req)
+               })
+       }
+}
diff --git a/routers/web/dev/template.go b/routers/web/dev/template.go
new file mode 100644 (file)
index 0000000..de334c4
--- /dev/null
@@ -0,0 +1,29 @@
+// Copyright 2014 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 dev
+
+import (
+       "net/http"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/timeutil"
+)
+
+// TemplatePreview render for previewing the indicated template
+func TemplatePreview(ctx *context.Context) {
+       ctx.Data["User"] = models.User{Name: "Unknown"}
+       ctx.Data["AppName"] = setting.AppName
+       ctx.Data["AppVer"] = setting.AppVer
+       ctx.Data["AppUrl"] = setting.AppURL
+       ctx.Data["Code"] = "2014031910370000009fff6782aadb2162b4a997acb69d4400888e0b9274657374"
+       ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())
+       ctx.Data["ResetPwdCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, ctx.Locale.Language())
+       ctx.Data["CurDbValue"] = ""
+
+       ctx.HTML(http.StatusOK, base.TplName(ctx.Params("*")))
+}
diff --git a/routers/web/events/events.go b/routers/web/events/events.go
new file mode 100644 (file)
index 0000000..f9cc274
--- /dev/null
@@ -0,0 +1,156 @@
+// 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 events
+
+import (
+       "net/http"
+       "time"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/convert"
+       "code.gitea.io/gitea/modules/eventsource"
+       "code.gitea.io/gitea/modules/graceful"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/routers/web/user"
+       jsoniter "github.com/json-iterator/go"
+)
+
+// Events listens for events
+func Events(ctx *context.Context) {
+       // FIXME: Need to check if resp is actually a http.Flusher! - how though?
+
+       // Set the headers related to event streaming.
+       ctx.Resp.Header().Set("Content-Type", "text/event-stream")
+       ctx.Resp.Header().Set("Cache-Control", "no-cache")
+       ctx.Resp.Header().Set("Connection", "keep-alive")
+       ctx.Resp.Header().Set("X-Accel-Buffering", "no")
+       ctx.Resp.WriteHeader(http.StatusOK)
+
+       if !ctx.IsSigned {
+               // Return unauthorized status event
+               event := &eventsource.Event{
+                       Name: "close",
+                       Data: "unauthorized",
+               }
+               _, _ = event.WriteTo(ctx)
+               ctx.Resp.Flush()
+               return
+       }
+
+       // Listen to connection close and un-register messageChan
+       notify := ctx.Done()
+       ctx.Resp.Flush()
+
+       shutdownCtx := graceful.GetManager().ShutdownContext()
+
+       uid := ctx.User.ID
+
+       messageChan := eventsource.GetManager().Register(uid)
+
+       unregister := func() {
+               eventsource.GetManager().Unregister(uid, messageChan)
+               // ensure the messageChan is closed
+               for {
+                       _, ok := <-messageChan
+                       if !ok {
+                               break
+                       }
+               }
+       }
+
+       if _, err := ctx.Resp.Write([]byte("\n")); err != nil {
+               log.Error("Unable to write to EventStream: %v", err)
+               unregister()
+               return
+       }
+
+       timer := time.NewTicker(30 * time.Second)
+
+       stopwatchTimer := time.NewTicker(setting.UI.Notification.MinTimeout)
+
+loop:
+       for {
+               select {
+               case <-timer.C:
+                       event := &eventsource.Event{
+                               Name: "ping",
+                       }
+                       _, err := event.WriteTo(ctx.Resp)
+                       if err != nil {
+                               log.Error("Unable to write to EventStream for user %s: %v", ctx.User.Name, err)
+                               go unregister()
+                               break loop
+                       }
+                       ctx.Resp.Flush()
+               case <-notify:
+                       go unregister()
+                       break loop
+               case <-shutdownCtx.Done():
+                       go unregister()
+                       break loop
+               case <-stopwatchTimer.C:
+                       sws, err := models.GetUserStopwatches(ctx.User.ID, models.ListOptions{})
+                       if err != nil {
+                               log.Error("Unable to GetUserStopwatches: %v", err)
+                               continue
+                       }
+                       apiSWs, err := convert.ToStopWatches(sws)
+                       if err != nil {
+                               log.Error("Unable to APIFormat stopwatches: %v", err)
+                               continue
+                       }
+                       json := jsoniter.ConfigCompatibleWithStandardLibrary
+                       dataBs, err := json.Marshal(apiSWs)
+                       if err != nil {
+                               log.Error("Unable to marshal stopwatches: %v", err)
+                               continue
+                       }
+                       _, err = (&eventsource.Event{
+                               Name: "stopwatches",
+                               Data: string(dataBs),
+                       }).WriteTo(ctx.Resp)
+                       if err != nil {
+                               log.Error("Unable to write to EventStream for user %s: %v", ctx.User.Name, err)
+                               go unregister()
+                               break loop
+                       }
+                       ctx.Resp.Flush()
+               case event, ok := <-messageChan:
+                       if !ok {
+                               break loop
+                       }
+
+                       // Handle logout
+                       if event.Name == "logout" {
+                               if ctx.Session.ID() == event.Data {
+                                       _, _ = (&eventsource.Event{
+                                               Name: "logout",
+                                               Data: "here",
+                                       }).WriteTo(ctx.Resp)
+                                       ctx.Resp.Flush()
+                                       go unregister()
+                                       user.HandleSignOut(ctx)
+                                       break loop
+                               }
+                               // Replace the event - we don't want to expose the session ID to the user
+                               event = &eventsource.Event{
+                                       Name: "logout",
+                                       Data: "elsewhere",
+                               }
+                       }
+
+                       _, err := event.WriteTo(ctx.Resp)
+                       if err != nil {
+                               log.Error("Unable to write to EventStream for user %s: %v", ctx.User.Name, err)
+                               go unregister()
+                               break loop
+                       }
+                       ctx.Resp.Flush()
+               }
+       }
+       timer.Stop()
+}
diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go
new file mode 100644 (file)
index 0000000..bf15b93
--- /dev/null
@@ -0,0 +1,139 @@
+// Copyright 2021 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 explore
+
+import (
+       "net/http"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       code_indexer "code.gitea.io/gitea/modules/indexer/code"
+       "code.gitea.io/gitea/modules/setting"
+)
+
+const (
+       // tplExploreCode explore code page template
+       tplExploreCode base.TplName = "explore/code"
+)
+
+// Code render explore code page
+func Code(ctx *context.Context) {
+       if !setting.Indexer.RepoIndexerEnabled {
+               ctx.Redirect(setting.AppSubURL+"/explore", 302)
+               return
+       }
+
+       ctx.Data["UsersIsDisabled"] = setting.Service.Explore.DisableUsersPage
+       ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+       ctx.Data["Title"] = ctx.Tr("explore")
+       ctx.Data["PageIsExplore"] = true
+       ctx.Data["PageIsExploreCode"] = true
+
+       language := strings.TrimSpace(ctx.Query("l"))
+       keyword := strings.TrimSpace(ctx.Query("q"))
+       page := ctx.QueryInt("page")
+       if page <= 0 {
+               page = 1
+       }
+
+       queryType := strings.TrimSpace(ctx.Query("t"))
+       isMatch := queryType == "match"
+
+       var (
+               repoIDs []int64
+               err     error
+               isAdmin bool
+       )
+       if ctx.User != nil {
+               isAdmin = ctx.User.IsAdmin
+       }
+
+       // guest user or non-admin user
+       if ctx.User == nil || !isAdmin {
+               repoIDs, err = models.FindUserAccessibleRepoIDs(ctx.User)
+               if err != nil {
+                       ctx.ServerError("SearchResults", err)
+                       return
+               }
+       }
+
+       var (
+               total                 int
+               searchResults         []*code_indexer.Result
+               searchResultLanguages []*code_indexer.SearchResultLanguages
+       )
+
+       // if non-admin login user, we need check UnitTypeCode at first
+       if ctx.User != nil && len(repoIDs) > 0 {
+               repoMaps, err := models.GetRepositoriesMapByIDs(repoIDs)
+               if err != nil {
+                       ctx.ServerError("SearchResults", err)
+                       return
+               }
+
+               var rightRepoMap = make(map[int64]*models.Repository, len(repoMaps))
+               repoIDs = make([]int64, 0, len(repoMaps))
+               for id, repo := range repoMaps {
+                       if repo.CheckUnitUser(ctx.User, models.UnitTypeCode) {
+                               rightRepoMap[id] = repo
+                               repoIDs = append(repoIDs, id)
+                       }
+               }
+
+               ctx.Data["RepoMaps"] = rightRepoMap
+
+               total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
+               if err != nil {
+                       ctx.ServerError("SearchResults", err)
+                       return
+               }
+               // if non-login user or isAdmin, no need to check UnitTypeCode
+       } else if (ctx.User == nil && len(repoIDs) > 0) || isAdmin {
+               total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
+               if err != nil {
+                       ctx.ServerError("SearchResults", err)
+                       return
+               }
+
+               var loadRepoIDs = make([]int64, 0, len(searchResults))
+               for _, result := range searchResults {
+                       var find bool
+                       for _, id := range loadRepoIDs {
+                               if id == result.RepoID {
+                                       find = true
+                                       break
+                               }
+                       }
+                       if !find {
+                               loadRepoIDs = append(loadRepoIDs, result.RepoID)
+                       }
+               }
+
+               repoMaps, err := models.GetRepositoriesMapByIDs(loadRepoIDs)
+               if err != nil {
+                       ctx.ServerError("SearchResults", err)
+                       return
+               }
+
+               ctx.Data["RepoMaps"] = repoMaps
+       }
+
+       ctx.Data["Keyword"] = keyword
+       ctx.Data["Language"] = language
+       ctx.Data["queryType"] = queryType
+       ctx.Data["SearchResults"] = searchResults
+       ctx.Data["SearchResultLanguages"] = searchResultLanguages
+       ctx.Data["RequireHighlightJS"] = true
+       ctx.Data["PageIsViewCode"] = true
+
+       pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
+       pager.SetDefaultParams(ctx)
+       pager.AddParam(ctx, "l", "Language")
+       ctx.Data["Page"] = pager
+
+       ctx.HTML(http.StatusOK, tplExploreCode)
+}
diff --git a/routers/web/explore/org.go b/routers/web/explore/org.go
new file mode 100644 (file)
index 0000000..470e0eb
--- /dev/null
@@ -0,0 +1,39 @@
+// Copyright 2021 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 explore
+
+import (
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/structs"
+)
+
+const (
+       // tplExploreOrganizations explore organizations page template
+       tplExploreOrganizations base.TplName = "explore/organizations"
+)
+
+// Organizations render explore organizations page
+func Organizations(ctx *context.Context) {
+       ctx.Data["UsersIsDisabled"] = setting.Service.Explore.DisableUsersPage
+       ctx.Data["Title"] = ctx.Tr("explore")
+       ctx.Data["PageIsExplore"] = true
+       ctx.Data["PageIsExploreOrganizations"] = true
+       ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+
+       visibleTypes := []structs.VisibleType{structs.VisibleTypePublic}
+       if ctx.User != nil {
+               visibleTypes = append(visibleTypes, structs.VisibleTypeLimited, structs.VisibleTypePrivate)
+       }
+
+       RenderUserSearch(ctx, &models.SearchUserOptions{
+               Actor:       ctx.User,
+               Type:        models.UserTypeOrganization,
+               ListOptions: models.ListOptions{PageSize: setting.UI.ExplorePagingNum},
+               Visible:     visibleTypes,
+       }, tplExploreOrganizations)
+}
diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go
new file mode 100644 (file)
index 0000000..e9efae5
--- /dev/null
@@ -0,0 +1,131 @@
+// Copyright 2021 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 explore
+
+import (
+       "net/http"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/setting"
+)
+
+const (
+       // tplExploreRepos explore repositories page template
+       tplExploreRepos base.TplName = "explore/repos"
+)
+
+// RepoSearchOptions when calling search repositories
+type RepoSearchOptions struct {
+       OwnerID    int64
+       Private    bool
+       Restricted bool
+       PageSize   int
+       TplName    base.TplName
+}
+
+// RenderRepoSearch render repositories search page
+func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
+       page := ctx.QueryInt("page")
+       if page <= 0 {
+               page = 1
+       }
+
+       var (
+               repos   []*models.Repository
+               count   int64
+               err     error
+               orderBy models.SearchOrderBy
+       )
+
+       ctx.Data["SortType"] = ctx.Query("sort")
+       switch ctx.Query("sort") {
+       case "newest":
+               orderBy = models.SearchOrderByNewest
+       case "oldest":
+               orderBy = models.SearchOrderByOldest
+       case "recentupdate":
+               orderBy = models.SearchOrderByRecentUpdated
+       case "leastupdate":
+               orderBy = models.SearchOrderByLeastUpdated
+       case "reversealphabetically":
+               orderBy = models.SearchOrderByAlphabeticallyReverse
+       case "alphabetically":
+               orderBy = models.SearchOrderByAlphabetically
+       case "reversesize":
+               orderBy = models.SearchOrderBySizeReverse
+       case "size":
+               orderBy = models.SearchOrderBySize
+       case "moststars":
+               orderBy = models.SearchOrderByStarsReverse
+       case "feweststars":
+               orderBy = models.SearchOrderByStars
+       case "mostforks":
+               orderBy = models.SearchOrderByForksReverse
+       case "fewestforks":
+               orderBy = models.SearchOrderByForks
+       default:
+               ctx.Data["SortType"] = "recentupdate"
+               orderBy = models.SearchOrderByRecentUpdated
+       }
+
+       keyword := strings.Trim(ctx.Query("q"), " ")
+       topicOnly := ctx.QueryBool("topic")
+       ctx.Data["TopicOnly"] = topicOnly
+
+       repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
+               ListOptions: models.ListOptions{
+                       Page:     page,
+                       PageSize: opts.PageSize,
+               },
+               Actor:              ctx.User,
+               OrderBy:            orderBy,
+               Private:            opts.Private,
+               Keyword:            keyword,
+               OwnerID:            opts.OwnerID,
+               AllPublic:          true,
+               AllLimited:         true,
+               TopicOnly:          topicOnly,
+               IncludeDescription: setting.UI.SearchRepoDescription,
+       })
+       if err != nil {
+               ctx.ServerError("SearchRepository", err)
+               return
+       }
+       ctx.Data["Keyword"] = keyword
+       ctx.Data["Total"] = count
+       ctx.Data["Repos"] = repos
+       ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+
+       pager := context.NewPagination(int(count), opts.PageSize, page, 5)
+       pager.SetDefaultParams(ctx)
+       pager.AddParam(ctx, "topic", "TopicOnly")
+       ctx.Data["Page"] = pager
+
+       ctx.HTML(http.StatusOK, opts.TplName)
+}
+
+// Repos render explore repositories page
+func Repos(ctx *context.Context) {
+       ctx.Data["UsersIsDisabled"] = setting.Service.Explore.DisableUsersPage
+       ctx.Data["Title"] = ctx.Tr("explore")
+       ctx.Data["PageIsExplore"] = true
+       ctx.Data["PageIsExploreRepositories"] = true
+       ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+
+       var ownerID int64
+       if ctx.User != nil && !ctx.User.IsAdmin {
+               ownerID = ctx.User.ID
+       }
+
+       RenderRepoSearch(ctx, &RepoSearchOptions{
+               PageSize: setting.UI.ExplorePagingNum,
+               OwnerID:  ownerID,
+               Private:  ctx.User != nil,
+               TplName:  tplExploreRepos,
+       })
+}
diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go
new file mode 100644 (file)
index 0000000..52f543f
--- /dev/null
@@ -0,0 +1,107 @@
+// Copyright 2021 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 explore
+
+import (
+       "bytes"
+       "net/http"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/structs"
+       "code.gitea.io/gitea/modules/util"
+)
+
+const (
+       // tplExploreUsers explore users page template
+       tplExploreUsers base.TplName = "explore/users"
+)
+
+var (
+       nullByte = []byte{0x00}
+)
+
+func isKeywordValid(keyword string) bool {
+       return !bytes.Contains([]byte(keyword), nullByte)
+}
+
+// RenderUserSearch render user search page
+func RenderUserSearch(ctx *context.Context, opts *models.SearchUserOptions, tplName base.TplName) {
+       opts.Page = ctx.QueryInt("page")
+       if opts.Page <= 1 {
+               opts.Page = 1
+       }
+
+       var (
+               users   []*models.User
+               count   int64
+               err     error
+               orderBy models.SearchOrderBy
+       )
+
+       ctx.Data["SortType"] = ctx.Query("sort")
+       switch ctx.Query("sort") {
+       case "newest":
+               orderBy = models.SearchOrderByIDReverse
+       case "oldest":
+               orderBy = models.SearchOrderByID
+       case "recentupdate":
+               orderBy = models.SearchOrderByRecentUpdated
+       case "leastupdate":
+               orderBy = models.SearchOrderByLeastUpdated
+       case "reversealphabetically":
+               orderBy = models.SearchOrderByAlphabeticallyReverse
+       case "alphabetically":
+               orderBy = models.SearchOrderByAlphabetically
+       default:
+               ctx.Data["SortType"] = "alphabetically"
+               orderBy = models.SearchOrderByAlphabetically
+       }
+
+       opts.Keyword = strings.Trim(ctx.Query("q"), " ")
+       opts.OrderBy = orderBy
+       if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
+               users, count, err = models.SearchUsers(opts)
+               if err != nil {
+                       ctx.ServerError("SearchUsers", err)
+                       return
+               }
+       }
+       ctx.Data["Keyword"] = opts.Keyword
+       ctx.Data["Total"] = count
+       ctx.Data["Users"] = users
+       ctx.Data["UsersTwoFaStatus"] = models.UserList(users).GetTwoFaStatus()
+       ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail
+       ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+
+       pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
+       pager.SetDefaultParams(ctx)
+       ctx.Data["Page"] = pager
+
+       ctx.HTML(http.StatusOK, tplName)
+}
+
+// Users render explore users page
+func Users(ctx *context.Context) {
+       if setting.Service.Explore.DisableUsersPage {
+               ctx.Redirect(setting.AppSubURL + "/explore/repos")
+               return
+       }
+       ctx.Data["Title"] = ctx.Tr("explore")
+       ctx.Data["PageIsExplore"] = true
+       ctx.Data["PageIsExploreUsers"] = true
+       ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+
+       RenderUserSearch(ctx, &models.SearchUserOptions{
+               Actor:       ctx.User,
+               Type:        models.UserTypeIndividual,
+               ListOptions: models.ListOptions{PageSize: setting.UI.ExplorePagingNum},
+               IsActive:    util.OptionalBoolTrue,
+               Visible:     []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate},
+       }, tplExploreUsers)
+}
diff --git a/routers/web/goget.go b/routers/web/goget.go
new file mode 100644 (file)
index 0000000..77934e7
--- /dev/null
@@ -0,0 +1,86 @@
+// Copyright 2021 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 web
+
+import (
+       "net/http"
+       "net/url"
+       "path"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/util"
+       "github.com/unknwon/com"
+)
+
+func goGet(ctx *context.Context) {
+       if ctx.Req.Method != "GET" || ctx.Query("go-get") != "1" || len(ctx.Req.URL.Query()) > 1 {
+               return
+       }
+
+       parts := strings.SplitN(ctx.Req.URL.EscapedPath(), "/", 4)
+
+       if len(parts) < 3 {
+               return
+       }
+
+       ownerName := parts[1]
+       repoName := parts[2]
+
+       // Quick responses appropriate go-get meta with status 200
+       // regardless of if user have access to the repository,
+       // or the repository does not exist at all.
+       // This is particular a workaround for "go get" command which does not respect
+       // .netrc file.
+
+       trimmedRepoName := strings.TrimSuffix(repoName, ".git")
+
+       if ownerName == "" || trimmedRepoName == "" {
+               _, _ = ctx.Write([]byte(`<!doctype html>
+<html>
+       <body>
+               invalid import path
+       </body>
+</html>
+`))
+               ctx.Status(400)
+               return
+       }
+       branchName := setting.Repository.DefaultBranch
+
+       repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName)
+       if err == nil && len(repo.DefaultBranch) > 0 {
+               branchName = repo.DefaultBranch
+       }
+       prefix := setting.AppURL + path.Join(url.PathEscape(ownerName), url.PathEscape(repoName), "src", "branch", util.PathEscapeSegments(branchName))
+
+       appURL, _ := url.Parse(setting.AppURL)
+
+       insecure := ""
+       if appURL.Scheme == string(setting.HTTP) {
+               insecure = "--insecure "
+       }
+       ctx.Header().Set("Content-Type", "text/html")
+       ctx.Status(http.StatusOK)
+       _, _ = ctx.Write([]byte(com.Expand(`<!doctype html>
+<html>
+       <head>
+               <meta name="go-import" content="{GoGetImport} git {CloneLink}">
+               <meta name="go-source" content="{GoGetImport} _ {GoDocDirectory} {GoDocFile}">
+       </head>
+       <body>
+               go get {Insecure}{GoGetImport}
+       </body>
+</html>
+`, map[string]string{
+               "GoGetImport":    context.ComposeGoGetImport(ownerName, trimmedRepoName),
+               "CloneLink":      models.ComposeHTTPSCloneURL(ownerName, repoName),
+               "GoDocDirectory": prefix + "{/dir}",
+               "GoDocFile":      prefix + "{/dir}/{file}#L{line}",
+               "Insecure":       insecure,
+       })))
+}
diff --git a/routers/web/home.go b/routers/web/home.go
new file mode 100644 (file)
index 0000000..f501976
--- /dev/null
@@ -0,0 +1,65 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2019 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 web
+
+import (
+       "net/http"
+
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/web/middleware"
+       "code.gitea.io/gitea/routers/web/user"
+)
+
+const (
+       // tplHome home page template
+       tplHome base.TplName = "home"
+)
+
+// Home render home page
+func Home(ctx *context.Context) {
+       if ctx.IsSigned {
+               if !ctx.User.IsActive && setting.Service.RegisterEmailConfirm {
+                       ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
+                       ctx.HTML(http.StatusOK, user.TplActivate)
+               } else if !ctx.User.IsActive || ctx.User.ProhibitLogin {
+                       log.Info("Failed authentication attempt for %s from %s", ctx.User.Name, ctx.RemoteAddr())
+                       ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
+                       ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
+               } else if ctx.User.MustChangePassword {
+                       ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
+                       ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/change_password"
+                       middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
+                       ctx.Redirect(setting.AppSubURL + "/user/settings/change_password")
+               } else {
+                       user.Dashboard(ctx)
+               }
+               return
+               // Check non-logged users landing page.
+       } else if setting.LandingPageURL != setting.LandingPageHome {
+               ctx.Redirect(setting.AppSubURL + string(setting.LandingPageURL))
+               return
+       }
+
+       // Check auto-login.
+       uname := ctx.GetCookie(setting.CookieUserName)
+       if len(uname) != 0 {
+               ctx.Redirect(setting.AppSubURL + "/user/login")
+               return
+       }
+
+       ctx.Data["PageIsHome"] = true
+       ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+       ctx.HTML(http.StatusOK, tplHome)
+}
+
+// NotFound render 404 page
+func NotFound(ctx *context.Context) {
+       ctx.Data["Title"] = "Page Not Found"
+       ctx.NotFound("home.NotFound", nil)
+}
diff --git a/routers/web/metrics.go b/routers/web/metrics.go
new file mode 100644 (file)
index 0000000..37558ee
--- /dev/null
@@ -0,0 +1,34 @@
+// Copyright 2018 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 web
+
+import (
+       "crypto/subtle"
+       "net/http"
+
+       "code.gitea.io/gitea/modules/setting"
+
+       "github.com/prometheus/client_golang/prometheus/promhttp"
+)
+
+// Metrics validate auth token and render prometheus metrics
+func Metrics(resp http.ResponseWriter, req *http.Request) {
+       if setting.Metrics.Token == "" {
+               promhttp.Handler().ServeHTTP(resp, req)
+               return
+       }
+       header := req.Header.Get("Authorization")
+       if header == "" {
+               http.Error(resp, "", 401)
+               return
+       }
+       got := []byte(header)
+       want := []byte("Bearer " + setting.Metrics.Token)
+       if subtle.ConstantTimeCompare(got, want) != 1 {
+               http.Error(resp, "", 401)
+               return
+       }
+       promhttp.Handler().ServeHTTP(resp, req)
+}
diff --git a/routers/web/org/home.go b/routers/web/org/home.go
new file mode 100644 (file)
index 0000000..d84ae87
--- /dev/null
@@ -0,0 +1,151 @@
+// Copyright 2019 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 org
+
+import (
+       "net/http"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/markup"
+       "code.gitea.io/gitea/modules/markup/markdown"
+       "code.gitea.io/gitea/modules/setting"
+)
+
+const (
+       tplOrgHome base.TplName = "org/home"
+)
+
+// Home show organization home page
+func Home(ctx *context.Context) {
+       ctx.SetParams(":org", ctx.Params(":username"))
+       context.HandleOrgAssignment(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       org := ctx.Org.Organization
+
+       if !models.HasOrgVisible(org, ctx.User) {
+               ctx.NotFound("HasOrgVisible", nil)
+               return
+       }
+
+       ctx.Data["PageIsUserProfile"] = true
+       ctx.Data["Title"] = org.DisplayName()
+       if len(org.Description) != 0 {
+               desc, err := markdown.RenderString(&markup.RenderContext{
+                       URLPrefix: ctx.Repo.RepoLink,
+                       Metas:     map[string]string{"mode": "document"},
+               }, org.Description)
+               if err != nil {
+                       ctx.ServerError("RenderString", err)
+                       return
+               }
+               ctx.Data["RenderedDescription"] = desc
+       }
+
+       var orderBy models.SearchOrderBy
+       ctx.Data["SortType"] = ctx.Query("sort")
+       switch ctx.Query("sort") {
+       case "newest":
+               orderBy = models.SearchOrderByNewest
+       case "oldest":
+               orderBy = models.SearchOrderByOldest
+       case "recentupdate":
+               orderBy = models.SearchOrderByRecentUpdated
+       case "leastupdate":
+               orderBy = models.SearchOrderByLeastUpdated
+       case "reversealphabetically":
+               orderBy = models.SearchOrderByAlphabeticallyReverse
+       case "alphabetically":
+               orderBy = models.SearchOrderByAlphabetically
+       case "moststars":
+               orderBy = models.SearchOrderByStarsReverse
+       case "feweststars":
+               orderBy = models.SearchOrderByStars
+       case "mostforks":
+               orderBy = models.SearchOrderByForksReverse
+       case "fewestforks":
+               orderBy = models.SearchOrderByForks
+       default:
+               ctx.Data["SortType"] = "recentupdate"
+               orderBy = models.SearchOrderByRecentUpdated
+       }
+
+       keyword := strings.Trim(ctx.Query("q"), " ")
+       ctx.Data["Keyword"] = keyword
+
+       page := ctx.QueryInt("page")
+       if page <= 0 {
+               page = 1
+       }
+
+       var (
+               repos []*models.Repository
+               count int64
+               err   error
+       )
+       repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
+               ListOptions: models.ListOptions{
+                       PageSize: setting.UI.User.RepoPagingNum,
+                       Page:     page,
+               },
+               Keyword:            keyword,
+               OwnerID:            org.ID,
+               OrderBy:            orderBy,
+               Private:            ctx.IsSigned,
+               Actor:              ctx.User,
+               IncludeDescription: setting.UI.SearchRepoDescription,
+       })
+       if err != nil {
+               ctx.ServerError("SearchRepository", err)
+               return
+       }
+
+       var opts = models.FindOrgMembersOpts{
+               OrgID:       org.ID,
+               PublicOnly:  true,
+               ListOptions: models.ListOptions{Page: 1, PageSize: 25},
+       }
+
+       if ctx.User != nil {
+               isMember, err := org.IsOrgMember(ctx.User.ID)
+               if err != nil {
+                       ctx.Error(http.StatusInternalServerError, "IsOrgMember")
+                       return
+               }
+               opts.PublicOnly = !isMember && !ctx.User.IsAdmin
+       }
+
+       members, _, err := models.FindOrgMembers(&opts)
+       if err != nil {
+               ctx.ServerError("FindOrgMembers", err)
+               return
+       }
+
+       membersCount, err := models.CountOrgMembers(opts)
+       if err != nil {
+               ctx.ServerError("CountOrgMembers", err)
+               return
+       }
+
+       ctx.Data["Owner"] = org
+       ctx.Data["Repos"] = repos
+       ctx.Data["Total"] = count
+       ctx.Data["MembersTotal"] = membersCount
+       ctx.Data["Members"] = members
+       ctx.Data["Teams"] = org.Teams
+
+       ctx.Data["DisabledMirrors"] = setting.Repository.DisableMirrors
+
+       pager := context.NewPagination(int(count), setting.UI.User.RepoPagingNum, page, 5)
+       pager.SetDefaultParams(ctx)
+       ctx.Data["Page"] = pager
+
+       ctx.HTML(http.StatusOK, tplOrgHome)
+}
diff --git a/routers/web/org/members.go b/routers/web/org/members.go
new file mode 100644 (file)
index 0000000..934529d
--- /dev/null
@@ -0,0 +1,128 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2020 The Gitea Authors.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package org
+
+import (
+       "net/http"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+)
+
+const (
+       // tplMembers template for organization members page
+       tplMembers base.TplName = "org/member/members"
+)
+
+// Members render organization users page
+func Members(ctx *context.Context) {
+       org := ctx.Org.Organization
+       ctx.Data["Title"] = org.FullName
+       ctx.Data["PageIsOrgMembers"] = true
+
+       page := ctx.QueryInt("page")
+       if page <= 1 {
+               page = 1
+       }
+
+       var opts = models.FindOrgMembersOpts{
+               OrgID:      org.ID,
+               PublicOnly: true,
+       }
+
+       if ctx.User != nil {
+               isMember, err := ctx.Org.Organization.IsOrgMember(ctx.User.ID)
+               if err != nil {
+                       ctx.Error(http.StatusInternalServerError, "IsOrgMember")
+                       return
+               }
+               opts.PublicOnly = !isMember && !ctx.User.IsAdmin
+       }
+
+       total, err := models.CountOrgMembers(opts)
+       if err != nil {
+               ctx.Error(http.StatusInternalServerError, "CountOrgMembers")
+               return
+       }
+
+       pager := context.NewPagination(int(total), setting.UI.MembersPagingNum, page, 5)
+       opts.ListOptions.Page = page
+       opts.ListOptions.PageSize = setting.UI.MembersPagingNum
+       members, membersIsPublic, err := models.FindOrgMembers(&opts)
+       if err != nil {
+               ctx.ServerError("GetMembers", err)
+               return
+       }
+       ctx.Data["Page"] = pager
+       ctx.Data["Members"] = members
+       ctx.Data["MembersIsPublicMember"] = membersIsPublic
+       ctx.Data["MembersIsUserOrgOwner"] = members.IsUserOrgOwner(org.ID)
+       ctx.Data["MembersTwoFaStatus"] = members.GetTwoFaStatus()
+
+       ctx.HTML(http.StatusOK, tplMembers)
+}
+
+// MembersAction response for operation to a member of organization
+func MembersAction(ctx *context.Context) {
+       uid := ctx.QueryInt64("uid")
+       if uid == 0 {
+               ctx.Redirect(ctx.Org.OrgLink + "/members")
+               return
+       }
+
+       org := ctx.Org.Organization
+       var err error
+       switch ctx.Params(":action") {
+       case "private":
+               if ctx.User.ID != uid && !ctx.Org.IsOwner {
+                       ctx.Error(http.StatusNotFound)
+                       return
+               }
+               err = models.ChangeOrgUserStatus(org.ID, uid, false)
+       case "public":
+               if ctx.User.ID != uid && !ctx.Org.IsOwner {
+                       ctx.Error(http.StatusNotFound)
+                       return
+               }
+               err = models.ChangeOrgUserStatus(org.ID, uid, true)
+       case "remove":
+               if !ctx.Org.IsOwner {
+                       ctx.Error(http.StatusNotFound)
+                       return
+               }
+               err = org.RemoveMember(uid)
+               if models.IsErrLastOrgOwner(err) {
+                       ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
+                       ctx.Redirect(ctx.Org.OrgLink + "/members")
+                       return
+               }
+       case "leave":
+               err = org.RemoveMember(ctx.User.ID)
+               if models.IsErrLastOrgOwner(err) {
+                       ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
+                       ctx.Redirect(ctx.Org.OrgLink + "/members")
+                       return
+               }
+       }
+
+       if err != nil {
+               log.Error("Action(%s): %v", ctx.Params(":action"), err)
+               ctx.JSON(http.StatusOK, map[string]interface{}{
+                       "ok":  false,
+                       "err": err.Error(),
+               })
+               return
+       }
+
+       if ctx.Params(":action") != "leave" {
+               ctx.Redirect(ctx.Org.OrgLink + "/members")
+       } else {
+               ctx.Redirect(setting.AppSubURL + "/")
+       }
+}
diff --git a/routers/web/org/org.go b/routers/web/org/org.go
new file mode 100644 (file)
index 0000000..beba3da
--- /dev/null
@@ -0,0 +1,79 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2018 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 org
+
+import (
+       "errors"
+       "net/http"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+)
+
+const (
+       // tplCreateOrg template path for create organization
+       tplCreateOrg base.TplName = "org/create"
+)
+
+// Create render the page for create organization
+func Create(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("new_org")
+       ctx.Data["DefaultOrgVisibilityMode"] = setting.Service.DefaultOrgVisibilityMode
+       if !ctx.User.CanCreateOrganization() {
+               ctx.ServerError("Not allowed", errors.New(ctx.Tr("org.form.create_org_not_allowed")))
+               return
+       }
+       ctx.HTML(http.StatusOK, tplCreateOrg)
+}
+
+// CreatePost response for create organization
+func CreatePost(ctx *context.Context) {
+       form := *web.GetForm(ctx).(*forms.CreateOrgForm)
+       ctx.Data["Title"] = ctx.Tr("new_org")
+
+       if !ctx.User.CanCreateOrganization() {
+               ctx.ServerError("Not allowed", errors.New(ctx.Tr("org.form.create_org_not_allowed")))
+               return
+       }
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplCreateOrg)
+               return
+       }
+
+       org := &models.User{
+               Name:                      form.OrgName,
+               IsActive:                  true,
+               Type:                      models.UserTypeOrganization,
+               Visibility:                form.Visibility,
+               RepoAdminChangeTeamAccess: form.RepoAdminChangeTeamAccess,
+       }
+
+       if err := models.CreateOrganization(org, ctx.User); err != nil {
+               ctx.Data["Err_OrgName"] = true
+               switch {
+               case models.IsErrUserAlreadyExist(err):
+                       ctx.RenderWithErr(ctx.Tr("form.org_name_been_taken"), tplCreateOrg, &form)
+               case models.IsErrNameReserved(err):
+                       ctx.RenderWithErr(ctx.Tr("org.form.name_reserved", err.(models.ErrNameReserved).Name), tplCreateOrg, &form)
+               case models.IsErrNamePatternNotAllowed(err):
+                       ctx.RenderWithErr(ctx.Tr("org.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplCreateOrg, &form)
+               case models.IsErrUserNotAllowedCreateOrg(err):
+                       ctx.RenderWithErr(ctx.Tr("org.form.create_org_not_allowed"), tplCreateOrg, &form)
+               default:
+                       ctx.ServerError("CreateOrganization", err)
+               }
+               return
+       }
+       log.Trace("Organization created: %s", org.Name)
+
+       ctx.Redirect(org.DashboardLink())
+}
diff --git a/routers/web/org/org_labels.go b/routers/web/org/org_labels.go
new file mode 100644 (file)
index 0000000..26e232b
--- /dev/null
@@ -0,0 +1,112 @@
+// 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 org
+
+import (
+       "net/http"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+)
+
+// RetrieveLabels find all the labels of an organization
+func RetrieveLabels(ctx *context.Context) {
+       labels, err := models.GetLabelsByOrgID(ctx.Org.Organization.ID, ctx.Query("sort"), models.ListOptions{})
+       if err != nil {
+               ctx.ServerError("RetrieveLabels.GetLabels", err)
+               return
+       }
+       for _, l := range labels {
+               l.CalOpenIssues()
+       }
+       ctx.Data["Labels"] = labels
+       ctx.Data["NumLabels"] = len(labels)
+       ctx.Data["SortType"] = ctx.Query("sort")
+}
+
+// NewLabel create new label for organization
+func NewLabel(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.CreateLabelForm)
+       ctx.Data["Title"] = ctx.Tr("repo.labels")
+       ctx.Data["PageIsLabels"] = true
+
+       if ctx.HasError() {
+               ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
+               ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
+               return
+       }
+
+       l := &models.Label{
+               OrgID:       ctx.Org.Organization.ID,
+               Name:        form.Title,
+               Description: form.Description,
+               Color:       form.Color,
+       }
+       if err := models.NewLabel(l); err != nil {
+               ctx.ServerError("NewLabel", err)
+               return
+       }
+       ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
+}
+
+// UpdateLabel update a label's name and color
+func UpdateLabel(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.CreateLabelForm)
+       l, err := models.GetLabelInOrgByID(ctx.Org.Organization.ID, form.ID)
+       if err != nil {
+               switch {
+               case models.IsErrOrgLabelNotExist(err):
+                       ctx.Error(http.StatusNotFound)
+               default:
+                       ctx.ServerError("UpdateLabel", err)
+               }
+               return
+       }
+
+       l.Name = form.Title
+       l.Description = form.Description
+       l.Color = form.Color
+       if err := models.UpdateLabel(l); err != nil {
+               ctx.ServerError("UpdateLabel", err)
+               return
+       }
+       ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
+}
+
+// DeleteLabel delete a label
+func DeleteLabel(ctx *context.Context) {
+       if err := models.DeleteLabel(ctx.Org.Organization.ID, ctx.QueryInt64("id")); err != nil {
+               ctx.Flash.Error("DeleteLabel: " + err.Error())
+       } else {
+               ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": ctx.Org.OrgLink + "/settings/labels",
+       })
+}
+
+// InitializeLabels init labels for an organization
+func InitializeLabels(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.InitializeLabelsForm)
+       if ctx.HasError() {
+               ctx.Redirect(ctx.Repo.RepoLink + "/labels")
+               return
+       }
+
+       if err := models.InitializeLabels(models.DefaultDBContext(), ctx.Org.Organization.ID, form.TemplateName, true); err != nil {
+               if models.IsErrIssueLabelTemplateLoad(err) {
+                       originalErr := err.(models.ErrIssueLabelTemplateLoad).OriginalError
+                       ctx.Flash.Error(ctx.Tr("repo.issues.label_templates.fail_to_load_file", form.TemplateName, originalErr))
+                       ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
+                       return
+               }
+               ctx.ServerError("InitializeLabels", err)
+               return
+       }
+       ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
+}
diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go
new file mode 100644 (file)
index 0000000..aed90c6
--- /dev/null
@@ -0,0 +1,219 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2019 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 org
+
+import (
+       "net/http"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/web"
+       userSetting "code.gitea.io/gitea/routers/web/user/setting"
+       "code.gitea.io/gitea/services/forms"
+)
+
+const (
+       // tplSettingsOptions template path for render settings
+       tplSettingsOptions base.TplName = "org/settings/options"
+       // tplSettingsDelete template path for render delete repository
+       tplSettingsDelete base.TplName = "org/settings/delete"
+       // tplSettingsHooks template path for render hook settings
+       tplSettingsHooks base.TplName = "org/settings/hooks"
+       // tplSettingsLabels template path for render labels settings
+       tplSettingsLabels base.TplName = "org/settings/labels"
+)
+
+// Settings render the main settings page
+func Settings(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("org.settings")
+       ctx.Data["PageIsSettingsOptions"] = true
+       ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility
+       ctx.Data["RepoAdminChangeTeamAccess"] = ctx.Org.Organization.RepoAdminChangeTeamAccess
+       ctx.HTML(http.StatusOK, tplSettingsOptions)
+}
+
+// SettingsPost response for settings change submited
+func SettingsPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.UpdateOrgSettingForm)
+       ctx.Data["Title"] = ctx.Tr("org.settings")
+       ctx.Data["PageIsSettingsOptions"] = true
+       ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplSettingsOptions)
+               return
+       }
+
+       org := ctx.Org.Organization
+       nameChanged := org.Name != form.Name
+
+       // Check if organization name has been changed.
+       if org.LowerName != strings.ToLower(form.Name) {
+               isExist, err := models.IsUserExist(org.ID, form.Name)
+               if err != nil {
+                       ctx.ServerError("IsUserExist", err)
+                       return
+               } else if isExist {
+                       ctx.Data["OrgName"] = true
+                       ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form)
+                       return
+               } else if err = models.ChangeUserName(org, form.Name); err != nil {
+                       if err == models.ErrUserNameIllegal {
+                               ctx.Data["OrgName"] = true
+                               ctx.RenderWithErr(ctx.Tr("form.illegal_username"), tplSettingsOptions, &form)
+                       } else {
+                               ctx.ServerError("ChangeUserName", err)
+                       }
+                       return
+               }
+               // reset ctx.org.OrgLink with new name
+               ctx.Org.OrgLink = setting.AppSubURL + "/org/" + form.Name
+               log.Trace("Organization name changed: %s -> %s", org.Name, form.Name)
+               nameChanged = false
+       }
+
+       // In case it's just a case change.
+       org.Name = form.Name
+       org.LowerName = strings.ToLower(form.Name)
+
+       if ctx.User.IsAdmin {
+               org.MaxRepoCreation = form.MaxRepoCreation
+       }
+
+       org.FullName = form.FullName
+       org.Description = form.Description
+       org.Website = form.Website
+       org.Location = form.Location
+       org.RepoAdminChangeTeamAccess = form.RepoAdminChangeTeamAccess
+
+       visibilityChanged := form.Visibility != org.Visibility
+       org.Visibility = form.Visibility
+
+       if err := models.UpdateUser(org); err != nil {
+               ctx.ServerError("UpdateUser", err)
+               return
+       }
+
+       // update forks visibility
+       if visibilityChanged {
+               if err := org.GetRepositories(models.ListOptions{Page: 1, PageSize: org.NumRepos}); err != nil {
+                       ctx.ServerError("GetRepositories", err)
+                       return
+               }
+               for _, repo := range org.Repos {
+                       repo.OwnerName = org.Name
+                       if err := models.UpdateRepository(repo, true); err != nil {
+                               ctx.ServerError("UpdateRepository", err)
+                               return
+                       }
+               }
+       } else if nameChanged {
+               if err := models.UpdateRepositoryOwnerNames(org.ID, org.Name); err != nil {
+                       ctx.ServerError("UpdateRepository", err)
+                       return
+               }
+       }
+
+       log.Trace("Organization setting updated: %s", org.Name)
+       ctx.Flash.Success(ctx.Tr("org.settings.update_setting_success"))
+       ctx.Redirect(ctx.Org.OrgLink + "/settings")
+}
+
+// SettingsAvatar response for change avatar on settings page
+func SettingsAvatar(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.AvatarForm)
+       form.Source = forms.AvatarLocal
+       if err := userSetting.UpdateAvatarSetting(ctx, form, ctx.Org.Organization); err != nil {
+               ctx.Flash.Error(err.Error())
+       } else {
+               ctx.Flash.Success(ctx.Tr("org.settings.update_avatar_success"))
+       }
+
+       ctx.Redirect(ctx.Org.OrgLink + "/settings")
+}
+
+// SettingsDeleteAvatar response for delete avatar on setings page
+func SettingsDeleteAvatar(ctx *context.Context) {
+       if err := ctx.Org.Organization.DeleteAvatar(); err != nil {
+               ctx.Flash.Error(err.Error())
+       }
+
+       ctx.Redirect(ctx.Org.OrgLink + "/settings")
+}
+
+// SettingsDelete response for deleting an organization
+func SettingsDelete(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("org.settings")
+       ctx.Data["PageIsSettingsDelete"] = true
+
+       org := ctx.Org.Organization
+       if ctx.Req.Method == "POST" {
+               if org.Name != ctx.Query("org_name") {
+                       ctx.Data["Err_OrgName"] = true
+                       ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_org_name"), tplSettingsDelete, nil)
+                       return
+               }
+
+               if err := models.DeleteOrganization(org); err != nil {
+                       if models.IsErrUserOwnRepos(err) {
+                               ctx.Flash.Error(ctx.Tr("form.org_still_own_repo"))
+                               ctx.Redirect(ctx.Org.OrgLink + "/settings/delete")
+                       } else {
+                               ctx.ServerError("DeleteOrganization", err)
+                       }
+               } else {
+                       log.Trace("Organization deleted: %s", org.Name)
+                       ctx.Redirect(setting.AppSubURL + "/")
+               }
+               return
+       }
+
+       ctx.HTML(http.StatusOK, tplSettingsDelete)
+}
+
+// Webhooks render webhook list page
+func Webhooks(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("org.settings")
+       ctx.Data["PageIsSettingsHooks"] = true
+       ctx.Data["BaseLink"] = ctx.Org.OrgLink + "/settings/hooks"
+       ctx.Data["BaseLinkNew"] = ctx.Org.OrgLink + "/settings/hooks"
+       ctx.Data["Description"] = ctx.Tr("org.settings.hooks_desc")
+
+       ws, err := models.GetWebhooksByOrgID(ctx.Org.Organization.ID, models.ListOptions{})
+       if err != nil {
+               ctx.ServerError("GetWebhooksByOrgId", err)
+               return
+       }
+
+       ctx.Data["Webhooks"] = ws
+       ctx.HTML(http.StatusOK, tplSettingsHooks)
+}
+
+// DeleteWebhook response for delete webhook
+func DeleteWebhook(ctx *context.Context) {
+       if err := models.DeleteWebhookByOrgID(ctx.Org.Organization.ID, ctx.QueryInt64("id")); err != nil {
+               ctx.Flash.Error("DeleteWebhookByOrgID: " + err.Error())
+       } else {
+               ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": ctx.Org.OrgLink + "/settings/hooks",
+       })
+}
+
+// Labels render organization labels page
+func Labels(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.labels")
+       ctx.Data["PageIsOrgSettingsLabels"] = true
+       ctx.Data["RequireTribute"] = true
+       ctx.Data["LabelTemplates"] = models.LabelTemplates
+       ctx.HTML(http.StatusOK, tplSettingsLabels)
+}
diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go
new file mode 100644 (file)
index 0000000..e612cd7
--- /dev/null
@@ -0,0 +1,357 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2019 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 org
+
+import (
+       "net/http"
+       "path"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/routers/utils"
+       "code.gitea.io/gitea/services/forms"
+)
+
+const (
+       // tplTeams template path for teams list page
+       tplTeams base.TplName = "org/team/teams"
+       // tplTeamNew template path for create new team page
+       tplTeamNew base.TplName = "org/team/new"
+       // tplTeamMembers template path for showing team members page
+       tplTeamMembers base.TplName = "org/team/members"
+       // tplTeamRepositories template path for showing team repositories page
+       tplTeamRepositories base.TplName = "org/team/repositories"
+)
+
+// Teams render teams list page
+func Teams(ctx *context.Context) {
+       org := ctx.Org.Organization
+       ctx.Data["Title"] = org.FullName
+       ctx.Data["PageIsOrgTeams"] = true
+
+       for _, t := range org.Teams {
+               if err := t.GetMembers(&models.SearchMembersOptions{}); err != nil {
+                       ctx.ServerError("GetMembers", err)
+                       return
+               }
+       }
+       ctx.Data["Teams"] = org.Teams
+
+       ctx.HTML(http.StatusOK, tplTeams)
+}
+
+// TeamsAction response for join, leave, remove, add operations to team
+func TeamsAction(ctx *context.Context) {
+       uid := ctx.QueryInt64("uid")
+       if uid == 0 {
+               ctx.Redirect(ctx.Org.OrgLink + "/teams")
+               return
+       }
+
+       page := ctx.Query("page")
+       var err error
+       switch ctx.Params(":action") {
+       case "join":
+               if !ctx.Org.IsOwner {
+                       ctx.Error(http.StatusNotFound)
+                       return
+               }
+               err = ctx.Org.Team.AddMember(ctx.User.ID)
+       case "leave":
+               err = ctx.Org.Team.RemoveMember(ctx.User.ID)
+       case "remove":
+               if !ctx.Org.IsOwner {
+                       ctx.Error(http.StatusNotFound)
+                       return
+               }
+               err = ctx.Org.Team.RemoveMember(uid)
+               page = "team"
+       case "add":
+               if !ctx.Org.IsOwner {
+                       ctx.Error(http.StatusNotFound)
+                       return
+               }
+               uname := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.Query("uname")))
+               var u *models.User
+               u, err = models.GetUserByName(uname)
+               if err != nil {
+                       if models.IsErrUserNotExist(err) {
+                               ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
+                               ctx.Redirect(ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName)
+                       } else {
+                               ctx.ServerError(" GetUserByName", err)
+                       }
+                       return
+               }
+
+               if u.IsOrganization() {
+                       ctx.Flash.Error(ctx.Tr("form.cannot_add_org_to_team"))
+                       ctx.Redirect(ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName)
+                       return
+               }
+
+               if ctx.Org.Team.IsMember(u.ID) {
+                       ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users"))
+               } else {
+                       err = ctx.Org.Team.AddMember(u.ID)
+               }
+
+               page = "team"
+       }
+
+       if err != nil {
+               if models.IsErrLastOrgOwner(err) {
+                       ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
+               } else {
+                       log.Error("Action(%s): %v", ctx.Params(":action"), err)
+                       ctx.JSON(http.StatusOK, map[string]interface{}{
+                               "ok":  false,
+                               "err": err.Error(),
+                       })
+                       return
+               }
+       }
+
+       switch page {
+       case "team":
+               ctx.Redirect(ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName)
+       case "home":
+               ctx.Redirect(ctx.Org.Organization.HomeLink())
+       default:
+               ctx.Redirect(ctx.Org.OrgLink + "/teams")
+       }
+}
+
+// TeamsRepoAction operate team's repository
+func TeamsRepoAction(ctx *context.Context) {
+       if !ctx.Org.IsOwner {
+               ctx.Error(http.StatusNotFound)
+               return
+       }
+
+       var err error
+       action := ctx.Params(":action")
+       switch action {
+       case "add":
+               repoName := path.Base(ctx.Query("repo_name"))
+               var repo *models.Repository
+               repo, err = models.GetRepositoryByName(ctx.Org.Organization.ID, repoName)
+               if err != nil {
+                       if models.IsErrRepoNotExist(err) {
+                               ctx.Flash.Error(ctx.Tr("org.teams.add_nonexistent_repo"))
+                               ctx.Redirect(ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName + "/repositories")
+                               return
+                       }
+                       ctx.ServerError("GetRepositoryByName", err)
+                       return
+               }
+               err = ctx.Org.Team.AddRepository(repo)
+       case "remove":
+               err = ctx.Org.Team.RemoveRepository(ctx.QueryInt64("repoid"))
+       case "addall":
+               err = ctx.Org.Team.AddAllRepositories()
+       case "removeall":
+               err = ctx.Org.Team.RemoveAllRepositories()
+       }
+
+       if err != nil {
+               log.Error("Action(%s): '%s' %v", ctx.Params(":action"), ctx.Org.Team.Name, err)
+               ctx.ServerError("TeamsRepoAction", err)
+               return
+       }
+
+       if action == "addall" || action == "removeall" {
+               ctx.JSON(http.StatusOK, map[string]interface{}{
+                       "redirect": ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName + "/repositories",
+               })
+               return
+       }
+       ctx.Redirect(ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName + "/repositories")
+}
+
+// NewTeam render create new team page
+func NewTeam(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Org.Organization.FullName
+       ctx.Data["PageIsOrgTeams"] = true
+       ctx.Data["PageIsOrgTeamsNew"] = true
+       ctx.Data["Team"] = &models.Team{}
+       ctx.Data["Units"] = models.Units
+       ctx.HTML(http.StatusOK, tplTeamNew)
+}
+
+// NewTeamPost response for create new team
+func NewTeamPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.CreateTeamForm)
+       ctx.Data["Title"] = ctx.Org.Organization.FullName
+       ctx.Data["PageIsOrgTeams"] = true
+       ctx.Data["PageIsOrgTeamsNew"] = true
+       ctx.Data["Units"] = models.Units
+       var includesAllRepositories = form.RepoAccess == "all"
+
+       t := &models.Team{
+               OrgID:                   ctx.Org.Organization.ID,
+               Name:                    form.TeamName,
+               Description:             form.Description,
+               Authorize:               models.ParseAccessMode(form.Permission),
+               IncludesAllRepositories: includesAllRepositories,
+               CanCreateOrgRepo:        form.CanCreateOrgRepo,
+       }
+
+       if t.Authorize < models.AccessModeOwner {
+               var units = make([]*models.TeamUnit, 0, len(form.Units))
+               for _, tp := range form.Units {
+                       units = append(units, &models.TeamUnit{
+                               OrgID: ctx.Org.Organization.ID,
+                               Type:  tp,
+                       })
+               }
+               t.Units = units
+       }
+
+       ctx.Data["Team"] = t
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplTeamNew)
+               return
+       }
+
+       if t.Authorize < models.AccessModeAdmin && len(form.Units) == 0 {
+               ctx.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form)
+               return
+       }
+
+       if err := models.NewTeam(t); err != nil {
+               ctx.Data["Err_TeamName"] = true
+               switch {
+               case models.IsErrTeamAlreadyExist(err):
+                       ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form)
+               default:
+                       ctx.ServerError("NewTeam", err)
+               }
+               return
+       }
+       log.Trace("Team created: %s/%s", ctx.Org.Organization.Name, t.Name)
+       ctx.Redirect(ctx.Org.OrgLink + "/teams/" + t.LowerName)
+}
+
+// TeamMembers render team members page
+func TeamMembers(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Org.Team.Name
+       ctx.Data["PageIsOrgTeams"] = true
+       ctx.Data["PageIsOrgTeamMembers"] = true
+       if err := ctx.Org.Team.GetMembers(&models.SearchMembersOptions{}); err != nil {
+               ctx.ServerError("GetMembers", err)
+               return
+       }
+       ctx.HTML(http.StatusOK, tplTeamMembers)
+}
+
+// TeamRepositories show the repositories of team
+func TeamRepositories(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Org.Team.Name
+       ctx.Data["PageIsOrgTeams"] = true
+       ctx.Data["PageIsOrgTeamRepos"] = true
+       if err := ctx.Org.Team.GetRepositories(&models.SearchTeamOptions{}); err != nil {
+               ctx.ServerError("GetRepositories", err)
+               return
+       }
+       ctx.HTML(http.StatusOK, tplTeamRepositories)
+}
+
+// EditTeam render team edit page
+func EditTeam(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Org.Organization.FullName
+       ctx.Data["PageIsOrgTeams"] = true
+       ctx.Data["team_name"] = ctx.Org.Team.Name
+       ctx.Data["desc"] = ctx.Org.Team.Description
+       ctx.Data["Units"] = models.Units
+       ctx.HTML(http.StatusOK, tplTeamNew)
+}
+
+// EditTeamPost response for modify team information
+func EditTeamPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.CreateTeamForm)
+       t := ctx.Org.Team
+       ctx.Data["Title"] = ctx.Org.Organization.FullName
+       ctx.Data["PageIsOrgTeams"] = true
+       ctx.Data["Team"] = t
+       ctx.Data["Units"] = models.Units
+
+       isAuthChanged := false
+       isIncludeAllChanged := false
+       var includesAllRepositories = form.RepoAccess == "all"
+       if !t.IsOwnerTeam() {
+               // Validate permission level.
+               auth := models.ParseAccessMode(form.Permission)
+
+               t.Name = form.TeamName
+               if t.Authorize != auth {
+                       isAuthChanged = true
+                       t.Authorize = auth
+               }
+
+               if t.IncludesAllRepositories != includesAllRepositories {
+                       isIncludeAllChanged = true
+                       t.IncludesAllRepositories = includesAllRepositories
+               }
+       }
+       t.Description = form.Description
+       if t.Authorize < models.AccessModeOwner {
+               var units = make([]models.TeamUnit, 0, len(form.Units))
+               for _, tp := range form.Units {
+                       units = append(units, models.TeamUnit{
+                               OrgID:  t.OrgID,
+                               TeamID: t.ID,
+                               Type:   tp,
+                       })
+               }
+               err := models.UpdateTeamUnits(t, units)
+               if err != nil {
+                       ctx.Error(http.StatusInternalServerError, "LoadIssue", err.Error())
+                       return
+               }
+       }
+       t.CanCreateOrgRepo = form.CanCreateOrgRepo
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplTeamNew)
+               return
+       }
+
+       if t.Authorize < models.AccessModeAdmin && len(form.Units) == 0 {
+               ctx.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form)
+               return
+       }
+
+       if err := models.UpdateTeam(t, isAuthChanged, isIncludeAllChanged); err != nil {
+               ctx.Data["Err_TeamName"] = true
+               switch {
+               case models.IsErrTeamAlreadyExist(err):
+                       ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form)
+               default:
+                       ctx.ServerError("UpdateTeam", err)
+               }
+               return
+       }
+       ctx.Redirect(ctx.Org.OrgLink + "/teams/" + t.LowerName)
+}
+
+// DeleteTeam response for the delete team request
+func DeleteTeam(ctx *context.Context) {
+       if err := models.DeleteTeam(ctx.Org.Team); err != nil {
+               ctx.Flash.Error("DeleteTeam: " + err.Error())
+       } else {
+               ctx.Flash.Success(ctx.Tr("org.teams.delete_team_success"))
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": ctx.Org.OrgLink + "/teams",
+       })
+}
diff --git a/routers/web/repo/activity.go b/routers/web/repo/activity.go
new file mode 100644 (file)
index 0000000..dcb7bf5
--- /dev/null
@@ -0,0 +1,103 @@
+// 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 repo
+
+import (
+       "net/http"
+       "time"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+)
+
+const (
+       tplActivity base.TplName = "repo/activity"
+)
+
+// Activity render the page to show repository latest changes
+func Activity(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.activity")
+       ctx.Data["PageIsActivity"] = true
+
+       ctx.Data["Period"] = ctx.Params("period")
+
+       timeUntil := time.Now()
+       var timeFrom time.Time
+
+       switch ctx.Data["Period"] {
+       case "daily":
+               timeFrom = timeUntil.Add(-time.Hour * 24)
+       case "halfweekly":
+               timeFrom = timeUntil.Add(-time.Hour * 72)
+       case "weekly":
+               timeFrom = timeUntil.Add(-time.Hour * 168)
+       case "monthly":
+               timeFrom = timeUntil.AddDate(0, -1, 0)
+       case "quarterly":
+               timeFrom = timeUntil.AddDate(0, -3, 0)
+       case "semiyearly":
+               timeFrom = timeUntil.AddDate(0, -6, 0)
+       case "yearly":
+               timeFrom = timeUntil.AddDate(-1, 0, 0)
+       default:
+               ctx.Data["Period"] = "weekly"
+               timeFrom = timeUntil.Add(-time.Hour * 168)
+       }
+       ctx.Data["DateFrom"] = timeFrom.Format("January 2, 2006")
+       ctx.Data["DateUntil"] = timeUntil.Format("January 2, 2006")
+       ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + ctx.Data["Period"].(string))
+
+       var err error
+       if ctx.Data["Activity"], err = models.GetActivityStats(ctx.Repo.Repository, timeFrom,
+               ctx.Repo.CanRead(models.UnitTypeReleases),
+               ctx.Repo.CanRead(models.UnitTypeIssues),
+               ctx.Repo.CanRead(models.UnitTypePullRequests),
+               ctx.Repo.CanRead(models.UnitTypeCode)); err != nil {
+               ctx.ServerError("GetActivityStats", err)
+               return
+       }
+
+       if ctx.Data["ActivityTopAuthors"], err = models.GetActivityStatsTopAuthors(ctx.Repo.Repository, timeFrom, 10); err != nil {
+               ctx.ServerError("GetActivityStatsTopAuthors", err)
+               return
+       }
+
+       ctx.HTML(http.StatusOK, tplActivity)
+}
+
+// ActivityAuthors renders JSON with top commit authors for given time period over all branches
+func ActivityAuthors(ctx *context.Context) {
+       timeUntil := time.Now()
+       var timeFrom time.Time
+
+       switch ctx.Params("period") {
+       case "daily":
+               timeFrom = timeUntil.Add(-time.Hour * 24)
+       case "halfweekly":
+               timeFrom = timeUntil.Add(-time.Hour * 72)
+       case "weekly":
+               timeFrom = timeUntil.Add(-time.Hour * 168)
+       case "monthly":
+               timeFrom = timeUntil.AddDate(0, -1, 0)
+       case "quarterly":
+               timeFrom = timeUntil.AddDate(0, -3, 0)
+       case "semiyearly":
+               timeFrom = timeUntil.AddDate(0, -6, 0)
+       case "yearly":
+               timeFrom = timeUntil.AddDate(-1, 0, 0)
+       default:
+               timeFrom = timeUntil.Add(-time.Hour * 168)
+       }
+
+       var err error
+       authors, err := models.GetActivityStatsTopAuthors(ctx.Repo.Repository, timeFrom, 10)
+       if err != nil {
+               ctx.ServerError("GetActivityStatsTopAuthors", err)
+               return
+       }
+
+       ctx.JSON(http.StatusOK, authors)
+}
diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go
new file mode 100644 (file)
index 0000000..5becbea
--- /dev/null
@@ -0,0 +1,160 @@
+// 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 repo
+
+import (
+       "fmt"
+       "net/http"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/httpcache"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/storage"
+       "code.gitea.io/gitea/modules/upload"
+       "code.gitea.io/gitea/routers/common"
+)
+
+// UploadIssueAttachment response for Issue/PR attachments
+func UploadIssueAttachment(ctx *context.Context) {
+       uploadAttachment(ctx, setting.Attachment.AllowedTypes)
+}
+
+// UploadReleaseAttachment response for uploading release attachments
+func UploadReleaseAttachment(ctx *context.Context) {
+       uploadAttachment(ctx, setting.Repository.Release.AllowedTypes)
+}
+
+// UploadAttachment response for uploading attachments
+func uploadAttachment(ctx *context.Context, allowedTypes string) {
+       if !setting.Attachment.Enabled {
+               ctx.Error(http.StatusNotFound, "attachment is not enabled")
+               return
+       }
+
+       file, header, err := ctx.Req.FormFile("file")
+       if err != nil {
+               ctx.Error(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err))
+               return
+       }
+       defer file.Close()
+
+       buf := make([]byte, 1024)
+       n, _ := file.Read(buf)
+       if n > 0 {
+               buf = buf[:n]
+       }
+
+       err = upload.Verify(buf, header.Filename, allowedTypes)
+       if err != nil {
+               ctx.Error(http.StatusBadRequest, err.Error())
+               return
+       }
+
+       attach, err := models.NewAttachment(&models.Attachment{
+               UploaderID: ctx.User.ID,
+               Name:       header.Filename,
+       }, buf, file)
+       if err != nil {
+               ctx.Error(http.StatusInternalServerError, fmt.Sprintf("NewAttachment: %v", err))
+               return
+       }
+
+       log.Trace("New attachment uploaded: %s", attach.UUID)
+       ctx.JSON(http.StatusOK, map[string]string{
+               "uuid": attach.UUID,
+       })
+}
+
+// DeleteAttachment response for deleting issue's attachment
+func DeleteAttachment(ctx *context.Context) {
+       file := ctx.Query("file")
+       attach, err := models.GetAttachmentByUUID(file)
+       if err != nil {
+               ctx.Error(http.StatusBadRequest, err.Error())
+               return
+       }
+       if !ctx.IsSigned || (ctx.User.ID != attach.UploaderID) {
+               ctx.Error(http.StatusForbidden)
+               return
+       }
+       err = models.DeleteAttachment(attach, true)
+       if err != nil {
+               ctx.Error(http.StatusInternalServerError, fmt.Sprintf("DeleteAttachment: %v", err))
+               return
+       }
+       ctx.JSON(http.StatusOK, map[string]string{
+               "uuid": attach.UUID,
+       })
+}
+
+// GetAttachment serve attachements
+func GetAttachment(ctx *context.Context) {
+       attach, err := models.GetAttachmentByUUID(ctx.Params(":uuid"))
+       if err != nil {
+               if models.IsErrAttachmentNotExist(err) {
+                       ctx.Error(http.StatusNotFound)
+               } else {
+                       ctx.ServerError("GetAttachmentByUUID", err)
+               }
+               return
+       }
+
+       repository, unitType, err := attach.LinkedRepository()
+       if err != nil {
+               ctx.ServerError("LinkedRepository", err)
+               return
+       }
+
+       if repository == nil { //If not linked
+               if !(ctx.IsSigned && attach.UploaderID == ctx.User.ID) { //We block if not the uploader
+                       ctx.Error(http.StatusNotFound)
+                       return
+               }
+       } else { //If we have the repository we check access
+               perm, err := models.GetUserRepoPermission(repository, ctx.User)
+               if err != nil {
+                       ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err.Error())
+                       return
+               }
+               if !perm.CanRead(unitType) {
+                       ctx.Error(http.StatusNotFound)
+                       return
+               }
+       }
+
+       if err := attach.IncreaseDownloadCount(); err != nil {
+               ctx.ServerError("IncreaseDownloadCount", err)
+               return
+       }
+
+       if setting.Attachment.ServeDirect {
+               //If we have a signed url (S3, object storage), redirect to this directly.
+               u, err := storage.Attachments.URL(attach.RelativePath(), attach.Name)
+
+               if u != nil && err == nil {
+                       ctx.Redirect(u.String())
+                       return
+               }
+       }
+
+       if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+attach.UUID+`"`) {
+               return
+       }
+
+       //If we have matched and access to release or issue
+       fr, err := storage.Attachments.Open(attach.RelativePath())
+       if err != nil {
+               ctx.ServerError("Open", err)
+               return
+       }
+       defer fr.Close()
+
+       if err = common.ServeData(ctx, attach.Name, attach.Size, fr); err != nil {
+               ctx.ServerError("ServeData", err)
+               return
+       }
+}
diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go
new file mode 100644 (file)
index 0000000..1a3e1dc
--- /dev/null
@@ -0,0 +1,251 @@
+// Copyright 2019 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 repo
+
+import (
+       "bytes"
+       "container/list"
+       "fmt"
+       "html"
+       gotemplate "html/template"
+       "net/http"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/highlight"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/templates"
+       "code.gitea.io/gitea/modules/timeutil"
+)
+
+const (
+       tplBlame base.TplName = "repo/home"
+)
+
+// RefBlame render blame page
+func RefBlame(ctx *context.Context) {
+       fileName := ctx.Repo.TreePath
+       if len(fileName) == 0 {
+               ctx.NotFound("Blame FileName", nil)
+               return
+       }
+
+       userName := ctx.Repo.Owner.Name
+       repoName := ctx.Repo.Repository.Name
+       commitID := ctx.Repo.CommitID
+
+       commit, err := ctx.Repo.GitRepo.GetCommit(commitID)
+       if err != nil {
+               if git.IsErrNotExist(err) {
+                       ctx.NotFound("Repo.GitRepo.GetCommit", err)
+               } else {
+                       ctx.ServerError("Repo.GitRepo.GetCommit", err)
+               }
+               return
+       }
+       if len(commitID) != 40 {
+               commitID = commit.ID.String()
+       }
+
+       branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
+       treeLink := branchLink
+       rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL()
+
+       if len(ctx.Repo.TreePath) > 0 {
+               treeLink += "/" + ctx.Repo.TreePath
+       }
+
+       var treeNames []string
+       paths := make([]string, 0, 5)
+       if len(ctx.Repo.TreePath) > 0 {
+               treeNames = strings.Split(ctx.Repo.TreePath, "/")
+               for i := range treeNames {
+                       paths = append(paths, strings.Join(treeNames[:i+1], "/"))
+               }
+
+               ctx.Data["HasParentPath"] = true
+               if len(paths)-2 >= 0 {
+                       ctx.Data["ParentPath"] = "/" + paths[len(paths)-1]
+               }
+       }
+
+       // Show latest commit info of repository in table header,
+       // or of directory if not in root directory.
+       latestCommit := ctx.Repo.Commit
+       if len(ctx.Repo.TreePath) > 0 {
+               latestCommit, err = ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath)
+               if err != nil {
+                       ctx.ServerError("GetCommitByPath", err)
+                       return
+               }
+       }
+       ctx.Data["LatestCommit"] = latestCommit
+       ctx.Data["LatestCommitVerification"] = models.ParseCommitWithSignature(latestCommit)
+       ctx.Data["LatestCommitUser"] = models.ValidateCommitWithEmail(latestCommit)
+
+       statuses, err := models.GetLatestCommitStatus(ctx.Repo.Repository.ID, ctx.Repo.Commit.ID.String(), models.ListOptions{})
+       if err != nil {
+               log.Error("GetLatestCommitStatus: %v", err)
+       }
+
+       // Get current entry user currently looking at.
+       entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
+       if err != nil {
+               ctx.NotFoundOrServerError("Repo.Commit.GetTreeEntryByPath", git.IsErrNotExist, err)
+               return
+       }
+
+       blob := entry.Blob()
+
+       ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(statuses)
+       ctx.Data["LatestCommitStatuses"] = statuses
+
+       ctx.Data["Paths"] = paths
+       ctx.Data["TreeLink"] = treeLink
+       ctx.Data["TreeNames"] = treeNames
+       ctx.Data["BranchLink"] = branchLink
+
+       ctx.Data["RawFileLink"] = rawLink + "/" + ctx.Repo.TreePath
+       ctx.Data["PageIsViewCode"] = true
+
+       ctx.Data["IsBlame"] = true
+
+       ctx.Data["FileSize"] = blob.Size()
+       ctx.Data["FileName"] = blob.Name()
+
+       ctx.Data["NumLines"], err = blob.GetBlobLineCount()
+       if err != nil {
+               ctx.NotFound("GetBlobLineCount", err)
+               return
+       }
+
+       blameReader, err := git.CreateBlameReader(ctx, models.RepoPath(userName, repoName), commitID, fileName)
+       if err != nil {
+               ctx.NotFound("CreateBlameReader", err)
+               return
+       }
+       defer blameReader.Close()
+
+       blameParts := make([]git.BlamePart, 0)
+
+       for {
+               blamePart, err := blameReader.NextPart()
+               if err != nil {
+                       ctx.NotFound("NextPart", err)
+                       return
+               }
+               if blamePart == nil {
+                       break
+               }
+               blameParts = append(blameParts, *blamePart)
+       }
+
+       commitNames := make(map[string]models.UserCommit)
+       commits := list.New()
+
+       for _, part := range blameParts {
+               sha := part.Sha
+               if _, ok := commitNames[sha]; ok {
+                       continue
+               }
+
+               commit, err := ctx.Repo.GitRepo.GetCommit(sha)
+               if err != nil {
+                       if git.IsErrNotExist(err) {
+                               ctx.NotFound("Repo.GitRepo.GetCommit", err)
+                       } else {
+                               ctx.ServerError("Repo.GitRepo.GetCommit", err)
+                       }
+                       return
+               }
+
+               commits.PushBack(commit)
+
+               commitNames[commit.ID.String()] = models.UserCommit{}
+       }
+
+       commits = models.ValidateCommitsWithEmails(commits)
+
+       for e := commits.Front(); e != nil; e = e.Next() {
+               c := e.Value.(models.UserCommit)
+
+               commitNames[c.ID.String()] = c
+       }
+
+       // Get Topics of this repo
+       renderRepoTopics(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       renderBlame(ctx, blameParts, commitNames)
+
+       ctx.HTML(http.StatusOK, tplBlame)
+}
+
+func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames map[string]models.UserCommit) {
+       repoLink := ctx.Repo.RepoLink
+
+       var lines = make([]string, 0)
+
+       var commitInfo bytes.Buffer
+       var lineNumbers bytes.Buffer
+       var codeLines bytes.Buffer
+
+       var i = 0
+       for pi, part := range blameParts {
+               for index, line := range part.Lines {
+                       i++
+                       lines = append(lines, line)
+
+                       var attr = ""
+                       if len(part.Lines)-1 == index && len(blameParts)-1 != pi {
+                               attr = " bottom-line"
+                       }
+                       commit := commitNames[part.Sha]
+                       if index == 0 {
+                               // User avatar image
+                               commitSince := timeutil.TimeSinceUnix(timeutil.TimeStamp(commit.Author.When.Unix()), ctx.Data["Lang"].(string))
+
+                               var avatar string
+                               if commit.User != nil {
+                                       avatar = string(templates.Avatar(commit.User, 18, "mr-3"))
+                               } else {
+                                       avatar = string(templates.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18, "mr-3"))
+                               }
+
+                               commitInfo.WriteString(fmt.Sprintf(`<div class="blame-info%s"><div class="blame-data"><div class="blame-avatar">%s</div><div class="blame-message"><a href="%s/commit/%s" title="%[5]s">%[5]s</a></div><div class="blame-time">%s</div></div></div>`, attr, avatar, repoLink, part.Sha, html.EscapeString(commit.CommitMessage), commitSince))
+                       } else {
+                               commitInfo.WriteString(fmt.Sprintf(`<div class="blame-info%s">&#8203;</div>`, attr))
+                       }
+
+                       //Line number
+                       if len(part.Lines)-1 == index && len(blameParts)-1 != pi {
+                               lineNumbers.WriteString(fmt.Sprintf(`<span id="L%d" data-line-number="%d" class="bottom-line"></span>`, i, i))
+                       } else {
+                               lineNumbers.WriteString(fmt.Sprintf(`<span id="L%d" data-line-number="%d"></span>`, i, i))
+                       }
+
+                       if i != len(lines)-1 {
+                               line += "\n"
+                       }
+                       fileName := fmt.Sprintf("%v", ctx.Data["FileName"])
+                       line = highlight.Code(fileName, line)
+                       line = `<code class="code-inner">` + line + `</code>`
+                       if len(part.Lines)-1 == index && len(blameParts)-1 != pi {
+                               codeLines.WriteString(fmt.Sprintf(`<li class="L%d bottom-line" rel="L%d">%s</li>`, i, i, line))
+                       } else {
+                               codeLines.WriteString(fmt.Sprintf(`<li class="L%d" rel="L%d">%s</li>`, i, i, line))
+                       }
+               }
+       }
+
+       ctx.Data["BlameContent"] = gotemplate.HTML(codeLines.String())
+       ctx.Data["BlameCommitInfo"] = gotemplate.HTML(commitInfo.String())
+       ctx.Data["BlameLineNums"] = gotemplate.HTML(lineNumbers.String())
+}
diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go
new file mode 100644 (file)
index 0000000..4625b1a
--- /dev/null
@@ -0,0 +1,407 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2018 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 repo
+
+import (
+       "errors"
+       "fmt"
+       "net/http"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/repofiles"
+       repo_module "code.gitea.io/gitea/modules/repository"
+       "code.gitea.io/gitea/modules/util"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/routers/utils"
+       "code.gitea.io/gitea/services/forms"
+       release_service "code.gitea.io/gitea/services/release"
+       repo_service "code.gitea.io/gitea/services/repository"
+)
+
+const (
+       tplBranch base.TplName = "repo/branch/list"
+)
+
+// Branch contains the branch information
+type Branch struct {
+       Name              string
+       Commit            *git.Commit
+       IsProtected       bool
+       IsDeleted         bool
+       IsIncluded        bool
+       DeletedBranch     *models.DeletedBranch
+       CommitsAhead      int
+       CommitsBehind     int
+       LatestPullRequest *models.PullRequest
+       MergeMovedOn      bool
+}
+
+// Branches render repository branch page
+func Branches(ctx *context.Context) {
+       ctx.Data["Title"] = "Branches"
+       ctx.Data["IsRepoToolbarBranches"] = true
+       ctx.Data["DefaultBranch"] = ctx.Repo.Repository.DefaultBranch
+       ctx.Data["AllowsPulls"] = ctx.Repo.Repository.AllowsPulls()
+       ctx.Data["IsWriter"] = ctx.Repo.CanWrite(models.UnitTypeCode)
+       ctx.Data["IsMirror"] = ctx.Repo.Repository.IsMirror
+       ctx.Data["CanPull"] = ctx.Repo.CanWrite(models.UnitTypeCode) || (ctx.IsSigned && ctx.User.HasForkedRepo(ctx.Repo.Repository.ID))
+       ctx.Data["PageIsViewCode"] = true
+       ctx.Data["PageIsBranches"] = true
+
+       page := ctx.QueryInt("page")
+       if page <= 1 {
+               page = 1
+       }
+
+       limit := ctx.QueryInt("limit")
+       if limit <= 0 || limit > git.BranchesRangeSize {
+               limit = git.BranchesRangeSize
+       }
+
+       skip := (page - 1) * limit
+       log.Debug("Branches: skip: %d limit: %d", skip, limit)
+       branches, branchesCount := loadBranches(ctx, skip, limit)
+       if ctx.Written() {
+               return
+       }
+       ctx.Data["Branches"] = branches
+       pager := context.NewPagination(int(branchesCount), git.BranchesRangeSize, page, 5)
+       pager.SetDefaultParams(ctx)
+       ctx.Data["Page"] = pager
+
+       ctx.HTML(http.StatusOK, tplBranch)
+}
+
+// DeleteBranchPost responses for delete merged branch
+func DeleteBranchPost(ctx *context.Context) {
+       defer redirect(ctx)
+       branchName := ctx.Query("name")
+
+       if err := repo_service.DeleteBranch(ctx.User, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil {
+               switch {
+               case git.IsErrBranchNotExist(err):
+                       log.Debug("DeleteBranch: Can't delete non existing branch '%s'", branchName)
+                       ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", branchName))
+               case errors.Is(err, repo_service.ErrBranchIsDefault):
+                       log.Debug("DeleteBranch: Can't delete default branch '%s'", branchName)
+                       ctx.Flash.Error(ctx.Tr("repo.branch.default_deletion_failed", branchName))
+               case errors.Is(err, repo_service.ErrBranchIsProtected):
+                       log.Debug("DeleteBranch: Can't delete protected branch '%s'", branchName)
+                       ctx.Flash.Error(ctx.Tr("repo.branch.protected_deletion_failed", branchName))
+               default:
+                       log.Error("DeleteBranch: %v", err)
+                       ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", branchName))
+               }
+
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.branch.deletion_success", branchName))
+}
+
+// RestoreBranchPost responses for delete merged branch
+func RestoreBranchPost(ctx *context.Context) {
+       defer redirect(ctx)
+
+       branchID := ctx.QueryInt64("branch_id")
+       branchName := ctx.Query("name")
+
+       deletedBranch, err := ctx.Repo.Repository.GetDeletedBranchByID(branchID)
+       if err != nil {
+               log.Error("GetDeletedBranchByID: %v", err)
+               ctx.Flash.Error(ctx.Tr("repo.branch.restore_failed", branchName))
+               return
+       }
+
+       if err := git.Push(ctx.Repo.Repository.RepoPath(), git.PushOptions{
+               Remote: ctx.Repo.Repository.RepoPath(),
+               Branch: fmt.Sprintf("%s:%s%s", deletedBranch.Commit, git.BranchPrefix, deletedBranch.Name),
+               Env:    models.PushingEnvironment(ctx.User, ctx.Repo.Repository),
+       }); err != nil {
+               if strings.Contains(err.Error(), "already exists") {
+                       log.Debug("RestoreBranch: Can't restore branch '%s', since one with same name already exist", deletedBranch.Name)
+                       ctx.Flash.Error(ctx.Tr("repo.branch.already_exists", deletedBranch.Name))
+                       return
+               }
+               log.Error("RestoreBranch: CreateBranch: %v", err)
+               ctx.Flash.Error(ctx.Tr("repo.branch.restore_failed", deletedBranch.Name))
+               return
+       }
+
+       // Don't return error below this
+       if err := repo_service.PushUpdate(
+               &repo_module.PushUpdateOptions{
+                       RefFullName:  git.BranchPrefix + deletedBranch.Name,
+                       OldCommitID:  git.EmptySHA,
+                       NewCommitID:  deletedBranch.Commit,
+                       PusherID:     ctx.User.ID,
+                       PusherName:   ctx.User.Name,
+                       RepoUserName: ctx.Repo.Owner.Name,
+                       RepoName:     ctx.Repo.Repository.Name,
+               }); err != nil {
+               log.Error("RestoreBranch: Update: %v", err)
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.branch.restore_success", deletedBranch.Name))
+}
+
+func redirect(ctx *context.Context) {
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": ctx.Repo.RepoLink + "/branches",
+       })
+}
+
+// loadBranches loads branches from the repository limited by page & pageSize.
+// NOTE: May write to context on error.
+func loadBranches(ctx *context.Context, skip, limit int) ([]*Branch, int) {
+       defaultBranch, err := repo_module.GetBranch(ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch)
+       if err != nil {
+               log.Error("loadBranches: get default branch: %v", err)
+               ctx.ServerError("GetDefaultBranch", err)
+               return nil, 0
+       }
+
+       rawBranches, totalNumOfBranches, err := repo_module.GetBranches(ctx.Repo.Repository, skip, limit)
+       if err != nil {
+               log.Error("GetBranches: %v", err)
+               ctx.ServerError("GetBranches", err)
+               return nil, 0
+       }
+
+       protectedBranches, err := ctx.Repo.Repository.GetProtectedBranches()
+       if err != nil {
+               ctx.ServerError("GetProtectedBranches", err)
+               return nil, 0
+       }
+
+       repoIDToRepo := map[int64]*models.Repository{}
+       repoIDToRepo[ctx.Repo.Repository.ID] = ctx.Repo.Repository
+
+       repoIDToGitRepo := map[int64]*git.Repository{}
+       repoIDToGitRepo[ctx.Repo.Repository.ID] = ctx.Repo.GitRepo
+
+       var branches []*Branch
+       for i := range rawBranches {
+               if rawBranches[i].Name == defaultBranch.Name {
+                       // Skip default branch
+                       continue
+               }
+
+               var branch = loadOneBranch(ctx, rawBranches[i], protectedBranches, repoIDToRepo, repoIDToGitRepo)
+               if branch == nil {
+                       return nil, 0
+               }
+
+               branches = append(branches, branch)
+       }
+
+       // Always add the default branch
+       log.Debug("loadOneBranch: load default: '%s'", defaultBranch.Name)
+       branches = append(branches, loadOneBranch(ctx, defaultBranch, protectedBranches, repoIDToRepo, repoIDToGitRepo))
+
+       if ctx.Repo.CanWrite(models.UnitTypeCode) {
+               deletedBranches, err := getDeletedBranches(ctx)
+               if err != nil {
+                       ctx.ServerError("getDeletedBranches", err)
+                       return nil, 0
+               }
+               branches = append(branches, deletedBranches...)
+       }
+
+       return branches, totalNumOfBranches - 1
+}
+
+func loadOneBranch(ctx *context.Context, rawBranch *git.Branch, protectedBranches []*models.ProtectedBranch,
+       repoIDToRepo map[int64]*models.Repository,
+       repoIDToGitRepo map[int64]*git.Repository) *Branch {
+       log.Trace("loadOneBranch: '%s'", rawBranch.Name)
+
+       commit, err := rawBranch.GetCommit()
+       if err != nil {
+               ctx.ServerError("GetCommit", err)
+               return nil
+       }
+
+       branchName := rawBranch.Name
+       var isProtected bool
+       for _, b := range protectedBranches {
+               if b.BranchName == branchName {
+                       isProtected = true
+                       break
+               }
+       }
+
+       divergence, divergenceError := repofiles.CountDivergingCommits(ctx.Repo.Repository, git.BranchPrefix+branchName)
+       if divergenceError != nil {
+               ctx.ServerError("CountDivergingCommits", divergenceError)
+               return nil
+       }
+
+       pr, err := models.GetLatestPullRequestByHeadInfo(ctx.Repo.Repository.ID, branchName)
+       if err != nil {
+               ctx.ServerError("GetLatestPullRequestByHeadInfo", err)
+               return nil
+       }
+       headCommit := commit.ID.String()
+
+       mergeMovedOn := false
+       if pr != nil {
+               pr.HeadRepo = ctx.Repo.Repository
+               if err := pr.LoadIssue(); err != nil {
+                       ctx.ServerError("pr.LoadIssue", err)
+                       return nil
+               }
+               if repo, ok := repoIDToRepo[pr.BaseRepoID]; ok {
+                       pr.BaseRepo = repo
+               } else if err := pr.LoadBaseRepo(); err != nil {
+                       ctx.ServerError("pr.LoadBaseRepo", err)
+                       return nil
+               } else {
+                       repoIDToRepo[pr.BaseRepoID] = pr.BaseRepo
+               }
+               pr.Issue.Repo = pr.BaseRepo
+
+               if pr.HasMerged {
+                       baseGitRepo, ok := repoIDToGitRepo[pr.BaseRepoID]
+                       if !ok {
+                               baseGitRepo, err = git.OpenRepository(pr.BaseRepo.RepoPath())
+                               if err != nil {
+                                       ctx.ServerError("OpenRepository", err)
+                                       return nil
+                               }
+                               defer baseGitRepo.Close()
+                               repoIDToGitRepo[pr.BaseRepoID] = baseGitRepo
+                       }
+                       pullCommit, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
+                       if err != nil && !git.IsErrNotExist(err) {
+                               ctx.ServerError("GetBranchCommitID", err)
+                               return nil
+                       }
+                       if err == nil && headCommit != pullCommit {
+                               // the head has moved on from the merge - we shouldn't delete
+                               mergeMovedOn = true
+                       }
+               }
+       }
+
+       isIncluded := divergence.Ahead == 0 && ctx.Repo.Repository.DefaultBranch != branchName
+       return &Branch{
+               Name:              branchName,
+               Commit:            commit,
+               IsProtected:       isProtected,
+               IsIncluded:        isIncluded,
+               CommitsAhead:      divergence.Ahead,
+               CommitsBehind:     divergence.Behind,
+               LatestPullRequest: pr,
+               MergeMovedOn:      mergeMovedOn,
+       }
+}
+
+func getDeletedBranches(ctx *context.Context) ([]*Branch, error) {
+       branches := []*Branch{}
+
+       deletedBranches, err := ctx.Repo.Repository.GetDeletedBranches()
+       if err != nil {
+               return branches, err
+       }
+
+       for i := range deletedBranches {
+               deletedBranches[i].LoadUser()
+               branches = append(branches, &Branch{
+                       Name:          deletedBranches[i].Name,
+                       IsDeleted:     true,
+                       DeletedBranch: deletedBranches[i],
+               })
+       }
+
+       return branches, nil
+}
+
+// CreateBranch creates new branch in repository
+func CreateBranch(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.NewBranchForm)
+       if !ctx.Repo.CanCreateBranch() {
+               ctx.NotFound("CreateBranch", nil)
+               return
+       }
+
+       if ctx.HasError() {
+               ctx.Flash.Error(ctx.GetErrMsg())
+               ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
+               return
+       }
+
+       var err error
+
+       if form.CreateTag {
+               if ctx.Repo.IsViewTag {
+                       err = release_service.CreateNewTag(ctx.User, ctx.Repo.Repository, ctx.Repo.CommitID, form.NewBranchName, "")
+               } else {
+                       err = release_service.CreateNewTag(ctx.User, ctx.Repo.Repository, ctx.Repo.BranchName, form.NewBranchName, "")
+               }
+       } else if ctx.Repo.IsViewBranch {
+               err = repo_module.CreateNewBranch(ctx.User, ctx.Repo.Repository, ctx.Repo.BranchName, form.NewBranchName)
+       } else if ctx.Repo.IsViewTag {
+               err = repo_module.CreateNewBranchFromCommit(ctx.User, ctx.Repo.Repository, ctx.Repo.CommitID, form.NewBranchName)
+       } else {
+               err = repo_module.CreateNewBranchFromCommit(ctx.User, ctx.Repo.Repository, ctx.Repo.BranchName, form.NewBranchName)
+       }
+       if err != nil {
+               if models.IsErrTagAlreadyExists(err) {
+                       e := err.(models.ErrTagAlreadyExists)
+                       ctx.Flash.Error(ctx.Tr("repo.branch.tag_collision", e.TagName))
+                       ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
+                       return
+               }
+               if models.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) {
+                       ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.NewBranchName))
+                       ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
+                       return
+               }
+               if models.IsErrBranchNameConflict(err) {
+                       e := err.(models.ErrBranchNameConflict)
+                       ctx.Flash.Error(ctx.Tr("repo.branch.branch_name_conflict", form.NewBranchName, e.BranchName))
+                       ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
+                       return
+               }
+               if git.IsErrPushRejected(err) {
+                       e := err.(*git.ErrPushRejected)
+                       if len(e.Message) == 0 {
+                               ctx.Flash.Error(ctx.Tr("repo.editor.push_rejected_no_message"))
+                       } else {
+                               flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
+                                       "Message": ctx.Tr("repo.editor.push_rejected"),
+                                       "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
+                                       "Details": utils.SanitizeFlashErrorString(e.Message),
+                               })
+                               if err != nil {
+                                       ctx.ServerError("UpdatePullRequest.HTMLString", err)
+                                       return
+                               }
+                               ctx.Flash.Error(flashError)
+                       }
+                       ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
+                       return
+               }
+
+               ctx.ServerError("CreateNewBranch", err)
+               return
+       }
+
+       if form.CreateTag {
+               ctx.Flash.Success(ctx.Tr("repo.tag.create_success", form.NewBranchName))
+               ctx.Redirect(ctx.Repo.RepoLink + "/src/tag/" + util.PathEscapeSegments(form.NewBranchName))
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.branch.create_success", form.NewBranchName))
+       ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(form.NewBranchName))
+}
diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go
new file mode 100644 (file)
index 0000000..3e6148b
--- /dev/null
@@ -0,0 +1,401 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2019 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 repo
+
+import (
+       "errors"
+       "net/http"
+       "path"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/charset"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/gitgraph"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/services/gitdiff"
+)
+
+const (
+       tplCommits    base.TplName = "repo/commits"
+       tplGraph      base.TplName = "repo/graph"
+       tplGraphDiv   base.TplName = "repo/graph/div"
+       tplCommitPage base.TplName = "repo/commit_page"
+)
+
+// RefCommits render commits page
+func RefCommits(ctx *context.Context) {
+       switch {
+       case len(ctx.Repo.TreePath) == 0:
+               Commits(ctx)
+       case ctx.Repo.TreePath == "search":
+               SearchCommits(ctx)
+       default:
+               FileHistory(ctx)
+       }
+}
+
+// Commits render branch's commits
+func Commits(ctx *context.Context) {
+       ctx.Data["PageIsCommits"] = true
+       if ctx.Repo.Commit == nil {
+               ctx.NotFound("Commit not found", nil)
+               return
+       }
+       ctx.Data["PageIsViewCode"] = true
+
+       commitsCount, err := ctx.Repo.GetCommitsCount()
+       if err != nil {
+               ctx.ServerError("GetCommitsCount", err)
+               return
+       }
+
+       page := ctx.QueryInt("page")
+       if page <= 1 {
+               page = 1
+       }
+
+       pageSize := ctx.QueryInt("limit")
+       if pageSize <= 0 {
+               pageSize = git.CommitsRangeSize
+       }
+
+       // Both `git log branchName` and `git log commitId` work.
+       commits, err := ctx.Repo.Commit.CommitsByRange(page, pageSize)
+       if err != nil {
+               ctx.ServerError("CommitsByRange", err)
+               return
+       }
+       commits = models.ValidateCommitsWithEmails(commits)
+       commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
+       commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
+       ctx.Data["Commits"] = commits
+
+       ctx.Data["Username"] = ctx.Repo.Owner.Name
+       ctx.Data["Reponame"] = ctx.Repo.Repository.Name
+       ctx.Data["CommitCount"] = commitsCount
+       ctx.Data["Branch"] = ctx.Repo.BranchName
+
+       pager := context.NewPagination(int(commitsCount), git.CommitsRangeSize, page, 5)
+       pager.SetDefaultParams(ctx)
+       ctx.Data["Page"] = pager
+
+       ctx.HTML(http.StatusOK, tplCommits)
+}
+
+// Graph render commit graph - show commits from all branches.
+func Graph(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.commit_graph")
+       ctx.Data["PageIsCommits"] = true
+       ctx.Data["PageIsViewCode"] = true
+       mode := strings.ToLower(ctx.QueryTrim("mode"))
+       if mode != "monochrome" {
+               mode = "color"
+       }
+       ctx.Data["Mode"] = mode
+       hidePRRefs := ctx.QueryBool("hide-pr-refs")
+       ctx.Data["HidePRRefs"] = hidePRRefs
+       branches := ctx.QueryStrings("branch")
+       realBranches := make([]string, len(branches))
+       copy(realBranches, branches)
+       for i, branch := range realBranches {
+               if strings.HasPrefix(branch, "--") {
+                       realBranches[i] = "refs/heads/" + branch
+               }
+       }
+       ctx.Data["SelectedBranches"] = realBranches
+       files := ctx.QueryStrings("file")
+
+       commitsCount, err := ctx.Repo.GetCommitsCount()
+       if err != nil {
+               ctx.ServerError("GetCommitsCount", err)
+               return
+       }
+
+       graphCommitsCount, err := ctx.Repo.GetCommitGraphsCount(hidePRRefs, realBranches, files)
+       if err != nil {
+               log.Warn("GetCommitGraphsCount error for generate graph exclude prs: %t branches: %s in %-v, Will Ignore branches and try again. Underlying Error: %v", hidePRRefs, branches, ctx.Repo.Repository, err)
+               realBranches = []string{}
+               branches = []string{}
+               graphCommitsCount, err = ctx.Repo.GetCommitGraphsCount(hidePRRefs, realBranches, files)
+               if err != nil {
+                       ctx.ServerError("GetCommitGraphsCount", err)
+                       return
+               }
+       }
+
+       page := ctx.QueryInt("page")
+
+       graph, err := gitgraph.GetCommitGraph(ctx.Repo.GitRepo, page, 0, hidePRRefs, realBranches, files)
+       if err != nil {
+               ctx.ServerError("GetCommitGraph", err)
+               return
+       }
+
+       if err := graph.LoadAndProcessCommits(ctx.Repo.Repository, ctx.Repo.GitRepo); err != nil {
+               ctx.ServerError("LoadAndProcessCommits", err)
+               return
+       }
+
+       ctx.Data["Graph"] = graph
+
+       gitRefs, err := ctx.Repo.GitRepo.GetRefs()
+       if err != nil {
+               ctx.ServerError("GitRepo.GetRefs", err)
+               return
+       }
+
+       ctx.Data["AllRefs"] = gitRefs
+
+       ctx.Data["Username"] = ctx.Repo.Owner.Name
+       ctx.Data["Reponame"] = ctx.Repo.Repository.Name
+       ctx.Data["CommitCount"] = commitsCount
+       ctx.Data["Branch"] = ctx.Repo.BranchName
+       paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5)
+       paginator.AddParam(ctx, "mode", "Mode")
+       paginator.AddParam(ctx, "hide-pr-refs", "HidePRRefs")
+       for _, branch := range branches {
+               paginator.AddParamString("branch", branch)
+       }
+       for _, file := range files {
+               paginator.AddParamString("file", file)
+       }
+       ctx.Data["Page"] = paginator
+       if ctx.QueryBool("div-only") {
+               ctx.HTML(http.StatusOK, tplGraphDiv)
+               return
+       }
+
+       ctx.HTML(http.StatusOK, tplGraph)
+}
+
+// SearchCommits render commits filtered by keyword
+func SearchCommits(ctx *context.Context) {
+       ctx.Data["PageIsCommits"] = true
+       ctx.Data["PageIsViewCode"] = true
+
+       query := strings.Trim(ctx.Query("q"), " ")
+       if len(query) == 0 {
+               ctx.Redirect(ctx.Repo.RepoLink + "/commits/" + ctx.Repo.BranchNameSubURL())
+               return
+       }
+
+       all := ctx.QueryBool("all")
+       opts := git.NewSearchCommitsOptions(query, all)
+       commits, err := ctx.Repo.Commit.SearchCommits(opts)
+       if err != nil {
+               ctx.ServerError("SearchCommits", err)
+               return
+       }
+       commits = models.ValidateCommitsWithEmails(commits)
+       commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
+       commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
+       ctx.Data["Commits"] = commits
+
+       ctx.Data["Keyword"] = query
+       if all {
+               ctx.Data["All"] = "checked"
+       }
+       ctx.Data["Username"] = ctx.Repo.Owner.Name
+       ctx.Data["Reponame"] = ctx.Repo.Repository.Name
+       ctx.Data["CommitCount"] = commits.Len()
+       ctx.Data["Branch"] = ctx.Repo.BranchName
+       ctx.HTML(http.StatusOK, tplCommits)
+}
+
+// FileHistory show a file's reversions
+func FileHistory(ctx *context.Context) {
+       ctx.Data["IsRepoToolbarCommits"] = true
+
+       fileName := ctx.Repo.TreePath
+       if len(fileName) == 0 {
+               Commits(ctx)
+               return
+       }
+
+       branchName := ctx.Repo.BranchName
+       commitsCount, err := ctx.Repo.GitRepo.FileCommitsCount(branchName, fileName)
+       if err != nil {
+               ctx.ServerError("FileCommitsCount", err)
+               return
+       } else if commitsCount == 0 {
+               ctx.NotFound("FileCommitsCount", nil)
+               return
+       }
+
+       page := ctx.QueryInt("page")
+       if page <= 1 {
+               page = 1
+       }
+
+       commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange(branchName, fileName, page)
+       if err != nil {
+               ctx.ServerError("CommitsByFileAndRange", err)
+               return
+       }
+       commits = models.ValidateCommitsWithEmails(commits)
+       commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
+       commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
+       ctx.Data["Commits"] = commits
+
+       ctx.Data["Username"] = ctx.Repo.Owner.Name
+       ctx.Data["Reponame"] = ctx.Repo.Repository.Name
+       ctx.Data["FileName"] = fileName
+       ctx.Data["CommitCount"] = commitsCount
+       ctx.Data["Branch"] = branchName
+
+       pager := context.NewPagination(int(commitsCount), git.CommitsRangeSize, page, 5)
+       pager.SetDefaultParams(ctx)
+       ctx.Data["Page"] = pager
+
+       ctx.HTML(http.StatusOK, tplCommits)
+}
+
+// Diff show different from current commit to previous commit
+func Diff(ctx *context.Context) {
+       ctx.Data["PageIsDiff"] = true
+       ctx.Data["RequireHighlightJS"] = true
+       ctx.Data["RequireSimpleMDE"] = true
+       ctx.Data["RequireTribute"] = true
+
+       userName := ctx.Repo.Owner.Name
+       repoName := ctx.Repo.Repository.Name
+       commitID := ctx.Params(":sha")
+       var (
+               gitRepo  *git.Repository
+               err      error
+               repoPath string
+       )
+
+       if ctx.Data["PageIsWiki"] != nil {
+               gitRepo, err = git.OpenRepository(ctx.Repo.Repository.WikiPath())
+               if err != nil {
+                       ctx.ServerError("Repo.GitRepo.GetCommit", err)
+                       return
+               }
+               repoPath = ctx.Repo.Repository.WikiPath()
+       } else {
+               gitRepo = ctx.Repo.GitRepo
+               repoPath = models.RepoPath(userName, repoName)
+       }
+
+       commit, err := gitRepo.GetCommit(commitID)
+       if err != nil {
+               if git.IsErrNotExist(err) {
+                       ctx.NotFound("Repo.GitRepo.GetCommit", err)
+               } else {
+                       ctx.ServerError("Repo.GitRepo.GetCommit", err)
+               }
+               return
+       }
+       if len(commitID) != 40 {
+               commitID = commit.ID.String()
+       }
+
+       statuses, err := models.GetLatestCommitStatus(ctx.Repo.Repository.ID, commitID, models.ListOptions{})
+       if err != nil {
+               log.Error("GetLatestCommitStatus: %v", err)
+       }
+
+       ctx.Data["CommitStatus"] = models.CalcCommitStatus(statuses)
+       ctx.Data["CommitStatuses"] = statuses
+
+       diff, err := gitdiff.GetDiffCommitWithWhitespaceBehavior(repoPath,
+               commitID, setting.Git.MaxGitDiffLines,
+               setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles,
+               gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
+       if err != nil {
+               ctx.NotFound("GetDiffCommitWithWhitespaceBehavior", err)
+               return
+       }
+
+       parents := make([]string, commit.ParentCount())
+       for i := 0; i < commit.ParentCount(); i++ {
+               sha, err := commit.ParentID(i)
+               if err != nil {
+                       ctx.NotFound("repo.Diff", err)
+                       return
+               }
+               parents[i] = sha.String()
+       }
+
+       ctx.Data["CommitID"] = commitID
+       ctx.Data["AfterCommitID"] = commitID
+       ctx.Data["Username"] = userName
+       ctx.Data["Reponame"] = repoName
+
+       var parentCommit *git.Commit
+       if commit.ParentCount() > 0 {
+               parentCommit, err = gitRepo.GetCommit(parents[0])
+               if err != nil {
+                       ctx.NotFound("GetParentCommit", err)
+                       return
+               }
+       }
+       headTarget := path.Join(userName, repoName)
+       setCompareContext(ctx, parentCommit, commit, headTarget)
+       ctx.Data["Title"] = commit.Summary() + " Â· " + base.ShortSha(commitID)
+       ctx.Data["Commit"] = commit
+       verification := models.ParseCommitWithSignature(commit)
+       ctx.Data["Verification"] = verification
+       ctx.Data["Author"] = models.ValidateCommitWithEmail(commit)
+       ctx.Data["Diff"] = diff
+       ctx.Data["Parents"] = parents
+       ctx.Data["DiffNotAvailable"] = diff.NumFiles == 0
+
+       if err := models.CalculateTrustStatus(verification, ctx.Repo.Repository, nil); err != nil {
+               ctx.ServerError("CalculateTrustStatus", err)
+               return
+       }
+
+       note := &git.Note{}
+       err = git.GetNote(ctx, ctx.Repo.GitRepo, commitID, note)
+       if err == nil {
+               ctx.Data["Note"] = string(charset.ToUTF8WithFallback(note.Message))
+               ctx.Data["NoteCommit"] = note.Commit
+               ctx.Data["NoteAuthor"] = models.ValidateCommitWithEmail(note.Commit)
+       }
+
+       ctx.Data["BranchName"], err = commit.GetBranchName()
+       if err != nil {
+               ctx.ServerError("commit.GetBranchName", err)
+               return
+       }
+
+       ctx.Data["TagName"], err = commit.GetTagName()
+       if err != nil {
+               ctx.ServerError("commit.GetTagName", err)
+               return
+       }
+       ctx.HTML(http.StatusOK, tplCommitPage)
+}
+
+// RawDiff dumps diff results of repository in given commit ID to io.Writer
+func RawDiff(ctx *context.Context) {
+       var repoPath string
+       if ctx.Data["PageIsWiki"] != nil {
+               repoPath = ctx.Repo.Repository.WikiPath()
+       } else {
+               repoPath = models.RepoPath(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
+       }
+       if err := git.GetRawDiff(
+               repoPath,
+               ctx.Params(":sha"),
+               git.RawDiffType(ctx.Params(":ext")),
+               ctx.Resp,
+       ); err != nil {
+               if git.IsErrNotExist(err) {
+                       ctx.NotFound("GetRawDiff",
+                               errors.New("commit "+ctx.Params(":sha")+" does not exist."))
+                       return
+               }
+               ctx.ServerError("GetRawDiff", err)
+               return
+       }
+}
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
new file mode 100644 (file)
index 0000000..f53a317
--- /dev/null
@@ -0,0 +1,787 @@
+// Copyright 2019 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 repo
+
+import (
+       "bufio"
+       "encoding/csv"
+       "errors"
+       "fmt"
+       "html"
+       "net/http"
+       "path"
+       "path/filepath"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/charset"
+       "code.gitea.io/gitea/modules/context"
+       csv_module "code.gitea.io/gitea/modules/csv"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/upload"
+       "code.gitea.io/gitea/services/gitdiff"
+)
+
+const (
+       tplCompare     base.TplName = "repo/diff/compare"
+       tplBlobExcerpt base.TplName = "repo/diff/blob_excerpt"
+)
+
+// setCompareContext sets context data.
+func setCompareContext(ctx *context.Context, base *git.Commit, head *git.Commit, headTarget string) {
+       ctx.Data["BaseCommit"] = base
+       ctx.Data["HeadCommit"] = head
+
+       ctx.Data["GetBlobByPathForCommit"] = func(commit *git.Commit, path string) *git.Blob {
+               if commit == nil {
+                       return nil
+               }
+
+               blob, err := commit.GetBlobByPath(path)
+               if err != nil {
+                       return nil
+               }
+               return blob
+       }
+
+       setPathsCompareContext(ctx, base, head, headTarget)
+       setImageCompareContext(ctx)
+       setCsvCompareContext(ctx)
+}
+
+// setPathsCompareContext sets context data for source and raw paths
+func setPathsCompareContext(ctx *context.Context, base *git.Commit, head *git.Commit, headTarget string) {
+       sourcePath := setting.AppSubURL + "/%s/src/commit/%s"
+       rawPath := setting.AppSubURL + "/%s/raw/commit/%s"
+
+       ctx.Data["SourcePath"] = fmt.Sprintf(sourcePath, headTarget, head.ID)
+       ctx.Data["RawPath"] = fmt.Sprintf(rawPath, headTarget, head.ID)
+       if base != nil {
+               baseTarget := path.Join(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
+               ctx.Data["BeforeSourcePath"] = fmt.Sprintf(sourcePath, baseTarget, base.ID)
+               ctx.Data["BeforeRawPath"] = fmt.Sprintf(rawPath, baseTarget, base.ID)
+       }
+}
+
+// setImageCompareContext sets context data that is required by image compare template
+func setImageCompareContext(ctx *context.Context) {
+       ctx.Data["IsBlobAnImage"] = func(blob *git.Blob) bool {
+               if blob == nil {
+                       return false
+               }
+
+               st, err := blob.GuessContentType()
+               if err != nil {
+                       log.Error("GuessContentType failed: %v", err)
+                       return false
+               }
+               return st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage())
+       }
+}
+
+// setCsvCompareContext sets context data that is required by the CSV compare template
+func setCsvCompareContext(ctx *context.Context) {
+       ctx.Data["IsCsvFile"] = func(diffFile *gitdiff.DiffFile) bool {
+               extension := strings.ToLower(filepath.Ext(diffFile.Name))
+               return extension == ".csv" || extension == ".tsv"
+       }
+
+       type CsvDiffResult struct {
+               Sections []*gitdiff.TableDiffSection
+               Error    string
+       }
+
+       ctx.Data["CreateCsvDiff"] = func(diffFile *gitdiff.DiffFile, baseCommit *git.Commit, headCommit *git.Commit) CsvDiffResult {
+               if diffFile == nil || baseCommit == nil || headCommit == nil {
+                       return CsvDiffResult{nil, ""}
+               }
+
+               errTooLarge := errors.New(ctx.Locale.Tr("repo.error.csv.too_large"))
+
+               csvReaderFromCommit := func(c *git.Commit) (*csv.Reader, error) {
+                       blob, err := c.GetBlobByPath(diffFile.Name)
+                       if err != nil {
+                               return nil, err
+                       }
+
+                       if setting.UI.CSV.MaxFileSize != 0 && setting.UI.CSV.MaxFileSize < blob.Size() {
+                               return nil, errTooLarge
+                       }
+
+                       reader, err := blob.DataAsync()
+                       if err != nil {
+                               return nil, err
+                       }
+                       defer reader.Close()
+
+                       return csv_module.CreateReaderAndGuessDelimiter(charset.ToUTF8WithFallbackReader(reader))
+               }
+
+               baseReader, err := csvReaderFromCommit(baseCommit)
+               if err == errTooLarge {
+                       return CsvDiffResult{nil, err.Error()}
+               }
+               headReader, err := csvReaderFromCommit(headCommit)
+               if err == errTooLarge {
+                       return CsvDiffResult{nil, err.Error()}
+               }
+
+               sections, err := gitdiff.CreateCsvDiff(diffFile, baseReader, headReader)
+               if err != nil {
+                       errMessage, err := csv_module.FormatError(err, ctx.Locale)
+                       if err != nil {
+                               log.Error("RenderCsvDiff failed: %v", err)
+                               return CsvDiffResult{nil, ""}
+                       }
+                       return CsvDiffResult{nil, errMessage}
+               }
+               return CsvDiffResult{sections, ""}
+       }
+}
+
+// ParseCompareInfo parse compare info between two commit for preparing comparing references
+func ParseCompareInfo(ctx *context.Context) (*models.User, *models.Repository, *git.Repository, *git.CompareInfo, string, string) {
+       baseRepo := ctx.Repo.Repository
+
+       // Get compared branches information
+       // A full compare url is of the form:
+       //
+       // 1. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headBranch}
+       // 2. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}:{:headBranch}
+       // 3. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}/{:headRepoName}:{:headBranch}
+       //
+       // Here we obtain the infoPath "{:baseBranch}...[{:headOwner}/{:headRepoName}:]{:headBranch}" as ctx.Params("*")
+       // with the :baseRepo in ctx.Repo.
+       //
+       // Note: Generally :headRepoName is not provided here - we are only passed :headOwner.
+       //
+       // How do we determine the :headRepo?
+       //
+       // 1. If :headOwner is not set then the :headRepo = :baseRepo
+       // 2. If :headOwner is set - then look for the fork of :baseRepo owned by :headOwner
+       // 3. But... :baseRepo could be a fork of :headOwner's repo - so check that
+       // 4. Now, :baseRepo and :headRepos could be forks of the same repo - so check that
+       //
+       // format: <base branch>...[<head repo>:]<head branch>
+       // base<-head: master...head:feature
+       // same repo: master...feature
+
+       var (
+               headUser   *models.User
+               headRepo   *models.Repository
+               headBranch string
+               isSameRepo bool
+               infoPath   string
+               err        error
+       )
+       infoPath = ctx.Params("*")
+       infos := strings.SplitN(infoPath, "...", 2)
+       if len(infos) != 2 {
+               log.Trace("ParseCompareInfo[%d]: not enough compared branches information %s", baseRepo.ID, infos)
+               ctx.NotFound("CompareAndPullRequest", nil)
+               return nil, nil, nil, nil, "", ""
+       }
+
+       ctx.Data["BaseName"] = baseRepo.OwnerName
+       baseBranch := infos[0]
+       ctx.Data["BaseBranch"] = baseBranch
+
+       // If there is no head repository, it means compare between same repository.
+       headInfos := strings.Split(infos[1], ":")
+       if len(headInfos) == 1 {
+               isSameRepo = true
+               headUser = ctx.Repo.Owner
+               headBranch = headInfos[0]
+
+       } else if len(headInfos) == 2 {
+               headInfosSplit := strings.Split(headInfos[0], "/")
+               if len(headInfosSplit) == 1 {
+                       headUser, err = models.GetUserByName(headInfos[0])
+                       if err != nil {
+                               if models.IsErrUserNotExist(err) {
+                                       ctx.NotFound("GetUserByName", nil)
+                               } else {
+                                       ctx.ServerError("GetUserByName", err)
+                               }
+                               return nil, nil, nil, nil, "", ""
+                       }
+                       headBranch = headInfos[1]
+                       isSameRepo = headUser.ID == ctx.Repo.Owner.ID
+                       if isSameRepo {
+                               headRepo = baseRepo
+                       }
+               } else {
+                       headRepo, err = models.GetRepositoryByOwnerAndName(headInfosSplit[0], headInfosSplit[1])
+                       if err != nil {
+                               if models.IsErrRepoNotExist(err) {
+                                       ctx.NotFound("GetRepositoryByOwnerAndName", nil)
+                               } else {
+                                       ctx.ServerError("GetRepositoryByOwnerAndName", err)
+                               }
+                               return nil, nil, nil, nil, "", ""
+                       }
+                       if err := headRepo.GetOwner(); err != nil {
+                               if models.IsErrUserNotExist(err) {
+                                       ctx.NotFound("GetUserByName", nil)
+                               } else {
+                                       ctx.ServerError("GetUserByName", err)
+                               }
+                               return nil, nil, nil, nil, "", ""
+                       }
+                       headBranch = headInfos[1]
+                       headUser = headRepo.Owner
+                       isSameRepo = headRepo.ID == ctx.Repo.Repository.ID
+               }
+       } else {
+               ctx.NotFound("CompareAndPullRequest", nil)
+               return nil, nil, nil, nil, "", ""
+       }
+       ctx.Data["HeadUser"] = headUser
+       ctx.Data["HeadBranch"] = headBranch
+       ctx.Repo.PullRequest.SameRepo = isSameRepo
+
+       // Check if base branch is valid.
+       baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(baseBranch)
+       baseIsBranch := ctx.Repo.GitRepo.IsBranchExist(baseBranch)
+       baseIsTag := ctx.Repo.GitRepo.IsTagExist(baseBranch)
+       if !baseIsCommit && !baseIsBranch && !baseIsTag {
+               // Check if baseBranch is short sha commit hash
+               if baseCommit, _ := ctx.Repo.GitRepo.GetCommit(baseBranch); baseCommit != nil {
+                       baseBranch = baseCommit.ID.String()
+                       ctx.Data["BaseBranch"] = baseBranch
+                       baseIsCommit = true
+               } else {
+                       ctx.NotFound("IsRefExist", nil)
+                       return nil, nil, nil, nil, "", ""
+               }
+       }
+       ctx.Data["BaseIsCommit"] = baseIsCommit
+       ctx.Data["BaseIsBranch"] = baseIsBranch
+       ctx.Data["BaseIsTag"] = baseIsTag
+       ctx.Data["IsPull"] = true
+
+       // Now we have the repository that represents the base
+
+       // The current base and head repositories and branches may not
+       // actually be the intended branches that the user wants to
+       // create a pull-request from - but also determining the head
+       // repo is difficult.
+
+       // We will want therefore to offer a few repositories to set as
+       // our base and head
+
+       // 1. First if the baseRepo is a fork get the "RootRepo" it was
+       // forked from
+       var rootRepo *models.Repository
+       if baseRepo.IsFork {
+               err = baseRepo.GetBaseRepo()
+               if err != nil {
+                       if !models.IsErrRepoNotExist(err) {
+                               ctx.ServerError("Unable to find root repo", err)
+                               return nil, nil, nil, nil, "", ""
+                       }
+               } else {
+                       rootRepo = baseRepo.BaseRepo
+               }
+       }
+
+       // 2. Now if the current user is not the owner of the baseRepo,
+       // check if they have a fork of the base repo and offer that as
+       // "OwnForkRepo"
+       var ownForkRepo *models.Repository
+       if ctx.User != nil && baseRepo.OwnerID != ctx.User.ID {
+               repo, has := models.HasForkedRepo(ctx.User.ID, baseRepo.ID)
+               if has {
+                       ownForkRepo = repo
+                       ctx.Data["OwnForkRepo"] = ownForkRepo
+               }
+       }
+
+       has := headRepo != nil
+       // 3. If the base is a forked from "RootRepo" and the owner of
+       // the "RootRepo" is the :headUser - set headRepo to that
+       if !has && rootRepo != nil && rootRepo.OwnerID == headUser.ID {
+               headRepo = rootRepo
+               has = true
+       }
+
+       // 4. If the ctx.User has their own fork of the baseRepo and the headUser is the ctx.User
+       // set the headRepo to the ownFork
+       if !has && ownForkRepo != nil && ownForkRepo.OwnerID == headUser.ID {
+               headRepo = ownForkRepo
+               has = true
+       }
+
+       // 5. If the headOwner has a fork of the baseRepo - use that
+       if !has {
+               headRepo, has = models.HasForkedRepo(headUser.ID, baseRepo.ID)
+       }
+
+       // 6. If the baseRepo is a fork and the headUser has a fork of that use that
+       if !has && baseRepo.IsFork {
+               headRepo, has = models.HasForkedRepo(headUser.ID, baseRepo.ForkID)
+       }
+
+       // 7. Otherwise if we're not the same repo and haven't found a repo give up
+       if !isSameRepo && !has {
+               ctx.Data["PageIsComparePull"] = false
+       }
+
+       // 8. Finally open the git repo
+       var headGitRepo *git.Repository
+       if isSameRepo {
+               headRepo = ctx.Repo.Repository
+               headGitRepo = ctx.Repo.GitRepo
+       } else if has {
+               headGitRepo, err = git.OpenRepository(headRepo.RepoPath())
+               if err != nil {
+                       ctx.ServerError("OpenRepository", err)
+                       return nil, nil, nil, nil, "", ""
+               }
+               defer headGitRepo.Close()
+       }
+
+       ctx.Data["HeadRepo"] = headRepo
+
+       // Now we need to assert that the ctx.User has permission to read
+       // the baseRepo's code and pulls
+       // (NOT headRepo's)
+       permBase, err := models.GetUserRepoPermission(baseRepo, ctx.User)
+       if err != nil {
+               ctx.ServerError("GetUserRepoPermission", err)
+               return nil, nil, nil, nil, "", ""
+       }
+       if !permBase.CanRead(models.UnitTypeCode) {
+               if log.IsTrace() {
+                       log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in baseRepo has Permissions: %-+v",
+                               ctx.User,
+                               baseRepo,
+                               permBase)
+               }
+               ctx.NotFound("ParseCompareInfo", nil)
+               return nil, nil, nil, nil, "", ""
+       }
+
+       // If we're not merging from the same repo:
+       if !isSameRepo {
+               // Assert ctx.User has permission to read headRepo's codes
+               permHead, err := models.GetUserRepoPermission(headRepo, ctx.User)
+               if err != nil {
+                       ctx.ServerError("GetUserRepoPermission", err)
+                       return nil, nil, nil, nil, "", ""
+               }
+               if !permHead.CanRead(models.UnitTypeCode) {
+                       if log.IsTrace() {
+                               log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v",
+                                       ctx.User,
+                                       headRepo,
+                                       permHead)
+                       }
+                       ctx.NotFound("ParseCompareInfo", nil)
+                       return nil, nil, nil, nil, "", ""
+               }
+       }
+
+       // If we have a rootRepo and it's different from:
+       // 1. the computed base
+       // 2. the computed head
+       // then get the branches of it
+       if rootRepo != nil &&
+               rootRepo.ID != headRepo.ID &&
+               rootRepo.ID != baseRepo.ID {
+               perm, branches, tags, err := getBranchesAndTagsForRepo(ctx.User, rootRepo)
+               if err != nil {
+                       ctx.ServerError("GetBranchesForRepo", err)
+                       return nil, nil, nil, nil, "", ""
+               }
+               if perm {
+                       ctx.Data["RootRepo"] = rootRepo
+                       ctx.Data["RootRepoBranches"] = branches
+                       ctx.Data["RootRepoTags"] = tags
+               }
+       }
+
+       // If we have a ownForkRepo and it's different from:
+       // 1. The computed base
+       // 2. The computed head
+       // 3. The rootRepo (if we have one)
+       // then get the branches from it.
+       if ownForkRepo != nil &&
+               ownForkRepo.ID != headRepo.ID &&
+               ownForkRepo.ID != baseRepo.ID &&
+               (rootRepo == nil || ownForkRepo.ID != rootRepo.ID) {
+               perm, branches, tags, err := getBranchesAndTagsForRepo(ctx.User, ownForkRepo)
+               if err != nil {
+                       ctx.ServerError("GetBranchesForRepo", err)
+                       return nil, nil, nil, nil, "", ""
+               }
+               if perm {
+                       ctx.Data["OwnForkRepo"] = ownForkRepo
+                       ctx.Data["OwnForkRepoBranches"] = branches
+                       ctx.Data["OwnForkRepoTags"] = tags
+               }
+       }
+
+       // Check if head branch is valid.
+       headIsCommit := headGitRepo.IsCommitExist(headBranch)
+       headIsBranch := headGitRepo.IsBranchExist(headBranch)
+       headIsTag := headGitRepo.IsTagExist(headBranch)
+       if !headIsCommit && !headIsBranch && !headIsTag {
+               // Check if headBranch is short sha commit hash
+               if headCommit, _ := headGitRepo.GetCommit(headBranch); headCommit != nil {
+                       headBranch = headCommit.ID.String()
+                       ctx.Data["HeadBranch"] = headBranch
+                       headIsCommit = true
+               } else {
+                       ctx.NotFound("IsRefExist", nil)
+                       return nil, nil, nil, nil, "", ""
+               }
+       }
+       ctx.Data["HeadIsCommit"] = headIsCommit
+       ctx.Data["HeadIsBranch"] = headIsBranch
+       ctx.Data["HeadIsTag"] = headIsTag
+
+       // Treat as pull request if both references are branches
+       if ctx.Data["PageIsComparePull"] == nil {
+               ctx.Data["PageIsComparePull"] = headIsBranch && baseIsBranch
+       }
+
+       if ctx.Data["PageIsComparePull"] == true && !permBase.CanReadIssuesOrPulls(true) {
+               if log.IsTrace() {
+                       log.Trace("Permission Denied: User: %-v cannot create/read pull requests in Repo: %-v\nUser in baseRepo has Permissions: %-+v",
+                               ctx.User,
+                               baseRepo,
+                               permBase)
+               }
+               ctx.NotFound("ParseCompareInfo", nil)
+               return nil, nil, nil, nil, "", ""
+       }
+
+       baseBranchRef := baseBranch
+       if baseIsBranch {
+               baseBranchRef = git.BranchPrefix + baseBranch
+       } else if baseIsTag {
+               baseBranchRef = git.TagPrefix + baseBranch
+       }
+       headBranchRef := headBranch
+       if headIsBranch {
+               headBranchRef = git.BranchPrefix + headBranch
+       } else if headIsTag {
+               headBranchRef = git.TagPrefix + headBranch
+       }
+
+       compareInfo, err := headGitRepo.GetCompareInfo(baseRepo.RepoPath(), baseBranchRef, headBranchRef)
+       if err != nil {
+               ctx.ServerError("GetCompareInfo", err)
+               return nil, nil, nil, nil, "", ""
+       }
+       ctx.Data["BeforeCommitID"] = compareInfo.MergeBase
+
+       return headUser, headRepo, headGitRepo, compareInfo, baseBranch, headBranch
+}
+
+// PrepareCompareDiff renders compare diff page
+func PrepareCompareDiff(
+       ctx *context.Context,
+       headUser *models.User,
+       headRepo *models.Repository,
+       headGitRepo *git.Repository,
+       compareInfo *git.CompareInfo,
+       baseBranch, headBranch string,
+       whitespaceBehavior string) bool {
+
+       var (
+               repo  = ctx.Repo.Repository
+               err   error
+               title string
+       )
+
+       // Get diff information.
+       ctx.Data["CommitRepoLink"] = headRepo.Link()
+
+       headCommitID := compareInfo.HeadCommitID
+
+       ctx.Data["AfterCommitID"] = headCommitID
+
+       if headCommitID == compareInfo.MergeBase {
+               ctx.Data["IsNothingToCompare"] = true
+               if unit, err := repo.GetUnit(models.UnitTypePullRequests); err == nil {
+                       config := unit.PullRequestsConfig()
+
+                       if !config.AutodetectManualMerge {
+                               allowEmptyPr := !(baseBranch == headBranch && ctx.Repo.Repository.Name == headRepo.Name)
+                               ctx.Data["AllowEmptyPr"] = allowEmptyPr
+
+                               return !allowEmptyPr
+                       }
+
+                       ctx.Data["AllowEmptyPr"] = false
+               }
+               return true
+       }
+
+       diff, err := gitdiff.GetDiffRangeWithWhitespaceBehavior(models.RepoPath(headUser.Name, headRepo.Name),
+               compareInfo.MergeBase, headCommitID, setting.Git.MaxGitDiffLines,
+               setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, whitespaceBehavior)
+       if err != nil {
+               ctx.ServerError("GetDiffRangeWithWhitespaceBehavior", err)
+               return false
+       }
+       ctx.Data["Diff"] = diff
+       ctx.Data["DiffNotAvailable"] = diff.NumFiles == 0
+
+       headCommit, err := headGitRepo.GetCommit(headCommitID)
+       if err != nil {
+               ctx.ServerError("GetCommit", err)
+               return false
+       }
+
+       baseGitRepo := ctx.Repo.GitRepo
+       baseCommitID := compareInfo.BaseCommitID
+
+       baseCommit, err := baseGitRepo.GetCommit(baseCommitID)
+       if err != nil {
+               ctx.ServerError("GetCommit", err)
+               return false
+       }
+
+       compareInfo.Commits = models.ValidateCommitsWithEmails(compareInfo.Commits)
+       compareInfo.Commits = models.ParseCommitsWithSignature(compareInfo.Commits, headRepo)
+       compareInfo.Commits = models.ParseCommitsWithStatus(compareInfo.Commits, headRepo)
+       ctx.Data["Commits"] = compareInfo.Commits
+       ctx.Data["CommitCount"] = compareInfo.Commits.Len()
+
+       if compareInfo.Commits.Len() == 1 {
+               c := compareInfo.Commits.Front().Value.(models.SignCommitWithStatuses)
+               title = strings.TrimSpace(c.UserCommit.Summary())
+
+               body := strings.Split(strings.TrimSpace(c.UserCommit.Message()), "\n")
+               if len(body) > 1 {
+                       ctx.Data["content"] = strings.Join(body[1:], "\n")
+               }
+       } else {
+               title = headBranch
+       }
+       ctx.Data["title"] = title
+       ctx.Data["Username"] = headUser.Name
+       ctx.Data["Reponame"] = headRepo.Name
+
+       headTarget := path.Join(headUser.Name, repo.Name)
+       setCompareContext(ctx, baseCommit, headCommit, headTarget)
+
+       return false
+}
+
+func getBranchesAndTagsForRepo(user *models.User, repo *models.Repository) (bool, []string, []string, error) {
+       perm, err := models.GetUserRepoPermission(repo, user)
+       if err != nil {
+               return false, nil, nil, err
+       }
+       if !perm.CanRead(models.UnitTypeCode) {
+               return false, nil, nil, nil
+       }
+       gitRepo, err := git.OpenRepository(repo.RepoPath())
+       if err != nil {
+               return false, nil, nil, err
+       }
+       defer gitRepo.Close()
+
+       branches, _, err := gitRepo.GetBranches(0, 0)
+       if err != nil {
+               return false, nil, nil, err
+       }
+       tags, err := gitRepo.GetTags()
+       if err != nil {
+               return false, nil, nil, err
+       }
+       return true, branches, tags, nil
+}
+
+// CompareDiff show different from one commit to another commit
+func CompareDiff(ctx *context.Context) {
+       headUser, headRepo, headGitRepo, compareInfo, baseBranch, headBranch := ParseCompareInfo(ctx)
+
+       if ctx.Written() {
+               return
+       }
+       defer headGitRepo.Close()
+
+       nothingToCompare := PrepareCompareDiff(ctx, headUser, headRepo, headGitRepo, compareInfo, baseBranch, headBranch,
+               gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
+       if ctx.Written() {
+               return
+       }
+
+       baseGitRepo := ctx.Repo.GitRepo
+       baseTags, err := baseGitRepo.GetTags()
+       if err != nil {
+               ctx.ServerError("GetTags", err)
+               return
+       }
+       ctx.Data["Tags"] = baseTags
+
+       headBranches, _, err := headGitRepo.GetBranches(0, 0)
+       if err != nil {
+               ctx.ServerError("GetBranches", err)
+               return
+       }
+       ctx.Data["HeadBranches"] = headBranches
+
+       headTags, err := headGitRepo.GetTags()
+       if err != nil {
+               ctx.ServerError("GetTags", err)
+               return
+       }
+       ctx.Data["HeadTags"] = headTags
+
+       if ctx.Data["PageIsComparePull"] == true {
+               pr, err := models.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch)
+               if err != nil {
+                       if !models.IsErrPullRequestNotExist(err) {
+                               ctx.ServerError("GetUnmergedPullRequest", err)
+                               return
+                       }
+               } else {
+                       ctx.Data["HasPullRequest"] = true
+                       ctx.Data["PullRequest"] = pr
+                       ctx.HTML(http.StatusOK, tplCompareDiff)
+                       return
+               }
+
+               if !nothingToCompare {
+                       // Setup information for new form.
+                       RetrieveRepoMetas(ctx, ctx.Repo.Repository, true)
+                       if ctx.Written() {
+                               return
+                       }
+               }
+       }
+       beforeCommitID := ctx.Data["BeforeCommitID"].(string)
+       afterCommitID := ctx.Data["AfterCommitID"].(string)
+
+       ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + "..." + base.ShortSha(afterCommitID)
+
+       ctx.Data["IsRepoToolbarCommits"] = true
+       ctx.Data["IsDiffCompare"] = true
+       ctx.Data["RequireTribute"] = true
+       ctx.Data["RequireSimpleMDE"] = true
+       ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
+       setTemplateIfExists(ctx, pullRequestTemplateKey, nil, pullRequestTemplateCandidates)
+       ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
+       upload.AddUploadContext(ctx, "comment")
+
+       ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(models.UnitTypePullRequests)
+
+       ctx.HTML(http.StatusOK, tplCompare)
+}
+
+// ExcerptBlob render blob excerpt contents
+func ExcerptBlob(ctx *context.Context) {
+       commitID := ctx.Params("sha")
+       lastLeft := ctx.QueryInt("last_left")
+       lastRight := ctx.QueryInt("last_right")
+       idxLeft := ctx.QueryInt("left")
+       idxRight := ctx.QueryInt("right")
+       leftHunkSize := ctx.QueryInt("left_hunk_size")
+       rightHunkSize := ctx.QueryInt("right_hunk_size")
+       anchor := ctx.Query("anchor")
+       direction := ctx.Query("direction")
+       filePath := ctx.Query("path")
+       gitRepo := ctx.Repo.GitRepo
+       chunkSize := gitdiff.BlobExcerptChunkSize
+       commit, err := gitRepo.GetCommit(commitID)
+       if err != nil {
+               ctx.Error(http.StatusInternalServerError, "GetCommit")
+               return
+       }
+       section := &gitdiff.DiffSection{
+               FileName: filePath,
+               Name:     filePath,
+       }
+       if direction == "up" && (idxLeft-lastLeft) > chunkSize {
+               idxLeft -= chunkSize
+               idxRight -= chunkSize
+               leftHunkSize += chunkSize
+               rightHunkSize += chunkSize
+               section.Lines, err = getExcerptLines(commit, filePath, idxLeft-1, idxRight-1, chunkSize)
+       } else if direction == "down" && (idxLeft-lastLeft) > chunkSize {
+               section.Lines, err = getExcerptLines(commit, filePath, lastLeft, lastRight, chunkSize)
+               lastLeft += chunkSize
+               lastRight += chunkSize
+       } else {
+               section.Lines, err = getExcerptLines(commit, filePath, lastLeft, lastRight, idxRight-lastRight-1)
+               leftHunkSize = 0
+               rightHunkSize = 0
+               idxLeft = lastLeft
+               idxRight = lastRight
+       }
+       if err != nil {
+               ctx.Error(http.StatusInternalServerError, "getExcerptLines")
+               return
+       }
+       if idxRight > lastRight {
+               lineText := " "
+               if rightHunkSize > 0 || leftHunkSize > 0 {
+                       lineText = fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", idxLeft, leftHunkSize, idxRight, rightHunkSize)
+               }
+               lineText = html.EscapeString(lineText)
+               lineSection := &gitdiff.DiffLine{
+                       Type:    gitdiff.DiffLineSection,
+                       Content: lineText,
+                       SectionInfo: &gitdiff.DiffLineSectionInfo{
+                               Path:          filePath,
+                               LastLeftIdx:   lastLeft,
+                               LastRightIdx:  lastRight,
+                               LeftIdx:       idxLeft,
+                               RightIdx:      idxRight,
+                               LeftHunkSize:  leftHunkSize,
+                               RightHunkSize: rightHunkSize,
+                       }}
+               if direction == "up" {
+                       section.Lines = append([]*gitdiff.DiffLine{lineSection}, section.Lines...)
+               } else if direction == "down" {
+                       section.Lines = append(section.Lines, lineSection)
+               }
+       }
+       ctx.Data["section"] = section
+       ctx.Data["fileName"] = filePath
+       ctx.Data["AfterCommitID"] = commitID
+       ctx.Data["Anchor"] = anchor
+       ctx.HTML(http.StatusOK, tplBlobExcerpt)
+}
+
+func getExcerptLines(commit *git.Commit, filePath string, idxLeft int, idxRight int, chunkSize int) ([]*gitdiff.DiffLine, error) {
+       blob, err := commit.Tree.GetBlobByPath(filePath)
+       if err != nil {
+               return nil, err
+       }
+       reader, err := blob.DataAsync()
+       if err != nil {
+               return nil, err
+       }
+       defer reader.Close()
+       scanner := bufio.NewScanner(reader)
+       var diffLines []*gitdiff.DiffLine
+       for line := 0; line < idxRight+chunkSize; line++ {
+               if ok := scanner.Scan(); !ok {
+                       break
+               }
+               if line < idxRight {
+                       continue
+               }
+               lineText := scanner.Text()
+               diffLine := &gitdiff.DiffLine{
+                       LeftIdx:  idxLeft + (line - idxRight) + 1,
+                       RightIdx: line + 1,
+                       Type:     gitdiff.DiffLinePlain,
+                       Content:  " " + lineText,
+               }
+               diffLines = append(diffLines, diffLine)
+       }
+       return diffLines, nil
+}
diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go
new file mode 100644 (file)
index 0000000..6f43d4b
--- /dev/null
@@ -0,0 +1,131 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2018 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 repo
+
+import (
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/httpcache"
+       "code.gitea.io/gitea/modules/lfs"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/routers/common"
+)
+
+// ServeBlobOrLFS download a git.Blob redirecting to LFS if necessary
+func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob) error {
+       if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`) {
+               return nil
+       }
+
+       dataRc, err := blob.DataAsync()
+       if err != nil {
+               return err
+       }
+       closed := false
+       defer func() {
+               if closed {
+                       return
+               }
+               if err = dataRc.Close(); err != nil {
+                       log.Error("ServeBlobOrLFS: Close: %v", err)
+               }
+       }()
+
+       pointer, _ := lfs.ReadPointer(dataRc)
+       if pointer.IsValid() {
+               meta, _ := ctx.Repo.Repository.GetLFSMetaObjectByOid(pointer.Oid)
+               if meta == nil {
+                       if err = dataRc.Close(); err != nil {
+                               log.Error("ServeBlobOrLFS: Close: %v", err)
+                       }
+                       closed = true
+                       return common.ServeBlob(ctx, blob)
+               }
+               if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`) {
+                       return nil
+               }
+               lfsDataRc, err := lfs.ReadMetaObject(meta.Pointer)
+               if err != nil {
+                       return err
+               }
+               defer func() {
+                       if err = lfsDataRc.Close(); err != nil {
+                               log.Error("ServeBlobOrLFS: Close: %v", err)
+                       }
+               }()
+               return common.ServeData(ctx, ctx.Repo.TreePath, meta.Size, lfsDataRc)
+       }
+       if err = dataRc.Close(); err != nil {
+               log.Error("ServeBlobOrLFS: Close: %v", err)
+       }
+       closed = true
+
+       return common.ServeBlob(ctx, blob)
+}
+
+// SingleDownload download a file by repos path
+func SingleDownload(ctx *context.Context) {
+       blob, err := ctx.Repo.Commit.GetBlobByPath(ctx.Repo.TreePath)
+       if err != nil {
+               if git.IsErrNotExist(err) {
+                       ctx.NotFound("GetBlobByPath", nil)
+               } else {
+                       ctx.ServerError("GetBlobByPath", err)
+               }
+               return
+       }
+       if err = common.ServeBlob(ctx, blob); err != nil {
+               ctx.ServerError("ServeBlob", err)
+       }
+}
+
+// SingleDownloadOrLFS download a file by repos path redirecting to LFS if necessary
+func SingleDownloadOrLFS(ctx *context.Context) {
+       blob, err := ctx.Repo.Commit.GetBlobByPath(ctx.Repo.TreePath)
+       if err != nil {
+               if git.IsErrNotExist(err) {
+                       ctx.NotFound("GetBlobByPath", nil)
+               } else {
+                       ctx.ServerError("GetBlobByPath", err)
+               }
+               return
+       }
+       if err = ServeBlobOrLFS(ctx, blob); err != nil {
+               ctx.ServerError("ServeBlobOrLFS", err)
+       }
+}
+
+// DownloadByID download a file by sha1 ID
+func DownloadByID(ctx *context.Context) {
+       blob, err := ctx.Repo.GitRepo.GetBlob(ctx.Params("sha"))
+       if err != nil {
+               if git.IsErrNotExist(err) {
+                       ctx.NotFound("GetBlob", nil)
+               } else {
+                       ctx.ServerError("GetBlob", err)
+               }
+               return
+       }
+       if err = common.ServeBlob(ctx, blob); err != nil {
+               ctx.ServerError("ServeBlob", err)
+       }
+}
+
+// DownloadByIDOrLFS download a file by sha1 ID taking account of LFS
+func DownloadByIDOrLFS(ctx *context.Context) {
+       blob, err := ctx.Repo.GitRepo.GetBlob(ctx.Params("sha"))
+       if err != nil {
+               if git.IsErrNotExist(err) {
+                       ctx.NotFound("GetBlob", nil)
+               } else {
+                       ctx.ServerError("GetBlob", err)
+               }
+               return
+       }
+       if err = ServeBlobOrLFS(ctx, blob); err != nil {
+               ctx.ServerError("ServeBlob", err)
+       }
+}
diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go
new file mode 100644 (file)
index 0000000..0f978c7
--- /dev/null
@@ -0,0 +1,831 @@
+// Copyright 2016 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 repo
+
+import (
+       "fmt"
+       "io/ioutil"
+       "net/http"
+       "path"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/charset"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/repofiles"
+       repo_module "code.gitea.io/gitea/modules/repository"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/typesniffer"
+       "code.gitea.io/gitea/modules/upload"
+       "code.gitea.io/gitea/modules/util"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/routers/utils"
+       "code.gitea.io/gitea/services/forms"
+       jsoniter "github.com/json-iterator/go"
+)
+
+const (
+       tplEditFile        base.TplName = "repo/editor/edit"
+       tplEditDiffPreview base.TplName = "repo/editor/diff_preview"
+       tplDeleteFile      base.TplName = "repo/editor/delete"
+       tplUploadFile      base.TplName = "repo/editor/upload"
+
+       frmCommitChoiceDirect    string = "direct"
+       frmCommitChoiceNewBranch string = "commit-to-new-branch"
+)
+
+func renderCommitRights(ctx *context.Context) bool {
+       canCommitToBranch, err := ctx.Repo.CanCommitToBranch(ctx.User)
+       if err != nil {
+               log.Error("CanCommitToBranch: %v", err)
+       }
+       ctx.Data["CanCommitToBranch"] = canCommitToBranch
+
+       return canCommitToBranch.CanCommitToBranch
+}
+
+// getParentTreeFields returns list of parent tree names and corresponding tree paths
+// based on given tree path.
+func getParentTreeFields(treePath string) (treeNames []string, treePaths []string) {
+       if len(treePath) == 0 {
+               return treeNames, treePaths
+       }
+
+       treeNames = strings.Split(treePath, "/")
+       treePaths = make([]string, len(treeNames))
+       for i := range treeNames {
+               treePaths[i] = strings.Join(treeNames[:i+1], "/")
+       }
+       return treeNames, treePaths
+}
+
+func editFile(ctx *context.Context, isNewFile bool) {
+       ctx.Data["PageIsEdit"] = true
+       ctx.Data["IsNewFile"] = isNewFile
+       ctx.Data["RequireHighlightJS"] = true
+       ctx.Data["RequireSimpleMDE"] = true
+       canCommit := renderCommitRights(ctx)
+
+       treePath := cleanUploadFileName(ctx.Repo.TreePath)
+       if treePath != ctx.Repo.TreePath {
+               if isNewFile {
+                       ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_new", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
+               } else {
+                       ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_edit", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
+               }
+               return
+       }
+
+       treeNames, treePaths := getParentTreeFields(ctx.Repo.TreePath)
+
+       if !isNewFile {
+               entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
+               if err != nil {
+                       ctx.NotFoundOrServerError("GetTreeEntryByPath", git.IsErrNotExist, err)
+                       return
+               }
+
+               // No way to edit a directory online.
+               if entry.IsDir() {
+                       ctx.NotFound("entry.IsDir", nil)
+                       return
+               }
+
+               blob := entry.Blob()
+               if blob.Size() >= setting.UI.MaxDisplayFileSize {
+                       ctx.NotFound("blob.Size", err)
+                       return
+               }
+
+               dataRc, err := blob.DataAsync()
+               if err != nil {
+                       ctx.NotFound("blob.Data", err)
+                       return
+               }
+
+               defer dataRc.Close()
+
+               ctx.Data["FileSize"] = blob.Size()
+               ctx.Data["FileName"] = blob.Name()
+
+               buf := make([]byte, 1024)
+               n, _ := dataRc.Read(buf)
+               buf = buf[:n]
+
+               // Only some file types are editable online as text.
+               if !typesniffer.DetectContentType(buf).IsRepresentableAsText() {
+                       ctx.NotFound("typesniffer.IsRepresentableAsText", nil)
+                       return
+               }
+
+               d, _ := ioutil.ReadAll(dataRc)
+               if err := dataRc.Close(); err != nil {
+                       log.Error("Error whilst closing blob data: %v", err)
+               }
+
+               buf = append(buf, d...)
+               if content, err := charset.ToUTF8WithErr(buf); err != nil {
+                       log.Error("ToUTF8WithErr: %v", err)
+                       ctx.Data["FileContent"] = string(buf)
+               } else {
+                       ctx.Data["FileContent"] = content
+               }
+       } else {
+               treeNames = append(treeNames, "") // Append empty string to allow user name the new file.
+       }
+
+       ctx.Data["TreeNames"] = treeNames
+       ctx.Data["TreePaths"] = treePaths
+       ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
+       ctx.Data["commit_summary"] = ""
+       ctx.Data["commit_message"] = ""
+       if canCommit {
+               ctx.Data["commit_choice"] = frmCommitChoiceDirect
+       } else {
+               ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
+       }
+       ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
+       ctx.Data["last_commit"] = ctx.Repo.CommitID
+       ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",")
+       ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
+       ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",")
+       ctx.Data["Editorconfig"] = GetEditorConfig(ctx, treePath)
+
+       ctx.HTML(http.StatusOK, tplEditFile)
+}
+
+// GetEditorConfig returns a editorconfig JSON string for given treePath or "null"
+func GetEditorConfig(ctx *context.Context, treePath string) string {
+       ec, err := ctx.Repo.GetEditorconfig()
+       if err == nil {
+               def, err := ec.GetDefinitionForFilename(treePath)
+               if err == nil {
+                       json := jsoniter.ConfigCompatibleWithStandardLibrary
+                       jsonStr, _ := json.Marshal(def)
+                       return string(jsonStr)
+               }
+       }
+       return "null"
+}
+
+// EditFile render edit file page
+func EditFile(ctx *context.Context) {
+       editFile(ctx, false)
+}
+
+// NewFile render create file page
+func NewFile(ctx *context.Context) {
+       editFile(ctx, true)
+}
+
+func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile bool) {
+       canCommit := renderCommitRights(ctx)
+       treeNames, treePaths := getParentTreeFields(form.TreePath)
+       branchName := ctx.Repo.BranchName
+       if form.CommitChoice == frmCommitChoiceNewBranch {
+               branchName = form.NewBranchName
+       }
+
+       ctx.Data["PageIsEdit"] = true
+       ctx.Data["PageHasPosted"] = true
+       ctx.Data["IsNewFile"] = isNewFile
+       ctx.Data["RequireHighlightJS"] = true
+       ctx.Data["RequireSimpleMDE"] = true
+       ctx.Data["TreePath"] = form.TreePath
+       ctx.Data["TreeNames"] = treeNames
+       ctx.Data["TreePaths"] = treePaths
+       ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + ctx.Repo.BranchName
+       ctx.Data["FileContent"] = form.Content
+       ctx.Data["commit_summary"] = form.CommitSummary
+       ctx.Data["commit_message"] = form.CommitMessage
+       ctx.Data["commit_choice"] = form.CommitChoice
+       ctx.Data["new_branch_name"] = form.NewBranchName
+       ctx.Data["last_commit"] = ctx.Repo.CommitID
+       ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",")
+       ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
+       ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",")
+       ctx.Data["Editorconfig"] = GetEditorConfig(ctx, form.TreePath)
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplEditFile)
+               return
+       }
+
+       // Cannot commit to a an existing branch if user doesn't have rights
+       if branchName == ctx.Repo.BranchName && !canCommit {
+               ctx.Data["Err_NewBranchName"] = true
+               ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
+               ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form)
+               return
+       }
+
+       // CommitSummary is optional in the web form, if empty, give it a default message based on add or update
+       // `message` will be both the summary and message combined
+       message := strings.TrimSpace(form.CommitSummary)
+       if len(message) == 0 {
+               if isNewFile {
+                       message = ctx.Tr("repo.editor.add", form.TreePath)
+               } else {
+                       message = ctx.Tr("repo.editor.update", form.TreePath)
+               }
+       }
+       form.CommitMessage = strings.TrimSpace(form.CommitMessage)
+       if len(form.CommitMessage) > 0 {
+               message += "\n\n" + form.CommitMessage
+       }
+
+       if _, err := repofiles.CreateOrUpdateRepoFile(ctx.Repo.Repository, ctx.User, &repofiles.UpdateRepoFileOptions{
+               LastCommitID: form.LastCommit,
+               OldBranch:    ctx.Repo.BranchName,
+               NewBranch:    branchName,
+               FromTreePath: ctx.Repo.TreePath,
+               TreePath:     form.TreePath,
+               Message:      message,
+               Content:      strings.ReplaceAll(form.Content, "\r", ""),
+               IsNewFile:    isNewFile,
+               Signoff:      form.Signoff,
+       }); err != nil {
+               // This is where we handle all the errors thrown by repofiles.CreateOrUpdateRepoFile
+               if git.IsErrNotExist(err) {
+                       ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form)
+               } else if models.IsErrLFSFileLocked(err) {
+                       ctx.Data["Err_TreePath"] = true
+                       ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(models.ErrLFSFileLocked).Path, err.(models.ErrLFSFileLocked).UserName), tplEditFile, &form)
+               } else if models.IsErrFilenameInvalid(err) {
+                       ctx.Data["Err_TreePath"] = true
+                       ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplEditFile, &form)
+               } else if models.IsErrFilePathInvalid(err) {
+                       ctx.Data["Err_TreePath"] = true
+                       if fileErr, ok := err.(models.ErrFilePathInvalid); ok {
+                               switch fileErr.Type {
+                               case git.EntryModeSymlink:
+                                       ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplEditFile, &form)
+                               case git.EntryModeTree:
+                                       ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplEditFile, &form)
+                               case git.EntryModeBlob:
+                                       ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplEditFile, &form)
+                               default:
+                                       ctx.Error(http.StatusInternalServerError, err.Error())
+                               }
+                       } else {
+                               ctx.Error(http.StatusInternalServerError, err.Error())
+                       }
+               } else if models.IsErrRepoFileAlreadyExists(err) {
+                       ctx.Data["Err_TreePath"] = true
+                       ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplEditFile, &form)
+               } else if git.IsErrBranchNotExist(err) {
+                       // For when a user adds/updates a file to a branch that no longer exists
+                       if branchErr, ok := err.(git.ErrBranchNotExist); ok {
+                               ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplEditFile, &form)
+                       } else {
+                               ctx.Error(http.StatusInternalServerError, err.Error())
+                       }
+               } else if models.IsErrBranchAlreadyExists(err) {
+                       // For when a user specifies a new branch that already exists
+                       ctx.Data["Err_NewBranchName"] = true
+                       if branchErr, ok := err.(models.ErrBranchAlreadyExists); ok {
+                               ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form)
+                       } else {
+                               ctx.Error(http.StatusInternalServerError, err.Error())
+                       }
+               } else if models.IsErrCommitIDDoesNotMatch(err) {
+                       ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplEditFile, &form)
+               } else if git.IsErrPushOutOfDate(err) {
+                       ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+util.PathEscapeSegments(form.NewBranchName)), tplEditFile, &form)
+               } else if git.IsErrPushRejected(err) {
+                       errPushRej := err.(*git.ErrPushRejected)
+                       if len(errPushRej.Message) == 0 {
+                               ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form)
+                       } else {
+                               flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
+                                       "Message": ctx.Tr("repo.editor.push_rejected"),
+                                       "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
+                                       "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
+                               })
+                               if err != nil {
+                                       ctx.ServerError("editFilePost.HTMLString", err)
+                                       return
+                               }
+                               ctx.RenderWithErr(flashError, tplEditFile, &form)
+                       }
+               } else {
+                       flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
+                               "Message": ctx.Tr("repo.editor.fail_to_update_file", form.TreePath),
+                               "Summary": ctx.Tr("repo.editor.fail_to_update_file_summary"),
+                               "Details": utils.SanitizeFlashErrorString(err.Error()),
+                       })
+                       if err != nil {
+                               ctx.ServerError("editFilePost.HTMLString", err)
+                               return
+                       }
+                       ctx.RenderWithErr(flashError, tplEditFile, &form)
+               }
+       }
+
+       if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(models.UnitTypePullRequests) {
+               ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
+       } else {
+               ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(form.TreePath))
+       }
+}
+
+// EditFilePost response for editing file
+func EditFilePost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.EditRepoFileForm)
+       editFilePost(ctx, *form, false)
+}
+
+// NewFilePost response for creating file
+func NewFilePost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.EditRepoFileForm)
+       editFilePost(ctx, *form, true)
+}
+
+// DiffPreviewPost render preview diff page
+func DiffPreviewPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.EditPreviewDiffForm)
+       treePath := cleanUploadFileName(ctx.Repo.TreePath)
+       if len(treePath) == 0 {
+               ctx.Error(http.StatusInternalServerError, "file name to diff is invalid")
+               return
+       }
+
+       entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
+       if err != nil {
+               ctx.Error(http.StatusInternalServerError, "GetTreeEntryByPath: "+err.Error())
+               return
+       } else if entry.IsDir() {
+               ctx.Error(http.StatusUnprocessableEntity)
+               return
+       }
+
+       diff, err := repofiles.GetDiffPreview(ctx.Repo.Repository, ctx.Repo.BranchName, treePath, form.Content)
+       if err != nil {
+               ctx.Error(http.StatusInternalServerError, "GetDiffPreview: "+err.Error())
+               return
+       }
+
+       if diff.NumFiles == 0 {
+               ctx.PlainText(200, []byte(ctx.Tr("repo.editor.no_changes_to_show")))
+               return
+       }
+       ctx.Data["File"] = diff.Files[0]
+
+       ctx.HTML(http.StatusOK, tplEditDiffPreview)
+}
+
+// DeleteFile render delete file page
+func DeleteFile(ctx *context.Context) {
+       ctx.Data["PageIsDelete"] = true
+       ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
+       treePath := cleanUploadFileName(ctx.Repo.TreePath)
+
+       if treePath != ctx.Repo.TreePath {
+               ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_delete", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
+               return
+       }
+
+       ctx.Data["TreePath"] = treePath
+       canCommit := renderCommitRights(ctx)
+
+       ctx.Data["commit_summary"] = ""
+       ctx.Data["commit_message"] = ""
+       ctx.Data["last_commit"] = ctx.Repo.CommitID
+       if canCommit {
+               ctx.Data["commit_choice"] = frmCommitChoiceDirect
+       } else {
+               ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
+       }
+       ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
+
+       ctx.HTML(http.StatusOK, tplDeleteFile)
+}
+
+// DeleteFilePost response for deleting file
+func DeleteFilePost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.DeleteRepoFileForm)
+       canCommit := renderCommitRights(ctx)
+       branchName := ctx.Repo.BranchName
+       if form.CommitChoice == frmCommitChoiceNewBranch {
+               branchName = form.NewBranchName
+       }
+
+       ctx.Data["PageIsDelete"] = true
+       ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
+       ctx.Data["TreePath"] = ctx.Repo.TreePath
+       ctx.Data["commit_summary"] = form.CommitSummary
+       ctx.Data["commit_message"] = form.CommitMessage
+       ctx.Data["commit_choice"] = form.CommitChoice
+       ctx.Data["new_branch_name"] = form.NewBranchName
+       ctx.Data["last_commit"] = ctx.Repo.CommitID
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplDeleteFile)
+               return
+       }
+
+       if branchName == ctx.Repo.BranchName && !canCommit {
+               ctx.Data["Err_NewBranchName"] = true
+               ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
+               ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplDeleteFile, &form)
+               return
+       }
+
+       message := strings.TrimSpace(form.CommitSummary)
+       if len(message) == 0 {
+               message = ctx.Tr("repo.editor.delete", ctx.Repo.TreePath)
+       }
+       form.CommitMessage = strings.TrimSpace(form.CommitMessage)
+       if len(form.CommitMessage) > 0 {
+               message += "\n\n" + form.CommitMessage
+       }
+
+       if _, err := repofiles.DeleteRepoFile(ctx.Repo.Repository, ctx.User, &repofiles.DeleteRepoFileOptions{
+               LastCommitID: form.LastCommit,
+               OldBranch:    ctx.Repo.BranchName,
+               NewBranch:    branchName,
+               TreePath:     ctx.Repo.TreePath,
+               Message:      message,
+               Signoff:      form.Signoff,
+       }); err != nil {
+               // This is where we handle all the errors thrown by repofiles.DeleteRepoFile
+               if git.IsErrNotExist(err) || models.IsErrRepoFileDoesNotExist(err) {
+                       ctx.RenderWithErr(ctx.Tr("repo.editor.file_deleting_no_longer_exists", ctx.Repo.TreePath), tplDeleteFile, &form)
+               } else if models.IsErrFilenameInvalid(err) {
+                       ctx.Data["Err_TreePath"] = true
+                       ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", ctx.Repo.TreePath), tplDeleteFile, &form)
+               } else if models.IsErrFilePathInvalid(err) {
+                       ctx.Data["Err_TreePath"] = true
+                       if fileErr, ok := err.(models.ErrFilePathInvalid); ok {
+                               switch fileErr.Type {
+                               case git.EntryModeSymlink:
+                                       ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplDeleteFile, &form)
+                               case git.EntryModeTree:
+                                       ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplDeleteFile, &form)
+                               case git.EntryModeBlob:
+                                       ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplDeleteFile, &form)
+                               default:
+                                       ctx.ServerError("DeleteRepoFile", err)
+                               }
+                       } else {
+                               ctx.ServerError("DeleteRepoFile", err)
+                       }
+               } else if git.IsErrBranchNotExist(err) {
+                       // For when a user deletes a file to a branch that no longer exists
+                       if branchErr, ok := err.(git.ErrBranchNotExist); ok {
+                               ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplDeleteFile, &form)
+                       } else {
+                               ctx.Error(http.StatusInternalServerError, err.Error())
+                       }
+               } else if models.IsErrBranchAlreadyExists(err) {
+                       // For when a user specifies a new branch that already exists
+                       if branchErr, ok := err.(models.ErrBranchAlreadyExists); ok {
+                               ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplDeleteFile, &form)
+                       } else {
+                               ctx.Error(http.StatusInternalServerError, err.Error())
+                       }
+               } else if models.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) {
+                       ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_deleting", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplDeleteFile, &form)
+               } else if git.IsErrPushRejected(err) {
+                       errPushRej := err.(*git.ErrPushRejected)
+                       if len(errPushRej.Message) == 0 {
+                               ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form)
+                       } else {
+                               flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
+                                       "Message": ctx.Tr("repo.editor.push_rejected"),
+                                       "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
+                                       "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
+                               })
+                               if err != nil {
+                                       ctx.ServerError("DeleteFilePost.HTMLString", err)
+                                       return
+                               }
+                               ctx.RenderWithErr(flashError, tplDeleteFile, &form)
+                       }
+               } else {
+                       ctx.ServerError("DeleteRepoFile", err)
+               }
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath))
+       if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(models.UnitTypePullRequests) {
+               ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
+       } else {
+               treePath := path.Dir(ctx.Repo.TreePath)
+               if treePath == "." {
+                       treePath = "" // the file deleted was in the root, so we return the user to the root directory
+               }
+               if len(treePath) > 0 {
+                       // Need to get the latest commit since it changed
+                       commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
+                       if err == nil && commit != nil {
+                               // We have the comment, now find what directory we can return the user to
+                               // (must have entries)
+                               treePath = GetClosestParentWithFiles(treePath, commit)
+                       } else {
+                               treePath = "" // otherwise return them to the root of the repo
+                       }
+               }
+               ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(treePath))
+       }
+}
+
+// UploadFile render upload file page
+func UploadFile(ctx *context.Context) {
+       ctx.Data["PageIsUpload"] = true
+       ctx.Data["RequireTribute"] = true
+       ctx.Data["RequireSimpleMDE"] = true
+       upload.AddUploadContext(ctx, "repo")
+       canCommit := renderCommitRights(ctx)
+       treePath := cleanUploadFileName(ctx.Repo.TreePath)
+       if treePath != ctx.Repo.TreePath {
+               ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_upload", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
+               return
+       }
+       ctx.Repo.TreePath = treePath
+
+       treeNames, treePaths := getParentTreeFields(ctx.Repo.TreePath)
+       if len(treeNames) == 0 {
+               // We must at least have one element for user to input.
+               treeNames = []string{""}
+       }
+
+       ctx.Data["TreeNames"] = treeNames
+       ctx.Data["TreePaths"] = treePaths
+       ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
+       ctx.Data["commit_summary"] = ""
+       ctx.Data["commit_message"] = ""
+       if canCommit {
+               ctx.Data["commit_choice"] = frmCommitChoiceDirect
+       } else {
+               ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
+       }
+       ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
+
+       ctx.HTML(http.StatusOK, tplUploadFile)
+}
+
+// UploadFilePost response for uploading file
+func UploadFilePost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.UploadRepoFileForm)
+       ctx.Data["PageIsUpload"] = true
+       ctx.Data["RequireTribute"] = true
+       ctx.Data["RequireSimpleMDE"] = true
+       upload.AddUploadContext(ctx, "repo")
+       canCommit := renderCommitRights(ctx)
+
+       oldBranchName := ctx.Repo.BranchName
+       branchName := oldBranchName
+
+       if form.CommitChoice == frmCommitChoiceNewBranch {
+               branchName = form.NewBranchName
+       }
+
+       form.TreePath = cleanUploadFileName(form.TreePath)
+
+       treeNames, treePaths := getParentTreeFields(form.TreePath)
+       if len(treeNames) == 0 {
+               // We must at least have one element for user to input.
+               treeNames = []string{""}
+       }
+
+       ctx.Data["TreePath"] = form.TreePath
+       ctx.Data["TreeNames"] = treeNames
+       ctx.Data["TreePaths"] = treePaths
+       ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + branchName
+       ctx.Data["commit_summary"] = form.CommitSummary
+       ctx.Data["commit_message"] = form.CommitMessage
+       ctx.Data["commit_choice"] = form.CommitChoice
+       ctx.Data["new_branch_name"] = branchName
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplUploadFile)
+               return
+       }
+
+       if oldBranchName != branchName {
+               if _, err := repo_module.GetBranch(ctx.Repo.Repository, branchName); err == nil {
+                       ctx.Data["Err_NewBranchName"] = true
+                       ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tplUploadFile, &form)
+                       return
+               }
+       } else if !canCommit {
+               ctx.Data["Err_NewBranchName"] = true
+               ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
+               ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplUploadFile, &form)
+               return
+       }
+
+       var newTreePath string
+       for _, part := range treeNames {
+               newTreePath = path.Join(newTreePath, part)
+               entry, err := ctx.Repo.Commit.GetTreeEntryByPath(newTreePath)
+               if err != nil {
+                       if git.IsErrNotExist(err) {
+                               // Means there is no item with that name, so we're good
+                               break
+                       }
+
+                       ctx.ServerError("Repo.Commit.GetTreeEntryByPath", err)
+                       return
+               }
+
+               // User can only upload files to a directory.
+               if !entry.IsDir() {
+                       ctx.Data["Err_TreePath"] = true
+                       ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", part), tplUploadFile, &form)
+                       return
+               }
+       }
+
+       message := strings.TrimSpace(form.CommitSummary)
+       if len(message) == 0 {
+               message = ctx.Tr("repo.editor.upload_files_to_dir", form.TreePath)
+       }
+
+       form.CommitMessage = strings.TrimSpace(form.CommitMessage)
+       if len(form.CommitMessage) > 0 {
+               message += "\n\n" + form.CommitMessage
+       }
+
+       if err := repofiles.UploadRepoFiles(ctx.Repo.Repository, ctx.User, &repofiles.UploadRepoFileOptions{
+               LastCommitID: ctx.Repo.CommitID,
+               OldBranch:    oldBranchName,
+               NewBranch:    branchName,
+               TreePath:     form.TreePath,
+               Message:      message,
+               Files:        form.Files,
+               Signoff:      form.Signoff,
+       }); err != nil {
+               if models.IsErrLFSFileLocked(err) {
+                       ctx.Data["Err_TreePath"] = true
+                       ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(models.ErrLFSFileLocked).Path, err.(models.ErrLFSFileLocked).UserName), tplUploadFile, &form)
+               } else if models.IsErrFilenameInvalid(err) {
+                       ctx.Data["Err_TreePath"] = true
+                       ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplUploadFile, &form)
+               } else if models.IsErrFilePathInvalid(err) {
+                       ctx.Data["Err_TreePath"] = true
+                       fileErr := err.(models.ErrFilePathInvalid)
+                       switch fileErr.Type {
+                       case git.EntryModeSymlink:
+                               ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplUploadFile, &form)
+                       case git.EntryModeTree:
+                               ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplUploadFile, &form)
+                       case git.EntryModeBlob:
+                               ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplUploadFile, &form)
+                       default:
+                               ctx.Error(http.StatusInternalServerError, err.Error())
+                       }
+               } else if models.IsErrRepoFileAlreadyExists(err) {
+                       ctx.Data["Err_TreePath"] = true
+                       ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplUploadFile, &form)
+               } else if git.IsErrBranchNotExist(err) {
+                       branchErr := err.(git.ErrBranchNotExist)
+                       ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplUploadFile, &form)
+               } else if models.IsErrBranchAlreadyExists(err) {
+                       // For when a user specifies a new branch that already exists
+                       ctx.Data["Err_NewBranchName"] = true
+                       branchErr := err.(models.ErrBranchAlreadyExists)
+                       ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplUploadFile, &form)
+               } else if git.IsErrPushOutOfDate(err) {
+                       ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+ctx.Repo.CommitID+"..."+util.PathEscapeSegments(form.NewBranchName)), tplUploadFile, &form)
+               } else if git.IsErrPushRejected(err) {
+                       errPushRej := err.(*git.ErrPushRejected)
+                       if len(errPushRej.Message) == 0 {
+                               ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplUploadFile, &form)
+                       } else {
+                               flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
+                                       "Message": ctx.Tr("repo.editor.push_rejected"),
+                                       "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
+                                       "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
+                               })
+                               if err != nil {
+                                       ctx.ServerError("UploadFilePost.HTMLString", err)
+                                       return
+                               }
+                               ctx.RenderWithErr(flashError, tplUploadFile, &form)
+                       }
+               } else {
+                       // os.ErrNotExist - upload file missing in the intervening time?!
+                       log.Error("Error during upload to repo: %-v to filepath: %s on %s from %s: %v", ctx.Repo.Repository, form.TreePath, oldBranchName, form.NewBranchName, err)
+                       ctx.RenderWithErr(ctx.Tr("repo.editor.unable_to_upload_files", form.TreePath, err), tplUploadFile, &form)
+               }
+               return
+       }
+
+       if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(models.UnitTypePullRequests) {
+               ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
+       } else {
+               ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(form.TreePath))
+       }
+}
+
+func cleanUploadFileName(name string) string {
+       // Rebase the filename
+       name = strings.Trim(path.Clean("/"+name), " /")
+       // Git disallows any filenames to have a .git directory in them.
+       for _, part := range strings.Split(name, "/") {
+               if strings.ToLower(part) == ".git" {
+                       return ""
+               }
+       }
+       return name
+}
+
+// UploadFileToServer upload file to server file dir not git
+func UploadFileToServer(ctx *context.Context) {
+       file, header, err := ctx.Req.FormFile("file")
+       if err != nil {
+               ctx.Error(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err))
+               return
+       }
+       defer file.Close()
+
+       buf := make([]byte, 1024)
+       n, _ := file.Read(buf)
+       if n > 0 {
+               buf = buf[:n]
+       }
+
+       err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes)
+       if err != nil {
+               ctx.Error(http.StatusBadRequest, err.Error())
+               return
+       }
+
+       name := cleanUploadFileName(header.Filename)
+       if len(name) == 0 {
+               ctx.Error(http.StatusInternalServerError, "Upload file name is invalid")
+               return
+       }
+
+       upload, err := models.NewUpload(name, buf, file)
+       if err != nil {
+               ctx.Error(http.StatusInternalServerError, fmt.Sprintf("NewUpload: %v", err))
+               return
+       }
+
+       log.Trace("New file uploaded: %s", upload.UUID)
+       ctx.JSON(http.StatusOK, map[string]string{
+               "uuid": upload.UUID,
+       })
+}
+
+// RemoveUploadFileFromServer remove file from server file dir
+func RemoveUploadFileFromServer(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.RemoveUploadFileForm)
+       if len(form.File) == 0 {
+               ctx.Status(204)
+               return
+       }
+
+       if err := models.DeleteUploadByUUID(form.File); err != nil {
+               ctx.Error(http.StatusInternalServerError, fmt.Sprintf("DeleteUploadByUUID: %v", err))
+               return
+       }
+
+       log.Trace("Upload file removed: %s", form.File)
+       ctx.Status(204)
+}
+
+// GetUniquePatchBranchName Gets a unique branch name for a new patch branch
+// It will be in the form of <username>-patch-<num> where <num> is the first branch of this format
+// that doesn't already exist. If we exceed 1000 tries or an error is thrown, we just return "" so the user has to
+// type in the branch name themselves (will be an empty field)
+func GetUniquePatchBranchName(ctx *context.Context) string {
+       prefix := ctx.User.LowerName + "-patch-"
+       for i := 1; i <= 1000; i++ {
+               branchName := fmt.Sprintf("%s%d", prefix, i)
+               if _, err := repo_module.GetBranch(ctx.Repo.Repository, branchName); err != nil {
+                       if git.IsErrBranchNotExist(err) {
+                               return branchName
+                       }
+                       log.Error("GetUniquePatchBranchName: %v", err)
+                       return ""
+               }
+       }
+       return ""
+}
+
+// GetClosestParentWithFiles Recursively gets the path of parent in a tree that has files (used when file in a tree is
+// deleted). Returns "" for the root if no parents other than the root have files. If the given treePath isn't a
+// SubTree or it has no entries, we go up one dir and see if we can return the user to that listing.
+func GetClosestParentWithFiles(treePath string, commit *git.Commit) string {
+       if len(treePath) == 0 || treePath == "." {
+               return ""
+       }
+       // see if the tree has entries
+       if tree, err := commit.SubTree(treePath); err != nil {
+               // failed to get tree, going up a dir
+               return GetClosestParentWithFiles(path.Dir(treePath), commit)
+       } else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 {
+               // no files in this dir, going up a dir
+               return GetClosestParentWithFiles(path.Dir(treePath), commit)
+       }
+       return treePath
+}
diff --git a/routers/web/repo/editor_test.go b/routers/web/repo/editor_test.go
new file mode 100644 (file)
index 0000000..ec7aee1
--- /dev/null
@@ -0,0 +1,83 @@
+// Copyright 2018 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 repo
+
+import (
+       "testing"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/test"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestCleanUploadName(t *testing.T) {
+       models.PrepareTestEnv(t)
+
+       var kases = map[string]string{
+               ".git/refs/master":               "",
+               "/root/abc":                      "root/abc",
+               "./../../abc":                    "abc",
+               "a/../.git":                      "",
+               "a/../../../abc":                 "abc",
+               "../../../acd":                   "acd",
+               "../../.git/abc":                 "",
+               "..\\..\\.git/abc":               "..\\..\\.git/abc",
+               "..\\../.git/abc":                "",
+               "..\\../.git":                    "",
+               "abc/../def":                     "def",
+               ".drone.yml":                     ".drone.yml",
+               ".abc/def/.drone.yml":            ".abc/def/.drone.yml",
+               "..drone.yml.":                   "..drone.yml.",
+               "..a.dotty...name...":            "..a.dotty...name...",
+               "..a.dotty../.folder../.name...": "..a.dotty../.folder../.name...",
+       }
+       for k, v := range kases {
+               assert.EqualValues(t, cleanUploadFileName(k), v)
+       }
+}
+
+func TestGetUniquePatchBranchName(t *testing.T) {
+       models.PrepareTestEnv(t)
+       ctx := test.MockContext(t, "user2/repo1")
+       ctx.SetParams(":id", "1")
+       test.LoadRepo(t, ctx, 1)
+       test.LoadRepoCommit(t, ctx)
+       test.LoadUser(t, ctx, 2)
+       test.LoadGitRepo(t, ctx)
+       defer ctx.Repo.GitRepo.Close()
+
+       expectedBranchName := "user2-patch-1"
+       branchName := GetUniquePatchBranchName(ctx)
+       assert.Equal(t, expectedBranchName, branchName)
+}
+
+func TestGetClosestParentWithFiles(t *testing.T) {
+       models.PrepareTestEnv(t)
+       ctx := test.MockContext(t, "user2/repo1")
+       ctx.SetParams(":id", "1")
+       test.LoadRepo(t, ctx, 1)
+       test.LoadRepoCommit(t, ctx)
+       test.LoadUser(t, ctx, 2)
+       test.LoadGitRepo(t, ctx)
+       defer ctx.Repo.GitRepo.Close()
+
+       repo := ctx.Repo.Repository
+       branch := repo.DefaultBranch
+       gitRepo, _ := git.OpenRepository(repo.RepoPath())
+       defer gitRepo.Close()
+       commit, _ := gitRepo.GetBranchCommit(branch)
+       expectedTreePath := ""
+
+       expectedTreePath = "" // Should return the root dir, empty string, since there are no subdirs in this repo
+       for _, deletedFile := range []string{
+               "dir1/dir2/dir3/file.txt",
+               "file.txt",
+       } {
+               treePath := GetClosestParentWithFiles(deletedFile, commit)
+               assert.Equal(t, expectedTreePath, treePath)
+       }
+}
diff --git a/routers/web/repo/http.go b/routers/web/repo/http.go
new file mode 100644 (file)
index 0000000..30d382b
--- /dev/null
@@ -0,0 +1,602 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2019 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 repo
+
+import (
+       "bytes"
+       "compress/gzip"
+       gocontext "context"
+       "fmt"
+       "io/ioutil"
+       "net/http"
+       "os"
+       "os/exec"
+       "path"
+       "regexp"
+       "strconv"
+       "strings"
+       "sync"
+       "time"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/process"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/structs"
+       "code.gitea.io/gitea/modules/util"
+       repo_service "code.gitea.io/gitea/services/repository"
+)
+
+// httpBase implmentation git smart HTTP protocol
+func httpBase(ctx *context.Context) (h *serviceHandler) {
+       if setting.Repository.DisableHTTPGit {
+               ctx.Resp.WriteHeader(http.StatusForbidden)
+               _, err := ctx.Resp.Write([]byte("Interacting with repositories by HTTP protocol is not allowed"))
+               if err != nil {
+                       log.Error(err.Error())
+               }
+               return
+       }
+
+       if len(setting.Repository.AccessControlAllowOrigin) > 0 {
+               allowedOrigin := setting.Repository.AccessControlAllowOrigin
+               // Set CORS headers for browser-based git clients
+               ctx.Resp.Header().Set("Access-Control-Allow-Origin", allowedOrigin)
+               ctx.Resp.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, User-Agent")
+
+               // Handle preflight OPTIONS request
+               if ctx.Req.Method == "OPTIONS" {
+                       if allowedOrigin == "*" {
+                               ctx.Status(http.StatusOK)
+                       } else if allowedOrigin == "null" {
+                               ctx.Status(http.StatusForbidden)
+                       } else {
+                               origin := ctx.Req.Header.Get("Origin")
+                               if len(origin) > 0 && origin == allowedOrigin {
+                                       ctx.Status(http.StatusOK)
+                               } else {
+                                       ctx.Status(http.StatusForbidden)
+                               }
+                       }
+                       return
+               }
+       }
+
+       username := ctx.Params(":username")
+       reponame := strings.TrimSuffix(ctx.Params(":reponame"), ".git")
+
+       if ctx.Query("go-get") == "1" {
+               context.EarlyResponseForGoGetMeta(ctx)
+               return
+       }
+
+       var isPull, receivePack bool
+       service := ctx.Query("service")
+       if service == "git-receive-pack" ||
+               strings.HasSuffix(ctx.Req.URL.Path, "git-receive-pack") {
+               isPull = false
+               receivePack = true
+       } else if service == "git-upload-pack" ||
+               strings.HasSuffix(ctx.Req.URL.Path, "git-upload-pack") {
+               isPull = true
+       } else if service == "git-upload-archive" ||
+               strings.HasSuffix(ctx.Req.URL.Path, "git-upload-archive") {
+               isPull = true
+       } else {
+               isPull = ctx.Req.Method == "GET"
+       }
+
+       var accessMode models.AccessMode
+       if isPull {
+               accessMode = models.AccessModeRead
+       } else {
+               accessMode = models.AccessModeWrite
+       }
+
+       isWiki := false
+       var unitType = models.UnitTypeCode
+       var wikiRepoName string
+       if strings.HasSuffix(reponame, ".wiki") {
+               isWiki = true
+               unitType = models.UnitTypeWiki
+               wikiRepoName = reponame
+               reponame = reponame[:len(reponame)-5]
+       }
+
+       owner, err := models.GetUserByName(username)
+       if err != nil {
+               if models.IsErrUserNotExist(err) {
+                       if redirectUserID, err := models.LookupUserRedirect(username); err == nil {
+                               context.RedirectToUser(ctx, username, redirectUserID)
+                       } else {
+                               ctx.NotFound(fmt.Sprintf("User %s does not exist", username), nil)
+                       }
+               } else {
+                       ctx.ServerError("GetUserByName", err)
+               }
+               return
+       }
+       if !owner.IsOrganization() && !owner.IsActive {
+               ctx.HandleText(http.StatusForbidden, "Repository cannot be accessed. You cannot push or open issues/pull-requests.")
+               return
+       }
+
+       repoExist := true
+       repo, err := models.GetRepositoryByName(owner.ID, reponame)
+       if err != nil {
+               if models.IsErrRepoNotExist(err) {
+                       if redirectRepoID, err := models.LookupRepoRedirect(owner.ID, reponame); err == nil {
+                               context.RedirectToRepo(ctx, redirectRepoID)
+                               return
+                       }
+                       repoExist = false
+               } else {
+                       ctx.ServerError("GetRepositoryByName", err)
+                       return
+               }
+       }
+
+       // Don't allow pushing if the repo is archived
+       if repoExist && repo.IsArchived && !isPull {
+               ctx.HandleText(http.StatusForbidden, "This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.")
+               return
+       }
+
+       // Only public pull don't need auth.
+       isPublicPull := repoExist && !repo.IsPrivate && isPull
+       var (
+               askAuth = !isPublicPull || setting.Service.RequireSignInView
+               environ []string
+       )
+
+       // don't allow anonymous pulls if organization is not public
+       if isPublicPull {
+               if err := repo.GetOwner(); err != nil {
+                       ctx.ServerError("GetOwner", err)
+                       return
+               }
+
+               askAuth = askAuth || (repo.Owner.Visibility != structs.VisibleTypePublic)
+       }
+
+       // check access
+       if askAuth {
+               // rely on the results of Contexter
+               if !ctx.IsSigned {
+                       // TODO: support digit auth - which would be Authorization header with digit
+                       ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=\".\"")
+                       ctx.Error(http.StatusUnauthorized)
+                       return
+               }
+
+               if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true {
+                       _, err = models.GetTwoFactorByUID(ctx.User.ID)
+                       if err == nil {
+                               // TODO: This response should be changed to "invalid credentials" for security reasons once the expectation behind it (creating an app token to authenticate) is properly documented
+                               ctx.HandleText(http.StatusUnauthorized, "Users with two-factor authentication enabled cannot perform HTTP/HTTPS operations via plain username and password. Please create and use a personal access token on the user settings page")
+                               return
+                       } else if !models.IsErrTwoFactorNotEnrolled(err) {
+                               ctx.ServerError("IsErrTwoFactorNotEnrolled", err)
+                               return
+                       }
+               }
+
+               if !ctx.User.IsActive || ctx.User.ProhibitLogin {
+                       ctx.HandleText(http.StatusForbidden, "Your account is disabled.")
+                       return
+               }
+
+               if repoExist {
+                       perm, err := models.GetUserRepoPermission(repo, ctx.User)
+                       if err != nil {
+                               ctx.ServerError("GetUserRepoPermission", err)
+                               return
+                       }
+
+                       if !perm.CanAccess(accessMode, unitType) {
+                               ctx.HandleText(http.StatusForbidden, "User permission denied")
+                               return
+                       }
+
+                       if !isPull && repo.IsMirror {
+                               ctx.HandleText(http.StatusForbidden, "mirror repository is read-only")
+                               return
+                       }
+               }
+
+               environ = []string{
+                       models.EnvRepoUsername + "=" + username,
+                       models.EnvRepoName + "=" + reponame,
+                       models.EnvPusherName + "=" + ctx.User.Name,
+                       models.EnvPusherID + fmt.Sprintf("=%d", ctx.User.ID),
+                       models.EnvIsDeployKey + "=false",
+                       models.EnvAppURL + "=" + setting.AppURL,
+               }
+
+               if !ctx.User.KeepEmailPrivate {
+                       environ = append(environ, models.EnvPusherEmail+"="+ctx.User.Email)
+               }
+
+               if isWiki {
+                       environ = append(environ, models.EnvRepoIsWiki+"=true")
+               } else {
+                       environ = append(environ, models.EnvRepoIsWiki+"=false")
+               }
+       }
+
+       if !repoExist {
+               if !receivePack {
+                       ctx.HandleText(http.StatusNotFound, "Repository not found")
+                       return
+               }
+
+               if isWiki { // you cannot send wiki operation before create the repository
+                       ctx.HandleText(http.StatusNotFound, "Repository not found")
+                       return
+               }
+
+               if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg {
+                       ctx.HandleText(http.StatusForbidden, "Push to create is not enabled for organizations.")
+                       return
+               }
+               if !owner.IsOrganization() && !setting.Repository.EnablePushCreateUser {
+                       ctx.HandleText(http.StatusForbidden, "Push to create is not enabled for users.")
+                       return
+               }
+
+               // Return dummy payload if GET receive-pack
+               if ctx.Req.Method == http.MethodGet {
+                       dummyInfoRefs(ctx)
+                       return
+               }
+
+               repo, err = repo_service.PushCreateRepo(ctx.User, owner, reponame)
+               if err != nil {
+                       log.Error("pushCreateRepo: %v", err)
+                       ctx.Status(http.StatusNotFound)
+                       return
+               }
+       }
+
+       if isWiki {
+               // Ensure the wiki is enabled before we allow access to it
+               if _, err := repo.GetUnit(models.UnitTypeWiki); err != nil {
+                       if models.IsErrUnitTypeNotExist(err) {
+                               ctx.HandleText(http.StatusForbidden, "repository wiki is disabled")
+                               return
+                       }
+                       log.Error("Failed to get the wiki unit in %-v Error: %v", repo, err)
+                       ctx.ServerError("GetUnit(UnitTypeWiki) for "+repo.FullName(), err)
+                       return
+               }
+       }
+
+       environ = append(environ, models.EnvRepoID+fmt.Sprintf("=%d", repo.ID))
+
+       w := ctx.Resp
+       r := ctx.Req
+       cfg := &serviceConfig{
+               UploadPack:  true,
+               ReceivePack: true,
+               Env:         environ,
+       }
+
+       r.URL.Path = strings.ToLower(r.URL.Path) // blue: In case some repo name has upper case name
+
+       dir := models.RepoPath(username, reponame)
+       if isWiki {
+               dir = models.RepoPath(username, wikiRepoName)
+       }
+
+       return &serviceHandler{cfg, w, r, dir, cfg.Env}
+}
+
+var (
+       infoRefsCache []byte
+       infoRefsOnce  sync.Once
+)
+
+func dummyInfoRefs(ctx *context.Context) {
+       infoRefsOnce.Do(func() {
+               tmpDir, err := ioutil.TempDir(os.TempDir(), "gitea-info-refs-cache")
+               if err != nil {
+                       log.Error("Failed to create temp dir for git-receive-pack cache: %v", err)
+                       return
+               }
+
+               defer func() {
+                       if err := util.RemoveAll(tmpDir); err != nil {
+                               log.Error("RemoveAll: %v", err)
+                       }
+               }()
+
+               if err := git.InitRepository(tmpDir, true); err != nil {
+                       log.Error("Failed to init bare repo for git-receive-pack cache: %v", err)
+                       return
+               }
+
+               refs, err := git.NewCommand("receive-pack", "--stateless-rpc", "--advertise-refs", ".").RunInDirBytes(tmpDir)
+               if err != nil {
+                       log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
+               }
+
+               log.Debug("populating infoRefsCache: \n%s", string(refs))
+               infoRefsCache = refs
+       })
+
+       ctx.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
+       ctx.Header().Set("Pragma", "no-cache")
+       ctx.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
+       ctx.Header().Set("Content-Type", "application/x-git-receive-pack-advertisement")
+       _, _ = ctx.Write(packetWrite("# service=git-receive-pack\n"))
+       _, _ = ctx.Write([]byte("0000"))
+       _, _ = ctx.Write(infoRefsCache)
+}
+
+type serviceConfig struct {
+       UploadPack  bool
+       ReceivePack bool
+       Env         []string
+}
+
+type serviceHandler struct {
+       cfg     *serviceConfig
+       w       http.ResponseWriter
+       r       *http.Request
+       dir     string
+       environ []string
+}
+
+func (h *serviceHandler) setHeaderNoCache() {
+       h.w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
+       h.w.Header().Set("Pragma", "no-cache")
+       h.w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
+}
+
+func (h *serviceHandler) setHeaderCacheForever() {
+       now := time.Now().Unix()
+       expires := now + 31536000
+       h.w.Header().Set("Date", fmt.Sprintf("%d", now))
+       h.w.Header().Set("Expires", fmt.Sprintf("%d", expires))
+       h.w.Header().Set("Cache-Control", "public, max-age=31536000")
+}
+
+func (h *serviceHandler) sendFile(contentType, file string) {
+       reqFile := path.Join(h.dir, file)
+
+       fi, err := os.Stat(reqFile)
+       if os.IsNotExist(err) {
+               h.w.WriteHeader(http.StatusNotFound)
+               return
+       }
+
+       h.w.Header().Set("Content-Type", contentType)
+       h.w.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size()))
+       h.w.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat))
+       http.ServeFile(h.w, h.r, reqFile)
+}
+
+// one or more key=value pairs separated by colons
+var safeGitProtocolHeader = regexp.MustCompile(`^[0-9a-zA-Z]+=[0-9a-zA-Z]+(:[0-9a-zA-Z]+=[0-9a-zA-Z]+)*$`)
+
+func getGitConfig(option, dir string) string {
+       out, err := git.NewCommand("config", option).RunInDir(dir)
+       if err != nil {
+               log.Error("%v - %s", err, out)
+       }
+       return out[0 : len(out)-1]
+}
+
+func getConfigSetting(service, dir string) bool {
+       service = strings.ReplaceAll(service, "-", "")
+       setting := getGitConfig("http."+service, dir)
+
+       if service == "uploadpack" {
+               return setting != "false"
+       }
+
+       return setting == "true"
+}
+
+func hasAccess(service string, h serviceHandler, checkContentType bool) bool {
+       if checkContentType {
+               if h.r.Header.Get("Content-Type") != fmt.Sprintf("application/x-git-%s-request", service) {
+                       return false
+               }
+       }
+
+       if !(service == "upload-pack" || service == "receive-pack") {
+               return false
+       }
+       if service == "receive-pack" {
+               return h.cfg.ReceivePack
+       }
+       if service == "upload-pack" {
+               return h.cfg.UploadPack
+       }
+
+       return getConfigSetting(service, h.dir)
+}
+
+func serviceRPC(h serviceHandler, service string) {
+       defer func() {
+               if err := h.r.Body.Close(); err != nil {
+                       log.Error("serviceRPC: Close: %v", err)
+               }
+
+       }()
+
+       if !hasAccess(service, h, true) {
+               h.w.WriteHeader(http.StatusUnauthorized)
+               return
+       }
+
+       h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service))
+
+       var err error
+       var reqBody = h.r.Body
+
+       // Handle GZIP.
+       if h.r.Header.Get("Content-Encoding") == "gzip" {
+               reqBody, err = gzip.NewReader(reqBody)
+               if err != nil {
+                       log.Error("Fail to create gzip reader: %v", err)
+                       h.w.WriteHeader(http.StatusInternalServerError)
+                       return
+               }
+       }
+
+       // set this for allow pre-receive and post-receive execute
+       h.environ = append(h.environ, "SSH_ORIGINAL_COMMAND="+service)
+
+       if protocol := h.r.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
+               h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
+       }
+
+       ctx, cancel := gocontext.WithCancel(git.DefaultContext)
+       defer cancel()
+       var stderr bytes.Buffer
+       cmd := exec.CommandContext(ctx, git.GitExecutable, service, "--stateless-rpc", h.dir)
+       cmd.Dir = h.dir
+       cmd.Env = append(os.Environ(), h.environ...)
+       cmd.Stdout = h.w
+       cmd.Stdin = reqBody
+       cmd.Stderr = &stderr
+
+       pid := process.GetManager().Add(fmt.Sprintf("%s %s %s [repo_path: %s]", git.GitExecutable, service, "--stateless-rpc", h.dir), cancel)
+       defer process.GetManager().Remove(pid)
+
+       if err := cmd.Run(); err != nil {
+               log.Error("Fail to serve RPC(%s) in %s: %v - %s", service, h.dir, err, stderr.String())
+               return
+       }
+}
+
+// ServiceUploadPack implements Git Smart HTTP protocol
+func ServiceUploadPack(ctx *context.Context) {
+       h := httpBase(ctx)
+       if h != nil {
+               serviceRPC(*h, "upload-pack")
+       }
+}
+
+// ServiceReceivePack implements Git Smart HTTP protocol
+func ServiceReceivePack(ctx *context.Context) {
+       h := httpBase(ctx)
+       if h != nil {
+               serviceRPC(*h, "receive-pack")
+       }
+}
+
+func getServiceType(r *http.Request) string {
+       serviceType := r.FormValue("service")
+       if !strings.HasPrefix(serviceType, "git-") {
+               return ""
+       }
+       return strings.Replace(serviceType, "git-", "", 1)
+}
+
+func updateServerInfo(dir string) []byte {
+       out, err := git.NewCommand("update-server-info").RunInDirBytes(dir)
+       if err != nil {
+               log.Error(fmt.Sprintf("%v - %s", err, string(out)))
+       }
+       return out
+}
+
+func packetWrite(str string) []byte {
+       s := strconv.FormatInt(int64(len(str)+4), 16)
+       if len(s)%4 != 0 {
+               s = strings.Repeat("0", 4-len(s)%4) + s
+       }
+       return []byte(s + str)
+}
+
+// GetInfoRefs implements Git dumb HTTP
+func GetInfoRefs(ctx *context.Context) {
+       h := httpBase(ctx)
+       if h == nil {
+               return
+       }
+       h.setHeaderNoCache()
+       if hasAccess(getServiceType(h.r), *h, false) {
+               service := getServiceType(h.r)
+
+               if protocol := h.r.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
+                       h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
+               }
+               h.environ = append(os.Environ(), h.environ...)
+
+               refs, err := git.NewCommand(service, "--stateless-rpc", "--advertise-refs", ".").RunInDirTimeoutEnv(h.environ, -1, h.dir)
+               if err != nil {
+                       log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
+               }
+
+               h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service))
+               h.w.WriteHeader(http.StatusOK)
+               _, _ = h.w.Write(packetWrite("# service=git-" + service + "\n"))
+               _, _ = h.w.Write([]byte("0000"))
+               _, _ = h.w.Write(refs)
+       } else {
+               updateServerInfo(h.dir)
+               h.sendFile("text/plain; charset=utf-8", "info/refs")
+       }
+}
+
+// GetTextFile implements Git dumb HTTP
+func GetTextFile(p string) func(*context.Context) {
+       return func(ctx *context.Context) {
+               h := httpBase(ctx)
+               if h != nil {
+                       h.setHeaderNoCache()
+                       file := ctx.Params("file")
+                       if file != "" {
+                               h.sendFile("text/plain", "objects/info/"+file)
+                       } else {
+                               h.sendFile("text/plain", p)
+                       }
+               }
+       }
+}
+
+// GetInfoPacks implements Git dumb HTTP
+func GetInfoPacks(ctx *context.Context) {
+       h := httpBase(ctx)
+       if h != nil {
+               h.setHeaderCacheForever()
+               h.sendFile("text/plain; charset=utf-8", "objects/info/packs")
+       }
+}
+
+// GetLooseObject implements Git dumb HTTP
+func GetLooseObject(ctx *context.Context) {
+       h := httpBase(ctx)
+       if h != nil {
+               h.setHeaderCacheForever()
+               h.sendFile("application/x-git-loose-object", fmt.Sprintf("objects/%s/%s",
+                       ctx.Params("head"), ctx.Params("hash")))
+       }
+}
+
+// GetPackFile implements Git dumb HTTP
+func GetPackFile(ctx *context.Context) {
+       h := httpBase(ctx)
+       if h != nil {
+               h.setHeaderCacheForever()
+               h.sendFile("application/x-git-packed-objects", "objects/pack/pack-"+ctx.Params("file")+".pack")
+       }
+}
+
+// GetIdxFile implements Git dumb HTTP
+func GetIdxFile(ctx *context.Context) {
+       h := httpBase(ctx)
+       if h != nil {
+               h.setHeaderCacheForever()
+               h.sendFile("application/x-git-packed-objects-toc", "objects/pack/pack-"+ctx.Params("file")+".idx")
+       }
+}
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
new file mode 100644 (file)
index 0000000..fd2877e
--- /dev/null
@@ -0,0 +1,2599 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2018 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 repo
+
+import (
+       "bytes"
+       "errors"
+       "fmt"
+       "io/ioutil"
+       "net/http"
+       "path"
+       "strconv"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/convert"
+       "code.gitea.io/gitea/modules/git"
+       issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/markup"
+       "code.gitea.io/gitea/modules/markup/markdown"
+       "code.gitea.io/gitea/modules/setting"
+       api "code.gitea.io/gitea/modules/structs"
+       "code.gitea.io/gitea/modules/upload"
+       "code.gitea.io/gitea/modules/util"
+       "code.gitea.io/gitea/modules/web"
+       comment_service "code.gitea.io/gitea/services/comments"
+       "code.gitea.io/gitea/services/forms"
+       issue_service "code.gitea.io/gitea/services/issue"
+       pull_service "code.gitea.io/gitea/services/pull"
+
+       "github.com/unknwon/com"
+)
+
+const (
+       tplAttachment base.TplName = "repo/issue/view_content/attachments"
+
+       tplIssues      base.TplName = "repo/issue/list"
+       tplIssueNew    base.TplName = "repo/issue/new"
+       tplIssueChoose base.TplName = "repo/issue/choose"
+       tplIssueView   base.TplName = "repo/issue/view"
+
+       tplReactions base.TplName = "repo/issue/view_content/reactions"
+
+       issueTemplateKey      = "IssueTemplate"
+       issueTemplateTitleKey = "IssueTemplateTitle"
+)
+
+var (
+       // IssueTemplateCandidates issue templates
+       IssueTemplateCandidates = []string{
+               "ISSUE_TEMPLATE.md",
+               "issue_template.md",
+               ".gitea/ISSUE_TEMPLATE.md",
+               ".gitea/issue_template.md",
+               ".github/ISSUE_TEMPLATE.md",
+               ".github/issue_template.md",
+       }
+)
+
+// MustAllowUserComment checks to make sure if an issue is locked.
+// If locked and user has permissions to write to the repository,
+// then the comment is allowed, else it is blocked
+func MustAllowUserComment(ctx *context.Context) {
+       issue := GetActionIssue(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.User.IsAdmin {
+               ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked"))
+               ctx.Redirect(issue.HTMLURL())
+               return
+       }
+}
+
+// MustEnableIssues check if repository enable internal issues
+func MustEnableIssues(ctx *context.Context) {
+       if !ctx.Repo.CanRead(models.UnitTypeIssues) &&
+               !ctx.Repo.CanRead(models.UnitTypeExternalTracker) {
+               ctx.NotFound("MustEnableIssues", nil)
+               return
+       }
+
+       unit, err := ctx.Repo.Repository.GetUnit(models.UnitTypeExternalTracker)
+       if err == nil {
+               ctx.Redirect(unit.ExternalTrackerConfig().ExternalTrackerURL)
+               return
+       }
+}
+
+// MustAllowPulls check if repository enable pull requests and user have right to do that
+func MustAllowPulls(ctx *context.Context) {
+       if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(models.UnitTypePullRequests) {
+               ctx.NotFound("MustAllowPulls", nil)
+               return
+       }
+
+       // User can send pull request if owns a forked repository.
+       if ctx.IsSigned && ctx.User.HasForkedRepo(ctx.Repo.Repository.ID) {
+               ctx.Repo.PullRequest.Allowed = true
+               ctx.Repo.PullRequest.HeadInfo = ctx.User.Name + ":" + ctx.Repo.BranchName
+       }
+}
+
+func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption util.OptionalBool) {
+       var err error
+       viewType := ctx.Query("type")
+       sortType := ctx.Query("sort")
+       types := []string{"all", "your_repositories", "assigned", "created_by", "mentioned", "review_requested"}
+       if !util.IsStringInSlice(viewType, types, true) {
+               viewType = "all"
+       }
+
+       var (
+               assigneeID        = ctx.QueryInt64("assignee")
+               posterID          int64
+               mentionedID       int64
+               reviewRequestedID int64
+               forceEmpty        bool
+       )
+
+       if ctx.IsSigned {
+               switch viewType {
+               case "created_by":
+                       posterID = ctx.User.ID
+               case "mentioned":
+                       mentionedID = ctx.User.ID
+               case "assigned":
+                       assigneeID = ctx.User.ID
+               case "review_requested":
+                       reviewRequestedID = ctx.User.ID
+               }
+       }
+
+       repo := ctx.Repo.Repository
+       var labelIDs []int64
+       selectLabels := ctx.Query("labels")
+       if len(selectLabels) > 0 && selectLabels != "0" {
+               labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
+               if err != nil {
+                       ctx.ServerError("StringsToInt64s", err)
+                       return
+               }
+       }
+
+       keyword := strings.Trim(ctx.Query("q"), " ")
+       if bytes.Contains([]byte(keyword), []byte{0x00}) {
+               keyword = ""
+       }
+
+       var issueIDs []int64
+       if len(keyword) > 0 {
+               issueIDs, err = issue_indexer.SearchIssuesByKeyword([]int64{repo.ID}, keyword)
+               if err != nil {
+                       ctx.ServerError("issueIndexer.Search", err)
+                       return
+               }
+               if len(issueIDs) == 0 {
+                       forceEmpty = true
+               }
+       }
+
+       var issueStats *models.IssueStats
+       if forceEmpty {
+               issueStats = &models.IssueStats{}
+       } else {
+               issueStats, err = models.GetIssueStats(&models.IssueStatsOptions{
+                       RepoID:            repo.ID,
+                       Labels:            selectLabels,
+                       MilestoneID:       milestoneID,
+                       AssigneeID:        assigneeID,
+                       MentionedID:       mentionedID,
+                       PosterID:          posterID,
+                       ReviewRequestedID: reviewRequestedID,
+                       IsPull:            isPullOption,
+                       IssueIDs:          issueIDs,
+               })
+               if err != nil {
+                       ctx.ServerError("GetIssueStats", err)
+                       return
+               }
+       }
+
+       isShowClosed := ctx.Query("state") == "closed"
+       // if open issues are zero and close don't, use closed as default
+       if len(ctx.Query("state")) == 0 && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 {
+               isShowClosed = true
+       }
+
+       page := ctx.QueryInt("page")
+       if page <= 1 {
+               page = 1
+       }
+
+       var total int
+       if !isShowClosed {
+               total = int(issueStats.OpenCount)
+       } else {
+               total = int(issueStats.ClosedCount)
+       }
+       pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
+
+       var mileIDs []int64
+       if milestoneID > 0 {
+               mileIDs = []int64{milestoneID}
+       }
+
+       var issues []*models.Issue
+       if forceEmpty {
+               issues = []*models.Issue{}
+       } else {
+               issues, err = models.Issues(&models.IssuesOptions{
+                       ListOptions: models.ListOptions{
+                               Page:     pager.Paginater.Current(),
+                               PageSize: setting.UI.IssuePagingNum,
+                       },
+                       RepoIDs:           []int64{repo.ID},
+                       AssigneeID:        assigneeID,
+                       PosterID:          posterID,
+                       MentionedID:       mentionedID,
+                       ReviewRequestedID: reviewRequestedID,
+                       MilestoneIDs:      mileIDs,
+                       ProjectID:         projectID,
+                       IsClosed:          util.OptionalBoolOf(isShowClosed),
+                       IsPull:            isPullOption,
+                       LabelIDs:          labelIDs,
+                       SortType:          sortType,
+                       IssueIDs:          issueIDs,
+               })
+               if err != nil {
+                       ctx.ServerError("Issues", err)
+                       return
+               }
+       }
+
+       var issueList = models.IssueList(issues)
+       approvalCounts, err := issueList.GetApprovalCounts()
+       if err != nil {
+               ctx.ServerError("ApprovalCounts", err)
+               return
+       }
+
+       // Get posters.
+       for i := range issues {
+               // Check read status
+               if !ctx.IsSigned {
+                       issues[i].IsRead = true
+               } else if err = issues[i].GetIsRead(ctx.User.ID); err != nil {
+                       ctx.ServerError("GetIsRead", err)
+                       return
+               }
+       }
+
+       commitStatus, err := pull_service.GetIssuesLastCommitStatus(issues)
+       if err != nil {
+               ctx.ServerError("GetIssuesLastCommitStatus", err)
+               return
+       }
+
+       ctx.Data["Issues"] = issues
+       ctx.Data["CommitStatus"] = commitStatus
+
+       // Get assignees.
+       ctx.Data["Assignees"], err = repo.GetAssignees()
+       if err != nil {
+               ctx.ServerError("GetAssignees", err)
+               return
+       }
+
+       handleTeamMentions(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       labels, err := models.GetLabelsByRepoID(repo.ID, "", models.ListOptions{})
+       if err != nil {
+               ctx.ServerError("GetLabelsByRepoID", err)
+               return
+       }
+
+       if repo.Owner.IsOrganization() {
+               orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.Query("sort"), models.ListOptions{})
+               if err != nil {
+                       ctx.ServerError("GetLabelsByOrgID", err)
+                       return
+               }
+
+               ctx.Data["OrgLabels"] = orgLabels
+               labels = append(labels, orgLabels...)
+       }
+
+       for _, l := range labels {
+               l.LoadSelectedLabelsAfterClick(labelIDs)
+       }
+       ctx.Data["Labels"] = labels
+       ctx.Data["NumLabels"] = len(labels)
+
+       if ctx.QueryInt64("assignee") == 0 {
+               assigneeID = 0 // Reset ID to prevent unexpected selection of assignee.
+       }
+
+       ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] =
+               issue_service.GetRefEndNamesAndURLs(issues, ctx.Repo.RepoLink)
+
+       ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 {
+               counts, ok := approvalCounts[issueID]
+               if !ok || len(counts) == 0 {
+                       return 0
+               }
+               reviewTyp := models.ReviewTypeApprove
+               if typ == "reject" {
+                       reviewTyp = models.ReviewTypeReject
+               } else if typ == "waiting" {
+                       reviewTyp = models.ReviewTypeRequest
+               }
+               for _, count := range counts {
+                       if count.Type == reviewTyp {
+                               return count.Count
+                       }
+               }
+               return 0
+       }
+       ctx.Data["IssueStats"] = issueStats
+       ctx.Data["SelLabelIDs"] = labelIDs
+       ctx.Data["SelectLabels"] = selectLabels
+       ctx.Data["ViewType"] = viewType
+       ctx.Data["SortType"] = sortType
+       ctx.Data["MilestoneID"] = milestoneID
+       ctx.Data["AssigneeID"] = assigneeID
+       ctx.Data["IsShowClosed"] = isShowClosed
+       ctx.Data["Keyword"] = keyword
+       if isShowClosed {
+               ctx.Data["State"] = "closed"
+       } else {
+               ctx.Data["State"] = "open"
+       }
+
+       pager.AddParam(ctx, "q", "Keyword")
+       pager.AddParam(ctx, "type", "ViewType")
+       pager.AddParam(ctx, "sort", "SortType")
+       pager.AddParam(ctx, "state", "State")
+       pager.AddParam(ctx, "labels", "SelectLabels")
+       pager.AddParam(ctx, "milestone", "MilestoneID")
+       pager.AddParam(ctx, "assignee", "AssigneeID")
+       ctx.Data["Page"] = pager
+}
+
+// Issues render issues page
+func Issues(ctx *context.Context) {
+       isPullList := ctx.Params(":type") == "pulls"
+       if isPullList {
+               MustAllowPulls(ctx)
+               if ctx.Written() {
+                       return
+               }
+               ctx.Data["Title"] = ctx.Tr("repo.pulls")
+               ctx.Data["PageIsPullList"] = true
+       } else {
+               MustEnableIssues(ctx)
+               if ctx.Written() {
+                       return
+               }
+               ctx.Data["Title"] = ctx.Tr("repo.issues")
+               ctx.Data["PageIsIssueList"] = true
+               ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
+       }
+
+       issues(ctx, ctx.QueryInt64("milestone"), ctx.QueryInt64("project"), util.OptionalBoolOf(isPullList))
+       if ctx.Written() {
+               return
+       }
+
+       var err error
+       // Get milestones
+       ctx.Data["Milestones"], err = models.GetMilestones(models.GetMilestonesOption{
+               RepoID: ctx.Repo.Repository.ID,
+               State:  api.StateType(ctx.Query("state")),
+       })
+       if err != nil {
+               ctx.ServerError("GetAllRepoMilestones", err)
+               return
+       }
+
+       ctx.Data["CanWriteIssuesOrPulls"] = ctx.Repo.CanWriteIssuesOrPulls(isPullList)
+
+       ctx.HTML(http.StatusOK, tplIssues)
+}
+
+// RetrieveRepoMilestonesAndAssignees find all the milestones and assignees of a repository
+func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *models.Repository) {
+       var err error
+       ctx.Data["OpenMilestones"], err = models.GetMilestones(models.GetMilestonesOption{
+               RepoID: repo.ID,
+               State:  api.StateOpen,
+       })
+       if err != nil {
+               ctx.ServerError("GetMilestones", err)
+               return
+       }
+       ctx.Data["ClosedMilestones"], err = models.GetMilestones(models.GetMilestonesOption{
+               RepoID: repo.ID,
+               State:  api.StateClosed,
+       })
+       if err != nil {
+               ctx.ServerError("GetMilestones", err)
+               return
+       }
+
+       ctx.Data["Assignees"], err = repo.GetAssignees()
+       if err != nil {
+               ctx.ServerError("GetAssignees", err)
+               return
+       }
+
+       handleTeamMentions(ctx)
+       if ctx.Written() {
+               return
+       }
+}
+
+func retrieveProjects(ctx *context.Context, repo *models.Repository) {
+
+       var err error
+
+       ctx.Data["OpenProjects"], _, err = models.GetProjects(models.ProjectSearchOptions{
+               RepoID:   repo.ID,
+               Page:     -1,
+               IsClosed: util.OptionalBoolFalse,
+               Type:     models.ProjectTypeRepository,
+       })
+       if err != nil {
+               ctx.ServerError("GetProjects", err)
+               return
+       }
+
+       ctx.Data["ClosedProjects"], _, err = models.GetProjects(models.ProjectSearchOptions{
+               RepoID:   repo.ID,
+               Page:     -1,
+               IsClosed: util.OptionalBoolTrue,
+               Type:     models.ProjectTypeRepository,
+       })
+       if err != nil {
+               ctx.ServerError("GetProjects", err)
+               return
+       }
+}
+
+// repoReviewerSelection items to bee shown
+type repoReviewerSelection struct {
+       IsTeam    bool
+       Team      *models.Team
+       User      *models.User
+       Review    *models.Review
+       CanChange bool
+       Checked   bool
+       ItemID    int64
+}
+
+// RetrieveRepoReviewers find all reviewers of a repository
+func RetrieveRepoReviewers(ctx *context.Context, repo *models.Repository, issue *models.Issue, canChooseReviewer bool) {
+       ctx.Data["CanChooseReviewer"] = canChooseReviewer
+
+       originalAuthorReviews, err := models.GetReviewersFromOriginalAuthorsByIssueID(issue.ID)
+       if err != nil {
+               ctx.ServerError("GetReviewersFromOriginalAuthorsByIssueID", err)
+               return
+       }
+       ctx.Data["OriginalReviews"] = originalAuthorReviews
+
+       reviews, err := models.GetReviewersByIssueID(issue.ID)
+       if err != nil {
+               ctx.ServerError("GetReviewersByIssueID", err)
+               return
+       }
+
+       if len(reviews) == 0 && !canChooseReviewer {
+               return
+       }
+
+       var (
+               pullReviews         []*repoReviewerSelection
+               reviewersResult     []*repoReviewerSelection
+               teamReviewersResult []*repoReviewerSelection
+               teamReviewers       []*models.Team
+               reviewers           []*models.User
+       )
+
+       if canChooseReviewer {
+               posterID := issue.PosterID
+               if issue.OriginalAuthorID > 0 {
+                       posterID = 0
+               }
+
+               reviewers, err = repo.GetReviewers(ctx.User.ID, posterID)
+               if err != nil {
+                       ctx.ServerError("GetReviewers", err)
+                       return
+               }
+
+               teamReviewers, err = repo.GetReviewerTeams()
+               if err != nil {
+                       ctx.ServerError("GetReviewerTeams", err)
+                       return
+               }
+
+               if len(reviewers) > 0 {
+                       reviewersResult = make([]*repoReviewerSelection, 0, len(reviewers))
+               }
+
+               if len(teamReviewers) > 0 {
+                       teamReviewersResult = make([]*repoReviewerSelection, 0, len(teamReviewers))
+               }
+       }
+
+       pullReviews = make([]*repoReviewerSelection, 0, len(reviews))
+
+       for _, review := range reviews {
+               tmp := &repoReviewerSelection{
+                       Checked: review.Type == models.ReviewTypeRequest,
+                       Review:  review,
+                       ItemID:  review.ReviewerID,
+               }
+               if review.ReviewerTeamID > 0 {
+                       tmp.IsTeam = true
+                       tmp.ItemID = -review.ReviewerTeamID
+               }
+
+               if ctx.Repo.IsAdmin() {
+                       // Admin can dismiss or re-request any review requests
+                       tmp.CanChange = true
+               } else if ctx.User != nil && ctx.User.ID == review.ReviewerID && review.Type == models.ReviewTypeRequest {
+                       // A user can refuse review requests
+                       tmp.CanChange = true
+               } else if (canChooseReviewer || (ctx.User != nil && ctx.User.ID == issue.PosterID)) && review.Type != models.ReviewTypeRequest &&
+                       ctx.User.ID != review.ReviewerID {
+                       // The poster of the PR, a manager, or official reviewers can re-request review from other reviewers
+                       tmp.CanChange = true
+               }
+
+               pullReviews = append(pullReviews, tmp)
+
+               if canChooseReviewer {
+                       if tmp.IsTeam {
+                               teamReviewersResult = append(teamReviewersResult, tmp)
+                       } else {
+                               reviewersResult = append(reviewersResult, tmp)
+                       }
+               }
+       }
+
+       if len(pullReviews) > 0 {
+               // Drop all non-existing users and teams from the reviews
+               currentPullReviewers := make([]*repoReviewerSelection, 0, len(pullReviews))
+               for _, item := range pullReviews {
+                       if item.Review.ReviewerID > 0 {
+                               if err = item.Review.LoadReviewer(); err != nil {
+                                       if models.IsErrUserNotExist(err) {
+                                               continue
+                                       }
+                                       ctx.ServerError("LoadReviewer", err)
+                                       return
+                               }
+                               item.User = item.Review.Reviewer
+                       } else if item.Review.ReviewerTeamID > 0 {
+                               if err = item.Review.LoadReviewerTeam(); err != nil {
+                                       if models.IsErrTeamNotExist(err) {
+                                               continue
+                                       }
+                                       ctx.ServerError("LoadReviewerTeam", err)
+                                       return
+                               }
+                               item.Team = item.Review.ReviewerTeam
+                       } else {
+                               continue
+                       }
+
+                       currentPullReviewers = append(currentPullReviewers, item)
+               }
+               ctx.Data["PullReviewers"] = currentPullReviewers
+       }
+
+       if canChooseReviewer && reviewersResult != nil {
+               preadded := len(reviewersResult)
+               for _, reviewer := range reviewers {
+                       found := false
+               reviewAddLoop:
+                       for _, tmp := range reviewersResult[:preadded] {
+                               if tmp.ItemID == reviewer.ID {
+                                       tmp.User = reviewer
+                                       found = true
+                                       break reviewAddLoop
+                               }
+                       }
+
+                       if found {
+                               continue
+                       }
+
+                       reviewersResult = append(reviewersResult, &repoReviewerSelection{
+                               IsTeam:    false,
+                               CanChange: true,
+                               User:      reviewer,
+                               ItemID:    reviewer.ID,
+                       })
+               }
+
+               ctx.Data["Reviewers"] = reviewersResult
+       }
+
+       if canChooseReviewer && teamReviewersResult != nil {
+               preadded := len(teamReviewersResult)
+               for _, team := range teamReviewers {
+                       found := false
+               teamReviewAddLoop:
+                       for _, tmp := range teamReviewersResult[:preadded] {
+                               if tmp.ItemID == -team.ID {
+                                       tmp.Team = team
+                                       found = true
+                                       break teamReviewAddLoop
+                               }
+                       }
+
+                       if found {
+                               continue
+                       }
+
+                       teamReviewersResult = append(teamReviewersResult, &repoReviewerSelection{
+                               IsTeam:    true,
+                               CanChange: true,
+                               Team:      team,
+                               ItemID:    -team.ID,
+                       })
+               }
+
+               ctx.Data["TeamReviewers"] = teamReviewersResult
+       }
+}
+
+// RetrieveRepoMetas find all the meta information of a repository
+func RetrieveRepoMetas(ctx *context.Context, repo *models.Repository, isPull bool) []*models.Label {
+       if !ctx.Repo.CanWriteIssuesOrPulls(isPull) {
+               return nil
+       }
+
+       labels, err := models.GetLabelsByRepoID(repo.ID, "", models.ListOptions{})
+       if err != nil {
+               ctx.ServerError("GetLabelsByRepoID", err)
+               return nil
+       }
+       ctx.Data["Labels"] = labels
+       if repo.Owner.IsOrganization() {
+               orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.Query("sort"), models.ListOptions{})
+               if err != nil {
+                       return nil
+               }
+
+               ctx.Data["OrgLabels"] = orgLabels
+               labels = append(labels, orgLabels...)
+       }
+
+       RetrieveRepoMilestonesAndAssignees(ctx, repo)
+       if ctx.Written() {
+               return nil
+       }
+
+       retrieveProjects(ctx, repo)
+       if ctx.Written() {
+               return nil
+       }
+
+       brs, _, err := ctx.Repo.GitRepo.GetBranches(0, 0)
+       if err != nil {
+               ctx.ServerError("GetBranches", err)
+               return nil
+       }
+       ctx.Data["Branches"] = brs
+
+       // Contains true if the user can create issue dependencies
+       ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.User, isPull)
+
+       return labels
+}
+
+func getFileContentFromDefaultBranch(ctx *context.Context, filename string) (string, bool) {
+       var bytes []byte
+
+       if ctx.Repo.Commit == nil {
+               var err error
+               ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
+               if err != nil {
+                       return "", false
+               }
+       }
+
+       entry, err := ctx.Repo.Commit.GetTreeEntryByPath(filename)
+       if err != nil {
+               return "", false
+       }
+       if entry.Blob().Size() >= setting.UI.MaxDisplayFileSize {
+               return "", false
+       }
+       r, err := entry.Blob().DataAsync()
+       if err != nil {
+               return "", false
+       }
+       defer r.Close()
+       bytes, err = ioutil.ReadAll(r)
+       if err != nil {
+               return "", false
+       }
+       return string(bytes), true
+}
+
+func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleDirs []string, possibleFiles []string) {
+       templateCandidates := make([]string, 0, len(possibleFiles))
+       if ctx.Query("template") != "" {
+               for _, dirName := range possibleDirs {
+                       templateCandidates = append(templateCandidates, path.Join(dirName, ctx.Query("template")))
+               }
+       }
+       templateCandidates = append(templateCandidates, possibleFiles...) // Append files to the end because they should be fallback
+       for _, filename := range templateCandidates {
+               templateContent, found := getFileContentFromDefaultBranch(ctx, filename)
+               if found {
+                       var meta api.IssueTemplate
+                       templateBody, err := markdown.ExtractMetadata(templateContent, &meta)
+                       if err != nil {
+                               log.Debug("could not extract metadata from %s [%s]: %v", filename, ctx.Repo.Repository.FullName(), err)
+                               ctx.Data[ctxDataKey] = templateContent
+                               return
+                       }
+                       ctx.Data[issueTemplateTitleKey] = meta.Title
+                       ctx.Data[ctxDataKey] = templateBody
+                       labelIDs := make([]string, 0, len(meta.Labels))
+                       if repoLabels, err := models.GetLabelsByRepoID(ctx.Repo.Repository.ID, "", models.ListOptions{}); err == nil {
+                               ctx.Data["Labels"] = repoLabels
+                               if ctx.Repo.Owner.IsOrganization() {
+                                       if orgLabels, err := models.GetLabelsByOrgID(ctx.Repo.Owner.ID, ctx.Query("sort"), models.ListOptions{}); err == nil {
+                                               ctx.Data["OrgLabels"] = orgLabels
+                                               repoLabels = append(repoLabels, orgLabels...)
+                                       }
+                               }
+
+                               for _, metaLabel := range meta.Labels {
+                                       for _, repoLabel := range repoLabels {
+                                               if strings.EqualFold(repoLabel.Name, metaLabel) {
+                                                       repoLabel.IsChecked = true
+                                                       labelIDs = append(labelIDs, fmt.Sprintf("%d", repoLabel.ID))
+                                                       break
+                                               }
+                                       }
+                               }
+                       }
+                       ctx.Data["HasSelectedLabel"] = len(labelIDs) > 0
+                       ctx.Data["label_ids"] = strings.Join(labelIDs, ",")
+                       return
+               }
+       }
+}
+
+// NewIssue render creating issue page
+func NewIssue(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.issues.new")
+       ctx.Data["PageIsIssueList"] = true
+       ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
+       ctx.Data["RequireHighlightJS"] = true
+       ctx.Data["RequireSimpleMDE"] = true
+       ctx.Data["RequireTribute"] = true
+       ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
+       title := ctx.Query("title")
+       ctx.Data["TitleQuery"] = title
+       body := ctx.Query("body")
+       ctx.Data["BodyQuery"] = body
+
+       ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(models.UnitTypeProjects)
+       ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
+       upload.AddUploadContext(ctx, "comment")
+
+       milestoneID := ctx.QueryInt64("milestone")
+       if milestoneID > 0 {
+               milestone, err := models.GetMilestoneByID(milestoneID)
+               if err != nil {
+                       log.Error("GetMilestoneByID: %d: %v", milestoneID, err)
+               } else {
+                       ctx.Data["milestone_id"] = milestoneID
+                       ctx.Data["Milestone"] = milestone
+               }
+       }
+
+       projectID := ctx.QueryInt64("project")
+       if projectID > 0 {
+               project, err := models.GetProjectByID(projectID)
+               if err != nil {
+                       log.Error("GetProjectByID: %d: %v", projectID, err)
+               } else if project.RepoID != ctx.Repo.Repository.ID {
+                       log.Error("GetProjectByID: %d: %v", projectID, fmt.Errorf("project[%d] not in repo [%d]", project.ID, ctx.Repo.Repository.ID))
+               } else {
+                       ctx.Data["project_id"] = projectID
+                       ctx.Data["Project"] = project
+               }
+
+       }
+
+       RetrieveRepoMetas(ctx, ctx.Repo.Repository, false)
+       setTemplateIfExists(ctx, issueTemplateKey, context.IssueTemplateDirCandidates, IssueTemplateCandidates)
+       if ctx.Written() {
+               return
+       }
+
+       ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(models.UnitTypeIssues)
+
+       ctx.HTML(http.StatusOK, tplIssueNew)
+}
+
+// NewIssueChooseTemplate render creating issue from template page
+func NewIssueChooseTemplate(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.issues.new")
+       ctx.Data["PageIsIssueList"] = true
+       ctx.Data["milestone"] = ctx.QueryInt64("milestone")
+
+       issueTemplates := ctx.IssueTemplatesFromDefaultBranch()
+       ctx.Data["NewIssueChooseTemplate"] = len(issueTemplates) > 0
+       ctx.Data["IssueTemplates"] = issueTemplates
+
+       ctx.HTML(http.StatusOK, tplIssueChoose)
+}
+
+// ValidateRepoMetas check and returns repository's meta informations
+func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull bool) ([]int64, []int64, int64, int64) {
+       var (
+               repo = ctx.Repo.Repository
+               err  error
+       )
+
+       labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository, isPull)
+       if ctx.Written() {
+               return nil, nil, 0, 0
+       }
+
+       var labelIDs []int64
+       hasSelected := false
+       // Check labels.
+       if len(form.LabelIDs) > 0 {
+               labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
+               if err != nil {
+                       return nil, nil, 0, 0
+               }
+               labelIDMark := base.Int64sToMap(labelIDs)
+
+               for i := range labels {
+                       if labelIDMark[labels[i].ID] {
+                               labels[i].IsChecked = true
+                               hasSelected = true
+                       }
+               }
+       }
+
+       ctx.Data["Labels"] = labels
+       ctx.Data["HasSelectedLabel"] = hasSelected
+       ctx.Data["label_ids"] = form.LabelIDs
+
+       // Check milestone.
+       milestoneID := form.MilestoneID
+       if milestoneID > 0 {
+               ctx.Data["Milestone"], err = repo.GetMilestoneByID(milestoneID)
+               if err != nil {
+                       ctx.ServerError("GetMilestoneByID", err)
+                       return nil, nil, 0, 0
+               }
+               ctx.Data["milestone_id"] = milestoneID
+       }
+
+       if form.ProjectID > 0 {
+               p, err := models.GetProjectByID(form.ProjectID)
+               if err != nil {
+                       ctx.ServerError("GetProjectByID", err)
+                       return nil, nil, 0, 0
+               }
+               if p.RepoID != ctx.Repo.Repository.ID {
+                       ctx.NotFound("", nil)
+                       return nil, nil, 0, 0
+               }
+
+               ctx.Data["Project"] = p
+               ctx.Data["project_id"] = form.ProjectID
+       }
+
+       // Check assignees
+       var assigneeIDs []int64
+       if len(form.AssigneeIDs) > 0 {
+               assigneeIDs, err = base.StringsToInt64s(strings.Split(form.AssigneeIDs, ","))
+               if err != nil {
+                       return nil, nil, 0, 0
+               }
+
+               // Check if the passed assignees actually exists and is assignable
+               for _, aID := range assigneeIDs {
+                       assignee, err := models.GetUserByID(aID)
+                       if err != nil {
+                               ctx.ServerError("GetUserByID", err)
+                               return nil, nil, 0, 0
+                       }
+
+                       valid, err := models.CanBeAssigned(assignee, repo, isPull)
+                       if err != nil {
+                               ctx.ServerError("CanBeAssigned", err)
+                               return nil, nil, 0, 0
+                       }
+
+                       if !valid {
+                               ctx.ServerError("canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
+                               return nil, nil, 0, 0
+                       }
+               }
+       }
+
+       // Keep the old assignee id thingy for compatibility reasons
+       if form.AssigneeID > 0 {
+               assigneeIDs = append(assigneeIDs, form.AssigneeID)
+       }
+
+       return labelIDs, assigneeIDs, milestoneID, form.ProjectID
+}
+
+// NewIssuePost response for creating new issue
+func NewIssuePost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.CreateIssueForm)
+       ctx.Data["Title"] = ctx.Tr("repo.issues.new")
+       ctx.Data["PageIsIssueList"] = true
+       ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
+       ctx.Data["RequireHighlightJS"] = true
+       ctx.Data["RequireSimpleMDE"] = true
+       ctx.Data["ReadOnly"] = false
+       ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
+       ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
+       upload.AddUploadContext(ctx, "comment")
+
+       var (
+               repo        = ctx.Repo.Repository
+               attachments []string
+       )
+
+       labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, false)
+       if ctx.Written() {
+               return
+       }
+
+       if setting.Attachment.Enabled {
+               attachments = form.Files
+       }
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplIssueNew)
+               return
+       }
+
+       if util.IsEmptyString(form.Title) {
+               ctx.RenderWithErr(ctx.Tr("repo.issues.new.title_empty"), tplIssueNew, form)
+               return
+       }
+
+       issue := &models.Issue{
+               RepoID:      repo.ID,
+               Title:       form.Title,
+               PosterID:    ctx.User.ID,
+               Poster:      ctx.User,
+               MilestoneID: milestoneID,
+               Content:     form.Content,
+               Ref:         form.Ref,
+       }
+
+       if err := issue_service.NewIssue(repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
+               if models.IsErrUserDoesNotHaveAccessToRepo(err) {
+                       ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
+                       return
+               }
+               ctx.ServerError("NewIssue", err)
+               return
+       }
+
+       if projectID > 0 {
+               if err := models.ChangeProjectAssign(issue, ctx.User, projectID); err != nil {
+                       ctx.ServerError("ChangeProjectAssign", err)
+                       return
+               }
+       }
+
+       log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
+       ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + fmt.Sprint(issue.Index))
+}
+
+// commentTag returns the CommentTag for a comment in/with the given repo, poster and issue
+func commentTag(repo *models.Repository, poster *models.User, issue *models.Issue) (models.CommentTag, error) {
+       perm, err := models.GetUserRepoPermission(repo, poster)
+       if err != nil {
+               return models.CommentTagNone, err
+       }
+       if perm.IsOwner() {
+               if !poster.IsAdmin {
+                       return models.CommentTagOwner, nil
+               }
+
+               ok, err := models.IsUserRealRepoAdmin(repo, poster)
+               if err != nil {
+                       return models.CommentTagNone, err
+               }
+
+               if ok {
+                       return models.CommentTagOwner, nil
+               }
+
+               if ok, err = repo.IsCollaborator(poster.ID); ok && err == nil {
+                       return models.CommentTagWriter, nil
+               }
+
+               return models.CommentTagNone, err
+       }
+
+       if perm.CanWrite(models.UnitTypeCode) {
+               return models.CommentTagWriter, nil
+       }
+
+       return models.CommentTagNone, nil
+}
+
+func getBranchData(ctx *context.Context, issue *models.Issue) {
+       ctx.Data["BaseBranch"] = nil
+       ctx.Data["HeadBranch"] = nil
+       ctx.Data["HeadUserName"] = nil
+       ctx.Data["BaseName"] = ctx.Repo.Repository.OwnerName
+       if issue.IsPull {
+               pull := issue.PullRequest
+               ctx.Data["BaseBranch"] = pull.BaseBranch
+               ctx.Data["HeadBranch"] = pull.HeadBranch
+               ctx.Data["HeadUserName"] = pull.MustHeadUserName()
+       }
+}
+
+// ViewIssue render issue view page
+func ViewIssue(ctx *context.Context) {
+       if ctx.Params(":type") == "issues" {
+               // If issue was requested we check if repo has external tracker and redirect
+               extIssueUnit, err := ctx.Repo.Repository.GetUnit(models.UnitTypeExternalTracker)
+               if err == nil && extIssueUnit != nil {
+                       if extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == markup.IssueNameStyleNumeric || extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == "" {
+                               metas := ctx.Repo.Repository.ComposeMetas()
+                               metas["index"] = ctx.Params(":index")
+                               ctx.Redirect(com.Expand(extIssueUnit.ExternalTrackerConfig().ExternalTrackerFormat, metas))
+                               return
+                       }
+               } else if err != nil && !models.IsErrUnitTypeNotExist(err) {
+                       ctx.ServerError("GetUnit", err)
+                       return
+               }
+       }
+
+       issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+       if err != nil {
+               if models.IsErrIssueNotExist(err) {
+                       ctx.NotFound("GetIssueByIndex", err)
+               } else {
+                       ctx.ServerError("GetIssueByIndex", err)
+               }
+               return
+       }
+
+       // Make sure type and URL matches.
+       if ctx.Params(":type") == "issues" && issue.IsPull {
+               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
+               return
+       } else if ctx.Params(":type") == "pulls" && !issue.IsPull {
+               ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + fmt.Sprint(issue.Index))
+               return
+       }
+
+       if issue.IsPull {
+               MustAllowPulls(ctx)
+               if ctx.Written() {
+                       return
+               }
+               ctx.Data["PageIsPullList"] = true
+               ctx.Data["PageIsPullConversation"] = true
+       } else {
+               MustEnableIssues(ctx)
+               if ctx.Written() {
+                       return
+               }
+               ctx.Data["PageIsIssueList"] = true
+               ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
+       }
+
+       if issue.IsPull && !ctx.Repo.CanRead(models.UnitTypeIssues) {
+               ctx.Data["IssueType"] = "pulls"
+       } else if !issue.IsPull && !ctx.Repo.CanRead(models.UnitTypePullRequests) {
+               ctx.Data["IssueType"] = "issues"
+       } else {
+               ctx.Data["IssueType"] = "all"
+       }
+
+       ctx.Data["RequireHighlightJS"] = true
+       ctx.Data["RequireTribute"] = true
+       ctx.Data["RequireSimpleMDE"] = true
+       ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(models.UnitTypeProjects)
+       ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
+       upload.AddUploadContext(ctx, "comment")
+
+       if err = issue.LoadAttributes(); err != nil {
+               ctx.ServerError("LoadAttributes", err)
+               return
+       }
+
+       if err = filterXRefComments(ctx, issue); err != nil {
+               ctx.ServerError("filterXRefComments", err)
+               return
+       }
+
+       ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
+
+       iw := new(models.IssueWatch)
+       if ctx.User != nil {
+               iw.UserID = ctx.User.ID
+               iw.IssueID = issue.ID
+               iw.IsWatching, err = models.CheckIssueWatch(ctx.User, issue)
+               if err != nil {
+                       ctx.ServerError("CheckIssueWatch", err)
+                       return
+               }
+       }
+       ctx.Data["IssueWatch"] = iw
+
+       issue.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
+               URLPrefix: ctx.Repo.RepoLink,
+               Metas:     ctx.Repo.Repository.ComposeMetas(),
+       }, issue.Content)
+       if err != nil {
+               ctx.ServerError("RenderString", err)
+               return
+       }
+
+       repo := ctx.Repo.Repository
+
+       // Get more information if it's a pull request.
+       if issue.IsPull {
+               if issue.PullRequest.HasMerged {
+                       ctx.Data["DisableStatusChange"] = issue.PullRequest.HasMerged
+                       PrepareMergedViewPullInfo(ctx, issue)
+               } else {
+                       PrepareViewPullInfo(ctx, issue)
+                       ctx.Data["DisableStatusChange"] = ctx.Data["IsPullRequestBroken"] == true && issue.IsClosed
+               }
+               if ctx.Written() {
+                       return
+               }
+       }
+
+       // Metas.
+       // Check labels.
+       labelIDMark := make(map[int64]bool)
+       for i := range issue.Labels {
+               labelIDMark[issue.Labels[i].ID] = true
+       }
+       labels, err := models.GetLabelsByRepoID(repo.ID, "", models.ListOptions{})
+       if err != nil {
+               ctx.ServerError("GetLabelsByRepoID", err)
+               return
+       }
+       ctx.Data["Labels"] = labels
+
+       if repo.Owner.IsOrganization() {
+               orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.Query("sort"), models.ListOptions{})
+               if err != nil {
+                       ctx.ServerError("GetLabelsByOrgID", err)
+                       return
+               }
+               ctx.Data["OrgLabels"] = orgLabels
+
+               labels = append(labels, orgLabels...)
+       }
+
+       hasSelected := false
+       for i := range labels {
+               if labelIDMark[labels[i].ID] {
+                       labels[i].IsChecked = true
+                       hasSelected = true
+               }
+       }
+       ctx.Data["HasSelectedLabel"] = hasSelected
+
+       // Check milestone and assignee.
+       if ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
+               RetrieveRepoMilestonesAndAssignees(ctx, repo)
+               retrieveProjects(ctx, repo)
+
+               if ctx.Written() {
+                       return
+               }
+       }
+
+       if issue.IsPull {
+               canChooseReviewer := ctx.Repo.CanWrite(models.UnitTypePullRequests)
+               if !canChooseReviewer && ctx.User != nil && ctx.IsSigned {
+                       canChooseReviewer, err = models.IsOfficialReviewer(issue, ctx.User)
+                       if err != nil {
+                               ctx.ServerError("IsOfficialReviewer", err)
+                               return
+                       }
+               }
+
+               RetrieveRepoReviewers(ctx, repo, issue, canChooseReviewer)
+               if ctx.Written() {
+                       return
+               }
+       }
+
+       if ctx.IsSigned {
+               // Update issue-user.
+               if err = issue.ReadBy(ctx.User.ID); err != nil {
+                       ctx.ServerError("ReadBy", err)
+                       return
+               }
+       }
+
+       var (
+               tag          models.CommentTag
+               ok           bool
+               marked       = make(map[int64]models.CommentTag)
+               comment      *models.Comment
+               participants = make([]*models.User, 1, 10)
+       )
+       if ctx.Repo.Repository.IsTimetrackerEnabled() {
+               if ctx.IsSigned {
+                       // Deal with the stopwatch
+                       ctx.Data["IsStopwatchRunning"] = models.StopwatchExists(ctx.User.ID, issue.ID)
+                       if !ctx.Data["IsStopwatchRunning"].(bool) {
+                               var exists bool
+                               var sw *models.Stopwatch
+                               if exists, sw, err = models.HasUserStopwatch(ctx.User.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 {
+                                               ctx.ServerError("GetIssueByID", err)
+                                               return
+                                       }
+                                       if err = otherIssue.LoadRepo(); err != nil {
+                                               ctx.ServerError("LoadRepo", err)
+                                               return
+                                       }
+                                       // Add link to the issue of the already running stopwatch
+                                       ctx.Data["OtherStopwatchURL"] = otherIssue.HTMLURL()
+                               }
+                       }
+                       ctx.Data["CanUseTimetracker"] = ctx.Repo.CanUseTimetracker(issue, ctx.User)
+               } else {
+                       ctx.Data["CanUseTimetracker"] = false
+               }
+               if ctx.Data["WorkingUsers"], err = models.TotalTimes(models.FindTrackedTimesOptions{IssueID: issue.ID}); err != nil {
+                       ctx.ServerError("TotalTimes", err)
+                       return
+               }
+       }
+
+       // Check if the user can use the dependencies
+       ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.User, issue.IsPull)
+
+       // check if dependencies can be created across repositories
+       ctx.Data["AllowCrossRepositoryDependencies"] = setting.Service.AllowCrossRepositoryDependencies
+
+       if issue.ShowTag, err = commentTag(repo, issue.Poster, issue); err != nil {
+               ctx.ServerError("commentTag", err)
+               return
+       }
+       marked[issue.PosterID] = issue.ShowTag
+
+       // Render comments and and fetch participants.
+       participants[0] = issue.Poster
+       for _, comment = range issue.Comments {
+               comment.Issue = issue
+
+               if err := comment.LoadPoster(); err != nil {
+                       ctx.ServerError("LoadPoster", err)
+                       return
+               }
+
+               if comment.Type == models.CommentTypeComment {
+                       if err := comment.LoadAttachments(); err != nil {
+                               ctx.ServerError("LoadAttachments", err)
+                               return
+                       }
+
+                       comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
+                               URLPrefix: ctx.Repo.RepoLink,
+                               Metas:     ctx.Repo.Repository.ComposeMetas(),
+                       }, comment.Content)
+                       if err != nil {
+                               ctx.ServerError("RenderString", err)
+                               return
+                       }
+                       // Check tag.
+                       tag, ok = marked[comment.PosterID]
+                       if ok {
+                               comment.ShowTag = tag
+                               continue
+                       }
+
+                       comment.ShowTag, err = commentTag(repo, comment.Poster, issue)
+                       if err != nil {
+                               ctx.ServerError("commentTag", err)
+                               return
+                       }
+                       marked[comment.PosterID] = comment.ShowTag
+                       participants = addParticipant(comment.Poster, participants)
+               } else if comment.Type == models.CommentTypeLabel {
+                       if err = comment.LoadLabel(); err != nil {
+                               ctx.ServerError("LoadLabel", err)
+                               return
+                       }
+               } else if comment.Type == models.CommentTypeMilestone {
+                       if err = comment.LoadMilestone(); err != nil {
+                               ctx.ServerError("LoadMilestone", err)
+                               return
+                       }
+                       ghostMilestone := &models.Milestone{
+                               ID:   -1,
+                               Name: ctx.Tr("repo.issues.deleted_milestone"),
+                       }
+                       if comment.OldMilestoneID > 0 && comment.OldMilestone == nil {
+                               comment.OldMilestone = ghostMilestone
+                       }
+                       if comment.MilestoneID > 0 && comment.Milestone == nil {
+                               comment.Milestone = ghostMilestone
+                       }
+               } else if comment.Type == models.CommentTypeProject {
+
+                       if err = comment.LoadProject(); err != nil {
+                               ctx.ServerError("LoadProject", err)
+                               return
+                       }
+
+                       ghostProject := &models.Project{
+                               ID:    -1,
+                               Title: ctx.Tr("repo.issues.deleted_project"),
+                       }
+
+                       if comment.OldProjectID > 0 && comment.OldProject == nil {
+                               comment.OldProject = ghostProject
+                       }
+
+                       if comment.ProjectID > 0 && comment.Project == nil {
+                               comment.Project = ghostProject
+                       }
+
+               } else if comment.Type == models.CommentTypeAssignees || comment.Type == models.CommentTypeReviewRequest {
+                       if err = comment.LoadAssigneeUserAndTeam(); err != nil {
+                               ctx.ServerError("LoadAssigneeUserAndTeam", err)
+                               return
+                       }
+               } else if comment.Type == models.CommentTypeRemoveDependency || comment.Type == models.CommentTypeAddDependency {
+                       if err = comment.LoadDepIssueDetails(); err != nil {
+                               if !models.IsErrIssueNotExist(err) {
+                                       ctx.ServerError("LoadDepIssueDetails", err)
+                                       return
+                               }
+                       }
+               } else if comment.Type == models.CommentTypeCode || comment.Type == models.CommentTypeReview || comment.Type == models.CommentTypeDismissReview {
+                       comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
+                               URLPrefix: ctx.Repo.RepoLink,
+                               Metas:     ctx.Repo.Repository.ComposeMetas(),
+                       }, comment.Content)
+                       if err != nil {
+                               ctx.ServerError("RenderString", err)
+                               return
+                       }
+                       if err = comment.LoadReview(); err != nil && !models.IsErrReviewNotExist(err) {
+                               ctx.ServerError("LoadReview", err)
+                               return
+                       }
+                       participants = addParticipant(comment.Poster, participants)
+                       if comment.Review == nil {
+                               continue
+                       }
+                       if err = comment.Review.LoadAttributes(); err != nil {
+                               if !models.IsErrUserNotExist(err) {
+                                       ctx.ServerError("Review.LoadAttributes", err)
+                                       return
+                               }
+                               comment.Review.Reviewer = models.NewGhostUser()
+                       }
+                       if err = comment.Review.LoadCodeComments(); err != nil {
+                               ctx.ServerError("Review.LoadCodeComments", err)
+                               return
+                       }
+                       for _, codeComments := range comment.Review.CodeComments {
+                               for _, lineComments := range codeComments {
+                                       for _, c := range lineComments {
+                                               // Check tag.
+                                               tag, ok = marked[c.PosterID]
+                                               if ok {
+                                                       c.ShowTag = tag
+                                                       continue
+                                               }
+
+                                               c.ShowTag, err = commentTag(repo, c.Poster, issue)
+                                               if err != nil {
+                                                       ctx.ServerError("commentTag", err)
+                                                       return
+                                               }
+                                               marked[c.PosterID] = c.ShowTag
+                                               participants = addParticipant(c.Poster, participants)
+                                       }
+                               }
+                       }
+                       if err = comment.LoadResolveDoer(); err != nil {
+                               ctx.ServerError("LoadResolveDoer", err)
+                               return
+                       }
+               } else if comment.Type == models.CommentTypePullPush {
+                       participants = addParticipant(comment.Poster, participants)
+                       if err = comment.LoadPushCommits(); err != nil {
+                               ctx.ServerError("LoadPushCommits", err)
+                               return
+                       }
+               } else if comment.Type == models.CommentTypeAddTimeManual ||
+                       comment.Type == models.CommentTypeStopTracking {
+                       // drop error since times could be pruned from DB..
+                       _ = comment.LoadTime()
+               }
+       }
+
+       // Combine multiple label assignments into a single comment
+       combineLabelComments(issue)
+
+       getBranchData(ctx, issue)
+       if issue.IsPull {
+               pull := issue.PullRequest
+               pull.Issue = issue
+               canDelete := false
+               ctx.Data["AllowMerge"] = false
+
+               if ctx.IsSigned {
+                       if err := pull.LoadHeadRepo(); err != nil {
+                               log.Error("LoadHeadRepo: %v", err)
+                       } else if pull.HeadRepo != nil && pull.HeadBranch != pull.HeadRepo.DefaultBranch {
+                               perm, err := models.GetUserRepoPermission(pull.HeadRepo, ctx.User)
+                               if err != nil {
+                                       ctx.ServerError("GetUserRepoPermission", err)
+                                       return
+                               }
+                               if perm.CanWrite(models.UnitTypeCode) {
+                                       // Check if branch is not protected
+                                       if protected, err := pull.HeadRepo.IsProtectedBranch(pull.HeadBranch, ctx.User); err != nil {
+                                               log.Error("IsProtectedBranch: %v", err)
+                                       } else if !protected {
+                                               canDelete = true
+                                               ctx.Data["DeleteBranchLink"] = ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index) + "/cleanup"
+                                       }
+                               }
+                       }
+
+                       if err := pull.LoadBaseRepo(); err != nil {
+                               log.Error("LoadBaseRepo: %v", err)
+                       }
+                       perm, err := models.GetUserRepoPermission(pull.BaseRepo, ctx.User)
+                       if err != nil {
+                               ctx.ServerError("GetUserRepoPermission", err)
+                               return
+                       }
+                       ctx.Data["AllowMerge"], err = pull_service.IsUserAllowedToMerge(pull, perm, ctx.User)
+                       if err != nil {
+                               ctx.ServerError("IsUserAllowedToMerge", err)
+                               return
+                       }
+
+                       if ctx.Data["CanMarkConversation"], err = models.CanMarkConversation(issue, ctx.User); err != nil {
+                               ctx.ServerError("CanMarkConversation", err)
+                               return
+                       }
+               }
+
+               prUnit, err := repo.GetUnit(models.UnitTypePullRequests)
+               if err != nil {
+                       ctx.ServerError("GetUnit", err)
+                       return
+               }
+               prConfig := prUnit.PullRequestsConfig()
+
+               // Check correct values and select default
+               if ms, ok := ctx.Data["MergeStyle"].(models.MergeStyle); !ok ||
+                       !prConfig.IsMergeStyleAllowed(ms) {
+                       defaultMergeStyle := prConfig.GetDefaultMergeStyle()
+                       if prConfig.IsMergeStyleAllowed(defaultMergeStyle) && !ok {
+                               ctx.Data["MergeStyle"] = defaultMergeStyle
+                       } else if prConfig.AllowMerge {
+                               ctx.Data["MergeStyle"] = models.MergeStyleMerge
+                       } else if prConfig.AllowRebase {
+                               ctx.Data["MergeStyle"] = models.MergeStyleRebase
+                       } else if prConfig.AllowRebaseMerge {
+                               ctx.Data["MergeStyle"] = models.MergeStyleRebaseMerge
+                       } else if prConfig.AllowSquash {
+                               ctx.Data["MergeStyle"] = models.MergeStyleSquash
+                       } else if prConfig.AllowManualMerge {
+                               ctx.Data["MergeStyle"] = models.MergeStyleManuallyMerged
+                       } else {
+                               ctx.Data["MergeStyle"] = ""
+                       }
+               }
+               if err = pull.LoadProtectedBranch(); err != nil {
+                       ctx.ServerError("LoadProtectedBranch", err)
+                       return
+               }
+               if pull.ProtectedBranch != nil {
+                       cnt := pull.ProtectedBranch.GetGrantedApprovalsCount(pull)
+                       ctx.Data["IsBlockedByApprovals"] = !pull.ProtectedBranch.HasEnoughApprovals(pull)
+                       ctx.Data["IsBlockedByRejection"] = pull.ProtectedBranch.MergeBlockedByRejectedReview(pull)
+                       ctx.Data["IsBlockedByOfficialReviewRequests"] = pull.ProtectedBranch.MergeBlockedByOfficialReviewRequests(pull)
+                       ctx.Data["IsBlockedByOutdatedBranch"] = pull.ProtectedBranch.MergeBlockedByOutdatedBranch(pull)
+                       ctx.Data["GrantedApprovals"] = cnt
+                       ctx.Data["RequireSigned"] = pull.ProtectedBranch.RequireSignedCommits
+                       ctx.Data["ChangedProtectedFiles"] = pull.ChangedProtectedFiles
+                       ctx.Data["IsBlockedByChangedProtectedFiles"] = len(pull.ChangedProtectedFiles) != 0
+                       ctx.Data["ChangedProtectedFilesNum"] = len(pull.ChangedProtectedFiles)
+               }
+               ctx.Data["WillSign"] = false
+               if ctx.User != nil {
+                       sign, key, _, err := pull.SignMerge(ctx.User, pull.BaseRepo.RepoPath(), pull.BaseBranch, pull.GetGitRefName())
+                       ctx.Data["WillSign"] = sign
+                       ctx.Data["SigningKey"] = key
+                       if err != nil {
+                               if models.IsErrWontSign(err) {
+                                       ctx.Data["WontSignReason"] = err.(*models.ErrWontSign).Reason
+                               } else {
+                                       ctx.Data["WontSignReason"] = "error"
+                                       log.Error("Error whilst checking if could sign pr %d in repo %s. Error: %v", pull.ID, pull.BaseRepo.FullName(), err)
+                               }
+                       }
+               } else {
+                       ctx.Data["WontSignReason"] = "not_signed_in"
+               }
+               ctx.Data["IsPullBranchDeletable"] = canDelete &&
+                       pull.HeadRepo != nil &&
+                       git.IsBranchExist(pull.HeadRepo.RepoPath(), pull.HeadBranch) &&
+                       (!pull.HasMerged || ctx.Data["HeadBranchCommitID"] == ctx.Data["PullHeadCommitID"])
+
+               stillCanManualMerge := func() bool {
+                       if pull.HasMerged || issue.IsClosed || !ctx.IsSigned {
+                               return false
+                       }
+                       if pull.CanAutoMerge() || pull.IsWorkInProgress() || pull.IsChecking() {
+                               return false
+                       }
+                       if (ctx.User.IsAdmin || ctx.Repo.IsAdmin()) && prConfig.AllowManualMerge {
+                               return true
+                       }
+
+                       return false
+               }
+
+               ctx.Data["StillCanManualMerge"] = stillCanManualMerge()
+       }
+
+       // Get Dependencies
+       ctx.Data["BlockedByDependencies"], err = issue.BlockedByDependencies()
+       if err != nil {
+               ctx.ServerError("BlockedByDependencies", err)
+               return
+       }
+       ctx.Data["BlockingDependencies"], err = issue.BlockingDependencies()
+       if err != nil {
+               ctx.ServerError("BlockingDependencies", err)
+               return
+       }
+
+       ctx.Data["Participants"] = participants
+       ctx.Data["NumParticipants"] = len(participants)
+       ctx.Data["Issue"] = issue
+       ctx.Data["ReadOnly"] = false
+       ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + ctx.Data["Link"].(string)
+       ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.User.ID)
+       ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
+       ctx.Data["HasProjectsWritePermission"] = ctx.Repo.CanWrite(models.UnitTypeProjects)
+       ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.User.IsAdmin)
+       ctx.Data["LockReasons"] = setting.Repository.Issue.LockReasons
+       ctx.Data["RefEndName"] = git.RefEndName(issue.Ref)
+       ctx.HTML(http.StatusOK, tplIssueView)
+}
+
+// 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"))
+       if err != nil {
+               ctx.NotFoundOrServerError("GetIssueByIndex", models.IsErrIssueNotExist, err)
+               return nil
+       }
+       issue.Repo = ctx.Repo.Repository
+       checkIssueRights(ctx, issue)
+       if ctx.Written() {
+               return nil
+       }
+       if err = issue.LoadAttributes(); err != nil {
+               ctx.ServerError("LoadAttributes", nil)
+               return nil
+       }
+       return issue
+}
+
+func checkIssueRights(ctx *context.Context, issue *models.Issue) {
+       if issue.IsPull && !ctx.Repo.CanRead(models.UnitTypePullRequests) ||
+               !issue.IsPull && !ctx.Repo.CanRead(models.UnitTypeIssues) {
+               ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
+       }
+}
+
+func getActionIssues(ctx *context.Context) []*models.Issue {
+       commaSeparatedIssueIDs := ctx.Query("issue_ids")
+       if len(commaSeparatedIssueIDs) == 0 {
+               return nil
+       }
+       issueIDs := make([]int64, 0, 10)
+       for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") {
+               issueID, err := strconv.ParseInt(stringIssueID, 10, 64)
+               if err != nil {
+                       ctx.ServerError("ParseInt", err)
+                       return nil
+               }
+               issueIDs = append(issueIDs, issueID)
+       }
+       issues, err := models.GetIssuesByIDs(issueIDs)
+       if err != nil {
+               ctx.ServerError("GetIssuesByIDs", err)
+               return nil
+       }
+       // Check access rights for all issues
+       issueUnitEnabled := ctx.Repo.CanRead(models.UnitTypeIssues)
+       prUnitEnabled := ctx.Repo.CanRead(models.UnitTypePullRequests)
+       for _, issue := range issues {
+               if issue.IsPull && !prUnitEnabled || !issue.IsPull && !issueUnitEnabled {
+                       ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
+                       return nil
+               }
+               if err = issue.LoadAttributes(); err != nil {
+                       ctx.ServerError("LoadAttributes", err)
+                       return nil
+               }
+       }
+       return issues
+}
+
+// UpdateIssueTitle change issue's title
+func UpdateIssueTitle(ctx *context.Context) {
+       issue := GetActionIssue(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       if !ctx.IsSigned || (!issue.IsPoster(ctx.User.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
+               ctx.Error(http.StatusForbidden)
+               return
+       }
+
+       title := ctx.QueryTrim("title")
+       if len(title) == 0 {
+               ctx.Error(http.StatusNoContent)
+               return
+       }
+
+       if err := issue_service.ChangeTitle(issue, ctx.User, title); err != nil {
+               ctx.ServerError("ChangeTitle", err)
+               return
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "title": issue.Title,
+       })
+}
+
+// UpdateIssueRef change issue's ref (branch)
+func UpdateIssueRef(ctx *context.Context) {
+       issue := GetActionIssue(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       if !ctx.IsSigned || (!issue.IsPoster(ctx.User.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) || issue.IsPull {
+               ctx.Error(http.StatusForbidden)
+               return
+       }
+
+       ref := ctx.QueryTrim("ref")
+
+       if err := issue_service.ChangeIssueRef(issue, ctx.User, ref); err != nil {
+               ctx.ServerError("ChangeRef", err)
+               return
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "ref": ref,
+       })
+}
+
+// UpdateIssueContent change issue's content
+func UpdateIssueContent(ctx *context.Context) {
+       issue := GetActionIssue(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
+               ctx.Error(http.StatusForbidden)
+               return
+       }
+
+       content := ctx.Query("content")
+       if err := issue_service.ChangeContent(issue, ctx.User, content); err != nil {
+               ctx.ServerError("ChangeContent", err)
+               return
+       }
+
+       files := ctx.QueryStrings("files[]")
+       if err := updateAttachments(issue, files); err != nil {
+               ctx.ServerError("UpdateAttachments", err)
+               return
+       }
+
+       content, err := markdown.RenderString(&markup.RenderContext{
+               URLPrefix: ctx.Query("context"),
+               Metas:     ctx.Repo.Repository.ComposeMetas(),
+       }, issue.Content)
+       if err != nil {
+               ctx.ServerError("RenderString", err)
+               return
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "content":     content,
+               "attachments": attachmentsHTML(ctx, issue.Attachments, issue.Content),
+       })
+}
+
+// UpdateIssueMilestone change issue's milestone
+func UpdateIssueMilestone(ctx *context.Context) {
+       issues := getActionIssues(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       milestoneID := ctx.QueryInt64("id")
+       for _, issue := range issues {
+               oldMilestoneID := issue.MilestoneID
+               if oldMilestoneID == milestoneID {
+                       continue
+               }
+               issue.MilestoneID = milestoneID
+               if err := issue_service.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil {
+                       ctx.ServerError("ChangeMilestoneAssign", err)
+                       return
+               }
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "ok": true,
+       })
+}
+
+// UpdateIssueAssignee change issue's or pull's assignee
+func UpdateIssueAssignee(ctx *context.Context) {
+       issues := getActionIssues(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       assigneeID := ctx.QueryInt64("id")
+       action := ctx.Query("action")
+
+       for _, issue := range issues {
+               switch action {
+               case "clear":
+                       if err := issue_service.DeleteNotPassedAssignee(issue, ctx.User, []*models.User{}); err != nil {
+                               ctx.ServerError("ClearAssignees", err)
+                               return
+                       }
+               default:
+                       assignee, err := models.GetUserByID(assigneeID)
+                       if err != nil {
+                               ctx.ServerError("GetUserByID", err)
+                               return
+                       }
+
+                       valid, err := models.CanBeAssigned(assignee, issue.Repo, issue.IsPull)
+                       if err != nil {
+                               ctx.ServerError("canBeAssigned", err)
+                               return
+                       }
+                       if !valid {
+                               ctx.ServerError("canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name})
+                               return
+                       }
+
+                       _, _, err = issue_service.ToggleAssignee(issue, ctx.User, assigneeID)
+                       if err != nil {
+                               ctx.ServerError("ToggleAssignee", err)
+                               return
+                       }
+               }
+       }
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "ok": true,
+       })
+}
+
+// UpdatePullReviewRequest add or remove review request
+func UpdatePullReviewRequest(ctx *context.Context) {
+       issues := getActionIssues(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       reviewID := ctx.QueryInt64("id")
+       action := ctx.Query("action")
+
+       // TODO: Not support 'clear' now
+       if action != "attach" && action != "detach" {
+               ctx.Status(403)
+               return
+       }
+
+       for _, issue := range issues {
+               if err := issue.LoadRepo(); err != nil {
+                       ctx.ServerError("issue.LoadRepo", err)
+                       return
+               }
+
+               if !issue.IsPull {
+                       log.Warn(
+                               "UpdatePullReviewRequest: refusing to add review request for non-PR issue %-v#%d",
+                               issue.Repo, issue.Index,
+                       )
+                       ctx.Status(403)
+                       return
+               }
+               if reviewID < 0 {
+                       // negative reviewIDs represent team requests
+                       if err := issue.Repo.GetOwner(); err != nil {
+                               ctx.ServerError("issue.Repo.GetOwner", err)
+                               return
+                       }
+
+                       if !issue.Repo.Owner.IsOrganization() {
+                               log.Warn(
+                                       "UpdatePullReviewRequest: refusing to add team review request for %s#%d owned by non organization UID[%d]",
+                                       issue.Repo.FullName(), issue.Index, issue.Repo.ID,
+                               )
+                               ctx.Status(403)
+                               return
+                       }
+
+                       team, err := models.GetTeamByID(-reviewID)
+                       if err != nil {
+                               ctx.ServerError("models.GetTeamByID", err)
+                               return
+                       }
+
+                       if team.OrgID != issue.Repo.OwnerID {
+                               log.Warn(
+                                       "UpdatePullReviewRequest: refusing to add team review request for UID[%d] team %s to %s#%d owned by UID[%d]",
+                                       team.OrgID, team.Name, issue.Repo.FullName(), issue.Index, issue.Repo.ID)
+                               ctx.Status(403)
+                               return
+                       }
+
+                       err = issue_service.IsValidTeamReviewRequest(team, ctx.User, action == "attach", issue)
+                       if err != nil {
+                               if models.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,
+                                               err,
+                                       )
+                                       ctx.Status(403)
+                                       return
+                               }
+                               ctx.ServerError("IsValidTeamReviewRequest", err)
+                               return
+                       }
+
+                       _, err = issue_service.TeamReviewRequest(issue, ctx.User, team, action == "attach")
+                       if err != nil {
+                               ctx.ServerError("TeamReviewRequest", err)
+                               return
+                       }
+                       continue
+               }
+
+               reviewer, err := models.GetUserByID(reviewID)
+               if err != nil {
+                       if models.IsErrUserNotExist(err) {
+                               log.Warn(
+                                       "UpdatePullReviewRequest: requested reviewer [%d] for %-v to %-v#%d is not exist: Error: %v",
+                                       reviewID, issue.Repo, issue.Index,
+                                       err,
+                               )
+                               ctx.Status(403)
+                               return
+                       }
+                       ctx.ServerError("GetUserByID", err)
+                       return
+               }
+
+               err = issue_service.IsValidReviewRequest(reviewer, ctx.User, action == "attach", issue, nil)
+               if err != nil {
+                       if models.IsErrNotValidReviewRequest(err) {
+                               log.Warn(
+                                       "UpdatePullReviewRequest: refusing to add invalid review request for %-v to %-v#%d: Error: %v",
+                                       reviewer, issue.Repo, issue.Index,
+                                       err,
+                               )
+                               ctx.Status(403)
+                               return
+                       }
+                       ctx.ServerError("isValidReviewRequest", err)
+                       return
+               }
+
+               _, err = issue_service.ReviewRequest(issue, ctx.User, reviewer, action == "attach")
+               if err != nil {
+                       ctx.ServerError("ReviewRequest", err)
+                       return
+               }
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "ok": true,
+       })
+}
+
+// UpdateIssueStatus change issue's status
+func UpdateIssueStatus(ctx *context.Context) {
+       issues := getActionIssues(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       var isClosed bool
+       switch action := ctx.Query("action"); action {
+       case "open":
+               isClosed = false
+       case "close":
+               isClosed = true
+       default:
+               log.Warn("Unrecognized action: %s", action)
+       }
+
+       if _, err := models.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.User, isClosed); err != nil {
+                               if models.IsErrDependenciesLeft(err) {
+                                       ctx.JSON(http.StatusPreconditionFailed, map[string]interface{}{
+                                               "error": "cannot close this issue because it still has open dependencies",
+                                       })
+                                       return
+                               }
+                               ctx.ServerError("ChangeStatus", err)
+                               return
+                       }
+               }
+       }
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "ok": true,
+       })
+}
+
+// NewComment create a comment for issue
+func NewComment(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.CreateCommentForm)
+       issue := GetActionIssue(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
+               if log.IsTrace() {
+                       if ctx.IsSigned {
+                               issueType := "issues"
+                               if issue.IsPull {
+                                       issueType = "pulls"
+                               }
+                               log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
+                                       "User in Repo has Permissions: %-+v",
+                                       ctx.User,
+                                       log.NewColoredIDValue(issue.PosterID),
+                                       issueType,
+                                       ctx.Repo.Repository,
+                                       ctx.Repo.Permission)
+                       } else {
+                               log.Trace("Permission Denied: Not logged in")
+                       }
+               }
+
+               ctx.Error(http.StatusForbidden)
+               return
+       }
+
+       if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.User.IsAdmin {
+               ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked"))
+               ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
+               return
+       }
+
+       var attachments []string
+       if setting.Attachment.Enabled {
+               attachments = form.Files
+       }
+
+       if ctx.HasError() {
+               ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
+               ctx.Redirect(issue.HTMLURL())
+               return
+       }
+
+       var comment *models.Comment
+       defer func() {
+               // Check if issue admin/poster changes the status of issue.
+               if (ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) || (ctx.IsSigned && issue.IsPoster(ctx.User.ID))) &&
+                       (form.Status == "reopen" || form.Status == "close") &&
+                       !(issue.IsPull && issue.PullRequest.HasMerged) {
+
+                       // Duplication and conflict check should apply to reopen pull request.
+                       var pr *models.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)
+                               if err != nil {
+                                       if !models.IsErrPullRequestNotExist(err) {
+                                               ctx.ServerError("GetUnmergedPullRequest", err)
+                                               return
+                                       }
+                               }
+
+                               // Regenerate patch and test conflict.
+                               if pr == nil {
+                                       pull_service.AddToTaskQueue(issue.PullRequest)
+                               }
+                       }
+
+                       if pr != nil {
+                               ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index))
+                       } else {
+                               isClosed := form.Status == "close"
+                               if err := issue_service.ChangeStatus(issue, ctx.User, isClosed); err != nil {
+                                       log.Error("ChangeStatus: %v", err)
+
+                                       if models.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), http.StatusSeeOther)
+                                               } else {
+                                                       ctx.Flash.Error(ctx.Tr("repo.issues.dependency.issue_close_blocked"))
+                                                       ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index), http.StatusSeeOther)
+                                               }
+                                               return
+                                       }
+                               } else {
+                                       if err := stopTimerIfAvailable(ctx.User, issue); err != nil {
+                                               ctx.ServerError("CreateOrStopIssueStopwatch", err)
+                                               return
+                                       }
+
+                                       log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed)
+                               }
+                       }
+               }
+
+               // Redirect to comment hashtag if there is any actual content.
+               typeName := "issues"
+               if issue.IsPull {
+                       typeName = "pulls"
+               }
+               if comment != nil {
+                       ctx.Redirect(fmt.Sprintf("%s/%s/%d#%s", ctx.Repo.RepoLink, typeName, issue.Index, comment.HashTag()))
+               } else {
+                       ctx.Redirect(fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, typeName, issue.Index))
+               }
+       }()
+
+       // Fix #321: Allow empty comments, as long as we have attachments.
+       if len(form.Content) == 0 && len(attachments) == 0 {
+               return
+       }
+
+       comment, err := comment_service.CreateIssueComment(ctx.User, ctx.Repo.Repository, issue, form.Content, attachments)
+       if err != nil {
+               ctx.ServerError("CreateIssueComment", err)
+               return
+       }
+
+       log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
+}
+
+// UpdateCommentContent change comment of issue's content
+func UpdateCommentContent(ctx *context.Context) {
+       comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
+       if err != nil {
+               ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
+               return
+       }
+
+       if err := comment.LoadIssue(); err != nil {
+               ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
+               return
+       }
+
+       if comment.Type == models.CommentTypeComment {
+               if err := comment.LoadAttachments(); err != nil {
+                       ctx.ServerError("LoadAttachments", err)
+                       return
+               }
+       }
+
+       if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
+               ctx.Error(http.StatusForbidden)
+               return
+       } else if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode {
+               ctx.Error(http.StatusNoContent)
+               return
+       }
+
+       oldContent := comment.Content
+       comment.Content = ctx.Query("content")
+       if len(comment.Content) == 0 {
+               ctx.JSON(http.StatusOK, map[string]interface{}{
+                       "content": "",
+               })
+               return
+       }
+       if err = comment_service.UpdateComment(comment, ctx.User, oldContent); err != nil {
+               ctx.ServerError("UpdateComment", err)
+               return
+       }
+
+       files := ctx.QueryStrings("files[]")
+       if err := updateAttachments(comment, files); err != nil {
+               ctx.ServerError("UpdateAttachments", err)
+               return
+       }
+
+       content, err := markdown.RenderString(&markup.RenderContext{
+               URLPrefix: ctx.Query("context"),
+               Metas:     ctx.Repo.Repository.ComposeMetas(),
+       }, comment.Content)
+       if err != nil {
+               ctx.ServerError("RenderString", err)
+               return
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "content":     content,
+               "attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content),
+       })
+}
+
+// DeleteComment delete comment of issue
+func DeleteComment(ctx *context.Context) {
+       comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
+       if err != nil {
+               ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
+               return
+       }
+
+       if err := comment.LoadIssue(); err != nil {
+               ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
+               return
+       }
+
+       if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
+               ctx.Error(http.StatusForbidden)
+               return
+       } else if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode {
+               ctx.Error(http.StatusNoContent)
+               return
+       }
+
+       if err = comment_service.DeleteComment(ctx.User, comment); err != nil {
+               ctx.ServerError("DeleteCommentByID", err)
+               return
+       }
+
+       ctx.Status(200)
+}
+
+// ChangeIssueReaction create a reaction for issue
+func ChangeIssueReaction(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.ReactionForm)
+       issue := GetActionIssue(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
+               if log.IsTrace() {
+                       if ctx.IsSigned {
+                               issueType := "issues"
+                               if issue.IsPull {
+                                       issueType = "pulls"
+                               }
+                               log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
+                                       "User in Repo has Permissions: %-+v",
+                                       ctx.User,
+                                       log.NewColoredIDValue(issue.PosterID),
+                                       issueType,
+                                       ctx.Repo.Repository,
+                                       ctx.Repo.Permission)
+                       } else {
+                               log.Trace("Permission Denied: Not logged in")
+                       }
+               }
+
+               ctx.Error(http.StatusForbidden)
+               return
+       }
+
+       if ctx.HasError() {
+               ctx.ServerError("ChangeIssueReaction", errors.New(ctx.GetErrMsg()))
+               return
+       }
+
+       switch ctx.Params(":action") {
+       case "react":
+               reaction, err := models.CreateIssueReaction(ctx.User, issue, form.Content)
+               if err != nil {
+                       if models.IsErrForbiddenIssueReaction(err) {
+                               ctx.ServerError("ChangeIssueReaction", err)
+                               return
+                       }
+                       log.Info("CreateIssueReaction: %s", err)
+                       break
+               }
+               // Reload new reactions
+               issue.Reactions = nil
+               if err = issue.LoadAttributes(); err != nil {
+                       log.Info("issue.LoadAttributes: %s", err)
+                       break
+               }
+
+               log.Trace("Reaction for issue created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, reaction.ID)
+       case "unreact":
+               if err := models.DeleteIssueReaction(ctx.User, issue, form.Content); err != nil {
+                       ctx.ServerError("DeleteIssueReaction", err)
+                       return
+               }
+
+               // Reload new reactions
+               issue.Reactions = nil
+               if err := issue.LoadAttributes(); err != nil {
+                       log.Info("issue.LoadAttributes: %s", err)
+                       break
+               }
+
+               log.Trace("Reaction for issue removed: %d/%d", ctx.Repo.Repository.ID, issue.ID)
+       default:
+               ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.Params(":action")), nil)
+               return
+       }
+
+       if len(issue.Reactions) == 0 {
+               ctx.JSON(http.StatusOK, map[string]interface{}{
+                       "empty": true,
+                       "html":  "",
+               })
+               return
+       }
+
+       html, err := ctx.HTMLString(string(tplReactions), map[string]interface{}{
+               "ctx":       ctx.Data,
+               "ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index),
+               "Reactions": issue.Reactions.GroupByType(),
+       })
+       if err != nil {
+               ctx.ServerError("ChangeIssueReaction.HTMLString", err)
+               return
+       }
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "html": html,
+       })
+}
+
+// ChangeCommentReaction create a reaction for comment
+func ChangeCommentReaction(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.ReactionForm)
+       comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
+       if err != nil {
+               ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
+               return
+       }
+
+       if err := comment.LoadIssue(); err != nil {
+               ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
+               return
+       }
+
+       if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull)) {
+               if log.IsTrace() {
+                       if ctx.IsSigned {
+                               issueType := "issues"
+                               if comment.Issue.IsPull {
+                                       issueType = "pulls"
+                               }
+                               log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
+                                       "User in Repo has Permissions: %-+v",
+                                       ctx.User,
+                                       log.NewColoredIDValue(comment.Issue.PosterID),
+                                       issueType,
+                                       ctx.Repo.Repository,
+                                       ctx.Repo.Permission)
+                       } else {
+                               log.Trace("Permission Denied: Not logged in")
+                       }
+               }
+
+               ctx.Error(http.StatusForbidden)
+               return
+       } else if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode {
+               ctx.Error(http.StatusNoContent)
+               return
+       }
+
+       switch ctx.Params(":action") {
+       case "react":
+               reaction, err := models.CreateCommentReaction(ctx.User, comment.Issue, comment, form.Content)
+               if err != nil {
+                       if models.IsErrForbiddenIssueReaction(err) {
+                               ctx.ServerError("ChangeIssueReaction", err)
+                               return
+                       }
+                       log.Info("CreateCommentReaction: %s", err)
+                       break
+               }
+               // Reload new reactions
+               comment.Reactions = nil
+               if err = comment.LoadReactions(ctx.Repo.Repository); err != nil {
+                       log.Info("comment.LoadReactions: %s", err)
+                       break
+               }
+
+               log.Trace("Reaction for comment created: %d/%d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID, reaction.ID)
+       case "unreact":
+               if err := models.DeleteCommentReaction(ctx.User, comment.Issue, comment, form.Content); err != nil {
+                       ctx.ServerError("DeleteCommentReaction", err)
+                       return
+               }
+
+               // Reload new reactions
+               comment.Reactions = nil
+               if err = comment.LoadReactions(ctx.Repo.Repository); err != nil {
+                       log.Info("comment.LoadReactions: %s", err)
+                       break
+               }
+
+               log.Trace("Reaction for comment removed: %d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID)
+       default:
+               ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.Params(":action")), nil)
+               return
+       }
+
+       if len(comment.Reactions) == 0 {
+               ctx.JSON(http.StatusOK, map[string]interface{}{
+                       "empty": true,
+                       "html":  "",
+               })
+               return
+       }
+
+       html, err := ctx.HTMLString(string(tplReactions), map[string]interface{}{
+               "ctx":       ctx.Data,
+               "ActionURL": fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID),
+               "Reactions": comment.Reactions.GroupByType(),
+       })
+       if err != nil {
+               ctx.ServerError("ChangeCommentReaction.HTMLString", err)
+               return
+       }
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "html": html,
+       })
+}
+
+func addParticipant(poster *models.User, participants []*models.User) []*models.User {
+       for _, part := range participants {
+               if poster.ID == part.ID {
+                       return participants
+               }
+       }
+       return append(participants, poster)
+}
+
+func filterXRefComments(ctx *context.Context, issue *models.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 {
+                       var err error
+                       // Set RefRepo for description in template
+                       c.RefRepo, err = models.GetRepositoryByID(c.RefRepoID)
+                       if err != nil {
+                               return err
+                       }
+                       perm, err := models.GetUserRepoPermission(c.RefRepo, ctx.User)
+                       if err != nil {
+                               return err
+                       }
+                       if !perm.CanReadIssuesOrPulls(c.RefIsPull) {
+                               issue.Comments = append(issue.Comments[:i], issue.Comments[i+1:]...)
+                               continue
+                       }
+               }
+               i++
+       }
+       return nil
+}
+
+// GetIssueAttachments returns attachments for the issue
+func GetIssueAttachments(ctx *context.Context) {
+       issue := GetActionIssue(ctx)
+       var attachments = make([]*api.Attachment, len(issue.Attachments))
+       for i := 0; i < len(issue.Attachments); i++ {
+               attachments[i] = convert.ToReleaseAttachment(issue.Attachments[i])
+       }
+       ctx.JSON(http.StatusOK, attachments)
+}
+
+// GetCommentAttachments returns attachments for the comment
+func GetCommentAttachments(ctx *context.Context) {
+       comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
+       if err != nil {
+               ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
+               return
+       }
+       var attachments = make([]*api.Attachment, 0)
+       if comment.Type == models.CommentTypeComment {
+               if err := comment.LoadAttachments(); err != nil {
+                       ctx.ServerError("LoadAttachments", err)
+                       return
+               }
+               for i := 0; i < len(comment.Attachments); i++ {
+                       attachments = append(attachments, convert.ToReleaseAttachment(comment.Attachments[i]))
+               }
+       }
+       ctx.JSON(http.StatusOK, attachments)
+}
+
+func updateAttachments(item interface{}, files []string) error {
+       var attachments []*models.Attachment
+       switch content := item.(type) {
+       case *models.Issue:
+               attachments = content.Attachments
+       case *models.Comment:
+               attachments = content.Attachments
+       default:
+               return fmt.Errorf("Unknown Type: %T", content)
+       }
+       for i := 0; i < len(attachments); i++ {
+               if util.IsStringInSlice(attachments[i].UUID, files) {
+                       continue
+               }
+               if err := models.DeleteAttachment(attachments[i], true); err != nil {
+                       return err
+               }
+       }
+       var err error
+       if len(files) > 0 {
+               switch content := item.(type) {
+               case *models.Issue:
+                       err = content.UpdateAttachments(files)
+               case *models.Comment:
+                       err = content.UpdateAttachments(files)
+               default:
+                       return fmt.Errorf("Unknown Type: %T", content)
+               }
+               if err != nil {
+                       return err
+               }
+       }
+       switch content := item.(type) {
+       case *models.Issue:
+               content.Attachments, err = models.GetAttachmentsByIssueID(content.ID)
+       case *models.Comment:
+               content.Attachments, err = models.GetAttachmentsByCommentID(content.ID)
+       default:
+               return fmt.Errorf("Unknown Type: %T", content)
+       }
+       return err
+}
+
+func attachmentsHTML(ctx *context.Context, attachments []*models.Attachment, content string) string {
+       attachHTML, err := ctx.HTMLString(string(tplAttachment), map[string]interface{}{
+               "ctx":         ctx.Data,
+               "Attachments": attachments,
+               "Content":     content,
+       })
+       if err != nil {
+               ctx.ServerError("attachmentsHTML.HTMLString", err)
+               return ""
+       }
+       return attachHTML
+}
+
+// combineLabelComments combine the nearby label comments as one.
+func combineLabelComments(issue *models.Issue) {
+       var prev, cur *models.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 ||
+                       (prev != nil && prev.PosterID != cur.PosterID) ||
+                       (prev != nil && cur.CreatedUnix-prev.CreatedUnix >= 60) {
+                       if cur.Type == models.CommentTypeLabel && cur.Label != nil {
+                               if cur.Content != "1" {
+                                       cur.RemovedLabels = append(cur.RemovedLabels, cur.Label)
+                               } else {
+                                       cur.AddedLabels = append(cur.AddedLabels, cur.Label)
+                               }
+                       }
+                       continue
+               }
+
+               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 cur.Content != "1" {
+                                       prev.RemovedLabels = append(prev.RemovedLabels, cur.Label)
+                               } else {
+                                       prev.AddedLabels = append(prev.AddedLabels, cur.Label)
+                               }
+                               prev.CreatedUnix = cur.CreatedUnix
+                               // remove the current comment since it has been combined to prev comment
+                               issue.Comments = append(issue.Comments[:i], issue.Comments[i+1:]...)
+                               i--
+                       } else { // if prev is not a label comment, start a new group
+                               if cur.Content != "1" {
+                                       cur.RemovedLabels = append(cur.RemovedLabels, cur.Label)
+                               } else {
+                                       cur.AddedLabels = append(cur.AddedLabels, cur.Label)
+                               }
+                       }
+               }
+       }
+}
+
+// get all teams that current user can mention
+func handleTeamMentions(ctx *context.Context) {
+       if ctx.User == nil || !ctx.Repo.Owner.IsOrganization() {
+               return
+       }
+
+       isAdmin := false
+       var err error
+       // Admin has super access.
+       if ctx.User.IsAdmin {
+               isAdmin = true
+       } else {
+               isAdmin, err = ctx.Repo.Owner.IsOwnedBy(ctx.User.ID)
+               if err != nil {
+                       ctx.ServerError("IsOwnedBy", err)
+                       return
+               }
+       }
+
+       if isAdmin {
+               if err := ctx.Repo.Owner.GetTeams(&models.SearchTeamOptions{}); err != nil {
+                       ctx.ServerError("GetTeams", err)
+                       return
+               }
+       } else {
+               ctx.Repo.Owner.Teams, err = ctx.Repo.Owner.GetUserTeams(ctx.User.ID)
+               if err != nil {
+                       ctx.ServerError("GetUserTeams", err)
+                       return
+               }
+       }
+
+       ctx.Data["MentionableTeams"] = ctx.Repo.Owner.Teams
+       ctx.Data["MentionableTeamsOrg"] = ctx.Repo.Owner.Name
+       ctx.Data["MentionableTeamsOrgAvatar"] = ctx.Repo.Owner.RelAvatarLink()
+}
diff --git a/routers/web/repo/issue_dependency.go b/routers/web/repo/issue_dependency.go
new file mode 100644 (file)
index 0000000..8a83c7b
--- /dev/null
@@ -0,0 +1,129 @@
+// Copyright 2018 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 repo
+
+import (
+       "net/http"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/setting"
+)
+
+// AddDependency adds new dependencies
+func AddDependency(ctx *context.Context) {
+       issueIndex := ctx.ParamsInt64("index")
+       issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex)
+       if err != nil {
+               ctx.ServerError("GetIssueByIndex", err)
+               return
+       }
+
+       // Check if the Repo is allowed to have dependencies
+       if !ctx.Repo.CanCreateIssueDependencies(ctx.User, issue.IsPull) {
+               ctx.Error(http.StatusForbidden, "CanCreateIssueDependencies")
+               return
+       }
+
+       depID := ctx.QueryInt64("newDependency")
+
+       if err = issue.LoadRepo(); err != nil {
+               ctx.ServerError("LoadRepo", err)
+               return
+       }
+
+       // Redirect
+       defer ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
+
+       // Dependency
+       dep, err := models.GetIssueByID(depID)
+       if err != nil {
+               ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_issue_not_exist"))
+               return
+       }
+
+       // Check if both issues are in the same repo if cross repository dependencies is not enabled
+       if issue.RepoID != dep.RepoID && !setting.Service.AllowCrossRepositoryDependencies {
+               ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_not_same_repo"))
+               return
+       }
+
+       // Check if issue and dependency is the same
+       if dep.ID == issue.ID {
+               ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_same_issue"))
+               return
+       }
+
+       err = models.CreateIssueDependency(ctx.User, issue, dep)
+       if err != nil {
+               if models.IsErrDependencyExists(err) {
+                       ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_exists"))
+                       return
+               } else if models.IsErrCircularDependency(err) {
+                       ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_cannot_create_circular"))
+                       return
+               } else {
+                       ctx.ServerError("CreateOrUpdateIssueDependency", err)
+                       return
+               }
+       }
+}
+
+// RemoveDependency removes the dependency
+func RemoveDependency(ctx *context.Context) {
+       issueIndex := ctx.ParamsInt64("index")
+       issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex)
+       if err != nil {
+               ctx.ServerError("GetIssueByIndex", err)
+               return
+       }
+
+       // Check if the Repo is allowed to have dependencies
+       if !ctx.Repo.CanCreateIssueDependencies(ctx.User, issue.IsPull) {
+               ctx.Error(http.StatusForbidden, "CanCreateIssueDependencies")
+               return
+       }
+
+       depID := ctx.QueryInt64("removeDependencyID")
+
+       if err = issue.LoadRepo(); err != nil {
+               ctx.ServerError("LoadRepo", err)
+               return
+       }
+
+       // Dependency Type
+       depTypeStr := ctx.Req.PostForm.Get("dependencyType")
+
+       var depType models.DependencyType
+
+       switch depTypeStr {
+       case "blockedBy":
+               depType = models.DependencyTypeBlockedBy
+       case "blocking":
+               depType = models.DependencyTypeBlocking
+       default:
+               ctx.Error(http.StatusBadRequest, "GetDependecyType")
+               return
+       }
+
+       // Dependency
+       dep, err := models.GetIssueByID(depID)
+       if err != nil {
+               ctx.ServerError("GetIssueByID", err)
+               return
+       }
+
+       if err = models.RemoveIssueDependency(ctx.User, issue, dep, depType); err != nil {
+               if models.IsErrDependencyNotExists(err) {
+                       ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_not_exist"))
+                       return
+               }
+               ctx.ServerError("RemoveIssueDependency", err)
+               return
+       }
+
+       // Redirect
+       ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
+}
diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go
new file mode 100644 (file)
index 0000000..7361260
--- /dev/null
@@ -0,0 +1,222 @@
+// 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 repo
+
+import (
+       "net/http"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+       issue_service "code.gitea.io/gitea/services/issue"
+)
+
+const (
+       tplLabels base.TplName = "repo/issue/labels"
+)
+
+// Labels render issue's labels page
+func Labels(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.labels")
+       ctx.Data["PageIsIssueList"] = true
+       ctx.Data["PageIsLabels"] = true
+       ctx.Data["RequireTribute"] = true
+       ctx.Data["LabelTemplates"] = models.LabelTemplates
+       ctx.HTML(http.StatusOK, tplLabels)
+}
+
+// InitializeLabels init labels for a repository
+func InitializeLabels(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.InitializeLabelsForm)
+       if ctx.HasError() {
+               ctx.Redirect(ctx.Repo.RepoLink + "/labels")
+               return
+       }
+
+       if err := models.InitializeLabels(models.DefaultDBContext(), ctx.Repo.Repository.ID, form.TemplateName, false); err != nil {
+               if models.IsErrIssueLabelTemplateLoad(err) {
+                       originalErr := err.(models.ErrIssueLabelTemplateLoad).OriginalError
+                       ctx.Flash.Error(ctx.Tr("repo.issues.label_templates.fail_to_load_file", form.TemplateName, originalErr))
+                       ctx.Redirect(ctx.Repo.RepoLink + "/labels")
+                       return
+               }
+               ctx.ServerError("InitializeLabels", err)
+               return
+       }
+       ctx.Redirect(ctx.Repo.RepoLink + "/labels")
+}
+
+// RetrieveLabels find all the labels of a repository and organization
+func RetrieveLabels(ctx *context.Context) {
+       labels, err := models.GetLabelsByRepoID(ctx.Repo.Repository.ID, ctx.Query("sort"), models.ListOptions{})
+       if err != nil {
+               ctx.ServerError("RetrieveLabels.GetLabels", err)
+               return
+       }
+
+       for _, l := range labels {
+               l.CalOpenIssues()
+       }
+
+       ctx.Data["Labels"] = labels
+
+       if ctx.Repo.Owner.IsOrganization() {
+               orgLabels, err := models.GetLabelsByOrgID(ctx.Repo.Owner.ID, ctx.Query("sort"), models.ListOptions{})
+               if err != nil {
+                       ctx.ServerError("GetLabelsByOrgID", err)
+                       return
+               }
+               for _, l := range orgLabels {
+                       l.CalOpenOrgIssues(ctx.Repo.Repository.ID, l.ID)
+               }
+               ctx.Data["OrgLabels"] = orgLabels
+
+               org, err := models.GetOrgByName(ctx.Repo.Owner.LowerName)
+               if err != nil {
+                       ctx.ServerError("GetOrgByName", err)
+                       return
+               }
+               if ctx.User != nil {
+                       ctx.Org.IsOwner, err = org.IsOwnedBy(ctx.User.ID)
+                       if err != nil {
+                               ctx.ServerError("org.IsOwnedBy", err)
+                               return
+                       }
+                       ctx.Org.OrgLink = org.OrganisationLink()
+                       ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner
+                       ctx.Data["OrganizationLink"] = ctx.Org.OrgLink
+               }
+       }
+       ctx.Data["NumLabels"] = len(labels)
+       ctx.Data["SortType"] = ctx.Query("sort")
+}
+
+// NewLabel create new label for repository
+func NewLabel(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.CreateLabelForm)
+       ctx.Data["Title"] = ctx.Tr("repo.labels")
+       ctx.Data["PageIsLabels"] = true
+
+       if ctx.HasError() {
+               ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
+               ctx.Redirect(ctx.Repo.RepoLink + "/labels")
+               return
+       }
+
+       l := &models.Label{
+               RepoID:      ctx.Repo.Repository.ID,
+               Name:        form.Title,
+               Description: form.Description,
+               Color:       form.Color,
+       }
+       if err := models.NewLabel(l); err != nil {
+               ctx.ServerError("NewLabel", err)
+               return
+       }
+       ctx.Redirect(ctx.Repo.RepoLink + "/labels")
+}
+
+// UpdateLabel update a label's name and color
+func UpdateLabel(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.CreateLabelForm)
+       l, err := models.GetLabelInRepoByID(ctx.Repo.Repository.ID, form.ID)
+       if err != nil {
+               switch {
+               case models.IsErrRepoLabelNotExist(err):
+                       ctx.Error(http.StatusNotFound)
+               default:
+                       ctx.ServerError("UpdateLabel", err)
+               }
+               return
+       }
+
+       l.Name = form.Title
+       l.Description = form.Description
+       l.Color = form.Color
+       if err := models.UpdateLabel(l); err != nil {
+               ctx.ServerError("UpdateLabel", err)
+               return
+       }
+       ctx.Redirect(ctx.Repo.RepoLink + "/labels")
+}
+
+// DeleteLabel delete a label
+func DeleteLabel(ctx *context.Context) {
+       if err := models.DeleteLabel(ctx.Repo.Repository.ID, ctx.QueryInt64("id")); err != nil {
+               ctx.Flash.Error("DeleteLabel: " + err.Error())
+       } else {
+               ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": ctx.Repo.RepoLink + "/labels",
+       })
+}
+
+// UpdateIssueLabel change issue's labels
+func UpdateIssueLabel(ctx *context.Context) {
+       issues := getActionIssues(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       switch action := ctx.Query("action"); action {
+       case "clear":
+               for _, issue := range issues {
+                       if err := issue_service.ClearLabels(issue, ctx.User); err != nil {
+                               ctx.ServerError("ClearLabels", err)
+                               return
+                       }
+               }
+       case "attach", "detach", "toggle":
+               label, err := models.GetLabelByID(ctx.QueryInt64("id"))
+               if err != nil {
+                       if models.IsErrRepoLabelNotExist(err) {
+                               ctx.Error(http.StatusNotFound, "GetLabelByID")
+                       } else {
+                               ctx.ServerError("GetLabelByID", err)
+                       }
+                       return
+               }
+
+               if action == "toggle" {
+                       // detach if any issues already have label, otherwise attach
+                       action = "attach"
+                       for _, issue := range issues {
+                               if issue.HasLabel(label.ID) {
+                                       action = "detach"
+                                       break
+                               }
+                       }
+               }
+
+               if action == "attach" {
+                       for _, issue := range issues {
+                               if err = issue_service.AddLabel(issue, ctx.User, label); err != nil {
+                                       ctx.ServerError("AddLabel", err)
+                                       return
+                               }
+                       }
+               } else {
+                       for _, issue := range issues {
+                               if err = issue_service.RemoveLabel(issue, ctx.User, label); err != nil {
+                                       ctx.ServerError("RemoveLabel", err)
+                                       return
+                               }
+                       }
+               }
+       default:
+               log.Warn("Unrecognized action: %s", action)
+               ctx.Error(http.StatusInternalServerError)
+               return
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "ok": true,
+       })
+}
diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go
new file mode 100644 (file)
index 0000000..bf9e72a
--- /dev/null
@@ -0,0 +1,168 @@
+// 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 repo
+
+import (
+       "net/http"
+       "strconv"
+       "testing"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/test"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func int64SliceToCommaSeparated(a []int64) string {
+       s := ""
+       for i, n := range a {
+               if i > 0 {
+                       s += ","
+               }
+               s += strconv.Itoa(int(n))
+       }
+       return s
+}
+
+func TestInitializeLabels(t *testing.T) {
+       models.PrepareTestEnv(t)
+       ctx := test.MockContext(t, "user2/repo1/labels/initialize")
+       test.LoadUser(t, ctx, 2)
+       test.LoadRepo(t, ctx, 2)
+       web.SetForm(ctx, &forms.InitializeLabelsForm{TemplateName: "Default"})
+       InitializeLabels(ctx)
+       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
+       models.AssertExistsAndLoadBean(t, &models.Label{
+               RepoID: 2,
+               Name:   "enhancement",
+               Color:  "#84b6eb",
+       })
+       assert.Equal(t, "/user2/repo2/labels", test.RedirectURL(ctx.Resp))
+}
+
+func TestRetrieveLabels(t *testing.T) {
+       models.PrepareTestEnv(t)
+       for _, testCase := range []struct {
+               RepoID           int64
+               Sort             string
+               ExpectedLabelIDs []int64
+       }{
+               {1, "", []int64{1, 2}},
+               {1, "leastissues", []int64{2, 1}},
+               {2, "", []int64{}},
+       } {
+               ctx := test.MockContext(t, "user/repo/issues")
+               test.LoadUser(t, ctx, 2)
+               test.LoadRepo(t, ctx, testCase.RepoID)
+               ctx.Req.Form.Set("sort", testCase.Sort)
+               RetrieveLabels(ctx)
+               assert.False(t, ctx.Written())
+               labels, ok := ctx.Data["Labels"].([]*models.Label)
+               assert.True(t, ok)
+               if assert.Len(t, labels, len(testCase.ExpectedLabelIDs)) {
+                       for i, label := range labels {
+                               assert.EqualValues(t, testCase.ExpectedLabelIDs[i], label.ID)
+                       }
+               }
+       }
+}
+
+func TestNewLabel(t *testing.T) {
+       models.PrepareTestEnv(t)
+       ctx := test.MockContext(t, "user2/repo1/labels/edit")
+       test.LoadUser(t, ctx, 2)
+       test.LoadRepo(t, ctx, 1)
+       web.SetForm(ctx, &forms.CreateLabelForm{
+               Title: "newlabel",
+               Color: "#abcdef",
+       })
+       NewLabel(ctx)
+       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
+       models.AssertExistsAndLoadBean(t, &models.Label{
+               Name:  "newlabel",
+               Color: "#abcdef",
+       })
+       assert.Equal(t, "/user2/repo1/labels", test.RedirectURL(ctx.Resp))
+}
+
+func TestUpdateLabel(t *testing.T) {
+       models.PrepareTestEnv(t)
+       ctx := test.MockContext(t, "user2/repo1/labels/edit")
+       test.LoadUser(t, ctx, 2)
+       test.LoadRepo(t, ctx, 1)
+       web.SetForm(ctx, &forms.CreateLabelForm{
+               ID:    2,
+               Title: "newnameforlabel",
+               Color: "#abcdef",
+       })
+       UpdateLabel(ctx)
+       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
+       models.AssertExistsAndLoadBean(t, &models.Label{
+               ID:    2,
+               Name:  "newnameforlabel",
+               Color: "#abcdef",
+       })
+       assert.Equal(t, "/user2/repo1/labels", test.RedirectURL(ctx.Resp))
+}
+
+func TestDeleteLabel(t *testing.T) {
+       models.PrepareTestEnv(t)
+       ctx := test.MockContext(t, "user2/repo1/labels/delete")
+       test.LoadUser(t, ctx, 2)
+       test.LoadRepo(t, ctx, 1)
+       ctx.Req.Form.Set("id", "2")
+       DeleteLabel(ctx)
+       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+       models.AssertNotExistsBean(t, &models.Label{ID: 2})
+       models.AssertNotExistsBean(t, &models.IssueLabel{LabelID: 2})
+       assert.Equal(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg)
+}
+
+func TestUpdateIssueLabel_Clear(t *testing.T) {
+       models.PrepareTestEnv(t)
+       ctx := test.MockContext(t, "user2/repo1/issues/labels")
+       test.LoadUser(t, ctx, 2)
+       test.LoadRepo(t, ctx, 1)
+       ctx.Req.Form.Set("issue_ids", "1,3")
+       ctx.Req.Form.Set("action", "clear")
+       UpdateIssueLabel(ctx)
+       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+       models.AssertNotExistsBean(t, &models.IssueLabel{IssueID: 1})
+       models.AssertNotExistsBean(t, &models.IssueLabel{IssueID: 3})
+       models.CheckConsistencyFor(t, &models.Label{})
+}
+
+func TestUpdateIssueLabel_Toggle(t *testing.T) {
+       for _, testCase := range []struct {
+               Action      string
+               IssueIDs    []int64
+               LabelID     int64
+               ExpectedAdd bool // whether we expect the label to be added to the issues
+       }{
+               {"attach", []int64{1, 3}, 1, true},
+               {"detach", []int64{1, 3}, 1, false},
+               {"toggle", []int64{1, 3}, 1, false},
+               {"toggle", []int64{1, 2}, 2, true},
+       } {
+               models.PrepareTestEnv(t)
+               ctx := test.MockContext(t, "user2/repo1/issues/labels")
+               test.LoadUser(t, ctx, 2)
+               test.LoadRepo(t, ctx, 1)
+               ctx.Req.Form.Set("issue_ids", int64SliceToCommaSeparated(testCase.IssueIDs))
+               ctx.Req.Form.Set("action", testCase.Action)
+               ctx.Req.Form.Set("id", strconv.Itoa(int(testCase.LabelID)))
+               UpdateIssueLabel(ctx)
+               assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+               for _, issueID := range testCase.IssueIDs {
+                       models.AssertExistsIf(t, testCase.ExpectedAdd, &models.IssueLabel{
+                               IssueID: issueID,
+                               LabelID: testCase.LabelID,
+                       })
+               }
+               models.CheckConsistencyFor(t, &models.Label{})
+       }
+}
diff --git a/routers/web/repo/issue_lock.go b/routers/web/repo/issue_lock.go
new file mode 100644 (file)
index 0000000..36894b4
--- /dev/null
@@ -0,0 +1,72 @@
+// Copyright 2019 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 repo
+
+import (
+       "net/http"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+)
+
+// LockIssue locks an issue. This would limit commenting abilities to
+// users with write access to the repo.
+func LockIssue(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.IssueLockForm)
+       issue := GetActionIssue(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       if issue.IsLocked {
+               ctx.Flash.Error(ctx.Tr("repo.issues.lock_duplicate"))
+               ctx.Redirect(issue.HTMLURL())
+               return
+       }
+
+       if !form.HasValidReason() {
+               ctx.Flash.Error(ctx.Tr("repo.issues.lock.unknown_reason"))
+               ctx.Redirect(issue.HTMLURL())
+               return
+       }
+
+       if err := models.LockIssue(&models.IssueLockOptions{
+               Doer:   ctx.User,
+               Issue:  issue,
+               Reason: form.Reason,
+       }); err != nil {
+               ctx.ServerError("LockIssue", err)
+               return
+       }
+
+       ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
+}
+
+// UnlockIssue unlocks a previously locked issue.
+func UnlockIssue(ctx *context.Context) {
+
+       issue := GetActionIssue(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       if !issue.IsLocked {
+               ctx.Flash.Error(ctx.Tr("repo.issues.unlock_error"))
+               ctx.Redirect(issue.HTMLURL())
+               return
+       }
+
+       if err := models.UnlockIssue(&models.IssueLockOptions{
+               Doer:  ctx.User,
+               Issue: issue,
+       }); err != nil {
+               ctx.ServerError("UnlockIssue", err)
+               return
+       }
+
+       ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
+}
diff --git a/routers/web/repo/issue_stopwatch.go b/routers/web/repo/issue_stopwatch.go
new file mode 100644 (file)
index 0000000..b8efb3b
--- /dev/null
@@ -0,0 +1,108 @@
+// 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 repo
+
+import (
+       "net/http"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/context"
+)
+
+// IssueStopwatch creates or stops a stopwatch for the given issue.
+func IssueStopwatch(c *context.Context) {
+       issue := GetActionIssue(c)
+       if c.Written() {
+               return
+       }
+
+       var showSuccessMessage bool
+
+       if !models.StopwatchExists(c.User.ID, issue.ID) {
+               showSuccessMessage = true
+       }
+
+       if !c.Repo.CanUseTimetracker(issue, c.User) {
+               c.NotFound("CanUseTimetracker", nil)
+               return
+       }
+
+       if err := models.CreateOrStopIssueStopwatch(c.User, issue); err != nil {
+               c.ServerError("CreateOrStopIssueStopwatch", err)
+               return
+       }
+
+       if showSuccessMessage {
+               c.Flash.Success(c.Tr("repo.issues.tracker_auto_close"))
+       }
+
+       url := issue.HTMLURL()
+       c.Redirect(url, http.StatusSeeOther)
+}
+
+// CancelStopwatch cancel the stopwatch
+func CancelStopwatch(c *context.Context) {
+       issue := GetActionIssue(c)
+       if c.Written() {
+               return
+       }
+       if !c.Repo.CanUseTimetracker(issue, c.User) {
+               c.NotFound("CanUseTimetracker", nil)
+               return
+       }
+
+       if err := models.CancelStopwatch(c.User, issue); err != nil {
+               c.ServerError("CancelStopwatch", err)
+               return
+       }
+
+       url := issue.HTMLURL()
+       c.Redirect(url, http.StatusSeeOther)
+}
+
+// GetActiveStopwatch is the middleware that sets .ActiveStopwatch on context
+func GetActiveStopwatch(c *context.Context) {
+       if strings.HasPrefix(c.Req.URL.Path, "/api") {
+               return
+       }
+
+       if !c.IsSigned {
+               return
+       }
+
+       _, sw, err := models.HasUserStopwatch(c.User.ID)
+       if err != nil {
+               c.ServerError("HasUserStopwatch", err)
+               return
+       }
+
+       if sw == nil || sw.ID == 0 {
+               return
+       }
+
+       issue, err := models.GetIssueByID(sw.IssueID)
+       if err != nil || issue == nil {
+               c.ServerError("GetIssueByID", err)
+               return
+       }
+       if err = issue.LoadRepo(); err != nil {
+               c.ServerError("LoadRepo", err)
+               return
+       }
+
+       c.Data["ActiveStopwatch"] = StopwatchTmplInfo{
+               issue.Repo.FullName(),
+               issue.Index,
+               sw.Seconds() + 1, // ensure time is never zero in ui
+       }
+}
+
+// StopwatchTmplInfo is a view on a stopwatch specifically for template rendering
+type StopwatchTmplInfo struct {
+       RepoSlug   string
+       IssueIndex int64
+       Seconds    int64
+}
diff --git a/routers/web/repo/issue_test.go b/routers/web/repo/issue_test.go
new file mode 100644 (file)
index 0000000..7fb837f
--- /dev/null
@@ -0,0 +1,324 @@
+// 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 repo
+
+import (
+       "testing"
+
+       "code.gitea.io/gitea/models"
+       "github.com/stretchr/testify/assert"
+)
+
+func TestCombineLabelComments(t *testing.T) {
+       var kases = []struct {
+               name           string
+               beforeCombined []*models.Comment
+               afterCombined  []*models.Comment
+       }{
+               {
+                       name: "kase 1",
+                       beforeCombined: []*models.Comment{
+                               {
+                                       Type:     models.CommentTypeLabel,
+                                       PosterID: 1,
+                                       Content:  "1",
+                                       Label: &models.Label{
+                                               Name: "kind/bug",
+                                       },
+                                       CreatedUnix: 0,
+                               },
+                               {
+                                       Type:     models.CommentTypeLabel,
+                                       PosterID: 1,
+                                       Content:  "",
+                                       Label: &models.Label{
+                                               Name: "kind/bug",
+                                       },
+                                       CreatedUnix: 0,
+                               },
+                               {
+                                       Type:        models.CommentTypeComment,
+                                       PosterID:    1,
+                                       Content:     "test",
+                                       CreatedUnix: 0,
+                               },
+                       },
+                       afterCombined: []*models.Comment{
+                               {
+                                       Type:        models.CommentTypeLabel,
+                                       PosterID:    1,
+                                       Content:     "1",
+                                       CreatedUnix: 0,
+                                       AddedLabels: []*models.Label{
+                                               {
+                                                       Name: "kind/bug",
+                                               },
+                                       },
+                                       RemovedLabels: []*models.Label{
+                                               {
+                                                       Name: "kind/bug",
+                                               },
+                                       },
+                                       Label: &models.Label{
+                                               Name: "kind/bug",
+                                       },
+                               },
+                               {
+                                       Type:        models.CommentTypeComment,
+                                       PosterID:    1,
+                                       Content:     "test",
+                                       CreatedUnix: 0,
+                               },
+                       },
+               },
+               {
+                       name: "kase 2",
+                       beforeCombined: []*models.Comment{
+                               {
+                                       Type:     models.CommentTypeLabel,
+                                       PosterID: 1,
+                                       Content:  "1",
+                                       Label: &models.Label{
+                                               Name: "kind/bug",
+                                       },
+                                       CreatedUnix: 0,
+                               },
+                               {
+                                       Type:     models.CommentTypeLabel,
+                                       PosterID: 1,
+                                       Content:  "",
+                                       Label: &models.Label{
+                                               Name: "kind/bug",
+                                       },
+                                       CreatedUnix: 70,
+                               },
+                               {
+                                       Type:        models.CommentTypeComment,
+                                       PosterID:    1,
+                                       Content:     "test",
+                                       CreatedUnix: 0,
+                               },
+                       },
+                       afterCombined: []*models.Comment{
+                               {
+                                       Type:        models.CommentTypeLabel,
+                                       PosterID:    1,
+                                       Content:     "1",
+                                       CreatedUnix: 0,
+                                       AddedLabels: []*models.Label{
+                                               {
+                                                       Name: "kind/bug",
+                                               },
+                                       },
+                                       Label: &models.Label{
+                                               Name: "kind/bug",
+                                       },
+                               },
+                               {
+                                       Type:        models.CommentTypeLabel,
+                                       PosterID:    1,
+                                       Content:     "",
+                                       CreatedUnix: 70,
+                                       RemovedLabels: []*models.Label{
+                                               {
+                                                       Name: "kind/bug",
+                                               },
+                                       },
+                                       Label: &models.Label{
+                                               Name: "kind/bug",
+                                       },
+                               },
+                               {
+                                       Type:        models.CommentTypeComment,
+                                       PosterID:    1,
+                                       Content:     "test",
+                                       CreatedUnix: 0,
+                               },
+                       },
+               },
+               {
+                       name: "kase 3",
+                       beforeCombined: []*models.Comment{
+                               {
+                                       Type:     models.CommentTypeLabel,
+                                       PosterID: 1,
+                                       Content:  "1",
+                                       Label: &models.Label{
+                                               Name: "kind/bug",
+                                       },
+                                       CreatedUnix: 0,
+                               },
+                               {
+                                       Type:     models.CommentTypeLabel,
+                                       PosterID: 2,
+                                       Content:  "",
+                                       Label: &models.Label{
+                                               Name: "kind/bug",
+                                       },
+                                       CreatedUnix: 0,
+                               },
+                               {
+                                       Type:        models.CommentTypeComment,
+                                       PosterID:    1,
+                                       Content:     "test",
+                                       CreatedUnix: 0,
+                               },
+                       },
+                       afterCombined: []*models.Comment{
+                               {
+                                       Type:        models.CommentTypeLabel,
+                                       PosterID:    1,
+                                       Content:     "1",
+                                       CreatedUnix: 0,
+                                       AddedLabels: []*models.Label{
+                                               {
+                                                       Name: "kind/bug",
+                                               },
+                                       },
+                                       Label: &models.Label{
+                                               Name: "kind/bug",
+                                       },
+                               },
+                               {
+                                       Type:        models.CommentTypeLabel,
+                                       PosterID:    2,
+                                       Content:     "",
+                                       CreatedUnix: 0,
+                                       RemovedLabels: []*models.Label{
+                                               {
+                                                       Name: "kind/bug",
+                                               },
+                                       },
+                                       Label: &models.Label{
+                                               Name: "kind/bug",
+                                       },
+                               },
+                               {
+                                       Type:        models.CommentTypeComment,
+                                       PosterID:    1,
+                                       Content:     "test",
+                                       CreatedUnix: 0,
+                               },
+                       },
+               },
+               {
+                       name: "kase 4",
+                       beforeCombined: []*models.Comment{
+                               {
+                                       Type:     models.CommentTypeLabel,
+                                       PosterID: 1,
+                                       Content:  "1",
+                                       Label: &models.Label{
+                                               Name: "kind/bug",
+                                       },
+                                       CreatedUnix: 0,
+                               },
+                               {
+                                       Type:     models.CommentTypeLabel,
+                                       PosterID: 1,
+                                       Content:  "1",
+                                       Label: &models.Label{
+                                               Name: "kind/backport",
+                                       },
+                                       CreatedUnix: 10,
+                               },
+                       },
+                       afterCombined: []*models.Comment{
+                               {
+                                       Type:        models.CommentTypeLabel,
+                                       PosterID:    1,
+                                       Content:     "1",
+                                       CreatedUnix: 10,
+                                       AddedLabels: []*models.Label{
+                                               {
+                                                       Name: "kind/bug",
+                                               },
+                                               {
+                                                       Name: "kind/backport",
+                                               },
+                                       },
+                                       Label: &models.Label{
+                                               Name: "kind/bug",
+                                       },
+                               },
+                       },
+               },
+               {
+                       name: "kase 5",
+                       beforeCombined: []*models.Comment{
+                               {
+                                       Type:     models.CommentTypeLabel,
+                                       PosterID: 1,
+                                       Content:  "1",
+                                       Label: &models.Label{
+                                               Name: "kind/bug",
+                                       },
+                                       CreatedUnix: 0,
+                               },
+                               {
+                                       Type:        models.CommentTypeComment,
+                                       PosterID:    2,
+                                       Content:     "testtest",
+                                       CreatedUnix: 0,
+                               },
+                               {
+                                       Type:     models.CommentTypeLabel,
+                                       PosterID: 1,
+                                       Content:  "",
+                                       Label: &models.Label{
+                                               Name: "kind/bug",
+                                       },
+                                       CreatedUnix: 0,
+                               },
+                       },
+                       afterCombined: []*models.Comment{
+                               {
+                                       Type:     models.CommentTypeLabel,
+                                       PosterID: 1,
+                                       Content:  "1",
+                                       Label: &models.Label{
+                                               Name: "kind/bug",
+                                       },
+                                       AddedLabels: []*models.Label{
+                                               {
+                                                       Name: "kind/bug",
+                                               },
+                                       },
+                                       CreatedUnix: 0,
+                               },
+                               {
+                                       Type:        models.CommentTypeComment,
+                                       PosterID:    2,
+                                       Content:     "testtest",
+                                       CreatedUnix: 0,
+                               },
+                               {
+                                       Type:     models.CommentTypeLabel,
+                                       PosterID: 1,
+                                       Content:  "",
+                                       RemovedLabels: []*models.Label{
+                                               {
+                                                       Name: "kind/bug",
+                                               },
+                                       },
+                                       Label: &models.Label{
+                                               Name: "kind/bug",
+                                       },
+                                       CreatedUnix: 0,
+                               },
+                       },
+               },
+       }
+
+       for _, kase := range kases {
+               t.Run(kase.name, func(t *testing.T) {
+                       var issue = models.Issue{
+                               Comments: kase.beforeCombined,
+                       }
+                       combineLabelComments(&issue)
+                       assert.EqualValues(t, kase.afterCombined, issue.Comments)
+               })
+       }
+}
diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go
new file mode 100644 (file)
index 0000000..3770cd7
--- /dev/null
@@ -0,0 +1,86 @@
+// 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 repo
+
+import (
+       "net/http"
+       "time"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+)
+
+// AddTimeManually tracks time manually
+func AddTimeManually(c *context.Context) {
+       form := web.GetForm(c).(*forms.AddTimeManuallyForm)
+       issue := GetActionIssue(c)
+       if c.Written() {
+               return
+       }
+       if !c.Repo.CanUseTimetracker(issue, c.User) {
+               c.NotFound("CanUseTimetracker", nil)
+               return
+       }
+       url := issue.HTMLURL()
+
+       if c.HasError() {
+               c.Flash.Error(c.GetErrMsg())
+               c.Redirect(url)
+               return
+       }
+
+       total := time.Duration(form.Hours)*time.Hour + time.Duration(form.Minutes)*time.Minute
+
+       if total <= 0 {
+               c.Flash.Error(c.Tr("repo.issues.add_time_sum_to_small"))
+               c.Redirect(url, http.StatusSeeOther)
+               return
+       }
+
+       if _, err := models.AddTime(c.User, issue, int64(total.Seconds()), time.Now()); err != nil {
+               c.ServerError("AddTime", err)
+               return
+       }
+
+       c.Redirect(url, http.StatusSeeOther)
+}
+
+// DeleteTime deletes tracked time
+func DeleteTime(c *context.Context) {
+       issue := GetActionIssue(c)
+       if c.Written() {
+               return
+       }
+       if !c.Repo.CanUseTimetracker(issue, c.User) {
+               c.NotFound("CanUseTimetracker", nil)
+               return
+       }
+
+       t, err := models.GetTrackedTimeByID(c.ParamsInt64(":timeid"))
+       if err != nil {
+               if models.IsErrNotExist(err) {
+                       c.NotFound("time not found", err)
+                       return
+               }
+               c.Error(http.StatusInternalServerError, "GetTrackedTimeByID", err.Error())
+               return
+       }
+
+       // only OP or admin may delete
+       if !c.IsSigned || (!c.IsUserSiteAdmin() && c.User.ID != t.UserID) {
+               c.Error(http.StatusForbidden, "not allowed")
+               return
+       }
+
+       if err = models.DeleteTime(t); err != nil {
+               c.ServerError("DeleteTime", err)
+               return
+       }
+
+       c.Flash.Success(c.Tr("repo.issues.del_time_history", models.SecToTime(t.Time)))
+       c.Redirect(issue.HTMLURL())
+}
diff --git a/routers/web/repo/issue_watch.go b/routers/web/repo/issue_watch.go
new file mode 100644 (file)
index 0000000..dabbff8
--- /dev/null
@@ -0,0 +1,57 @@
+// 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 repo
+
+import (
+       "net/http"
+       "strconv"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+)
+
+// IssueWatch sets issue watching
+func IssueWatch(ctx *context.Context) {
+       issue := GetActionIssue(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
+               if log.IsTrace() {
+                       if ctx.IsSigned {
+                               issueType := "issues"
+                               if issue.IsPull {
+                                       issueType = "pulls"
+                               }
+                               log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
+                                       "User in Repo has Permissions: %-+v",
+                                       ctx.User,
+                                       log.NewColoredIDValue(issue.PosterID),
+                                       issueType,
+                                       ctx.Repo.Repository,
+                                       ctx.Repo.Permission)
+                       } else {
+                               log.Trace("Permission Denied: Not logged in")
+                       }
+               }
+               ctx.Error(http.StatusForbidden)
+               return
+       }
+
+       watch, err := strconv.ParseBool(ctx.Req.PostForm.Get("watch"))
+       if err != nil {
+               ctx.ServerError("watch is not bool", err)
+               return
+       }
+
+       if err := models.CreateOrUpdateIssueWatch(ctx.User.ID, issue.ID, watch); err != nil {
+               ctx.ServerError("CreateOrUpdateIssueWatch", err)
+               return
+       }
+
+       ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
+}
diff --git a/routers/web/repo/lfs.go b/routers/web/repo/lfs.go
new file mode 100644 (file)
index 0000000..173ffb7
--- /dev/null
@@ -0,0 +1,537 @@
+// Copyright 2019 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 repo
+
+import (
+       "bytes"
+       "fmt"
+       gotemplate "html/template"
+       "io"
+       "io/ioutil"
+       "net/http"
+       "path"
+       "strconv"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/charset"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/git/pipeline"
+       "code.gitea.io/gitea/modules/lfs"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/storage"
+       "code.gitea.io/gitea/modules/typesniffer"
+)
+
+const (
+       tplSettingsLFS         base.TplName = "repo/settings/lfs"
+       tplSettingsLFSLocks    base.TplName = "repo/settings/lfs_locks"
+       tplSettingsLFSFile     base.TplName = "repo/settings/lfs_file"
+       tplSettingsLFSFileFind base.TplName = "repo/settings/lfs_file_find"
+       tplSettingsLFSPointers base.TplName = "repo/settings/lfs_pointers"
+)
+
+// LFSFiles shows a repository's LFS files
+func LFSFiles(ctx *context.Context) {
+       if !setting.LFS.StartServer {
+               ctx.NotFound("LFSFiles", nil)
+               return
+       }
+       page := ctx.QueryInt("page")
+       if page <= 1 {
+               page = 1
+       }
+       total, err := ctx.Repo.Repository.CountLFSMetaObjects()
+       if err != nil {
+               ctx.ServerError("LFSFiles", err)
+               return
+       }
+       ctx.Data["Total"] = total
+
+       pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
+       ctx.Data["Title"] = ctx.Tr("repo.settings.lfs")
+       ctx.Data["PageIsSettingsLFS"] = true
+       lfsMetaObjects, err := ctx.Repo.Repository.GetLFSMetaObjects(pager.Paginater.Current(), setting.UI.ExplorePagingNum)
+       if err != nil {
+               ctx.ServerError("LFSFiles", err)
+               return
+       }
+       ctx.Data["LFSFiles"] = lfsMetaObjects
+       ctx.Data["Page"] = pager
+       ctx.HTML(http.StatusOK, tplSettingsLFS)
+}
+
+// LFSLocks shows a repository's LFS locks
+func LFSLocks(ctx *context.Context) {
+       if !setting.LFS.StartServer {
+               ctx.NotFound("LFSLocks", nil)
+               return
+       }
+       ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
+
+       page := ctx.QueryInt("page")
+       if page <= 1 {
+               page = 1
+       }
+       total, err := models.CountLFSLockByRepoID(ctx.Repo.Repository.ID)
+       if err != nil {
+               ctx.ServerError("LFSLocks", err)
+               return
+       }
+       ctx.Data["Total"] = total
+
+       pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
+       ctx.Data["Title"] = ctx.Tr("repo.settings.lfs_locks")
+       ctx.Data["PageIsSettingsLFS"] = true
+       lfsLocks, err := models.GetLFSLockByRepoID(ctx.Repo.Repository.ID, pager.Paginater.Current(), setting.UI.ExplorePagingNum)
+       if err != nil {
+               ctx.ServerError("LFSLocks", err)
+               return
+       }
+       ctx.Data["LFSLocks"] = lfsLocks
+
+       if len(lfsLocks) == 0 {
+               ctx.Data["Page"] = pager
+               ctx.HTML(http.StatusOK, tplSettingsLFSLocks)
+               return
+       }
+
+       // Clone base repo.
+       tmpBasePath, err := models.CreateTemporaryPath("locks")
+       if err != nil {
+               log.Error("Failed to create temporary path: %v", err)
+               ctx.ServerError("LFSLocks", err)
+               return
+       }
+       defer func() {
+               if err := models.RemoveTemporaryPath(tmpBasePath); err != nil {
+                       log.Error("LFSLocks: RemoveTemporaryPath: %v", err)
+               }
+       }()
+
+       if err := git.Clone(ctx.Repo.Repository.RepoPath(), tmpBasePath, git.CloneRepoOptions{
+               Bare:   true,
+               Shared: true,
+       }); err != nil {
+               log.Error("Failed to clone repository: %s (%v)", ctx.Repo.Repository.FullName(), err)
+               ctx.ServerError("LFSLocks", fmt.Errorf("Failed to clone repository: %s (%v)", ctx.Repo.Repository.FullName(), err))
+               return
+       }
+
+       gitRepo, err := git.OpenRepository(tmpBasePath)
+       if err != nil {
+               log.Error("Unable to open temporary repository: %s (%v)", tmpBasePath, err)
+               ctx.ServerError("LFSLocks", fmt.Errorf("Failed to open new temporary repository in: %s %v", tmpBasePath, err))
+               return
+       }
+       defer gitRepo.Close()
+
+       filenames := make([]string, len(lfsLocks))
+
+       for i, lock := range lfsLocks {
+               filenames[i] = lock.Path
+       }
+
+       if err := gitRepo.ReadTreeToIndex(ctx.Repo.Repository.DefaultBranch); err != nil {
+               log.Error("Unable to read the default branch to the index: %s (%v)", ctx.Repo.Repository.DefaultBranch, err)
+               ctx.ServerError("LFSLocks", fmt.Errorf("Unable to read the default branch to the index: %s (%v)", ctx.Repo.Repository.DefaultBranch, err))
+               return
+       }
+
+       name2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{
+               Attributes: []string{"lockable"},
+               Filenames:  filenames,
+               CachedOnly: true,
+       })
+       if err != nil {
+               log.Error("Unable to check attributes in %s (%v)", tmpBasePath, err)
+               ctx.ServerError("LFSLocks", err)
+               return
+       }
+
+       lockables := make([]bool, len(lfsLocks))
+       for i, lock := range lfsLocks {
+               attribute2info, has := name2attribute2info[lock.Path]
+               if !has {
+                       continue
+               }
+               if attribute2info["lockable"] != "set" {
+                       continue
+               }
+               lockables[i] = true
+       }
+       ctx.Data["Lockables"] = lockables
+
+       filelist, err := gitRepo.LsFiles(filenames...)
+       if err != nil {
+               log.Error("Unable to lsfiles in %s (%v)", tmpBasePath, err)
+               ctx.ServerError("LFSLocks", err)
+               return
+       }
+
+       filemap := make(map[string]bool, len(filelist))
+       for _, name := range filelist {
+               filemap[name] = true
+       }
+
+       linkable := make([]bool, len(lfsLocks))
+       for i, lock := range lfsLocks {
+               linkable[i] = filemap[lock.Path]
+       }
+       ctx.Data["Linkable"] = linkable
+
+       ctx.Data["Page"] = pager
+       ctx.HTML(http.StatusOK, tplSettingsLFSLocks)
+}
+
+// LFSLockFile locks a file
+func LFSLockFile(ctx *context.Context) {
+       if !setting.LFS.StartServer {
+               ctx.NotFound("LFSLocks", nil)
+               return
+       }
+       originalPath := ctx.Query("path")
+       lockPath := originalPath
+       if len(lockPath) == 0 {
+               ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_locking_path", originalPath))
+               ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
+               return
+       }
+       if lockPath[len(lockPath)-1] == '/' {
+               ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_lock_directory", originalPath))
+               ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
+               return
+       }
+       lockPath = path.Clean("/" + lockPath)[1:]
+       if len(lockPath) == 0 {
+               ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_locking_path", originalPath))
+               ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
+               return
+       }
+
+       _, err := models.CreateLFSLock(&models.LFSLock{
+               Repo:  ctx.Repo.Repository,
+               Path:  lockPath,
+               Owner: ctx.User,
+       })
+       if err != nil {
+               if models.IsErrLFSLockAlreadyExist(err) {
+                       ctx.Flash.Error(ctx.Tr("repo.settings.lfs_lock_already_exists", originalPath))
+                       ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
+                       return
+               }
+               ctx.ServerError("LFSLockFile", err)
+               return
+       }
+       ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
+}
+
+// LFSUnlock forcibly unlocks an LFS lock
+func LFSUnlock(ctx *context.Context) {
+       if !setting.LFS.StartServer {
+               ctx.NotFound("LFSUnlock", nil)
+               return
+       }
+       _, err := models.DeleteLFSLockByID(ctx.ParamsInt64("lid"), ctx.User, true)
+       if err != nil {
+               ctx.ServerError("LFSUnlock", err)
+               return
+       }
+       ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
+}
+
+// LFSFileGet serves a single LFS file
+func LFSFileGet(ctx *context.Context) {
+       if !setting.LFS.StartServer {
+               ctx.NotFound("LFSFileGet", nil)
+               return
+       }
+       ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
+       oid := ctx.Params("oid")
+       ctx.Data["Title"] = oid
+       ctx.Data["PageIsSettingsLFS"] = true
+       meta, err := ctx.Repo.Repository.GetLFSMetaObjectByOid(oid)
+       if err != nil {
+               if err == models.ErrLFSObjectNotExist {
+                       ctx.NotFound("LFSFileGet", nil)
+                       return
+               }
+               ctx.ServerError("LFSFileGet", err)
+               return
+       }
+       ctx.Data["LFSFile"] = meta
+       dataRc, err := lfs.ReadMetaObject(meta.Pointer)
+       if err != nil {
+               ctx.ServerError("LFSFileGet", err)
+               return
+       }
+       defer dataRc.Close()
+       buf := make([]byte, 1024)
+       n, err := dataRc.Read(buf)
+       if err != nil {
+               ctx.ServerError("Data", err)
+               return
+       }
+       buf = buf[:n]
+
+       st := typesniffer.DetectContentType(buf)
+       ctx.Data["IsTextFile"] = st.IsText()
+       isRepresentableAsText := st.IsRepresentableAsText()
+
+       fileSize := meta.Size
+       ctx.Data["FileSize"] = meta.Size
+       ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s.git/info/lfs/objects/%s/%s", setting.AppURL, ctx.Repo.Repository.FullName(), meta.Oid, "direct")
+       switch {
+       case isRepresentableAsText:
+               if st.IsSvgImage() {
+                       ctx.Data["IsImageFile"] = true
+               }
+
+               if fileSize >= setting.UI.MaxDisplayFileSize {
+                       ctx.Data["IsFileTooLarge"] = true
+                       break
+               }
+
+               buf := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc))
+
+               // Building code view blocks with line number on server side.
+               fileContent, _ := ioutil.ReadAll(buf)
+
+               var output bytes.Buffer
+               lines := strings.Split(string(fileContent), "\n")
+               //Remove blank line at the end of file
+               if len(lines) > 0 && lines[len(lines)-1] == "" {
+                       lines = lines[:len(lines)-1]
+               }
+               for index, line := range lines {
+                       line = gotemplate.HTMLEscapeString(line)
+                       if index != len(lines)-1 {
+                               line += "\n"
+                       }
+                       output.WriteString(fmt.Sprintf(`<li class="L%d" rel="L%d">%s</li>`, index+1, index+1, line))
+               }
+               ctx.Data["FileContent"] = gotemplate.HTML(output.String())
+
+               output.Reset()
+               for i := 0; i < len(lines); i++ {
+                       output.WriteString(fmt.Sprintf(`<span id="L%d">%d</span>`, i+1, i+1))
+               }
+               ctx.Data["LineNums"] = gotemplate.HTML(output.String())
+
+       case st.IsPDF():
+               ctx.Data["IsPDFFile"] = true
+       case st.IsVideo():
+               ctx.Data["IsVideoFile"] = true
+       case st.IsAudio():
+               ctx.Data["IsAudioFile"] = true
+       case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()):
+               ctx.Data["IsImageFile"] = true
+       }
+       ctx.HTML(http.StatusOK, tplSettingsLFSFile)
+}
+
+// LFSDelete disassociates the provided oid from the repository and if the lfs file is no longer associated with any repositories - deletes it
+func LFSDelete(ctx *context.Context) {
+       if !setting.LFS.StartServer {
+               ctx.NotFound("LFSDelete", nil)
+               return
+       }
+       oid := ctx.Params("oid")
+       count, err := ctx.Repo.Repository.RemoveLFSMetaObjectByOid(oid)
+       if err != nil {
+               ctx.ServerError("LFSDelete", err)
+               return
+       }
+       // FIXME: Warning: the LFS store is not locked - and can't be locked - there could be a race condition here
+       // Please note a similar condition happens in models/repo.go DeleteRepository
+       if count == 0 {
+               oidPath := path.Join(oid[0:2], oid[2:4], oid[4:])
+               err = storage.LFS.Delete(oidPath)
+               if err != nil {
+                       ctx.ServerError("LFSDelete", err)
+                       return
+               }
+       }
+       ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs")
+}
+
+// LFSFileFind guesses a sha for the provided oid (or uses the provided sha) and then finds the commits that contain this sha
+func LFSFileFind(ctx *context.Context) {
+       if !setting.LFS.StartServer {
+               ctx.NotFound("LFSFind", nil)
+               return
+       }
+       oid := ctx.Query("oid")
+       size := ctx.QueryInt64("size")
+       if len(oid) == 0 || size == 0 {
+               ctx.NotFound("LFSFind", nil)
+               return
+       }
+       sha := ctx.Query("sha")
+       ctx.Data["Title"] = oid
+       ctx.Data["PageIsSettingsLFS"] = true
+       var hash git.SHA1
+       if len(sha) == 0 {
+               pointer := lfs.Pointer{Oid: oid, Size: size}
+               hash = git.ComputeBlobHash([]byte(pointer.StringContent()))
+               sha = hash.String()
+       } else {
+               hash = git.MustIDFromString(sha)
+       }
+       ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
+       ctx.Data["Oid"] = oid
+       ctx.Data["Size"] = size
+       ctx.Data["SHA"] = sha
+
+       results, err := pipeline.FindLFSFile(ctx.Repo.GitRepo, hash)
+       if err != nil && err != io.EOF {
+               log.Error("Failure in FindLFSFile: %v", err)
+               ctx.ServerError("LFSFind: FindLFSFile.", err)
+               return
+       }
+
+       ctx.Data["Results"] = results
+       ctx.HTML(http.StatusOK, tplSettingsLFSFileFind)
+}
+
+// LFSPointerFiles will search the repository for pointer files and report which are missing LFS files in the content store
+func LFSPointerFiles(ctx *context.Context) {
+       if !setting.LFS.StartServer {
+               ctx.NotFound("LFSFileGet", nil)
+               return
+       }
+       ctx.Data["PageIsSettingsLFS"] = true
+       err := git.LoadGitVersion()
+       if err != nil {
+               log.Fatal("Error retrieving git version: %v", err)
+       }
+       ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
+
+       err = func() error {
+               pointerChan := make(chan lfs.PointerBlob)
+               errChan := make(chan error, 1)
+               go lfs.SearchPointerBlobs(ctx, ctx.Repo.GitRepo, pointerChan, errChan)
+
+               numPointers := 0
+               var numAssociated, numNoExist, numAssociatable int
+
+               type pointerResult struct {
+                       SHA        string
+                       Oid        string
+                       Size       int64
+                       InRepo     bool
+                       Exists     bool
+                       Accessible bool
+               }
+
+               results := []pointerResult{}
+
+               contentStore := lfs.NewContentStore()
+               repo := ctx.Repo.Repository
+
+               for pointerBlob := range pointerChan {
+                       numPointers++
+
+                       result := pointerResult{
+                               SHA:  pointerBlob.Hash,
+                               Oid:  pointerBlob.Oid,
+                               Size: pointerBlob.Size,
+                       }
+
+                       if _, err := repo.GetLFSMetaObjectByOid(pointerBlob.Oid); err != nil {
+                               if err != models.ErrLFSObjectNotExist {
+                                       return err
+                               }
+                       } else {
+                               result.InRepo = true
+                       }
+
+                       result.Exists, err = contentStore.Exists(pointerBlob.Pointer)
+                       if err != nil {
+                               return err
+                       }
+
+                       if result.Exists {
+                               if !result.InRepo {
+                                       // Can we fix?
+                                       // OK well that's "simple"
+                                       // - we need to check whether current user has access to a repo that has access to the file
+                                       result.Accessible, err = models.LFSObjectAccessible(ctx.User, pointerBlob.Oid)
+                                       if err != nil {
+                                               return err
+                                       }
+                               } else {
+                                       result.Accessible = true
+                               }
+                       }
+
+                       if result.InRepo {
+                               numAssociated++
+                       }
+                       if !result.Exists {
+                               numNoExist++
+                       }
+                       if !result.InRepo && result.Accessible {
+                               numAssociatable++
+                       }
+
+                       results = append(results, result)
+               }
+
+               err, has := <-errChan
+               if has {
+                       return err
+               }
+
+               ctx.Data["Pointers"] = results
+               ctx.Data["NumPointers"] = numPointers
+               ctx.Data["NumAssociated"] = numAssociated
+               ctx.Data["NumAssociatable"] = numAssociatable
+               ctx.Data["NumNoExist"] = numNoExist
+               ctx.Data["NumNotAssociated"] = numPointers - numAssociated
+
+               return nil
+       }()
+       if err != nil {
+               ctx.ServerError("LFSPointerFiles", err)
+               return
+       }
+
+       ctx.HTML(http.StatusOK, tplSettingsLFSPointers)
+}
+
+// LFSAutoAssociate auto associates accessible lfs files
+func LFSAutoAssociate(ctx *context.Context) {
+       if !setting.LFS.StartServer {
+               ctx.NotFound("LFSAutoAssociate", nil)
+               return
+       }
+       oids := ctx.QueryStrings("oid")
+       metas := make([]*models.LFSMetaObject, len(oids))
+       for i, oid := range oids {
+               idx := strings.IndexRune(oid, ' ')
+               if idx < 0 || idx+1 > len(oid) {
+                       ctx.ServerError("LFSAutoAssociate", fmt.Errorf("Illegal oid input: %s", oid))
+                       return
+               }
+               var err error
+               metas[i] = &models.LFSMetaObject{}
+               metas[i].Size, err = strconv.ParseInt(oid[idx+1:], 10, 64)
+               if err != nil {
+                       ctx.ServerError("LFSAutoAssociate", fmt.Errorf("Illegal oid input: %s %v", oid, err))
+                       return
+               }
+               metas[i].Oid = oid[:idx]
+               //metas[i].RepositoryID = ctx.Repo.Repository.ID
+       }
+       if err := models.LFSAutoAssociate(metas, ctx.User, ctx.Repo.Repository.ID); err != nil {
+               ctx.ServerError("LFSAutoAssociate", err)
+               return
+       }
+       ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs")
+}
diff --git a/routers/web/repo/main_test.go b/routers/web/repo/main_test.go
new file mode 100644 (file)
index 0000000..47f2663
--- /dev/null
@@ -0,0 +1,16 @@
+// 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 repo
+
+import (
+       "path/filepath"
+       "testing"
+
+       "code.gitea.io/gitea/models"
+)
+
+func TestMain(m *testing.M) {
+       models.MainTest(m, filepath.Join("..", "..", ".."))
+}
diff --git a/routers/web/repo/middlewares.go b/routers/web/repo/middlewares.go
new file mode 100644 (file)
index 0000000..1b95a13
--- /dev/null
@@ -0,0 +1,72 @@
+// 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 repo
+
+import (
+       "fmt"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/git"
+)
+
+// SetEditorconfigIfExists set editor config as render variable
+func SetEditorconfigIfExists(ctx *context.Context) {
+       if ctx.Repo.Repository.IsEmpty {
+               ctx.Data["Editorconfig"] = nil
+               return
+       }
+
+       ec, err := ctx.Repo.GetEditorconfig()
+
+       if err != nil && !git.IsErrNotExist(err) {
+               description := fmt.Sprintf("Error while getting .editorconfig file: %v", err)
+               if err := models.CreateRepositoryNotice(description); err != nil {
+                       ctx.ServerError("ErrCreatingReporitoryNotice", err)
+               }
+               return
+       }
+
+       ctx.Data["Editorconfig"] = ec
+}
+
+// SetDiffViewStyle set diff style as render variable
+func SetDiffViewStyle(ctx *context.Context) {
+       queryStyle := ctx.Query("style")
+
+       if !ctx.IsSigned {
+               ctx.Data["IsSplitStyle"] = queryStyle == "split"
+               return
+       }
+
+       var (
+               userStyle = ctx.User.DiffViewStyle
+               style     string
+       )
+
+       if queryStyle == "unified" || queryStyle == "split" {
+               style = queryStyle
+       } else if userStyle == "unified" || userStyle == "split" {
+               style = userStyle
+       } else {
+               style = "unified"
+       }
+
+       ctx.Data["IsSplitStyle"] = style == "split"
+       if err := ctx.User.UpdateDiffViewStyle(style); err != nil {
+               ctx.ServerError("ErrUpdateDiffViewStyle", err)
+       }
+}
+
+// SetWhitespaceBehavior set whitespace behavior as render variable
+func SetWhitespaceBehavior(ctx *context.Context) {
+       whitespaceBehavior := ctx.Query("whitespace")
+       switch whitespaceBehavior {
+       case "ignore-all", "ignore-eol", "ignore-change":
+               ctx.Data["WhitespaceBehavior"] = whitespaceBehavior
+       default:
+               ctx.Data["WhitespaceBehavior"] = ""
+       }
+}
diff --git a/routers/web/repo/migrate.go b/routers/web/repo/migrate.go
new file mode 100644 (file)
index 0000000..24d4ef4
--- /dev/null
@@ -0,0 +1,254 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// 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 repo
+
+import (
+       "net/http"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/lfs"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/migrations"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/structs"
+       "code.gitea.io/gitea/modules/task"
+       "code.gitea.io/gitea/modules/util"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+)
+
+const (
+       tplMigrate base.TplName = "repo/migrate/migrate"
+)
+
+// Migrate render migration of repository page
+func Migrate(ctx *context.Context) {
+       if setting.Repository.DisableMigrations {
+               ctx.Error(http.StatusForbidden, "Migrate: the site administrator has disabled migrations")
+               return
+       }
+
+       serviceType := structs.GitServiceType(ctx.QueryInt("service_type"))
+
+       setMigrationContextData(ctx, serviceType)
+
+       if serviceType == 0 {
+               ctx.Data["Org"] = ctx.Query("org")
+               ctx.Data["Mirror"] = ctx.Query("mirror")
+
+               ctx.HTML(http.StatusOK, tplMigrate)
+               return
+       }
+
+       ctx.Data["private"] = getRepoPrivate(ctx)
+       ctx.Data["mirror"] = ctx.Query("mirror") == "1"
+       ctx.Data["lfs"] = ctx.Query("lfs") == "1"
+       ctx.Data["wiki"] = ctx.Query("wiki") == "1"
+       ctx.Data["milestones"] = ctx.Query("milestones") == "1"
+       ctx.Data["labels"] = ctx.Query("labels") == "1"
+       ctx.Data["issues"] = ctx.Query("issues") == "1"
+       ctx.Data["pull_requests"] = ctx.Query("pull_requests") == "1"
+       ctx.Data["releases"] = ctx.Query("releases") == "1"
+
+       ctxUser := checkContextUser(ctx, ctx.QueryInt64("org"))
+       if ctx.Written() {
+               return
+       }
+       ctx.Data["ContextUser"] = ctxUser
+
+       ctx.HTML(http.StatusOK, base.TplName("repo/migrate/"+serviceType.Name()))
+}
+
+func handleMigrateError(ctx *context.Context, owner *models.User, err error, name string, tpl base.TplName, form *forms.MigrateRepoForm) {
+       if setting.Repository.DisableMigrations {
+               ctx.Error(http.StatusForbidden, "MigrateError: the site administrator has disabled migrations")
+               return
+       }
+
+       switch {
+       case migrations.IsRateLimitError(err):
+               ctx.RenderWithErr(ctx.Tr("form.visit_rate_limit"), tpl, form)
+       case migrations.IsTwoFactorAuthError(err):
+               ctx.RenderWithErr(ctx.Tr("form.2fa_auth_required"), tpl, form)
+       case models.IsErrReachLimitOfRepo(err):
+               ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_creation", owner.MaxCreationLimit()), tpl, form)
+       case models.IsErrRepoAlreadyExist(err):
+               ctx.Data["Err_RepoName"] = true
+               ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form)
+       case models.IsErrRepoFilesAlreadyExist(err):
+               ctx.Data["Err_RepoName"] = true
+               switch {
+               case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
+                       ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form)
+               case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
+                       ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form)
+               case setting.Repository.AllowDeleteOfUnadoptedRepositories:
+                       ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form)
+               default:
+                       ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form)
+               }
+       case models.IsErrNameReserved(err):
+               ctx.Data["Err_RepoName"] = true
+               ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tpl, form)
+       case models.IsErrNamePatternNotAllowed(err):
+               ctx.Data["Err_RepoName"] = true
+               ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tpl, form)
+       default:
+               remoteAddr, _ := forms.ParseRemoteAddr(form.CloneAddr, form.AuthUsername, form.AuthPassword)
+               err = util.URLSanitizedError(err, remoteAddr)
+               if strings.Contains(err.Error(), "Authentication failed") ||
+                       strings.Contains(err.Error(), "Bad credentials") ||
+                       strings.Contains(err.Error(), "could not read Username") {
+                       ctx.Data["Err_Auth"] = true
+                       ctx.RenderWithErr(ctx.Tr("form.auth_failed", err.Error()), tpl, form)
+               } else if strings.Contains(err.Error(), "fatal:") {
+                       ctx.Data["Err_CloneAddr"] = true
+                       ctx.RenderWithErr(ctx.Tr("repo.migrate.failed", err.Error()), tpl, form)
+               } else {
+                       ctx.ServerError(name, err)
+               }
+       }
+}
+
+func handleMigrateRemoteAddrError(ctx *context.Context, err error, tpl base.TplName, form *forms.MigrateRepoForm) {
+       if models.IsErrInvalidCloneAddr(err) {
+               addrErr := err.(*models.ErrInvalidCloneAddr)
+               switch {
+               case addrErr.IsProtocolInvalid:
+                       ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tpl, form)
+               case addrErr.IsURLError:
+                       ctx.RenderWithErr(ctx.Tr("form.url_error"), tpl, form)
+               case addrErr.IsPermissionDenied:
+                       if addrErr.LocalPath {
+                               ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tpl, form)
+                       } else if len(addrErr.PrivateNet) == 0 {
+                               ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_blocked"), tpl, form)
+                       } else {
+                               ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_private_ip"), tpl, form)
+                       }
+               case addrErr.IsInvalidPath:
+                       ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tpl, form)
+               default:
+                       log.Error("Error whilst updating url: %v", err)
+                       ctx.RenderWithErr(ctx.Tr("form.url_error"), tpl, form)
+               }
+       } else {
+               log.Error("Error whilst updating url: %v", err)
+               ctx.RenderWithErr(ctx.Tr("form.url_error"), tpl, form)
+       }
+}
+
+// MigratePost response for migrating from external git repository
+func MigratePost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.MigrateRepoForm)
+       if setting.Repository.DisableMigrations {
+               ctx.Error(http.StatusForbidden, "MigratePost: the site administrator has disabled migrations")
+               return
+       }
+
+       serviceType := structs.GitServiceType(form.Service)
+
+       setMigrationContextData(ctx, serviceType)
+
+       ctxUser := checkContextUser(ctx, form.UID)
+       if ctx.Written() {
+               return
+       }
+       ctx.Data["ContextUser"] = ctxUser
+
+       tpl := base.TplName("repo/migrate/" + serviceType.Name())
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tpl)
+               return
+       }
+
+       remoteAddr, err := forms.ParseRemoteAddr(form.CloneAddr, form.AuthUsername, form.AuthPassword)
+       if err == nil {
+               err = migrations.IsMigrateURLAllowed(remoteAddr, ctx.User)
+       }
+       if err != nil {
+               ctx.Data["Err_CloneAddr"] = true
+               handleMigrateRemoteAddrError(ctx, err, tpl, form)
+               return
+       }
+
+       form.LFS = form.LFS && setting.LFS.StartServer
+
+       if form.LFS && len(form.LFSEndpoint) > 0 {
+               ep := lfs.DetermineEndpoint("", form.LFSEndpoint)
+               if ep == nil {
+                       ctx.Data["Err_LFSEndpoint"] = true
+                       ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tpl, &form)
+                       return
+               }
+               err = migrations.IsMigrateURLAllowed(ep.String(), ctx.User)
+               if err != nil {
+                       ctx.Data["Err_LFSEndpoint"] = true
+                       handleMigrateRemoteAddrError(ctx, err, tpl, form)
+                       return
+               }
+       }
+
+       var opts = migrations.MigrateOptions{
+               OriginalURL:    form.CloneAddr,
+               GitServiceType: serviceType,
+               CloneAddr:      remoteAddr,
+               RepoName:       form.RepoName,
+               Description:    form.Description,
+               Private:        form.Private || setting.Repository.ForcePrivate,
+               Mirror:         form.Mirror && !setting.Repository.DisableMirrors,
+               LFS:            form.LFS,
+               LFSEndpoint:    form.LFSEndpoint,
+               AuthUsername:   form.AuthUsername,
+               AuthPassword:   form.AuthPassword,
+               AuthToken:      form.AuthToken,
+               Wiki:           form.Wiki,
+               Issues:         form.Issues,
+               Milestones:     form.Milestones,
+               Labels:         form.Labels,
+               Comments:       form.Issues || form.PullRequests,
+               PullRequests:   form.PullRequests,
+               Releases:       form.Releases,
+       }
+       if opts.Mirror {
+               opts.Issues = false
+               opts.Milestones = false
+               opts.Labels = false
+               opts.Comments = false
+               opts.PullRequests = false
+               opts.Releases = false
+       }
+
+       err = models.CheckCreateRepository(ctx.User, ctxUser, opts.RepoName, false)
+       if err != nil {
+               handleMigrateError(ctx, ctxUser, err, "MigratePost", tpl, form)
+               return
+       }
+
+       err = task.MigrateRepository(ctx.User, ctxUser, opts)
+       if err == nil {
+               ctx.Redirect(ctxUser.HomeLink() + "/" + opts.RepoName)
+               return
+       }
+
+       handleMigrateError(ctx, ctxUser, err, "MigratePost", tpl, form)
+}
+
+func setMigrationContextData(ctx *context.Context, serviceType structs.GitServiceType) {
+       ctx.Data["Title"] = ctx.Tr("new_migrate")
+
+       ctx.Data["LFSActive"] = setting.LFS.StartServer
+       ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate
+       ctx.Data["DisableMirrors"] = setting.Repository.DisableMirrors
+
+       // Plain git should be first
+       ctx.Data["Services"] = append([]structs.GitServiceType{structs.PlainGitService}, structs.SupportedFullGitService...)
+       ctx.Data["service"] = serviceType
+}
diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go
new file mode 100644 (file)
index 0000000..bb6b310
--- /dev/null
@@ -0,0 +1,299 @@
+// Copyright 2018 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 repo
+
+import (
+       "net/http"
+       "strings"
+       "time"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/markup"
+       "code.gitea.io/gitea/modules/markup/markdown"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/structs"
+       "code.gitea.io/gitea/modules/timeutil"
+       "code.gitea.io/gitea/modules/util"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+
+       "xorm.io/builder"
+)
+
+const (
+       tplMilestone       base.TplName = "repo/issue/milestones"
+       tplMilestoneNew    base.TplName = "repo/issue/milestone_new"
+       tplMilestoneIssues base.TplName = "repo/issue/milestone_issues"
+)
+
+// Milestones render milestones page
+func Milestones(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.milestones")
+       ctx.Data["PageIsIssueList"] = true
+       ctx.Data["PageIsMilestones"] = true
+
+       isShowClosed := ctx.Query("state") == "closed"
+       stats, err := models.GetMilestonesStatsByRepoCond(builder.And(builder.Eq{"id": ctx.Repo.Repository.ID}))
+       if err != nil {
+               ctx.ServerError("MilestoneStats", err)
+               return
+       }
+       ctx.Data["OpenCount"] = stats.OpenCount
+       ctx.Data["ClosedCount"] = stats.ClosedCount
+
+       sortType := ctx.Query("sort")
+
+       keyword := strings.Trim(ctx.Query("q"), " ")
+
+       page := ctx.QueryInt("page")
+       if page <= 1 {
+               page = 1
+       }
+
+       var total int
+       var state structs.StateType
+       if !isShowClosed {
+               total = int(stats.OpenCount)
+               state = structs.StateOpen
+       } else {
+               total = int(stats.ClosedCount)
+               state = structs.StateClosed
+       }
+
+       miles, err := models.GetMilestones(models.GetMilestonesOption{
+               ListOptions: models.ListOptions{
+                       Page:     page,
+                       PageSize: setting.UI.IssuePagingNum,
+               },
+               RepoID:   ctx.Repo.Repository.ID,
+               State:    state,
+               SortType: sortType,
+               Name:     keyword,
+       })
+       if err != nil {
+               ctx.ServerError("GetMilestones", err)
+               return
+       }
+       if ctx.Repo.Repository.IsTimetrackerEnabled() {
+               if err := miles.LoadTotalTrackedTimes(); err != nil {
+                       ctx.ServerError("LoadTotalTrackedTimes", err)
+                       return
+               }
+       }
+       for _, m := range miles {
+               m.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
+                       URLPrefix: ctx.Repo.RepoLink,
+                       Metas:     ctx.Repo.Repository.ComposeMetas(),
+               }, m.Content)
+               if err != nil {
+                       ctx.ServerError("RenderString", err)
+                       return
+               }
+       }
+       ctx.Data["Milestones"] = miles
+
+       if isShowClosed {
+               ctx.Data["State"] = "closed"
+       } else {
+               ctx.Data["State"] = "open"
+       }
+
+       ctx.Data["SortType"] = sortType
+       ctx.Data["Keyword"] = keyword
+       ctx.Data["IsShowClosed"] = isShowClosed
+
+       pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
+       pager.AddParam(ctx, "state", "State")
+       pager.AddParam(ctx, "q", "Keyword")
+       ctx.Data["Page"] = pager
+
+       ctx.HTML(http.StatusOK, tplMilestone)
+}
+
+// NewMilestone render creating milestone page
+func NewMilestone(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.milestones.new")
+       ctx.Data["PageIsIssueList"] = true
+       ctx.Data["PageIsMilestones"] = true
+       ctx.HTML(http.StatusOK, tplMilestoneNew)
+}
+
+// NewMilestonePost response for creating milestone
+func NewMilestonePost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.CreateMilestoneForm)
+       ctx.Data["Title"] = ctx.Tr("repo.milestones.new")
+       ctx.Data["PageIsIssueList"] = true
+       ctx.Data["PageIsMilestones"] = true
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplMilestoneNew)
+               return
+       }
+
+       if len(form.Deadline) == 0 {
+               form.Deadline = "9999-12-31"
+       }
+       deadline, err := time.ParseInLocation("2006-01-02", form.Deadline, time.Local)
+       if err != nil {
+               ctx.Data["Err_Deadline"] = true
+               ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form)
+               return
+       }
+
+       deadline = time.Date(deadline.Year(), deadline.Month(), deadline.Day(), 23, 59, 59, 0, deadline.Location())
+       if err = models.NewMilestone(&models.Milestone{
+               RepoID:       ctx.Repo.Repository.ID,
+               Name:         form.Title,
+               Content:      form.Content,
+               DeadlineUnix: timeutil.TimeStamp(deadline.Unix()),
+       }); err != nil {
+               ctx.ServerError("NewMilestone", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.milestones.create_success", form.Title))
+       ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
+}
+
+// EditMilestone render edting milestone page
+func EditMilestone(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.milestones.edit")
+       ctx.Data["PageIsMilestones"] = true
+       ctx.Data["PageIsEditMilestone"] = true
+
+       m, err := models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
+       if err != nil {
+               if models.IsErrMilestoneNotExist(err) {
+                       ctx.NotFound("", nil)
+               } else {
+                       ctx.ServerError("GetMilestoneByRepoID", err)
+               }
+               return
+       }
+       ctx.Data["title"] = m.Name
+       ctx.Data["content"] = m.Content
+       if len(m.DeadlineString) > 0 {
+               ctx.Data["deadline"] = m.DeadlineString
+       }
+       ctx.HTML(http.StatusOK, tplMilestoneNew)
+}
+
+// EditMilestonePost response for edting milestone
+func EditMilestonePost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.CreateMilestoneForm)
+       ctx.Data["Title"] = ctx.Tr("repo.milestones.edit")
+       ctx.Data["PageIsMilestones"] = true
+       ctx.Data["PageIsEditMilestone"] = true
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplMilestoneNew)
+               return
+       }
+
+       if len(form.Deadline) == 0 {
+               form.Deadline = "9999-12-31"
+       }
+       deadline, err := time.ParseInLocation("2006-01-02", form.Deadline, time.Local)
+       if err != nil {
+               ctx.Data["Err_Deadline"] = true
+               ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form)
+               return
+       }
+
+       deadline = time.Date(deadline.Year(), deadline.Month(), deadline.Day(), 23, 59, 59, 0, deadline.Location())
+       m, err := models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
+       if err != nil {
+               if models.IsErrMilestoneNotExist(err) {
+                       ctx.NotFound("", nil)
+               } else {
+                       ctx.ServerError("GetMilestoneByRepoID", err)
+               }
+               return
+       }
+       m.Name = form.Title
+       m.Content = form.Content
+       m.DeadlineUnix = timeutil.TimeStamp(deadline.Unix())
+       if err = models.UpdateMilestone(m, m.IsClosed); err != nil {
+               ctx.ServerError("UpdateMilestone", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.milestones.edit_success", m.Name))
+       ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
+}
+
+// ChangeMilestoneStatus response for change a milestone's status
+func ChangeMilestoneStatus(ctx *context.Context) {
+       toClose := false
+       switch ctx.Params(":action") {
+       case "open":
+               toClose = false
+       case "close":
+               toClose = true
+       default:
+               ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
+       }
+       id := ctx.ParamsInt64(":id")
+
+       if err := models.ChangeMilestoneStatusByRepoIDAndID(ctx.Repo.Repository.ID, id, toClose); err != nil {
+               if models.IsErrMilestoneNotExist(err) {
+                       ctx.NotFound("", err)
+               } else {
+                       ctx.ServerError("ChangeMilestoneStatusByIDAndRepoID", err)
+               }
+               return
+       }
+       ctx.Redirect(ctx.Repo.RepoLink + "/milestones?state=" + ctx.Params(":action"))
+}
+
+// DeleteMilestone delete a milestone
+func DeleteMilestone(ctx *context.Context) {
+       if err := models.DeleteMilestoneByRepoID(ctx.Repo.Repository.ID, ctx.QueryInt64("id")); err != nil {
+               ctx.Flash.Error("DeleteMilestoneByRepoID: " + err.Error())
+       } else {
+               ctx.Flash.Success(ctx.Tr("repo.milestones.deletion_success"))
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": ctx.Repo.RepoLink + "/milestones",
+       })
+}
+
+// MilestoneIssuesAndPulls lists all the issues and pull requests of the milestone
+func MilestoneIssuesAndPulls(ctx *context.Context) {
+       milestoneID := ctx.ParamsInt64(":id")
+       milestone, err := models.GetMilestoneByID(milestoneID)
+       if err != nil {
+               if models.IsErrMilestoneNotExist(err) {
+                       ctx.NotFound("GetMilestoneByID", err)
+                       return
+               }
+
+               ctx.ServerError("GetMilestoneByID", err)
+               return
+       }
+
+       milestone.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
+               URLPrefix: ctx.Repo.RepoLink,
+               Metas:     ctx.Repo.Repository.ComposeMetas(),
+       }, milestone.Content)
+       if err != nil {
+               ctx.ServerError("RenderString", err)
+               return
+       }
+
+       ctx.Data["Title"] = milestone.Name
+       ctx.Data["Milestone"] = milestone
+
+       issues(ctx, milestoneID, 0, util.OptionalBoolNone)
+       ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
+
+       ctx.Data["CanWriteIssues"] = ctx.Repo.CanWriteIssuesOrPulls(false)
+       ctx.Data["CanWritePulls"] = ctx.Repo.CanWriteIssuesOrPulls(true)
+
+       ctx.HTML(http.StatusOK, tplMilestoneIssues)
+}
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
new file mode 100644 (file)
index 0000000..eb07199
--- /dev/null
@@ -0,0 +1,665 @@
+// 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 repo
+
+import (
+       "fmt"
+       "net/http"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/markup"
+       "code.gitea.io/gitea/modules/markup/markdown"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/util"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+)
+
+const (
+       tplProjects           base.TplName = "repo/projects/list"
+       tplProjectsNew        base.TplName = "repo/projects/new"
+       tplProjectsView       base.TplName = "repo/projects/view"
+       tplGenericProjectsNew base.TplName = "user/project"
+)
+
+// MustEnableProjects check if projects are enabled in settings
+func MustEnableProjects(ctx *context.Context) {
+       if models.UnitTypeProjects.UnitGlobalDisabled() {
+               ctx.NotFound("EnableKanbanBoard", nil)
+               return
+       }
+
+       if ctx.Repo.Repository != nil {
+               if !ctx.Repo.CanRead(models.UnitTypeProjects) {
+                       ctx.NotFound("MustEnableProjects", nil)
+                       return
+               }
+       }
+}
+
+// Projects renders the home page of projects
+func Projects(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.project_board")
+
+       sortType := ctx.QueryTrim("sort")
+
+       isShowClosed := strings.ToLower(ctx.QueryTrim("state")) == "closed"
+       repo := ctx.Repo.Repository
+       page := ctx.QueryInt("page")
+       if page <= 1 {
+               page = 1
+       }
+
+       ctx.Data["OpenCount"] = repo.NumOpenProjects
+       ctx.Data["ClosedCount"] = repo.NumClosedProjects
+
+       var total int
+       if !isShowClosed {
+               total = repo.NumOpenProjects
+       } else {
+               total = repo.NumClosedProjects
+       }
+
+       projects, count, err := models.GetProjects(models.ProjectSearchOptions{
+               RepoID:   repo.ID,
+               Page:     page,
+               IsClosed: util.OptionalBoolOf(isShowClosed),
+               SortType: sortType,
+               Type:     models.ProjectTypeRepository,
+       })
+       if err != nil {
+               ctx.ServerError("GetProjects", err)
+               return
+       }
+
+       for i := range projects {
+               projects[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{
+                       URLPrefix: ctx.Repo.RepoLink,
+                       Metas:     ctx.Repo.Repository.ComposeMetas(),
+               }, projects[i].Description)
+               if err != nil {
+                       ctx.ServerError("RenderString", err)
+                       return
+               }
+       }
+
+       ctx.Data["Projects"] = projects
+
+       if isShowClosed {
+               ctx.Data["State"] = "closed"
+       } else {
+               ctx.Data["State"] = "open"
+       }
+
+       numPages := 0
+       if count > 0 {
+               numPages = int((int(count) - 1) / setting.UI.IssuePagingNum)
+       }
+
+       pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, numPages)
+       pager.AddParam(ctx, "state", "State")
+       ctx.Data["Page"] = pager
+
+       ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
+       ctx.Data["IsShowClosed"] = isShowClosed
+       ctx.Data["IsProjectsPage"] = true
+       ctx.Data["SortType"] = sortType
+
+       ctx.HTML(http.StatusOK, tplProjects)
+}
+
+// NewProject render creating a project page
+func NewProject(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.projects.new")
+       ctx.Data["ProjectTypes"] = models.GetProjectsConfig()
+       ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
+       ctx.HTML(http.StatusOK, tplProjectsNew)
+}
+
+// NewProjectPost creates a new project
+func NewProjectPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.CreateProjectForm)
+       ctx.Data["Title"] = ctx.Tr("repo.projects.new")
+
+       if ctx.HasError() {
+               ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
+               ctx.Data["ProjectTypes"] = models.GetProjectsConfig()
+               ctx.HTML(http.StatusOK, tplProjectsNew)
+               return
+       }
+
+       if err := models.NewProject(&models.Project{
+               RepoID:      ctx.Repo.Repository.ID,
+               Title:       form.Title,
+               Description: form.Content,
+               CreatorID:   ctx.User.ID,
+               BoardType:   form.BoardType,
+               Type:        models.ProjectTypeRepository,
+       }); err != nil {
+               ctx.ServerError("NewProject", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title))
+       ctx.Redirect(ctx.Repo.RepoLink + "/projects")
+}
+
+// ChangeProjectStatus updates the status of a project between "open" and "close"
+func ChangeProjectStatus(ctx *context.Context) {
+       toClose := false
+       switch ctx.Params(":action") {
+       case "open":
+               toClose = false
+       case "close":
+               toClose = true
+       default:
+               ctx.Redirect(ctx.Repo.RepoLink + "/projects")
+       }
+       id := ctx.ParamsInt64(":id")
+
+       if err := models.ChangeProjectStatusByRepoIDAndID(ctx.Repo.Repository.ID, id, toClose); err != nil {
+               if models.IsErrProjectNotExist(err) {
+                       ctx.NotFound("", err)
+               } else {
+                       ctx.ServerError("ChangeProjectStatusByIDAndRepoID", err)
+               }
+               return
+       }
+       ctx.Redirect(ctx.Repo.RepoLink + "/projects?state=" + ctx.Params(":action"))
+}
+
+// DeleteProject delete a project
+func DeleteProject(ctx *context.Context) {
+       p, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
+       if err != nil {
+               if models.IsErrProjectNotExist(err) {
+                       ctx.NotFound("", nil)
+               } else {
+                       ctx.ServerError("GetProjectByID", err)
+               }
+               return
+       }
+       if p.RepoID != ctx.Repo.Repository.ID {
+               ctx.NotFound("", nil)
+               return
+       }
+
+       if err := models.DeleteProjectByID(p.ID); err != nil {
+               ctx.Flash.Error("DeleteProjectByID: " + err.Error())
+       } else {
+               ctx.Flash.Success(ctx.Tr("repo.projects.deletion_success"))
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": ctx.Repo.RepoLink + "/projects",
+       })
+}
+
+// EditProject allows a project to be edited
+func EditProject(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
+       ctx.Data["PageIsProjects"] = true
+       ctx.Data["PageIsEditProjects"] = true
+       ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
+
+       p, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
+       if err != nil {
+               if models.IsErrProjectNotExist(err) {
+                       ctx.NotFound("", nil)
+               } else {
+                       ctx.ServerError("GetProjectByID", err)
+               }
+               return
+       }
+       if p.RepoID != ctx.Repo.Repository.ID {
+               ctx.NotFound("", nil)
+               return
+       }
+
+       ctx.Data["title"] = p.Title
+       ctx.Data["content"] = p.Description
+
+       ctx.HTML(http.StatusOK, tplProjectsNew)
+}
+
+// EditProjectPost response for editing a project
+func EditProjectPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.CreateProjectForm)
+       ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
+       ctx.Data["PageIsProjects"] = true
+       ctx.Data["PageIsEditProjects"] = true
+       ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplProjectsNew)
+               return
+       }
+
+       p, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
+       if err != nil {
+               if models.IsErrProjectNotExist(err) {
+                       ctx.NotFound("", nil)
+               } else {
+                       ctx.ServerError("GetProjectByID", err)
+               }
+               return
+       }
+       if p.RepoID != ctx.Repo.Repository.ID {
+               ctx.NotFound("", nil)
+               return
+       }
+
+       p.Title = form.Title
+       p.Description = form.Content
+       if err = models.UpdateProject(p); err != nil {
+               ctx.ServerError("UpdateProjects", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.projects.edit_success", p.Title))
+       ctx.Redirect(ctx.Repo.RepoLink + "/projects")
+}
+
+// ViewProject renders the project board for a project
+func ViewProject(ctx *context.Context) {
+
+       project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
+       if err != nil {
+               if models.IsErrProjectNotExist(err) {
+                       ctx.NotFound("", nil)
+               } else {
+                       ctx.ServerError("GetProjectByID", err)
+               }
+               return
+       }
+       if project.RepoID != ctx.Repo.Repository.ID {
+               ctx.NotFound("", nil)
+               return
+       }
+
+       boards, err := models.GetProjectBoards(project.ID)
+       if err != nil {
+               ctx.ServerError("GetProjectBoards", err)
+               return
+       }
+
+       if boards[0].ID == 0 {
+               boards[0].Title = ctx.Tr("repo.projects.type.uncategorized")
+       }
+
+       issueList, err := boards.LoadIssues()
+       if err != nil {
+               ctx.ServerError("LoadIssuesOfBoards", err)
+               return
+       }
+       ctx.Data["Issues"] = issueList
+
+       linkedPrsMap := make(map[int64][]*models.Issue)
+       for _, issue := range issueList {
+               var referencedIds []int64
+               for _, comment := range issue.Comments {
+                       if comment.RefIssueID != 0 && comment.RefIsPull {
+                               referencedIds = append(referencedIds, comment.RefIssueID)
+                       }
+               }
+
+               if len(referencedIds) > 0 {
+                       if linkedPrs, err := models.Issues(&models.IssuesOptions{
+                               IssueIDs: referencedIds,
+                               IsPull:   util.OptionalBoolTrue,
+                       }); err == nil {
+                               linkedPrsMap[issue.ID] = linkedPrs
+                       }
+               }
+       }
+       ctx.Data["LinkedPRs"] = linkedPrsMap
+
+       project.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
+               URLPrefix: ctx.Repo.RepoLink,
+               Metas:     ctx.Repo.Repository.ComposeMetas(),
+       }, project.Description)
+       if err != nil {
+               ctx.ServerError("RenderString", err)
+               return
+       }
+
+       ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
+       ctx.Data["Project"] = project
+       ctx.Data["Boards"] = boards
+       ctx.Data["PageIsProjects"] = true
+       ctx.Data["RequiresDraggable"] = true
+
+       ctx.HTML(http.StatusOK, tplProjectsView)
+}
+
+// UpdateIssueProject change an issue's project
+func UpdateIssueProject(ctx *context.Context) {
+       issues := getActionIssues(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       projectID := ctx.QueryInt64("id")
+       for _, issue := range issues {
+               oldProjectID := issue.ProjectID()
+               if oldProjectID == projectID {
+                       continue
+               }
+
+               if err := models.ChangeProjectAssign(issue, ctx.User, projectID); err != nil {
+                       ctx.ServerError("ChangeProjectAssign", err)
+                       return
+               }
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "ok": true,
+       })
+}
+
+// DeleteProjectBoard allows for the deletion of a project board
+func DeleteProjectBoard(ctx *context.Context) {
+       if ctx.User == nil {
+               ctx.JSON(http.StatusForbidden, map[string]string{
+                       "message": "Only signed in users are allowed to perform this action.",
+               })
+               return
+       }
+
+       if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) {
+               ctx.JSON(http.StatusForbidden, map[string]string{
+                       "message": "Only authorized users are allowed to perform this action.",
+               })
+               return
+       }
+
+       project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
+       if err != nil {
+               if models.IsErrProjectNotExist(err) {
+                       ctx.NotFound("", nil)
+               } else {
+                       ctx.ServerError("GetProjectByID", err)
+               }
+               return
+       }
+
+       pb, err := models.GetProjectBoard(ctx.ParamsInt64(":boardID"))
+       if err != nil {
+               ctx.ServerError("GetProjectBoard", err)
+               return
+       }
+       if pb.ProjectID != ctx.ParamsInt64(":id") {
+               ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
+                       "message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", pb.ID, project.ID),
+               })
+               return
+       }
+
+       if project.RepoID != ctx.Repo.Repository.ID {
+               ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
+                       "message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", pb.ID, ctx.Repo.Repository.ID),
+               })
+               return
+       }
+
+       if err := models.DeleteProjectBoardByID(ctx.ParamsInt64(":boardID")); err != nil {
+               ctx.ServerError("DeleteProjectBoardByID", err)
+               return
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "ok": true,
+       })
+}
+
+// AddBoardToProjectPost allows a new board to be added to a project.
+func AddBoardToProjectPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
+       if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) {
+               ctx.JSON(http.StatusForbidden, map[string]string{
+                       "message": "Only authorized users are allowed to perform this action.",
+               })
+               return
+       }
+
+       project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
+       if err != nil {
+               if models.IsErrProjectNotExist(err) {
+                       ctx.NotFound("", nil)
+               } else {
+                       ctx.ServerError("GetProjectByID", err)
+               }
+               return
+       }
+
+       if err := models.NewProjectBoard(&models.ProjectBoard{
+               ProjectID: project.ID,
+               Title:     form.Title,
+               CreatorID: ctx.User.ID,
+       }); err != nil {
+               ctx.ServerError("NewProjectBoard", err)
+               return
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "ok": true,
+       })
+}
+
+func checkProjectBoardChangePermissions(ctx *context.Context) (*models.Project, *models.ProjectBoard) {
+       if ctx.User == nil {
+               ctx.JSON(http.StatusForbidden, map[string]string{
+                       "message": "Only signed in users are allowed to perform this action.",
+               })
+               return nil, nil
+       }
+
+       if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) {
+               ctx.JSON(http.StatusForbidden, map[string]string{
+                       "message": "Only authorized users are allowed to perform this action.",
+               })
+               return nil, nil
+       }
+
+       project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
+       if err != nil {
+               if models.IsErrProjectNotExist(err) {
+                       ctx.NotFound("", nil)
+               } else {
+                       ctx.ServerError("GetProjectByID", err)
+               }
+               return nil, nil
+       }
+
+       board, err := models.GetProjectBoard(ctx.ParamsInt64(":boardID"))
+       if err != nil {
+               ctx.ServerError("GetProjectBoard", err)
+               return nil, nil
+       }
+       if board.ProjectID != ctx.ParamsInt64(":id") {
+               ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
+                       "message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", board.ID, project.ID),
+               })
+               return nil, nil
+       }
+
+       if project.RepoID != ctx.Repo.Repository.ID {
+               ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
+                       "message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", board.ID, ctx.Repo.Repository.ID),
+               })
+               return nil, nil
+       }
+       return project, board
+}
+
+// EditProjectBoard allows a project board's to be updated
+func EditProjectBoard(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
+       _, board := checkProjectBoardChangePermissions(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       if form.Title != "" {
+               board.Title = form.Title
+       }
+
+       if form.Sorting != 0 {
+               board.Sorting = form.Sorting
+       }
+
+       if err := models.UpdateProjectBoard(board); err != nil {
+               ctx.ServerError("UpdateProjectBoard", err)
+               return
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "ok": true,
+       })
+}
+
+// SetDefaultProjectBoard set default board for uncategorized issues/pulls
+func SetDefaultProjectBoard(ctx *context.Context) {
+
+       project, board := checkProjectBoardChangePermissions(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       if err := models.SetDefaultBoard(project.ID, board.ID); err != nil {
+               ctx.ServerError("SetDefaultBoard", err)
+               return
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "ok": true,
+       })
+}
+
+// MoveIssueAcrossBoards move a card from one board to another in a project
+func MoveIssueAcrossBoards(ctx *context.Context) {
+
+       if ctx.User == nil {
+               ctx.JSON(http.StatusForbidden, map[string]string{
+                       "message": "Only signed in users are allowed to perform this action.",
+               })
+               return
+       }
+
+       if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) {
+               ctx.JSON(http.StatusForbidden, map[string]string{
+                       "message": "Only authorized users are allowed to perform this action.",
+               })
+               return
+       }
+
+       p, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
+       if err != nil {
+               if models.IsErrProjectNotExist(err) {
+                       ctx.NotFound("", nil)
+               } else {
+                       ctx.ServerError("GetProjectByID", err)
+               }
+               return
+       }
+       if p.RepoID != ctx.Repo.Repository.ID {
+               ctx.NotFound("", nil)
+               return
+       }
+
+       var board *models.ProjectBoard
+
+       if ctx.ParamsInt64(":boardID") == 0 {
+
+               board = &models.ProjectBoard{
+                       ID:        0,
+                       ProjectID: 0,
+                       Title:     ctx.Tr("repo.projects.type.uncategorized"),
+               }
+
+       } else {
+               board, err = models.GetProjectBoard(ctx.ParamsInt64(":boardID"))
+               if err != nil {
+                       if models.IsErrProjectBoardNotExist(err) {
+                               ctx.NotFound("", nil)
+                       } else {
+                               ctx.ServerError("GetProjectBoard", err)
+                       }
+                       return
+               }
+               if board.ProjectID != p.ID {
+                       ctx.NotFound("", nil)
+                       return
+               }
+       }
+
+       issue, err := models.GetIssueByID(ctx.ParamsInt64(":index"))
+       if err != nil {
+               if models.IsErrIssueNotExist(err) {
+                       ctx.NotFound("", nil)
+               } else {
+                       ctx.ServerError("GetIssueByID", err)
+               }
+
+               return
+       }
+
+       if err := models.MoveIssueAcrossProjectBoards(issue, board); err != nil {
+               ctx.ServerError("MoveIssueAcrossProjectBoards", err)
+               return
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "ok": true,
+       })
+}
+
+// CreateProject renders the generic project creation page
+func CreateProject(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.projects.new")
+       ctx.Data["ProjectTypes"] = models.GetProjectsConfig()
+       ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
+
+       ctx.HTML(http.StatusOK, tplGenericProjectsNew)
+}
+
+// CreateProjectPost creates an individual and/or organization project
+func CreateProjectPost(ctx *context.Context, form forms.UserCreateProjectForm) {
+
+       user := checkContextUser(ctx, form.UID)
+       if ctx.Written() {
+               return
+       }
+
+       ctx.Data["ContextUser"] = user
+
+       if ctx.HasError() {
+               ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
+               ctx.HTML(http.StatusOK, tplGenericProjectsNew)
+               return
+       }
+
+       var projectType = models.ProjectTypeIndividual
+       if user.IsOrganization() {
+               projectType = models.ProjectTypeOrganization
+       }
+
+       if err := models.NewProject(&models.Project{
+               Title:       form.Title,
+               Description: form.Content,
+               CreatorID:   user.ID,
+               BoardType:   form.BoardType,
+               Type:        projectType,
+       }); err != nil {
+               ctx.ServerError("NewProject", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title))
+       ctx.Redirect(setting.AppSubURL + "/")
+}
diff --git a/routers/web/repo/projects_test.go b/routers/web/repo/projects_test.go
new file mode 100644 (file)
index 0000000..c43cf6d
--- /dev/null
@@ -0,0 +1,28 @@
+// 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 repo
+
+import (
+       "testing"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/test"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestCheckProjectBoardChangePermissions(t *testing.T) {
+       models.PrepareTestEnv(t)
+       ctx := test.MockContext(t, "user2/repo1/projects/1/2")
+       test.LoadUser(t, ctx, 2)
+       test.LoadRepo(t, ctx, 1)
+       ctx.SetParams(":id", "1")
+       ctx.SetParams(":boardID", "2")
+
+       project, board := checkProjectBoardChangePermissions(ctx)
+       assert.NotNil(t, project)
+       assert.NotNil(t, board)
+       assert.False(t, ctx.Written())
+}
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
new file mode 100644 (file)
index 0000000..28f94c8
--- /dev/null
@@ -0,0 +1,1341 @@
+// Copyright 2018 The Gitea Authors.
+// Copyright 2014 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 repo
+
+import (
+       "container/list"
+       "crypto/subtle"
+       "errors"
+       "fmt"
+       "net/http"
+       "path"
+       "strings"
+       "time"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/notification"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/structs"
+       "code.gitea.io/gitea/modules/upload"
+       "code.gitea.io/gitea/modules/util"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/modules/web/middleware"
+       "code.gitea.io/gitea/routers/utils"
+       "code.gitea.io/gitea/services/forms"
+       "code.gitea.io/gitea/services/gitdiff"
+       pull_service "code.gitea.io/gitea/services/pull"
+       repo_service "code.gitea.io/gitea/services/repository"
+       "github.com/unknwon/com"
+)
+
+const (
+       tplFork        base.TplName = "repo/pulls/fork"
+       tplCompareDiff base.TplName = "repo/diff/compare"
+       tplPullCommits base.TplName = "repo/pulls/commits"
+       tplPullFiles   base.TplName = "repo/pulls/files"
+
+       pullRequestTemplateKey = "PullRequestTemplate"
+)
+
+var (
+       pullRequestTemplateCandidates = []string{
+               "PULL_REQUEST_TEMPLATE.md",
+               "pull_request_template.md",
+               ".gitea/PULL_REQUEST_TEMPLATE.md",
+               ".gitea/pull_request_template.md",
+               ".github/PULL_REQUEST_TEMPLATE.md",
+               ".github/pull_request_template.md",
+       }
+)
+
+func getRepository(ctx *context.Context, repoID int64) *models.Repository {
+       repo, err := models.GetRepositoryByID(repoID)
+       if err != nil {
+               if models.IsErrRepoNotExist(err) {
+                       ctx.NotFound("GetRepositoryByID", nil)
+               } else {
+                       ctx.ServerError("GetRepositoryByID", err)
+               }
+               return nil
+       }
+
+       perm, err := models.GetUserRepoPermission(repo, ctx.User)
+       if err != nil {
+               ctx.ServerError("GetUserRepoPermission", err)
+               return nil
+       }
+
+       if !perm.CanRead(models.UnitTypeCode) {
+               log.Trace("Permission Denied: User %-v cannot read %-v of repo %-v\n"+
+                       "User in repo has Permissions: %-+v",
+                       ctx.User,
+                       models.UnitTypeCode,
+                       ctx.Repo,
+                       perm)
+               ctx.NotFound("getRepository", nil)
+               return nil
+       }
+       return repo
+}
+
+func getForkRepository(ctx *context.Context) *models.Repository {
+       forkRepo := getRepository(ctx, ctx.ParamsInt64(":repoid"))
+       if ctx.Written() {
+               return nil
+       }
+
+       if forkRepo.IsEmpty {
+               log.Trace("Empty repository %-v", forkRepo)
+               ctx.NotFound("getForkRepository", nil)
+               return nil
+       }
+
+       if err := forkRepo.GetOwner(); err != nil {
+               ctx.ServerError("GetOwner", err)
+               return nil
+       }
+
+       ctx.Data["repo_name"] = forkRepo.Name
+       ctx.Data["description"] = forkRepo.Description
+       ctx.Data["IsPrivate"] = forkRepo.IsPrivate || forkRepo.Owner.Visibility == structs.VisibleTypePrivate
+       canForkToUser := forkRepo.OwnerID != ctx.User.ID && !ctx.User.HasForkedRepo(forkRepo.ID)
+
+       ctx.Data["ForkFrom"] = forkRepo.Owner.Name + "/" + forkRepo.Name
+       ctx.Data["ForkFromOwnerID"] = forkRepo.Owner.ID
+
+       if err := ctx.User.GetOwnedOrganizations(); err != nil {
+               ctx.ServerError("GetOwnedOrganizations", err)
+               return nil
+       }
+       var orgs []*models.User
+       for _, org := range ctx.User.OwnedOrgs {
+               if forkRepo.OwnerID != org.ID && !org.HasForkedRepo(forkRepo.ID) {
+                       orgs = append(orgs, org)
+               }
+       }
+
+       var traverseParentRepo = forkRepo
+       var err error
+       for {
+               if ctx.User.ID == traverseParentRepo.OwnerID {
+                       canForkToUser = false
+               } else {
+                       for i, org := range orgs {
+                               if org.ID == traverseParentRepo.OwnerID {
+                                       orgs = append(orgs[:i], orgs[i+1:]...)
+                                       break
+                               }
+                       }
+               }
+
+               if !traverseParentRepo.IsFork {
+                       break
+               }
+               traverseParentRepo, err = models.GetRepositoryByID(traverseParentRepo.ForkID)
+               if err != nil {
+                       ctx.ServerError("GetRepositoryByID", err)
+                       return nil
+               }
+       }
+
+       ctx.Data["CanForkToUser"] = canForkToUser
+       ctx.Data["Orgs"] = orgs
+
+       if canForkToUser {
+               ctx.Data["ContextUser"] = ctx.User
+       } else if len(orgs) > 0 {
+               ctx.Data["ContextUser"] = orgs[0]
+       }
+
+       return forkRepo
+}
+
+// Fork render repository fork page
+func Fork(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("new_fork")
+
+       getForkRepository(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       ctx.HTML(http.StatusOK, tplFork)
+}
+
+// ForkPost response for forking a repository
+func ForkPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.CreateRepoForm)
+       ctx.Data["Title"] = ctx.Tr("new_fork")
+
+       ctxUser := checkContextUser(ctx, form.UID)
+       if ctx.Written() {
+               return
+       }
+
+       forkRepo := getForkRepository(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       ctx.Data["ContextUser"] = ctxUser
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplFork)
+               return
+       }
+
+       var err error
+       var traverseParentRepo = forkRepo
+       for {
+               if ctxUser.ID == traverseParentRepo.OwnerID {
+                       ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
+                       return
+               }
+               repo, has := models.HasForkedRepo(ctxUser.ID, traverseParentRepo.ID)
+               if has {
+                       ctx.Redirect(ctxUser.HomeLink() + "/" + repo.Name)
+                       return
+               }
+               if !traverseParentRepo.IsFork {
+                       break
+               }
+               traverseParentRepo, err = models.GetRepositoryByID(traverseParentRepo.ForkID)
+               if err != nil {
+                       ctx.ServerError("GetRepositoryByID", err)
+                       return
+               }
+       }
+
+       // Check ownership of organization.
+       if ctxUser.IsOrganization() {
+               isOwner, err := ctxUser.IsOwnedBy(ctx.User.ID)
+               if err != nil {
+                       ctx.ServerError("IsOwnedBy", err)
+                       return
+               } else if !isOwner {
+                       ctx.Error(http.StatusForbidden)
+                       return
+               }
+       }
+
+       repo, err := repo_service.ForkRepository(ctx.User, ctxUser, forkRepo, form.RepoName, form.Description)
+       if err != nil {
+               ctx.Data["Err_RepoName"] = true
+               switch {
+               case models.IsErrRepoAlreadyExist(err):
+                       ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
+               case models.IsErrNameReserved(err):
+                       ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tplFork, &form)
+               case models.IsErrNamePatternNotAllowed(err):
+                       ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplFork, &form)
+               default:
+                       ctx.ServerError("ForkPost", err)
+               }
+               return
+       }
+
+       log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
+       ctx.Redirect(ctxUser.HomeLink() + "/" + repo.Name)
+}
+
+func checkPullInfo(ctx *context.Context) *models.Issue {
+       issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+       if err != nil {
+               if models.IsErrIssueNotExist(err) {
+                       ctx.NotFound("GetIssueByIndex", err)
+               } else {
+                       ctx.ServerError("GetIssueByIndex", err)
+               }
+               return nil
+       }
+       if err = issue.LoadPoster(); err != nil {
+               ctx.ServerError("LoadPoster", err)
+               return nil
+       }
+       if err := issue.LoadRepo(); err != nil {
+               ctx.ServerError("LoadRepo", err)
+               return nil
+       }
+       ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
+       ctx.Data["Issue"] = issue
+
+       if !issue.IsPull {
+               ctx.NotFound("ViewPullCommits", nil)
+               return nil
+       }
+
+       if err = issue.LoadPullRequest(); err != nil {
+               ctx.ServerError("LoadPullRequest", err)
+               return nil
+       }
+
+       if err = issue.PullRequest.LoadHeadRepo(); err != nil {
+               ctx.ServerError("LoadHeadRepo", err)
+               return nil
+       }
+
+       if ctx.IsSigned {
+               // Update issue-user.
+               if err = issue.ReadBy(ctx.User.ID); err != nil {
+                       ctx.ServerError("ReadBy", err)
+                       return nil
+               }
+       }
+
+       return issue
+}
+
+func setMergeTarget(ctx *context.Context, pull *models.PullRequest) {
+       if ctx.Repo.Owner.Name == pull.MustHeadUserName() {
+               ctx.Data["HeadTarget"] = pull.HeadBranch
+       } else if pull.HeadRepo == nil {
+               ctx.Data["HeadTarget"] = pull.MustHeadUserName() + ":" + pull.HeadBranch
+       } else {
+               ctx.Data["HeadTarget"] = pull.MustHeadUserName() + "/" + pull.HeadRepo.Name + ":" + pull.HeadBranch
+       }
+       ctx.Data["BaseTarget"] = pull.BaseBranch
+       ctx.Data["HeadBranchHTMLURL"] = pull.GetHeadBranchHTMLURL()
+       ctx.Data["BaseBranchHTMLURL"] = pull.GetBaseBranchHTMLURL()
+}
+
+// PrepareMergedViewPullInfo show meta information for a merged pull request view page
+func PrepareMergedViewPullInfo(ctx *context.Context, issue *models.Issue) *git.CompareInfo {
+       pull := issue.PullRequest
+
+       setMergeTarget(ctx, pull)
+       ctx.Data["HasMerged"] = true
+
+       compareInfo, err := ctx.Repo.GitRepo.GetCompareInfo(ctx.Repo.Repository.RepoPath(),
+               pull.MergeBase, pull.GetGitRefName())
+       if err != nil {
+               if strings.Contains(err.Error(), "fatal: Not a valid object name") || strings.Contains(err.Error(), "unknown revision or path not in the working tree") {
+                       ctx.Data["IsPullRequestBroken"] = true
+                       ctx.Data["BaseTarget"] = pull.BaseBranch
+                       ctx.Data["NumCommits"] = 0
+                       ctx.Data["NumFiles"] = 0
+                       return nil
+               }
+
+               ctx.ServerError("GetCompareInfo", err)
+               return nil
+       }
+       ctx.Data["NumCommits"] = compareInfo.Commits.Len()
+       ctx.Data["NumFiles"] = compareInfo.NumFiles
+
+       if compareInfo.Commits.Len() != 0 {
+               sha := compareInfo.Commits.Front().Value.(*git.Commit).ID.String()
+               commitStatuses, err := models.GetLatestCommitStatus(ctx.Repo.Repository.ID, sha, models.ListOptions{})
+               if err != nil {
+                       ctx.ServerError("GetLatestCommitStatus", err)
+                       return nil
+               }
+               if len(commitStatuses) != 0 {
+                       ctx.Data["LatestCommitStatuses"] = commitStatuses
+                       ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(commitStatuses)
+               }
+       }
+
+       return compareInfo
+}
+
+// PrepareViewPullInfo show meta information for a pull request preview page
+func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.CompareInfo {
+       repo := ctx.Repo.Repository
+       pull := issue.PullRequest
+
+       if err := pull.LoadHeadRepo(); err != nil {
+               ctx.ServerError("LoadHeadRepo", err)
+               return nil
+       }
+
+       if err := pull.LoadBaseRepo(); err != nil {
+               ctx.ServerError("LoadBaseRepo", err)
+               return nil
+       }
+
+       setMergeTarget(ctx, pull)
+
+       if err := pull.LoadProtectedBranch(); err != nil {
+               ctx.ServerError("LoadProtectedBranch", err)
+               return nil
+       }
+       ctx.Data["EnableStatusCheck"] = pull.ProtectedBranch != nil && pull.ProtectedBranch.EnableStatusCheck
+
+       baseGitRepo, err := git.OpenRepository(pull.BaseRepo.RepoPath())
+       if err != nil {
+               ctx.ServerError("OpenRepository", err)
+               return nil
+       }
+       defer baseGitRepo.Close()
+
+       if !baseGitRepo.IsBranchExist(pull.BaseBranch) {
+               ctx.Data["IsPullRequestBroken"] = true
+               ctx.Data["BaseTarget"] = pull.BaseBranch
+               ctx.Data["HeadTarget"] = pull.HeadBranch
+
+               sha, err := baseGitRepo.GetRefCommitID(pull.GetGitRefName())
+               if err != nil {
+                       ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err)
+                       return nil
+               }
+               commitStatuses, err := models.GetLatestCommitStatus(repo.ID, sha, models.ListOptions{})
+               if err != nil {
+                       ctx.ServerError("GetLatestCommitStatus", err)
+                       return nil
+               }
+               if len(commitStatuses) > 0 {
+                       ctx.Data["LatestCommitStatuses"] = commitStatuses
+                       ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(commitStatuses)
+               }
+
+               compareInfo, err := baseGitRepo.GetCompareInfo(pull.BaseRepo.RepoPath(),
+                       pull.MergeBase, pull.GetGitRefName())
+               if err != nil {
+                       if strings.Contains(err.Error(), "fatal: Not a valid object name") {
+                               ctx.Data["IsPullRequestBroken"] = true
+                               ctx.Data["BaseTarget"] = pull.BaseBranch
+                               ctx.Data["NumCommits"] = 0
+                               ctx.Data["NumFiles"] = 0
+                               return nil
+                       }
+
+                       ctx.ServerError("GetCompareInfo", err)
+                       return nil
+               }
+
+               ctx.Data["NumCommits"] = compareInfo.Commits.Len()
+               ctx.Data["NumFiles"] = compareInfo.NumFiles
+               return compareInfo
+       }
+
+       var headBranchExist bool
+       var headBranchSha string
+       // HeadRepo may be missing
+       if pull.HeadRepo != nil {
+               headGitRepo, err := git.OpenRepository(pull.HeadRepo.RepoPath())
+               if err != nil {
+                       ctx.ServerError("OpenRepository", err)
+                       return nil
+               }
+               defer headGitRepo.Close()
+
+               headBranchExist = headGitRepo.IsBranchExist(pull.HeadBranch)
+
+               if headBranchExist {
+                       headBranchSha, err = headGitRepo.GetBranchCommitID(pull.HeadBranch)
+                       if err != nil {
+                               ctx.ServerError("GetBranchCommitID", err)
+                               return nil
+                       }
+               }
+       }
+
+       if headBranchExist {
+               ctx.Data["UpdateAllowed"], err = pull_service.IsUserAllowedToUpdate(pull, ctx.User)
+               if err != nil {
+                       ctx.ServerError("IsUserAllowedToUpdate", err)
+                       return nil
+               }
+               ctx.Data["GetCommitMessages"] = pull_service.GetSquashMergeCommitMessages(pull)
+       }
+
+       sha, err := baseGitRepo.GetRefCommitID(pull.GetGitRefName())
+       if err != nil {
+               if git.IsErrNotExist(err) {
+                       ctx.Data["IsPullRequestBroken"] = true
+                       if pull.IsSameRepo() {
+                               ctx.Data["HeadTarget"] = pull.HeadBranch
+                       } else if pull.HeadRepo == nil {
+                               ctx.Data["HeadTarget"] = "<deleted>:" + pull.HeadBranch
+                       } else {
+                               ctx.Data["HeadTarget"] = pull.HeadRepo.OwnerName + ":" + pull.HeadBranch
+                       }
+                       ctx.Data["BaseTarget"] = pull.BaseBranch
+                       ctx.Data["NumCommits"] = 0
+                       ctx.Data["NumFiles"] = 0
+                       return nil
+               }
+               ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err)
+               return nil
+       }
+
+       commitStatuses, err := models.GetLatestCommitStatus(repo.ID, sha, models.ListOptions{})
+       if err != nil {
+               ctx.ServerError("GetLatestCommitStatus", err)
+               return nil
+       }
+       if len(commitStatuses) > 0 {
+               ctx.Data["LatestCommitStatuses"] = commitStatuses
+               ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(commitStatuses)
+       }
+
+       if pull.ProtectedBranch != nil && pull.ProtectedBranch.EnableStatusCheck {
+               ctx.Data["is_context_required"] = func(context string) bool {
+                       for _, c := range pull.ProtectedBranch.StatusCheckContexts {
+                               if c == context {
+                                       return true
+                               }
+                       }
+                       return false
+               }
+               ctx.Data["RequiredStatusCheckState"] = pull_service.MergeRequiredContextsCommitStatus(commitStatuses, pull.ProtectedBranch.StatusCheckContexts)
+       }
+
+       ctx.Data["HeadBranchMovedOn"] = headBranchSha != sha
+       ctx.Data["HeadBranchCommitID"] = headBranchSha
+       ctx.Data["PullHeadCommitID"] = sha
+
+       if pull.HeadRepo == nil || !headBranchExist || headBranchSha != sha {
+               ctx.Data["IsPullRequestBroken"] = true
+               if pull.IsSameRepo() {
+                       ctx.Data["HeadTarget"] = pull.HeadBranch
+               } else if pull.HeadRepo == nil {
+                       ctx.Data["HeadTarget"] = "<deleted>:" + pull.HeadBranch
+               } else {
+                       ctx.Data["HeadTarget"] = pull.HeadRepo.OwnerName + ":" + pull.HeadBranch
+               }
+       }
+
+       compareInfo, err := baseGitRepo.GetCompareInfo(pull.BaseRepo.RepoPath(),
+               git.BranchPrefix+pull.BaseBranch, pull.GetGitRefName())
+       if err != nil {
+               if strings.Contains(err.Error(), "fatal: Not a valid object name") {
+                       ctx.Data["IsPullRequestBroken"] = true
+                       ctx.Data["BaseTarget"] = pull.BaseBranch
+                       ctx.Data["NumCommits"] = 0
+                       ctx.Data["NumFiles"] = 0
+                       return nil
+               }
+
+               ctx.ServerError("GetCompareInfo", err)
+               return nil
+       }
+
+       ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
+
+       if pull.IsWorkInProgress() {
+               ctx.Data["IsPullWorkInProgress"] = true
+               ctx.Data["WorkInProgressPrefix"] = pull.GetWorkInProgressPrefix()
+       }
+
+       if pull.IsFilesConflicted() {
+               ctx.Data["IsPullFilesConflicted"] = true
+               ctx.Data["ConflictedFiles"] = pull.ConflictedFiles
+       }
+
+       ctx.Data["NumCommits"] = compareInfo.Commits.Len()
+       ctx.Data["NumFiles"] = compareInfo.NumFiles
+       return compareInfo
+}
+
+// ViewPullCommits show commits for a pull request
+func ViewPullCommits(ctx *context.Context) {
+       ctx.Data["PageIsPullList"] = true
+       ctx.Data["PageIsPullCommits"] = true
+
+       issue := checkPullInfo(ctx)
+       if ctx.Written() {
+               return
+       }
+       pull := issue.PullRequest
+
+       var commits *list.List
+       var prInfo *git.CompareInfo
+       if pull.HasMerged {
+               prInfo = PrepareMergedViewPullInfo(ctx, issue)
+       } else {
+               prInfo = PrepareViewPullInfo(ctx, issue)
+       }
+
+       if ctx.Written() {
+               return
+       } else if prInfo == nil {
+               ctx.NotFound("ViewPullCommits", nil)
+               return
+       }
+
+       ctx.Data["Username"] = ctx.Repo.Owner.Name
+       ctx.Data["Reponame"] = ctx.Repo.Repository.Name
+       commits = prInfo.Commits
+       commits = models.ValidateCommitsWithEmails(commits)
+       commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
+       commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
+       ctx.Data["Commits"] = commits
+       ctx.Data["CommitCount"] = commits.Len()
+
+       getBranchData(ctx, issue)
+       ctx.HTML(http.StatusOK, tplPullCommits)
+}
+
+// ViewPullFiles render pull request changed files list page
+func ViewPullFiles(ctx *context.Context) {
+       ctx.Data["PageIsPullList"] = true
+       ctx.Data["PageIsPullFiles"] = true
+
+       issue := checkPullInfo(ctx)
+       if ctx.Written() {
+               return
+       }
+       pull := issue.PullRequest
+
+       var (
+               diffRepoPath  string
+               startCommitID string
+               endCommitID   string
+               gitRepo       *git.Repository
+       )
+
+       var prInfo *git.CompareInfo
+       if pull.HasMerged {
+               prInfo = PrepareMergedViewPullInfo(ctx, issue)
+       } else {
+               prInfo = PrepareViewPullInfo(ctx, issue)
+       }
+
+       if ctx.Written() {
+               return
+       } else if prInfo == nil {
+               ctx.NotFound("ViewPullFiles", nil)
+               return
+       }
+
+       diffRepoPath = ctx.Repo.GitRepo.Path
+       gitRepo = ctx.Repo.GitRepo
+
+       headCommitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
+       if err != nil {
+               ctx.ServerError("GetRefCommitID", err)
+               return
+       }
+
+       startCommitID = prInfo.MergeBase
+       endCommitID = headCommitID
+
+       ctx.Data["Username"] = ctx.Repo.Owner.Name
+       ctx.Data["Reponame"] = ctx.Repo.Repository.Name
+       ctx.Data["AfterCommitID"] = endCommitID
+
+       diff, err := gitdiff.GetDiffRangeWithWhitespaceBehavior(diffRepoPath,
+               startCommitID, endCommitID, setting.Git.MaxGitDiffLines,
+               setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles,
+               gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
+       if err != nil {
+               ctx.ServerError("GetDiffRangeWithWhitespaceBehavior", err)
+               return
+       }
+
+       if err = diff.LoadComments(issue, ctx.User); err != nil {
+               ctx.ServerError("LoadComments", err)
+               return
+       }
+
+       if err = pull.LoadProtectedBranch(); err != nil {
+               ctx.ServerError("LoadProtectedBranch", err)
+               return
+       }
+
+       if pull.ProtectedBranch != nil {
+               glob := pull.ProtectedBranch.GetProtectedFilePatterns()
+               if len(glob) != 0 {
+                       for _, file := range diff.Files {
+                               file.IsProtected = pull.ProtectedBranch.IsProtectedFile(glob, file.Name)
+                       }
+               }
+       }
+
+       ctx.Data["Diff"] = diff
+       ctx.Data["DiffNotAvailable"] = diff.NumFiles == 0
+
+       baseCommit, err := ctx.Repo.GitRepo.GetCommit(startCommitID)
+       if err != nil {
+               ctx.ServerError("GetCommit", err)
+               return
+       }
+       commit, err := gitRepo.GetCommit(endCommitID)
+       if err != nil {
+               ctx.ServerError("GetCommit", err)
+               return
+       }
+
+       if ctx.IsSigned && ctx.User != nil {
+               if ctx.Data["CanMarkConversation"], err = models.CanMarkConversation(issue, ctx.User); err != nil {
+                       ctx.ServerError("CanMarkConversation", err)
+                       return
+               }
+       }
+
+       headTarget := path.Join(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
+       setCompareContext(ctx, baseCommit, commit, headTarget)
+
+       ctx.Data["RequireHighlightJS"] = true
+       ctx.Data["RequireSimpleMDE"] = true
+       ctx.Data["RequireTribute"] = true
+       if ctx.Data["Assignees"], err = ctx.Repo.Repository.GetAssignees(); err != nil {
+               ctx.ServerError("GetAssignees", err)
+               return
+       }
+       handleTeamMentions(ctx)
+       if ctx.Written() {
+               return
+       }
+       ctx.Data["CurrentReview"], err = models.GetCurrentReview(ctx.User, issue)
+       if err != nil && !models.IsErrReviewNotExist(err) {
+               ctx.ServerError("GetCurrentReview", err)
+               return
+       }
+       getBranchData(ctx, issue)
+       ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.User.ID)
+       ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
+       ctx.HTML(http.StatusOK, tplPullFiles)
+}
+
+// UpdatePullRequest merge PR's baseBranch into headBranch
+func UpdatePullRequest(ctx *context.Context) {
+       issue := checkPullInfo(ctx)
+       if ctx.Written() {
+               return
+       }
+       if issue.IsClosed {
+               ctx.NotFound("MergePullRequest", nil)
+               return
+       }
+       if issue.PullRequest.HasMerged {
+               ctx.NotFound("MergePullRequest", nil)
+               return
+       }
+
+       if err := issue.PullRequest.LoadBaseRepo(); err != nil {
+               ctx.ServerError("LoadBaseRepo", err)
+               return
+       }
+       if err := issue.PullRequest.LoadHeadRepo(); err != nil {
+               ctx.ServerError("LoadHeadRepo", err)
+               return
+       }
+
+       allowedUpdate, err := pull_service.IsUserAllowedToUpdate(issue.PullRequest, ctx.User)
+       if err != nil {
+               ctx.ServerError("IsUserAllowedToMerge", err)
+               return
+       }
+
+       // ToDo: add check if maintainers are allowed to change branch ... (need migration & co)
+       if !allowedUpdate {
+               ctx.Flash.Error(ctx.Tr("repo.pulls.update_not_allowed"))
+               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
+               return
+       }
+
+       // default merge commit message
+       message := fmt.Sprintf("Merge branch '%s' into %s", issue.PullRequest.BaseBranch, issue.PullRequest.HeadBranch)
+
+       if err = pull_service.Update(issue.PullRequest, ctx.User, message); err != nil {
+               if models.IsErrMergeConflicts(err) {
+                       conflictError := err.(models.ErrMergeConflicts)
+                       flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
+                               "Message": ctx.Tr("repo.pulls.merge_conflict"),
+                               "Summary": ctx.Tr("repo.pulls.merge_conflict_summary"),
+                               "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
+                       })
+                       if err != nil {
+                               ctx.ServerError("UpdatePullRequest.HTMLString", err)
+                               return
+                       }
+                       ctx.Flash.Error(flashError)
+                       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
+                       return
+               }
+               ctx.Flash.Error(err.Error())
+               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
+               return
+       }
+
+       time.Sleep(1 * time.Second)
+
+       ctx.Flash.Success(ctx.Tr("repo.pulls.update_branch_success"))
+       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
+}
+
+// MergePullRequest response for merging pull request
+func MergePullRequest(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.MergePullRequestForm)
+       issue := checkPullInfo(ctx)
+       if ctx.Written() {
+               return
+       }
+       if issue.IsClosed {
+               if issue.IsPull {
+                       ctx.Flash.Error(ctx.Tr("repo.pulls.is_closed"))
+                       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
+                       return
+               }
+               ctx.Flash.Error(ctx.Tr("repo.issues.closed_title"))
+               ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + fmt.Sprint(issue.Index))
+               return
+       }
+
+       pr := issue.PullRequest
+
+       allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, ctx.Repo.Permission, ctx.User)
+       if err != nil {
+               ctx.ServerError("IsUserAllowedToMerge", err)
+               return
+       }
+       if !allowedMerge {
+               ctx.Flash.Error(ctx.Tr("repo.pulls.update_not_allowed"))
+               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
+               return
+       }
+
+       if pr.HasMerged {
+               ctx.Flash.Error(ctx.Tr("repo.pulls.has_merged"))
+               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
+               return
+       }
+
+       // handle manually-merged mark
+       if models.MergeStyle(form.Do) == models.MergeStyleManuallyMerged {
+               if err = pull_service.MergedManually(pr, ctx.User, ctx.Repo.GitRepo, form.MergeCommitID); err != nil {
+                       if models.IsErrInvalidMergeStyle(err) {
+                               ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option"))
+                               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
+                               return
+                       } else if strings.Contains(err.Error(), "Wrong commit ID") {
+                               ctx.Flash.Error(ctx.Tr("repo.pulls.wrong_commit_id"))
+                               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
+                               return
+                       }
+
+                       ctx.ServerError("MergedManually", err)
+                       return
+               }
+
+               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
+               return
+       }
+
+       if !pr.CanAutoMerge() {
+               ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_not_ready"))
+               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
+               return
+       }
+
+       if pr.IsWorkInProgress() {
+               ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_wip"))
+               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
+               return
+       }
+
+       if err := pull_service.CheckPRReadyToMerge(pr, false); err != nil {
+               if !models.IsErrNotAllowedToMerge(err) {
+                       ctx.ServerError("Merge PR status", err)
+                       return
+               }
+               if isRepoAdmin, err := models.IsUserRepoAdmin(pr.BaseRepo, ctx.User); err != nil {
+                       ctx.ServerError("IsUserRepoAdmin", err)
+                       return
+               } else if !isRepoAdmin {
+                       ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_not_ready"))
+                       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
+                       return
+               }
+       }
+
+       if ctx.HasError() {
+               ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
+               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
+               return
+       }
+
+       message := strings.TrimSpace(form.MergeTitleField)
+       if len(message) == 0 {
+               if models.MergeStyle(form.Do) == models.MergeStyleMerge {
+                       message = pr.GetDefaultMergeMessage()
+               }
+               if models.MergeStyle(form.Do) == models.MergeStyleRebaseMerge {
+                       message = pr.GetDefaultMergeMessage()
+               }
+               if models.MergeStyle(form.Do) == models.MergeStyleSquash {
+                       message = pr.GetDefaultSquashMessage()
+               }
+       }
+
+       form.MergeMessageField = strings.TrimSpace(form.MergeMessageField)
+       if len(form.MergeMessageField) > 0 {
+               message += "\n\n" + form.MergeMessageField
+       }
+
+       pr.Issue = issue
+       pr.Issue.Repo = ctx.Repo.Repository
+
+       noDeps, err := models.IssueNoDependenciesLeft(issue)
+       if err != nil {
+               return
+       }
+
+       if !noDeps {
+               ctx.Flash.Error(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
+               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
+               return
+       }
+
+       if err = pull_service.Merge(pr, ctx.User, ctx.Repo.GitRepo, models.MergeStyle(form.Do), message); err != nil {
+               if models.IsErrInvalidMergeStyle(err) {
+                       ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option"))
+                       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
+                       return
+               } else if models.IsErrMergeConflicts(err) {
+                       conflictError := err.(models.ErrMergeConflicts)
+                       flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
+                               "Message": ctx.Tr("repo.editor.merge_conflict"),
+                               "Summary": ctx.Tr("repo.editor.merge_conflict_summary"),
+                               "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
+                       })
+                       if err != nil {
+                               ctx.ServerError("MergePullRequest.HTMLString", err)
+                               return
+                       }
+                       ctx.Flash.Error(flashError)
+                       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
+                       return
+               } else if models.IsErrRebaseConflicts(err) {
+                       conflictError := err.(models.ErrRebaseConflicts)
+                       flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
+                               "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)),
+                               "Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"),
+                               "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
+                       })
+                       if err != nil {
+                               ctx.ServerError("MergePullRequest.HTMLString", err)
+                               return
+                       }
+                       ctx.Flash.Error(flashError)
+                       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
+                       return
+               } else if models.IsErrMergeUnrelatedHistories(err) {
+                       log.Debug("MergeUnrelatedHistories error: %v", err)
+                       ctx.Flash.Error(ctx.Tr("repo.pulls.unrelated_histories"))
+                       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
+                       return
+               } else if git.IsErrPushOutOfDate(err) {
+                       log.Debug("MergePushOutOfDate error: %v", err)
+                       ctx.Flash.Error(ctx.Tr("repo.pulls.merge_out_of_date"))
+                       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
+                       return
+               } else if git.IsErrPushRejected(err) {
+                       log.Debug("MergePushRejected error: %v", err)
+                       pushrejErr := err.(*git.ErrPushRejected)
+                       message := pushrejErr.Message
+                       if len(message) == 0 {
+                               ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected_no_message"))
+                       } else {
+                               flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
+                                       "Message": ctx.Tr("repo.pulls.push_rejected"),
+                                       "Summary": ctx.Tr("repo.pulls.push_rejected_summary"),
+                                       "Details": utils.SanitizeFlashErrorString(pushrejErr.Message),
+                               })
+                               if err != nil {
+                                       ctx.ServerError("MergePullRequest.HTMLString", err)
+                                       return
+                               }
+                               ctx.Flash.Error(flashError)
+                       }
+                       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
+                       return
+               }
+               ctx.ServerError("Merge", err)
+               return
+       }
+
+       if err := stopTimerIfAvailable(ctx.User, issue); err != nil {
+               ctx.ServerError("CreateOrStopIssueStopwatch", err)
+               return
+       }
+
+       log.Trace("Pull request merged: %d", pr.ID)
+       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
+}
+
+func stopTimerIfAvailable(user *models.User, issue *models.Issue) error {
+
+       if models.StopwatchExists(user.ID, issue.ID) {
+               if err := models.CreateOrStopIssueStopwatch(user, issue); err != nil {
+                       return err
+               }
+       }
+
+       return nil
+}
+
+// CompareAndPullRequestPost response for creating pull request
+func CompareAndPullRequestPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.CreateIssueForm)
+       ctx.Data["Title"] = ctx.Tr("repo.pulls.compare_changes")
+       ctx.Data["PageIsComparePull"] = true
+       ctx.Data["IsDiffCompare"] = true
+       ctx.Data["RequireHighlightJS"] = true
+       ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
+       ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
+       upload.AddUploadContext(ctx, "comment")
+
+       var (
+               repo        = ctx.Repo.Repository
+               attachments []string
+       )
+
+       headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch := ParseCompareInfo(ctx)
+       if ctx.Written() {
+               return
+       }
+       defer headGitRepo.Close()
+
+       labelIDs, assigneeIDs, milestoneID, _ := ValidateRepoMetas(ctx, *form, true)
+       if ctx.Written() {
+               return
+       }
+
+       if setting.Attachment.Enabled {
+               attachments = form.Files
+       }
+
+       if ctx.HasError() {
+               middleware.AssignForm(form, ctx.Data)
+
+               // This stage is already stop creating new pull request, so it does not matter if it has
+               // something to compare or not.
+               PrepareCompareDiff(ctx, headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch,
+                       gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
+               if ctx.Written() {
+                       return
+               }
+
+               ctx.HTML(http.StatusOK, tplCompareDiff)
+               return
+       }
+
+       if util.IsEmptyString(form.Title) {
+               PrepareCompareDiff(ctx, headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch,
+                       gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
+               if ctx.Written() {
+                       return
+               }
+
+               ctx.RenderWithErr(ctx.Tr("repo.issues.new.title_empty"), tplCompareDiff, form)
+               return
+       }
+
+       pullIssue := &models.Issue{
+               RepoID:      repo.ID,
+               Title:       form.Title,
+               PosterID:    ctx.User.ID,
+               Poster:      ctx.User,
+               MilestoneID: milestoneID,
+               IsPull:      true,
+               Content:     form.Content,
+       }
+       pullRequest := &models.PullRequest{
+               HeadRepoID: headRepo.ID,
+               BaseRepoID: repo.ID,
+               HeadBranch: headBranch,
+               BaseBranch: baseBranch,
+               HeadRepo:   headRepo,
+               BaseRepo:   repo,
+               MergeBase:  prInfo.MergeBase,
+               Type:       models.PullRequestGitea,
+       }
+       // 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(repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
+               if models.IsErrUserDoesNotHaveAccessToRepo(err) {
+                       ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
+                       return
+               } else if git.IsErrPushRejected(err) {
+                       pushrejErr := err.(*git.ErrPushRejected)
+                       message := pushrejErr.Message
+                       if len(message) == 0 {
+                               ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected_no_message"))
+                       } else {
+                               flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
+                                       "Message": ctx.Tr("repo.pulls.push_rejected"),
+                                       "Summary": ctx.Tr("repo.pulls.push_rejected_summary"),
+                                       "Details": utils.SanitizeFlashErrorString(pushrejErr.Message),
+                               })
+                               if err != nil {
+                                       ctx.ServerError("CompareAndPullRequest.HTMLString", err)
+                                       return
+                               }
+                               ctx.Flash.Error(flashError)
+                       }
+                       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pullIssue.Index))
+                       return
+               }
+               ctx.ServerError("NewPullRequest", err)
+               return
+       }
+
+       log.Trace("Pull request created: %d/%d", repo.ID, pullIssue.ID)
+       ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pullIssue.Index))
+}
+
+// TriggerTask response for a trigger task request
+func TriggerTask(ctx *context.Context) {
+       pusherID := ctx.QueryInt64("pusher")
+       branch := ctx.Query("branch")
+       secret := ctx.Query("secret")
+       if len(branch) == 0 || len(secret) == 0 || pusherID <= 0 {
+               ctx.Error(http.StatusNotFound)
+               log.Trace("TriggerTask: branch or secret is empty, or pusher ID is not valid")
+               return
+       }
+       owner, repo := parseOwnerAndRepo(ctx)
+       if ctx.Written() {
+               return
+       }
+       got := []byte(base.EncodeMD5(owner.Salt))
+       want := []byte(secret)
+       if subtle.ConstantTimeCompare(got, want) != 1 {
+               ctx.Error(http.StatusNotFound)
+               log.Trace("TriggerTask [%s/%s]: invalid secret", owner.Name, repo.Name)
+               return
+       }
+
+       pusher, err := models.GetUserByID(pusherID)
+       if err != nil {
+               if models.IsErrUserNotExist(err) {
+                       ctx.Error(http.StatusNotFound)
+               } else {
+                       ctx.ServerError("GetUserByID", err)
+               }
+               return
+       }
+
+       log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name)
+
+       go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true, "", "")
+       ctx.Status(202)
+}
+
+// CleanUpPullRequest responses for delete merged branch when PR has been merged
+func CleanUpPullRequest(ctx *context.Context) {
+       issue := checkPullInfo(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       pr := issue.PullRequest
+
+       // Don't cleanup unmerged and unclosed PRs
+       if !pr.HasMerged && !issue.IsClosed {
+               ctx.NotFound("CleanUpPullRequest", nil)
+               return
+       }
+
+       if err := pr.LoadHeadRepo(); err != nil {
+               ctx.ServerError("LoadHeadRepo", err)
+               return
+       } else if pr.HeadRepo == nil {
+               // Forked repository has already been deleted
+               ctx.NotFound("CleanUpPullRequest", nil)
+               return
+       } else if err = pr.LoadBaseRepo(); err != nil {
+               ctx.ServerError("LoadBaseRepo", err)
+               return
+       } else if err = pr.HeadRepo.GetOwner(); err != nil {
+               ctx.ServerError("HeadRepo.GetOwner", err)
+               return
+       }
+
+       perm, err := models.GetUserRepoPermission(pr.HeadRepo, ctx.User)
+       if err != nil {
+               ctx.ServerError("GetUserRepoPermission", err)
+               return
+       }
+       if !perm.CanWrite(models.UnitTypeCode) {
+               ctx.NotFound("CleanUpPullRequest", nil)
+               return
+       }
+
+       fullBranchName := pr.HeadRepo.Owner.Name + "/" + pr.HeadBranch
+
+       gitRepo, err := git.OpenRepository(pr.HeadRepo.RepoPath())
+       if err != nil {
+               ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.RepoPath()), err)
+               return
+       }
+       defer gitRepo.Close()
+
+       gitBaseRepo, err := git.OpenRepository(pr.BaseRepo.RepoPath())
+       if err != nil {
+               ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.BaseRepo.RepoPath()), err)
+               return
+       }
+       defer gitBaseRepo.Close()
+
+       defer func() {
+               ctx.JSON(http.StatusOK, map[string]interface{}{
+                       "redirect": pr.BaseRepo.Link() + "/pulls/" + fmt.Sprint(issue.Index),
+               })
+       }()
+
+       // Check if branch has no new commits
+       headCommitID, err := gitBaseRepo.GetRefCommitID(pr.GetGitRefName())
+       if err != nil {
+               log.Error("GetRefCommitID: %v", err)
+               ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
+               return
+       }
+       branchCommitID, err := gitRepo.GetBranchCommitID(pr.HeadBranch)
+       if err != nil {
+               log.Error("GetBranchCommitID: %v", err)
+               ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
+               return
+       }
+       if headCommitID != branchCommitID {
+               ctx.Flash.Error(ctx.Tr("repo.branch.delete_branch_has_new_commits", fullBranchName))
+               return
+       }
+
+       if err := repo_service.DeleteBranch(ctx.User, pr.HeadRepo, gitRepo, pr.HeadBranch); err != nil {
+               switch {
+               case git.IsErrBranchNotExist(err):
+                       ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
+               case errors.Is(err, repo_service.ErrBranchIsDefault):
+                       ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
+               case errors.Is(err, repo_service.ErrBranchIsProtected):
+                       ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
+               default:
+                       log.Error("DeleteBranch: %v", err)
+                       ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
+               }
+               return
+       }
+
+       if err := models.AddDeletePRBranchComment(ctx.User, pr.BaseRepo, issue.ID, pr.HeadBranch); err != nil {
+               // Do not fail here as branch has already been deleted
+               log.Error("DeleteBranch: %v", err)
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.branch.deletion_success", fullBranchName))
+}
+
+// DownloadPullDiff render a pull's raw diff
+func DownloadPullDiff(ctx *context.Context) {
+       DownloadPullDiffOrPatch(ctx, false)
+}
+
+// DownloadPullPatch render a pull's raw patch
+func DownloadPullPatch(ctx *context.Context) {
+       DownloadPullDiffOrPatch(ctx, true)
+}
+
+// DownloadPullDiffOrPatch render a pull's raw diff or patch
+func DownloadPullDiffOrPatch(ctx *context.Context, patch bool) {
+       issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+       if err != nil {
+               if models.IsErrIssueNotExist(err) {
+                       ctx.NotFound("GetIssueByIndex", err)
+               } else {
+                       ctx.ServerError("GetIssueByIndex", err)
+               }
+               return
+       }
+
+       // Return not found if it's not a pull request
+       if !issue.IsPull {
+               ctx.NotFound("DownloadPullDiff",
+                       fmt.Errorf("Issue is not a pull request"))
+               return
+       }
+
+       if err = issue.LoadPullRequest(); err != nil {
+               ctx.ServerError("LoadPullRequest", err)
+               return
+       }
+
+       pr := issue.PullRequest
+
+       if err := pull_service.DownloadDiffOrPatch(pr, ctx, patch); err != nil {
+               ctx.ServerError("DownloadDiffOrPatch", err)
+               return
+       }
+}
+
+// UpdatePullRequestTarget change pull request's target branch
+func UpdatePullRequestTarget(ctx *context.Context) {
+       issue := GetActionIssue(ctx)
+       pr := issue.PullRequest
+       if ctx.Written() {
+               return
+       }
+       if !issue.IsPull {
+               ctx.Error(http.StatusNotFound)
+               return
+       }
+
+       if !ctx.IsSigned || (!issue.IsPoster(ctx.User.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
+               ctx.Error(http.StatusForbidden)
+               return
+       }
+
+       targetBranch := ctx.QueryTrim("target_branch")
+       if len(targetBranch) == 0 {
+               ctx.Error(http.StatusNoContent)
+               return
+       }
+
+       if err := pull_service.ChangeTargetBranch(pr, ctx.User, targetBranch); err != nil {
+               if models.IsErrPullRequestAlreadyExists(err) {
+                       err := err.(models.ErrPullRequestAlreadyExists)
+
+                       RepoRelPath := ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name
+                       errorMessage := ctx.Tr("repo.pulls.has_pull_request", ctx.Repo.RepoLink, RepoRelPath, err.IssueID)
+
+                       ctx.Flash.Error(errorMessage)
+                       ctx.JSON(http.StatusConflict, map[string]interface{}{
+                               "error":      err.Error(),
+                               "user_error": errorMessage,
+                       })
+               } else if models.IsErrIssueIsClosed(err) {
+                       errorMessage := ctx.Tr("repo.pulls.is_closed")
+
+                       ctx.Flash.Error(errorMessage)
+                       ctx.JSON(http.StatusConflict, map[string]interface{}{
+                               "error":      err.Error(),
+                               "user_error": errorMessage,
+                       })
+               } else if models.IsErrPullRequestHasMerged(err) {
+                       errorMessage := ctx.Tr("repo.pulls.has_merged")
+
+                       ctx.Flash.Error(errorMessage)
+                       ctx.JSON(http.StatusConflict, map[string]interface{}{
+                               "error":      err.Error(),
+                               "user_error": errorMessage,
+                       })
+               } else if models.IsErrBranchesEqual(err) {
+                       errorMessage := ctx.Tr("repo.pulls.nothing_to_compare")
+
+                       ctx.Flash.Error(errorMessage)
+                       ctx.JSON(http.StatusBadRequest, map[string]interface{}{
+                               "error":      err.Error(),
+                               "user_error": errorMessage,
+                       })
+               } else {
+                       ctx.ServerError("UpdatePullRequestTarget", err)
+               }
+               return
+       }
+       notification.NotifyPullRequestChangeTargetBranch(ctx.User, pr, targetBranch)
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "base_branch": pr.BaseBranch,
+       })
+}
diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go
new file mode 100644 (file)
index 0000000..9e505c3
--- /dev/null
@@ -0,0 +1,238 @@
+// Copyright 2018 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 repo
+
+import (
+       "fmt"
+       "net/http"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+       pull_service "code.gitea.io/gitea/services/pull"
+)
+
+const (
+       tplConversation base.TplName = "repo/diff/conversation"
+       tplNewComment   base.TplName = "repo/diff/new_comment"
+)
+
+// RenderNewCodeCommentForm will render the form for creating a new review comment
+func RenderNewCodeCommentForm(ctx *context.Context) {
+       issue := GetActionIssue(ctx)
+       if !issue.IsPull {
+               return
+       }
+       currentReview, err := models.GetCurrentReview(ctx.User, issue)
+       if err != nil && !models.IsErrReviewNotExist(err) {
+               ctx.ServerError("GetCurrentReview", err)
+               return
+       }
+       ctx.Data["PageIsPullFiles"] = true
+       ctx.Data["Issue"] = issue
+       ctx.Data["CurrentReview"] = currentReview
+       pullHeadCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(issue.PullRequest.GetGitRefName())
+       if err != nil {
+               ctx.ServerError("GetRefCommitID", err)
+               return
+       }
+       ctx.Data["AfterCommitID"] = pullHeadCommitID
+       ctx.HTML(http.StatusOK, tplNewComment)
+}
+
+// CreateCodeComment will create a code comment including an pending review if required
+func CreateCodeComment(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.CodeCommentForm)
+       issue := GetActionIssue(ctx)
+       if !issue.IsPull {
+               return
+       }
+       if ctx.Written() {
+               return
+       }
+
+       if ctx.HasError() {
+               ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
+               ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
+               return
+       }
+
+       signedLine := form.Line
+       if form.Side == "previous" {
+               signedLine *= -1
+       }
+
+       comment, err := pull_service.CreateCodeComment(
+               ctx.User,
+               ctx.Repo.GitRepo,
+               issue,
+               signedLine,
+               form.Content,
+               form.TreePath,
+               form.IsReview,
+               form.Reply,
+               form.LatestCommitID,
+       )
+       if err != nil {
+               ctx.ServerError("CreateCodeComment", err)
+               return
+       }
+
+       if comment == nil {
+               log.Trace("Comment not created: %-v #%d[%d]", ctx.Repo.Repository, issue.Index, issue.ID)
+               ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
+               return
+       }
+
+       log.Trace("Comment created: %-v #%d[%d] Comment[%d]", ctx.Repo.Repository, issue.Index, issue.ID, comment.ID)
+
+       if form.Origin == "diff" {
+               renderConversation(ctx, comment)
+               return
+       }
+       ctx.Redirect(comment.HTMLURL())
+}
+
+// UpdateResolveConversation add or remove an Conversation resolved mark
+func UpdateResolveConversation(ctx *context.Context) {
+       origin := ctx.Query("origin")
+       action := ctx.Query("action")
+       commentID := ctx.QueryInt64("comment_id")
+
+       comment, err := models.GetCommentByID(commentID)
+       if err != nil {
+               ctx.ServerError("GetIssueByID", err)
+               return
+       }
+
+       if err = comment.LoadIssue(); err != nil {
+               ctx.ServerError("comment.LoadIssue", err)
+               return
+       }
+
+       var permResult bool
+       if permResult, err = models.CanMarkConversation(comment.Issue, ctx.User); err != nil {
+               ctx.ServerError("CanMarkConversation", err)
+               return
+       }
+       if !permResult {
+               ctx.Error(http.StatusForbidden)
+               return
+       }
+
+       if !comment.Issue.IsPull {
+               ctx.Error(http.StatusBadRequest)
+               return
+       }
+
+       if action == "Resolve" || action == "UnResolve" {
+               err = models.MarkConversation(comment, ctx.User, action == "Resolve")
+               if err != nil {
+                       ctx.ServerError("MarkConversation", err)
+                       return
+               }
+       } else {
+               ctx.Error(http.StatusBadRequest)
+               return
+       }
+
+       if origin == "diff" {
+               renderConversation(ctx, comment)
+               return
+       }
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "ok": true,
+       })
+}
+
+func renderConversation(ctx *context.Context, comment *models.Comment) {
+       comments, err := models.FetchCodeCommentsByLine(comment.Issue, ctx.User, comment.TreePath, comment.Line)
+       if err != nil {
+               ctx.ServerError("FetchCodeCommentsByLine", err)
+               return
+       }
+       ctx.Data["PageIsPullFiles"] = true
+       ctx.Data["comments"] = comments
+       ctx.Data["CanMarkConversation"] = true
+       ctx.Data["Issue"] = comment.Issue
+       if err = comment.Issue.LoadPullRequest(); err != nil {
+               ctx.ServerError("comment.Issue.LoadPullRequest", err)
+               return
+       }
+       pullHeadCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(comment.Issue.PullRequest.GetGitRefName())
+       if err != nil {
+               ctx.ServerError("GetRefCommitID", err)
+               return
+       }
+       ctx.Data["AfterCommitID"] = pullHeadCommitID
+       ctx.HTML(http.StatusOK, tplConversation)
+}
+
+// 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) {
+       form := web.GetForm(ctx).(*forms.SubmitReviewForm)
+       issue := GetActionIssue(ctx)
+       if !issue.IsPull {
+               return
+       }
+       if ctx.Written() {
+               return
+       }
+       if ctx.HasError() {
+               ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
+               ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
+               return
+       }
+
+       reviewType := form.ReviewType()
+       switch reviewType {
+       case models.ReviewTypeUnknown:
+               ctx.ServerError("ReviewType", fmt.Errorf("unknown ReviewType: %s", form.Type))
+               return
+
+       // can not approve/reject your own PR
+       case models.ReviewTypeApprove, models.ReviewTypeReject:
+               if issue.IsPoster(ctx.User.ID) {
+                       var translated string
+                       if reviewType == models.ReviewTypeApprove {
+                               translated = ctx.Tr("repo.issues.review.self.approval")
+                       } else {
+                               translated = ctx.Tr("repo.issues.review.self.rejection")
+                       }
+
+                       ctx.Flash.Error(translated)
+                       ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
+                       return
+               }
+       }
+
+       _, comm, err := pull_service.SubmitReview(ctx.User, ctx.Repo.GitRepo, issue, reviewType, form.Content, form.CommitID)
+       if err != nil {
+               if models.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 {
+                       ctx.ServerError("SubmitReview", err)
+               }
+               return
+       }
+
+       ctx.Redirect(fmt.Sprintf("%s/pulls/%d#%s", ctx.Repo.RepoLink, issue.Index, comm.HashTag()))
+}
+
+// DismissReview dismissing stale review by repo admin
+func DismissReview(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.DismissReviewForm)
+       comm, err := pull_service.DismissReview(form.ReviewID, form.Message, ctx.User, true)
+       if err != nil {
+               ctx.ServerError("pull_service.DismissReview", err)
+               return
+       }
+
+       ctx.Redirect(fmt.Sprintf("%s/pulls/%d#%s", ctx.Repo.RepoLink, comm.Issue.Index, comm.HashTag()))
+}
diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go
new file mode 100644 (file)
index 0000000..b7730e4
--- /dev/null
@@ -0,0 +1,512 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2018 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 repo
+
+import (
+       "fmt"
+       "net/http"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/convert"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/markup"
+       "code.gitea.io/gitea/modules/markup/markdown"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/upload"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+       releaseservice "code.gitea.io/gitea/services/release"
+)
+
+const (
+       tplReleases   base.TplName = "repo/release/list"
+       tplReleaseNew base.TplName = "repo/release/new"
+)
+
+// calReleaseNumCommitsBehind calculates given release has how many commits behind release target.
+func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *models.Release, countCache map[string]int64) error {
+       // Fast return if release target is same as default branch.
+       if repoCtx.BranchName == release.Target {
+               release.NumCommitsBehind = repoCtx.CommitsCount - release.NumCommits
+               return nil
+       }
+
+       // Get count if not exists
+       if _, ok := countCache[release.Target]; !ok {
+               if repoCtx.GitRepo.IsBranchExist(release.Target) {
+                       commit, err := repoCtx.GitRepo.GetBranchCommit(release.Target)
+                       if err != nil {
+                               return fmt.Errorf("GetBranchCommit: %v", err)
+                       }
+                       countCache[release.Target], err = commit.CommitsCount()
+                       if err != nil {
+                               return fmt.Errorf("CommitsCount: %v", err)
+                       }
+               } else {
+                       // Use NumCommits of the newest release on that target
+                       countCache[release.Target] = release.NumCommits
+               }
+       }
+       release.NumCommitsBehind = countCache[release.Target] - release.NumCommits
+       return nil
+}
+
+// Releases render releases list page
+func Releases(ctx *context.Context) {
+       releasesOrTags(ctx, false)
+}
+
+// TagsList render tags list page
+func TagsList(ctx *context.Context) {
+       releasesOrTags(ctx, true)
+}
+
+func releasesOrTags(ctx *context.Context, isTagList bool) {
+       ctx.Data["PageIsReleaseList"] = true
+       ctx.Data["DefaultBranch"] = ctx.Repo.Repository.DefaultBranch
+       ctx.Data["IsViewBranch"] = false
+       ctx.Data["IsViewTag"] = true
+       // Disable the showCreateNewBranch form in the dropdown on this page.
+       ctx.Data["CanCreateBranch"] = false
+       ctx.Data["HideBranchesInDropdown"] = true
+
+       if isTagList {
+               ctx.Data["Title"] = ctx.Tr("repo.release.tags")
+               ctx.Data["PageIsTagList"] = true
+       } else {
+               ctx.Data["Title"] = ctx.Tr("repo.release.releases")
+               ctx.Data["PageIsTagList"] = false
+       }
+
+       tags, err := ctx.Repo.GitRepo.GetTags()
+       if err != nil {
+               ctx.ServerError("GetTags", err)
+               return
+       }
+       ctx.Data["Tags"] = tags
+
+       writeAccess := ctx.Repo.CanWrite(models.UnitTypeReleases)
+       ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
+
+       opts := models.FindReleasesOptions{
+               ListOptions: models.ListOptions{
+                       Page:     ctx.QueryInt("page"),
+                       PageSize: convert.ToCorrectPageSize(ctx.QueryInt("limit")),
+               },
+               IncludeDrafts: writeAccess && !isTagList,
+               IncludeTags:   isTagList,
+       }
+
+       releases, err := models.GetReleasesByRepoID(ctx.Repo.Repository.ID, opts)
+       if err != nil {
+               ctx.ServerError("GetReleasesByRepoID", err)
+               return
+       }
+
+       count, err := models.GetReleaseCountByRepoID(ctx.Repo.Repository.ID, opts)
+       if err != nil {
+               ctx.ServerError("GetReleaseCountByRepoID", err)
+               return
+       }
+
+       if err = models.GetReleaseAttachments(releases...); err != nil {
+               ctx.ServerError("GetReleaseAttachments", err)
+               return
+       }
+
+       // Temporary cache commits count of used branches to speed up.
+       countCache := make(map[string]int64)
+       cacheUsers := make(map[int64]*models.User)
+       if ctx.User != nil {
+               cacheUsers[ctx.User.ID] = ctx.User
+       }
+       var ok bool
+
+       for _, r := range releases {
+               if r.Publisher, ok = cacheUsers[r.PublisherID]; !ok {
+                       r.Publisher, err = models.GetUserByID(r.PublisherID)
+                       if err != nil {
+                               if models.IsErrUserNotExist(err) {
+                                       r.Publisher = models.NewGhostUser()
+                               } else {
+                                       ctx.ServerError("GetUserByID", err)
+                                       return
+                               }
+                       }
+                       cacheUsers[r.PublisherID] = r.Publisher
+               }
+
+               r.Note, err = markdown.RenderString(&markup.RenderContext{
+                       URLPrefix: ctx.Repo.RepoLink,
+                       Metas:     ctx.Repo.Repository.ComposeMetas(),
+               }, r.Note)
+               if err != nil {
+                       ctx.ServerError("RenderString", err)
+                       return
+               }
+
+               if r.IsDraft {
+                       continue
+               }
+
+               if err := calReleaseNumCommitsBehind(ctx.Repo, r, countCache); err != nil {
+                       ctx.ServerError("calReleaseNumCommitsBehind", err)
+                       return
+               }
+       }
+
+       ctx.Data["Releases"] = releases
+       ctx.Data["ReleasesNum"] = len(releases)
+
+       pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
+       pager.SetDefaultParams(ctx)
+       ctx.Data["Page"] = pager
+
+       ctx.HTML(http.StatusOK, tplReleases)
+}
+
+// SingleRelease renders a single release's page
+func SingleRelease(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.release.releases")
+       ctx.Data["PageIsReleaseList"] = true
+
+       writeAccess := ctx.Repo.CanWrite(models.UnitTypeReleases)
+       ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
+
+       release, err := models.GetRelease(ctx.Repo.Repository.ID, ctx.Params("*"))
+       if err != nil {
+               if models.IsErrReleaseNotExist(err) {
+                       ctx.NotFound("GetRelease", err)
+                       return
+               }
+               ctx.ServerError("GetReleasesByRepoID", err)
+               return
+       }
+
+       err = models.GetReleaseAttachments(release)
+       if err != nil {
+               ctx.ServerError("GetReleaseAttachments", err)
+               return
+       }
+
+       release.Publisher, err = models.GetUserByID(release.PublisherID)
+       if err != nil {
+               if models.IsErrUserNotExist(err) {
+                       release.Publisher = models.NewGhostUser()
+               } else {
+                       ctx.ServerError("GetUserByID", err)
+                       return
+               }
+       }
+       if !release.IsDraft {
+               if err := calReleaseNumCommitsBehind(ctx.Repo, release, make(map[string]int64)); err != nil {
+                       ctx.ServerError("calReleaseNumCommitsBehind", err)
+                       return
+               }
+       }
+       release.Note, err = markdown.RenderString(&markup.RenderContext{
+               URLPrefix: ctx.Repo.RepoLink,
+               Metas:     ctx.Repo.Repository.ComposeMetas(),
+       }, release.Note)
+       if err != nil {
+               ctx.ServerError("RenderString", err)
+               return
+       }
+
+       ctx.Data["Releases"] = []*models.Release{release}
+       ctx.HTML(http.StatusOK, tplReleases)
+}
+
+// LatestRelease redirects to the latest release
+func LatestRelease(ctx *context.Context) {
+       release, err := models.GetLatestReleaseByRepoID(ctx.Repo.Repository.ID)
+       if err != nil {
+               if models.IsErrReleaseNotExist(err) {
+                       ctx.NotFound("LatestRelease", err)
+                       return
+               }
+               ctx.ServerError("GetLatestReleaseByRepoID", err)
+               return
+       }
+
+       if err := release.LoadAttributes(); err != nil {
+               ctx.ServerError("LoadAttributes", err)
+               return
+       }
+
+       ctx.Redirect(release.HTMLURL())
+}
+
+// NewRelease render creating or edit release page
+func NewRelease(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.release.new_release")
+       ctx.Data["PageIsReleaseList"] = true
+       ctx.Data["RequireSimpleMDE"] = true
+       ctx.Data["RequireTribute"] = true
+       ctx.Data["tag_target"] = ctx.Repo.Repository.DefaultBranch
+       if tagName := ctx.Query("tag"); len(tagName) > 0 {
+               rel, err := models.GetRelease(ctx.Repo.Repository.ID, tagName)
+               if err != nil && !models.IsErrReleaseNotExist(err) {
+                       ctx.ServerError("GetRelease", err)
+                       return
+               }
+
+               if rel != nil {
+                       rel.Repo = ctx.Repo.Repository
+                       if err := rel.LoadAttributes(); err != nil {
+                               ctx.ServerError("LoadAttributes", err)
+                               return
+                       }
+
+                       ctx.Data["tag_name"] = rel.TagName
+                       ctx.Data["tag_target"] = rel.Target
+                       ctx.Data["title"] = rel.Title
+                       ctx.Data["content"] = rel.Note
+                       ctx.Data["attachments"] = rel.Attachments
+               }
+       }
+       ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
+       upload.AddUploadContext(ctx, "release")
+       ctx.HTML(http.StatusOK, tplReleaseNew)
+}
+
+// NewReleasePost response for creating a release
+func NewReleasePost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.NewReleaseForm)
+       ctx.Data["Title"] = ctx.Tr("repo.release.new_release")
+       ctx.Data["PageIsReleaseList"] = true
+       ctx.Data["RequireSimpleMDE"] = true
+       ctx.Data["RequireTribute"] = true
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplReleaseNew)
+               return
+       }
+
+       if !ctx.Repo.GitRepo.IsBranchExist(form.Target) {
+               ctx.RenderWithErr(ctx.Tr("form.target_branch_not_exist"), tplReleaseNew, &form)
+               return
+       }
+
+       var attachmentUUIDs []string
+       if setting.Attachment.Enabled {
+               attachmentUUIDs = form.Files
+       }
+
+       rel, err := models.GetRelease(ctx.Repo.Repository.ID, form.TagName)
+       if err != nil {
+               if !models.IsErrReleaseNotExist(err) {
+                       ctx.ServerError("GetRelease", err)
+                       return
+               }
+
+               msg := ""
+               if len(form.Title) > 0 && form.AddTagMsg {
+                       msg = form.Title + "\n\n" + form.Content
+               }
+
+               if len(form.TagOnly) > 0 {
+                       if err = releaseservice.CreateNewTag(ctx.User, ctx.Repo.Repository, form.Target, form.TagName, msg); err != nil {
+                               if models.IsErrTagAlreadyExists(err) {
+                                       e := err.(models.ErrTagAlreadyExists)
+                                       ctx.Flash.Error(ctx.Tr("repo.branch.tag_collision", e.TagName))
+                                       ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
+                                       return
+                               }
+
+                               ctx.ServerError("releaseservice.CreateNewTag", err)
+                               return
+                       }
+
+                       ctx.Flash.Success(ctx.Tr("repo.tag.create_success", form.TagName))
+                       ctx.Redirect(ctx.Repo.RepoLink + "/src/tag/" + form.TagName)
+                       return
+               }
+
+               rel = &models.Release{
+                       RepoID:       ctx.Repo.Repository.ID,
+                       PublisherID:  ctx.User.ID,
+                       Title:        form.Title,
+                       TagName:      form.TagName,
+                       Target:       form.Target,
+                       Note:         form.Content,
+                       IsDraft:      len(form.Draft) > 0,
+                       IsPrerelease: form.Prerelease,
+                       IsTag:        false,
+               }
+
+               if err = releaseservice.CreateRelease(ctx.Repo.GitRepo, rel, attachmentUUIDs, msg); err != nil {
+                       ctx.Data["Err_TagName"] = true
+                       switch {
+                       case models.IsErrReleaseAlreadyExist(err):
+                               ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
+                       case models.IsErrInvalidTagName(err):
+                               ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form)
+                       default:
+                               ctx.ServerError("CreateRelease", err)
+                       }
+                       return
+               }
+       } else {
+               if !rel.IsTag {
+                       ctx.Data["Err_TagName"] = true
+                       ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
+                       return
+               }
+
+               rel.Title = form.Title
+               rel.Note = form.Content
+               rel.Target = form.Target
+               rel.IsDraft = len(form.Draft) > 0
+               rel.IsPrerelease = form.Prerelease
+               rel.PublisherID = ctx.User.ID
+               rel.IsTag = false
+
+               if err = releaseservice.UpdateRelease(ctx.User, ctx.Repo.GitRepo, rel, attachmentUUIDs, nil, nil); err != nil {
+                       ctx.Data["Err_TagName"] = true
+                       ctx.ServerError("UpdateRelease", err)
+                       return
+               }
+       }
+       log.Trace("Release created: %s/%s:%s", ctx.User.LowerName, ctx.Repo.Repository.Name, form.TagName)
+
+       ctx.Redirect(ctx.Repo.RepoLink + "/releases")
+}
+
+// EditRelease render release edit page
+func EditRelease(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.release.edit_release")
+       ctx.Data["PageIsReleaseList"] = true
+       ctx.Data["PageIsEditRelease"] = true
+       ctx.Data["RequireSimpleMDE"] = true
+       ctx.Data["RequireTribute"] = true
+       ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
+       upload.AddUploadContext(ctx, "release")
+
+       tagName := ctx.Params("*")
+       rel, err := models.GetRelease(ctx.Repo.Repository.ID, tagName)
+       if err != nil {
+               if models.IsErrReleaseNotExist(err) {
+                       ctx.NotFound("GetRelease", err)
+               } else {
+                       ctx.ServerError("GetRelease", err)
+               }
+               return
+       }
+       ctx.Data["ID"] = rel.ID
+       ctx.Data["tag_name"] = rel.TagName
+       ctx.Data["tag_target"] = rel.Target
+       ctx.Data["title"] = rel.Title
+       ctx.Data["content"] = rel.Note
+       ctx.Data["prerelease"] = rel.IsPrerelease
+       ctx.Data["IsDraft"] = rel.IsDraft
+
+       rel.Repo = ctx.Repo.Repository
+       if err := rel.LoadAttributes(); err != nil {
+               ctx.ServerError("LoadAttributes", err)
+               return
+       }
+       ctx.Data["attachments"] = rel.Attachments
+
+       ctx.HTML(http.StatusOK, tplReleaseNew)
+}
+
+// EditReleasePost response for edit release
+func EditReleasePost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.EditReleaseForm)
+       ctx.Data["Title"] = ctx.Tr("repo.release.edit_release")
+       ctx.Data["PageIsReleaseList"] = true
+       ctx.Data["PageIsEditRelease"] = true
+       ctx.Data["RequireSimpleMDE"] = true
+       ctx.Data["RequireTribute"] = true
+
+       tagName := ctx.Params("*")
+       rel, err := models.GetRelease(ctx.Repo.Repository.ID, tagName)
+       if err != nil {
+               if models.IsErrReleaseNotExist(err) {
+                       ctx.NotFound("GetRelease", err)
+               } else {
+                       ctx.ServerError("GetRelease", err)
+               }
+               return
+       }
+       if rel.IsTag {
+               ctx.NotFound("GetRelease", err)
+               return
+       }
+       ctx.Data["tag_name"] = rel.TagName
+       ctx.Data["tag_target"] = rel.Target
+       ctx.Data["title"] = rel.Title
+       ctx.Data["content"] = rel.Note
+       ctx.Data["prerelease"] = rel.IsPrerelease
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplReleaseNew)
+               return
+       }
+
+       const delPrefix = "attachment-del-"
+       const editPrefix = "attachment-edit-"
+       var addAttachmentUUIDs, delAttachmentUUIDs []string
+       var editAttachments = make(map[string]string) // uuid -> new name
+       if setting.Attachment.Enabled {
+               addAttachmentUUIDs = form.Files
+               for k, v := range ctx.Req.Form {
+                       if strings.HasPrefix(k, delPrefix) && v[0] == "true" {
+                               delAttachmentUUIDs = append(delAttachmentUUIDs, k[len(delPrefix):])
+                       } else if strings.HasPrefix(k, editPrefix) {
+                               editAttachments[k[len(editPrefix):]] = v[0]
+                       }
+               }
+       }
+
+       rel.Title = form.Title
+       rel.Note = form.Content
+       rel.IsDraft = len(form.Draft) > 0
+       rel.IsPrerelease = form.Prerelease
+       if err = releaseservice.UpdateRelease(ctx.User, ctx.Repo.GitRepo,
+               rel, addAttachmentUUIDs, delAttachmentUUIDs, editAttachments); err != nil {
+               ctx.ServerError("UpdateRelease", err)
+               return
+       }
+       ctx.Redirect(ctx.Repo.RepoLink + "/releases")
+}
+
+// DeleteRelease delete a release
+func DeleteRelease(ctx *context.Context) {
+       deleteReleaseOrTag(ctx, false)
+}
+
+// DeleteTag delete a tag
+func DeleteTag(ctx *context.Context) {
+       deleteReleaseOrTag(ctx, true)
+}
+
+func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) {
+       if err := releaseservice.DeleteReleaseByID(ctx.QueryInt64("id"), ctx.User, isDelTag); err != nil {
+               ctx.Flash.Error("DeleteReleaseByID: " + err.Error())
+       } else {
+               if isDelTag {
+                       ctx.Flash.Success(ctx.Tr("repo.release.deletion_tag_success"))
+               } else {
+                       ctx.Flash.Success(ctx.Tr("repo.release.deletion_success"))
+               }
+       }
+
+       if isDelTag {
+               ctx.JSON(http.StatusOK, map[string]interface{}{
+                       "redirect": ctx.Repo.RepoLink + "/tags",
+               })
+               return
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": ctx.Repo.RepoLink + "/releases",
+       })
+}
diff --git a/routers/web/repo/release_test.go b/routers/web/repo/release_test.go
new file mode 100644 (file)
index 0000000..004a6ef
--- /dev/null
@@ -0,0 +1,64 @@
+// 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 repo
+
+import (
+       "testing"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/test"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+)
+
+func TestNewReleasePost(t *testing.T) {
+       for _, testCase := range []struct {
+               RepoID  int64
+               UserID  int64
+               TagName string
+               Form    forms.NewReleaseForm
+       }{
+               {
+                       RepoID:  1,
+                       UserID:  2,
+                       TagName: "v1.1", // pre-existing tag
+                       Form: forms.NewReleaseForm{
+                               TagName: "newtag",
+                               Target:  "master",
+                               Title:   "title",
+                               Content: "content",
+                       },
+               },
+               {
+                       RepoID:  1,
+                       UserID:  2,
+                       TagName: "newtag",
+                       Form: forms.NewReleaseForm{
+                               TagName: "newtag",
+                               Target:  "master",
+                               Title:   "title",
+                               Content: "content",
+                       },
+               },
+       } {
+               models.PrepareTestEnv(t)
+
+               ctx := test.MockContext(t, "user2/repo1/releases/new")
+               test.LoadUser(t, ctx, 2)
+               test.LoadRepo(t, ctx, 1)
+               test.LoadGitRepo(t, ctx)
+               web.SetForm(ctx, &testCase.Form)
+               NewReleasePost(ctx)
+               models.AssertExistsAndLoadBean(t, &models.Release{
+                       RepoID:      1,
+                       PublisherID: 2,
+                       TagName:     testCase.Form.TagName,
+                       Target:      testCase.Form.Target,
+                       Title:       testCase.Form.Title,
+                       Note:        testCase.Form.Content,
+               }, models.Cond("is_draft=?", len(testCase.Form.Draft) > 0))
+               ctx.Repo.GitRepo.Close()
+       }
+}
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
new file mode 100644 (file)
index 0000000..f149e92
--- /dev/null
@@ -0,0 +1,388 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// 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 repo
+
+import (
+       "errors"
+       "fmt"
+       "net/http"
+       "strings"
+       "time"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/web"
+       archiver_service "code.gitea.io/gitea/services/archiver"
+       "code.gitea.io/gitea/services/forms"
+       repo_service "code.gitea.io/gitea/services/repository"
+)
+
+const (
+       tplCreate       base.TplName = "repo/create"
+       tplAlertDetails base.TplName = "base/alert_details"
+)
+
+// MustBeNotEmpty render when a repo is a empty git dir
+func MustBeNotEmpty(ctx *context.Context) {
+       if ctx.Repo.Repository.IsEmpty {
+               ctx.NotFound("MustBeNotEmpty", nil)
+       }
+}
+
+// MustBeEditable check that repo can be edited
+func MustBeEditable(ctx *context.Context) {
+       if !ctx.Repo.Repository.CanEnableEditor() || ctx.Repo.IsViewCommit {
+               ctx.NotFound("", nil)
+               return
+       }
+}
+
+// MustBeAbleToUpload check that repo can be uploaded to
+func MustBeAbleToUpload(ctx *context.Context) {
+       if !setting.Repository.Upload.Enabled {
+               ctx.NotFound("", nil)
+       }
+}
+
+func checkContextUser(ctx *context.Context, uid int64) *models.User {
+       orgs, err := models.GetOrgsCanCreateRepoByUserID(ctx.User.ID)
+       if err != nil {
+               ctx.ServerError("GetOrgsCanCreateRepoByUserID", err)
+               return nil
+       }
+
+       if !ctx.User.IsAdmin {
+               orgsAvailable := []*models.User{}
+               for i := 0; i < len(orgs); i++ {
+                       if orgs[i].CanCreateRepo() {
+                               orgsAvailable = append(orgsAvailable, orgs[i])
+                       }
+               }
+               ctx.Data["Orgs"] = orgsAvailable
+       } else {
+               ctx.Data["Orgs"] = orgs
+       }
+
+       // Not equal means current user is an organization.
+       if uid == ctx.User.ID || uid == 0 {
+               return ctx.User
+       }
+
+       org, err := models.GetUserByID(uid)
+       if models.IsErrUserNotExist(err) {
+               return ctx.User
+       }
+
+       if err != nil {
+               ctx.ServerError("GetUserByID", fmt.Errorf("[%d]: %v", uid, err))
+               return nil
+       }
+
+       // Check ownership of organization.
+       if !org.IsOrganization() {
+               ctx.Error(http.StatusForbidden)
+               return nil
+       }
+       if !ctx.User.IsAdmin {
+               canCreate, err := org.CanCreateOrgRepo(ctx.User.ID)
+               if err != nil {
+                       ctx.ServerError("CanCreateOrgRepo", err)
+                       return nil
+               } else if !canCreate {
+                       ctx.Error(http.StatusForbidden)
+                       return nil
+               }
+       } else {
+               ctx.Data["Orgs"] = orgs
+       }
+       return org
+}
+
+func getRepoPrivate(ctx *context.Context) bool {
+       switch strings.ToLower(setting.Repository.DefaultPrivate) {
+       case setting.RepoCreatingLastUserVisibility:
+               return ctx.User.LastRepoVisibility
+       case setting.RepoCreatingPrivate:
+               return true
+       case setting.RepoCreatingPublic:
+               return false
+       default:
+               return ctx.User.LastRepoVisibility
+       }
+}
+
+// Create render creating repository page
+func Create(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("new_repo")
+
+       // Give default value for template to render.
+       ctx.Data["Gitignores"] = models.Gitignores
+       ctx.Data["LabelTemplates"] = models.LabelTemplates
+       ctx.Data["Licenses"] = models.Licenses
+       ctx.Data["Readmes"] = models.Readmes
+       ctx.Data["readme"] = "Default"
+       ctx.Data["private"] = getRepoPrivate(ctx)
+       ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate
+       ctx.Data["default_branch"] = setting.Repository.DefaultBranch
+
+       ctxUser := checkContextUser(ctx, ctx.QueryInt64("org"))
+       if ctx.Written() {
+               return
+       }
+       ctx.Data["ContextUser"] = ctxUser
+
+       ctx.Data["repo_template_name"] = ctx.Tr("repo.template_select")
+       templateID := ctx.QueryInt64("template_id")
+       if templateID > 0 {
+               templateRepo, err := models.GetRepositoryByID(templateID)
+               if err == nil && templateRepo.CheckUnitUser(ctxUser, models.UnitTypeCode) {
+                       ctx.Data["repo_template"] = templateID
+                       ctx.Data["repo_template_name"] = templateRepo.Name
+               }
+       }
+
+       ctx.Data["CanCreateRepo"] = ctx.User.CanCreateRepo()
+       ctx.Data["MaxCreationLimit"] = ctx.User.MaxCreationLimit()
+
+       ctx.HTML(http.StatusOK, tplCreate)
+}
+
+func handleCreateError(ctx *context.Context, owner *models.User, err error, name string, tpl base.TplName, form interface{}) {
+       switch {
+       case models.IsErrReachLimitOfRepo(err):
+               ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_creation", owner.MaxCreationLimit()), tpl, form)
+       case models.IsErrRepoAlreadyExist(err):
+               ctx.Data["Err_RepoName"] = true
+               ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form)
+       case models.IsErrRepoFilesAlreadyExist(err):
+               ctx.Data["Err_RepoName"] = true
+               switch {
+               case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
+                       ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form)
+               case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
+                       ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form)
+               case setting.Repository.AllowDeleteOfUnadoptedRepositories:
+                       ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form)
+               default:
+                       ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form)
+               }
+       case models.IsErrNameReserved(err):
+               ctx.Data["Err_RepoName"] = true
+               ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tpl, form)
+       case models.IsErrNamePatternNotAllowed(err):
+               ctx.Data["Err_RepoName"] = true
+               ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tpl, form)
+       default:
+               ctx.ServerError(name, err)
+       }
+}
+
+// CreatePost response for creating repository
+func CreatePost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.CreateRepoForm)
+       ctx.Data["Title"] = ctx.Tr("new_repo")
+
+       ctx.Data["Gitignores"] = models.Gitignores
+       ctx.Data["LabelTemplates"] = models.LabelTemplates
+       ctx.Data["Licenses"] = models.Licenses
+       ctx.Data["Readmes"] = models.Readmes
+
+       ctx.Data["CanCreateRepo"] = ctx.User.CanCreateRepo()
+       ctx.Data["MaxCreationLimit"] = ctx.User.MaxCreationLimit()
+
+       ctxUser := checkContextUser(ctx, form.UID)
+       if ctx.Written() {
+               return
+       }
+       ctx.Data["ContextUser"] = ctxUser
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplCreate)
+               return
+       }
+
+       var repo *models.Repository
+       var err error
+       if form.RepoTemplate > 0 {
+               opts := models.GenerateRepoOptions{
+                       Name:        form.RepoName,
+                       Description: form.Description,
+                       Private:     form.Private,
+                       GitContent:  form.GitContent,
+                       Topics:      form.Topics,
+                       GitHooks:    form.GitHooks,
+                       Webhooks:    form.Webhooks,
+                       Avatar:      form.Avatar,
+                       IssueLabels: form.Labels,
+               }
+
+               if !opts.IsValid() {
+                       ctx.RenderWithErr(ctx.Tr("repo.template.one_item"), tplCreate, form)
+                       return
+               }
+
+               templateRepo := getRepository(ctx, form.RepoTemplate)
+               if ctx.Written() {
+                       return
+               }
+
+               if !templateRepo.IsTemplate {
+                       ctx.RenderWithErr(ctx.Tr("repo.template.invalid"), tplCreate, form)
+                       return
+               }
+
+               repo, err = repo_service.GenerateRepository(ctx.User, ctxUser, templateRepo, opts)
+               if err == nil {
+                       log.Trace("Repository generated [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)
+                       ctx.Redirect(ctxUser.HomeLink() + "/" + repo.Name)
+                       return
+               }
+       } else {
+               repo, err = repo_service.CreateRepository(ctx.User, ctxUser, models.CreateRepoOptions{
+                       Name:          form.RepoName,
+                       Description:   form.Description,
+                       Gitignores:    form.Gitignores,
+                       IssueLabels:   form.IssueLabels,
+                       License:       form.License,
+                       Readme:        form.Readme,
+                       IsPrivate:     form.Private || setting.Repository.ForcePrivate,
+                       DefaultBranch: form.DefaultBranch,
+                       AutoInit:      form.AutoInit,
+                       IsTemplate:    form.Template,
+                       TrustModel:    models.ToTrustModel(form.TrustModel),
+               })
+               if err == nil {
+                       log.Trace("Repository created [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)
+                       ctx.Redirect(ctxUser.HomeLink() + "/" + repo.Name)
+                       return
+               }
+       }
+
+       handleCreateError(ctx, ctxUser, err, "CreatePost", tplCreate, &form)
+}
+
+// Action response for actions to a repository
+func Action(ctx *context.Context) {
+       var err error
+       switch ctx.Params(":action") {
+       case "watch":
+               err = models.WatchRepo(ctx.User.ID, ctx.Repo.Repository.ID, true)
+       case "unwatch":
+               err = models.WatchRepo(ctx.User.ID, ctx.Repo.Repository.ID, false)
+       case "star":
+               err = models.StarRepo(ctx.User.ID, ctx.Repo.Repository.ID, true)
+       case "unstar":
+               err = models.StarRepo(ctx.User.ID, ctx.Repo.Repository.ID, false)
+       case "accept_transfer":
+               err = acceptOrRejectRepoTransfer(ctx, true)
+       case "reject_transfer":
+               err = acceptOrRejectRepoTransfer(ctx, false)
+       case "desc": // FIXME: this is not used
+               if !ctx.Repo.IsOwner() {
+                       ctx.Error(http.StatusNotFound)
+                       return
+               }
+
+               ctx.Repo.Repository.Description = ctx.Query("desc")
+               ctx.Repo.Repository.Website = ctx.Query("site")
+               err = models.UpdateRepository(ctx.Repo.Repository, false)
+       }
+
+       if err != nil {
+               ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err)
+               return
+       }
+
+       ctx.RedirectToFirst(ctx.Query("redirect_to"), ctx.Repo.RepoLink)
+}
+
+func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error {
+       repoTransfer, err := models.GetPendingRepositoryTransfer(ctx.Repo.Repository)
+       if err != nil {
+               return err
+       }
+
+       if err := repoTransfer.LoadAttributes(); err != nil {
+               return err
+       }
+
+       if !repoTransfer.CanUserAcceptTransfer(ctx.User) {
+               return errors.New("user does not have enough permissions")
+       }
+
+       if accept {
+               if err := repo_service.TransferOwnership(repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams); err != nil {
+                       return err
+               }
+               ctx.Flash.Success(ctx.Tr("repo.settings.transfer.success"))
+       } else {
+               if err := models.CancelRepositoryTransfer(ctx.Repo.Repository); err != nil {
+                       return err
+               }
+               ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected"))
+       }
+
+       ctx.Redirect(ctx.Repo.Repository.HTMLURL())
+       return nil
+}
+
+// RedirectDownload return a file based on the following infos:
+func RedirectDownload(ctx *context.Context) {
+       var (
+               vTag     = ctx.Params("vTag")
+               fileName = ctx.Params("fileName")
+       )
+       tagNames := []string{vTag}
+       curRepo := ctx.Repo.Repository
+       releases, err := models.GetReleasesByRepoIDAndNames(models.DefaultDBContext(), curRepo.ID, tagNames)
+       if err != nil {
+               if models.IsErrAttachmentNotExist(err) {
+                       ctx.Error(http.StatusNotFound)
+                       return
+               }
+               ctx.ServerError("RedirectDownload", err)
+               return
+       }
+       if len(releases) == 1 {
+               release := releases[0]
+               att, err := models.GetAttachmentByReleaseIDFileName(release.ID, fileName)
+               if err != nil {
+                       ctx.Error(http.StatusNotFound)
+                       return
+               }
+               if att != nil {
+                       ctx.Redirect(att.DownloadURL())
+                       return
+               }
+       }
+       ctx.Error(http.StatusNotFound)
+}
+
+// InitiateDownload will enqueue an archival request, as needed.  It may submit
+// a request that's already in-progress, but the archiver service will just
+// kind of drop it on the floor if this is the case.
+func InitiateDownload(ctx *context.Context) {
+       uri := ctx.Params("*")
+       aReq := archiver_service.DeriveRequestFrom(ctx, uri)
+
+       if aReq == nil {
+               ctx.Error(http.StatusNotFound)
+               return
+       }
+
+       complete := aReq.IsComplete()
+       if !complete {
+               aReq = archiver_service.ArchiveRepository(aReq)
+               complete, _ = aReq.TimedWaitForCompletion(ctx, 2*time.Second)
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "complete": complete,
+       })
+}
diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go
new file mode 100644 (file)
index 0000000..d9604ba
--- /dev/null
@@ -0,0 +1,55 @@
+// 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 repo
+
+import (
+       "net/http"
+       "strings"
+
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       code_indexer "code.gitea.io/gitea/modules/indexer/code"
+       "code.gitea.io/gitea/modules/setting"
+)
+
+const tplSearch base.TplName = "repo/search"
+
+// Search render repository search page
+func Search(ctx *context.Context) {
+       if !setting.Indexer.RepoIndexerEnabled {
+               ctx.Redirect(ctx.Repo.RepoLink, 302)
+               return
+       }
+       language := strings.TrimSpace(ctx.Query("l"))
+       keyword := strings.TrimSpace(ctx.Query("q"))
+       page := ctx.QueryInt("page")
+       if page <= 0 {
+               page = 1
+       }
+       queryType := strings.TrimSpace(ctx.Query("t"))
+       isMatch := queryType == "match"
+
+       total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch([]int64{ctx.Repo.Repository.ID},
+               language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
+       if err != nil {
+               ctx.ServerError("SearchResults", err)
+               return
+       }
+       ctx.Data["Keyword"] = keyword
+       ctx.Data["Language"] = language
+       ctx.Data["queryType"] = queryType
+       ctx.Data["SourcePath"] = ctx.Repo.Repository.HTMLURL()
+       ctx.Data["SearchResults"] = searchResults
+       ctx.Data["SearchResultLanguages"] = searchResultLanguages
+       ctx.Data["RequireHighlightJS"] = true
+       ctx.Data["PageIsViewCode"] = true
+
+       pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
+       pager.SetDefaultParams(ctx)
+       pager.AddParam(ctx, "l", "Language")
+       ctx.Data["Page"] = pager
+
+       ctx.HTML(http.StatusOK, tplSearch)
+}
diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go
new file mode 100644 (file)
index 0000000..21a8249
--- /dev/null
@@ -0,0 +1,1053 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2018 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 repo
+
+import (
+       "errors"
+       "fmt"
+       "io/ioutil"
+       "net/http"
+       "strings"
+       "time"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/lfs"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/migrations"
+       "code.gitea.io/gitea/modules/repository"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/structs"
+       "code.gitea.io/gitea/modules/timeutil"
+       "code.gitea.io/gitea/modules/typesniffer"
+       "code.gitea.io/gitea/modules/validation"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/routers/utils"
+       "code.gitea.io/gitea/services/forms"
+       "code.gitea.io/gitea/services/mailer"
+       mirror_service "code.gitea.io/gitea/services/mirror"
+       repo_service "code.gitea.io/gitea/services/repository"
+)
+
+const (
+       tplSettingsOptions base.TplName = "repo/settings/options"
+       tplCollaboration   base.TplName = "repo/settings/collaboration"
+       tplBranches        base.TplName = "repo/settings/branches"
+       tplGithooks        base.TplName = "repo/settings/githooks"
+       tplGithookEdit     base.TplName = "repo/settings/githook_edit"
+       tplDeployKeys      base.TplName = "repo/settings/deploy_keys"
+       tplProtectedBranch base.TplName = "repo/settings/protected_branch"
+)
+
+// Settings show a repository's settings page
+func Settings(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.settings")
+       ctx.Data["PageIsSettingsOptions"] = true
+       ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate
+
+       signing, _ := models.SigningKey(ctx.Repo.Repository.RepoPath())
+       ctx.Data["SigningKeyAvailable"] = len(signing) > 0
+       ctx.Data["SigningSettings"] = setting.Repository.Signing
+
+       ctx.HTML(http.StatusOK, tplSettingsOptions)
+}
+
+// SettingsPost response for changes of a repository
+func SettingsPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.RepoSettingForm)
+       ctx.Data["Title"] = ctx.Tr("repo.settings")
+       ctx.Data["PageIsSettingsOptions"] = true
+
+       repo := ctx.Repo.Repository
+
+       switch ctx.Query("action") {
+       case "update":
+               if ctx.HasError() {
+                       ctx.HTML(http.StatusOK, tplSettingsOptions)
+                       return
+               }
+
+               newRepoName := form.RepoName
+               // Check if repository name has been changed.
+               if repo.LowerName != strings.ToLower(newRepoName) {
+                       // Close the GitRepo if open
+                       if ctx.Repo.GitRepo != nil {
+                               ctx.Repo.GitRepo.Close()
+                               ctx.Repo.GitRepo = nil
+                       }
+                       if err := repo_service.ChangeRepositoryName(ctx.User, repo, newRepoName); err != nil {
+                               ctx.Data["Err_RepoName"] = true
+                               switch {
+                               case models.IsErrRepoAlreadyExist(err):
+                                       ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplSettingsOptions, &form)
+                               case models.IsErrNameReserved(err):
+                                       ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tplSettingsOptions, &form)
+                               case models.IsErrRepoFilesAlreadyExist(err):
+                                       ctx.Data["Err_RepoName"] = true
+                                       switch {
+                                       case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
+                                               ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplSettingsOptions, form)
+                                       case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
+                                               ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplSettingsOptions, form)
+                                       case setting.Repository.AllowDeleteOfUnadoptedRepositories:
+                                               ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form)
+                                       default:
+                                               ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form)
+                                       }
+                               case models.IsErrNamePatternNotAllowed(err):
+                                       ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
+                               default:
+                                       ctx.ServerError("ChangeRepositoryName", err)
+                               }
+                               return
+                       }
+
+                       log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName)
+               }
+               // In case it's just a case change.
+               repo.Name = newRepoName
+               repo.LowerName = strings.ToLower(newRepoName)
+               repo.Description = form.Description
+               repo.Website = form.Website
+               repo.IsTemplate = form.Template
+
+               // Visibility of forked repository is forced sync with base repository.
+               if repo.IsFork {
+                       form.Private = repo.BaseRepo.IsPrivate || repo.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate
+               }
+
+               visibilityChanged := repo.IsPrivate != form.Private
+               // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
+               if visibilityChanged && setting.Repository.ForcePrivate && !form.Private && !ctx.User.IsAdmin {
+                       ctx.ServerError("Force Private enabled", errors.New("cannot change private repository to public"))
+                       return
+               }
+
+               repo.IsPrivate = form.Private
+               if err := models.UpdateRepository(repo, visibilityChanged); err != nil {
+                       ctx.ServerError("UpdateRepository", err)
+                       return
+               }
+               log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+
+               ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+               ctx.Redirect(repo.Link() + "/settings")
+
+       case "mirror":
+               if !repo.IsMirror {
+                       ctx.NotFound("", nil)
+                       return
+               }
+
+               // This section doesn't require repo_name/RepoName to be set in the form, don't show it
+               // as an error on the UI for this action
+               ctx.Data["Err_RepoName"] = nil
+
+               interval, err := time.ParseDuration(form.Interval)
+               if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
+                       ctx.Data["Err_Interval"] = true
+                       ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
+               } else {
+                       ctx.Repo.Mirror.EnablePrune = form.EnablePrune
+                       ctx.Repo.Mirror.Interval = interval
+                       if interval != 0 {
+                               ctx.Repo.Mirror.NextUpdateUnix = timeutil.TimeStampNow().AddDuration(interval)
+                       } else {
+                               ctx.Repo.Mirror.NextUpdateUnix = 0
+                       }
+                       if err := models.UpdateMirror(ctx.Repo.Mirror); err != nil {
+                               ctx.Data["Err_Interval"] = true
+                               ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
+                               return
+                       }
+               }
+
+               oldUsername := mirror_service.Username(ctx.Repo.Mirror)
+               oldPassword := mirror_service.Password(ctx.Repo.Mirror)
+               if form.MirrorPassword == "" && form.MirrorUsername == oldUsername {
+                       form.MirrorPassword = oldPassword
+               }
+
+               address, err := forms.ParseRemoteAddr(form.MirrorAddress, form.MirrorUsername, form.MirrorPassword)
+               if err == nil {
+                       err = migrations.IsMigrateURLAllowed(address, ctx.User)
+               }
+               if err != nil {
+                       ctx.Data["Err_MirrorAddress"] = true
+                       handleSettingRemoteAddrError(ctx, err, form)
+                       return
+               }
+
+               if err := mirror_service.UpdateAddress(ctx.Repo.Mirror, address); err != nil {
+                       ctx.ServerError("UpdateAddress", err)
+                       return
+               }
+
+               form.LFS = form.LFS && setting.LFS.StartServer
+
+               if len(form.LFSEndpoint) > 0 {
+                       ep := lfs.DetermineEndpoint("", form.LFSEndpoint)
+                       if ep == nil {
+                               ctx.Data["Err_LFSEndpoint"] = true
+                               ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tplSettingsOptions, &form)
+                               return
+                       }
+                       err = migrations.IsMigrateURLAllowed(ep.String(), ctx.User)
+                       if err != nil {
+                               ctx.Data["Err_LFSEndpoint"] = true
+                               handleSettingRemoteAddrError(ctx, err, form)
+                               return
+                       }
+               }
+
+               ctx.Repo.Mirror.LFS = form.LFS
+               ctx.Repo.Mirror.LFSEndpoint = form.LFSEndpoint
+               if err := models.UpdateMirror(ctx.Repo.Mirror); err != nil {
+                       ctx.ServerError("UpdateMirror", err)
+                       return
+               }
+
+               ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+               ctx.Redirect(repo.Link() + "/settings")
+
+       case "mirror-sync":
+               if !repo.IsMirror {
+                       ctx.NotFound("", nil)
+                       return
+               }
+
+               mirror_service.StartToMirror(repo.ID)
+
+               ctx.Flash.Info(ctx.Tr("repo.settings.mirror_sync_in_progress"))
+               ctx.Redirect(repo.Link() + "/settings")
+
+       case "advanced":
+               var repoChanged bool
+               var units []models.RepoUnit
+               var deleteUnitTypes []models.UnitType
+
+               // This section doesn't require repo_name/RepoName to be set in the form, don't show it
+               // as an error on the UI for this action
+               ctx.Data["Err_RepoName"] = nil
+
+               if repo.CloseIssuesViaCommitInAnyBranch != form.EnableCloseIssuesViaCommitInAnyBranch {
+                       repo.CloseIssuesViaCommitInAnyBranch = form.EnableCloseIssuesViaCommitInAnyBranch
+                       repoChanged = true
+               }
+
+               if form.EnableWiki && form.EnableExternalWiki && !models.UnitTypeExternalWiki.UnitGlobalDisabled() {
+                       if !validation.IsValidExternalURL(form.ExternalWikiURL) {
+                               ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error"))
+                               ctx.Redirect(repo.Link() + "/settings")
+                               return
+                       }
+
+                       units = append(units, models.RepoUnit{
+                               RepoID: repo.ID,
+                               Type:   models.UnitTypeExternalWiki,
+                               Config: &models.ExternalWikiConfig{
+                                       ExternalWikiURL: form.ExternalWikiURL,
+                               },
+                       })
+                       deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeWiki)
+               } else if form.EnableWiki && !form.EnableExternalWiki && !models.UnitTypeWiki.UnitGlobalDisabled() {
+                       units = append(units, models.RepoUnit{
+                               RepoID: repo.ID,
+                               Type:   models.UnitTypeWiki,
+                               Config: new(models.UnitConfig),
+                       })
+                       deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalWiki)
+               } else {
+                       if !models.UnitTypeExternalWiki.UnitGlobalDisabled() {
+                               deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalWiki)
+                       }
+                       if !models.UnitTypeWiki.UnitGlobalDisabled() {
+                               deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeWiki)
+                       }
+               }
+
+               if form.EnableIssues && form.EnableExternalTracker && !models.UnitTypeExternalTracker.UnitGlobalDisabled() {
+                       if !validation.IsValidExternalURL(form.ExternalTrackerURL) {
+                               ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error"))
+                               ctx.Redirect(repo.Link() + "/settings")
+                               return
+                       }
+                       if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(form.TrackerURLFormat) {
+                               ctx.Flash.Error(ctx.Tr("repo.settings.tracker_url_format_error"))
+                               ctx.Redirect(repo.Link() + "/settings")
+                               return
+                       }
+                       units = append(units, models.RepoUnit{
+                               RepoID: repo.ID,
+                               Type:   models.UnitTypeExternalTracker,
+                               Config: &models.ExternalTrackerConfig{
+                                       ExternalTrackerURL:    form.ExternalTrackerURL,
+                                       ExternalTrackerFormat: form.TrackerURLFormat,
+                                       ExternalTrackerStyle:  form.TrackerIssueStyle,
+                               },
+                       })
+                       deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeIssues)
+               } else if form.EnableIssues && !form.EnableExternalTracker && !models.UnitTypeIssues.UnitGlobalDisabled() {
+                       units = append(units, models.RepoUnit{
+                               RepoID: repo.ID,
+                               Type:   models.UnitTypeIssues,
+                               Config: &models.IssuesConfig{
+                                       EnableTimetracker:                form.EnableTimetracker,
+                                       AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
+                                       EnableDependencies:               form.EnableIssueDependencies,
+                               },
+                       })
+                       deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalTracker)
+               } else {
+                       if !models.UnitTypeExternalTracker.UnitGlobalDisabled() {
+                               deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalTracker)
+                       }
+                       if !models.UnitTypeIssues.UnitGlobalDisabled() {
+                               deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeIssues)
+                       }
+               }
+
+               if form.EnableProjects && !models.UnitTypeProjects.UnitGlobalDisabled() {
+                       units = append(units, models.RepoUnit{
+                               RepoID: repo.ID,
+                               Type:   models.UnitTypeProjects,
+                       })
+               } else if !models.UnitTypeProjects.UnitGlobalDisabled() {
+                       deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeProjects)
+               }
+
+               if form.EnablePulls && !models.UnitTypePullRequests.UnitGlobalDisabled() {
+                       units = append(units, models.RepoUnit{
+                               RepoID: repo.ID,
+                               Type:   models.UnitTypePullRequests,
+                               Config: &models.PullRequestsConfig{
+                                       IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace,
+                                       AllowMerge:                form.PullsAllowMerge,
+                                       AllowRebase:               form.PullsAllowRebase,
+                                       AllowRebaseMerge:          form.PullsAllowRebaseMerge,
+                                       AllowSquash:               form.PullsAllowSquash,
+                                       AllowManualMerge:          form.PullsAllowManualMerge,
+                                       AutodetectManualMerge:     form.EnableAutodetectManualMerge,
+                                       DefaultMergeStyle:         models.MergeStyle(form.PullsDefaultMergeStyle),
+                               },
+                       })
+               } else if !models.UnitTypePullRequests.UnitGlobalDisabled() {
+                       deleteUnitTypes = append(deleteUnitTypes, models.UnitTypePullRequests)
+               }
+
+               if err := models.UpdateRepositoryUnits(repo, units, deleteUnitTypes); err != nil {
+                       ctx.ServerError("UpdateRepositoryUnits", err)
+                       return
+               }
+               if repoChanged {
+                       if err := models.UpdateRepository(repo, false); err != nil {
+                               ctx.ServerError("UpdateRepository", err)
+                               return
+                       }
+               }
+               log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+
+               ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+               ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+
+       case "signing":
+               changed := false
+
+               trustModel := models.ToTrustModel(form.TrustModel)
+               if trustModel != repo.TrustModel {
+                       repo.TrustModel = trustModel
+                       changed = true
+               }
+
+               if changed {
+                       if err := models.UpdateRepository(repo, false); err != nil {
+                               ctx.ServerError("UpdateRepository", err)
+                               return
+                       }
+               }
+               log.Trace("Repository signing settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+
+               ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+               ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+
+       case "admin":
+               if !ctx.User.IsAdmin {
+                       ctx.Error(http.StatusForbidden)
+                       return
+               }
+
+               if repo.IsFsckEnabled != form.EnableHealthCheck {
+                       repo.IsFsckEnabled = form.EnableHealthCheck
+               }
+
+               if err := models.UpdateRepository(repo, false); err != nil {
+                       ctx.ServerError("UpdateRepository", err)
+                       return
+               }
+
+               log.Trace("Repository admin settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+
+               ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+               ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+
+       case "convert":
+               if !ctx.Repo.IsOwner() {
+                       ctx.Error(http.StatusNotFound)
+                       return
+               }
+               if repo.Name != form.RepoName {
+                       ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+                       return
+               }
+
+               if !repo.IsMirror {
+                       ctx.Error(http.StatusNotFound)
+                       return
+               }
+               repo.IsMirror = false
+
+               if _, err := repository.CleanUpMigrateInfo(repo); err != nil {
+                       ctx.ServerError("CleanUpMigrateInfo", err)
+                       return
+               } else if err = models.DeleteMirrorByRepoID(ctx.Repo.Repository.ID); err != nil {
+                       ctx.ServerError("DeleteMirrorByRepoID", err)
+                       return
+               }
+               log.Trace("Repository converted from mirror to regular: %s", repo.FullName())
+               ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed"))
+               ctx.Redirect(repo.Link())
+
+       case "convert_fork":
+               if !ctx.Repo.IsOwner() {
+                       ctx.Error(http.StatusNotFound)
+                       return
+               }
+               if err := repo.GetOwner(); err != nil {
+                       ctx.ServerError("Convert Fork", err)
+                       return
+               }
+               if repo.Name != form.RepoName {
+                       ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+                       return
+               }
+
+               if !repo.IsFork {
+                       ctx.Error(http.StatusNotFound)
+                       return
+               }
+
+               if !ctx.Repo.Owner.CanCreateRepo() {
+                       ctx.Flash.Error(ctx.Tr("repo.form.reach_limit_of_creation", ctx.User.MaxCreationLimit()))
+                       ctx.Redirect(repo.Link() + "/settings")
+                       return
+               }
+
+               repo.IsFork = false
+               repo.ForkID = 0
+               if err := models.UpdateRepository(repo, false); err != nil {
+                       log.Error("Unable to update repository %-v whilst converting from fork", repo)
+                       ctx.ServerError("Convert Fork", err)
+                       return
+               }
+
+               log.Trace("Repository converted from fork to regular: %s", repo.FullName())
+               ctx.Flash.Success(ctx.Tr("repo.settings.convert_fork_succeed"))
+               ctx.Redirect(repo.Link())
+
+       case "transfer":
+               if !ctx.Repo.IsOwner() {
+                       ctx.Error(http.StatusNotFound)
+                       return
+               }
+               if repo.Name != form.RepoName {
+                       ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+                       return
+               }
+
+               newOwner, err := models.GetUserByName(ctx.Query("new_owner_name"))
+               if err != nil {
+                       if models.IsErrUserNotExist(err) {
+                               ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
+                               return
+                       }
+                       ctx.ServerError("IsUserExist", err)
+                       return
+               }
+
+               if newOwner.Type == models.UserTypeOrganization {
+                       if !ctx.User.IsAdmin && newOwner.Visibility == structs.VisibleTypePrivate && !newOwner.HasMemberWithUserID(ctx.User.ID) {
+                               // The user shouldn't know about this organization
+                               ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
+                               return
+                       }
+               }
+
+               // Close the GitRepo if open
+               if ctx.Repo.GitRepo != nil {
+                       ctx.Repo.GitRepo.Close()
+                       ctx.Repo.GitRepo = nil
+               }
+
+               if err := repo_service.StartRepositoryTransfer(ctx.User, newOwner, repo, nil); err != nil {
+                       if models.IsErrRepoAlreadyExist(err) {
+                               ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil)
+                       } else if models.IsErrRepoTransferInProgress(err) {
+                               ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil)
+                       } else {
+                               ctx.ServerError("TransferOwnership", err)
+                       }
+
+                       return
+               }
+
+               log.Trace("Repository transfer process was started: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner)
+               ctx.Flash.Success(ctx.Tr("repo.settings.transfer_started", newOwner.DisplayName()))
+               ctx.Redirect(ctx.Repo.Owner.HomeLink() + "/" + repo.Name + "/settings")
+
+       case "cancel_transfer":
+               if !ctx.Repo.IsOwner() {
+                       ctx.Error(http.StatusNotFound)
+                       return
+               }
+
+               repoTransfer, err := models.GetPendingRepositoryTransfer(ctx.Repo.Repository)
+               if err != nil {
+                       if models.IsErrNoPendingTransfer(err) {
+                               ctx.Flash.Error("repo.settings.transfer_abort_invalid")
+                               ctx.Redirect(ctx.User.HomeLink() + "/" + repo.Name + "/settings")
+                       } else {
+                               ctx.ServerError("GetPendingRepositoryTransfer", err)
+                       }
+
+                       return
+               }
+
+               if err := repoTransfer.LoadAttributes(); err != nil {
+                       ctx.ServerError("LoadRecipient", err)
+                       return
+               }
+
+               if err := models.CancelRepositoryTransfer(ctx.Repo.Repository); err != nil {
+                       ctx.ServerError("CancelRepositoryTransfer", err)
+                       return
+               }
+
+               log.Trace("Repository transfer process was cancelled: %s/%s ", ctx.Repo.Owner.Name, repo.Name)
+               ctx.Flash.Success(ctx.Tr("repo.settings.transfer_abort_success", repoTransfer.Recipient.Name))
+               ctx.Redirect(ctx.Repo.Owner.HomeLink() + "/" + repo.Name + "/settings")
+
+       case "delete":
+               if !ctx.Repo.IsOwner() {
+                       ctx.Error(http.StatusNotFound)
+                       return
+               }
+               if repo.Name != form.RepoName {
+                       ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+                       return
+               }
+
+               // Close the gitrepository before doing this.
+               if ctx.Repo.GitRepo != nil {
+                       ctx.Repo.GitRepo.Close()
+               }
+
+               if err := repo_service.DeleteRepository(ctx.User, ctx.Repo.Repository); err != nil {
+                       ctx.ServerError("DeleteRepository", err)
+                       return
+               }
+               log.Trace("Repository deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+
+               ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success"))
+               ctx.Redirect(ctx.Repo.Owner.DashboardLink())
+
+       case "delete-wiki":
+               if !ctx.Repo.IsOwner() {
+                       ctx.Error(http.StatusNotFound)
+                       return
+               }
+               if repo.Name != form.RepoName {
+                       ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+                       return
+               }
+
+               err := repo.DeleteWiki()
+               if err != nil {
+                       log.Error("Delete Wiki: %v", err.Error())
+               }
+               log.Trace("Repository wiki deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+
+               ctx.Flash.Success(ctx.Tr("repo.settings.wiki_deletion_success"))
+               ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+
+       case "archive":
+               if !ctx.Repo.IsOwner() {
+                       ctx.Error(http.StatusForbidden)
+                       return
+               }
+
+               if repo.IsMirror {
+                       ctx.Flash.Error(ctx.Tr("repo.settings.archive.error_ismirror"))
+                       ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+                       return
+               }
+
+               if err := repo.SetArchiveRepoState(true); err != nil {
+                       log.Error("Tried to archive a repo: %s", err)
+                       ctx.Flash.Error(ctx.Tr("repo.settings.archive.error"))
+                       ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+                       return
+               }
+
+               ctx.Flash.Success(ctx.Tr("repo.settings.archive.success"))
+
+               log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+               ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+       case "unarchive":
+               if !ctx.Repo.IsOwner() {
+                       ctx.Error(http.StatusForbidden)
+                       return
+               }
+
+               if err := repo.SetArchiveRepoState(false); err != nil {
+                       log.Error("Tried to unarchive a repo: %s", err)
+                       ctx.Flash.Error(ctx.Tr("repo.settings.unarchive.error"))
+                       ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+                       return
+               }
+
+               ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success"))
+
+               log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+               ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+
+       default:
+               ctx.NotFound("", nil)
+       }
+}
+
+func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.RepoSettingForm) {
+       if models.IsErrInvalidCloneAddr(err) {
+               addrErr := err.(*models.ErrInvalidCloneAddr)
+               switch {
+               case addrErr.IsProtocolInvalid:
+                       ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tplSettingsOptions, form)
+               case addrErr.IsURLError:
+                       ctx.RenderWithErr(ctx.Tr("form.url_error"), tplSettingsOptions, form)
+               case addrErr.IsPermissionDenied:
+                       if addrErr.LocalPath {
+                               ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tplSettingsOptions, form)
+                       } else if len(addrErr.PrivateNet) == 0 {
+                               ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_blocked"), tplSettingsOptions, form)
+                       } else {
+                               ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_private_ip"), tplSettingsOptions, form)
+                       }
+               case addrErr.IsInvalidPath:
+                       ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tplSettingsOptions, form)
+               default:
+                       ctx.ServerError("Unknown error", err)
+               }
+       }
+       ctx.RenderWithErr(ctx.Tr("repo.mirror_address_url_invalid"), tplSettingsOptions, form)
+}
+
+// Collaboration render a repository's collaboration page
+func Collaboration(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.settings")
+       ctx.Data["PageIsSettingsCollaboration"] = true
+
+       users, err := ctx.Repo.Repository.GetCollaborators(models.ListOptions{})
+       if err != nil {
+               ctx.ServerError("GetCollaborators", err)
+               return
+       }
+       ctx.Data["Collaborators"] = users
+
+       teams, err := ctx.Repo.Repository.GetRepoTeams()
+       if err != nil {
+               ctx.ServerError("GetRepoTeams", err)
+               return
+       }
+       ctx.Data["Teams"] = teams
+       ctx.Data["Repo"] = ctx.Repo.Repository
+       ctx.Data["OrgID"] = ctx.Repo.Repository.OwnerID
+       ctx.Data["OrgName"] = ctx.Repo.Repository.OwnerName
+       ctx.Data["Org"] = ctx.Repo.Repository.Owner
+       ctx.Data["Units"] = models.Units
+
+       ctx.HTML(http.StatusOK, tplCollaboration)
+}
+
+// CollaborationPost response for actions for a collaboration of a repository
+func CollaborationPost(ctx *context.Context) {
+       name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.Query("collaborator")))
+       if len(name) == 0 || ctx.Repo.Owner.LowerName == name {
+               ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
+               return
+       }
+
+       u, err := models.GetUserByName(name)
+       if err != nil {
+               if models.IsErrUserNotExist(err) {
+                       ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
+                       ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
+               } else {
+                       ctx.ServerError("GetUserByName", err)
+               }
+               return
+       }
+
+       if !u.IsActive {
+               ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator_inactive_user"))
+               ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
+               return
+       }
+
+       // Organization is not allowed to be added as a collaborator.
+       if u.IsOrganization() {
+               ctx.Flash.Error(ctx.Tr("repo.settings.org_not_allowed_to_be_collaborator"))
+               ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
+               return
+       }
+
+       if got, err := ctx.Repo.Repository.IsCollaborator(u.ID); err == nil && got {
+               ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator_duplicate"))
+               ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+               return
+       }
+
+       if err = ctx.Repo.Repository.AddCollaborator(u); err != nil {
+               ctx.ServerError("AddCollaborator", err)
+               return
+       }
+
+       if setting.Service.EnableNotifyMail {
+               mailer.SendCollaboratorMail(u, ctx.User, ctx.Repo.Repository)
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.settings.add_collaborator_success"))
+       ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
+}
+
+// ChangeCollaborationAccessMode response for changing access of a collaboration
+func ChangeCollaborationAccessMode(ctx *context.Context) {
+       if err := ctx.Repo.Repository.ChangeCollaborationAccessMode(
+               ctx.QueryInt64("uid"),
+               models.AccessMode(ctx.QueryInt("mode"))); err != nil {
+               log.Error("ChangeCollaborationAccessMode: %v", err)
+       }
+}
+
+// DeleteCollaboration delete a collaboration for a repository
+func DeleteCollaboration(ctx *context.Context) {
+       if err := ctx.Repo.Repository.DeleteCollaboration(ctx.QueryInt64("id")); err != nil {
+               ctx.Flash.Error("DeleteCollaboration: " + err.Error())
+       } else {
+               ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": ctx.Repo.RepoLink + "/settings/collaboration",
+       })
+}
+
+// AddTeamPost response for adding a team to a repository
+func AddTeamPost(ctx *context.Context) {
+       if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() {
+               ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed"))
+               ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+               return
+       }
+
+       name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.Query("team")))
+       if len(name) == 0 {
+               ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+               return
+       }
+
+       team, err := ctx.Repo.Owner.GetTeam(name)
+       if err != nil {
+               if models.IsErrTeamNotExist(err) {
+                       ctx.Flash.Error(ctx.Tr("form.team_not_exist"))
+                       ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+               } else {
+                       ctx.ServerError("GetTeam", err)
+               }
+               return
+       }
+
+       if team.OrgID != ctx.Repo.Repository.OwnerID {
+               ctx.Flash.Error(ctx.Tr("repo.settings.team_not_in_organization"))
+               ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+               return
+       }
+
+       if models.HasTeamRepo(ctx.Repo.Repository.OwnerID, team.ID, ctx.Repo.Repository.ID) {
+               ctx.Flash.Error(ctx.Tr("repo.settings.add_team_duplicate"))
+               ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+               return
+       }
+
+       if err = team.AddRepository(ctx.Repo.Repository); err != nil {
+               ctx.ServerError("team.AddRepository", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.settings.add_team_success"))
+       ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+}
+
+// DeleteTeam response for deleting a team from a repository
+func DeleteTeam(ctx *context.Context) {
+       if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() {
+               ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed"))
+               ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+               return
+       }
+
+       team, err := models.GetTeamByID(ctx.QueryInt64("id"))
+       if err != nil {
+               ctx.ServerError("GetTeamByID", err)
+               return
+       }
+
+       if err = team.RemoveRepository(ctx.Repo.Repository.ID); err != nil {
+               ctx.ServerError("team.RemoveRepositorys", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.settings.remove_team_success"))
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": ctx.Repo.RepoLink + "/settings/collaboration",
+       })
+}
+
+// parseOwnerAndRepo get repos by owner
+func parseOwnerAndRepo(ctx *context.Context) (*models.User, *models.Repository) {
+       owner, err := models.GetUserByName(ctx.Params(":username"))
+       if err != nil {
+               if models.IsErrUserNotExist(err) {
+                       ctx.NotFound("GetUserByName", err)
+               } else {
+                       ctx.ServerError("GetUserByName", err)
+               }
+               return nil, nil
+       }
+
+       repo, err := models.GetRepositoryByName(owner.ID, ctx.Params(":reponame"))
+       if err != nil {
+               if models.IsErrRepoNotExist(err) {
+                       ctx.NotFound("GetRepositoryByName", err)
+               } else {
+                       ctx.ServerError("GetRepositoryByName", err)
+               }
+               return nil, nil
+       }
+
+       return owner, repo
+}
+
+// GitHooks hooks of a repository
+func GitHooks(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.settings.githooks")
+       ctx.Data["PageIsSettingsGitHooks"] = true
+
+       hooks, err := ctx.Repo.GitRepo.Hooks()
+       if err != nil {
+               ctx.ServerError("Hooks", err)
+               return
+       }
+       ctx.Data["Hooks"] = hooks
+
+       ctx.HTML(http.StatusOK, tplGithooks)
+}
+
+// GitHooksEdit render for editing a hook of repository page
+func GitHooksEdit(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.settings.githooks")
+       ctx.Data["PageIsSettingsGitHooks"] = true
+
+       name := ctx.Params(":name")
+       hook, err := ctx.Repo.GitRepo.GetHook(name)
+       if err != nil {
+               if err == git.ErrNotValidHook {
+                       ctx.NotFound("GetHook", err)
+               } else {
+                       ctx.ServerError("GetHook", err)
+               }
+               return
+       }
+       ctx.Data["Hook"] = hook
+       ctx.HTML(http.StatusOK, tplGithookEdit)
+}
+
+// GitHooksEditPost response for editing a git hook of a repository
+func GitHooksEditPost(ctx *context.Context) {
+       name := ctx.Params(":name")
+       hook, err := ctx.Repo.GitRepo.GetHook(name)
+       if err != nil {
+               if err == git.ErrNotValidHook {
+                       ctx.NotFound("GetHook", err)
+               } else {
+                       ctx.ServerError("GetHook", err)
+               }
+               return
+       }
+       hook.Content = ctx.Query("content")
+       if err = hook.Update(); err != nil {
+               ctx.ServerError("hook.Update", err)
+               return
+       }
+       ctx.Redirect(ctx.Repo.RepoLink + "/settings/hooks/git")
+}
+
+// DeployKeys render the deploy keys list of a repository page
+func DeployKeys(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys")
+       ctx.Data["PageIsSettingsKeys"] = true
+       ctx.Data["DisableSSH"] = setting.SSH.Disabled
+
+       keys, err := models.ListDeployKeys(ctx.Repo.Repository.ID, models.ListOptions{})
+       if err != nil {
+               ctx.ServerError("ListDeployKeys", err)
+               return
+       }
+       ctx.Data["Deploykeys"] = keys
+
+       ctx.HTML(http.StatusOK, tplDeployKeys)
+}
+
+// DeployKeysPost response for adding a deploy key of a repository
+func DeployKeysPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.AddKeyForm)
+       ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys")
+       ctx.Data["PageIsSettingsKeys"] = true
+
+       keys, err := models.ListDeployKeys(ctx.Repo.Repository.ID, models.ListOptions{})
+       if err != nil {
+               ctx.ServerError("ListDeployKeys", err)
+               return
+       }
+       ctx.Data["Deploykeys"] = keys
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplDeployKeys)
+               return
+       }
+
+       content, err := models.CheckPublicKeyString(form.Content)
+       if err != nil {
+               if models.IsErrSSHDisabled(err) {
+                       ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
+               } else if models.IsErrKeyUnableVerify(err) {
+                       ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
+               } else {
+                       ctx.Data["HasError"] = true
+                       ctx.Data["Err_Content"] = true
+                       ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error()))
+               }
+               ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
+               return
+       }
+
+       key, err := models.AddDeployKey(ctx.Repo.Repository.ID, form.Title, content, !form.IsWritable)
+       if err != nil {
+               ctx.Data["HasError"] = true
+               switch {
+               case models.IsErrDeployKeyAlreadyExist(err):
+                       ctx.Data["Err_Content"] = true
+                       ctx.RenderWithErr(ctx.Tr("repo.settings.key_been_used"), tplDeployKeys, &form)
+               case models.IsErrKeyAlreadyExist(err):
+                       ctx.Data["Err_Content"] = true
+                       ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplDeployKeys, &form)
+               case models.IsErrKeyNameAlreadyUsed(err):
+                       ctx.Data["Err_Title"] = true
+                       ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
+               case models.IsErrDeployKeyNameAlreadyUsed(err):
+                       ctx.Data["Err_Title"] = true
+                       ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
+               default:
+                       ctx.ServerError("AddDeployKey", err)
+               }
+               return
+       }
+
+       log.Trace("Deploy key added: %d", ctx.Repo.Repository.ID)
+       ctx.Flash.Success(ctx.Tr("repo.settings.add_key_success", key.Name))
+       ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
+}
+
+// DeleteDeployKey response for deleting a deploy key
+func DeleteDeployKey(ctx *context.Context) {
+       if err := models.DeleteDeployKey(ctx.User, ctx.QueryInt64("id")); err != nil {
+               ctx.Flash.Error("DeleteDeployKey: " + err.Error())
+       } else {
+               ctx.Flash.Success(ctx.Tr("repo.settings.deploy_key_deletion_success"))
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": ctx.Repo.RepoLink + "/settings/keys",
+       })
+}
+
+// UpdateAvatarSetting update repo's avatar
+func UpdateAvatarSetting(ctx *context.Context, form forms.AvatarForm) error {
+       ctxRepo := ctx.Repo.Repository
+
+       if form.Avatar == nil {
+               // No avatar is uploaded and we not removing it here.
+               // No random avatar generated here.
+               // Just exit, no action.
+               if ctxRepo.CustomAvatarRelativePath() == "" {
+                       log.Trace("No avatar was uploaded for repo: %d. Default icon will appear instead.", ctxRepo.ID)
+               }
+               return nil
+       }
+
+       r, err := form.Avatar.Open()
+       if err != nil {
+               return fmt.Errorf("Avatar.Open: %v", err)
+       }
+       defer r.Close()
+
+       if form.Avatar.Size > setting.Avatar.MaxFileSize {
+               return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big"))
+       }
+
+       data, err := ioutil.ReadAll(r)
+       if err != nil {
+               return fmt.Errorf("ioutil.ReadAll: %v", err)
+       }
+       st := typesniffer.DetectContentType(data)
+       if !(st.IsImage() && !st.IsSvgImage()) {
+               return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image"))
+       }
+       if err = ctxRepo.UploadAvatar(data); err != nil {
+               return fmt.Errorf("UploadAvatar: %v", err)
+       }
+       return nil
+}
+
+// SettingsAvatar save new POSTed repository avatar
+func SettingsAvatar(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.AvatarForm)
+       form.Source = forms.AvatarLocal
+       if err := UpdateAvatarSetting(ctx, *form); err != nil {
+               ctx.Flash.Error(err.Error())
+       } else {
+               ctx.Flash.Success(ctx.Tr("repo.settings.update_avatar_success"))
+       }
+       ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
+
+// SettingsDeleteAvatar delete repository avatar
+func SettingsDeleteAvatar(ctx *context.Context) {
+       if err := ctx.Repo.Repository.DeleteAvatar(); err != nil {
+               ctx.Flash.Error(fmt.Sprintf("DeleteAvatar: %v", err))
+       }
+       ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
diff --git a/routers/web/repo/setting_protected_branch.go b/routers/web/repo/setting_protected_branch.go
new file mode 100644 (file)
index 0000000..fba2c09
--- /dev/null
@@ -0,0 +1,286 @@
+// 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 repo
+
+import (
+       "fmt"
+       "net/http"
+       "strings"
+       "time"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+       pull_service "code.gitea.io/gitea/services/pull"
+)
+
+// ProtectedBranch render the page to protect the repository
+func ProtectedBranch(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.settings")
+       ctx.Data["PageIsSettingsBranches"] = true
+
+       protectedBranches, err := ctx.Repo.Repository.GetProtectedBranches()
+       if err != nil {
+               ctx.ServerError("GetProtectedBranches", err)
+               return
+       }
+       ctx.Data["ProtectedBranches"] = protectedBranches
+
+       branches := ctx.Data["Branches"].([]string)
+       leftBranches := make([]string, 0, len(branches)-len(protectedBranches))
+       for _, b := range branches {
+               var protected bool
+               for _, pb := range protectedBranches {
+                       if b == pb.BranchName {
+                               protected = true
+                               break
+                       }
+               }
+               if !protected {
+                       leftBranches = append(leftBranches, b)
+               }
+       }
+
+       ctx.Data["LeftBranches"] = leftBranches
+
+       ctx.HTML(http.StatusOK, tplBranches)
+}
+
+// ProtectedBranchPost response for protect for a branch of a repository
+func ProtectedBranchPost(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.settings")
+       ctx.Data["PageIsSettingsBranches"] = true
+
+       repo := ctx.Repo.Repository
+
+       switch ctx.Query("action") {
+       case "default_branch":
+               if ctx.HasError() {
+                       ctx.HTML(http.StatusOK, tplBranches)
+                       return
+               }
+
+               branch := ctx.Query("branch")
+               if !ctx.Repo.GitRepo.IsBranchExist(branch) {
+                       ctx.Status(404)
+                       return
+               } else if repo.DefaultBranch != branch {
+                       repo.DefaultBranch = branch
+                       if err := ctx.Repo.GitRepo.SetDefaultBranch(branch); err != nil {
+                               if !git.IsErrUnsupportedVersion(err) {
+                                       ctx.ServerError("SetDefaultBranch", err)
+                                       return
+                               }
+                       }
+                       if err := repo.UpdateDefaultBranch(); err != nil {
+                               ctx.ServerError("SetDefaultBranch", err)
+                               return
+                       }
+               }
+
+               log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+
+               ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+               ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
+       default:
+               ctx.NotFound("", nil)
+       }
+}
+
+// SettingsProtectedBranch renders the protected branch setting page
+func SettingsProtectedBranch(c *context.Context) {
+       branch := c.Params("*")
+       if !c.Repo.GitRepo.IsBranchExist(branch) {
+               c.NotFound("IsBranchExist", nil)
+               return
+       }
+
+       c.Data["Title"] = c.Tr("repo.settings.protected_branch") + " - " + branch
+       c.Data["PageIsSettingsBranches"] = true
+
+       protectBranch, err := models.GetProtectedBranchBy(c.Repo.Repository.ID, branch)
+       if err != nil {
+               if !git.IsErrBranchNotExist(err) {
+                       c.ServerError("GetProtectBranchOfRepoByName", err)
+                       return
+               }
+       }
+
+       if protectBranch == nil {
+               // No options found, create defaults.
+               protectBranch = &models.ProtectedBranch{
+                       BranchName: branch,
+               }
+       }
+
+       users, err := c.Repo.Repository.GetReaders()
+       if err != nil {
+               c.ServerError("Repo.Repository.GetReaders", err)
+               return
+       }
+       c.Data["Users"] = users
+       c.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistUserIDs), ",")
+       c.Data["merge_whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.MergeWhitelistUserIDs), ",")
+       c.Data["approvals_whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.ApprovalsWhitelistUserIDs), ",")
+       contexts, _ := models.FindRepoRecentCommitStatusContexts(c.Repo.Repository.ID, 7*24*time.Hour) // Find last week status check contexts
+       for _, ctx := range protectBranch.StatusCheckContexts {
+               var found bool
+               for i := range contexts {
+                       if contexts[i] == ctx {
+                               found = true
+                               break
+                       }
+               }
+               if !found {
+                       contexts = append(contexts, ctx)
+               }
+       }
+
+       c.Data["branch_status_check_contexts"] = contexts
+       c.Data["is_context_required"] = func(context string) bool {
+               for _, c := range protectBranch.StatusCheckContexts {
+                       if c == context {
+                               return true
+                       }
+               }
+               return false
+       }
+
+       if c.Repo.Owner.IsOrganization() {
+               teams, err := c.Repo.Owner.TeamsWithAccessToRepo(c.Repo.Repository.ID, models.AccessModeRead)
+               if err != nil {
+                       c.ServerError("Repo.Owner.TeamsWithAccessToRepo", err)
+                       return
+               }
+               c.Data["Teams"] = teams
+               c.Data["whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistTeamIDs), ",")
+               c.Data["merge_whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.MergeWhitelistTeamIDs), ",")
+               c.Data["approvals_whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.ApprovalsWhitelistTeamIDs), ",")
+       }
+
+       c.Data["Branch"] = protectBranch
+       c.HTML(http.StatusOK, tplProtectedBranch)
+}
+
+// SettingsProtectedBranchPost updates the protected branch settings
+func SettingsProtectedBranchPost(ctx *context.Context) {
+       f := web.GetForm(ctx).(*forms.ProtectBranchForm)
+       branch := ctx.Params("*")
+       if !ctx.Repo.GitRepo.IsBranchExist(branch) {
+               ctx.NotFound("IsBranchExist", nil)
+               return
+       }
+
+       protectBranch, err := models.GetProtectedBranchBy(ctx.Repo.Repository.ID, branch)
+       if err != nil {
+               if !git.IsErrBranchNotExist(err) {
+                       ctx.ServerError("GetProtectBranchOfRepoByName", err)
+                       return
+               }
+       }
+
+       if f.Protected {
+               if protectBranch == nil {
+                       // No options found, create defaults.
+                       protectBranch = &models.ProtectedBranch{
+                               RepoID:     ctx.Repo.Repository.ID,
+                               BranchName: branch,
+                       }
+               }
+               if f.RequiredApprovals < 0 {
+                       ctx.Flash.Error(ctx.Tr("repo.settings.protected_branch_required_approvals_min"))
+                       ctx.Redirect(fmt.Sprintf("%s/settings/branches/%s", ctx.Repo.RepoLink, branch))
+               }
+
+               var whitelistUsers, whitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams []int64
+               switch f.EnablePush {
+               case "all":
+                       protectBranch.CanPush = true
+                       protectBranch.EnableWhitelist = false
+                       protectBranch.WhitelistDeployKeys = false
+               case "whitelist":
+                       protectBranch.CanPush = true
+                       protectBranch.EnableWhitelist = true
+                       protectBranch.WhitelistDeployKeys = f.WhitelistDeployKeys
+                       if strings.TrimSpace(f.WhitelistUsers) != "" {
+                               whitelistUsers, _ = base.StringsToInt64s(strings.Split(f.WhitelistUsers, ","))
+                       }
+                       if strings.TrimSpace(f.WhitelistTeams) != "" {
+                               whitelistTeams, _ = base.StringsToInt64s(strings.Split(f.WhitelistTeams, ","))
+                       }
+               default:
+                       protectBranch.CanPush = false
+                       protectBranch.EnableWhitelist = false
+                       protectBranch.WhitelistDeployKeys = false
+               }
+
+               protectBranch.EnableMergeWhitelist = f.EnableMergeWhitelist
+               if f.EnableMergeWhitelist {
+                       if strings.TrimSpace(f.MergeWhitelistUsers) != "" {
+                               mergeWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistUsers, ","))
+                       }
+                       if strings.TrimSpace(f.MergeWhitelistTeams) != "" {
+                               mergeWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistTeams, ","))
+                       }
+               }
+
+               protectBranch.EnableStatusCheck = f.EnableStatusCheck
+               if f.EnableStatusCheck {
+                       protectBranch.StatusCheckContexts = f.StatusCheckContexts
+               } else {
+                       protectBranch.StatusCheckContexts = nil
+               }
+
+               protectBranch.RequiredApprovals = f.RequiredApprovals
+               protectBranch.EnableApprovalsWhitelist = f.EnableApprovalsWhitelist
+               if f.EnableApprovalsWhitelist {
+                       if strings.TrimSpace(f.ApprovalsWhitelistUsers) != "" {
+                               approvalsWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistUsers, ","))
+                       }
+                       if strings.TrimSpace(f.ApprovalsWhitelistTeams) != "" {
+                               approvalsWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistTeams, ","))
+                       }
+               }
+               protectBranch.BlockOnRejectedReviews = f.BlockOnRejectedReviews
+               protectBranch.BlockOnOfficialReviewRequests = f.BlockOnOfficialReviewRequests
+               protectBranch.DismissStaleApprovals = f.DismissStaleApprovals
+               protectBranch.RequireSignedCommits = f.RequireSignedCommits
+               protectBranch.ProtectedFilePatterns = f.ProtectedFilePatterns
+               protectBranch.BlockOnOutdatedBranch = f.BlockOnOutdatedBranch
+
+               err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{
+                       UserIDs:          whitelistUsers,
+                       TeamIDs:          whitelistTeams,
+                       MergeUserIDs:     mergeWhitelistUsers,
+                       MergeTeamIDs:     mergeWhitelistTeams,
+                       ApprovalsUserIDs: approvalsWhitelistUsers,
+                       ApprovalsTeamIDs: approvalsWhitelistTeams,
+               })
+               if err != nil {
+                       ctx.ServerError("UpdateProtectBranch", err)
+                       return
+               }
+               if err = pull_service.CheckPrsForBaseBranch(ctx.Repo.Repository, protectBranch.BranchName); err != nil {
+                       ctx.ServerError("CheckPrsForBaseBranch", err)
+                       return
+               }
+               ctx.Flash.Success(ctx.Tr("repo.settings.update_protect_branch_success", branch))
+               ctx.Redirect(fmt.Sprintf("%s/settings/branches/%s", ctx.Repo.RepoLink, branch))
+       } else {
+               if protectBranch != nil {
+                       if err := ctx.Repo.Repository.DeleteProtectedBranch(protectBranch.ID); err != nil {
+                               ctx.ServerError("DeleteProtectedBranch", err)
+                               return
+                       }
+               }
+               ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success", branch))
+               ctx.Redirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
+       }
+}
diff --git a/routers/web/repo/settings_test.go b/routers/web/repo/settings_test.go
new file mode 100644 (file)
index 0000000..5190f12
--- /dev/null
@@ -0,0 +1,413 @@
+// 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 repo
+
+import (
+       "io/ioutil"
+       "net/http"
+       "testing"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/test"
+       "code.gitea.io/gitea/modules/util"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func createSSHAuthorizedKeysTmpPath(t *testing.T) func() {
+       tmpDir, err := ioutil.TempDir("", "tmp-ssh")
+       if err != nil {
+               assert.Fail(t, "Unable to create temporary directory: %v", err)
+               return nil
+       }
+
+       oldPath := setting.SSH.RootPath
+       setting.SSH.RootPath = tmpDir
+
+       return func() {
+               setting.SSH.RootPath = oldPath
+               util.RemoveAll(tmpDir)
+       }
+}
+
+func TestAddReadOnlyDeployKey(t *testing.T) {
+       if deferable := createSSHAuthorizedKeysTmpPath(t); deferable != nil {
+               defer deferable()
+       } else {
+               return
+       }
+       models.PrepareTestEnv(t)
+
+       ctx := test.MockContext(t, "user2/repo1/settings/keys")
+
+       test.LoadUser(t, ctx, 2)
+       test.LoadRepo(t, ctx, 2)
+
+       addKeyForm := forms.AddKeyForm{
+               Title:   "read-only",
+               Content: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment\n",
+       }
+       web.SetForm(ctx, &addKeyForm)
+       DeployKeysPost(ctx)
+       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
+
+       models.AssertExistsAndLoadBean(t, &models.DeployKey{
+               Name:    addKeyForm.Title,
+               Content: addKeyForm.Content,
+               Mode:    models.AccessModeRead,
+       })
+}
+
+func TestAddReadWriteOnlyDeployKey(t *testing.T) {
+       if deferable := createSSHAuthorizedKeysTmpPath(t); deferable != nil {
+               defer deferable()
+       } else {
+               return
+       }
+
+       models.PrepareTestEnv(t)
+
+       ctx := test.MockContext(t, "user2/repo1/settings/keys")
+
+       test.LoadUser(t, ctx, 2)
+       test.LoadRepo(t, ctx, 2)
+
+       addKeyForm := forms.AddKeyForm{
+               Title:      "read-write",
+               Content:    "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment\n",
+               IsWritable: true,
+       }
+       web.SetForm(ctx, &addKeyForm)
+       DeployKeysPost(ctx)
+       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
+
+       models.AssertExistsAndLoadBean(t, &models.DeployKey{
+               Name:    addKeyForm.Title,
+               Content: addKeyForm.Content,
+               Mode:    models.AccessModeWrite,
+       })
+}
+
+func TestCollaborationPost(t *testing.T) {
+
+       models.PrepareTestEnv(t)
+       ctx := test.MockContext(t, "user2/repo1/issues/labels")
+       test.LoadUser(t, ctx, 2)
+       test.LoadUser(t, ctx, 4)
+       test.LoadRepo(t, ctx, 1)
+
+       ctx.Req.Form.Set("collaborator", "user4")
+
+       u := &models.User{
+               LowerName: "user2",
+               Type:      models.UserTypeIndividual,
+       }
+
+       re := &models.Repository{
+               ID:    2,
+               Owner: u,
+       }
+
+       repo := &context.Repository{
+               Owner:      u,
+               Repository: re,
+       }
+
+       ctx.Repo = repo
+
+       CollaborationPost(ctx)
+
+       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
+
+       exists, err := re.IsCollaborator(4)
+       assert.NoError(t, err)
+       assert.True(t, exists)
+}
+
+func TestCollaborationPost_InactiveUser(t *testing.T) {
+
+       models.PrepareTestEnv(t)
+       ctx := test.MockContext(t, "user2/repo1/issues/labels")
+       test.LoadUser(t, ctx, 2)
+       test.LoadUser(t, ctx, 9)
+       test.LoadRepo(t, ctx, 1)
+
+       ctx.Req.Form.Set("collaborator", "user9")
+
+       repo := &context.Repository{
+               Owner: &models.User{
+                       LowerName: "user2",
+               },
+       }
+
+       ctx.Repo = repo
+
+       CollaborationPost(ctx)
+
+       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
+       assert.NotEmpty(t, ctx.Flash.ErrorMsg)
+}
+
+func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) {
+
+       models.PrepareTestEnv(t)
+       ctx := test.MockContext(t, "user2/repo1/issues/labels")
+       test.LoadUser(t, ctx, 2)
+       test.LoadUser(t, ctx, 4)
+       test.LoadRepo(t, ctx, 1)
+
+       ctx.Req.Form.Set("collaborator", "user4")
+
+       u := &models.User{
+               LowerName: "user2",
+               Type:      models.UserTypeIndividual,
+       }
+
+       re := &models.Repository{
+               ID:    2,
+               Owner: u,
+       }
+
+       repo := &context.Repository{
+               Owner:      u,
+               Repository: re,
+       }
+
+       ctx.Repo = repo
+
+       CollaborationPost(ctx)
+
+       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
+
+       exists, err := re.IsCollaborator(4)
+       assert.NoError(t, err)
+       assert.True(t, exists)
+
+       // Try adding the same collaborator again
+       CollaborationPost(ctx)
+
+       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
+       assert.NotEmpty(t, ctx.Flash.ErrorMsg)
+}
+
+func TestCollaborationPost_NonExistentUser(t *testing.T) {
+
+       models.PrepareTestEnv(t)
+       ctx := test.MockContext(t, "user2/repo1/issues/labels")
+       test.LoadUser(t, ctx, 2)
+       test.LoadRepo(t, ctx, 1)
+
+       ctx.Req.Form.Set("collaborator", "user34")
+
+       repo := &context.Repository{
+               Owner: &models.User{
+                       LowerName: "user2",
+               },
+       }
+
+       ctx.Repo = repo
+
+       CollaborationPost(ctx)
+
+       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
+       assert.NotEmpty(t, ctx.Flash.ErrorMsg)
+}
+
+func TestAddTeamPost(t *testing.T) {
+       models.PrepareTestEnv(t)
+       ctx := test.MockContext(t, "org26/repo43")
+
+       ctx.Req.Form.Set("team", "team11")
+
+       org := &models.User{
+               LowerName: "org26",
+               Type:      models.UserTypeOrganization,
+       }
+
+       team := &models.Team{
+               ID:    11,
+               OrgID: 26,
+       }
+
+       re := &models.Repository{
+               ID:      43,
+               Owner:   org,
+               OwnerID: 26,
+       }
+
+       repo := &context.Repository{
+               Owner: &models.User{
+                       ID:                        26,
+                       LowerName:                 "org26",
+                       RepoAdminChangeTeamAccess: true,
+               },
+               Repository: re,
+       }
+
+       ctx.Repo = repo
+
+       AddTeamPost(ctx)
+
+       assert.True(t, team.HasRepository(re.ID))
+       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
+       assert.Empty(t, ctx.Flash.ErrorMsg)
+}
+
+func TestAddTeamPost_NotAllowed(t *testing.T) {
+       models.PrepareTestEnv(t)
+       ctx := test.MockContext(t, "org26/repo43")
+
+       ctx.Req.Form.Set("team", "team11")
+
+       org := &models.User{
+               LowerName: "org26",
+               Type:      models.UserTypeOrganization,
+       }
+
+       team := &models.Team{
+               ID:    11,
+               OrgID: 26,
+       }
+
+       re := &models.Repository{
+               ID:      43,
+               Owner:   org,
+               OwnerID: 26,
+       }
+
+       repo := &context.Repository{
+               Owner: &models.User{
+                       ID:                        26,
+                       LowerName:                 "org26",
+                       RepoAdminChangeTeamAccess: false,
+               },
+               Repository: re,
+       }
+
+       ctx.Repo = repo
+
+       AddTeamPost(ctx)
+
+       assert.False(t, team.HasRepository(re.ID))
+       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
+       assert.NotEmpty(t, ctx.Flash.ErrorMsg)
+
+}
+
+func TestAddTeamPost_AddTeamTwice(t *testing.T) {
+       models.PrepareTestEnv(t)
+       ctx := test.MockContext(t, "org26/repo43")
+
+       ctx.Req.Form.Set("team", "team11")
+
+       org := &models.User{
+               LowerName: "org26",
+               Type:      models.UserTypeOrganization,
+       }
+
+       team := &models.Team{
+               ID:    11,
+               OrgID: 26,
+       }
+
+       re := &models.Repository{
+               ID:      43,
+               Owner:   org,
+               OwnerID: 26,
+       }
+
+       repo := &context.Repository{
+               Owner: &models.User{
+                       ID:                        26,
+                       LowerName:                 "org26",
+                       RepoAdminChangeTeamAccess: true,
+               },
+               Repository: re,
+       }
+
+       ctx.Repo = repo
+
+       AddTeamPost(ctx)
+
+       AddTeamPost(ctx)
+       assert.True(t, team.HasRepository(re.ID))
+       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
+       assert.NotEmpty(t, ctx.Flash.ErrorMsg)
+}
+
+func TestAddTeamPost_NonExistentTeam(t *testing.T) {
+       models.PrepareTestEnv(t)
+       ctx := test.MockContext(t, "org26/repo43")
+
+       ctx.Req.Form.Set("team", "team-non-existent")
+
+       org := &models.User{
+               LowerName: "org26",
+               Type:      models.UserTypeOrganization,
+       }
+
+       re := &models.Repository{
+               ID:      43,
+               Owner:   org,
+               OwnerID: 26,
+       }
+
+       repo := &context.Repository{
+               Owner: &models.User{
+                       ID:                        26,
+                       LowerName:                 "org26",
+                       RepoAdminChangeTeamAccess: true,
+               },
+               Repository: re,
+       }
+
+       ctx.Repo = repo
+
+       AddTeamPost(ctx)
+       assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
+       assert.NotEmpty(t, ctx.Flash.ErrorMsg)
+}
+
+func TestDeleteTeam(t *testing.T) {
+       models.PrepareTestEnv(t)
+       ctx := test.MockContext(t, "org3/team1/repo3")
+
+       ctx.Req.Form.Set("id", "2")
+
+       org := &models.User{
+               LowerName: "org3",
+               Type:      models.UserTypeOrganization,
+       }
+
+       team := &models.Team{
+               ID:    2,
+               OrgID: 3,
+       }
+
+       re := &models.Repository{
+               ID:      3,
+               Owner:   org,
+               OwnerID: 3,
+       }
+
+       repo := &context.Repository{
+               Owner: &models.User{
+                       ID:                        3,
+                       LowerName:                 "org3",
+                       RepoAdminChangeTeamAccess: true,
+               },
+               Repository: re,
+       }
+
+       ctx.Repo = repo
+
+       DeleteTeam(ctx)
+
+       assert.False(t, team.HasRepository(re.ID))
+}
diff --git a/routers/web/repo/topic.go b/routers/web/repo/topic.go
new file mode 100644 (file)
index 0000000..1d99b65
--- /dev/null
@@ -0,0 +1,61 @@
+// Copyright 2018 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 repo
+
+import (
+       "net/http"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+)
+
+// TopicsPost response for creating repository
+func TopicsPost(ctx *context.Context) {
+       if ctx.User == nil {
+               ctx.JSON(http.StatusForbidden, map[string]interface{}{
+                       "message": "Only owners could change the topics.",
+               })
+               return
+       }
+
+       var topics = make([]string, 0)
+       var topicsStr = strings.TrimSpace(ctx.Query("topics"))
+       if len(topicsStr) > 0 {
+               topics = strings.Split(topicsStr, ",")
+       }
+
+       validTopics, invalidTopics := models.SanitizeAndValidateTopics(topics)
+
+       if len(validTopics) > 25 {
+               ctx.JSON(http.StatusUnprocessableEntity, map[string]interface{}{
+                       "invalidTopics": nil,
+                       "message":       ctx.Tr("repo.topic.count_prompt"),
+               })
+               return
+       }
+
+       if len(invalidTopics) > 0 {
+               ctx.JSON(http.StatusUnprocessableEntity, map[string]interface{}{
+                       "invalidTopics": invalidTopics,
+                       "message":       ctx.Tr("repo.topic.format_prompt"),
+               })
+               return
+       }
+
+       err := models.SaveTopics(ctx.Repo.Repository.ID, validTopics...)
+       if err != nil {
+               log.Error("SaveTopics failed: %v", err)
+               ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
+                       "message": "Save topics failed.",
+               })
+               return
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "status": "ok",
+       })
+}
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
new file mode 100644 (file)
index 0000000..cd5b0f4
--- /dev/null
@@ -0,0 +1,808 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Copyright 2014 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 repo
+
+import (
+       "bytes"
+       "encoding/base64"
+       "fmt"
+       gotemplate "html/template"
+       "io"
+       "io/ioutil"
+       "net/http"
+       "net/url"
+       "path"
+       "strconv"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/cache"
+       "code.gitea.io/gitea/modules/charset"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/highlight"
+       "code.gitea.io/gitea/modules/lfs"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/markup"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/typesniffer"
+)
+
+const (
+       tplRepoEMPTY base.TplName = "repo/empty"
+       tplRepoHome  base.TplName = "repo/home"
+       tplWatchers  base.TplName = "repo/watchers"
+       tplForks     base.TplName = "repo/forks"
+       tplMigrating base.TplName = "repo/migrate/migrating"
+)
+
+type namedBlob struct {
+       name      string
+       isSymlink bool
+       blob      *git.Blob
+}
+
+func linesBytesCount(s []byte) int {
+       nl := []byte{'\n'}
+       n := bytes.Count(s, nl)
+       if len(s) > 0 && !bytes.HasSuffix(s, nl) {
+               n++
+       }
+       return n
+}
+
+// FIXME: There has to be a more efficient way of doing this
+func getReadmeFileFromPath(commit *git.Commit, treePath string) (*namedBlob, error) {
+       tree, err := commit.SubTree(treePath)
+       if err != nil {
+               return nil, err
+       }
+
+       entries, err := tree.ListEntries()
+       if err != nil {
+               return nil, err
+       }
+
+       var readmeFiles [4]*namedBlob
+       var exts = []string{".md", ".txt", ""} // sorted by priority
+       for _, entry := range entries {
+               if entry.IsDir() {
+                       continue
+               }
+               for i, ext := range exts {
+                       if markup.IsReadmeFile(entry.Name(), ext) {
+                               if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].name, entry.Blob().Name()) {
+                                       name := entry.Name()
+                                       isSymlink := entry.IsLink()
+                                       target := entry
+                                       if isSymlink {
+                                               target, err = entry.FollowLinks()
+                                               if err != nil && !git.IsErrBadLink(err) {
+                                                       return nil, err
+                                               }
+                                       }
+                                       if target != nil && (target.IsExecutable() || target.IsRegular()) {
+                                               readmeFiles[i] = &namedBlob{
+                                                       name,
+                                                       isSymlink,
+                                                       target.Blob(),
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               if markup.IsReadmeFile(entry.Name()) {
+                       if readmeFiles[3] == nil || base.NaturalSortLess(readmeFiles[3].name, entry.Blob().Name()) {
+                               name := entry.Name()
+                               isSymlink := entry.IsLink()
+                               if isSymlink {
+                                       entry, err = entry.FollowLinks()
+                                       if err != nil && !git.IsErrBadLink(err) {
+                                               return nil, err
+                                       }
+                               }
+                               if entry != nil && (entry.IsExecutable() || entry.IsRegular()) {
+                                       readmeFiles[3] = &namedBlob{
+                                               name,
+                                               isSymlink,
+                                               entry.Blob(),
+                                       }
+                               }
+                       }
+               }
+       }
+       var readmeFile *namedBlob
+       for _, f := range readmeFiles {
+               if f != nil {
+                       readmeFile = f
+                       break
+               }
+       }
+       return readmeFile, nil
+}
+
+func renderDirectory(ctx *context.Context, treeLink string) {
+       tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath)
+       if err != nil {
+               ctx.NotFoundOrServerError("Repo.Commit.SubTree", git.IsErrNotExist, err)
+               return
+       }
+
+       entries, err := tree.ListEntries()
+       if err != nil {
+               ctx.ServerError("ListEntries", err)
+               return
+       }
+       entries.CustomSort(base.NaturalSortLess)
+
+       var c *git.LastCommitCache
+       if setting.CacheService.LastCommit.Enabled && ctx.Repo.CommitsCount >= setting.CacheService.LastCommit.CommitsCount {
+               c = git.NewLastCommitCache(ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, setting.LastCommitCacheTTLSeconds, cache.GetCache())
+       }
+
+       var latestCommit *git.Commit
+       ctx.Data["Files"], latestCommit, err = entries.GetCommitsInfo(ctx, ctx.Repo.Commit, ctx.Repo.TreePath, c)
+       if err != nil {
+               ctx.ServerError("GetCommitsInfo", err)
+               return
+       }
+
+       // 3 for the extensions in exts[] in order
+       // the last one is for a readme that doesn't
+       // strictly match an extension
+       var readmeFiles [4]*namedBlob
+       var docsEntries [3]*git.TreeEntry
+       var exts = []string{".md", ".txt", ""} // sorted by priority
+       for _, entry := range entries {
+               if entry.IsDir() {
+                       lowerName := strings.ToLower(entry.Name())
+                       switch lowerName {
+                       case "docs":
+                               if entry.Name() == "docs" || docsEntries[0] == nil {
+                                       docsEntries[0] = entry
+                               }
+                       case ".gitea":
+                               if entry.Name() == ".gitea" || docsEntries[1] == nil {
+                                       docsEntries[1] = entry
+                               }
+                       case ".github":
+                               if entry.Name() == ".github" || docsEntries[2] == nil {
+                                       docsEntries[2] = entry
+                               }
+                       }
+                       continue
+               }
+
+               for i, ext := range exts {
+                       if markup.IsReadmeFile(entry.Name(), ext) {
+                               log.Debug("%s", entry.Name())
+                               name := entry.Name()
+                               isSymlink := entry.IsLink()
+                               target := entry
+                               if isSymlink {
+                                       target, err = entry.FollowLinks()
+                                       if err != nil && !git.IsErrBadLink(err) {
+                                               ctx.ServerError("FollowLinks", err)
+                                               return
+                                       }
+                               }
+                               log.Debug("%t", target == nil)
+                               if target != nil && (target.IsExecutable() || target.IsRegular()) {
+                                       readmeFiles[i] = &namedBlob{
+                                               name,
+                                               isSymlink,
+                                               target.Blob(),
+                                       }
+                               }
+                       }
+               }
+
+               if markup.IsReadmeFile(entry.Name()) {
+                       name := entry.Name()
+                       isSymlink := entry.IsLink()
+                       if isSymlink {
+                               entry, err = entry.FollowLinks()
+                               if err != nil && !git.IsErrBadLink(err) {
+                                       ctx.ServerError("FollowLinks", err)
+                                       return
+                               }
+                       }
+                       if entry != nil && (entry.IsExecutable() || entry.IsRegular()) {
+                               readmeFiles[3] = &namedBlob{
+                                       name,
+                                       isSymlink,
+                                       entry.Blob(),
+                               }
+                       }
+               }
+       }
+
+       var readmeFile *namedBlob
+       readmeTreelink := treeLink
+       for _, f := range readmeFiles {
+               if f != nil {
+                       readmeFile = f
+                       break
+               }
+       }
+
+       if ctx.Repo.TreePath == "" && readmeFile == nil {
+               for _, entry := range docsEntries {
+                       if entry == nil {
+                               continue
+                       }
+                       readmeFile, err = getReadmeFileFromPath(ctx.Repo.Commit, entry.GetSubJumpablePathName())
+                       if err != nil {
+                               ctx.ServerError("getReadmeFileFromPath", err)
+                               return
+                       }
+                       if readmeFile != nil {
+                               readmeFile.name = entry.Name() + "/" + readmeFile.name
+                               readmeTreelink = treeLink + "/" + entry.GetSubJumpablePathName()
+                               break
+                       }
+               }
+       }
+
+       if readmeFile != nil {
+               ctx.Data["RawFileLink"] = ""
+               ctx.Data["ReadmeInList"] = true
+               ctx.Data["ReadmeExist"] = true
+               ctx.Data["FileIsSymlink"] = readmeFile.isSymlink
+
+               dataRc, err := readmeFile.blob.DataAsync()
+               if err != nil {
+                       ctx.ServerError("Data", err)
+                       return
+               }
+               defer dataRc.Close()
+
+               buf := make([]byte, 1024)
+               n, _ := dataRc.Read(buf)
+               buf = buf[:n]
+
+               st := typesniffer.DetectContentType(buf)
+               isTextFile := st.IsText()
+
+               ctx.Data["FileIsText"] = isTextFile
+               ctx.Data["FileName"] = readmeFile.name
+               fileSize := int64(0)
+               isLFSFile := false
+               ctx.Data["IsLFSFile"] = false
+
+               // FIXME: what happens when README file is an image?
+               if isTextFile && setting.LFS.StartServer {
+                       pointer, _ := lfs.ReadPointerFromBuffer(buf)
+                       if pointer.IsValid() {
+                               meta, err := ctx.Repo.Repository.GetLFSMetaObjectByOid(pointer.Oid)
+                               if err != nil && err != models.ErrLFSObjectNotExist {
+                                       ctx.ServerError("GetLFSMetaObject", err)
+                                       return
+                               }
+                               if meta != nil {
+                                       ctx.Data["IsLFSFile"] = true
+                                       isLFSFile = true
+
+                                       // OK read the lfs object
+                                       var err error
+                                       dataRc, err = lfs.ReadMetaObject(pointer)
+                                       if err != nil {
+                                               ctx.ServerError("ReadMetaObject", err)
+                                               return
+                                       }
+                                       defer dataRc.Close()
+
+                                       buf = make([]byte, 1024)
+                                       n, err = dataRc.Read(buf)
+                                       if err != nil {
+                                               ctx.ServerError("Data", err)
+                                               return
+                                       }
+                                       buf = buf[:n]
+
+                                       st = typesniffer.DetectContentType(buf)
+                                       isTextFile = st.IsText()
+                                       ctx.Data["IsTextFile"] = isTextFile
+
+                                       fileSize = meta.Size
+                                       ctx.Data["FileSize"] = meta.Size
+                                       filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.name))
+                                       ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s.git/info/lfs/objects/%s/%s", setting.AppURL, ctx.Repo.Repository.FullName(), meta.Oid, filenameBase64)
+                               }
+                       }
+               }
+
+               if !isLFSFile {
+                       fileSize = readmeFile.blob.Size()
+               }
+
+               if isTextFile {
+                       if fileSize >= setting.UI.MaxDisplayFileSize {
+                               // Pretend that this is a normal text file to display 'This file is too large to be shown'
+                               ctx.Data["IsFileTooLarge"] = true
+                               ctx.Data["IsTextFile"] = true
+                               ctx.Data["FileSize"] = fileSize
+                       } else {
+                               rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc))
+
+                               if markupType := markup.Type(readmeFile.name); markupType != "" {
+                                       ctx.Data["IsMarkup"] = true
+                                       ctx.Data["MarkupType"] = string(markupType)
+                                       var result strings.Builder
+                                       err := markup.Render(&markup.RenderContext{
+                                               Filename:  readmeFile.name,
+                                               URLPrefix: readmeTreelink,
+                                               Metas:     ctx.Repo.Repository.ComposeDocumentMetas(),
+                                       }, rd, &result)
+                                       if err != nil {
+                                               log.Error("Render failed: %v then fallback", err)
+                                               bs, _ := ioutil.ReadAll(rd)
+                                               ctx.Data["FileContent"] = strings.ReplaceAll(
+                                                       gotemplate.HTMLEscapeString(string(bs)), "\n", `<br>`,
+                                               )
+                                       } else {
+                                               ctx.Data["FileContent"] = result.String()
+                                       }
+                               } else {
+                                       ctx.Data["IsRenderedHTML"] = true
+                                       ctx.Data["FileContent"] = strings.ReplaceAll(
+                                               gotemplate.HTMLEscapeString(string(buf)), "\n", `<br>`,
+                                       )
+                               }
+                       }
+               }
+       }
+
+       // Show latest commit info of repository in table header,
+       // or of directory if not in root directory.
+       ctx.Data["LatestCommit"] = latestCommit
+       verification := models.ParseCommitWithSignature(latestCommit)
+
+       if err := models.CalculateTrustStatus(verification, ctx.Repo.Repository, nil); err != nil {
+               ctx.ServerError("CalculateTrustStatus", err)
+               return
+       }
+       ctx.Data["LatestCommitVerification"] = verification
+
+       ctx.Data["LatestCommitUser"] = models.ValidateCommitWithEmail(latestCommit)
+
+       statuses, err := models.GetLatestCommitStatus(ctx.Repo.Repository.ID, ctx.Repo.Commit.ID.String(), models.ListOptions{})
+       if err != nil {
+               log.Error("GetLatestCommitStatus: %v", err)
+       }
+
+       ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(statuses)
+       ctx.Data["LatestCommitStatuses"] = statuses
+
+       // Check permission to add or upload new file.
+       if ctx.Repo.CanWrite(models.UnitTypeCode) && ctx.Repo.IsViewBranch {
+               ctx.Data["CanAddFile"] = !ctx.Repo.Repository.IsArchived
+               ctx.Data["CanUploadFile"] = setting.Repository.Upload.Enabled && !ctx.Repo.Repository.IsArchived
+       }
+
+       ctx.Data["SSHDomain"] = setting.SSH.Domain
+}
+
+func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink string) {
+       ctx.Data["IsViewFile"] = true
+       blob := entry.Blob()
+       dataRc, err := blob.DataAsync()
+       if err != nil {
+               ctx.ServerError("DataAsync", err)
+               return
+       }
+       defer dataRc.Close()
+
+       ctx.Data["Title"] = ctx.Data["Title"].(string) + " - " + ctx.Repo.TreePath + " at " + ctx.Repo.BranchName
+
+       fileSize := blob.Size()
+       ctx.Data["FileIsSymlink"] = entry.IsLink()
+       ctx.Data["FileName"] = blob.Name()
+       ctx.Data["RawFileLink"] = rawLink + "/" + ctx.Repo.TreePath
+
+       buf := make([]byte, 1024)
+       n, _ := dataRc.Read(buf)
+       buf = buf[:n]
+
+       st := typesniffer.DetectContentType(buf)
+       isTextFile := st.IsText()
+
+       isLFSFile := false
+       isDisplayingSource := ctx.Query("display") == "source"
+       isDisplayingRendered := !isDisplayingSource
+
+       //Check for LFS meta file
+       if isTextFile && setting.LFS.StartServer {
+               pointer, _ := lfs.ReadPointerFromBuffer(buf)
+               if pointer.IsValid() {
+                       meta, err := ctx.Repo.Repository.GetLFSMetaObjectByOid(pointer.Oid)
+                       if err != nil && err != models.ErrLFSObjectNotExist {
+                               ctx.ServerError("GetLFSMetaObject", err)
+                               return
+                       }
+                       if meta != nil {
+                               isLFSFile = true
+
+                               // OK read the lfs object
+                               var err error
+                               dataRc, err = lfs.ReadMetaObject(pointer)
+                               if err != nil {
+                                       ctx.ServerError("ReadMetaObject", err)
+                                       return
+                               }
+                               defer dataRc.Close()
+
+                               buf = make([]byte, 1024)
+                               n, err = dataRc.Read(buf)
+                               // Error EOF don't mean there is an error, it just means we read to
+                               // the end
+                               if err != nil && err != io.EOF {
+                                       ctx.ServerError("Data", err)
+                                       return
+                               }
+                               buf = buf[:n]
+
+                               st = typesniffer.DetectContentType(buf)
+                               isTextFile = st.IsText()
+
+                               fileSize = meta.Size
+                               ctx.Data["RawFileLink"] = fmt.Sprintf("%s/media/%s/%s", ctx.Repo.RepoLink, ctx.Repo.BranchNameSubURL(), ctx.Repo.TreePath)
+                       }
+               }
+       }
+
+       isRepresentableAsText := st.IsRepresentableAsText()
+       if !isRepresentableAsText {
+               // If we can't show plain text, always try to render.
+               isDisplayingSource = false
+               isDisplayingRendered = true
+       }
+       ctx.Data["IsLFSFile"] = isLFSFile
+       ctx.Data["FileSize"] = fileSize
+       ctx.Data["IsTextFile"] = isTextFile
+       ctx.Data["IsRepresentableAsText"] = isRepresentableAsText
+       ctx.Data["IsDisplayingSource"] = isDisplayingSource
+       ctx.Data["IsDisplayingRendered"] = isDisplayingRendered
+       ctx.Data["IsTextSource"] = isTextFile || isDisplayingSource
+
+       // Check LFS Lock
+       lfsLock, err := ctx.Repo.Repository.GetTreePathLock(ctx.Repo.TreePath)
+       ctx.Data["LFSLock"] = lfsLock
+       if err != nil {
+               ctx.ServerError("GetTreePathLock", err)
+               return
+       }
+       if lfsLock != nil {
+               ctx.Data["LFSLockOwner"] = lfsLock.Owner.DisplayName()
+               ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked")
+       }
+
+       // Assume file is not editable first.
+       if isLFSFile {
+               ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_lfs_files")
+       } else if !isRepresentableAsText {
+               ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files")
+       }
+
+       switch {
+       case isRepresentableAsText:
+               if st.IsSvgImage() {
+                       ctx.Data["IsImageFile"] = true
+                       ctx.Data["HasSourceRenderedToggle"] = true
+               }
+
+               if fileSize >= setting.UI.MaxDisplayFileSize {
+                       ctx.Data["IsFileTooLarge"] = true
+                       break
+               }
+
+               rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc))
+               readmeExist := markup.IsReadmeFile(blob.Name())
+               ctx.Data["ReadmeExist"] = readmeExist
+               if markupType := markup.Type(blob.Name()); markupType != "" {
+                       ctx.Data["IsMarkup"] = true
+                       ctx.Data["MarkupType"] = markupType
+                       var result strings.Builder
+                       err := markup.Render(&markup.RenderContext{
+                               Filename:  blob.Name(),
+                               URLPrefix: path.Dir(treeLink),
+                               Metas:     ctx.Repo.Repository.ComposeDocumentMetas(),
+                       }, rd, &result)
+                       if err != nil {
+                               ctx.ServerError("Render", err)
+                               return
+                       }
+                       ctx.Data["FileContent"] = result.String()
+               } else if readmeExist {
+                       buf, _ := ioutil.ReadAll(rd)
+                       ctx.Data["IsRenderedHTML"] = true
+                       ctx.Data["FileContent"] = strings.ReplaceAll(
+                               gotemplate.HTMLEscapeString(string(buf)), "\n", `<br>`,
+                       )
+               } else {
+                       buf, _ := ioutil.ReadAll(rd)
+                       lineNums := linesBytesCount(buf)
+                       ctx.Data["NumLines"] = strconv.Itoa(lineNums)
+                       ctx.Data["NumLinesSet"] = true
+                       ctx.Data["FileContent"] = highlight.File(lineNums, blob.Name(), buf)
+               }
+               if !isLFSFile {
+                       if ctx.Repo.CanEnableEditor() {
+                               if lfsLock != nil && lfsLock.OwnerID != ctx.User.ID {
+                                       ctx.Data["CanEditFile"] = false
+                                       ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
+                               } else {
+                                       ctx.Data["CanEditFile"] = true
+                                       ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file")
+                               }
+                       } else if !ctx.Repo.IsViewBranch {
+                               ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
+                       } else if !ctx.Repo.CanWrite(models.UnitTypeCode) {
+                               ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit")
+                       }
+               }
+
+       case st.IsPDF():
+               ctx.Data["IsPDFFile"] = true
+       case st.IsVideo():
+               ctx.Data["IsVideoFile"] = true
+       case st.IsAudio():
+               ctx.Data["IsAudioFile"] = true
+       case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()):
+               ctx.Data["IsImageFile"] = true
+       default:
+               if fileSize >= setting.UI.MaxDisplayFileSize {
+                       ctx.Data["IsFileTooLarge"] = true
+                       break
+               }
+
+               if markupType := markup.Type(blob.Name()); markupType != "" {
+                       rd := io.MultiReader(bytes.NewReader(buf), dataRc)
+                       ctx.Data["IsMarkup"] = true
+                       ctx.Data["MarkupType"] = markupType
+                       var result strings.Builder
+                       err := markup.Render(&markup.RenderContext{
+                               Filename:  blob.Name(),
+                               URLPrefix: path.Dir(treeLink),
+                               Metas:     ctx.Repo.Repository.ComposeDocumentMetas(),
+                       }, rd, &result)
+                       if err != nil {
+                               ctx.ServerError("Render", err)
+                               return
+                       }
+                       ctx.Data["FileContent"] = result.String()
+               }
+       }
+
+       if ctx.Repo.CanEnableEditor() {
+               if lfsLock != nil && lfsLock.OwnerID != ctx.User.ID {
+                       ctx.Data["CanDeleteFile"] = false
+                       ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
+               } else {
+                       ctx.Data["CanDeleteFile"] = true
+                       ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.delete_this_file")
+               }
+       } else if !ctx.Repo.IsViewBranch {
+               ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
+       } else if !ctx.Repo.CanWrite(models.UnitTypeCode) {
+               ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access")
+       }
+}
+
+func safeURL(address string) string {
+       u, err := url.Parse(address)
+       if err != nil {
+               return address
+       }
+       u.User = nil
+       return u.String()
+}
+
+// Home render repository home page
+func Home(ctx *context.Context) {
+       if len(ctx.Repo.Units) > 0 {
+               if ctx.Repo.Repository.IsBeingCreated() {
+                       task, err := models.GetMigratingTask(ctx.Repo.Repository.ID)
+                       if err != nil {
+                               ctx.ServerError("models.GetMigratingTask", err)
+                               return
+                       }
+                       cfg, err := task.MigrateConfig()
+                       if err != nil {
+                               ctx.ServerError("task.MigrateConfig", err)
+                               return
+                       }
+
+                       ctx.Data["Repo"] = ctx.Repo
+                       ctx.Data["MigrateTask"] = task
+                       ctx.Data["CloneAddr"] = safeURL(cfg.CloneAddr)
+                       ctx.HTML(http.StatusOK, tplMigrating)
+                       return
+               }
+
+               if ctx.IsSigned {
+                       // Set repo notification-status read if unread
+                       if err := ctx.Repo.Repository.ReadBy(ctx.User.ID); err != nil {
+                               ctx.ServerError("ReadBy", err)
+                               return
+                       }
+               }
+
+               var firstUnit *models.Unit
+               for _, repoUnit := range ctx.Repo.Units {
+                       if repoUnit.Type == models.UnitTypeCode {
+                               renderCode(ctx)
+                               return
+                       }
+
+                       unit, ok := models.Units[repoUnit.Type]
+                       if ok && (firstUnit == nil || !firstUnit.IsLessThan(unit)) {
+                               firstUnit = &unit
+                       }
+               }
+
+               if firstUnit != nil {
+                       ctx.Redirect(fmt.Sprintf("%s/%s%s", setting.AppSubURL, ctx.Repo.Repository.FullName(), firstUnit.URI))
+                       return
+               }
+       }
+
+       ctx.NotFound("Home", fmt.Errorf(ctx.Tr("units.error.no_unit_allowed_repo")))
+}
+
+func renderLanguageStats(ctx *context.Context) {
+       langs, err := ctx.Repo.Repository.GetTopLanguageStats(5)
+       if err != nil {
+               ctx.ServerError("Repo.GetTopLanguageStats", err)
+               return
+       }
+
+       ctx.Data["LanguageStats"] = langs
+}
+
+func renderRepoTopics(ctx *context.Context) {
+       topics, err := models.FindTopics(&models.FindTopicOptions{
+               RepoID: ctx.Repo.Repository.ID,
+       })
+       if err != nil {
+               ctx.ServerError("models.FindTopics", err)
+               return
+       }
+       ctx.Data["Topics"] = topics
+}
+
+func renderCode(ctx *context.Context) {
+       ctx.Data["PageIsViewCode"] = true
+
+       if ctx.Repo.Repository.IsEmpty {
+               ctx.HTML(http.StatusOK, tplRepoEMPTY)
+               return
+       }
+
+       title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name
+       if len(ctx.Repo.Repository.Description) > 0 {
+               title += ": " + ctx.Repo.Repository.Description
+       }
+       ctx.Data["Title"] = title
+
+       branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
+       treeLink := branchLink
+       rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL()
+
+       if len(ctx.Repo.TreePath) > 0 {
+               treeLink += "/" + ctx.Repo.TreePath
+       }
+
+       // Get Topics of this repo
+       renderRepoTopics(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       // Get current entry user currently looking at.
+       entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
+       if err != nil {
+               ctx.NotFoundOrServerError("Repo.Commit.GetTreeEntryByPath", git.IsErrNotExist, err)
+               return
+       }
+
+       renderLanguageStats(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       if entry.IsDir() {
+               renderDirectory(ctx, treeLink)
+       } else {
+               renderFile(ctx, entry, treeLink, rawLink)
+       }
+       if ctx.Written() {
+               return
+       }
+
+       var treeNames []string
+       paths := make([]string, 0, 5)
+       if len(ctx.Repo.TreePath) > 0 {
+               treeNames = strings.Split(ctx.Repo.TreePath, "/")
+               for i := range treeNames {
+                       paths = append(paths, strings.Join(treeNames[:i+1], "/"))
+               }
+
+               ctx.Data["HasParentPath"] = true
+               if len(paths)-2 >= 0 {
+                       ctx.Data["ParentPath"] = "/" + paths[len(paths)-2]
+               }
+       }
+
+       ctx.Data["Paths"] = paths
+       ctx.Data["TreeLink"] = treeLink
+       ctx.Data["TreeNames"] = treeNames
+       ctx.Data["BranchLink"] = branchLink
+       ctx.HTML(http.StatusOK, tplRepoHome)
+}
+
+// RenderUserCards render a page show users according the input templaet
+func RenderUserCards(ctx *context.Context, total int, getter func(opts models.ListOptions) ([]*models.User, error), tpl base.TplName) {
+       page := ctx.QueryInt("page")
+       if page <= 0 {
+               page = 1
+       }
+       pager := context.NewPagination(total, models.ItemsPerPage, page, 5)
+       ctx.Data["Page"] = pager
+
+       items, err := getter(models.ListOptions{
+               Page:     pager.Paginater.Current(),
+               PageSize: models.ItemsPerPage,
+       })
+       if err != nil {
+               ctx.ServerError("getter", err)
+               return
+       }
+       ctx.Data["Cards"] = items
+
+       ctx.HTML(http.StatusOK, tpl)
+}
+
+// Watchers render repository's watch users
+func Watchers(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.watchers")
+       ctx.Data["CardsTitle"] = ctx.Tr("repo.watchers")
+       ctx.Data["PageIsWatchers"] = true
+
+       RenderUserCards(ctx, ctx.Repo.Repository.NumWatches, ctx.Repo.Repository.GetWatchers, tplWatchers)
+}
+
+// Stars render repository's starred users
+func Stars(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.stargazers")
+       ctx.Data["CardsTitle"] = ctx.Tr("repo.stargazers")
+       ctx.Data["PageIsStargazers"] = true
+       RenderUserCards(ctx, ctx.Repo.Repository.NumStars, ctx.Repo.Repository.GetStargazers, tplWatchers)
+}
+
+// Forks render repository's forked users
+func Forks(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repos.forks")
+
+       // TODO: need pagination
+       forks, err := ctx.Repo.Repository.GetForks(models.ListOptions{})
+       if err != nil {
+               ctx.ServerError("GetForks", err)
+               return
+       }
+
+       for _, fork := range forks {
+               if err = fork.GetOwner(); err != nil {
+                       ctx.ServerError("GetOwner", err)
+                       return
+               }
+       }
+       ctx.Data["Forks"] = forks
+
+       ctx.HTML(http.StatusOK, tplForks)
+}
diff --git a/routers/web/repo/webhook.go b/routers/web/repo/webhook.go
new file mode 100644 (file)
index 0000000..fe16d24
--- /dev/null
@@ -0,0 +1,1131 @@
+// Copyright 2015 The Gogs Authors. All rights reserved.
+// 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 repo
+
+import (
+       "errors"
+       "fmt"
+       "net/http"
+       "path"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/convert"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/setting"
+       api "code.gitea.io/gitea/modules/structs"
+       "code.gitea.io/gitea/modules/util"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+       "code.gitea.io/gitea/services/webhook"
+       jsoniter "github.com/json-iterator/go"
+)
+
+const (
+       tplHooks        base.TplName = "repo/settings/webhook/base"
+       tplHookNew      base.TplName = "repo/settings/webhook/new"
+       tplOrgHookNew   base.TplName = "org/settings/hook_new"
+       tplAdminHookNew base.TplName = "admin/hook_new"
+)
+
+// Webhooks render web hooks list page
+func Webhooks(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.settings.hooks")
+       ctx.Data["PageIsSettingsHooks"] = true
+       ctx.Data["BaseLink"] = ctx.Repo.RepoLink + "/settings/hooks"
+       ctx.Data["BaseLinkNew"] = ctx.Repo.RepoLink + "/settings/hooks"
+       ctx.Data["Description"] = ctx.Tr("repo.settings.hooks_desc", "https://docs.gitea.io/en-us/webhooks/")
+
+       ws, err := models.GetWebhooksByRepoID(ctx.Repo.Repository.ID, models.ListOptions{})
+       if err != nil {
+               ctx.ServerError("GetWebhooksByRepoID", err)
+               return
+       }
+       ctx.Data["Webhooks"] = ws
+
+       ctx.HTML(http.StatusOK, tplHooks)
+}
+
+type orgRepoCtx struct {
+       OrgID           int64
+       RepoID          int64
+       IsAdmin         bool
+       IsSystemWebhook bool
+       Link            string
+       LinkNew         string
+       NewTemplate     base.TplName
+}
+
+// getOrgRepoCtx determines whether this is a repo, organization, or admin (both default and system) context.
+func getOrgRepoCtx(ctx *context.Context) (*orgRepoCtx, error) {
+       if len(ctx.Repo.RepoLink) > 0 {
+               return &orgRepoCtx{
+                       RepoID:      ctx.Repo.Repository.ID,
+                       Link:        path.Join(ctx.Repo.RepoLink, "settings/hooks"),
+                       LinkNew:     path.Join(ctx.Repo.RepoLink, "settings/hooks"),
+                       NewTemplate: tplHookNew,
+               }, nil
+       }
+
+       if len(ctx.Org.OrgLink) > 0 {
+               return &orgRepoCtx{
+                       OrgID:       ctx.Org.Organization.ID,
+                       Link:        path.Join(ctx.Org.OrgLink, "settings/hooks"),
+                       LinkNew:     path.Join(ctx.Org.OrgLink, "settings/hooks"),
+                       NewTemplate: tplOrgHookNew,
+               }, nil
+       }
+
+       if ctx.User.IsAdmin {
+               // Are we looking at default webhooks?
+               if ctx.Params(":configType") == "default-hooks" {
+                       return &orgRepoCtx{
+                               IsAdmin:     true,
+                               Link:        path.Join(setting.AppSubURL, "/admin/hooks"),
+                               LinkNew:     path.Join(setting.AppSubURL, "/admin/default-hooks"),
+                               NewTemplate: tplAdminHookNew,
+                       }, nil
+               }
+
+               // Must be system webhooks instead
+               return &orgRepoCtx{
+                       IsAdmin:         true,
+                       IsSystemWebhook: true,
+                       Link:            path.Join(setting.AppSubURL, "/admin/hooks"),
+                       LinkNew:         path.Join(setting.AppSubURL, "/admin/system-hooks"),
+                       NewTemplate:     tplAdminHookNew,
+               }, nil
+       }
+
+       return nil, errors.New("Unable to set OrgRepo context")
+}
+
+func checkHookType(ctx *context.Context) string {
+       hookType := strings.ToLower(ctx.Params(":type"))
+       if !util.IsStringInSlice(hookType, setting.Webhook.Types, true) {
+               ctx.NotFound("checkHookType", nil)
+               return ""
+       }
+       return hookType
+}
+
+// WebhooksNew render creating webhook page
+func WebhooksNew(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook")
+       ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
+
+       orCtx, err := getOrgRepoCtx(ctx)
+       if err != nil {
+               ctx.ServerError("getOrgRepoCtx", err)
+               return
+       }
+
+       if orCtx.IsAdmin && orCtx.IsSystemWebhook {
+               ctx.Data["PageIsAdminSystemHooks"] = true
+               ctx.Data["PageIsAdminSystemHooksNew"] = true
+       } else if orCtx.IsAdmin {
+               ctx.Data["PageIsAdminDefaultHooks"] = true
+               ctx.Data["PageIsAdminDefaultHooksNew"] = true
+       } else {
+               ctx.Data["PageIsSettingsHooks"] = true
+               ctx.Data["PageIsSettingsHooksNew"] = true
+       }
+
+       hookType := checkHookType(ctx)
+       ctx.Data["HookType"] = hookType
+       if ctx.Written() {
+               return
+       }
+       if hookType == "discord" {
+               ctx.Data["DiscordHook"] = map[string]interface{}{
+                       "Username": "Gitea",
+                       "IconURL":  setting.AppURL + "img/favicon.png",
+               }
+       }
+       ctx.Data["BaseLink"] = orCtx.LinkNew
+
+       ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+}
+
+// ParseHookEvent convert web form content to models.HookEvent
+func ParseHookEvent(form forms.WebhookForm) *models.HookEvent {
+       return &models.HookEvent{
+               PushOnly:       form.PushOnly(),
+               SendEverything: form.SendEverything(),
+               ChooseEvents:   form.ChooseEvents(),
+               HookEvents: models.HookEvents{
+                       Create:               form.Create,
+                       Delete:               form.Delete,
+                       Fork:                 form.Fork,
+                       Issues:               form.Issues,
+                       IssueAssign:          form.IssueAssign,
+                       IssueLabel:           form.IssueLabel,
+                       IssueMilestone:       form.IssueMilestone,
+                       IssueComment:         form.IssueComment,
+                       Release:              form.Release,
+                       Push:                 form.Push,
+                       PullRequest:          form.PullRequest,
+                       PullRequestAssign:    form.PullRequestAssign,
+                       PullRequestLabel:     form.PullRequestLabel,
+                       PullRequestMilestone: form.PullRequestMilestone,
+                       PullRequestComment:   form.PullRequestComment,
+                       PullRequestReview:    form.PullRequestReview,
+                       PullRequestSync:      form.PullRequestSync,
+                       Repository:           form.Repository,
+               },
+               BranchFilter: form.BranchFilter,
+       }
+}
+
+// GiteaHooksNewPost response for creating Gitea webhook
+func GiteaHooksNewPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.NewWebhookForm)
+       ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook")
+       ctx.Data["PageIsSettingsHooks"] = true
+       ctx.Data["PageIsSettingsHooksNew"] = true
+       ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
+       ctx.Data["HookType"] = models.GITEA
+
+       orCtx, err := getOrgRepoCtx(ctx)
+       if err != nil {
+               ctx.ServerError("getOrgRepoCtx", err)
+               return
+       }
+       ctx.Data["BaseLink"] = orCtx.LinkNew
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+               return
+       }
+
+       contentType := models.ContentTypeJSON
+       if models.HookContentType(form.ContentType) == models.ContentTypeForm {
+               contentType = models.ContentTypeForm
+       }
+
+       w := &models.Webhook{
+               RepoID:          orCtx.RepoID,
+               URL:             form.PayloadURL,
+               HTTPMethod:      form.HTTPMethod,
+               ContentType:     contentType,
+               Secret:          form.Secret,
+               HookEvent:       ParseHookEvent(form.WebhookForm),
+               IsActive:        form.Active,
+               Type:            models.GITEA,
+               OrgID:           orCtx.OrgID,
+               IsSystemWebhook: orCtx.IsSystemWebhook,
+       }
+       if err := w.UpdateEvent(); err != nil {
+               ctx.ServerError("UpdateEvent", err)
+               return
+       } else if err := models.CreateWebhook(w); err != nil {
+               ctx.ServerError("CreateWebhook", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
+       ctx.Redirect(orCtx.Link)
+}
+
+// GogsHooksNewPost response for creating webhook
+func GogsHooksNewPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.NewGogshookForm)
+       newGogsWebhookPost(ctx, *form, models.GOGS)
+}
+
+// newGogsWebhookPost response for creating gogs hook
+func newGogsWebhookPost(ctx *context.Context, form forms.NewGogshookForm, kind models.HookTaskType) {
+       ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook")
+       ctx.Data["PageIsSettingsHooks"] = true
+       ctx.Data["PageIsSettingsHooksNew"] = true
+       ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
+       ctx.Data["HookType"] = models.GOGS
+
+       orCtx, err := getOrgRepoCtx(ctx)
+       if err != nil {
+               ctx.ServerError("getOrgRepoCtx", err)
+               return
+       }
+       ctx.Data["BaseLink"] = orCtx.LinkNew
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+               return
+       }
+
+       contentType := models.ContentTypeJSON
+       if models.HookContentType(form.ContentType) == models.ContentTypeForm {
+               contentType = models.ContentTypeForm
+       }
+
+       w := &models.Webhook{
+               RepoID:          orCtx.RepoID,
+               URL:             form.PayloadURL,
+               ContentType:     contentType,
+               Secret:          form.Secret,
+               HookEvent:       ParseHookEvent(form.WebhookForm),
+               IsActive:        form.Active,
+               Type:            kind,
+               OrgID:           orCtx.OrgID,
+               IsSystemWebhook: orCtx.IsSystemWebhook,
+       }
+       if err := w.UpdateEvent(); err != nil {
+               ctx.ServerError("UpdateEvent", err)
+               return
+       } else if err := models.CreateWebhook(w); err != nil {
+               ctx.ServerError("CreateWebhook", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
+       ctx.Redirect(orCtx.Link)
+}
+
+// DiscordHooksNewPost response for creating discord hook
+func DiscordHooksNewPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.NewDiscordHookForm)
+       ctx.Data["Title"] = ctx.Tr("repo.settings")
+       ctx.Data["PageIsSettingsHooks"] = true
+       ctx.Data["PageIsSettingsHooksNew"] = true
+       ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
+       ctx.Data["HookType"] = models.DISCORD
+
+       orCtx, err := getOrgRepoCtx(ctx)
+       if err != nil {
+               ctx.ServerError("getOrgRepoCtx", err)
+               return
+       }
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+               return
+       }
+
+       json := jsoniter.ConfigCompatibleWithStandardLibrary
+       meta, err := json.Marshal(&webhook.DiscordMeta{
+               Username: form.Username,
+               IconURL:  form.IconURL,
+       })
+       if err != nil {
+               ctx.ServerError("Marshal", err)
+               return
+       }
+
+       w := &models.Webhook{
+               RepoID:          orCtx.RepoID,
+               URL:             form.PayloadURL,
+               ContentType:     models.ContentTypeJSON,
+               HookEvent:       ParseHookEvent(form.WebhookForm),
+               IsActive:        form.Active,
+               Type:            models.DISCORD,
+               Meta:            string(meta),
+               OrgID:           orCtx.OrgID,
+               IsSystemWebhook: orCtx.IsSystemWebhook,
+       }
+       if err := w.UpdateEvent(); err != nil {
+               ctx.ServerError("UpdateEvent", err)
+               return
+       } else if err := models.CreateWebhook(w); err != nil {
+               ctx.ServerError("CreateWebhook", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
+       ctx.Redirect(orCtx.Link)
+}
+
+// DingtalkHooksNewPost response for creating dingtalk hook
+func DingtalkHooksNewPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.NewDingtalkHookForm)
+       ctx.Data["Title"] = ctx.Tr("repo.settings")
+       ctx.Data["PageIsSettingsHooks"] = true
+       ctx.Data["PageIsSettingsHooksNew"] = true
+       ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
+       ctx.Data["HookType"] = models.DINGTALK
+
+       orCtx, err := getOrgRepoCtx(ctx)
+       if err != nil {
+               ctx.ServerError("getOrgRepoCtx", err)
+               return
+       }
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+               return
+       }
+
+       w := &models.Webhook{
+               RepoID:          orCtx.RepoID,
+               URL:             form.PayloadURL,
+               ContentType:     models.ContentTypeJSON,
+               HookEvent:       ParseHookEvent(form.WebhookForm),
+               IsActive:        form.Active,
+               Type:            models.DINGTALK,
+               Meta:            "",
+               OrgID:           orCtx.OrgID,
+               IsSystemWebhook: orCtx.IsSystemWebhook,
+       }
+       if err := w.UpdateEvent(); err != nil {
+               ctx.ServerError("UpdateEvent", err)
+               return
+       } else if err := models.CreateWebhook(w); err != nil {
+               ctx.ServerError("CreateWebhook", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
+       ctx.Redirect(orCtx.Link)
+}
+
+// TelegramHooksNewPost response for creating telegram hook
+func TelegramHooksNewPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.NewTelegramHookForm)
+       ctx.Data["Title"] = ctx.Tr("repo.settings")
+       ctx.Data["PageIsSettingsHooks"] = true
+       ctx.Data["PageIsSettingsHooksNew"] = true
+       ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
+       ctx.Data["HookType"] = models.TELEGRAM
+
+       orCtx, err := getOrgRepoCtx(ctx)
+       if err != nil {
+               ctx.ServerError("getOrgRepoCtx", err)
+               return
+       }
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+               return
+       }
+
+       json := jsoniter.ConfigCompatibleWithStandardLibrary
+       meta, err := json.Marshal(&webhook.TelegramMeta{
+               BotToken: form.BotToken,
+               ChatID:   form.ChatID,
+       })
+       if err != nil {
+               ctx.ServerError("Marshal", err)
+               return
+       }
+
+       w := &models.Webhook{
+               RepoID:          orCtx.RepoID,
+               URL:             fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s", form.BotToken, form.ChatID),
+               ContentType:     models.ContentTypeJSON,
+               HookEvent:       ParseHookEvent(form.WebhookForm),
+               IsActive:        form.Active,
+               Type:            models.TELEGRAM,
+               Meta:            string(meta),
+               OrgID:           orCtx.OrgID,
+               IsSystemWebhook: orCtx.IsSystemWebhook,
+       }
+       if err := w.UpdateEvent(); err != nil {
+               ctx.ServerError("UpdateEvent", err)
+               return
+       } else if err := models.CreateWebhook(w); err != nil {
+               ctx.ServerError("CreateWebhook", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
+       ctx.Redirect(orCtx.Link)
+}
+
+// MatrixHooksNewPost response for creating a Matrix hook
+func MatrixHooksNewPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.NewMatrixHookForm)
+       ctx.Data["Title"] = ctx.Tr("repo.settings")
+       ctx.Data["PageIsSettingsHooks"] = true
+       ctx.Data["PageIsSettingsHooksNew"] = true
+       ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
+       ctx.Data["HookType"] = models.MATRIX
+
+       orCtx, err := getOrgRepoCtx(ctx)
+       if err != nil {
+               ctx.ServerError("getOrgRepoCtx", err)
+               return
+       }
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+               return
+       }
+
+       json := jsoniter.ConfigCompatibleWithStandardLibrary
+       meta, err := json.Marshal(&webhook.MatrixMeta{
+               HomeserverURL: form.HomeserverURL,
+               Room:          form.RoomID,
+               AccessToken:   form.AccessToken,
+               MessageType:   form.MessageType,
+       })
+       if err != nil {
+               ctx.ServerError("Marshal", err)
+               return
+       }
+
+       w := &models.Webhook{
+               RepoID:          orCtx.RepoID,
+               URL:             fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, form.RoomID),
+               ContentType:     models.ContentTypeJSON,
+               HTTPMethod:      "PUT",
+               HookEvent:       ParseHookEvent(form.WebhookForm),
+               IsActive:        form.Active,
+               Type:            models.MATRIX,
+               Meta:            string(meta),
+               OrgID:           orCtx.OrgID,
+               IsSystemWebhook: orCtx.IsSystemWebhook,
+       }
+       if err := w.UpdateEvent(); err != nil {
+               ctx.ServerError("UpdateEvent", err)
+               return
+       } else if err := models.CreateWebhook(w); err != nil {
+               ctx.ServerError("CreateWebhook", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
+       ctx.Redirect(orCtx.Link)
+}
+
+// MSTeamsHooksNewPost response for creating MS Teams hook
+func MSTeamsHooksNewPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.NewMSTeamsHookForm)
+       ctx.Data["Title"] = ctx.Tr("repo.settings")
+       ctx.Data["PageIsSettingsHooks"] = true
+       ctx.Data["PageIsSettingsHooksNew"] = true
+       ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
+       ctx.Data["HookType"] = models.MSTEAMS
+
+       orCtx, err := getOrgRepoCtx(ctx)
+       if err != nil {
+               ctx.ServerError("getOrgRepoCtx", err)
+               return
+       }
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+               return
+       }
+
+       w := &models.Webhook{
+               RepoID:          orCtx.RepoID,
+               URL:             form.PayloadURL,
+               ContentType:     models.ContentTypeJSON,
+               HookEvent:       ParseHookEvent(form.WebhookForm),
+               IsActive:        form.Active,
+               Type:            models.MSTEAMS,
+               Meta:            "",
+               OrgID:           orCtx.OrgID,
+               IsSystemWebhook: orCtx.IsSystemWebhook,
+       }
+       if err := w.UpdateEvent(); err != nil {
+               ctx.ServerError("UpdateEvent", err)
+               return
+       } else if err := models.CreateWebhook(w); err != nil {
+               ctx.ServerError("CreateWebhook", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
+       ctx.Redirect(orCtx.Link)
+}
+
+// SlackHooksNewPost response for creating slack hook
+func SlackHooksNewPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.NewSlackHookForm)
+       ctx.Data["Title"] = ctx.Tr("repo.settings")
+       ctx.Data["PageIsSettingsHooks"] = true
+       ctx.Data["PageIsSettingsHooksNew"] = true
+       ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
+       ctx.Data["HookType"] = models.SLACK
+
+       orCtx, err := getOrgRepoCtx(ctx)
+       if err != nil {
+               ctx.ServerError("getOrgRepoCtx", err)
+               return
+       }
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+               return
+       }
+
+       if form.HasInvalidChannel() {
+               ctx.Flash.Error(ctx.Tr("repo.settings.add_webhook.invalid_channel_name"))
+               ctx.Redirect(orCtx.LinkNew + "/slack/new")
+               return
+       }
+
+       json := jsoniter.ConfigCompatibleWithStandardLibrary
+       meta, err := json.Marshal(&webhook.SlackMeta{
+               Channel:  strings.TrimSpace(form.Channel),
+               Username: form.Username,
+               IconURL:  form.IconURL,
+               Color:    form.Color,
+       })
+       if err != nil {
+               ctx.ServerError("Marshal", err)
+               return
+       }
+
+       w := &models.Webhook{
+               RepoID:          orCtx.RepoID,
+               URL:             form.PayloadURL,
+               ContentType:     models.ContentTypeJSON,
+               HookEvent:       ParseHookEvent(form.WebhookForm),
+               IsActive:        form.Active,
+               Type:            models.SLACK,
+               Meta:            string(meta),
+               OrgID:           orCtx.OrgID,
+               IsSystemWebhook: orCtx.IsSystemWebhook,
+       }
+       if err := w.UpdateEvent(); err != nil {
+               ctx.ServerError("UpdateEvent", err)
+               return
+       } else if err := models.CreateWebhook(w); err != nil {
+               ctx.ServerError("CreateWebhook", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
+       ctx.Redirect(orCtx.Link)
+}
+
+// FeishuHooksNewPost response for creating feishu hook
+func FeishuHooksNewPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.NewFeishuHookForm)
+       ctx.Data["Title"] = ctx.Tr("repo.settings")
+       ctx.Data["PageIsSettingsHooks"] = true
+       ctx.Data["PageIsSettingsHooksNew"] = true
+       ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
+       ctx.Data["HookType"] = models.FEISHU
+
+       orCtx, err := getOrgRepoCtx(ctx)
+       if err != nil {
+               ctx.ServerError("getOrgRepoCtx", err)
+               return
+       }
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+               return
+       }
+
+       w := &models.Webhook{
+               RepoID:          orCtx.RepoID,
+               URL:             form.PayloadURL,
+               ContentType:     models.ContentTypeJSON,
+               HookEvent:       ParseHookEvent(form.WebhookForm),
+               IsActive:        form.Active,
+               Type:            models.FEISHU,
+               Meta:            "",
+               OrgID:           orCtx.OrgID,
+               IsSystemWebhook: orCtx.IsSystemWebhook,
+       }
+       if err := w.UpdateEvent(); err != nil {
+               ctx.ServerError("UpdateEvent", err)
+               return
+       } else if err := models.CreateWebhook(w); err != nil {
+               ctx.ServerError("CreateWebhook", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
+       ctx.Redirect(orCtx.Link)
+}
+
+func checkWebhook(ctx *context.Context) (*orgRepoCtx, *models.Webhook) {
+       ctx.Data["RequireHighlightJS"] = true
+
+       orCtx, err := getOrgRepoCtx(ctx)
+       if err != nil {
+               ctx.ServerError("getOrgRepoCtx", err)
+               return nil, nil
+       }
+       ctx.Data["BaseLink"] = orCtx.Link
+
+       var w *models.Webhook
+       if orCtx.RepoID > 0 {
+               w, err = models.GetWebhookByRepoID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
+       } else if orCtx.OrgID > 0 {
+               w, err = models.GetWebhookByOrgID(ctx.Org.Organization.ID, ctx.ParamsInt64(":id"))
+       } else if orCtx.IsAdmin {
+               w, err = models.GetSystemOrDefaultWebhook(ctx.ParamsInt64(":id"))
+       }
+       if err != nil || w == nil {
+               if models.IsErrWebhookNotExist(err) {
+                       ctx.NotFound("GetWebhookByID", nil)
+               } else {
+                       ctx.ServerError("GetWebhookByID", err)
+               }
+               return nil, nil
+       }
+
+       ctx.Data["HookType"] = w.Type
+       switch w.Type {
+       case models.SLACK:
+               ctx.Data["SlackHook"] = webhook.GetSlackHook(w)
+       case models.DISCORD:
+               ctx.Data["DiscordHook"] = webhook.GetDiscordHook(w)
+       case models.TELEGRAM:
+               ctx.Data["TelegramHook"] = webhook.GetTelegramHook(w)
+       case models.MATRIX:
+               ctx.Data["MatrixHook"] = webhook.GetMatrixHook(w)
+       }
+
+       ctx.Data["History"], err = w.History(1)
+       if err != nil {
+               ctx.ServerError("History", err)
+       }
+       return orCtx, w
+}
+
+// WebHooksEdit render editing web hook page
+func WebHooksEdit(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.settings.update_webhook")
+       ctx.Data["PageIsSettingsHooks"] = true
+       ctx.Data["PageIsSettingsHooksEdit"] = true
+
+       orCtx, w := checkWebhook(ctx)
+       if ctx.Written() {
+               return
+       }
+       ctx.Data["Webhook"] = w
+
+       ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+}
+
+// WebHooksEditPost response for editing web hook
+func WebHooksEditPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.NewWebhookForm)
+       ctx.Data["Title"] = ctx.Tr("repo.settings.update_webhook")
+       ctx.Data["PageIsSettingsHooks"] = true
+       ctx.Data["PageIsSettingsHooksEdit"] = true
+
+       orCtx, w := checkWebhook(ctx)
+       if ctx.Written() {
+               return
+       }
+       ctx.Data["Webhook"] = w
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+               return
+       }
+
+       contentType := models.ContentTypeJSON
+       if models.HookContentType(form.ContentType) == models.ContentTypeForm {
+               contentType = models.ContentTypeForm
+       }
+
+       w.URL = form.PayloadURL
+       w.ContentType = contentType
+       w.Secret = form.Secret
+       w.HookEvent = ParseHookEvent(form.WebhookForm)
+       w.IsActive = form.Active
+       w.HTTPMethod = form.HTTPMethod
+       if err := w.UpdateEvent(); err != nil {
+               ctx.ServerError("UpdateEvent", err)
+               return
+       } else if err := models.UpdateWebhook(w); err != nil {
+               ctx.ServerError("WebHooksEditPost", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
+       ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+}
+
+// GogsHooksEditPost response for editing gogs hook
+func GogsHooksEditPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.NewGogshookForm)
+       ctx.Data["Title"] = ctx.Tr("repo.settings.update_webhook")
+       ctx.Data["PageIsSettingsHooks"] = true
+       ctx.Data["PageIsSettingsHooksEdit"] = true
+
+       orCtx, w := checkWebhook(ctx)
+       if ctx.Written() {
+               return
+       }
+       ctx.Data["Webhook"] = w
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+               return
+       }
+
+       contentType := models.ContentTypeJSON
+       if models.HookContentType(form.ContentType) == models.ContentTypeForm {
+               contentType = models.ContentTypeForm
+       }
+
+       w.URL = form.PayloadURL
+       w.ContentType = contentType
+       w.Secret = form.Secret
+       w.HookEvent = ParseHookEvent(form.WebhookForm)
+       w.IsActive = form.Active
+       if err := w.UpdateEvent(); err != nil {
+               ctx.ServerError("UpdateEvent", err)
+               return
+       } else if err := models.UpdateWebhook(w); err != nil {
+               ctx.ServerError("GogsHooksEditPost", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
+       ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+}
+
+// SlackHooksEditPost response for editing slack hook
+func SlackHooksEditPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.NewSlackHookForm)
+       ctx.Data["Title"] = ctx.Tr("repo.settings")
+       ctx.Data["PageIsSettingsHooks"] = true
+       ctx.Data["PageIsSettingsHooksEdit"] = true
+
+       orCtx, w := checkWebhook(ctx)
+       if ctx.Written() {
+               return
+       }
+       ctx.Data["Webhook"] = w
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+               return
+       }
+
+       if form.HasInvalidChannel() {
+               ctx.Flash.Error(ctx.Tr("repo.settings.add_webhook.invalid_channel_name"))
+               ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+               return
+       }
+
+       json := jsoniter.ConfigCompatibleWithStandardLibrary
+       meta, err := json.Marshal(&webhook.SlackMeta{
+               Channel:  strings.TrimSpace(form.Channel),
+               Username: form.Username,
+               IconURL:  form.IconURL,
+               Color:    form.Color,
+       })
+       if err != nil {
+               ctx.ServerError("Marshal", err)
+               return
+       }
+
+       w.URL = form.PayloadURL
+       w.Meta = string(meta)
+       w.HookEvent = ParseHookEvent(form.WebhookForm)
+       w.IsActive = form.Active
+       if err := w.UpdateEvent(); err != nil {
+               ctx.ServerError("UpdateEvent", err)
+               return
+       } else if err := models.UpdateWebhook(w); err != nil {
+               ctx.ServerError("UpdateWebhook", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
+       ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+}
+
+// DiscordHooksEditPost response for editing discord hook
+func DiscordHooksEditPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.NewDiscordHookForm)
+       ctx.Data["Title"] = ctx.Tr("repo.settings")
+       ctx.Data["PageIsSettingsHooks"] = true
+       ctx.Data["PageIsSettingsHooksEdit"] = true
+
+       orCtx, w := checkWebhook(ctx)
+       if ctx.Written() {
+               return
+       }
+       ctx.Data["Webhook"] = w
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+               return
+       }
+
+       json := jsoniter.ConfigCompatibleWithStandardLibrary
+       meta, err := json.Marshal(&webhook.DiscordMeta{
+               Username: form.Username,
+               IconURL:  form.IconURL,
+       })
+       if err != nil {
+               ctx.ServerError("Marshal", err)
+               return
+       }
+
+       w.URL = form.PayloadURL
+       w.Meta = string(meta)
+       w.HookEvent = ParseHookEvent(form.WebhookForm)
+       w.IsActive = form.Active
+       if err := w.UpdateEvent(); err != nil {
+               ctx.ServerError("UpdateEvent", err)
+               return
+       } else if err := models.UpdateWebhook(w); err != nil {
+               ctx.ServerError("UpdateWebhook", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
+       ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+}
+
+// DingtalkHooksEditPost response for editing discord hook
+func DingtalkHooksEditPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.NewDingtalkHookForm)
+       ctx.Data["Title"] = ctx.Tr("repo.settings")
+       ctx.Data["PageIsSettingsHooks"] = true
+       ctx.Data["PageIsSettingsHooksEdit"] = true
+
+       orCtx, w := checkWebhook(ctx)
+       if ctx.Written() {
+               return
+       }
+       ctx.Data["Webhook"] = w
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+               return
+       }
+
+       w.URL = form.PayloadURL
+       w.HookEvent = ParseHookEvent(form.WebhookForm)
+       w.IsActive = form.Active
+       if err := w.UpdateEvent(); err != nil {
+               ctx.ServerError("UpdateEvent", err)
+               return
+       } else if err := models.UpdateWebhook(w); err != nil {
+               ctx.ServerError("UpdateWebhook", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
+       ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+}
+
+// TelegramHooksEditPost response for editing discord hook
+func TelegramHooksEditPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.NewTelegramHookForm)
+       ctx.Data["Title"] = ctx.Tr("repo.settings")
+       ctx.Data["PageIsSettingsHooks"] = true
+       ctx.Data["PageIsSettingsHooksEdit"] = true
+
+       orCtx, w := checkWebhook(ctx)
+       if ctx.Written() {
+               return
+       }
+       ctx.Data["Webhook"] = w
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+               return
+       }
+       json := jsoniter.ConfigCompatibleWithStandardLibrary
+       meta, err := json.Marshal(&webhook.TelegramMeta{
+               BotToken: form.BotToken,
+               ChatID:   form.ChatID,
+       })
+       if err != nil {
+               ctx.ServerError("Marshal", err)
+               return
+       }
+       w.Meta = string(meta)
+       w.URL = fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s", form.BotToken, form.ChatID)
+       w.HookEvent = ParseHookEvent(form.WebhookForm)
+       w.IsActive = form.Active
+       if err := w.UpdateEvent(); err != nil {
+               ctx.ServerError("UpdateEvent", err)
+               return
+       } else if err := models.UpdateWebhook(w); err != nil {
+               ctx.ServerError("UpdateWebhook", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
+       ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+}
+
+// MatrixHooksEditPost response for editing a Matrix hook
+func MatrixHooksEditPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.NewMatrixHookForm)
+       ctx.Data["Title"] = ctx.Tr("repo.settings")
+       ctx.Data["PageIsSettingsHooks"] = true
+       ctx.Data["PageIsSettingsHooksEdit"] = true
+
+       orCtx, w := checkWebhook(ctx)
+       if ctx.Written() {
+               return
+       }
+       ctx.Data["Webhook"] = w
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+               return
+       }
+       json := jsoniter.ConfigCompatibleWithStandardLibrary
+       meta, err := json.Marshal(&webhook.MatrixMeta{
+               HomeserverURL: form.HomeserverURL,
+               Room:          form.RoomID,
+               AccessToken:   form.AccessToken,
+               MessageType:   form.MessageType,
+       })
+       if err != nil {
+               ctx.ServerError("Marshal", err)
+               return
+       }
+       w.Meta = string(meta)
+       w.URL = fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, form.RoomID)
+
+       w.HookEvent = ParseHookEvent(form.WebhookForm)
+       w.IsActive = form.Active
+       if err := w.UpdateEvent(); err != nil {
+               ctx.ServerError("UpdateEvent", err)
+               return
+       } else if err := models.UpdateWebhook(w); err != nil {
+               ctx.ServerError("UpdateWebhook", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
+       ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+}
+
+// MSTeamsHooksEditPost response for editing MS Teams hook
+func MSTeamsHooksEditPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.NewMSTeamsHookForm)
+       ctx.Data["Title"] = ctx.Tr("repo.settings")
+       ctx.Data["PageIsSettingsHooks"] = true
+       ctx.Data["PageIsSettingsHooksEdit"] = true
+
+       orCtx, w := checkWebhook(ctx)
+       if ctx.Written() {
+               return
+       }
+       ctx.Data["Webhook"] = w
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+               return
+       }
+
+       w.URL = form.PayloadURL
+       w.HookEvent = ParseHookEvent(form.WebhookForm)
+       w.IsActive = form.Active
+       if err := w.UpdateEvent(); err != nil {
+               ctx.ServerError("UpdateEvent", err)
+               return
+       } else if err := models.UpdateWebhook(w); err != nil {
+               ctx.ServerError("UpdateWebhook", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
+       ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+}
+
+// FeishuHooksEditPost response for editing feishu hook
+func FeishuHooksEditPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.NewFeishuHookForm)
+       ctx.Data["Title"] = ctx.Tr("repo.settings")
+       ctx.Data["PageIsSettingsHooks"] = true
+       ctx.Data["PageIsSettingsHooksEdit"] = true
+
+       orCtx, w := checkWebhook(ctx)
+       if ctx.Written() {
+               return
+       }
+       ctx.Data["Webhook"] = w
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+               return
+       }
+
+       w.URL = form.PayloadURL
+       w.HookEvent = ParseHookEvent(form.WebhookForm)
+       w.IsActive = form.Active
+       if err := w.UpdateEvent(); err != nil {
+               ctx.ServerError("UpdateEvent", err)
+               return
+       } else if err := models.UpdateWebhook(w); err != nil {
+               ctx.ServerError("UpdateWebhook", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
+       ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+}
+
+// TestWebhook test if web hook is work fine
+func TestWebhook(ctx *context.Context) {
+       hookID := ctx.ParamsInt64(":id")
+       w, err := models.GetWebhookByRepoID(ctx.Repo.Repository.ID, hookID)
+       if err != nil {
+               ctx.Flash.Error("GetWebhookByID: " + err.Error())
+               ctx.Status(500)
+               return
+       }
+
+       // Grab latest commit or fake one if it's empty repository.
+       commit := ctx.Repo.Commit
+       if commit == nil {
+               ghost := models.NewGhostUser()
+               commit = &git.Commit{
+                       ID:            git.MustIDFromString(git.EmptySHA),
+                       Author:        ghost.NewGitSig(),
+                       Committer:     ghost.NewGitSig(),
+                       CommitMessage: "This is a fake commit",
+               }
+       }
+
+       apiUser := convert.ToUserWithAccessMode(ctx.User, models.AccessModeNone)
+       p := &api.PushPayload{
+               Ref:    git.BranchPrefix + ctx.Repo.Repository.DefaultBranch,
+               Before: commit.ID.String(),
+               After:  commit.ID.String(),
+               Commits: []*api.PayloadCommit{
+                       {
+                               ID:      commit.ID.String(),
+                               Message: commit.Message(),
+                               URL:     ctx.Repo.Repository.HTMLURL() + "/commit/" + commit.ID.String(),
+                               Author: &api.PayloadUser{
+                                       Name:  commit.Author.Name,
+                                       Email: commit.Author.Email,
+                               },
+                               Committer: &api.PayloadUser{
+                                       Name:  commit.Committer.Name,
+                                       Email: commit.Committer.Email,
+                               },
+                       },
+               },
+               Repo:   convert.ToRepo(ctx.Repo.Repository, models.AccessModeNone),
+               Pusher: apiUser,
+               Sender: apiUser,
+       }
+       if err := webhook.PrepareWebhook(w, ctx.Repo.Repository, models.HookEventPush, p); err != nil {
+               ctx.Flash.Error("PrepareWebhook: " + err.Error())
+               ctx.Status(500)
+       } else {
+               ctx.Flash.Info(ctx.Tr("repo.settings.webhook.test_delivery_success"))
+               ctx.Status(200)
+       }
+}
+
+// DeleteWebhook delete a webhook
+func DeleteWebhook(ctx *context.Context) {
+       if err := models.DeleteWebhookByRepoID(ctx.Repo.Repository.ID, ctx.QueryInt64("id")); err != nil {
+               ctx.Flash.Error("DeleteWebhookByRepoID: " + err.Error())
+       } else {
+               ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": ctx.Repo.RepoLink + "/settings/hooks",
+       })
+}
diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go
new file mode 100644 (file)
index 0000000..cceb845
--- /dev/null
@@ -0,0 +1,684 @@
+// Copyright 2015 The Gogs Authors. All rights reserved.
+// Copyright 2018 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 repo
+
+import (
+       "bytes"
+       "fmt"
+       "io/ioutil"
+       "net/http"
+       "net/url"
+       "path/filepath"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/markup"
+       "code.gitea.io/gitea/modules/markup/markdown"
+       "code.gitea.io/gitea/modules/timeutil"
+       "code.gitea.io/gitea/modules/util"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/routers/common"
+       "code.gitea.io/gitea/services/forms"
+       wiki_service "code.gitea.io/gitea/services/wiki"
+)
+
+const (
+       tplWikiStart    base.TplName = "repo/wiki/start"
+       tplWikiView     base.TplName = "repo/wiki/view"
+       tplWikiRevision base.TplName = "repo/wiki/revision"
+       tplWikiNew      base.TplName = "repo/wiki/new"
+       tplWikiPages    base.TplName = "repo/wiki/pages"
+)
+
+// MustEnableWiki check if wiki is enabled, if external then redirect
+func MustEnableWiki(ctx *context.Context) {
+       if !ctx.Repo.CanRead(models.UnitTypeWiki) &&
+               !ctx.Repo.CanRead(models.UnitTypeExternalWiki) {
+               if log.IsTrace() {
+                       log.Trace("Permission Denied: User %-v cannot read %-v or %-v of repo %-v\n"+
+                               "User in repo has Permissions: %-+v",
+                               ctx.User,
+                               models.UnitTypeWiki,
+                               models.UnitTypeExternalWiki,
+                               ctx.Repo.Repository,
+                               ctx.Repo.Permission)
+               }
+               ctx.NotFound("MustEnableWiki", nil)
+               return
+       }
+
+       unit, err := ctx.Repo.Repository.GetUnit(models.UnitTypeExternalWiki)
+       if err == nil {
+               ctx.Redirect(unit.ExternalWikiConfig().ExternalWikiURL)
+               return
+       }
+}
+
+// PageMeta wiki page meta information
+type PageMeta struct {
+       Name        string
+       SubURL      string
+       UpdatedUnix timeutil.TimeStamp
+}
+
+// findEntryForFile finds the tree entry for a target filepath.
+func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) {
+       entry, err := commit.GetTreeEntryByPath(target)
+       if err != nil && !git.IsErrNotExist(err) {
+               return nil, err
+       }
+       if entry != nil {
+               return entry, nil
+       }
+
+       // Then the unescaped, shortest alternative
+       var unescapedTarget string
+       if unescapedTarget, err = url.QueryUnescape(target); err != nil {
+               return nil, err
+       }
+       return commit.GetTreeEntryByPath(unescapedTarget)
+}
+
+func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, error) {
+       wikiRepo, err := git.OpenRepository(ctx.Repo.Repository.WikiPath())
+       if err != nil {
+               ctx.ServerError("OpenRepository", err)
+               return nil, nil, err
+       }
+
+       commit, err := wikiRepo.GetBranchCommit("master")
+       if err != nil {
+               return wikiRepo, nil, err
+       }
+       return wikiRepo, commit, nil
+}
+
+// wikiContentsByEntry returns the contents of the wiki page referenced by the
+// given tree entry. Writes to ctx if an error occurs.
+func wikiContentsByEntry(ctx *context.Context, entry *git.TreeEntry) []byte {
+       reader, err := entry.Blob().DataAsync()
+       if err != nil {
+               ctx.ServerError("Blob.Data", err)
+               return nil
+       }
+       defer reader.Close()
+       content, err := ioutil.ReadAll(reader)
+       if err != nil {
+               ctx.ServerError("ReadAll", err)
+               return nil
+       }
+       return content
+}
+
+// wikiContentsByName returns the contents of a wiki page, along with a boolean
+// indicating whether the page exists. Writes to ctx if an error occurs.
+func wikiContentsByName(ctx *context.Context, commit *git.Commit, wikiName string) ([]byte, *git.TreeEntry, string, bool) {
+       pageFilename := wiki_service.NameToFilename(wikiName)
+       entry, err := findEntryForFile(commit, pageFilename)
+       if err != nil && !git.IsErrNotExist(err) {
+               ctx.ServerError("findEntryForFile", err)
+               return nil, nil, "", false
+       } else if entry == nil {
+               return nil, nil, "", true
+       }
+       return wikiContentsByEntry(ctx, entry), entry, pageFilename, false
+}
+
+func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
+       wikiRepo, commit, err := findWikiRepoCommit(ctx)
+       if err != nil {
+               if !git.IsErrNotExist(err) {
+                       ctx.ServerError("GetBranchCommit", err)
+               }
+               return nil, nil
+       }
+
+       // Get page list.
+       entries, err := commit.ListEntries()
+       if err != nil {
+               if wikiRepo != nil {
+                       wikiRepo.Close()
+               }
+               ctx.ServerError("ListEntries", err)
+               return nil, nil
+       }
+       pages := make([]PageMeta, 0, len(entries))
+       for _, entry := range entries {
+               if !entry.IsRegular() {
+                       continue
+               }
+               wikiName, err := wiki_service.FilenameToName(entry.Name())
+               if err != nil {
+                       if models.IsErrWikiInvalidFileName(err) {
+                               continue
+                       }
+                       if wikiRepo != nil {
+                               wikiRepo.Close()
+                       }
+                       ctx.ServerError("WikiFilenameToName", err)
+                       return nil, nil
+               } else if wikiName == "_Sidebar" || wikiName == "_Footer" {
+                       continue
+               }
+               pages = append(pages, PageMeta{
+                       Name:   wikiName,
+                       SubURL: wiki_service.NameToSubURL(wikiName),
+               })
+       }
+       ctx.Data["Pages"] = pages
+
+       // get requested pagename
+       pageName := wiki_service.NormalizeWikiName(ctx.Params(":page"))
+       if len(pageName) == 0 {
+               pageName = "Home"
+       }
+       ctx.Data["PageURL"] = wiki_service.NameToSubURL(pageName)
+       ctx.Data["old_title"] = pageName
+       ctx.Data["Title"] = pageName
+       ctx.Data["title"] = pageName
+       ctx.Data["RequireHighlightJS"] = true
+
+       //lookup filename in wiki - get filecontent, gitTree entry , real filename
+       data, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName)
+       if noEntry {
+               ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages")
+       }
+       if entry == nil || ctx.Written() {
+               if wikiRepo != nil {
+                       wikiRepo.Close()
+               }
+               return nil, nil
+       }
+
+       sidebarContent, _, _, _ := wikiContentsByName(ctx, commit, "_Sidebar")
+       if ctx.Written() {
+               if wikiRepo != nil {
+                       wikiRepo.Close()
+               }
+               return nil, nil
+       }
+
+       footerContent, _, _, _ := wikiContentsByName(ctx, commit, "_Footer")
+       if ctx.Written() {
+               if wikiRepo != nil {
+                       wikiRepo.Close()
+               }
+               return nil, nil
+       }
+
+       var rctx = &markup.RenderContext{
+               URLPrefix: ctx.Repo.RepoLink,
+               Metas:     ctx.Repo.Repository.ComposeDocumentMetas(),
+               IsWiki:    true,
+       }
+
+       var buf strings.Builder
+       if err := markdown.Render(rctx, bytes.NewReader(data), &buf); err != nil {
+               ctx.ServerError("Render", err)
+               return nil, nil
+       }
+       ctx.Data["content"] = buf.String()
+
+       buf.Reset()
+       if err := markdown.Render(rctx, bytes.NewReader(sidebarContent), &buf); err != nil {
+               ctx.ServerError("Render", err)
+               return nil, nil
+       }
+       ctx.Data["sidebarPresent"] = sidebarContent != nil
+       ctx.Data["sidebarContent"] = buf.String()
+
+       buf.Reset()
+       if err := markdown.Render(rctx, bytes.NewReader(footerContent), &buf); err != nil {
+               ctx.ServerError("Render", err)
+               return nil, nil
+       }
+       ctx.Data["footerPresent"] = footerContent != nil
+       ctx.Data["footerContent"] = buf.String()
+
+       // get commit count - wiki revisions
+       commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
+       ctx.Data["CommitCount"] = commitsCount
+
+       return wikiRepo, entry
+}
+
+func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
+       wikiRepo, commit, err := findWikiRepoCommit(ctx)
+       if err != nil {
+               if wikiRepo != nil {
+                       wikiRepo.Close()
+               }
+               if !git.IsErrNotExist(err) {
+                       ctx.ServerError("GetBranchCommit", err)
+               }
+               return nil, nil
+       }
+
+       // get requested pagename
+       pageName := wiki_service.NormalizeWikiName(ctx.Params(":page"))
+       if len(pageName) == 0 {
+               pageName = "Home"
+       }
+       ctx.Data["PageURL"] = wiki_service.NameToSubURL(pageName)
+       ctx.Data["old_title"] = pageName
+       ctx.Data["Title"] = pageName
+       ctx.Data["title"] = pageName
+       ctx.Data["RequireHighlightJS"] = true
+       ctx.Data["Username"] = ctx.Repo.Owner.Name
+       ctx.Data["Reponame"] = ctx.Repo.Repository.Name
+
+       //lookup filename in wiki - get filecontent, gitTree entry , real filename
+       data, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName)
+       if noEntry {
+               ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages")
+       }
+       if entry == nil || ctx.Written() {
+               if wikiRepo != nil {
+                       wikiRepo.Close()
+               }
+               return nil, nil
+       }
+
+       ctx.Data["content"] = string(data)
+       ctx.Data["sidebarPresent"] = false
+       ctx.Data["sidebarContent"] = ""
+       ctx.Data["footerPresent"] = false
+       ctx.Data["footerContent"] = ""
+
+       // get commit count - wiki revisions
+       commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
+       ctx.Data["CommitCount"] = commitsCount
+
+       // get page
+       page := ctx.QueryInt("page")
+       if page <= 1 {
+               page = 1
+       }
+
+       // get Commit Count
+       commitsHistory, err := wikiRepo.CommitsByFileAndRangeNoFollow("master", pageFilename, page)
+       if err != nil {
+               if wikiRepo != nil {
+                       wikiRepo.Close()
+               }
+               ctx.ServerError("CommitsByFileAndRangeNoFollow", err)
+               return nil, nil
+       }
+       commitsHistory = models.ValidateCommitsWithEmails(commitsHistory)
+       commitsHistory = models.ParseCommitsWithSignature(commitsHistory, ctx.Repo.Repository)
+
+       ctx.Data["Commits"] = commitsHistory
+
+       pager := context.NewPagination(int(commitsCount), git.CommitsRangeSize, page, 5)
+       pager.SetDefaultParams(ctx)
+       ctx.Data["Page"] = pager
+
+       return wikiRepo, entry
+}
+
+func renderEditPage(ctx *context.Context) {
+       wikiRepo, commit, err := findWikiRepoCommit(ctx)
+       if err != nil {
+               if wikiRepo != nil {
+                       wikiRepo.Close()
+               }
+               if !git.IsErrNotExist(err) {
+                       ctx.ServerError("GetBranchCommit", err)
+               }
+               return
+       }
+       defer func() {
+               if wikiRepo != nil {
+                       wikiRepo.Close()
+               }
+       }()
+
+       // get requested pagename
+       pageName := wiki_service.NormalizeWikiName(ctx.Params(":page"))
+       if len(pageName) == 0 {
+               pageName = "Home"
+       }
+       ctx.Data["PageURL"] = wiki_service.NameToSubURL(pageName)
+       ctx.Data["old_title"] = pageName
+       ctx.Data["Title"] = pageName
+       ctx.Data["title"] = pageName
+       ctx.Data["RequireHighlightJS"] = true
+
+       //lookup filename in wiki - get filecontent, gitTree entry , real filename
+       data, entry, _, noEntry := wikiContentsByName(ctx, commit, pageName)
+       if noEntry {
+               ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages")
+       }
+       if entry == nil || ctx.Written() {
+               return
+       }
+
+       ctx.Data["content"] = string(data)
+       ctx.Data["sidebarPresent"] = false
+       ctx.Data["sidebarContent"] = ""
+       ctx.Data["footerPresent"] = false
+       ctx.Data["footerContent"] = ""
+}
+
+// Wiki renders single wiki page
+func Wiki(ctx *context.Context) {
+       ctx.Data["PageIsWiki"] = true
+       ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(models.UnitTypeWiki) && !ctx.Repo.Repository.IsArchived
+
+       if !ctx.Repo.Repository.HasWiki() {
+               ctx.Data["Title"] = ctx.Tr("repo.wiki")
+               ctx.HTML(http.StatusOK, tplWikiStart)
+               return
+       }
+
+       wikiRepo, entry := renderViewPage(ctx)
+       if ctx.Written() {
+               if wikiRepo != nil {
+                       wikiRepo.Close()
+               }
+               return
+       }
+       defer func() {
+               if wikiRepo != nil {
+                       wikiRepo.Close()
+               }
+       }()
+       if entry == nil {
+               ctx.Data["Title"] = ctx.Tr("repo.wiki")
+               ctx.HTML(http.StatusOK, tplWikiStart)
+               return
+       }
+
+       wikiPath := entry.Name()
+       if markup.Type(wikiPath) != markdown.MarkupName {
+               ext := strings.ToUpper(filepath.Ext(wikiPath))
+               ctx.Data["FormatWarning"] = fmt.Sprintf("%s rendering is not supported at the moment. Rendered as Markdown.", ext)
+       }
+       // Get last change information.
+       lastCommit, err := wikiRepo.GetCommitByPath(wikiPath)
+       if err != nil {
+               ctx.ServerError("GetCommitByPath", err)
+               return
+       }
+       ctx.Data["Author"] = lastCommit.Author
+
+       ctx.HTML(http.StatusOK, tplWikiView)
+}
+
+// WikiRevision renders file revision list of wiki page
+func WikiRevision(ctx *context.Context) {
+       ctx.Data["PageIsWiki"] = true
+       ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(models.UnitTypeWiki) && !ctx.Repo.Repository.IsArchived
+
+       if !ctx.Repo.Repository.HasWiki() {
+               ctx.Data["Title"] = ctx.Tr("repo.wiki")
+               ctx.HTML(http.StatusOK, tplWikiStart)
+               return
+       }
+
+       wikiRepo, entry := renderRevisionPage(ctx)
+       if ctx.Written() {
+               if wikiRepo != nil {
+                       wikiRepo.Close()
+               }
+               return
+       }
+       defer func() {
+               if wikiRepo != nil {
+                       wikiRepo.Close()
+               }
+       }()
+       if entry == nil {
+               ctx.Data["Title"] = ctx.Tr("repo.wiki")
+               ctx.HTML(http.StatusOK, tplWikiStart)
+               return
+       }
+
+       // Get last change information.
+       wikiPath := entry.Name()
+       lastCommit, err := wikiRepo.GetCommitByPath(wikiPath)
+       if err != nil {
+               ctx.ServerError("GetCommitByPath", err)
+               return
+       }
+       ctx.Data["Author"] = lastCommit.Author
+
+       ctx.HTML(http.StatusOK, tplWikiRevision)
+}
+
+// WikiPages render wiki pages list page
+func WikiPages(ctx *context.Context) {
+       if !ctx.Repo.Repository.HasWiki() {
+               ctx.Redirect(ctx.Repo.RepoLink + "/wiki")
+               return
+       }
+
+       ctx.Data["Title"] = ctx.Tr("repo.wiki.pages")
+       ctx.Data["PageIsWiki"] = true
+       ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(models.UnitTypeWiki) && !ctx.Repo.Repository.IsArchived
+
+       wikiRepo, commit, err := findWikiRepoCommit(ctx)
+       if err != nil {
+               if wikiRepo != nil {
+                       wikiRepo.Close()
+               }
+               return
+       }
+
+       entries, err := commit.ListEntries()
+       if err != nil {
+               if wikiRepo != nil {
+                       wikiRepo.Close()
+               }
+
+               ctx.ServerError("ListEntries", err)
+               return
+       }
+       pages := make([]PageMeta, 0, len(entries))
+       for _, entry := range entries {
+               if !entry.IsRegular() {
+                       continue
+               }
+               c, err := wikiRepo.GetCommitByPath(entry.Name())
+               if err != nil {
+                       if wikiRepo != nil {
+                               wikiRepo.Close()
+                       }
+
+                       ctx.ServerError("GetCommit", err)
+                       return
+               }
+               wikiName, err := wiki_service.FilenameToName(entry.Name())
+               if err != nil {
+                       if models.IsErrWikiInvalidFileName(err) {
+                               continue
+                       }
+                       if wikiRepo != nil {
+                               wikiRepo.Close()
+                       }
+
+                       ctx.ServerError("WikiFilenameToName", err)
+                       return
+               }
+               pages = append(pages, PageMeta{
+                       Name:        wikiName,
+                       SubURL:      wiki_service.NameToSubURL(wikiName),
+                       UpdatedUnix: timeutil.TimeStamp(c.Author.When.Unix()),
+               })
+       }
+       ctx.Data["Pages"] = pages
+
+       defer func() {
+               if wikiRepo != nil {
+                       wikiRepo.Close()
+               }
+       }()
+       ctx.HTML(http.StatusOK, tplWikiPages)
+}
+
+// WikiRaw outputs raw blob requested by user (image for example)
+func WikiRaw(ctx *context.Context) {
+       wikiRepo, commit, err := findWikiRepoCommit(ctx)
+       if err != nil {
+               if wikiRepo != nil {
+                       return
+               }
+       }
+
+       providedPath := ctx.Params("*")
+
+       var entry *git.TreeEntry
+       if commit != nil {
+               // Try to find a file with that name
+               entry, err = findEntryForFile(commit, providedPath)
+               if err != nil && !git.IsErrNotExist(err) {
+                       ctx.ServerError("findFile", err)
+                       return
+               }
+
+               if entry == nil {
+                       // Try to find a wiki page with that name
+                       if strings.HasSuffix(providedPath, ".md") {
+                               providedPath = providedPath[:len(providedPath)-3]
+                       }
+
+                       wikiPath := wiki_service.NameToFilename(providedPath)
+                       entry, err = findEntryForFile(commit, wikiPath)
+                       if err != nil && !git.IsErrNotExist(err) {
+                               ctx.ServerError("findFile", err)
+                               return
+                       }
+               }
+       }
+
+       if entry != nil {
+               if err = common.ServeBlob(ctx, entry.Blob()); err != nil {
+                       ctx.ServerError("ServeBlob", err)
+               }
+               return
+       }
+
+       ctx.NotFound("findEntryForFile", nil)
+}
+
+// NewWiki render wiki create page
+func NewWiki(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page")
+       ctx.Data["PageIsWiki"] = true
+       ctx.Data["RequireSimpleMDE"] = true
+
+       if !ctx.Repo.Repository.HasWiki() {
+               ctx.Data["title"] = "Home"
+       }
+
+       ctx.HTML(http.StatusOK, tplWikiNew)
+}
+
+// NewWikiPost response for wiki create request
+func NewWikiPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.NewWikiForm)
+       ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page")
+       ctx.Data["PageIsWiki"] = true
+       ctx.Data["RequireSimpleMDE"] = true
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplWikiNew)
+               return
+       }
+
+       if util.IsEmptyString(form.Title) {
+               ctx.RenderWithErr(ctx.Tr("repo.issues.new.title_empty"), tplWikiNew, form)
+               return
+       }
+
+       wikiName := wiki_service.NormalizeWikiName(form.Title)
+
+       if len(form.Message) == 0 {
+               form.Message = ctx.Tr("repo.editor.add", form.Title)
+       }
+
+       if err := wiki_service.AddWikiPage(ctx.User, ctx.Repo.Repository, wikiName, form.Content, form.Message); err != nil {
+               if models.IsErrWikiReservedName(err) {
+                       ctx.Data["Err_Title"] = true
+                       ctx.RenderWithErr(ctx.Tr("repo.wiki.reserved_page", wikiName), tplWikiNew, &form)
+               } else if models.IsErrWikiAlreadyExist(err) {
+                       ctx.Data["Err_Title"] = true
+                       ctx.RenderWithErr(ctx.Tr("repo.wiki.page_already_exists"), tplWikiNew, &form)
+               } else {
+                       ctx.ServerError("AddWikiPage", err)
+               }
+               return
+       }
+
+       ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + wiki_service.NameToSubURL(wikiName))
+}
+
+// EditWiki render wiki modify page
+func EditWiki(ctx *context.Context) {
+       ctx.Data["PageIsWiki"] = true
+       ctx.Data["PageIsWikiEdit"] = true
+       ctx.Data["RequireSimpleMDE"] = true
+
+       if !ctx.Repo.Repository.HasWiki() {
+               ctx.Redirect(ctx.Repo.RepoLink + "/wiki")
+               return
+       }
+
+       renderEditPage(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       ctx.HTML(http.StatusOK, tplWikiNew)
+}
+
+// EditWikiPost response for wiki modify request
+func EditWikiPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.NewWikiForm)
+       ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page")
+       ctx.Data["PageIsWiki"] = true
+       ctx.Data["RequireSimpleMDE"] = true
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplWikiNew)
+               return
+       }
+
+       oldWikiName := wiki_service.NormalizeWikiName(ctx.Params(":page"))
+       newWikiName := wiki_service.NormalizeWikiName(form.Title)
+
+       if len(form.Message) == 0 {
+               form.Message = ctx.Tr("repo.editor.update", form.Title)
+       }
+
+       if err := wiki_service.EditWikiPage(ctx.User, ctx.Repo.Repository, oldWikiName, newWikiName, form.Content, form.Message); err != nil {
+               ctx.ServerError("EditWikiPage", err)
+               return
+       }
+
+       ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + wiki_service.NameToSubURL(newWikiName))
+}
+
+// DeleteWikiPagePost delete wiki page
+func DeleteWikiPagePost(ctx *context.Context) {
+       wikiName := wiki_service.NormalizeWikiName(ctx.Params(":page"))
+       if len(wikiName) == 0 {
+               wikiName = "Home"
+       }
+
+       if err := wiki_service.DeleteWikiPage(ctx.User, ctx.Repo.Repository, wikiName); err != nil {
+               ctx.ServerError("DeleteWikiPage", err)
+               return
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": ctx.Repo.RepoLink + "/wiki/",
+       })
+}
diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go
new file mode 100644 (file)
index 0000000..8934a66
--- /dev/null
@@ -0,0 +1,215 @@
+// 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 repo
+
+import (
+       "io/ioutil"
+       "net/http"
+       "testing"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/test"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+       wiki_service "code.gitea.io/gitea/services/wiki"
+
+       "github.com/stretchr/testify/assert"
+)
+
+const content = "Wiki contents for unit tests"
+const message = "Wiki commit message for unit tests"
+
+func wikiEntry(t *testing.T, repo *models.Repository, wikiName string) *git.TreeEntry {
+       wikiRepo, err := git.OpenRepository(repo.WikiPath())
+       assert.NoError(t, err)
+       defer wikiRepo.Close()
+       commit, err := wikiRepo.GetBranchCommit("master")
+       assert.NoError(t, err)
+       entries, err := commit.ListEntries()
+       assert.NoError(t, err)
+       for _, entry := range entries {
+               if entry.Name() == wiki_service.NameToFilename(wikiName) {
+                       return entry
+               }
+       }
+       return nil
+}
+
+func wikiContent(t *testing.T, repo *models.Repository, wikiName string) string {
+       entry := wikiEntry(t, repo, wikiName)
+       if !assert.NotNil(t, entry) {
+               return ""
+       }
+       reader, err := entry.Blob().DataAsync()
+       assert.NoError(t, err)
+       defer reader.Close()
+       bytes, err := ioutil.ReadAll(reader)
+       assert.NoError(t, err)
+       return string(bytes)
+}
+
+func assertWikiExists(t *testing.T, repo *models.Repository, wikiName string) {
+       assert.NotNil(t, wikiEntry(t, repo, wikiName))
+}
+
+func assertWikiNotExists(t *testing.T, repo *models.Repository, wikiName string) {
+       assert.Nil(t, wikiEntry(t, repo, wikiName))
+}
+
+func assertPagesMetas(t *testing.T, expectedNames []string, metas interface{}) {
+       pageMetas, ok := metas.([]PageMeta)
+       if !assert.True(t, ok) {
+               return
+       }
+       if !assert.Len(t, pageMetas, len(expectedNames)) {
+               return
+       }
+       for i, pageMeta := range pageMetas {
+               assert.EqualValues(t, expectedNames[i], pageMeta.Name)
+       }
+}
+
+func TestWiki(t *testing.T) {
+       models.PrepareTestEnv(t)
+
+       ctx := test.MockContext(t, "user2/repo1/wiki/_pages")
+       ctx.SetParams(":page", "Home")
+       test.LoadRepo(t, ctx, 1)
+       Wiki(ctx)
+       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+       assert.EqualValues(t, "Home", ctx.Data["Title"])
+       assertPagesMetas(t, []string{"Home", "Page With Image", "Page With Spaced Name"}, ctx.Data["Pages"])
+}
+
+func TestWikiPages(t *testing.T) {
+       models.PrepareTestEnv(t)
+
+       ctx := test.MockContext(t, "user2/repo1/wiki/_pages")
+       test.LoadRepo(t, ctx, 1)
+       WikiPages(ctx)
+       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+       assertPagesMetas(t, []string{"Home", "Page With Image", "Page With Spaced Name"}, ctx.Data["Pages"])
+}
+
+func TestNewWiki(t *testing.T) {
+       models.PrepareTestEnv(t)
+
+       ctx := test.MockContext(t, "user2/repo1/wiki/_new")
+       test.LoadUser(t, ctx, 2)
+       test.LoadRepo(t, ctx, 1)
+       NewWiki(ctx)
+       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+       assert.EqualValues(t, ctx.Tr("repo.wiki.new_page"), ctx.Data["Title"])
+}
+
+func TestNewWikiPost(t *testing.T) {
+       for _, title := range []string{
+               "New page",
+               "&&&&",
+       } {
+               models.PrepareTestEnv(t)
+
+               ctx := test.MockContext(t, "user2/repo1/wiki/_new")
+               test.LoadUser(t, ctx, 2)
+               test.LoadRepo(t, ctx, 1)
+               web.SetForm(ctx, &forms.NewWikiForm{
+                       Title:   title,
+                       Content: content,
+                       Message: message,
+               })
+               NewWikiPost(ctx)
+               assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
+               assertWikiExists(t, ctx.Repo.Repository, title)
+               assert.Equal(t, wikiContent(t, ctx.Repo.Repository, title), content)
+       }
+}
+
+func TestNewWikiPost_ReservedName(t *testing.T) {
+       models.PrepareTestEnv(t)
+
+       ctx := test.MockContext(t, "user2/repo1/wiki/_new")
+       test.LoadUser(t, ctx, 2)
+       test.LoadRepo(t, ctx, 1)
+       web.SetForm(ctx, &forms.NewWikiForm{
+               Title:   "_edit",
+               Content: content,
+               Message: message,
+       })
+       NewWikiPost(ctx)
+       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+       assert.EqualValues(t, ctx.Tr("repo.wiki.reserved_page"), ctx.Flash.ErrorMsg)
+       assertWikiNotExists(t, ctx.Repo.Repository, "_edit")
+}
+
+func TestEditWiki(t *testing.T) {
+       models.PrepareTestEnv(t)
+
+       ctx := test.MockContext(t, "user2/repo1/wiki/_edit/Home")
+       ctx.SetParams(":page", "Home")
+       test.LoadUser(t, ctx, 2)
+       test.LoadRepo(t, ctx, 1)
+       EditWiki(ctx)
+       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+       assert.EqualValues(t, "Home", ctx.Data["Title"])
+       assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["content"])
+}
+
+func TestEditWikiPost(t *testing.T) {
+       for _, title := range []string{
+               "Home",
+               "New/<page>",
+       } {
+               models.PrepareTestEnv(t)
+               ctx := test.MockContext(t, "user2/repo1/wiki/_new/Home")
+               ctx.SetParams(":page", "Home")
+               test.LoadUser(t, ctx, 2)
+               test.LoadRepo(t, ctx, 1)
+               web.SetForm(ctx, &forms.NewWikiForm{
+                       Title:   title,
+                       Content: content,
+                       Message: message,
+               })
+               EditWikiPost(ctx)
+               assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
+               assertWikiExists(t, ctx.Repo.Repository, title)
+               assert.Equal(t, wikiContent(t, ctx.Repo.Repository, title), content)
+               if title != "Home" {
+                       assertWikiNotExists(t, ctx.Repo.Repository, "Home")
+               }
+       }
+}
+
+func TestDeleteWikiPagePost(t *testing.T) {
+       models.PrepareTestEnv(t)
+
+       ctx := test.MockContext(t, "user2/repo1/wiki/Home/delete")
+       test.LoadUser(t, ctx, 2)
+       test.LoadRepo(t, ctx, 1)
+       DeleteWikiPagePost(ctx)
+       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+       assertWikiNotExists(t, ctx.Repo.Repository, "Home")
+}
+
+func TestWikiRaw(t *testing.T) {
+       for filepath, filetype := range map[string]string{
+               "jpeg.jpg":                 "image/jpeg",
+               "images/jpeg.jpg":          "image/jpeg",
+               "Page With Spaced Name":    "text/plain; charset=utf-8",
+               "Page-With-Spaced-Name":    "text/plain; charset=utf-8",
+               "Page With Spaced Name.md": "text/plain; charset=utf-8",
+               "Page-With-Spaced-Name.md": "text/plain; charset=utf-8",
+       } {
+               models.PrepareTestEnv(t)
+
+               ctx := test.MockContext(t, "user2/repo1/wiki/raw/"+filepath)
+               ctx.SetParams("*", filepath)
+               test.LoadUser(t, ctx, 2)
+               test.LoadRepo(t, ctx, 1)
+               WikiRaw(ctx)
+               assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+               assert.EqualValues(t, filetype, ctx.Resp.Header().Get("Content-Type"))
+       }
+}
diff --git a/routers/web/swagger_json.go b/routers/web/swagger_json.go
new file mode 100644 (file)
index 0000000..82d7269
--- /dev/null
@@ -0,0 +1,26 @@
+// 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 web
+
+import (
+       "net/http"
+
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+)
+
+// tplSwaggerV1Json swagger v1 json template
+const tplSwaggerV1Json base.TplName = "swagger/v1_json"
+
+// SwaggerV1Json render swagger v1 json
+func SwaggerV1Json(ctx *context.Context) {
+       t := ctx.Render.TemplateLookup(string(tplSwaggerV1Json))
+       ctx.Resp.Header().Set("Content-Type", "application/json")
+       if err := t.Execute(ctx.Resp, ctx.Data); err != nil {
+               log.Error("%v", err)
+               ctx.Error(http.StatusInternalServerError)
+       }
+}
diff --git a/routers/web/user/auth.go b/routers/web/user/auth.go
new file mode 100644 (file)
index 0000000..827b7cd
--- /dev/null
@@ -0,0 +1,1769 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2018 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 user
+
+import (
+       "errors"
+       "fmt"
+       "io"
+       "io/ioutil"
+       "net/http"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/auth/oauth2"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/eventsource"
+       "code.gitea.io/gitea/modules/hcaptcha"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/password"
+       "code.gitea.io/gitea/modules/recaptcha"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/timeutil"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/modules/web/middleware"
+       "code.gitea.io/gitea/routers/utils"
+       "code.gitea.io/gitea/services/externalaccount"
+       "code.gitea.io/gitea/services/forms"
+       "code.gitea.io/gitea/services/mailer"
+
+       "github.com/markbates/goth"
+       "github.com/tstranex/u2f"
+)
+
+const (
+       // tplMustChangePassword template for updating a user's password
+       tplMustChangePassword = "user/auth/change_passwd"
+       // tplSignIn template for sign in page
+       tplSignIn base.TplName = "user/auth/signin"
+       // tplSignUp template path for sign up page
+       tplSignUp base.TplName = "user/auth/signup"
+       // TplActivate template path for activate user
+       TplActivate       base.TplName = "user/auth/activate"
+       tplForgotPassword base.TplName = "user/auth/forgot_passwd"
+       tplResetPassword  base.TplName = "user/auth/reset_passwd"
+       tplTwofa          base.TplName = "user/auth/twofa"
+       tplTwofaScratch   base.TplName = "user/auth/twofa_scratch"
+       tplLinkAccount    base.TplName = "user/auth/link_account"
+       tplU2F            base.TplName = "user/auth/u2f"
+)
+
+// AutoSignIn reads cookie and try to auto-login.
+func AutoSignIn(ctx *context.Context) (bool, error) {
+       if !models.HasEngine {
+               return false, nil
+       }
+
+       uname := ctx.GetCookie(setting.CookieUserName)
+       if len(uname) == 0 {
+               return false, nil
+       }
+
+       isSucceed := false
+       defer func() {
+               if !isSucceed {
+                       log.Trace("auto-login cookie cleared: %s", uname)
+                       ctx.DeleteCookie(setting.CookieUserName)
+                       ctx.DeleteCookie(setting.CookieRememberName)
+               }
+       }()
+
+       u, err := models.GetUserByName(uname)
+       if err != nil {
+               if !models.IsErrUserNotExist(err) {
+                       return false, fmt.Errorf("GetUserByName: %v", err)
+               }
+               return false, nil
+       }
+
+       if val, ok := ctx.GetSuperSecureCookie(
+               base.EncodeMD5(u.Rands+u.Passwd), setting.CookieRememberName); !ok || val != u.Name {
+               return false, nil
+       }
+
+       isSucceed = true
+
+       // Set session IDs
+       if err := ctx.Session.Set("uid", u.ID); err != nil {
+               return false, err
+       }
+       if err := ctx.Session.Set("uname", u.Name); err != nil {
+               return false, err
+       }
+       if err := ctx.Session.Release(); err != nil {
+               return false, err
+       }
+
+       middleware.DeleteCSRFCookie(ctx.Resp)
+       return true, nil
+}
+
+func checkAutoLogin(ctx *context.Context) bool {
+       // Check auto-login.
+       isSucceed, err := AutoSignIn(ctx)
+       if err != nil {
+               ctx.ServerError("AutoSignIn", err)
+               return true
+       }
+
+       redirectTo := ctx.Query("redirect_to")
+       if len(redirectTo) > 0 {
+               middleware.SetRedirectToCookie(ctx.Resp, redirectTo)
+       } else {
+               redirectTo = ctx.GetCookie("redirect_to")
+       }
+
+       if isSucceed {
+               middleware.DeleteRedirectToCookie(ctx.Resp)
+               ctx.RedirectToFirst(redirectTo, setting.AppSubURL+string(setting.LandingPageURL))
+               return true
+       }
+
+       return false
+}
+
+// SignIn render sign in page
+func SignIn(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("sign_in")
+
+       // Check auto-login.
+       if checkAutoLogin(ctx) {
+               return
+       }
+
+       orderedOAuth2Names, oauth2Providers, err := models.GetActiveOAuth2Providers()
+       if err != nil {
+               ctx.ServerError("UserSignIn", err)
+               return
+       }
+       ctx.Data["OrderedOAuth2Names"] = orderedOAuth2Names
+       ctx.Data["OAuth2Providers"] = oauth2Providers
+       ctx.Data["Title"] = ctx.Tr("sign_in")
+       ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
+       ctx.Data["PageIsSignIn"] = true
+       ctx.Data["PageIsLogin"] = true
+       ctx.Data["EnableSSPI"] = models.IsSSPIEnabled()
+
+       ctx.HTML(http.StatusOK, tplSignIn)
+}
+
+// SignInPost response for sign in request
+func SignInPost(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("sign_in")
+
+       orderedOAuth2Names, oauth2Providers, err := models.GetActiveOAuth2Providers()
+       if err != nil {
+               ctx.ServerError("UserSignIn", err)
+               return
+       }
+       ctx.Data["OrderedOAuth2Names"] = orderedOAuth2Names
+       ctx.Data["OAuth2Providers"] = oauth2Providers
+       ctx.Data["Title"] = ctx.Tr("sign_in")
+       ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
+       ctx.Data["PageIsSignIn"] = true
+       ctx.Data["PageIsLogin"] = true
+       ctx.Data["EnableSSPI"] = models.IsSSPIEnabled()
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplSignIn)
+               return
+       }
+
+       form := web.GetForm(ctx).(*forms.SignInForm)
+       u, err := models.UserSignIn(form.UserName, form.Password)
+       if err != nil {
+               if models.IsErrUserNotExist(err) {
+                       ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form)
+                       log.Info("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
+               } else if models.IsErrEmailAlreadyUsed(err) {
+                       ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSignIn, &form)
+                       log.Info("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
+               } else if models.IsErrUserProhibitLogin(err) {
+                       log.Info("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
+                       ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
+                       ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
+               } else if models.IsErrUserInactive(err) {
+                       if setting.Service.RegisterEmailConfirm {
+                               ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
+                               ctx.HTML(http.StatusOK, TplActivate)
+                       } else {
+                               log.Info("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
+                               ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
+                               ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
+                       }
+               } else {
+                       ctx.ServerError("UserSignIn", err)
+               }
+               return
+       }
+       // If this user is enrolled in 2FA, we can't sign the user in just yet.
+       // Instead, redirect them to the 2FA authentication page.
+       _, err = models.GetTwoFactorByUID(u.ID)
+       if err != nil {
+               if models.IsErrTwoFactorNotEnrolled(err) {
+                       handleSignIn(ctx, u, form.Remember)
+               } else {
+                       ctx.ServerError("UserSignIn", err)
+               }
+               return
+       }
+
+       // User needs to use 2FA, save data and redirect to 2FA page.
+       if err := ctx.Session.Set("twofaUid", u.ID); err != nil {
+               ctx.ServerError("UserSignIn: Unable to set twofaUid in session", err)
+               return
+       }
+       if err := ctx.Session.Set("twofaRemember", form.Remember); err != nil {
+               ctx.ServerError("UserSignIn: Unable to set twofaRemember in session", err)
+               return
+       }
+       if err := ctx.Session.Release(); err != nil {
+               ctx.ServerError("UserSignIn: Unable to save session", err)
+               return
+       }
+
+       regs, err := models.GetU2FRegistrationsByUID(u.ID)
+       if err == nil && len(regs) > 0 {
+               ctx.Redirect(setting.AppSubURL + "/user/u2f")
+               return
+       }
+
+       ctx.Redirect(setting.AppSubURL + "/user/two_factor")
+}
+
+// TwoFactor shows the user a two-factor authentication page.
+func TwoFactor(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("twofa")
+
+       // Check auto-login.
+       if checkAutoLogin(ctx) {
+               return
+       }
+
+       // Ensure user is in a 2FA session.
+       if ctx.Session.Get("twofaUid") == nil {
+               ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
+               return
+       }
+
+       ctx.HTML(http.StatusOK, tplTwofa)
+}
+
+// TwoFactorPost validates a user's two-factor authentication token.
+func TwoFactorPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.TwoFactorAuthForm)
+       ctx.Data["Title"] = ctx.Tr("twofa")
+
+       // Ensure user is in a 2FA session.
+       idSess := ctx.Session.Get("twofaUid")
+       if idSess == nil {
+               ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
+               return
+       }
+
+       id := idSess.(int64)
+       twofa, err := models.GetTwoFactorByUID(id)
+       if err != nil {
+               ctx.ServerError("UserSignIn", err)
+               return
+       }
+
+       // Validate the passcode with the stored TOTP secret.
+       ok, err := twofa.ValidateTOTP(form.Passcode)
+       if err != nil {
+               ctx.ServerError("UserSignIn", err)
+               return
+       }
+
+       if ok && twofa.LastUsedPasscode != form.Passcode {
+               remember := ctx.Session.Get("twofaRemember").(bool)
+               u, err := models.GetUserByID(id)
+               if err != nil {
+                       ctx.ServerError("UserSignIn", err)
+                       return
+               }
+
+               if ctx.Session.Get("linkAccount") != nil {
+                       gothUser := ctx.Session.Get("linkAccountGothUser")
+                       if gothUser == nil {
+                               ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
+                               return
+                       }
+
+                       err = externalaccount.LinkAccountToUser(u, gothUser.(goth.User))
+                       if err != nil {
+                               ctx.ServerError("UserSignIn", err)
+                               return
+                       }
+               }
+
+               twofa.LastUsedPasscode = form.Passcode
+               if err = models.UpdateTwoFactor(twofa); err != nil {
+                       ctx.ServerError("UserSignIn", err)
+                       return
+               }
+
+               handleSignIn(ctx, u, remember)
+               return
+       }
+
+       ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplTwofa, forms.TwoFactorAuthForm{})
+}
+
+// TwoFactorScratch shows the scratch code form for two-factor authentication.
+func TwoFactorScratch(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("twofa_scratch")
+
+       // Check auto-login.
+       if checkAutoLogin(ctx) {
+               return
+       }
+
+       // Ensure user is in a 2FA session.
+       if ctx.Session.Get("twofaUid") == nil {
+               ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
+               return
+       }
+
+       ctx.HTML(http.StatusOK, tplTwofaScratch)
+}
+
+// TwoFactorScratchPost validates and invalidates a user's two-factor scratch token.
+func TwoFactorScratchPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.TwoFactorScratchAuthForm)
+       ctx.Data["Title"] = ctx.Tr("twofa_scratch")
+
+       // Ensure user is in a 2FA session.
+       idSess := ctx.Session.Get("twofaUid")
+       if idSess == nil {
+               ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
+               return
+       }
+
+       id := idSess.(int64)
+       twofa, err := models.GetTwoFactorByUID(id)
+       if err != nil {
+               ctx.ServerError("UserSignIn", err)
+               return
+       }
+
+       // Validate the passcode with the stored TOTP secret.
+       if twofa.VerifyScratchToken(form.Token) {
+               // Invalidate the scratch token.
+               _, err = twofa.GenerateScratchToken()
+               if err != nil {
+                       ctx.ServerError("UserSignIn", err)
+                       return
+               }
+               if err = models.UpdateTwoFactor(twofa); err != nil {
+                       ctx.ServerError("UserSignIn", err)
+                       return
+               }
+
+               remember := ctx.Session.Get("twofaRemember").(bool)
+               u, err := models.GetUserByID(id)
+               if err != nil {
+                       ctx.ServerError("UserSignIn", err)
+                       return
+               }
+
+               handleSignInFull(ctx, u, remember, false)
+               ctx.Flash.Info(ctx.Tr("auth.twofa_scratch_used"))
+               ctx.Redirect(setting.AppSubURL + "/user/settings/security")
+               return
+       }
+
+       ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplTwofaScratch, forms.TwoFactorScratchAuthForm{})
+}
+
+// U2F shows the U2F login page
+func U2F(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("twofa")
+       ctx.Data["RequireU2F"] = true
+       // Check auto-login.
+       if checkAutoLogin(ctx) {
+               return
+       }
+
+       // Ensure user is in a 2FA session.
+       if ctx.Session.Get("twofaUid") == nil {
+               ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
+               return
+       }
+
+       ctx.HTML(http.StatusOK, tplU2F)
+}
+
+// U2FChallenge submits a sign challenge to the browser
+func U2FChallenge(ctx *context.Context) {
+       // Ensure user is in a U2F session.
+       idSess := ctx.Session.Get("twofaUid")
+       if idSess == nil {
+               ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
+               return
+       }
+       id := idSess.(int64)
+       regs, err := models.GetU2FRegistrationsByUID(id)
+       if err != nil {
+               ctx.ServerError("UserSignIn", err)
+               return
+       }
+       if len(regs) == 0 {
+               ctx.ServerError("UserSignIn", errors.New("no device registered"))
+               return
+       }
+       challenge, err := u2f.NewChallenge(setting.U2F.AppID, setting.U2F.TrustedFacets)
+       if err != nil {
+               ctx.ServerError("u2f.NewChallenge", err)
+               return
+       }
+       if err := ctx.Session.Set("u2fChallenge", challenge); err != nil {
+               ctx.ServerError("UserSignIn: unable to set u2fChallenge in session", err)
+               return
+       }
+       if err := ctx.Session.Release(); err != nil {
+               ctx.ServerError("UserSignIn: unable to store session", err)
+       }
+
+       ctx.JSON(http.StatusOK, challenge.SignRequest(regs.ToRegistrations()))
+}
+
+// U2FSign authenticates the user by signResp
+func U2FSign(ctx *context.Context) {
+       signResp := web.GetForm(ctx).(*u2f.SignResponse)
+       challSess := ctx.Session.Get("u2fChallenge")
+       idSess := ctx.Session.Get("twofaUid")
+       if challSess == nil || idSess == nil {
+               ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
+               return
+       }
+       challenge := challSess.(*u2f.Challenge)
+       id := idSess.(int64)
+       regs, err := models.GetU2FRegistrationsByUID(id)
+       if err != nil {
+               ctx.ServerError("UserSignIn", err)
+               return
+       }
+       for _, reg := range regs {
+               r, err := reg.Parse()
+               if err != nil {
+                       log.Fatal("parsing u2f registration: %v", err)
+                       continue
+               }
+               newCounter, authErr := r.Authenticate(*signResp, *challenge, reg.Counter)
+               if authErr == nil {
+                       reg.Counter = newCounter
+                       user, err := models.GetUserByID(id)
+                       if err != nil {
+                               ctx.ServerError("UserSignIn", err)
+                               return
+                       }
+                       remember := ctx.Session.Get("twofaRemember").(bool)
+                       if err := reg.UpdateCounter(); err != nil {
+                               ctx.ServerError("UserSignIn", err)
+                               return
+                       }
+
+                       if ctx.Session.Get("linkAccount") != nil {
+                               gothUser := ctx.Session.Get("linkAccountGothUser")
+                               if gothUser == nil {
+                                       ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
+                                       return
+                               }
+
+                               err = externalaccount.LinkAccountToUser(user, gothUser.(goth.User))
+                               if err != nil {
+                                       ctx.ServerError("UserSignIn", err)
+                                       return
+                               }
+                       }
+                       redirect := handleSignInFull(ctx, user, remember, false)
+                       if redirect == "" {
+                               redirect = setting.AppSubURL + "/"
+                       }
+                       ctx.PlainText(200, []byte(redirect))
+                       return
+               }
+       }
+       ctx.Error(http.StatusUnauthorized)
+}
+
+// This handles the final part of the sign-in process of the user.
+func handleSignIn(ctx *context.Context, u *models.User, remember bool) {
+       handleSignInFull(ctx, u, remember, true)
+}
+
+func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyRedirect bool) string {
+       if remember {
+               days := 86400 * setting.LogInRememberDays
+               ctx.SetCookie(setting.CookieUserName, u.Name, days)
+               ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
+                       setting.CookieRememberName, u.Name, days)
+       }
+
+       _ = ctx.Session.Delete("openid_verified_uri")
+       _ = ctx.Session.Delete("openid_signin_remember")
+       _ = ctx.Session.Delete("openid_determined_email")
+       _ = ctx.Session.Delete("openid_determined_username")
+       _ = ctx.Session.Delete("twofaUid")
+       _ = ctx.Session.Delete("twofaRemember")
+       _ = ctx.Session.Delete("u2fChallenge")
+       _ = ctx.Session.Delete("linkAccount")
+       if err := ctx.Session.Set("uid", u.ID); err != nil {
+               log.Error("Error setting uid %d in session: %v", u.ID, err)
+       }
+       if err := ctx.Session.Set("uname", u.Name); err != nil {
+               log.Error("Error setting uname %s session: %v", u.Name, err)
+       }
+       if err := ctx.Session.Release(); err != nil {
+               log.Error("Unable to store session: %v", err)
+       }
+
+       // Language setting of the user overwrites the one previously set
+       // If the user does not have a locale set, we save the current one.
+       if len(u.Language) == 0 {
+               u.Language = ctx.Locale.Language()
+               if err := models.UpdateUserCols(u, "language"); err != nil {
+                       log.Error(fmt.Sprintf("Error updating user language [user: %d, locale: %s]", u.ID, u.Language))
+                       return setting.AppSubURL + "/"
+               }
+       }
+
+       middleware.SetLocaleCookie(ctx.Resp, u.Language, 0)
+
+       // Clear whatever CSRF has right now, force to generate a new one
+       middleware.DeleteCSRFCookie(ctx.Resp)
+
+       // Register last login
+       u.SetLastLogin()
+       if err := models.UpdateUserCols(u, "last_login_unix"); err != nil {
+               ctx.ServerError("UpdateUserCols", err)
+               return setting.AppSubURL + "/"
+       }
+
+       if redirectTo := ctx.GetCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
+               middleware.DeleteRedirectToCookie(ctx.Resp)
+               if obeyRedirect {
+                       ctx.RedirectToFirst(redirectTo)
+               }
+               return redirectTo
+       }
+
+       if obeyRedirect {
+               ctx.Redirect(setting.AppSubURL + "/")
+       }
+       return setting.AppSubURL + "/"
+}
+
+// SignInOAuth handles the OAuth2 login buttons
+func SignInOAuth(ctx *context.Context) {
+       provider := ctx.Params(":provider")
+
+       loginSource, err := models.GetActiveOAuth2LoginSourceByName(provider)
+       if err != nil {
+               ctx.ServerError("SignIn", err)
+               return
+       }
+
+       // try to do a direct callback flow, so we don't authenticate the user again but use the valid accesstoken to get the user
+       user, gothUser, err := oAuth2UserLoginCallback(loginSource, ctx.Req, ctx.Resp)
+       if err == nil && user != nil {
+               // we got the user without going through the whole OAuth2 authentication flow again
+               handleOAuth2SignIn(ctx, user, gothUser)
+               return
+       }
+
+       if err = oauth2.Auth(loginSource.Name, ctx.Req, ctx.Resp); err != nil {
+               if strings.Contains(err.Error(), "no provider for ") {
+                       if err = models.ResetOAuth2(); err != nil {
+                               ctx.ServerError("SignIn", err)
+                               return
+                       }
+                       if err = oauth2.Auth(loginSource.Name, ctx.Req, ctx.Resp); err != nil {
+                               ctx.ServerError("SignIn", err)
+                       }
+                       return
+               }
+               ctx.ServerError("SignIn", err)
+       }
+       // redirect is done in oauth2.Auth
+}
+
+// SignInOAuthCallback handles the callback from the given provider
+func SignInOAuthCallback(ctx *context.Context) {
+       provider := ctx.Params(":provider")
+
+       // first look if the provider is still active
+       loginSource, err := models.GetActiveOAuth2LoginSourceByName(provider)
+       if err != nil {
+               ctx.ServerError("SignIn", err)
+               return
+       }
+
+       if loginSource == nil {
+               ctx.ServerError("SignIn", errors.New("No valid provider found, check configured callback url in provider"))
+               return
+       }
+
+       u, gothUser, err := oAuth2UserLoginCallback(loginSource, ctx.Req, ctx.Resp)
+
+       if err != nil {
+               ctx.ServerError("UserSignIn", err)
+               return
+       }
+
+       if u == nil {
+               if !(setting.Service.DisableRegistration || setting.Service.AllowOnlyInternalRegistration) && setting.OAuth2Client.EnableAutoRegistration {
+                       // create new user with details from oauth2 provider
+                       var missingFields []string
+                       if gothUser.UserID == "" {
+                               missingFields = append(missingFields, "sub")
+                       }
+                       if gothUser.Email == "" {
+                               missingFields = append(missingFields, "email")
+                       }
+                       if setting.OAuth2Client.Username == setting.OAuth2UsernameNickname && gothUser.NickName == "" {
+                               missingFields = append(missingFields, "nickname")
+                       }
+                       if len(missingFields) > 0 {
+                               log.Error("OAuth2 Provider %s returned empty or missing fields: %s", loginSource.Name, missingFields)
+                               if loginSource.IsOAuth2() && loginSource.OAuth2().Provider == "openidConnect" {
+                                       log.Error("You may need to change the 'OPENID_CONNECT_SCOPES' setting to request all required fields")
+                               }
+                               err = fmt.Errorf("OAuth2 Provider %s returned empty or missing fields: %s", loginSource.Name, missingFields)
+                               ctx.ServerError("CreateUser", err)
+                               return
+                       }
+                       u = &models.User{
+                               Name:        getUserName(&gothUser),
+                               FullName:    gothUser.Name,
+                               Email:       gothUser.Email,
+                               IsActive:    !setting.OAuth2Client.RegisterEmailConfirm,
+                               LoginType:   models.LoginOAuth2,
+                               LoginSource: loginSource.ID,
+                               LoginName:   gothUser.UserID,
+                       }
+
+                       if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
+                               // error already handled
+                               return
+                       }
+               } else {
+                       // no existing user is found, request attach or new account
+                       showLinkingLogin(ctx, gothUser)
+                       return
+               }
+       }
+
+       handleOAuth2SignIn(ctx, u, gothUser)
+}
+
+func getUserName(gothUser *goth.User) string {
+       switch setting.OAuth2Client.Username {
+       case setting.OAuth2UsernameEmail:
+               return strings.Split(gothUser.Email, "@")[0]
+       case setting.OAuth2UsernameNickname:
+               return gothUser.NickName
+       default: // OAuth2UsernameUserid
+               return gothUser.UserID
+       }
+}
+
+func showLinkingLogin(ctx *context.Context, gothUser goth.User) {
+       if err := ctx.Session.Set("linkAccountGothUser", gothUser); err != nil {
+               log.Error("Error setting linkAccountGothUser in session: %v", err)
+       }
+       if err := ctx.Session.Release(); err != nil {
+               log.Error("Error storing session: %v", err)
+       }
+       ctx.Redirect(setting.AppSubURL + "/user/link_account")
+}
+
+func updateAvatarIfNeed(url string, u *models.User) {
+       if setting.OAuth2Client.UpdateAvatar && len(url) > 0 {
+               resp, err := http.Get(url)
+               if err == nil {
+                       defer func() {
+                               _ = resp.Body.Close()
+                       }()
+               }
+               // ignore any error
+               if err == nil && resp.StatusCode == http.StatusOK {
+                       data, err := ioutil.ReadAll(io.LimitReader(resp.Body, setting.Avatar.MaxFileSize+1))
+                       if err == nil && int64(len(data)) <= setting.Avatar.MaxFileSize {
+                               _ = u.UploadAvatar(data)
+                       }
+               }
+       }
+}
+
+func handleOAuth2SignIn(ctx *context.Context, u *models.User, gothUser goth.User) {
+       updateAvatarIfNeed(gothUser.AvatarURL, u)
+
+       // If this user is enrolled in 2FA, we can't sign the user in just yet.
+       // Instead, redirect them to the 2FA authentication page.
+       _, err := models.GetTwoFactorByUID(u.ID)
+       if err != nil {
+               if !models.IsErrTwoFactorNotEnrolled(err) {
+                       ctx.ServerError("UserSignIn", err)
+                       return
+               }
+
+               if err := ctx.Session.Set("uid", u.ID); err != nil {
+                       log.Error("Error setting uid in session: %v", err)
+               }
+               if err := ctx.Session.Set("uname", u.Name); err != nil {
+                       log.Error("Error setting uname in session: %v", err)
+               }
+               if err := ctx.Session.Release(); err != nil {
+                       log.Error("Error storing session: %v", err)
+               }
+
+               // Clear whatever CSRF has right now, force to generate a new one
+               middleware.DeleteCSRFCookie(ctx.Resp)
+
+               // Register last login
+               u.SetLastLogin()
+               if err := models.UpdateUserCols(u, "last_login_unix"); err != nil {
+                       ctx.ServerError("UpdateUserCols", err)
+                       return
+               }
+
+               // update external user information
+               if err := models.UpdateExternalUser(u, gothUser); err != nil {
+                       log.Error("UpdateExternalUser failed: %v", err)
+               }
+
+               if redirectTo := ctx.GetCookie("redirect_to"); len(redirectTo) > 0 {
+                       middleware.DeleteRedirectToCookie(ctx.Resp)
+                       ctx.RedirectToFirst(redirectTo)
+                       return
+               }
+
+               ctx.Redirect(setting.AppSubURL + "/")
+               return
+       }
+
+       // User needs to use 2FA, save data and redirect to 2FA page.
+       if err := ctx.Session.Set("twofaUid", u.ID); err != nil {
+               log.Error("Error setting twofaUid in session: %v", err)
+       }
+       if err := ctx.Session.Set("twofaRemember", false); err != nil {
+               log.Error("Error setting twofaRemember in session: %v", err)
+       }
+       if err := ctx.Session.Release(); err != nil {
+               log.Error("Error storing session: %v", err)
+       }
+
+       // If U2F is enrolled -> Redirect to U2F instead
+       regs, err := models.GetU2FRegistrationsByUID(u.ID)
+       if err == nil && len(regs) > 0 {
+               ctx.Redirect(setting.AppSubURL + "/user/u2f")
+               return
+       }
+
+       ctx.Redirect(setting.AppSubURL + "/user/two_factor")
+}
+
+// OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful
+// login the user
+func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Request, response http.ResponseWriter) (*models.User, goth.User, error) {
+       gothUser, err := oauth2.ProviderCallback(loginSource.Name, request, response)
+
+       if err != nil {
+               if err.Error() == "securecookie: the value is too long" {
+                       log.Error("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", loginSource.Name, setting.OAuth2.MaxTokenLength)
+                       err = fmt.Errorf("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", loginSource.Name, setting.OAuth2.MaxTokenLength)
+               }
+               return nil, goth.User{}, err
+       }
+
+       user := &models.User{
+               LoginName:   gothUser.UserID,
+               LoginType:   models.LoginOAuth2,
+               LoginSource: loginSource.ID,
+       }
+
+       hasUser, err := models.GetUser(user)
+       if err != nil {
+               return nil, goth.User{}, err
+       }
+
+       if hasUser {
+               return user, gothUser, nil
+       }
+
+       // search in external linked users
+       externalLoginUser := &models.ExternalLoginUser{
+               ExternalID:    gothUser.UserID,
+               LoginSourceID: loginSource.ID,
+       }
+       hasUser, err = models.GetExternalLogin(externalLoginUser)
+       if err != nil {
+               return nil, goth.User{}, err
+       }
+       if hasUser {
+               user, err = models.GetUserByID(externalLoginUser.UserID)
+               return user, gothUser, err
+       }
+
+       // no user found to login
+       return nil, gothUser, nil
+
+}
+
+// LinkAccount shows the page where the user can decide to login or create a new account
+func LinkAccount(ctx *context.Context) {
+       ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
+       ctx.Data["Title"] = ctx.Tr("link_account")
+       ctx.Data["LinkAccountMode"] = true
+       ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
+       ctx.Data["Captcha"] = context.GetImageCaptcha()
+       ctx.Data["CaptchaType"] = setting.Service.CaptchaType
+       ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
+       ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
+       ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
+       ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
+       ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
+       ctx.Data["ShowRegistrationButton"] = false
+
+       // use this to set the right link into the signIn and signUp templates in the link_account template
+       ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
+       ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
+
+       gothUser := ctx.Session.Get("linkAccountGothUser")
+       if gothUser == nil {
+               ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
+               return
+       }
+
+       gu, _ := gothUser.(goth.User)
+       uname := getUserName(&gu)
+       email := gu.Email
+       ctx.Data["user_name"] = uname
+       ctx.Data["email"] = email
+
+       if len(email) != 0 {
+               u, err := models.GetUserByEmail(email)
+               if err != nil && !models.IsErrUserNotExist(err) {
+                       ctx.ServerError("UserSignIn", err)
+                       return
+               }
+               if u != nil {
+                       ctx.Data["user_exists"] = true
+               }
+       } else if len(uname) != 0 {
+               u, err := models.GetUserByName(uname)
+               if err != nil && !models.IsErrUserNotExist(err) {
+                       ctx.ServerError("UserSignIn", err)
+                       return
+               }
+               if u != nil {
+                       ctx.Data["user_exists"] = true
+               }
+       }
+
+       ctx.HTML(http.StatusOK, tplLinkAccount)
+}
+
+// LinkAccountPostSignIn handle the coupling of external account with another account using signIn
+func LinkAccountPostSignIn(ctx *context.Context) {
+       signInForm := web.GetForm(ctx).(*forms.SignInForm)
+       ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
+       ctx.Data["Title"] = ctx.Tr("link_account")
+       ctx.Data["LinkAccountMode"] = true
+       ctx.Data["LinkAccountModeSignIn"] = true
+       ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
+       ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
+       ctx.Data["Captcha"] = context.GetImageCaptcha()
+       ctx.Data["CaptchaType"] = setting.Service.CaptchaType
+       ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
+       ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
+       ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
+       ctx.Data["ShowRegistrationButton"] = false
+
+       // use this to set the right link into the signIn and signUp templates in the link_account template
+       ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
+       ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
+
+       gothUser := ctx.Session.Get("linkAccountGothUser")
+       if gothUser == nil {
+               ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
+               return
+       }
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplLinkAccount)
+               return
+       }
+
+       u, err := models.UserSignIn(signInForm.UserName, signInForm.Password)
+       if err != nil {
+               if models.IsErrUserNotExist(err) {
+                       ctx.Data["user_exists"] = true
+                       ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplLinkAccount, &signInForm)
+               } else {
+                       ctx.ServerError("UserLinkAccount", err)
+               }
+               return
+       }
+
+       linkAccount(ctx, u, gothUser.(goth.User), signInForm.Remember)
+}
+
+func linkAccount(ctx *context.Context, u *models.User, gothUser goth.User, remember bool) {
+       updateAvatarIfNeed(gothUser.AvatarURL, u)
+
+       // If this user is enrolled in 2FA, we can't sign the user in just yet.
+       // Instead, redirect them to the 2FA authentication page.
+       _, err := models.GetTwoFactorByUID(u.ID)
+       if err != nil {
+               if !models.IsErrTwoFactorNotEnrolled(err) {
+                       ctx.ServerError("UserLinkAccount", err)
+                       return
+               }
+
+               err = externalaccount.LinkAccountToUser(u, gothUser)
+               if err != nil {
+                       ctx.ServerError("UserLinkAccount", err)
+                       return
+               }
+
+               handleSignIn(ctx, u, remember)
+               return
+       }
+
+       // User needs to use 2FA, save data and redirect to 2FA page.
+       if err := ctx.Session.Set("twofaUid", u.ID); err != nil {
+               log.Error("Error setting twofaUid in session: %v", err)
+       }
+       if err := ctx.Session.Set("twofaRemember", remember); err != nil {
+               log.Error("Error setting twofaRemember in session: %v", err)
+       }
+       if err := ctx.Session.Set("linkAccount", true); err != nil {
+               log.Error("Error setting linkAccount in session: %v", err)
+       }
+       if err := ctx.Session.Release(); err != nil {
+               log.Error("Error storing session: %v", err)
+       }
+
+       // If U2F is enrolled -> Redirect to U2F instead
+       regs, err := models.GetU2FRegistrationsByUID(u.ID)
+       if err == nil && len(regs) > 0 {
+               ctx.Redirect(setting.AppSubURL + "/user/u2f")
+               return
+       }
+
+       ctx.Redirect(setting.AppSubURL + "/user/two_factor")
+}
+
+// LinkAccountPostRegister handle the creation of a new account for an external account using signUp
+func LinkAccountPostRegister(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.RegisterForm)
+       // TODO Make insecure passwords optional for local accounts also,
+       //      once email-based Second-Factor Auth is available
+       ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
+       ctx.Data["Title"] = ctx.Tr("link_account")
+       ctx.Data["LinkAccountMode"] = true
+       ctx.Data["LinkAccountModeRegister"] = true
+       ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
+       ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
+       ctx.Data["Captcha"] = context.GetImageCaptcha()
+       ctx.Data["CaptchaType"] = setting.Service.CaptchaType
+       ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
+       ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
+       ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
+       ctx.Data["ShowRegistrationButton"] = false
+
+       // use this to set the right link into the signIn and signUp templates in the link_account template
+       ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
+       ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
+
+       gothUserInterface := ctx.Session.Get("linkAccountGothUser")
+       if gothUserInterface == nil {
+               ctx.ServerError("UserSignUp", errors.New("not in LinkAccount session"))
+               return
+       }
+       gothUser, ok := gothUserInterface.(goth.User)
+       if !ok {
+               ctx.ServerError("UserSignUp", fmt.Errorf("session linkAccountGothUser type is %t but not goth.User", gothUserInterface))
+               return
+       }
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplLinkAccount)
+               return
+       }
+
+       if setting.Service.DisableRegistration || setting.Service.AllowOnlyInternalRegistration {
+               ctx.Error(http.StatusForbidden)
+               return
+       }
+
+       if setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha {
+               var valid bool
+               var err error
+               switch setting.Service.CaptchaType {
+               case setting.ImageCaptcha:
+                       valid = context.GetImageCaptcha().VerifyReq(ctx.Req)
+               case setting.ReCaptcha:
+                       valid, err = recaptcha.Verify(ctx, form.GRecaptchaResponse)
+               case setting.HCaptcha:
+                       valid, err = hcaptcha.Verify(ctx, form.HcaptchaResponse)
+               default:
+                       ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
+                       return
+               }
+               if err != nil {
+                       log.Debug("%s", err.Error())
+               }
+
+               if !valid {
+                       ctx.Data["Err_Captcha"] = true
+                       ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplLinkAccount, &form)
+                       return
+               }
+       }
+
+       if !form.IsEmailDomainAllowed() {
+               ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplLinkAccount, &form)
+               return
+       }
+
+       if setting.Service.AllowOnlyExternalRegistration || !setting.Service.RequireExternalRegistrationPassword {
+               // In models.User an empty password is classed as not set, so we set form.Password to empty.
+               // Eventually the database should be changed to indicate "Second Factor"-enabled accounts
+               // (accounts that do not introduce the security vulnerabilities of a password).
+               // If a user decides to circumvent second-factor security, and purposefully create a password,
+               // they can still do so using the "Recover Account" option.
+               form.Password = ""
+       } else {
+               if (len(strings.TrimSpace(form.Password)) > 0 || len(strings.TrimSpace(form.Retype)) > 0) && form.Password != form.Retype {
+                       ctx.Data["Err_Password"] = true
+                       ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplLinkAccount, &form)
+                       return
+               }
+               if len(strings.TrimSpace(form.Password)) > 0 && len(form.Password) < setting.MinPasswordLength {
+                       ctx.Data["Err_Password"] = true
+                       ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplLinkAccount, &form)
+                       return
+               }
+       }
+
+       loginSource, err := models.GetActiveOAuth2LoginSourceByName(gothUser.Provider)
+       if err != nil {
+               ctx.ServerError("CreateUser", err)
+       }
+
+       u := &models.User{
+               Name:        form.UserName,
+               Email:       form.Email,
+               Passwd:      form.Password,
+               IsActive:    !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm),
+               LoginType:   models.LoginOAuth2,
+               LoginSource: loginSource.ID,
+               LoginName:   gothUser.UserID,
+       }
+
+       if !createAndHandleCreatedUser(ctx, tplLinkAccount, form, u, &gothUser, false) {
+               // error already handled
+               return
+       }
+
+       ctx.Redirect(setting.AppSubURL + "/user/login")
+}
+
+// HandleSignOut resets the session and sets the cookies
+func HandleSignOut(ctx *context.Context) {
+       _ = ctx.Session.Flush()
+       _ = ctx.Session.Destroy(ctx.Resp, ctx.Req)
+       ctx.DeleteCookie(setting.CookieUserName)
+       ctx.DeleteCookie(setting.CookieRememberName)
+       middleware.DeleteCSRFCookie(ctx.Resp)
+       middleware.DeleteLocaleCookie(ctx.Resp)
+       middleware.DeleteRedirectToCookie(ctx.Resp)
+}
+
+// SignOut sign out from login status
+func SignOut(ctx *context.Context) {
+       if ctx.User != nil {
+               eventsource.GetManager().SendMessageBlocking(ctx.User.ID, &eventsource.Event{
+                       Name: "logout",
+                       Data: ctx.Session.ID(),
+               })
+       }
+       HandleSignOut(ctx)
+       ctx.Redirect(setting.AppSubURL + "/")
+}
+
+// SignUp render the register page
+func SignUp(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("sign_up")
+
+       ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
+
+       ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
+       ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
+       ctx.Data["Captcha"] = context.GetImageCaptcha()
+       ctx.Data["CaptchaType"] = setting.Service.CaptchaType
+       ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
+       ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
+       ctx.Data["PageIsSignUp"] = true
+
+       //Show Disabled Registration message if DisableRegistration or AllowOnlyExternalRegistration options are true
+       ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration || setting.Service.AllowOnlyExternalRegistration
+
+       ctx.HTML(http.StatusOK, tplSignUp)
+}
+
+// SignUpPost response for sign up information submission
+func SignUpPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.RegisterForm)
+       ctx.Data["Title"] = ctx.Tr("sign_up")
+
+       ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
+
+       ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
+       ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
+       ctx.Data["Captcha"] = context.GetImageCaptcha()
+       ctx.Data["CaptchaType"] = setting.Service.CaptchaType
+       ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
+       ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
+       ctx.Data["PageIsSignUp"] = true
+
+       //Permission denied if DisableRegistration or AllowOnlyExternalRegistration options are true
+       if setting.Service.DisableRegistration || setting.Service.AllowOnlyExternalRegistration {
+               ctx.Error(http.StatusForbidden)
+               return
+       }
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplSignUp)
+               return
+       }
+
+       if setting.Service.EnableCaptcha {
+               var valid bool
+               var err error
+               switch setting.Service.CaptchaType {
+               case setting.ImageCaptcha:
+                       valid = context.GetImageCaptcha().VerifyReq(ctx.Req)
+               case setting.ReCaptcha:
+                       valid, err = recaptcha.Verify(ctx, form.GRecaptchaResponse)
+               case setting.HCaptcha:
+                       valid, err = hcaptcha.Verify(ctx, form.HcaptchaResponse)
+               default:
+                       ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
+                       return
+               }
+               if err != nil {
+                       log.Debug("%s", err.Error())
+               }
+
+               if !valid {
+                       ctx.Data["Err_Captcha"] = true
+                       ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplSignUp, &form)
+                       return
+               }
+       }
+
+       if !form.IsEmailDomainAllowed() {
+               ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplSignUp, &form)
+               return
+       }
+
+       if form.Password != form.Retype {
+               ctx.Data["Err_Password"] = true
+               ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplSignUp, &form)
+               return
+       }
+       if len(form.Password) < setting.MinPasswordLength {
+               ctx.Data["Err_Password"] = true
+               ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplSignUp, &form)
+               return
+       }
+       if !password.IsComplexEnough(form.Password) {
+               ctx.Data["Err_Password"] = true
+               ctx.RenderWithErr(password.BuildComplexityError(ctx), tplSignUp, &form)
+               return
+       }
+       pwned, err := password.IsPwned(ctx, form.Password)
+       if pwned {
+               errMsg := ctx.Tr("auth.password_pwned")
+               if err != nil {
+                       log.Error(err.Error())
+                       errMsg = ctx.Tr("auth.password_pwned_err")
+               }
+               ctx.Data["Err_Password"] = true
+               ctx.RenderWithErr(errMsg, tplSignUp, &form)
+               return
+       }
+
+       u := &models.User{
+               Name:     form.UserName,
+               Email:    form.Email,
+               Passwd:   form.Password,
+               IsActive: !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm),
+       }
+
+       if !createAndHandleCreatedUser(ctx, tplSignUp, form, u, nil, false) {
+               // error already handled
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("auth.sign_up_successful"))
+       handleSignInFull(ctx, u, false, true)
+}
+
+// createAndHandleCreatedUser calls createUserInContext and
+// then handleUserCreated.
+func createAndHandleCreatedUser(ctx *context.Context, tpl base.TplName, form interface{}, u *models.User, gothUser *goth.User, allowLink bool) bool {
+       if !createUserInContext(ctx, tpl, form, u, gothUser, allowLink) {
+               return false
+       }
+       return handleUserCreated(ctx, u, gothUser)
+}
+
+// createUserInContext creates a user and handles errors within a given context.
+// Optionally a template can be specified.
+func createUserInContext(ctx *context.Context, tpl base.TplName, form interface{}, u *models.User, gothUser *goth.User, allowLink bool) (ok bool) {
+       if err := models.CreateUser(u); err != nil {
+               if allowLink && (models.IsErrUserAlreadyExist(err) || models.IsErrEmailAlreadyUsed(err)) {
+                       if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingAuto {
+                               var user *models.User
+                               user = &models.User{Name: u.Name}
+                               hasUser, err := models.GetUser(user)
+                               if !hasUser || err != nil {
+                                       user = &models.User{Email: u.Email}
+                                       hasUser, err = models.GetUser(user)
+                                       if !hasUser || err != nil {
+                                               ctx.ServerError("UserLinkAccount", err)
+                                               return
+                                       }
+                               }
+
+                               // TODO: probably we should respect 'remember' user's choice...
+                               linkAccount(ctx, user, *gothUser, true)
+                               return // user is already created here, all redirects are handled
+                       } else if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingLogin {
+                               showLinkingLogin(ctx, *gothUser)
+                               return // user will be created only after linking login
+                       }
+               }
+
+               // handle error without template
+               if len(tpl) == 0 {
+                       ctx.ServerError("CreateUser", err)
+                       return
+               }
+
+               // handle error with template
+               switch {
+               case models.IsErrUserAlreadyExist(err):
+                       ctx.Data["Err_UserName"] = true
+                       ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tpl, form)
+               case models.IsErrEmailAlreadyUsed(err):
+                       ctx.Data["Err_Email"] = true
+                       ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tpl, form)
+               case models.IsErrEmailInvalid(err):
+                       ctx.Data["Err_Email"] = true
+                       ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form)
+               case models.IsErrNameReserved(err):
+                       ctx.Data["Err_UserName"] = true
+                       ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(models.ErrNameReserved).Name), tpl, form)
+               case models.IsErrNamePatternNotAllowed(err):
+                       ctx.Data["Err_UserName"] = true
+                       ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tpl, form)
+               case models.IsErrNameCharsNotAllowed(err):
+                       ctx.Data["Err_UserName"] = true
+                       ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(models.ErrNameCharsNotAllowed).Name), tpl, form)
+               default:
+                       ctx.ServerError("CreateUser", err)
+               }
+               return
+       }
+       log.Trace("Account created: %s", u.Name)
+       return true
+}
+
+// handleUserCreated does additional steps after a new user is created.
+// It auto-sets admin for the only user, updates the optional external user and
+// sends a confirmation email if required.
+func handleUserCreated(ctx *context.Context, u *models.User, gothUser *goth.User) (ok bool) {
+       // Auto-set admin for the only user.
+       if models.CountUsers() == 1 {
+               u.IsAdmin = true
+               u.IsActive = true
+               u.SetLastLogin()
+               if err := models.UpdateUserCols(u, "is_admin", "is_active", "last_login_unix"); err != nil {
+                       ctx.ServerError("UpdateUser", err)
+                       return
+               }
+       }
+
+       // update external user information
+       if gothUser != nil {
+               if err := models.UpdateExternalUser(u, *gothUser); err != nil {
+                       log.Error("UpdateExternalUser failed: %v", err)
+               }
+       }
+
+       // Send confirmation email
+       if !u.IsActive && u.ID > 1 {
+               mailer.SendActivateAccountMail(ctx.Locale, u)
+
+               ctx.Data["IsSendRegisterMail"] = true
+               ctx.Data["Email"] = u.Email
+               ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())
+               ctx.HTML(http.StatusOK, TplActivate)
+
+               if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
+                       log.Error("Set cache(MailResendLimit) fail: %v", err)
+               }
+               return
+       }
+
+       return true
+}
+
+// Activate render activate user page
+func Activate(ctx *context.Context) {
+       code := ctx.Query("code")
+
+       if len(code) == 0 {
+               ctx.Data["IsActivatePage"] = true
+               if ctx.User == nil || ctx.User.IsActive {
+                       ctx.NotFound("invalid user", nil)
+                       return
+               }
+               // Resend confirmation email.
+               if setting.Service.RegisterEmailConfirm {
+                       if ctx.Cache.IsExist("MailResendLimit_" + ctx.User.LowerName) {
+                               ctx.Data["ResendLimited"] = true
+                       } else {
+                               ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())
+                               mailer.SendActivateAccountMail(ctx.Locale, ctx.User)
+
+                               if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
+                                       log.Error("Set cache(MailResendLimit) fail: %v", err)
+                               }
+                       }
+               } else {
+                       ctx.Data["ServiceNotEnabled"] = true
+               }
+               ctx.HTML(http.StatusOK, TplActivate)
+               return
+       }
+
+       user := models.VerifyUserActiveCode(code)
+       // if code is wrong
+       if user == nil {
+               ctx.Data["IsActivateFailed"] = true
+               ctx.HTML(http.StatusOK, TplActivate)
+               return
+       }
+
+       // if account is local account, verify password
+       if user.LoginSource == 0 {
+               ctx.Data["Code"] = code
+               ctx.Data["NeedsPassword"] = true
+               ctx.HTML(http.StatusOK, TplActivate)
+               return
+       }
+
+       handleAccountActivation(ctx, user)
+}
+
+// ActivatePost handles account activation with password check
+func ActivatePost(ctx *context.Context) {
+       code := ctx.Query("code")
+       if len(code) == 0 {
+               ctx.Redirect(setting.AppSubURL + "/user/activate")
+               return
+       }
+
+       user := models.VerifyUserActiveCode(code)
+       // if code is wrong
+       if user == nil {
+               ctx.Data["IsActivateFailed"] = true
+               ctx.HTML(http.StatusOK, TplActivate)
+               return
+       }
+
+       // if account is local account, verify password
+       if user.LoginSource == 0 {
+               password := ctx.Query("password")
+               if len(password) == 0 {
+                       ctx.Data["Code"] = code
+                       ctx.Data["NeedsPassword"] = true
+                       ctx.HTML(http.StatusOK, TplActivate)
+                       return
+               }
+               if !user.ValidatePassword(password) {
+                       ctx.Data["IsActivateFailed"] = true
+                       ctx.HTML(http.StatusOK, TplActivate)
+                       return
+               }
+       }
+
+       handleAccountActivation(ctx, user)
+}
+
+func handleAccountActivation(ctx *context.Context, user *models.User) {
+       user.IsActive = true
+       var err error
+       if user.Rands, err = models.GetUserSalt(); err != nil {
+               ctx.ServerError("UpdateUser", err)
+               return
+       }
+       if err := models.UpdateUserCols(user, "is_active", "rands"); err != nil {
+               if models.IsErrUserNotExist(err) {
+                       ctx.NotFound("UpdateUserCols", err)
+               } else {
+                       ctx.ServerError("UpdateUser", err)
+               }
+               return
+       }
+
+       log.Trace("User activated: %s", user.Name)
+
+       if err := ctx.Session.Set("uid", user.ID); err != nil {
+               log.Error(fmt.Sprintf("Error setting uid in session: %v", err))
+       }
+       if err := ctx.Session.Set("uname", user.Name); err != nil {
+               log.Error(fmt.Sprintf("Error setting uname in session: %v", err))
+       }
+       if err := ctx.Session.Release(); err != nil {
+               log.Error("Error storing session: %v", err)
+       }
+
+       ctx.Flash.Success(ctx.Tr("auth.account_activated"))
+       ctx.Redirect(setting.AppSubURL + "/")
+}
+
+// ActivateEmail render the activate email page
+func ActivateEmail(ctx *context.Context) {
+       code := ctx.Query("code")
+       emailStr := ctx.Query("email")
+
+       // Verify code.
+       if email := models.VerifyActiveEmailCode(code, emailStr); email != nil {
+               if err := email.Activate(); err != nil {
+                       ctx.ServerError("ActivateEmail", err)
+               }
+
+               log.Trace("Email activated: %s", email.Email)
+               ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
+
+               if u, err := models.GetUserByID(email.UID); err != nil {
+                       log.Warn("GetUserByID: %d", email.UID)
+               } else {
+                       // Allow user to validate more emails
+                       _ = ctx.Cache.Delete("MailResendLimit_" + u.LowerName)
+               }
+       }
+
+       // FIXME: e-mail verification does not require the user to be logged in,
+       // so this could be redirecting to the login page.
+       // Should users be logged in automatically here? (consider 2FA requirements, etc.)
+       ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+}
+
+// ForgotPasswd render the forget pasword page
+func ForgotPasswd(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title")
+
+       if setting.MailService == nil {
+               ctx.Data["IsResetDisable"] = true
+               ctx.HTML(http.StatusOK, tplForgotPassword)
+               return
+       }
+
+       email := ctx.Query("email")
+       ctx.Data["Email"] = email
+
+       ctx.Data["IsResetRequest"] = true
+       ctx.HTML(http.StatusOK, tplForgotPassword)
+}
+
+// ForgotPasswdPost response for forget password request
+func ForgotPasswdPost(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title")
+
+       if setting.MailService == nil {
+               ctx.NotFound("ForgotPasswdPost", nil)
+               return
+       }
+       ctx.Data["IsResetRequest"] = true
+
+       email := ctx.Query("email")
+       ctx.Data["Email"] = email
+
+       u, err := models.GetUserByEmail(email)
+       if err != nil {
+               if models.IsErrUserNotExist(err) {
+                       ctx.Data["ResetPwdCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, ctx.Locale.Language())
+                       ctx.Data["IsResetSent"] = true
+                       ctx.HTML(http.StatusOK, tplForgotPassword)
+                       return
+               }
+
+               ctx.ServerError("user.ResetPasswd(check existence)", err)
+               return
+       }
+
+       if !u.IsLocal() && !u.IsOAuth2() {
+               ctx.Data["Err_Email"] = true
+               ctx.RenderWithErr(ctx.Tr("auth.non_local_account"), tplForgotPassword, nil)
+               return
+       }
+
+       if ctx.Cache.IsExist("MailResendLimit_" + u.LowerName) {
+               ctx.Data["ResendLimited"] = true
+               ctx.HTML(http.StatusOK, tplForgotPassword)
+               return
+       }
+
+       mailer.SendResetPasswordMail(u)
+
+       if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
+               log.Error("Set cache(MailResendLimit) fail: %v", err)
+       }
+
+       ctx.Data["ResetPwdCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, ctx.Locale.Language())
+       ctx.Data["IsResetSent"] = true
+       ctx.HTML(http.StatusOK, tplForgotPassword)
+}
+
+func commonResetPassword(ctx *context.Context) (*models.User, *models.TwoFactor) {
+       code := ctx.Query("code")
+
+       ctx.Data["Title"] = ctx.Tr("auth.reset_password")
+       ctx.Data["Code"] = code
+
+       if nil != ctx.User {
+               ctx.Data["user_signed_in"] = true
+       }
+
+       if len(code) == 0 {
+               ctx.Flash.Error(ctx.Tr("auth.invalid_code"))
+               return nil, nil
+       }
+
+       // Fail early, don't frustrate the user
+       u := models.VerifyUserActiveCode(code)
+       if u == nil {
+               ctx.Flash.Error(ctx.Tr("auth.invalid_code"))
+               return nil, nil
+       }
+
+       twofa, err := models.GetTwoFactorByUID(u.ID)
+       if err != nil {
+               if !models.IsErrTwoFactorNotEnrolled(err) {
+                       ctx.Error(http.StatusInternalServerError, "CommonResetPassword", err.Error())
+                       return nil, nil
+               }
+       } else {
+               ctx.Data["has_two_factor"] = true
+               ctx.Data["scratch_code"] = ctx.QueryBool("scratch_code")
+       }
+
+       // Show the user that they are affecting the account that they intended to
+       ctx.Data["user_email"] = u.Email
+
+       if nil != ctx.User && u.ID != ctx.User.ID {
+               ctx.Flash.Error(ctx.Tr("auth.reset_password_wrong_user", ctx.User.Email, u.Email))
+               return nil, nil
+       }
+
+       return u, twofa
+}
+
+// ResetPasswd render the account recovery page
+func ResetPasswd(ctx *context.Context) {
+       ctx.Data["IsResetForm"] = true
+
+       commonResetPassword(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       ctx.HTML(http.StatusOK, tplResetPassword)
+}
+
+// ResetPasswdPost response from account recovery request
+func ResetPasswdPost(ctx *context.Context) {
+       u, twofa := commonResetPassword(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       if u == nil {
+               // Flash error has been set
+               ctx.HTML(http.StatusOK, tplResetPassword)
+               return
+       }
+
+       // Validate password length.
+       passwd := ctx.Query("password")
+       if len(passwd) < setting.MinPasswordLength {
+               ctx.Data["IsResetForm"] = true
+               ctx.Data["Err_Password"] = true
+               ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil)
+               return
+       } else if !password.IsComplexEnough(passwd) {
+               ctx.Data["IsResetForm"] = true
+               ctx.Data["Err_Password"] = true
+               ctx.RenderWithErr(password.BuildComplexityError(ctx), tplResetPassword, nil)
+               return
+       } else if pwned, err := password.IsPwned(ctx, passwd); pwned || err != nil {
+               errMsg := ctx.Tr("auth.password_pwned")
+               if err != nil {
+                       log.Error(err.Error())
+                       errMsg = ctx.Tr("auth.password_pwned_err")
+               }
+               ctx.Data["IsResetForm"] = true
+               ctx.Data["Err_Password"] = true
+               ctx.RenderWithErr(errMsg, tplResetPassword, nil)
+               return
+       }
+
+       // Handle two-factor
+       regenerateScratchToken := false
+       if twofa != nil {
+               if ctx.QueryBool("scratch_code") {
+                       if !twofa.VerifyScratchToken(ctx.Query("token")) {
+                               ctx.Data["IsResetForm"] = true
+                               ctx.Data["Err_Token"] = true
+                               ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplResetPassword, nil)
+                               return
+                       }
+                       regenerateScratchToken = true
+               } else {
+                       passcode := ctx.Query("passcode")
+                       ok, err := twofa.ValidateTOTP(passcode)
+                       if err != nil {
+                               ctx.Error(http.StatusInternalServerError, "ValidateTOTP", err.Error())
+                               return
+                       }
+                       if !ok || twofa.LastUsedPasscode == passcode {
+                               ctx.Data["IsResetForm"] = true
+                               ctx.Data["Err_Passcode"] = true
+                               ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplResetPassword, nil)
+                               return
+                       }
+
+                       twofa.LastUsedPasscode = passcode
+                       if err = models.UpdateTwoFactor(twofa); err != nil {
+                               ctx.ServerError("ResetPasswdPost: UpdateTwoFactor", err)
+                               return
+                       }
+               }
+       }
+       var err error
+       if u.Rands, err = models.GetUserSalt(); err != nil {
+               ctx.ServerError("UpdateUser", err)
+               return
+       }
+       if err = u.SetPassword(passwd); err != nil {
+               ctx.ServerError("UpdateUser", err)
+               return
+       }
+       u.MustChangePassword = false
+       if err := models.UpdateUserCols(u, "must_change_password", "passwd", "passwd_hash_algo", "rands", "salt"); err != nil {
+               ctx.ServerError("UpdateUser", err)
+               return
+       }
+
+       log.Trace("User password reset: %s", u.Name)
+       ctx.Data["IsResetFailed"] = true
+       remember := len(ctx.Query("remember")) != 0
+
+       if regenerateScratchToken {
+               // Invalidate the scratch token.
+               _, err = twofa.GenerateScratchToken()
+               if err != nil {
+                       ctx.ServerError("UserSignIn", err)
+                       return
+               }
+               if err = models.UpdateTwoFactor(twofa); err != nil {
+                       ctx.ServerError("UserSignIn", err)
+                       return
+               }
+
+               handleSignInFull(ctx, u, remember, false)
+               ctx.Flash.Info(ctx.Tr("auth.twofa_scratch_used"))
+               ctx.Redirect(setting.AppSubURL + "/user/settings/security")
+               return
+       }
+
+       handleSignInFull(ctx, u, remember, true)
+}
+
+// MustChangePassword renders the page to change a user's password
+func MustChangePassword(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
+       ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/settings/change_password"
+       ctx.Data["MustChangePassword"] = true
+       ctx.HTML(http.StatusOK, tplMustChangePassword)
+}
+
+// MustChangePasswordPost response for updating a user's password after his/her
+// account was created by an admin
+func MustChangePasswordPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.MustChangePasswordForm)
+       ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
+       ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/settings/change_password"
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplMustChangePassword)
+               return
+       }
+       u := ctx.User
+       // Make sure only requests for users who are eligible to change their password via
+       // this method passes through
+       if !u.MustChangePassword {
+               ctx.ServerError("MustUpdatePassword", errors.New("cannot update password.. Please visit the settings page"))
+               return
+       }
+
+       if form.Password != form.Retype {
+               ctx.Data["Err_Password"] = true
+               ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplMustChangePassword, &form)
+               return
+       }
+
+       if len(form.Password) < setting.MinPasswordLength {
+               ctx.Data["Err_Password"] = true
+               ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form)
+               return
+       }
+
+       var err error
+       if err = u.SetPassword(form.Password); err != nil {
+               ctx.ServerError("UpdateUser", err)
+               return
+       }
+
+       u.MustChangePassword = false
+
+       if err := models.UpdateUserCols(u, "must_change_password", "passwd", "passwd_hash_algo", "salt"); err != nil {
+               ctx.ServerError("UpdateUser", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("settings.change_password_success"))
+
+       log.Trace("User updated password: %s", u.Name)
+
+       if redirectTo := ctx.GetCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
+               middleware.DeleteRedirectToCookie(ctx.Resp)
+               ctx.RedirectToFirst(redirectTo)
+               return
+       }
+
+       ctx.Redirect(setting.AppSubURL + "/")
+}
diff --git a/routers/web/user/auth_openid.go b/routers/web/user/auth_openid.go
new file mode 100644 (file)
index 0000000..1a73a08
--- /dev/null
@@ -0,0 +1,450 @@
+// 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 user
+
+import (
+       "fmt"
+       "net/http"
+       "net/url"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/auth/openid"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/hcaptcha"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/recaptcha"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/util"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/modules/web/middleware"
+       "code.gitea.io/gitea/services/forms"
+)
+
+const (
+       tplSignInOpenID base.TplName = "user/auth/signin_openid"
+       tplConnectOID   base.TplName = "user/auth/signup_openid_connect"
+       tplSignUpOID    base.TplName = "user/auth/signup_openid_register"
+)
+
+// SignInOpenID render sign in page
+func SignInOpenID(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("sign_in")
+
+       if ctx.Query("openid.return_to") != "" {
+               signInOpenIDVerify(ctx)
+               return
+       }
+
+       // Check auto-login.
+       isSucceed, err := AutoSignIn(ctx)
+       if err != nil {
+               ctx.ServerError("AutoSignIn", err)
+               return
+       }
+
+       redirectTo := ctx.Query("redirect_to")
+       if len(redirectTo) > 0 {
+               middleware.SetRedirectToCookie(ctx.Resp, redirectTo)
+       } else {
+               redirectTo = ctx.GetCookie("redirect_to")
+       }
+
+       if isSucceed {
+               middleware.DeleteRedirectToCookie(ctx.Resp)
+               ctx.RedirectToFirst(redirectTo)
+               return
+       }
+
+       ctx.Data["PageIsSignIn"] = true
+       ctx.Data["PageIsLoginOpenID"] = true
+       ctx.HTML(http.StatusOK, tplSignInOpenID)
+}
+
+// Check if the given OpenID URI is allowed by blacklist/whitelist
+func allowedOpenIDURI(uri string) (err error) {
+
+       // In case a Whitelist is present, URI must be in it
+       // in order to be accepted
+       if len(setting.Service.OpenIDWhitelist) != 0 {
+               for _, pat := range setting.Service.OpenIDWhitelist {
+                       if pat.MatchString(uri) {
+                               return nil // pass
+                       }
+               }
+               // must match one of this or be refused
+               return fmt.Errorf("URI not allowed by whitelist")
+       }
+
+       // A blacklist match expliclty forbids
+       for _, pat := range setting.Service.OpenIDBlacklist {
+               if pat.MatchString(uri) {
+                       return fmt.Errorf("URI forbidden by blacklist")
+               }
+       }
+
+       return nil
+}
+
+// SignInOpenIDPost response for openid sign in request
+func SignInOpenIDPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.SignInOpenIDForm)
+       ctx.Data["Title"] = ctx.Tr("sign_in")
+       ctx.Data["PageIsSignIn"] = true
+       ctx.Data["PageIsLoginOpenID"] = true
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplSignInOpenID)
+               return
+       }
+
+       id, err := openid.Normalize(form.Openid)
+       if err != nil {
+               ctx.RenderWithErr(err.Error(), tplSignInOpenID, &form)
+               return
+       }
+       form.Openid = id
+
+       log.Trace("OpenID uri: " + id)
+
+       err = allowedOpenIDURI(id)
+       if err != nil {
+               ctx.RenderWithErr(err.Error(), tplSignInOpenID, &form)
+               return
+       }
+
+       redirectTo := setting.AppURL + "user/login/openid"
+       url, err := openid.RedirectURL(id, redirectTo, setting.AppURL)
+       if err != nil {
+               log.Error("Error in OpenID redirect URL: %s, %v", redirectTo, err.Error())
+               ctx.RenderWithErr(fmt.Sprintf("Unable to find OpenID provider in %s", redirectTo), tplSignInOpenID, &form)
+               return
+       }
+
+       // Request optional nickname and email info
+       // NOTE: change to `openid.sreg.required` to require it
+       url += "&openid.ns.sreg=http%3A%2F%2Fopenid.net%2Fextensions%2Fsreg%2F1.1"
+       url += "&openid.sreg.optional=nickname%2Cemail"
+
+       log.Trace("Form-passed openid-remember: %t", form.Remember)
+
+       if err := ctx.Session.Set("openid_signin_remember", form.Remember); err != nil {
+               log.Error("SignInOpenIDPost: Could not set openid_signin_remember in session: %v", err)
+       }
+       if err := ctx.Session.Release(); err != nil {
+               log.Error("SignInOpenIDPost: Unable to save changes to the session: %v", err)
+       }
+
+       ctx.Redirect(url)
+}
+
+// signInOpenIDVerify handles response from OpenID provider
+func signInOpenIDVerify(ctx *context.Context) {
+
+       log.Trace("Incoming call to: " + ctx.Req.URL.String())
+
+       fullURL := setting.AppURL + ctx.Req.URL.String()[1:]
+       log.Trace("Full URL: " + fullURL)
+
+       var id, err = openid.Verify(fullURL)
+       if err != nil {
+               ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
+                       Openid: id,
+               })
+               return
+       }
+
+       log.Trace("Verified ID: " + id)
+
+       /* Now we should seek for the user and log him in, or prompt
+        * to register if not found */
+
+       u, err := models.GetUserByOpenID(id)
+       if err != nil {
+               if !models.IsErrUserNotExist(err) {
+                       ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
+                               Openid: id,
+                       })
+                       return
+               }
+               log.Error("signInOpenIDVerify: %v", err)
+       }
+       if u != nil {
+               log.Trace("User exists, logging in")
+               remember, _ := ctx.Session.Get("openid_signin_remember").(bool)
+               log.Trace("Session stored openid-remember: %t", remember)
+               handleSignIn(ctx, u, remember)
+               return
+       }
+
+       log.Trace("User with openid " + id + " does not exist, should connect or register")
+
+       parsedURL, err := url.Parse(fullURL)
+       if err != nil {
+               ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
+                       Openid: id,
+               })
+               return
+       }
+       values, err := url.ParseQuery(parsedURL.RawQuery)
+       if err != nil {
+               ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
+                       Openid: id,
+               })
+               return
+       }
+       email := values.Get("openid.sreg.email")
+       nickname := values.Get("openid.sreg.nickname")
+
+       log.Trace("User has email=" + email + " and nickname=" + nickname)
+
+       if email != "" {
+               u, err = models.GetUserByEmail(email)
+               if err != nil {
+                       if !models.IsErrUserNotExist(err) {
+                               ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
+                                       Openid: id,
+                               })
+                               return
+                       }
+                       log.Error("signInOpenIDVerify: %v", err)
+               }
+               if u != nil {
+                       log.Trace("Local user " + u.LowerName + " has OpenID provided email " + email)
+               }
+       }
+
+       if u == nil && nickname != "" {
+               u, _ = models.GetUserByName(nickname)
+               if err != nil {
+                       if !models.IsErrUserNotExist(err) {
+                               ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
+                                       Openid: id,
+                               })
+                               return
+                       }
+               }
+               if u != nil {
+                       log.Trace("Local user " + u.LowerName + " has OpenID provided nickname " + nickname)
+               }
+       }
+
+       if err := ctx.Session.Set("openid_verified_uri", id); err != nil {
+               log.Error("signInOpenIDVerify: Could not set openid_verified_uri in session: %v", err)
+       }
+       if err := ctx.Session.Set("openid_determined_email", email); err != nil {
+               log.Error("signInOpenIDVerify: Could not set openid_determined_email in session: %v", err)
+       }
+
+       if u != nil {
+               nickname = u.LowerName
+       }
+
+       if err := ctx.Session.Set("openid_determined_username", nickname); err != nil {
+               log.Error("signInOpenIDVerify: Could not set openid_determined_username in session: %v", err)
+       }
+       if err := ctx.Session.Release(); err != nil {
+               log.Error("signInOpenIDVerify: Unable to save changes to the session: %v", err)
+       }
+
+       if u != nil || !setting.Service.EnableOpenIDSignUp || setting.Service.AllowOnlyInternalRegistration {
+               ctx.Redirect(setting.AppSubURL + "/user/openid/connect")
+       } else {
+               ctx.Redirect(setting.AppSubURL + "/user/openid/register")
+       }
+}
+
+// ConnectOpenID shows a form to connect an OpenID URI to an existing account
+func ConnectOpenID(ctx *context.Context) {
+       oid, _ := ctx.Session.Get("openid_verified_uri").(string)
+       if oid == "" {
+               ctx.Redirect(setting.AppSubURL + "/user/login/openid")
+               return
+       }
+       ctx.Data["Title"] = "OpenID connect"
+       ctx.Data["PageIsSignIn"] = true
+       ctx.Data["PageIsOpenIDConnect"] = true
+       ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
+       ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
+       ctx.Data["OpenID"] = oid
+       userName, _ := ctx.Session.Get("openid_determined_username").(string)
+       if userName != "" {
+               ctx.Data["user_name"] = userName
+       }
+       ctx.HTML(http.StatusOK, tplConnectOID)
+}
+
+// ConnectOpenIDPost handles submission of a form to connect an OpenID URI to an existing account
+func ConnectOpenIDPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.ConnectOpenIDForm)
+       oid, _ := ctx.Session.Get("openid_verified_uri").(string)
+       if oid == "" {
+               ctx.Redirect(setting.AppSubURL + "/user/login/openid")
+               return
+       }
+       ctx.Data["Title"] = "OpenID connect"
+       ctx.Data["PageIsSignIn"] = true
+       ctx.Data["PageIsOpenIDConnect"] = true
+       ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
+       ctx.Data["OpenID"] = oid
+
+       u, err := models.UserSignIn(form.UserName, form.Password)
+       if err != nil {
+               if models.IsErrUserNotExist(err) {
+                       ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplConnectOID, &form)
+               } else {
+                       ctx.ServerError("ConnectOpenIDPost", err)
+               }
+               return
+       }
+
+       // add OpenID for the user
+       userOID := &models.UserOpenID{UID: u.ID, URI: oid}
+       if err = models.AddUserOpenID(userOID); err != nil {
+               if models.IsErrOpenIDAlreadyUsed(err) {
+                       ctx.RenderWithErr(ctx.Tr("form.openid_been_used", oid), tplConnectOID, &form)
+                       return
+               }
+               ctx.ServerError("AddUserOpenID", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("settings.add_openid_success"))
+
+       remember, _ := ctx.Session.Get("openid_signin_remember").(bool)
+       log.Trace("Session stored openid-remember: %t", remember)
+       handleSignIn(ctx, u, remember)
+}
+
+// RegisterOpenID shows a form to create a new user authenticated via an OpenID URI
+func RegisterOpenID(ctx *context.Context) {
+       oid, _ := ctx.Session.Get("openid_verified_uri").(string)
+       if oid == "" {
+               ctx.Redirect(setting.AppSubURL + "/user/login/openid")
+               return
+       }
+       ctx.Data["Title"] = "OpenID signup"
+       ctx.Data["PageIsSignIn"] = true
+       ctx.Data["PageIsOpenIDRegister"] = true
+       ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
+       ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
+       ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
+       ctx.Data["Captcha"] = context.GetImageCaptcha()
+       ctx.Data["CaptchaType"] = setting.Service.CaptchaType
+       ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
+       ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
+       ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
+       ctx.Data["OpenID"] = oid
+       userName, _ := ctx.Session.Get("openid_determined_username").(string)
+       if userName != "" {
+               ctx.Data["user_name"] = userName
+       }
+       email, _ := ctx.Session.Get("openid_determined_email").(string)
+       if email != "" {
+               ctx.Data["email"] = email
+       }
+       ctx.HTML(http.StatusOK, tplSignUpOID)
+}
+
+// RegisterOpenIDPost handles submission of a form to create a new user authenticated via an OpenID URI
+func RegisterOpenIDPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.SignUpOpenIDForm)
+       oid, _ := ctx.Session.Get("openid_verified_uri").(string)
+       if oid == "" {
+               ctx.Redirect(setting.AppSubURL + "/user/login/openid")
+               return
+       }
+
+       ctx.Data["Title"] = "OpenID signup"
+       ctx.Data["PageIsSignIn"] = true
+       ctx.Data["PageIsOpenIDRegister"] = true
+       ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
+       ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
+       ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
+       ctx.Data["Captcha"] = context.GetImageCaptcha()
+       ctx.Data["CaptchaType"] = setting.Service.CaptchaType
+       ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
+       ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
+       ctx.Data["OpenID"] = oid
+
+       if setting.Service.AllowOnlyInternalRegistration {
+               ctx.Error(http.StatusForbidden)
+               return
+       }
+
+       if setting.Service.EnableCaptcha {
+               var valid bool
+               var err error
+               switch setting.Service.CaptchaType {
+               case setting.ImageCaptcha:
+                       valid = context.GetImageCaptcha().VerifyReq(ctx.Req)
+               case setting.ReCaptcha:
+                       if err := ctx.Req.ParseForm(); err != nil {
+                               ctx.ServerError("", err)
+                               return
+                       }
+                       valid, err = recaptcha.Verify(ctx, form.GRecaptchaResponse)
+               case setting.HCaptcha:
+                       if err := ctx.Req.ParseForm(); err != nil {
+                               ctx.ServerError("", err)
+                               return
+                       }
+                       valid, err = hcaptcha.Verify(ctx, form.HcaptchaResponse)
+               default:
+                       ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
+                       return
+               }
+               if err != nil {
+                       log.Debug("%s", err.Error())
+               }
+
+               if !valid {
+                       ctx.Data["Err_Captcha"] = true
+                       ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplSignUpOID, &form)
+                       return
+               }
+       }
+
+       length := setting.MinPasswordLength
+       if length < 256 {
+               length = 256
+       }
+       password, err := util.RandomString(int64(length))
+       if err != nil {
+               ctx.RenderWithErr(err.Error(), tplSignUpOID, form)
+               return
+       }
+
+       u := &models.User{
+               Name:     form.UserName,
+               Email:    form.Email,
+               Passwd:   password,
+               IsActive: !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm),
+       }
+       if !createUserInContext(ctx, tplSignUpOID, form, u, nil, false) {
+               // error already handled
+               return
+       }
+
+       // add OpenID for the user
+       userOID := &models.UserOpenID{UID: u.ID, URI: oid}
+       if err = models.AddUserOpenID(userOID); err != nil {
+               if models.IsErrOpenIDAlreadyUsed(err) {
+                       ctx.RenderWithErr(ctx.Tr("form.openid_been_used", oid), tplSignUpOID, &form)
+                       return
+               }
+               ctx.ServerError("AddUserOpenID", err)
+               return
+       }
+
+       if !handleUserCreated(ctx, u, nil) {
+               // error already handled
+               return
+       }
+
+       remember, _ := ctx.Session.Get("openid_signin_remember").(bool)
+       log.Trace("Session stored openid-remember: %t", remember)
+       handleSignIn(ctx, u, remember)
+}
diff --git a/routers/web/user/avatar.go b/routers/web/user/avatar.go
new file mode 100644 (file)
index 0000000..4287589
--- /dev/null
@@ -0,0 +1,98 @@
+// Copyright 2019 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 user
+
+import (
+       "errors"
+       "net/url"
+       "path"
+       "strconv"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+)
+
+// Avatar redirect browser to user avatar of requested size
+func Avatar(ctx *context.Context) {
+       userName := ctx.Params(":username")
+       size, err := strconv.Atoi(ctx.Params(":size"))
+       if err != nil {
+               ctx.ServerError("Invalid avatar size", err)
+               return
+       }
+
+       log.Debug("Asked avatar for user %v and size %v", userName, size)
+
+       var user *models.User
+       if strings.ToLower(userName) != "ghost" {
+               user, err = models.GetUserByName(userName)
+               if err != nil {
+                       if models.IsErrUserNotExist(err) {
+                               ctx.ServerError("Requested avatar for invalid user", err)
+                       } else {
+                               ctx.ServerError("Retrieving user by name", err)
+                       }
+                       return
+               }
+       } else {
+               user = models.NewGhostUser()
+       }
+
+       ctx.Redirect(user.RealSizedAvatarLink(size))
+}
+
+// AvatarByEmailHash redirects the browser to the appropriate Avatar link
+func AvatarByEmailHash(ctx *context.Context) {
+       var err error
+
+       hash := ctx.Params(":hash")
+       if len(hash) == 0 {
+               ctx.ServerError("invalid avatar hash", errors.New("hash cannot be empty"))
+               return
+       }
+
+       var email string
+       email, err = models.GetEmailForHash(hash)
+       if err != nil {
+               ctx.ServerError("invalid avatar hash", err)
+               return
+       }
+       if len(email) == 0 {
+               ctx.Redirect(models.DefaultAvatarLink())
+               return
+       }
+       size := ctx.QueryInt("size")
+       if size == 0 {
+               size = models.DefaultAvatarSize
+       }
+
+       var avatarURL *url.URL
+
+       if setting.EnableFederatedAvatar && setting.LibravatarService != nil {
+               avatarURL, err = models.LibravatarURL(email)
+               if err != nil {
+                       avatarURL, err = url.Parse(models.DefaultAvatarLink())
+                       if err != nil {
+                               ctx.ServerError("invalid default avatar url", err)
+                               return
+                       }
+               }
+       } else if !setting.DisableGravatar {
+               copyOfGravatarSourceURL := *setting.GravatarSourceURL
+               avatarURL = &copyOfGravatarSourceURL
+               avatarURL.Path = path.Join(avatarURL.Path, hash)
+       } else {
+               avatarURL, err = url.Parse(models.DefaultAvatarLink())
+               if err != nil {
+                       ctx.ServerError("invalid default avatar url", err)
+                       return
+               }
+       }
+
+       ctx.Redirect(models.MakeFinalAvatarURL(avatarURL, size))
+}
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
new file mode 100644 (file)
index 0000000..acf73f8
--- /dev/null
@@ -0,0 +1,913 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2019 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 user
+
+import (
+       "bytes"
+       "fmt"
+       "net/http"
+       "regexp"
+       "sort"
+       "strconv"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/markup"
+       "code.gitea.io/gitea/modules/markup/markdown"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/util"
+       issue_service "code.gitea.io/gitea/services/issue"
+       pull_service "code.gitea.io/gitea/services/pull"
+
+       jsoniter "github.com/json-iterator/go"
+       "github.com/keybase/go-crypto/openpgp"
+       "github.com/keybase/go-crypto/openpgp/armor"
+       "xorm.io/builder"
+)
+
+const (
+       tplDashboard  base.TplName = "user/dashboard/dashboard"
+       tplIssues     base.TplName = "user/dashboard/issues"
+       tplMilestones base.TplName = "user/dashboard/milestones"
+       tplProfile    base.TplName = "user/profile"
+)
+
+// getDashboardContextUser finds out which context user dashboard is being viewed as .
+func getDashboardContextUser(ctx *context.Context) *models.User {
+       ctxUser := ctx.User
+       orgName := ctx.Params(":org")
+       if len(orgName) > 0 {
+               ctxUser = ctx.Org.Organization
+               ctx.Data["Teams"] = ctx.Org.Organization.Teams
+       }
+       ctx.Data["ContextUser"] = ctxUser
+
+       if err := ctx.User.GetOrganizations(&models.SearchOrganizationsOptions{All: true}); err != nil {
+               ctx.ServerError("GetOrganizations", err)
+               return nil
+       }
+       ctx.Data["Orgs"] = ctx.User.Orgs
+
+       return ctxUser
+}
+
+// retrieveFeeds loads feeds for the specified user
+func retrieveFeeds(ctx *context.Context, options models.GetFeedsOptions) {
+       actions, err := models.GetFeeds(options)
+       if err != nil {
+               ctx.ServerError("GetFeeds", err)
+               return
+       }
+
+       userCache := map[int64]*models.User{options.RequestedUser.ID: options.RequestedUser}
+       if ctx.User != nil {
+               userCache[ctx.User.ID] = ctx.User
+       }
+       for _, act := range actions {
+               if act.ActUser != nil {
+                       userCache[act.ActUserID] = act.ActUser
+               }
+       }
+
+       for _, act := range actions {
+               repoOwner, ok := userCache[act.Repo.OwnerID]
+               if !ok {
+                       repoOwner, err = models.GetUserByID(act.Repo.OwnerID)
+                       if err != nil {
+                               if models.IsErrUserNotExist(err) {
+                                       continue
+                               }
+                               ctx.ServerError("GetUserByID", err)
+                               return
+                       }
+                       userCache[repoOwner.ID] = repoOwner
+               }
+               act.Repo.Owner = repoOwner
+       }
+       ctx.Data["Feeds"] = actions
+}
+
+// Dashboard render the dashboard page
+func Dashboard(ctx *context.Context) {
+       ctxUser := getDashboardContextUser(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       ctx.Data["Title"] = ctxUser.DisplayName() + " - " + ctx.Tr("dashboard")
+       ctx.Data["PageIsDashboard"] = true
+       ctx.Data["PageIsNews"] = true
+       ctx.Data["SearchLimit"] = setting.UI.User.RepoPagingNum
+
+       if setting.Service.EnableUserHeatmap {
+               data, err := models.GetUserHeatmapDataByUserTeam(ctxUser, ctx.Org.Team, ctx.User)
+               if err != nil {
+                       ctx.ServerError("GetUserHeatmapDataByUserTeam", err)
+                       return
+               }
+               ctx.Data["HeatmapData"] = data
+       }
+
+       var err error
+       var mirrors []*models.Repository
+       if ctxUser.IsOrganization() {
+               var env models.AccessibleReposEnvironment
+               if ctx.Org.Team != nil {
+                       env = ctxUser.AccessibleTeamReposEnv(ctx.Org.Team)
+               } else {
+                       env, err = ctxUser.AccessibleReposEnv(ctx.User.ID)
+                       if err != nil {
+                               ctx.ServerError("AccessibleReposEnv", err)
+                               return
+                       }
+               }
+               mirrors, err = env.MirrorRepos()
+               if err != nil {
+                       ctx.ServerError("env.MirrorRepos", err)
+                       return
+               }
+       } else {
+               mirrors, err = ctxUser.GetMirrorRepositories()
+               if err != nil {
+                       ctx.ServerError("GetMirrorRepositories", err)
+                       return
+               }
+       }
+       ctx.Data["MaxShowRepoNum"] = setting.UI.User.RepoPagingNum
+
+       if err := models.MirrorRepositoryList(mirrors).LoadAttributes(); err != nil {
+               ctx.ServerError("MirrorRepositoryList.LoadAttributes", err)
+               return
+       }
+       ctx.Data["MirrorCount"] = len(mirrors)
+       ctx.Data["Mirrors"] = mirrors
+
+       retrieveFeeds(ctx, models.GetFeedsOptions{
+               RequestedUser:   ctxUser,
+               RequestedTeam:   ctx.Org.Team,
+               Actor:           ctx.User,
+               IncludePrivate:  true,
+               OnlyPerformedBy: false,
+               IncludeDeleted:  false,
+               Date:            ctx.Query("date"),
+       })
+
+       if ctx.Written() {
+               return
+       }
+       ctx.HTML(http.StatusOK, tplDashboard)
+}
+
+// Milestones render the user milestones page
+func Milestones(ctx *context.Context) {
+       if models.UnitTypeIssues.UnitGlobalDisabled() && models.UnitTypePullRequests.UnitGlobalDisabled() {
+               log.Debug("Milestones overview page not available as both issues and pull requests are globally disabled")
+               ctx.Status(404)
+               return
+       }
+
+       ctx.Data["Title"] = ctx.Tr("milestones")
+       ctx.Data["PageIsMilestonesDashboard"] = true
+
+       ctxUser := getDashboardContextUser(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       repoOpts := models.SearchRepoOptions{
+               Actor:         ctxUser,
+               OwnerID:       ctxUser.ID,
+               Private:       true,
+               AllPublic:     false,                 // Include also all public repositories of users and public organisations
+               AllLimited:    false,                 // Include also all public repositories of limited organisations
+               HasMilestones: util.OptionalBoolTrue, // Just needs display repos has milestones
+       }
+
+       if ctxUser.IsOrganization() && ctx.Org.Team != nil {
+               repoOpts.TeamID = ctx.Org.Team.ID
+       }
+
+       var (
+               userRepoCond = models.SearchRepositoryCondition(&repoOpts) // all repo condition user could visit
+               repoCond     = userRepoCond
+               repoIDs      []int64
+
+               reposQuery   = ctx.Query("repos")
+               isShowClosed = ctx.Query("state") == "closed"
+               sortType     = ctx.Query("sort")
+               page         = ctx.QueryInt("page")
+               keyword      = strings.Trim(ctx.Query("q"), " ")
+       )
+
+       if page <= 1 {
+               page = 1
+       }
+
+       if len(reposQuery) != 0 {
+               if issueReposQueryPattern.MatchString(reposQuery) {
+                       // remove "[" and "]" from string
+                       reposQuery = reposQuery[1 : len(reposQuery)-1]
+                       //for each ID (delimiter ",") add to int to repoIDs
+
+                       for _, rID := range strings.Split(reposQuery, ",") {
+                               // Ensure nonempty string entries
+                               if rID != "" && rID != "0" {
+                                       rIDint64, err := strconv.ParseInt(rID, 10, 64)
+                                       // If the repo id specified by query is not parseable or not accessible by user, just ignore it.
+                                       if err == nil {
+                                               repoIDs = append(repoIDs, rIDint64)
+                                       }
+                               }
+                       }
+                       if len(repoIDs) > 0 {
+                               // Don't just let repoCond = builder.In("id", repoIDs) because user may has no permission on repoIDs
+                               // But the original repoCond has a limitation
+                               repoCond = repoCond.And(builder.In("id", repoIDs))
+                       }
+               } else {
+                       log.Warn("issueReposQueryPattern not match with query")
+               }
+       }
+
+       counts, err := models.CountMilestonesByRepoCondAndKw(userRepoCond, keyword, isShowClosed)
+       if err != nil {
+               ctx.ServerError("CountMilestonesByRepoIDs", err)
+               return
+       }
+
+       milestones, err := models.SearchMilestones(repoCond, page, isShowClosed, sortType, keyword)
+       if err != nil {
+               ctx.ServerError("SearchMilestones", err)
+               return
+       }
+
+       showRepos, _, err := models.SearchRepositoryByCondition(&repoOpts, userRepoCond, false)
+       if err != nil {
+               ctx.ServerError("SearchRepositoryByCondition", err)
+               return
+       }
+       sort.Sort(showRepos)
+
+       for i := 0; i < len(milestones); {
+               for _, repo := range showRepos {
+                       if milestones[i].RepoID == repo.ID {
+                               milestones[i].Repo = repo
+                               break
+                       }
+               }
+               if milestones[i].Repo == nil {
+                       log.Warn("Cannot find milestone %d 's repository %d", milestones[i].ID, milestones[i].RepoID)
+                       milestones = append(milestones[:i], milestones[i+1:]...)
+                       continue
+               }
+
+               milestones[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{
+                       URLPrefix: milestones[i].Repo.Link(),
+                       Metas:     milestones[i].Repo.ComposeMetas(),
+               }, milestones[i].Content)
+               if err != nil {
+                       ctx.ServerError("RenderString", err)
+                       return
+               }
+
+               if milestones[i].Repo.IsTimetrackerEnabled() {
+                       err := milestones[i].LoadTotalTrackedTime()
+                       if err != nil {
+                               ctx.ServerError("LoadTotalTrackedTime", err)
+                               return
+                       }
+               }
+               i++
+       }
+
+       milestoneStats, err := models.GetMilestonesStatsByRepoCondAndKw(repoCond, keyword)
+       if err != nil {
+               ctx.ServerError("GetMilestoneStats", err)
+               return
+       }
+
+       var totalMilestoneStats *models.MilestonesStats
+       if len(repoIDs) == 0 {
+               totalMilestoneStats = milestoneStats
+       } else {
+               totalMilestoneStats, err = models.GetMilestonesStatsByRepoCondAndKw(userRepoCond, keyword)
+               if err != nil {
+                       ctx.ServerError("GetMilestoneStats", err)
+                       return
+               }
+       }
+
+       var pagerCount int
+       if isShowClosed {
+               ctx.Data["State"] = "closed"
+               ctx.Data["Total"] = totalMilestoneStats.ClosedCount
+               pagerCount = int(milestoneStats.ClosedCount)
+       } else {
+               ctx.Data["State"] = "open"
+               ctx.Data["Total"] = totalMilestoneStats.OpenCount
+               pagerCount = int(milestoneStats.OpenCount)
+       }
+
+       ctx.Data["Milestones"] = milestones
+       ctx.Data["Repos"] = showRepos
+       ctx.Data["Counts"] = counts
+       ctx.Data["MilestoneStats"] = milestoneStats
+       ctx.Data["SortType"] = sortType
+       ctx.Data["Keyword"] = keyword
+       if milestoneStats.Total() != totalMilestoneStats.Total() {
+               ctx.Data["RepoIDs"] = repoIDs
+       }
+       ctx.Data["IsShowClosed"] = isShowClosed
+
+       pager := context.NewPagination(pagerCount, setting.UI.IssuePagingNum, page, 5)
+       pager.AddParam(ctx, "q", "Keyword")
+       pager.AddParam(ctx, "repos", "RepoIDs")
+       pager.AddParam(ctx, "sort", "SortType")
+       pager.AddParam(ctx, "state", "State")
+       ctx.Data["Page"] = pager
+
+       ctx.HTML(http.StatusOK, tplMilestones)
+}
+
+// Pulls renders the user's pull request overview page
+func Pulls(ctx *context.Context) {
+       if models.UnitTypePullRequests.UnitGlobalDisabled() {
+               log.Debug("Pull request overview page not available as it is globally disabled.")
+               ctx.Status(404)
+               return
+       }
+
+       ctx.Data["Title"] = ctx.Tr("pull_requests")
+       ctx.Data["PageIsPulls"] = true
+       buildIssueOverview(ctx, models.UnitTypePullRequests)
+}
+
+// Issues renders the user's issues overview page
+func Issues(ctx *context.Context) {
+       if models.UnitTypeIssues.UnitGlobalDisabled() {
+               log.Debug("Issues overview page not available as it is globally disabled.")
+               ctx.Status(404)
+               return
+       }
+
+       ctx.Data["Title"] = ctx.Tr("issues")
+       ctx.Data["PageIsIssues"] = true
+       buildIssueOverview(ctx, models.UnitTypeIssues)
+}
+
+// Regexp for repos query
+var issueReposQueryPattern = regexp.MustCompile(`^\[\d+(,\d+)*,?\]$`)
+
+func buildIssueOverview(ctx *context.Context, unitType models.UnitType) {
+
+       // ----------------------------------------------------
+       // Determine user; can be either user or organization.
+       // Return with NotFound or ServerError if unsuccessful.
+       // ----------------------------------------------------
+
+       ctxUser := getDashboardContextUser(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       var (
+               viewType   string
+               sortType   = ctx.Query("sort")
+               filterMode = models.FilterModeAll
+       )
+
+       // --------------------------------------------------------------------------------
+       // Distinguish User from Organization.
+       // Org:
+       // - Remember pre-determined viewType string for later. Will be posted to ctx.Data.
+       //   Organization does not have view type and filter mode.
+       // User:
+       // - Use ctx.Query("type") to determine filterMode.
+       //  The type is set when clicking for example "assigned to me" on the overview page.
+       // - Remember either this or a fallback. Will be posted to ctx.Data.
+       // --------------------------------------------------------------------------------
+
+       // TODO: distinguish during routing
+
+       viewType = ctx.Query("type")
+       switch viewType {
+       case "assigned":
+               filterMode = models.FilterModeAssign
+       case "created_by":
+               filterMode = models.FilterModeCreate
+       case "mentioned":
+               filterMode = models.FilterModeMention
+       case "review_requested":
+               filterMode = models.FilterModeReviewRequested
+       case "your_repositories": // filterMode already set to All
+       default:
+               viewType = "your_repositories"
+       }
+
+       // --------------------------------------------------------------------------
+       // Build opts (IssuesOptions), which contains filter information.
+       // Will eventually be used to retrieve issues relevant for the overview page.
+       // Note: Non-final states of opts are used in-between, namely for:
+       //       - Keyword search
+       //       - Count Issues by repo
+       // --------------------------------------------------------------------------
+
+       isPullList := unitType == models.UnitTypePullRequests
+       opts := &models.IssuesOptions{
+               IsPull:     util.OptionalBoolOf(isPullList),
+               SortType:   sortType,
+               IsArchived: util.OptionalBoolFalse,
+       }
+
+       // Get repository IDs where User/Org/Team has access.
+       var team *models.Team
+       if ctx.Org != nil {
+               team = ctx.Org.Team
+       }
+       userRepoIDs, err := getActiveUserRepoIDs(ctxUser, team, unitType)
+       if err != nil {
+               ctx.ServerError("userRepoIDs", err)
+               return
+       }
+
+       switch filterMode {
+       case models.FilterModeAll:
+               opts.RepoIDs = userRepoIDs
+       case models.FilterModeAssign:
+               opts.AssigneeID = ctx.User.ID
+       case models.FilterModeCreate:
+               opts.PosterID = ctx.User.ID
+       case models.FilterModeMention:
+               opts.MentionedID = ctx.User.ID
+       case models.FilterModeReviewRequested:
+               opts.ReviewRequestedID = ctx.User.ID
+       }
+
+       if ctxUser.IsOrganization() {
+               opts.RepoIDs = userRepoIDs
+       }
+
+       // keyword holds the search term entered into the search field.
+       keyword := strings.Trim(ctx.Query("q"), " ")
+       ctx.Data["Keyword"] = keyword
+
+       // Execute keyword search for issues.
+       // USING NON-FINAL STATE OF opts FOR A QUERY.
+       issueIDsFromSearch, err := issueIDsFromSearch(ctxUser, keyword, opts)
+       if err != nil {
+               ctx.ServerError("issueIDsFromSearch", err)
+               return
+       }
+
+       // Ensure no issues are returned if a keyword was provided that didn't match any issues.
+       var forceEmpty bool
+
+       if len(issueIDsFromSearch) > 0 {
+               opts.IssueIDs = issueIDsFromSearch
+       } else if len(keyword) > 0 {
+               forceEmpty = true
+       }
+
+       // Educated guess: Do or don't show closed issues.
+       isShowClosed := ctx.Query("state") == "closed"
+       opts.IsClosed = util.OptionalBoolOf(isShowClosed)
+
+       // Filter repos and count issues in them. Count will be used later.
+       // USING NON-FINAL STATE OF opts FOR A QUERY.
+       var issueCountByRepo map[int64]int64
+       if !forceEmpty {
+               issueCountByRepo, err = models.CountIssuesByRepo(opts)
+               if err != nil {
+                       ctx.ServerError("CountIssuesByRepo", err)
+                       return
+               }
+       }
+
+       // Make sure page number is at least 1. Will be posted to ctx.Data.
+       page := ctx.QueryInt("page")
+       if page <= 1 {
+               page = 1
+       }
+       opts.Page = page
+       opts.PageSize = setting.UI.IssuePagingNum
+
+       // Get IDs for labels (a filter option for issues/pulls).
+       // Required for IssuesOptions.
+       var labelIDs []int64
+       selectedLabels := ctx.Query("labels")
+       if len(selectedLabels) > 0 && selectedLabels != "0" {
+               labelIDs, err = base.StringsToInt64s(strings.Split(selectedLabels, ","))
+               if err != nil {
+                       ctx.ServerError("StringsToInt64s", err)
+                       return
+               }
+       }
+       opts.LabelIDs = labelIDs
+
+       // Parse ctx.Query("repos") and remember matched repo IDs for later.
+       // Gets set when clicking filters on the issues overview page.
+       repoIDs := getRepoIDs(ctx.Query("repos"))
+       if len(repoIDs) > 0 {
+               opts.RepoIDs = repoIDs
+       }
+
+       // ------------------------------
+       // Get issues as defined by opts.
+       // ------------------------------
+
+       // Slice of Issues that will be displayed on the overview page
+       // USING FINAL STATE OF opts FOR A QUERY.
+       var issues []*models.Issue
+       if !forceEmpty {
+               issues, err = models.Issues(opts)
+               if err != nil {
+                       ctx.ServerError("Issues", err)
+                       return
+               }
+       } else {
+               issues = []*models.Issue{}
+       }
+
+       // ----------------------------------
+       // Add repository pointers to Issues.
+       // ----------------------------------
+
+       // showReposMap maps repository IDs to their Repository pointers.
+       showReposMap, err := repoIDMap(ctxUser, issueCountByRepo, unitType)
+       if err != nil {
+               if models.IsErrRepoNotExist(err) {
+                       ctx.NotFound("GetRepositoryByID", err)
+                       return
+               }
+               ctx.ServerError("repoIDMap", err)
+               return
+       }
+
+       // a RepositoryList
+       showRepos := models.RepositoryListOfMap(showReposMap)
+       sort.Sort(showRepos)
+       if err = showRepos.LoadAttributes(); err != nil {
+               ctx.ServerError("LoadAttributes", err)
+               return
+       }
+
+       // maps pull request IDs to their CommitStatus. Will be posted to ctx.Data.
+       for _, issue := range issues {
+               issue.Repo = showReposMap[issue.RepoID]
+       }
+
+       commitStatus, err := pull_service.GetIssuesLastCommitStatus(issues)
+       if err != nil {
+               ctx.ServerError("GetIssuesLastCommitStatus", err)
+               return
+       }
+
+       // -------------------------------
+       // Fill stats to post to ctx.Data.
+       // -------------------------------
+
+       userIssueStatsOpts := models.UserIssueStatsOptions{
+               UserID:      ctx.User.ID,
+               UserRepoIDs: userRepoIDs,
+               FilterMode:  filterMode,
+               IsPull:      isPullList,
+               IsClosed:    isShowClosed,
+               IsArchived:  util.OptionalBoolFalse,
+               LabelIDs:    opts.LabelIDs,
+       }
+       if len(repoIDs) > 0 {
+               userIssueStatsOpts.UserRepoIDs = repoIDs
+       }
+       if ctxUser.IsOrganization() {
+               userIssueStatsOpts.RepoIDs = userRepoIDs
+       }
+       userIssueStats, err := models.GetUserIssueStats(userIssueStatsOpts)
+       if err != nil {
+               ctx.ServerError("GetUserIssueStats User", err)
+               return
+       }
+
+       var shownIssueStats *models.IssueStats
+       if !forceEmpty {
+               statsOpts := models.UserIssueStatsOptions{
+                       UserID:      ctx.User.ID,
+                       UserRepoIDs: userRepoIDs,
+                       FilterMode:  filterMode,
+                       IsPull:      isPullList,
+                       IsClosed:    isShowClosed,
+                       IssueIDs:    issueIDsFromSearch,
+                       IsArchived:  util.OptionalBoolFalse,
+                       LabelIDs:    opts.LabelIDs,
+               }
+               if len(repoIDs) > 0 {
+                       statsOpts.RepoIDs = repoIDs
+               } else if ctxUser.IsOrganization() {
+                       statsOpts.RepoIDs = userRepoIDs
+               }
+               shownIssueStats, err = models.GetUserIssueStats(statsOpts)
+               if err != nil {
+                       ctx.ServerError("GetUserIssueStats Shown", err)
+                       return
+               }
+       } else {
+               shownIssueStats = &models.IssueStats{}
+       }
+
+       var allIssueStats *models.IssueStats
+       if !forceEmpty {
+               allIssueStatsOpts := models.UserIssueStatsOptions{
+                       UserID:      ctx.User.ID,
+                       UserRepoIDs: userRepoIDs,
+                       FilterMode:  filterMode,
+                       IsPull:      isPullList,
+                       IsClosed:    isShowClosed,
+                       IssueIDs:    issueIDsFromSearch,
+                       IsArchived:  util.OptionalBoolFalse,
+                       LabelIDs:    opts.LabelIDs,
+               }
+               if ctxUser.IsOrganization() {
+                       allIssueStatsOpts.RepoIDs = userRepoIDs
+               }
+               allIssueStats, err = models.GetUserIssueStats(allIssueStatsOpts)
+               if err != nil {
+                       ctx.ServerError("GetUserIssueStats All", err)
+                       return
+               }
+       } else {
+               allIssueStats = &models.IssueStats{}
+       }
+
+       // Will be posted to ctx.Data.
+       var shownIssues int
+       if !isShowClosed {
+               shownIssues = int(shownIssueStats.OpenCount)
+               ctx.Data["TotalIssueCount"] = int(allIssueStats.OpenCount)
+       } else {
+               shownIssues = int(shownIssueStats.ClosedCount)
+               ctx.Data["TotalIssueCount"] = int(allIssueStats.ClosedCount)
+       }
+
+       ctx.Data["IsShowClosed"] = isShowClosed
+
+       ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] =
+               issue_service.GetRefEndNamesAndURLs(issues, ctx.Query("RepoLink"))
+
+       ctx.Data["Issues"] = issues
+
+       approvalCounts, err := models.IssueList(issues).GetApprovalCounts()
+       if err != nil {
+               ctx.ServerError("ApprovalCounts", err)
+               return
+       }
+       ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 {
+               counts, ok := approvalCounts[issueID]
+               if !ok || len(counts) == 0 {
+                       return 0
+               }
+               reviewTyp := models.ReviewTypeApprove
+               if typ == "reject" {
+                       reviewTyp = models.ReviewTypeReject
+               } else if typ == "waiting" {
+                       reviewTyp = models.ReviewTypeRequest
+               }
+               for _, count := range counts {
+                       if count.Type == reviewTyp {
+                               return count.Count
+                       }
+               }
+               return 0
+       }
+       ctx.Data["CommitStatus"] = commitStatus
+       ctx.Data["Repos"] = showRepos
+       ctx.Data["Counts"] = issueCountByRepo
+       ctx.Data["IssueStats"] = userIssueStats
+       ctx.Data["ShownIssueStats"] = shownIssueStats
+       ctx.Data["ViewType"] = viewType
+       ctx.Data["SortType"] = sortType
+       ctx.Data["RepoIDs"] = repoIDs
+       ctx.Data["IsShowClosed"] = isShowClosed
+       ctx.Data["SelectLabels"] = selectedLabels
+
+       if isShowClosed {
+               ctx.Data["State"] = "closed"
+       } else {
+               ctx.Data["State"] = "open"
+       }
+
+       // Convert []int64 to string
+       json := jsoniter.ConfigCompatibleWithStandardLibrary
+       reposParam, _ := json.Marshal(repoIDs)
+
+       ctx.Data["ReposParam"] = string(reposParam)
+
+       pager := context.NewPagination(shownIssues, setting.UI.IssuePagingNum, page, 5)
+       pager.AddParam(ctx, "q", "Keyword")
+       pager.AddParam(ctx, "type", "ViewType")
+       pager.AddParam(ctx, "repos", "ReposParam")
+       pager.AddParam(ctx, "sort", "SortType")
+       pager.AddParam(ctx, "state", "State")
+       pager.AddParam(ctx, "labels", "SelectLabels")
+       pager.AddParam(ctx, "milestone", "MilestoneID")
+       pager.AddParam(ctx, "assignee", "AssigneeID")
+       ctx.Data["Page"] = pager
+
+       ctx.HTML(http.StatusOK, tplIssues)
+}
+
+func getRepoIDs(reposQuery string) []int64 {
+       if len(reposQuery) == 0 || reposQuery == "[]" {
+               return []int64{}
+       }
+       if !issueReposQueryPattern.MatchString(reposQuery) {
+               log.Warn("issueReposQueryPattern does not match query")
+               return []int64{}
+       }
+
+       var repoIDs []int64
+       // remove "[" and "]" from string
+       reposQuery = reposQuery[1 : len(reposQuery)-1]
+       //for each ID (delimiter ",") add to int to repoIDs
+       for _, rID := range strings.Split(reposQuery, ",") {
+               // Ensure nonempty string entries
+               if rID != "" && rID != "0" {
+                       rIDint64, err := strconv.ParseInt(rID, 10, 64)
+                       if err == nil {
+                               repoIDs = append(repoIDs, rIDint64)
+                       }
+               }
+       }
+
+       return repoIDs
+}
+
+func getActiveUserRepoIDs(ctxUser *models.User, team *models.Team, unitType models.UnitType) ([]int64, error) {
+       var userRepoIDs []int64
+       var err error
+
+       if ctxUser.IsOrganization() {
+               userRepoIDs, err = getActiveTeamOrOrgRepoIds(ctxUser, team, unitType)
+               if err != nil {
+                       return nil, fmt.Errorf("orgRepoIds: %v", err)
+               }
+       } else {
+               userRepoIDs, err = ctxUser.GetActiveAccessRepoIDs(unitType)
+               if err != nil {
+                       return nil, fmt.Errorf("ctxUser.GetAccessRepoIDs: %v", err)
+               }
+       }
+
+       if len(userRepoIDs) == 0 {
+               userRepoIDs = []int64{-1}
+       }
+
+       return userRepoIDs, nil
+}
+
+// getActiveTeamOrOrgRepoIds gets RepoIDs for ctxUser as Organization.
+// Should be called if and only if ctxUser.IsOrganization == true.
+func getActiveTeamOrOrgRepoIds(ctxUser *models.User, team *models.Team, unitType models.UnitType) ([]int64, error) {
+       var orgRepoIDs []int64
+       var err error
+       var env models.AccessibleReposEnvironment
+
+       if team != nil {
+               env = ctxUser.AccessibleTeamReposEnv(team)
+       } else {
+               env, err = ctxUser.AccessibleReposEnv(ctxUser.ID)
+               if err != nil {
+                       return nil, fmt.Errorf("AccessibleReposEnv: %v", err)
+               }
+       }
+       orgRepoIDs, err = env.RepoIDs(1, ctxUser.NumRepos)
+       if err != nil {
+               return nil, fmt.Errorf("env.RepoIDs: %v", err)
+       }
+       orgRepoIDs, err = models.FilterOutRepoIdsWithoutUnitAccess(ctxUser, orgRepoIDs, unitType)
+       if err != nil {
+               return nil, fmt.Errorf("FilterOutRepoIdsWithoutUnitAccess: %v", err)
+       }
+
+       return orgRepoIDs, nil
+}
+
+func issueIDsFromSearch(ctxUser *models.User, keyword string, opts *models.IssuesOptions) ([]int64, error) {
+       if len(keyword) == 0 {
+               return []int64{}, nil
+       }
+
+       searchRepoIDs, err := models.GetRepoIDsForIssuesOptions(opts, ctxUser)
+       if err != nil {
+               return nil, fmt.Errorf("GetRepoIDsForIssuesOptions: %v", err)
+       }
+       issueIDsFromSearch, err := issue_indexer.SearchIssuesByKeyword(searchRepoIDs, keyword)
+       if err != nil {
+               return nil, fmt.Errorf("SearchIssuesByKeyword: %v", err)
+       }
+
+       return issueIDsFromSearch, nil
+}
+
+func repoIDMap(ctxUser *models.User, issueCountByRepo map[int64]int64, unitType models.UnitType) (map[int64]*models.Repository, error) {
+       repoByID := make(map[int64]*models.Repository, len(issueCountByRepo))
+       for id := range issueCountByRepo {
+               if id <= 0 {
+                       continue
+               }
+               if _, ok := repoByID[id]; !ok {
+                       repo, err := models.GetRepositoryByID(id)
+                       if models.IsErrRepoNotExist(err) {
+                               return nil, err
+                       } else if err != nil {
+                               return nil, fmt.Errorf("GetRepositoryByID: [%d]%v", id, err)
+                       }
+                       repoByID[id] = repo
+               }
+               repo := repoByID[id]
+
+               // Check if user has access to given repository.
+               perm, err := models.GetUserRepoPermission(repo, ctxUser)
+               if err != nil {
+                       return nil, fmt.Errorf("GetUserRepoPermission: [%d]%v", id, err)
+               }
+               if !perm.CanRead(unitType) {
+                       log.Debug("User created Issues in Repository which they no longer have access to: [%d]", id)
+               }
+       }
+       return repoByID, nil
+}
+
+// ShowSSHKeys output all the ssh keys of user by uid
+func ShowSSHKeys(ctx *context.Context, uid int64) {
+       keys, err := models.ListPublicKeys(uid, models.ListOptions{})
+       if err != nil {
+               ctx.ServerError("ListPublicKeys", err)
+               return
+       }
+
+       var buf bytes.Buffer
+       for i := range keys {
+               buf.WriteString(keys[i].OmitEmail())
+               buf.WriteString("\n")
+       }
+       ctx.PlainText(200, buf.Bytes())
+}
+
+// ShowGPGKeys output all the public GPG keys of user by uid
+func ShowGPGKeys(ctx *context.Context, uid int64) {
+       keys, err := models.ListGPGKeys(uid, models.ListOptions{})
+       if err != nil {
+               ctx.ServerError("ListGPGKeys", err)
+               return
+       }
+       entities := make([]*openpgp.Entity, 0)
+       failedEntitiesID := make([]string, 0)
+       for _, k := range keys {
+               e, err := models.GPGKeyToEntity(k)
+               if err != nil {
+                       if models.IsErrGPGKeyImportNotExist(err) {
+                               failedEntitiesID = append(failedEntitiesID, k.KeyID)
+                               continue //Skip previous import without backup of imported armored key
+                       }
+                       ctx.ServerError("ShowGPGKeys", err)
+                       return
+               }
+               entities = append(entities, e)
+       }
+       var buf bytes.Buffer
+
+       headers := make(map[string]string)
+       if len(failedEntitiesID) > 0 { //If some key need re-import to be exported
+               headers["Note"] = fmt.Sprintf("The keys with the following IDs couldn't be exported and need to be reuploaded %s", strings.Join(failedEntitiesID, ", "))
+       }
+       writer, _ := armor.Encode(&buf, "PGP PUBLIC KEY BLOCK", headers)
+       for _, e := range entities {
+               err = e.Serialize(writer) //TODO find why key are exported with a different cipherTypeByte as original (should not be blocking but strange)
+               if err != nil {
+                       ctx.ServerError("ShowGPGKeys", err)
+                       return
+               }
+       }
+       writer.Close()
+       ctx.PlainText(200, buf.Bytes())
+}
+
+// Email2User show user page via email
+func Email2User(ctx *context.Context) {
+       u, err := models.GetUserByEmail(ctx.Query("email"))
+       if err != nil {
+               if models.IsErrUserNotExist(err) {
+                       ctx.NotFound("GetUserByEmail", err)
+               } else {
+                       ctx.ServerError("GetUserByEmail", err)
+               }
+               return
+       }
+       ctx.Redirect(setting.AppSubURL + "/user/" + u.Name)
+}
diff --git a/routers/web/user/home_test.go b/routers/web/user/home_test.go
new file mode 100644 (file)
index 0000000..b0109c3
--- /dev/null
@@ -0,0 +1,118 @@
+// 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 user
+
+import (
+       "net/http"
+       "testing"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/test"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestArchivedIssues(t *testing.T) {
+       // Arrange
+       setting.UI.IssuePagingNum = 1
+       assert.NoError(t, models.LoadFixtures())
+
+       ctx := test.MockContext(t, "issues")
+       test.LoadUser(t, ctx, 30)
+       ctx.Req.Form.Set("state", "open")
+
+       // Assume: User 30 has access to two Repos with Issues, one of the Repos being archived.
+       repos, _, _ := models.GetUserRepositories(&models.SearchRepoOptions{Actor: ctx.User})
+       assert.Len(t, repos, 2)
+       IsArchived := make(map[int64]bool)
+       NumIssues := make(map[int64]int)
+       for _, repo := range repos {
+               IsArchived[repo.ID] = repo.IsArchived
+               NumIssues[repo.ID] = repo.NumIssues
+       }
+       assert.False(t, IsArchived[50])
+       assert.EqualValues(t, 1, NumIssues[50])
+       assert.True(t, IsArchived[51])
+       assert.EqualValues(t, 1, NumIssues[51])
+
+       // Act
+       Issues(ctx)
+
+       // Assert: One Issue (ID 30) from one Repo (ID 50) is retrieved, while nothing from archived Repo 51 is retrieved
+       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+
+       assert.EqualValues(t, map[int64]int64{50: 1}, ctx.Data["Counts"])
+       assert.Len(t, ctx.Data["Issues"], 1)
+       assert.Len(t, ctx.Data["Repos"], 1)
+}
+
+func TestIssues(t *testing.T) {
+       setting.UI.IssuePagingNum = 1
+       assert.NoError(t, models.LoadFixtures())
+
+       ctx := test.MockContext(t, "issues")
+       test.LoadUser(t, ctx, 2)
+       ctx.Req.Form.Set("state", "closed")
+       Issues(ctx)
+       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+
+       assert.EqualValues(t, map[int64]int64{1: 1, 2: 1}, ctx.Data["Counts"])
+       assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
+       assert.Len(t, ctx.Data["Issues"], 1)
+       assert.Len(t, ctx.Data["Repos"], 2)
+}
+
+func TestPulls(t *testing.T) {
+       setting.UI.IssuePagingNum = 20
+       assert.NoError(t, models.LoadFixtures())
+
+       ctx := test.MockContext(t, "pulls")
+       test.LoadUser(t, ctx, 2)
+       ctx.Req.Form.Set("state", "open")
+       Pulls(ctx)
+       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+
+       assert.Len(t, ctx.Data["Issues"], 3)
+}
+
+func TestMilestones(t *testing.T) {
+       setting.UI.IssuePagingNum = 1
+       assert.NoError(t, models.LoadFixtures())
+
+       ctx := test.MockContext(t, "milestones")
+       test.LoadUser(t, ctx, 2)
+       ctx.SetParams("sort", "issues")
+       ctx.Req.Form.Set("state", "closed")
+       ctx.Req.Form.Set("sort", "furthestduedate")
+       Milestones(ctx)
+       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+       assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"])
+       assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
+       assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"])
+       assert.EqualValues(t, 1, ctx.Data["Total"])
+       assert.Len(t, ctx.Data["Milestones"], 1)
+       assert.Len(t, ctx.Data["Repos"], 2) // both repo 42 and 1 have milestones and both are owned by user 2
+}
+
+func TestMilestonesForSpecificRepo(t *testing.T) {
+       setting.UI.IssuePagingNum = 1
+       assert.NoError(t, models.LoadFixtures())
+
+       ctx := test.MockContext(t, "milestones")
+       test.LoadUser(t, ctx, 2)
+       ctx.SetParams("sort", "issues")
+       ctx.SetParams("repo", "1")
+       ctx.Req.Form.Set("state", "closed")
+       ctx.Req.Form.Set("sort", "furthestduedate")
+       Milestones(ctx)
+       assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+       assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"])
+       assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
+       assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"])
+       assert.EqualValues(t, 1, ctx.Data["Total"])
+       assert.Len(t, ctx.Data["Milestones"], 1)
+       assert.Len(t, ctx.Data["Repos"], 2) // both repo 42 and 1 have milestones and both are owned by user 2
+}
diff --git a/routers/web/user/main_test.go b/routers/web/user/main_test.go
new file mode 100644 (file)
index 0000000..be17dd1
--- /dev/null
@@ -0,0 +1,16 @@
+// 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 user
+
+import (
+       "path/filepath"
+       "testing"
+
+       "code.gitea.io/gitea/models"
+)
+
+func TestMain(m *testing.M) {
+       models.MainTest(m, filepath.Join("..", "..", ".."))
+}
diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go
new file mode 100644 (file)
index 0000000..523e945
--- /dev/null
@@ -0,0 +1,192 @@
+// Copyright 2019 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 user
+
+import (
+       "errors"
+       "fmt"
+       "net/http"
+       "strconv"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/setting"
+)
+
+const (
+       tplNotification    base.TplName = "user/notification/notification"
+       tplNotificationDiv base.TplName = "user/notification/notification_div"
+)
+
+// GetNotificationCount is the middleware that sets the notification count in the context
+func GetNotificationCount(c *context.Context) {
+       if strings.HasPrefix(c.Req.URL.Path, "/api") {
+               return
+       }
+
+       if !c.IsSigned {
+               return
+       }
+
+       c.Data["NotificationUnreadCount"] = func() int64 {
+               count, err := models.GetNotificationCount(c.User, models.NotificationStatusUnread)
+               if err != nil {
+                       c.ServerError("GetNotificationCount", err)
+                       return -1
+               }
+
+               return count
+       }
+}
+
+// Notifications is the notifications page
+func Notifications(c *context.Context) {
+       getNotifications(c)
+       if c.Written() {
+               return
+       }
+       if c.QueryBool("div-only") {
+               c.HTML(http.StatusOK, tplNotificationDiv)
+               return
+       }
+       c.HTML(http.StatusOK, tplNotification)
+}
+
+func getNotifications(c *context.Context) {
+       var (
+               keyword = strings.Trim(c.Query("q"), " ")
+               status  models.NotificationStatus
+               page    = c.QueryInt("page")
+               perPage = c.QueryInt("perPage")
+       )
+       if page < 1 {
+               page = 1
+       }
+       if perPage < 1 {
+               perPage = 20
+       }
+
+       switch keyword {
+       case "read":
+               status = models.NotificationStatusRead
+       default:
+               status = models.NotificationStatusUnread
+       }
+
+       total, err := models.GetNotificationCount(c.User, status)
+       if err != nil {
+               c.ServerError("ErrGetNotificationCount", err)
+               return
+       }
+
+       // redirect to last page if request page is more than total pages
+       pager := context.NewPagination(int(total), perPage, page, 5)
+       if pager.Paginater.Current() < page {
+               c.Redirect(fmt.Sprintf("/notifications?q=%s&page=%d", c.Query("q"), pager.Paginater.Current()))
+               return
+       }
+
+       statuses := []models.NotificationStatus{status, models.NotificationStatusPinned}
+       notifications, err := models.NotificationsForUser(c.User, statuses, page, perPage)
+       if err != nil {
+               c.ServerError("ErrNotificationsForUser", err)
+               return
+       }
+
+       failCount := 0
+
+       repos, failures, err := notifications.LoadRepos()
+       if err != nil {
+               c.ServerError("LoadRepos", err)
+               return
+       }
+       notifications = notifications.Without(failures)
+       if err := repos.LoadAttributes(); err != nil {
+               c.ServerError("LoadAttributes", err)
+               return
+       }
+       failCount += len(failures)
+
+       failures, err = notifications.LoadIssues()
+       if err != nil {
+               c.ServerError("LoadIssues", err)
+               return
+       }
+       notifications = notifications.Without(failures)
+       failCount += len(failures)
+
+       failures, err = notifications.LoadComments()
+       if err != nil {
+               c.ServerError("LoadComments", err)
+               return
+       }
+       notifications = notifications.Without(failures)
+       failCount += len(failures)
+
+       if failCount > 0 {
+               c.Flash.Error(fmt.Sprintf("ERROR: %d notifications were removed due to missing parts - check the logs", failCount))
+       }
+
+       c.Data["Title"] = c.Tr("notifications")
+       c.Data["Keyword"] = keyword
+       c.Data["Status"] = status
+       c.Data["Notifications"] = notifications
+
+       pager.SetDefaultParams(c)
+       c.Data["Page"] = pager
+}
+
+// NotificationStatusPost is a route for changing the status of a notification
+func NotificationStatusPost(c *context.Context) {
+       var (
+               notificationID, _ = strconv.ParseInt(c.Req.PostFormValue("notification_id"), 10, 64)
+               statusStr         = c.Req.PostFormValue("status")
+               status            models.NotificationStatus
+       )
+
+       switch statusStr {
+       case "read":
+               status = models.NotificationStatusRead
+       case "unread":
+               status = models.NotificationStatusUnread
+       case "pinned":
+               status = models.NotificationStatusPinned
+       default:
+               c.ServerError("InvalidNotificationStatus", errors.New("Invalid notification status"))
+               return
+       }
+
+       if err := models.SetNotificationStatus(notificationID, c.User, status); err != nil {
+               c.ServerError("SetNotificationStatus", err)
+               return
+       }
+
+       if !c.QueryBool("noredirect") {
+               url := fmt.Sprintf("%s/notifications?page=%s", setting.AppSubURL, c.Query("page"))
+               c.Redirect(url, http.StatusSeeOther)
+       }
+
+       getNotifications(c)
+       if c.Written() {
+               return
+       }
+       c.Data["Link"] = setting.AppURL + "notifications"
+
+       c.HTML(http.StatusOK, tplNotificationDiv)
+}
+
+// NotificationPurgePost is a route for 'purging' the list of notifications - marking all unread as read
+func NotificationPurgePost(c *context.Context) {
+       err := models.UpdateNotificationStatuses(c.User, models.NotificationStatusUnread, models.NotificationStatusRead)
+       if err != nil {
+               c.ServerError("ErrUpdateNotificationStatuses", err)
+               return
+       }
+
+       url := fmt.Sprintf("%s/notifications", setting.AppSubURL)
+       c.Redirect(url, http.StatusSeeOther)
+}
diff --git a/routers/web/user/oauth.go b/routers/web/user/oauth.go
new file mode 100644 (file)
index 0000000..3ef5a56
--- /dev/null
@@ -0,0 +1,646 @@
+// Copyright 2019 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 user
+
+import (
+       "encoding/base64"
+       "fmt"
+       "html"
+       "net/http"
+       "net/url"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/auth/sso"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/timeutil"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+
+       "gitea.com/go-chi/binding"
+       "github.com/dgrijalva/jwt-go"
+)
+
+const (
+       tplGrantAccess base.TplName = "user/auth/grant"
+       tplGrantError  base.TplName = "user/auth/grant_error"
+)
+
+// TODO move error and responses to SDK or models
+
+// AuthorizeErrorCode represents an error code specified in RFC 6749
+type AuthorizeErrorCode string
+
+const (
+       // ErrorCodeInvalidRequest represents the according error in RFC 6749
+       ErrorCodeInvalidRequest AuthorizeErrorCode = "invalid_request"
+       // ErrorCodeUnauthorizedClient represents the according error in RFC 6749
+       ErrorCodeUnauthorizedClient AuthorizeErrorCode = "unauthorized_client"
+       // ErrorCodeAccessDenied represents the according error in RFC 6749
+       ErrorCodeAccessDenied AuthorizeErrorCode = "access_denied"
+       // ErrorCodeUnsupportedResponseType represents the according error in RFC 6749
+       ErrorCodeUnsupportedResponseType AuthorizeErrorCode = "unsupported_response_type"
+       // ErrorCodeInvalidScope represents the according error in RFC 6749
+       ErrorCodeInvalidScope AuthorizeErrorCode = "invalid_scope"
+       // ErrorCodeServerError represents the according error in RFC 6749
+       ErrorCodeServerError AuthorizeErrorCode = "server_error"
+       // ErrorCodeTemporaryUnavailable represents the according error in RFC 6749
+       ErrorCodeTemporaryUnavailable AuthorizeErrorCode = "temporarily_unavailable"
+)
+
+// AuthorizeError represents an error type specified in RFC 6749
+type AuthorizeError struct {
+       ErrorCode        AuthorizeErrorCode `json:"error" form:"error"`
+       ErrorDescription string
+       State            string
+}
+
+// Error returns the error message
+func (err AuthorizeError) Error() string {
+       return fmt.Sprintf("%s: %s", err.ErrorCode, err.ErrorDescription)
+}
+
+// AccessTokenErrorCode represents an error code specified in RFC 6749
+type AccessTokenErrorCode string
+
+const (
+       // AccessTokenErrorCodeInvalidRequest represents an error code specified in RFC 6749
+       AccessTokenErrorCodeInvalidRequest AccessTokenErrorCode = "invalid_request"
+       // AccessTokenErrorCodeInvalidClient represents an error code specified in RFC 6749
+       AccessTokenErrorCodeInvalidClient = "invalid_client"
+       // AccessTokenErrorCodeInvalidGrant represents an error code specified in RFC 6749
+       AccessTokenErrorCodeInvalidGrant = "invalid_grant"
+       // AccessTokenErrorCodeUnauthorizedClient represents an error code specified in RFC 6749
+       AccessTokenErrorCodeUnauthorizedClient = "unauthorized_client"
+       // AccessTokenErrorCodeUnsupportedGrantType represents an error code specified in RFC 6749
+       AccessTokenErrorCodeUnsupportedGrantType = "unsupported_grant_type"
+       // AccessTokenErrorCodeInvalidScope represents an error code specified in RFC 6749
+       AccessTokenErrorCodeInvalidScope = "invalid_scope"
+)
+
+// AccessTokenError represents an error response specified in RFC 6749
+type AccessTokenError struct {
+       ErrorCode        AccessTokenErrorCode `json:"error" form:"error"`
+       ErrorDescription string               `json:"error_description"`
+}
+
+// Error returns the error message
+func (err AccessTokenError) Error() string {
+       return fmt.Sprintf("%s: %s", err.ErrorCode, err.ErrorDescription)
+}
+
+// BearerTokenErrorCode represents an error code specified in RFC 6750
+type BearerTokenErrorCode string
+
+const (
+       // BearerTokenErrorCodeInvalidRequest represents an error code specified in RFC 6750
+       BearerTokenErrorCodeInvalidRequest BearerTokenErrorCode = "invalid_request"
+       // BearerTokenErrorCodeInvalidToken represents an error code specified in RFC 6750
+       BearerTokenErrorCodeInvalidToken BearerTokenErrorCode = "invalid_token"
+       // BearerTokenErrorCodeInsufficientScope represents an error code specified in RFC 6750
+       BearerTokenErrorCodeInsufficientScope BearerTokenErrorCode = "insufficient_scope"
+)
+
+// BearerTokenError represents an error response specified in RFC 6750
+type BearerTokenError struct {
+       ErrorCode        BearerTokenErrorCode `json:"error" form:"error"`
+       ErrorDescription string               `json:"error_description"`
+}
+
+// TokenType specifies the kind of token
+type TokenType string
+
+const (
+       // TokenTypeBearer represents a token type specified in RFC 6749
+       TokenTypeBearer TokenType = "bearer"
+       // TokenTypeMAC represents a token type specified in RFC 6749
+       TokenTypeMAC = "mac"
+)
+
+// AccessTokenResponse represents a successful access token response
+type AccessTokenResponse struct {
+       AccessToken  string    `json:"access_token"`
+       TokenType    TokenType `json:"token_type"`
+       ExpiresIn    int64     `json:"expires_in"`
+       RefreshToken string    `json:"refresh_token"`
+       IDToken      string    `json:"id_token,omitempty"`
+}
+
+func newAccessTokenResponse(grant *models.OAuth2Grant, clientSecret string) (*AccessTokenResponse, *AccessTokenError) {
+       if setting.OAuth2.InvalidateRefreshTokens {
+               if err := grant.IncreaseCounter(); err != nil {
+                       return nil, &AccessTokenError{
+                               ErrorCode:        AccessTokenErrorCodeInvalidGrant,
+                               ErrorDescription: "cannot increase the grant counter",
+                       }
+               }
+       }
+       // generate access token to access the API
+       expirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.AccessTokenExpirationTime)
+       accessToken := &models.OAuth2Token{
+               GrantID: grant.ID,
+               Type:    models.TypeAccessToken,
+               StandardClaims: jwt.StandardClaims{
+                       ExpiresAt: expirationDate.AsTime().Unix(),
+               },
+       }
+       signedAccessToken, err := accessToken.SignToken()
+       if err != nil {
+               return nil, &AccessTokenError{
+                       ErrorCode:        AccessTokenErrorCodeInvalidRequest,
+                       ErrorDescription: "cannot sign token",
+               }
+       }
+
+       // generate refresh token to request an access token after it expired later
+       refreshExpirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.RefreshTokenExpirationTime * 60 * 60).AsTime().Unix()
+       refreshToken := &models.OAuth2Token{
+               GrantID: grant.ID,
+               Counter: grant.Counter,
+               Type:    models.TypeRefreshToken,
+               StandardClaims: jwt.StandardClaims{
+                       ExpiresAt: refreshExpirationDate,
+               },
+       }
+       signedRefreshToken, err := refreshToken.SignToken()
+       if err != nil {
+               return nil, &AccessTokenError{
+                       ErrorCode:        AccessTokenErrorCodeInvalidRequest,
+                       ErrorDescription: "cannot sign token",
+               }
+       }
+
+       // generate OpenID Connect id_token
+       signedIDToken := ""
+       if grant.ScopeContains("openid") {
+               app, err := models.GetOAuth2ApplicationByID(grant.ApplicationID)
+               if err != nil {
+                       return nil, &AccessTokenError{
+                               ErrorCode:        AccessTokenErrorCodeInvalidRequest,
+                               ErrorDescription: "cannot find application",
+                       }
+               }
+               idToken := &models.OIDCToken{
+                       StandardClaims: jwt.StandardClaims{
+                               ExpiresAt: expirationDate.AsTime().Unix(),
+                               Issuer:    setting.AppURL,
+                               Audience:  app.ClientID,
+                               Subject:   fmt.Sprint(grant.UserID),
+                       },
+                       Nonce: grant.Nonce,
+               }
+               signedIDToken, err = idToken.SignToken(clientSecret)
+               if err != nil {
+                       return nil, &AccessTokenError{
+                               ErrorCode:        AccessTokenErrorCodeInvalidRequest,
+                               ErrorDescription: "cannot sign token",
+                       }
+               }
+       }
+
+       return &AccessTokenResponse{
+               AccessToken:  signedAccessToken,
+               TokenType:    TokenTypeBearer,
+               ExpiresIn:    setting.OAuth2.AccessTokenExpirationTime,
+               RefreshToken: signedRefreshToken,
+               IDToken:      signedIDToken,
+       }, nil
+}
+
+type userInfoResponse struct {
+       Sub      string `json:"sub"`
+       Name     string `json:"name"`
+       Username string `json:"preferred_username"`
+       Email    string `json:"email"`
+       Picture  string `json:"picture"`
+}
+
+// InfoOAuth manages request for userinfo endpoint
+func InfoOAuth(ctx *context.Context) {
+       header := ctx.Req.Header.Get("Authorization")
+       auths := strings.Fields(header)
+       if len(auths) != 2 || auths[0] != "Bearer" {
+               ctx.HandleText(http.StatusUnauthorized, "no valid auth token authorization")
+               return
+       }
+       uid := sso.CheckOAuthAccessToken(auths[1])
+       if uid == 0 {
+               handleBearerTokenError(ctx, BearerTokenError{
+                       ErrorCode:        BearerTokenErrorCodeInvalidToken,
+                       ErrorDescription: "Access token not assigned to any user",
+               })
+               return
+       }
+       authUser, err := models.GetUserByID(uid)
+       if err != nil {
+               ctx.ServerError("GetUserByID", err)
+               return
+       }
+       response := &userInfoResponse{
+               Sub:      fmt.Sprint(authUser.ID),
+               Name:     authUser.FullName,
+               Username: authUser.Name,
+               Email:    authUser.Email,
+               Picture:  authUser.AvatarLink(),
+       }
+       ctx.JSON(http.StatusOK, response)
+}
+
+// AuthorizeOAuth manages authorize requests
+func AuthorizeOAuth(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.AuthorizationForm)
+       errs := binding.Errors{}
+       errs = form.Validate(ctx.Req, errs)
+       if len(errs) > 0 {
+               errstring := ""
+               for _, e := range errs {
+                       errstring += e.Error() + "\n"
+               }
+               ctx.ServerError("AuthorizeOAuth: Validate: ", fmt.Errorf("errors occurred during validation: %s", errstring))
+               return
+       }
+
+       app, err := models.GetOAuth2ApplicationByClientID(form.ClientID)
+       if err != nil {
+               if models.IsErrOauthClientIDInvalid(err) {
+                       handleAuthorizeError(ctx, AuthorizeError{
+                               ErrorCode:        ErrorCodeUnauthorizedClient,
+                               ErrorDescription: "Client ID not registered",
+                               State:            form.State,
+                       }, "")
+                       return
+               }
+               ctx.ServerError("GetOAuth2ApplicationByClientID", err)
+               return
+       }
+       if err := app.LoadUser(); err != nil {
+               ctx.ServerError("LoadUser", err)
+               return
+       }
+
+       if !app.ContainsRedirectURI(form.RedirectURI) {
+               handleAuthorizeError(ctx, AuthorizeError{
+                       ErrorCode:        ErrorCodeInvalidRequest,
+                       ErrorDescription: "Unregistered Redirect URI",
+                       State:            form.State,
+               }, "")
+               return
+       }
+
+       if form.ResponseType != "code" {
+               handleAuthorizeError(ctx, AuthorizeError{
+                       ErrorCode:        ErrorCodeUnsupportedResponseType,
+                       ErrorDescription: "Only code response type is supported.",
+                       State:            form.State,
+               }, form.RedirectURI)
+               return
+       }
+
+       // pkce support
+       switch form.CodeChallengeMethod {
+       case "S256":
+       case "plain":
+               if err := ctx.Session.Set("CodeChallengeMethod", form.CodeChallengeMethod); err != nil {
+                       handleAuthorizeError(ctx, AuthorizeError{
+                               ErrorCode:        ErrorCodeServerError,
+                               ErrorDescription: "cannot set code challenge method",
+                               State:            form.State,
+                       }, form.RedirectURI)
+                       return
+               }
+               if err := ctx.Session.Set("CodeChallengeMethod", form.CodeChallenge); err != nil {
+                       handleAuthorizeError(ctx, AuthorizeError{
+                               ErrorCode:        ErrorCodeServerError,
+                               ErrorDescription: "cannot set code challenge",
+                               State:            form.State,
+                       }, form.RedirectURI)
+                       return
+               }
+               // Here we're just going to try to release the session early
+               if err := ctx.Session.Release(); err != nil {
+                       // we'll tolerate errors here as they *should* get saved elsewhere
+                       log.Error("Unable to save changes to the session: %v", err)
+               }
+       case "":
+               break
+       default:
+               handleAuthorizeError(ctx, AuthorizeError{
+                       ErrorCode:        ErrorCodeInvalidRequest,
+                       ErrorDescription: "unsupported code challenge method",
+                       State:            form.State,
+               }, form.RedirectURI)
+               return
+       }
+
+       grant, err := app.GetGrantByUserID(ctx.User.ID)
+       if err != nil {
+               handleServerError(ctx, form.State, form.RedirectURI)
+               return
+       }
+
+       // Redirect if user already granted access
+       if grant != nil {
+               code, err := grant.GenerateNewAuthorizationCode(form.RedirectURI, form.CodeChallenge, form.CodeChallengeMethod)
+               if err != nil {
+                       handleServerError(ctx, form.State, form.RedirectURI)
+                       return
+               }
+               redirect, err := code.GenerateRedirectURI(form.State)
+               if err != nil {
+                       handleServerError(ctx, form.State, form.RedirectURI)
+                       return
+               }
+               // Update nonce to reflect the new session
+               if len(form.Nonce) > 0 {
+                       err := grant.SetNonce(form.Nonce)
+                       if err != nil {
+                               log.Error("Unable to update nonce: %v", err)
+                       }
+               }
+               ctx.Redirect(redirect.String(), 302)
+               return
+       }
+
+       // show authorize page to grant access
+       ctx.Data["Application"] = app
+       ctx.Data["RedirectURI"] = form.RedirectURI
+       ctx.Data["State"] = form.State
+       ctx.Data["Scope"] = form.Scope
+       ctx.Data["Nonce"] = form.Nonce
+       ctx.Data["ApplicationUserLink"] = "<a href=\"" + html.EscapeString(setting.AppURL) + html.EscapeString(url.PathEscape(app.User.LowerName)) + "\">@" + html.EscapeString(app.User.Name) + "</a>"
+       ctx.Data["ApplicationRedirectDomainHTML"] = "<strong>" + html.EscapeString(form.RedirectURI) + "</strong>"
+       // TODO document SESSION <=> FORM
+       err = ctx.Session.Set("client_id", app.ClientID)
+       if err != nil {
+               handleServerError(ctx, form.State, form.RedirectURI)
+               log.Error(err.Error())
+               return
+       }
+       err = ctx.Session.Set("redirect_uri", form.RedirectURI)
+       if err != nil {
+               handleServerError(ctx, form.State, form.RedirectURI)
+               log.Error(err.Error())
+               return
+       }
+       err = ctx.Session.Set("state", form.State)
+       if err != nil {
+               handleServerError(ctx, form.State, form.RedirectURI)
+               log.Error(err.Error())
+               return
+       }
+       // Here we're just going to try to release the session early
+       if err := ctx.Session.Release(); err != nil {
+               // we'll tolerate errors here as they *should* get saved elsewhere
+               log.Error("Unable to save changes to the session: %v", err)
+       }
+       ctx.HTML(http.StatusOK, tplGrantAccess)
+}
+
+// GrantApplicationOAuth manages the post request submitted when a user grants access to an application
+func GrantApplicationOAuth(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.GrantApplicationForm)
+       if ctx.Session.Get("client_id") != form.ClientID || ctx.Session.Get("state") != form.State ||
+               ctx.Session.Get("redirect_uri") != form.RedirectURI {
+               ctx.Error(http.StatusBadRequest)
+               return
+       }
+       app, err := models.GetOAuth2ApplicationByClientID(form.ClientID)
+       if err != nil {
+               ctx.ServerError("GetOAuth2ApplicationByClientID", err)
+               return
+       }
+       grant, err := app.CreateGrant(ctx.User.ID, form.Scope)
+       if err != nil {
+               handleAuthorizeError(ctx, AuthorizeError{
+                       State:            form.State,
+                       ErrorDescription: "cannot create grant for user",
+                       ErrorCode:        ErrorCodeServerError,
+               }, form.RedirectURI)
+               return
+       }
+       if len(form.Nonce) > 0 {
+               err := grant.SetNonce(form.Nonce)
+               if err != nil {
+                       log.Error("Unable to update nonce: %v", err)
+               }
+       }
+
+       var codeChallenge, codeChallengeMethod string
+       codeChallenge, _ = ctx.Session.Get("CodeChallenge").(string)
+       codeChallengeMethod, _ = ctx.Session.Get("CodeChallengeMethod").(string)
+
+       code, err := grant.GenerateNewAuthorizationCode(form.RedirectURI, codeChallenge, codeChallengeMethod)
+       if err != nil {
+               handleServerError(ctx, form.State, form.RedirectURI)
+               return
+       }
+       redirect, err := code.GenerateRedirectURI(form.State)
+       if err != nil {
+               handleServerError(ctx, form.State, form.RedirectURI)
+               return
+       }
+       ctx.Redirect(redirect.String(), 302)
+}
+
+// OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
+func OIDCWellKnown(ctx *context.Context) {
+       t := ctx.Render.TemplateLookup("user/auth/oidc_wellknown")
+       ctx.Resp.Header().Set("Content-Type", "application/json")
+       if err := t.Execute(ctx.Resp, ctx.Data); err != nil {
+               log.Error("%v", err)
+               ctx.Error(http.StatusInternalServerError)
+       }
+}
+
+// AccessTokenOAuth manages all access token requests by the client
+func AccessTokenOAuth(ctx *context.Context) {
+       form := *web.GetForm(ctx).(*forms.AccessTokenForm)
+       if form.ClientID == "" {
+               authHeader := ctx.Req.Header.Get("Authorization")
+               authContent := strings.SplitN(authHeader, " ", 2)
+               if len(authContent) == 2 && authContent[0] == "Basic" {
+                       payload, err := base64.StdEncoding.DecodeString(authContent[1])
+                       if err != nil {
+                               handleAccessTokenError(ctx, AccessTokenError{
+                                       ErrorCode:        AccessTokenErrorCodeInvalidRequest,
+                                       ErrorDescription: "cannot parse basic auth header",
+                               })
+                               return
+                       }
+                       pair := strings.SplitN(string(payload), ":", 2)
+                       if len(pair) != 2 {
+                               handleAccessTokenError(ctx, AccessTokenError{
+                                       ErrorCode:        AccessTokenErrorCodeInvalidRequest,
+                                       ErrorDescription: "cannot parse basic auth header",
+                               })
+                               return
+                       }
+                       form.ClientID = pair[0]
+                       form.ClientSecret = pair[1]
+               }
+       }
+       switch form.GrantType {
+       case "refresh_token":
+               handleRefreshToken(ctx, form)
+               return
+       case "authorization_code":
+               handleAuthorizationCode(ctx, form)
+               return
+       default:
+               handleAccessTokenError(ctx, AccessTokenError{
+                       ErrorCode:        AccessTokenErrorCodeUnsupportedGrantType,
+                       ErrorDescription: "Only refresh_token or authorization_code grant type is supported",
+               })
+       }
+}
+
+func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm) {
+       token, err := models.ParseOAuth2Token(form.RefreshToken)
+       if err != nil {
+               handleAccessTokenError(ctx, AccessTokenError{
+                       ErrorCode:        AccessTokenErrorCodeUnauthorizedClient,
+                       ErrorDescription: "client is not authorized",
+               })
+               return
+       }
+       // get grant before increasing counter
+       grant, err := models.GetOAuth2GrantByID(token.GrantID)
+       if err != nil || grant == nil {
+               handleAccessTokenError(ctx, AccessTokenError{
+                       ErrorCode:        AccessTokenErrorCodeInvalidGrant,
+                       ErrorDescription: "grant does not exist",
+               })
+               return
+       }
+
+       // check if token got already used
+       if setting.OAuth2.InvalidateRefreshTokens && (grant.Counter != token.Counter || token.Counter == 0) {
+               handleAccessTokenError(ctx, AccessTokenError{
+                       ErrorCode:        AccessTokenErrorCodeUnauthorizedClient,
+                       ErrorDescription: "token was already used",
+               })
+               log.Warn("A client tried to use a refresh token for grant_id = %d was used twice!", grant.ID)
+               return
+       }
+       accessToken, tokenErr := newAccessTokenResponse(grant, form.ClientSecret)
+       if tokenErr != nil {
+               handleAccessTokenError(ctx, *tokenErr)
+               return
+       }
+       ctx.JSON(http.StatusOK, accessToken)
+}
+
+func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm) {
+       app, err := models.GetOAuth2ApplicationByClientID(form.ClientID)
+       if err != nil {
+               handleAccessTokenError(ctx, AccessTokenError{
+                       ErrorCode:        AccessTokenErrorCodeInvalidClient,
+                       ErrorDescription: fmt.Sprintf("cannot load client with client id: '%s'", form.ClientID),
+               })
+               return
+       }
+       if !app.ValidateClientSecret([]byte(form.ClientSecret)) {
+               handleAccessTokenError(ctx, AccessTokenError{
+                       ErrorCode:        AccessTokenErrorCodeUnauthorizedClient,
+                       ErrorDescription: "client is not authorized",
+               })
+               return
+       }
+       if form.RedirectURI != "" && !app.ContainsRedirectURI(form.RedirectURI) {
+               handleAccessTokenError(ctx, AccessTokenError{
+                       ErrorCode:        AccessTokenErrorCodeUnauthorizedClient,
+                       ErrorDescription: "client is not authorized",
+               })
+               return
+       }
+       authorizationCode, err := models.GetOAuth2AuthorizationByCode(form.Code)
+       if err != nil || authorizationCode == nil {
+               handleAccessTokenError(ctx, AccessTokenError{
+                       ErrorCode:        AccessTokenErrorCodeUnauthorizedClient,
+                       ErrorDescription: "client is not authorized",
+               })
+               return
+       }
+       // check if code verifier authorizes the client, PKCE support
+       if !authorizationCode.ValidateCodeChallenge(form.CodeVerifier) {
+               handleAccessTokenError(ctx, AccessTokenError{
+                       ErrorCode:        AccessTokenErrorCodeUnauthorizedClient,
+                       ErrorDescription: "client is not authorized",
+               })
+               return
+       }
+       // check if granted for this application
+       if authorizationCode.Grant.ApplicationID != app.ID {
+               handleAccessTokenError(ctx, AccessTokenError{
+                       ErrorCode:        AccessTokenErrorCodeInvalidGrant,
+                       ErrorDescription: "invalid grant",
+               })
+               return
+       }
+       // remove token from database to deny duplicate usage
+       if err := authorizationCode.Invalidate(); err != nil {
+               handleAccessTokenError(ctx, AccessTokenError{
+                       ErrorCode:        AccessTokenErrorCodeInvalidRequest,
+                       ErrorDescription: "cannot proceed your request",
+               })
+       }
+       resp, tokenErr := newAccessTokenResponse(authorizationCode.Grant, form.ClientSecret)
+       if tokenErr != nil {
+               handleAccessTokenError(ctx, *tokenErr)
+               return
+       }
+       // send successful response
+       ctx.JSON(http.StatusOK, resp)
+}
+
+func handleAccessTokenError(ctx *context.Context, acErr AccessTokenError) {
+       ctx.JSON(http.StatusBadRequest, acErr)
+}
+
+func handleServerError(ctx *context.Context, state string, redirectURI string) {
+       handleAuthorizeError(ctx, AuthorizeError{
+               ErrorCode:        ErrorCodeServerError,
+               ErrorDescription: "A server error occurred",
+               State:            state,
+       }, redirectURI)
+}
+
+func handleAuthorizeError(ctx *context.Context, authErr AuthorizeError, redirectURI string) {
+       if redirectURI == "" {
+               log.Warn("Authorization failed: %v", authErr.ErrorDescription)
+               ctx.Data["Error"] = authErr
+               ctx.HTML(400, tplGrantError)
+               return
+       }
+       redirect, err := url.Parse(redirectURI)
+       if err != nil {
+               ctx.ServerError("url.Parse", err)
+               return
+       }
+       q := redirect.Query()
+       q.Set("error", string(authErr.ErrorCode))
+       q.Set("error_description", authErr.ErrorDescription)
+       q.Set("state", authErr.State)
+       redirect.RawQuery = q.Encode()
+       ctx.Redirect(redirect.String(), 302)
+}
+
+func handleBearerTokenError(ctx *context.Context, beErr BearerTokenError) {
+       ctx.Resp.Header().Set("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"\", error=\"%s\", error_description=\"%s\"", beErr.ErrorCode, beErr.ErrorDescription))
+       switch beErr.ErrorCode {
+       case BearerTokenErrorCodeInvalidRequest:
+               ctx.JSON(http.StatusBadRequest, beErr)
+       case BearerTokenErrorCodeInvalidToken:
+               ctx.JSON(http.StatusUnauthorized, beErr)
+       case BearerTokenErrorCodeInsufficientScope:
+               ctx.JSON(http.StatusForbidden, beErr)
+       default:
+               log.Error("Invalid BearerTokenErrorCode: %v", beErr.ErrorCode)
+               ctx.ServerError("Unhandled BearerTokenError", fmt.Errorf("BearerTokenError: error=\"%v\", error_description=\"%v\"", beErr.ErrorCode, beErr.ErrorDescription))
+       }
+}
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
new file mode 100644 (file)
index 0000000..e66820e
--- /dev/null
@@ -0,0 +1,329 @@
+// Copyright 2015 The Gogs Authors. All rights reserved.
+// Copyright 2019 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 user
+
+import (
+       "fmt"
+       "net/http"
+       "path"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/markup"
+       "code.gitea.io/gitea/modules/markup/markdown"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/util"
+       "code.gitea.io/gitea/routers/web/org"
+)
+
+// GetUserByName get user by name
+func GetUserByName(ctx *context.Context, name string) *models.User {
+       user, err := models.GetUserByName(name)
+       if err != nil {
+               if models.IsErrUserNotExist(err) {
+                       if redirectUserID, err := models.LookupUserRedirect(name); err == nil {
+                               context.RedirectToUser(ctx, name, redirectUserID)
+                       } else {
+                               ctx.NotFound("GetUserByName", err)
+                       }
+               } else {
+                       ctx.ServerError("GetUserByName", err)
+               }
+               return nil
+       }
+       return user
+}
+
+// GetUserByParams returns user whose name is presented in URL paramenter.
+func GetUserByParams(ctx *context.Context) *models.User {
+       return GetUserByName(ctx, ctx.Params(":username"))
+}
+
+// Profile render user's profile page
+func Profile(ctx *context.Context) {
+       uname := ctx.Params(":username")
+
+       // Special handle for FireFox requests favicon.ico.
+       if uname == "favicon.ico" {
+               ctx.ServeFile(path.Join(setting.StaticRootPath, "public/img/favicon.png"))
+               return
+       }
+
+       if strings.HasSuffix(uname, ".png") {
+               ctx.Error(http.StatusNotFound)
+               return
+       }
+
+       isShowKeys := false
+       if strings.HasSuffix(uname, ".keys") {
+               isShowKeys = true
+               uname = strings.TrimSuffix(uname, ".keys")
+       }
+
+       isShowGPG := false
+       if strings.HasSuffix(uname, ".gpg") {
+               isShowGPG = true
+               uname = strings.TrimSuffix(uname, ".gpg")
+       }
+
+       ctxUser := GetUserByName(ctx, uname)
+       if ctx.Written() {
+               return
+       }
+
+       // Show SSH keys.
+       if isShowKeys {
+               ShowSSHKeys(ctx, ctxUser.ID)
+               return
+       }
+
+       // Show GPG keys.
+       if isShowGPG {
+               ShowGPGKeys(ctx, ctxUser.ID)
+               return
+       }
+
+       if ctxUser.IsOrganization() {
+               org.Home(ctx)
+               return
+       }
+
+       // Show OpenID URIs
+       openIDs, err := models.GetUserOpenIDs(ctxUser.ID)
+       if err != nil {
+               ctx.ServerError("GetUserOpenIDs", err)
+               return
+       }
+
+       ctx.Data["Title"] = ctxUser.DisplayName()
+       ctx.Data["PageIsUserProfile"] = true
+       ctx.Data["Owner"] = ctxUser
+       ctx.Data["OpenIDs"] = openIDs
+
+       if setting.Service.EnableUserHeatmap {
+               data, err := models.GetUserHeatmapDataByUser(ctxUser, ctx.User)
+               if err != nil {
+                       ctx.ServerError("GetUserHeatmapDataByUser", err)
+                       return
+               }
+               ctx.Data["HeatmapData"] = data
+       }
+
+       if len(ctxUser.Description) != 0 {
+               content, err := markdown.RenderString(&markup.RenderContext{
+                       URLPrefix: ctx.Repo.RepoLink,
+                       Metas:     map[string]string{"mode": "document"},
+               }, ctxUser.Description)
+               if err != nil {
+                       ctx.ServerError("RenderString", err)
+                       return
+               }
+               ctx.Data["RenderedDescription"] = content
+       }
+
+       showPrivate := ctx.IsSigned && (ctx.User.IsAdmin || ctx.User.ID == ctxUser.ID)
+
+       orgs, err := models.GetOrgsByUserID(ctxUser.ID, showPrivate)
+       if err != nil {
+               ctx.ServerError("GetOrgsByUserIDDesc", err)
+               return
+       }
+
+       ctx.Data["Orgs"] = orgs
+       ctx.Data["HasOrgsVisible"] = models.HasOrgsVisible(orgs, ctx.User)
+
+       tab := ctx.Query("tab")
+       ctx.Data["TabName"] = tab
+
+       page := ctx.QueryInt("page")
+       if page <= 0 {
+               page = 1
+       }
+
+       topicOnly := ctx.QueryBool("topic")
+
+       var (
+               repos   []*models.Repository
+               count   int64
+               total   int
+               orderBy models.SearchOrderBy
+       )
+
+       ctx.Data["SortType"] = ctx.Query("sort")
+       switch ctx.Query("sort") {
+       case "newest":
+               orderBy = models.SearchOrderByNewest
+       case "oldest":
+               orderBy = models.SearchOrderByOldest
+       case "recentupdate":
+               orderBy = models.SearchOrderByRecentUpdated
+       case "leastupdate":
+               orderBy = models.SearchOrderByLeastUpdated
+       case "reversealphabetically":
+               orderBy = models.SearchOrderByAlphabeticallyReverse
+       case "alphabetically":
+               orderBy = models.SearchOrderByAlphabetically
+       case "moststars":
+               orderBy = models.SearchOrderByStarsReverse
+       case "feweststars":
+               orderBy = models.SearchOrderByStars
+       case "mostforks":
+               orderBy = models.SearchOrderByForksReverse
+       case "fewestforks":
+               orderBy = models.SearchOrderByForks
+       default:
+               ctx.Data["SortType"] = "recentupdate"
+               orderBy = models.SearchOrderByRecentUpdated
+       }
+
+       keyword := strings.Trim(ctx.Query("q"), " ")
+       ctx.Data["Keyword"] = keyword
+       switch tab {
+       case "followers":
+               items, err := ctxUser.GetFollowers(models.ListOptions{
+                       PageSize: setting.UI.User.RepoPagingNum,
+                       Page:     page,
+               })
+               if err != nil {
+                       ctx.ServerError("GetFollowers", err)
+                       return
+               }
+               ctx.Data["Cards"] = items
+
+               total = ctxUser.NumFollowers
+       case "following":
+               items, err := ctxUser.GetFollowing(models.ListOptions{
+                       PageSize: setting.UI.User.RepoPagingNum,
+                       Page:     page,
+               })
+               if err != nil {
+                       ctx.ServerError("GetFollowing", err)
+                       return
+               }
+               ctx.Data["Cards"] = items
+
+               total = ctxUser.NumFollowing
+       case "activity":
+               retrieveFeeds(ctx, models.GetFeedsOptions{RequestedUser: ctxUser,
+                       Actor:           ctx.User,
+                       IncludePrivate:  showPrivate,
+                       OnlyPerformedBy: true,
+                       IncludeDeleted:  false,
+                       Date:            ctx.Query("date"),
+               })
+               if ctx.Written() {
+                       return
+               }
+       case "stars":
+               ctx.Data["PageIsProfileStarList"] = true
+               repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
+                       ListOptions: models.ListOptions{
+                               PageSize: setting.UI.User.RepoPagingNum,
+                               Page:     page,
+                       },
+                       Actor:              ctx.User,
+                       Keyword:            keyword,
+                       OrderBy:            orderBy,
+                       Private:            ctx.IsSigned,
+                       StarredByID:        ctxUser.ID,
+                       Collaborate:        util.OptionalBoolFalse,
+                       TopicOnly:          topicOnly,
+                       IncludeDescription: setting.UI.SearchRepoDescription,
+               })
+               if err != nil {
+                       ctx.ServerError("SearchRepository", err)
+                       return
+               }
+
+               total = int(count)
+       case "projects":
+               ctx.Data["OpenProjects"], _, err = models.GetProjects(models.ProjectSearchOptions{
+                       Page:     -1,
+                       IsClosed: util.OptionalBoolFalse,
+                       Type:     models.ProjectTypeIndividual,
+               })
+               if err != nil {
+                       ctx.ServerError("GetProjects", err)
+                       return
+               }
+       case "watching":
+               repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
+                       ListOptions: models.ListOptions{
+                               PageSize: setting.UI.User.RepoPagingNum,
+                               Page:     page,
+                       },
+                       Actor:              ctx.User,
+                       Keyword:            keyword,
+                       OrderBy:            orderBy,
+                       Private:            ctx.IsSigned,
+                       WatchedByID:        ctxUser.ID,
+                       Collaborate:        util.OptionalBoolFalse,
+                       TopicOnly:          topicOnly,
+                       IncludeDescription: setting.UI.SearchRepoDescription,
+               })
+               if err != nil {
+                       ctx.ServerError("SearchRepository", err)
+                       return
+               }
+
+               total = int(count)
+       default:
+               repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
+                       ListOptions: models.ListOptions{
+                               PageSize: setting.UI.User.RepoPagingNum,
+                               Page:     page,
+                       },
+                       Actor:              ctx.User,
+                       Keyword:            keyword,
+                       OwnerID:            ctxUser.ID,
+                       OrderBy:            orderBy,
+                       Private:            ctx.IsSigned,
+                       Collaborate:        util.OptionalBoolFalse,
+                       TopicOnly:          topicOnly,
+                       IncludeDescription: setting.UI.SearchRepoDescription,
+               })
+               if err != nil {
+                       ctx.ServerError("SearchRepository", err)
+                       return
+               }
+
+               total = int(count)
+       }
+       ctx.Data["Repos"] = repos
+       ctx.Data["Total"] = total
+
+       pager := context.NewPagination(total, setting.UI.User.RepoPagingNum, page, 5)
+       pager.SetDefaultParams(ctx)
+       ctx.Data["Page"] = pager
+
+       ctx.Data["ShowUserEmail"] = len(ctxUser.Email) > 0 && ctx.IsSigned && (!ctxUser.KeepEmailPrivate || ctxUser.ID == ctx.User.ID)
+
+       ctx.HTML(http.StatusOK, tplProfile)
+}
+
+// Action response for follow/unfollow user request
+func Action(ctx *context.Context) {
+       u := GetUserByParams(ctx)
+       if ctx.Written() {
+               return
+       }
+
+       var err error
+       switch ctx.Params(":action") {
+       case "follow":
+               err = models.FollowUser(ctx.User.ID, u.ID)
+       case "unfollow":
+               err = models.UnfollowUser(ctx.User.ID, u.ID)
+       }
+
+       if err != nil {
+               ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err)
+               return
+       }
+
+       ctx.RedirectToFirst(ctx.Query("redirect_to"), u.HomeLink())
+}
diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go
new file mode 100644 (file)
index 0000000..48ab37d
--- /dev/null
@@ -0,0 +1,313 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2018 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 setting
+
+import (
+       "errors"
+       "net/http"
+       "time"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/password"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/timeutil"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+       "code.gitea.io/gitea/services/mailer"
+)
+
+const (
+       tplSettingsAccount base.TplName = "user/settings/account"
+)
+
+// Account renders change user's password, user's email and user suicide page
+func Account(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("settings")
+       ctx.Data["PageIsSettingsAccount"] = true
+       ctx.Data["Email"] = ctx.User.Email
+
+       loadAccountData(ctx)
+
+       ctx.HTML(http.StatusOK, tplSettingsAccount)
+}
+
+// AccountPost response for change user's password
+func AccountPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.ChangePasswordForm)
+       ctx.Data["Title"] = ctx.Tr("settings")
+       ctx.Data["PageIsSettingsAccount"] = true
+
+       if ctx.HasError() {
+               loadAccountData(ctx)
+
+               ctx.HTML(http.StatusOK, tplSettingsAccount)
+               return
+       }
+
+       if len(form.Password) < setting.MinPasswordLength {
+               ctx.Flash.Error(ctx.Tr("auth.password_too_short", setting.MinPasswordLength))
+       } else if ctx.User.IsPasswordSet() && !ctx.User.ValidatePassword(form.OldPassword) {
+               ctx.Flash.Error(ctx.Tr("settings.password_incorrect"))
+       } else if form.Password != form.Retype {
+               ctx.Flash.Error(ctx.Tr("form.password_not_match"))
+       } else if !password.IsComplexEnough(form.Password) {
+               ctx.Flash.Error(password.BuildComplexityError(ctx))
+       } else if pwned, err := password.IsPwned(ctx, form.Password); pwned || err != nil {
+               errMsg := ctx.Tr("auth.password_pwned")
+               if err != nil {
+                       log.Error(err.Error())
+                       errMsg = ctx.Tr("auth.password_pwned_err")
+               }
+               ctx.Flash.Error(errMsg)
+       } else {
+               var err error
+               if err = ctx.User.SetPassword(form.Password); err != nil {
+                       ctx.ServerError("UpdateUser", err)
+                       return
+               }
+               if err := models.UpdateUserCols(ctx.User, "salt", "passwd_hash_algo", "passwd"); err != nil {
+                       ctx.ServerError("UpdateUser", err)
+                       return
+               }
+               log.Trace("User password updated: %s", ctx.User.Name)
+               ctx.Flash.Success(ctx.Tr("settings.change_password_success"))
+       }
+
+       ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+}
+
+// EmailPost response for change user's email
+func EmailPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.AddEmailForm)
+       ctx.Data["Title"] = ctx.Tr("settings")
+       ctx.Data["PageIsSettingsAccount"] = true
+
+       // Make emailaddress primary.
+       if ctx.Query("_method") == "PRIMARY" {
+               if err := models.MakeEmailPrimary(&models.EmailAddress{ID: ctx.QueryInt64("id")}); err != nil {
+                       ctx.ServerError("MakeEmailPrimary", err)
+                       return
+               }
+
+               log.Trace("Email made primary: %s", ctx.User.Name)
+               ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+               return
+       }
+       // Send activation Email
+       if ctx.Query("_method") == "SENDACTIVATION" {
+               var address string
+               if ctx.Cache.IsExist("MailResendLimit_" + ctx.User.LowerName) {
+                       log.Error("Send activation: activation still pending")
+                       ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+                       return
+               }
+               if ctx.Query("id") == "PRIMARY" {
+                       if ctx.User.IsActive {
+                               log.Error("Send activation: email not set for activation")
+                               ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+                               return
+                       }
+                       mailer.SendActivateAccountMail(ctx.Locale, ctx.User)
+                       address = ctx.User.Email
+               } else {
+                       id := ctx.QueryInt64("id")
+                       email, err := models.GetEmailAddressByID(ctx.User.ID, id)
+                       if err != nil {
+                               log.Error("GetEmailAddressByID(%d,%d) error: %v", ctx.User.ID, id, err)
+                               ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+                               return
+                       }
+                       if email == nil {
+                               log.Error("Send activation: EmailAddress not found; user:%d, id: %d", ctx.User.ID, id)
+                               ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+                               return
+                       }
+                       if email.IsActivated {
+                               log.Error("Send activation: email not set for activation")
+                               ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+                               return
+                       }
+                       mailer.SendActivateEmailMail(ctx.User, email)
+                       address = email.Email
+               }
+
+               if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
+                       log.Error("Set cache(MailResendLimit) fail: %v", err)
+               }
+               ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", address, timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())))
+               ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+               return
+       }
+       // Set Email Notification Preference
+       if ctx.Query("_method") == "NOTIFICATION" {
+               preference := ctx.Query("preference")
+               if !(preference == models.EmailNotificationsEnabled ||
+                       preference == models.EmailNotificationsOnMention ||
+                       preference == models.EmailNotificationsDisabled) {
+                       log.Error("Email notifications preference change returned unrecognized option %s: %s", preference, ctx.User.Name)
+                       ctx.ServerError("SetEmailPreference", errors.New("option unrecognized"))
+                       return
+               }
+               if err := ctx.User.SetEmailNotifications(preference); err != nil {
+                       log.Error("Set Email Notifications failed: %v", err)
+                       ctx.ServerError("SetEmailNotifications", err)
+                       return
+               }
+               log.Trace("Email notifications preference made %s: %s", preference, ctx.User.Name)
+               ctx.Flash.Success(ctx.Tr("settings.email_preference_set_success"))
+               ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+               return
+       }
+
+       if ctx.HasError() {
+               loadAccountData(ctx)
+
+               ctx.HTML(http.StatusOK, tplSettingsAccount)
+               return
+       }
+
+       email := &models.EmailAddress{
+               UID:         ctx.User.ID,
+               Email:       form.Email,
+               IsActivated: !setting.Service.RegisterEmailConfirm,
+       }
+       if err := models.AddEmailAddress(email); err != nil {
+               if models.IsErrEmailAlreadyUsed(err) {
+                       loadAccountData(ctx)
+
+                       ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form)
+                       return
+               } else if models.IsErrEmailInvalid(err) {
+                       loadAccountData(ctx)
+
+                       ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsAccount, &form)
+                       return
+               }
+               ctx.ServerError("AddEmailAddress", err)
+               return
+       }
+
+       // Send confirmation email
+       if setting.Service.RegisterEmailConfirm {
+               mailer.SendActivateEmailMail(ctx.User, email)
+               if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
+                       log.Error("Set cache(MailResendLimit) fail: %v", err)
+               }
+               ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", email.Email, timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())))
+       } else {
+               ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
+       }
+
+       log.Trace("Email address added: %s", email.Email)
+       ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+}
+
+// DeleteEmail response for delete user's email
+func DeleteEmail(ctx *context.Context) {
+       if err := models.DeleteEmailAddress(&models.EmailAddress{ID: ctx.QueryInt64("id"), UID: ctx.User.ID}); err != nil {
+               ctx.ServerError("DeleteEmail", err)
+               return
+       }
+       log.Trace("Email address deleted: %s", ctx.User.Name)
+
+       ctx.Flash.Success(ctx.Tr("settings.email_deletion_success"))
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": setting.AppSubURL + "/user/settings/account",
+       })
+}
+
+// DeleteAccount render user suicide page and response for delete user himself
+func DeleteAccount(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("settings")
+       ctx.Data["PageIsSettingsAccount"] = true
+
+       if _, err := models.UserSignIn(ctx.User.Name, ctx.Query("password")); err != nil {
+               if models.IsErrUserNotExist(err) {
+                       loadAccountData(ctx)
+
+                       ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_password"), tplSettingsAccount, nil)
+               } else {
+                       ctx.ServerError("UserSignIn", err)
+               }
+               return
+       }
+
+       if err := models.DeleteUser(ctx.User); err != nil {
+               switch {
+               case models.IsErrUserOwnRepos(err):
+                       ctx.Flash.Error(ctx.Tr("form.still_own_repo"))
+                       ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+               case models.IsErrUserHasOrgs(err):
+                       ctx.Flash.Error(ctx.Tr("form.still_has_org"))
+                       ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+               default:
+                       ctx.ServerError("DeleteUser", err)
+               }
+       } else {
+               log.Trace("Account deleted: %s", ctx.User.Name)
+               ctx.Redirect(setting.AppSubURL + "/")
+       }
+}
+
+// UpdateUIThemePost is used to update users' specific theme
+func UpdateUIThemePost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.UpdateThemeForm)
+       ctx.Data["Title"] = ctx.Tr("settings")
+       ctx.Data["PageIsSettingsAccount"] = true
+
+       if ctx.HasError() {
+               ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+               return
+       }
+
+       if !form.IsThemeExists() {
+               ctx.Flash.Error(ctx.Tr("settings.theme_update_error"))
+               ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+               return
+       }
+
+       if err := ctx.User.UpdateTheme(form.Theme); err != nil {
+               ctx.Flash.Error(ctx.Tr("settings.theme_update_error"))
+               ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+               return
+       }
+
+       log.Trace("Update user theme: %s", ctx.User.Name)
+       ctx.Flash.Success(ctx.Tr("settings.theme_update_success"))
+       ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+}
+
+func loadAccountData(ctx *context.Context) {
+       emlist, err := models.GetEmailAddresses(ctx.User.ID)
+       if err != nil {
+               ctx.ServerError("GetEmailAddresses", err)
+               return
+       }
+       type UserEmail struct {
+               models.EmailAddress
+               CanBePrimary bool
+       }
+       pendingActivation := ctx.Cache.IsExist("MailResendLimit_" + ctx.User.LowerName)
+       emails := make([]*UserEmail, len(emlist))
+       for i, em := range emlist {
+               var email UserEmail
+               email.EmailAddress = *em
+               email.CanBePrimary = em.IsActivated
+               emails[i] = &email
+       }
+       ctx.Data["Emails"] = emails
+       ctx.Data["EmailNotificationsPreference"] = ctx.User.EmailNotifications()
+       ctx.Data["ActivationsPending"] = pendingActivation
+       ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm
+
+       if setting.Service.UserDeleteWithCommentsMaxTime != 0 {
+               ctx.Data["UserDeleteWithCommentsMaxTime"] = setting.Service.UserDeleteWithCommentsMaxTime.String()
+               ctx.Data["UserDeleteWithComments"] = ctx.User.CreatedUnix.AsTime().Add(setting.Service.UserDeleteWithCommentsMaxTime).After(time.Now())
+       }
+}
diff --git a/routers/web/user/setting/account_test.go b/routers/web/user/setting/account_test.go
new file mode 100644 (file)
index 0000000..25b68da
--- /dev/null
@@ -0,0 +1,99 @@
+// Copyright 2018 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 setting
+
+import (
+       "net/http"
+       "testing"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/test"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestChangePassword(t *testing.T) {
+       oldPassword := "password"
+       setting.MinPasswordLength = 6
+       var pcALL = []string{"lower", "upper", "digit", "spec"}
+       var pcLUN = []string{"lower", "upper", "digit"}
+       var pcLU = []string{"lower", "upper"}
+
+       for _, req := range []struct {
+               OldPassword        string
+               NewPassword        string
+               Retype             string
+               Message            string
+               PasswordComplexity []string
+       }{
+               {
+                       OldPassword:        oldPassword,
+                       NewPassword:        "Qwerty123456-",
+                       Retype:             "Qwerty123456-",
+                       Message:            "",
+                       PasswordComplexity: pcALL,
+               },
+               {
+                       OldPassword:        oldPassword,
+                       NewPassword:        "12345",
+                       Retype:             "12345",
+                       Message:            "auth.password_too_short",
+                       PasswordComplexity: pcALL,
+               },
+               {
+                       OldPassword:        "12334",
+                       NewPassword:        "123456",
+                       Retype:             "123456",
+                       Message:            "settings.password_incorrect",
+                       PasswordComplexity: pcALL,
+               },
+               {
+                       OldPassword:        oldPassword,
+                       NewPassword:        "123456",
+                       Retype:             "12345",
+                       Message:            "form.password_not_match",
+                       PasswordComplexity: pcALL,
+               },
+               {
+                       OldPassword:        oldPassword,
+                       NewPassword:        "Qwerty",
+                       Retype:             "Qwerty",
+                       Message:            "form.password_complexity",
+                       PasswordComplexity: pcALL,
+               },
+               {
+                       OldPassword:        oldPassword,
+                       NewPassword:        "Qwerty",
+                       Retype:             "Qwerty",
+                       Message:            "form.password_complexity",
+                       PasswordComplexity: pcLUN,
+               },
+               {
+                       OldPassword:        oldPassword,
+                       NewPassword:        "QWERTY",
+                       Retype:             "QWERTY",
+                       Message:            "form.password_complexity",
+                       PasswordComplexity: pcLU,
+               },
+       } {
+               models.PrepareTestEnv(t)
+               ctx := test.MockContext(t, "user/settings/security")
+               test.LoadUser(t, ctx, 2)
+               test.LoadRepo(t, ctx, 1)
+
+               web.SetForm(ctx, &forms.ChangePasswordForm{
+                       OldPassword: req.OldPassword,
+                       Password:    req.NewPassword,
+                       Retype:      req.Retype,
+               })
+               AccountPost(ctx)
+
+               assert.Contains(t, ctx.Flash.ErrorMsg, req.Message)
+               assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
+       }
+}
diff --git a/routers/web/user/setting/adopt.go b/routers/web/user/setting/adopt.go
new file mode 100644 (file)
index 0000000..b2d9187
--- /dev/null
@@ -0,0 +1,64 @@
+// 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 setting
+
+import (
+       "path/filepath"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/repository"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/util"
+)
+
+// AdoptOrDeleteRepository adopts or deletes a repository
+func AdoptOrDeleteRepository(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("settings")
+       ctx.Data["PageIsSettingsRepos"] = true
+       allowAdopt := ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories
+       ctx.Data["allowAdopt"] = allowAdopt
+       allowDelete := ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories
+       ctx.Data["allowDelete"] = allowDelete
+
+       dir := ctx.Query("id")
+       action := ctx.Query("action")
+
+       ctxUser := ctx.User
+       root := filepath.Join(models.UserPath(ctxUser.LowerName))
+
+       // check not a repo
+       has, err := models.IsRepositoryExist(ctxUser, dir)
+       if err != nil {
+               ctx.ServerError("IsRepositoryExist", err)
+               return
+       }
+
+       isDir, err := util.IsDir(filepath.Join(root, dir+".git"))
+       if err != nil {
+               ctx.ServerError("IsDir", err)
+               return
+       }
+       if has || !isDir {
+               // Fallthrough to failure mode
+       } else if action == "adopt" && allowAdopt {
+               if _, err := repository.AdoptRepository(ctxUser, ctxUser, models.CreateRepoOptions{
+                       Name:      dir,
+                       IsPrivate: true,
+               }); err != nil {
+                       ctx.ServerError("repository.AdoptRepository", err)
+                       return
+               }
+               ctx.Flash.Success(ctx.Tr("repo.adopt_preexisting_success", dir))
+       } else if action == "delete" && allowDelete {
+               if err := repository.DeleteUnadoptedRepository(ctxUser, ctxUser, dir); err != nil {
+                       ctx.ServerError("repository.AdoptRepository", err)
+                       return
+               }
+               ctx.Flash.Success(ctx.Tr("repo.delete_preexisting_success", dir))
+       }
+
+       ctx.Redirect(setting.AppSubURL + "/user/settings/repos")
+}
diff --git a/routers/web/user/setting/applications.go b/routers/web/user/setting/applications.go
new file mode 100644 (file)
index 0000000..4161efd
--- /dev/null
@@ -0,0 +1,106 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2018 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 setting
+
+import (
+       "net/http"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+)
+
+const (
+       tplSettingsApplications base.TplName = "user/settings/applications"
+)
+
+// Applications render manage access token page
+func Applications(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("settings")
+       ctx.Data["PageIsSettingsApplications"] = true
+
+       loadApplicationsData(ctx)
+
+       ctx.HTML(http.StatusOK, tplSettingsApplications)
+}
+
+// ApplicationsPost response for add user's access token
+func ApplicationsPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.NewAccessTokenForm)
+       ctx.Data["Title"] = ctx.Tr("settings")
+       ctx.Data["PageIsSettingsApplications"] = true
+
+       if ctx.HasError() {
+               loadApplicationsData(ctx)
+
+               ctx.HTML(http.StatusOK, tplSettingsApplications)
+               return
+       }
+
+       t := &models.AccessToken{
+               UID:  ctx.User.ID,
+               Name: form.Name,
+       }
+
+       exist, err := models.AccessTokenByNameExists(t)
+       if err != nil {
+               ctx.ServerError("AccessTokenByNameExists", err)
+               return
+       }
+       if exist {
+               ctx.Flash.Error(ctx.Tr("settings.generate_token_name_duplicate", t.Name))
+               ctx.Redirect(setting.AppSubURL + "/user/settings/applications")
+               return
+       }
+
+       if err := models.NewAccessToken(t); err != nil {
+               ctx.ServerError("NewAccessToken", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("settings.generate_token_success"))
+       ctx.Flash.Info(t.Token)
+
+       ctx.Redirect(setting.AppSubURL + "/user/settings/applications")
+}
+
+// DeleteApplication response for delete user access token
+func DeleteApplication(ctx *context.Context) {
+       if err := models.DeleteAccessTokenByID(ctx.QueryInt64("id"), ctx.User.ID); err != nil {
+               ctx.Flash.Error("DeleteAccessTokenByID: " + err.Error())
+       } else {
+               ctx.Flash.Success(ctx.Tr("settings.delete_token_success"))
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": setting.AppSubURL + "/user/settings/applications",
+       })
+}
+
+func loadApplicationsData(ctx *context.Context) {
+       tokens, err := models.ListAccessTokens(models.ListAccessTokensOptions{UserID: ctx.User.ID})
+       if err != nil {
+               ctx.ServerError("ListAccessTokens", err)
+               return
+       }
+       ctx.Data["Tokens"] = tokens
+       ctx.Data["EnableOAuth2"] = setting.OAuth2.Enable
+       if setting.OAuth2.Enable {
+               ctx.Data["Applications"], err = models.GetOAuth2ApplicationsByUserID(ctx.User.ID)
+               if err != nil {
+                       ctx.ServerError("GetOAuth2ApplicationsByUserID", err)
+                       return
+               }
+               ctx.Data["Grants"], err = models.GetOAuth2GrantsByUserID(ctx.User.ID)
+               if err != nil {
+                       ctx.ServerError("GetOAuth2GrantsByUserID", err)
+                       return
+               }
+       }
+}
diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go
new file mode 100644 (file)
index 0000000..e56a33a
--- /dev/null
@@ -0,0 +1,226 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2018 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 setting
+
+import (
+       "net/http"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+)
+
+const (
+       tplSettingsKeys base.TplName = "user/settings/keys"
+)
+
+// Keys render user's SSH/GPG public keys page
+func Keys(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("settings")
+       ctx.Data["PageIsSettingsKeys"] = true
+       ctx.Data["DisableSSH"] = setting.SSH.Disabled
+       ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer
+       ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled
+
+       loadKeysData(ctx)
+
+       ctx.HTML(http.StatusOK, tplSettingsKeys)
+}
+
+// KeysPost response for change user's SSH/GPG keys
+func KeysPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.AddKeyForm)
+       ctx.Data["Title"] = ctx.Tr("settings")
+       ctx.Data["PageIsSettingsKeys"] = true
+       ctx.Data["DisableSSH"] = setting.SSH.Disabled
+       ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer
+       ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled
+
+       if ctx.HasError() {
+               loadKeysData(ctx)
+
+               ctx.HTML(http.StatusOK, tplSettingsKeys)
+               return
+       }
+       switch form.Type {
+       case "principal":
+               content, err := models.CheckPrincipalKeyString(ctx.User, form.Content)
+               if err != nil {
+                       if models.IsErrSSHDisabled(err) {
+                               ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
+                       } else {
+                               ctx.Flash.Error(ctx.Tr("form.invalid_ssh_principal", err.Error()))
+                       }
+                       ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
+                       return
+               }
+               if _, err = models.AddPrincipalKey(ctx.User.ID, content, 0); err != nil {
+                       ctx.Data["HasPrincipalError"] = true
+                       switch {
+                       case models.IsErrKeyAlreadyExist(err), models.IsErrKeyNameAlreadyUsed(err):
+                               loadKeysData(ctx)
+
+                               ctx.Data["Err_Content"] = true
+                               ctx.RenderWithErr(ctx.Tr("settings.ssh_principal_been_used"), tplSettingsKeys, &form)
+                       default:
+                               ctx.ServerError("AddPrincipalKey", err)
+                       }
+                       return
+               }
+               ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content))
+               ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
+       case "gpg":
+               keys, err := models.AddGPGKey(ctx.User.ID, form.Content)
+               if err != nil {
+                       ctx.Data["HasGPGError"] = true
+                       switch {
+                       case models.IsErrGPGKeyParsing(err):
+                               ctx.Flash.Error(ctx.Tr("form.invalid_gpg_key", err.Error()))
+                               ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
+                       case models.IsErrGPGKeyIDAlreadyUsed(err):
+                               loadKeysData(ctx)
+
+                               ctx.Data["Err_Content"] = true
+                               ctx.RenderWithErr(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form)
+                       case models.IsErrGPGNoEmailFound(err):
+                               loadKeysData(ctx)
+
+                               ctx.Data["Err_Content"] = true
+                               ctx.RenderWithErr(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form)
+                       default:
+                               ctx.ServerError("AddPublicKey", err)
+                       }
+                       return
+               }
+               keyIDs := ""
+               for _, key := range keys {
+                       keyIDs += key.KeyID
+                       keyIDs += ", "
+               }
+               if len(keyIDs) > 0 {
+                       keyIDs = keyIDs[:len(keyIDs)-2]
+               }
+               ctx.Flash.Success(ctx.Tr("settings.add_gpg_key_success", keyIDs))
+               ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
+       case "ssh":
+               content, err := models.CheckPublicKeyString(form.Content)
+               if err != nil {
+                       if models.IsErrSSHDisabled(err) {
+                               ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
+                       } else if models.IsErrKeyUnableVerify(err) {
+                               ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
+                       } else {
+                               ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error()))
+                       }
+                       ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
+                       return
+               }
+
+               if _, err = models.AddPublicKey(ctx.User.ID, form.Title, content, 0); err != nil {
+                       ctx.Data["HasSSHError"] = true
+                       switch {
+                       case models.IsErrKeyAlreadyExist(err):
+                               loadKeysData(ctx)
+
+                               ctx.Data["Err_Content"] = true
+                               ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplSettingsKeys, &form)
+                       case models.IsErrKeyNameAlreadyUsed(err):
+                               loadKeysData(ctx)
+
+                               ctx.Data["Err_Title"] = true
+                               ctx.RenderWithErr(ctx.Tr("settings.ssh_key_name_used"), tplSettingsKeys, &form)
+                       case models.IsErrKeyUnableVerify(err):
+                               ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
+                               ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
+                       default:
+                               ctx.ServerError("AddPublicKey", err)
+                       }
+                       return
+               }
+               ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title))
+               ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
+
+       default:
+               ctx.Flash.Warning("Function not implemented")
+               ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
+       }
+
+}
+
+// DeleteKey response for delete user's SSH/GPG key
+func DeleteKey(ctx *context.Context) {
+
+       switch ctx.Query("type") {
+       case "gpg":
+               if err := models.DeleteGPGKey(ctx.User, ctx.QueryInt64("id")); err != nil {
+                       ctx.Flash.Error("DeleteGPGKey: " + err.Error())
+               } else {
+                       ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success"))
+               }
+       case "ssh":
+               keyID := ctx.QueryInt64("id")
+               external, err := models.PublicKeyIsExternallyManaged(keyID)
+               if err != nil {
+                       ctx.ServerError("sshKeysExternalManaged", err)
+                       return
+               }
+               if external {
+                       ctx.Flash.Error(ctx.Tr("setting.ssh_externally_managed"))
+                       ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
+                       return
+               }
+               if err := models.DeletePublicKey(ctx.User, keyID); err != nil {
+                       ctx.Flash.Error("DeletePublicKey: " + err.Error())
+               } else {
+                       ctx.Flash.Success(ctx.Tr("settings.ssh_key_deletion_success"))
+               }
+       case "principal":
+               if err := models.DeletePublicKey(ctx.User, ctx.QueryInt64("id")); err != nil {
+                       ctx.Flash.Error("DeletePublicKey: " + err.Error())
+               } else {
+                       ctx.Flash.Success(ctx.Tr("settings.ssh_principal_deletion_success"))
+               }
+       default:
+               ctx.Flash.Warning("Function not implemented")
+               ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
+       }
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": setting.AppSubURL + "/user/settings/keys",
+       })
+}
+
+func loadKeysData(ctx *context.Context) {
+       keys, err := models.ListPublicKeys(ctx.User.ID, models.ListOptions{})
+       if err != nil {
+               ctx.ServerError("ListPublicKeys", err)
+               return
+       }
+       ctx.Data["Keys"] = keys
+
+       externalKeys, err := models.PublicKeysAreExternallyManaged(keys)
+       if err != nil {
+               ctx.ServerError("ListPublicKeys", err)
+               return
+       }
+       ctx.Data["ExternalKeys"] = externalKeys
+
+       gpgkeys, err := models.ListGPGKeys(ctx.User.ID, models.ListOptions{})
+       if err != nil {
+               ctx.ServerError("ListGPGKeys", err)
+               return
+       }
+       ctx.Data["GPGKeys"] = gpgkeys
+
+       principals, err := models.ListPrincipalKeys(ctx.User.ID, models.ListOptions{})
+       if err != nil {
+               ctx.ServerError("ListPrincipalKeys", err)
+               return
+       }
+       ctx.Data["Principals"] = principals
+}
diff --git a/routers/web/user/setting/main_test.go b/routers/web/user/setting/main_test.go
new file mode 100644 (file)
index 0000000..daa3f7f
--- /dev/null
@@ -0,0 +1,16 @@
+// Copyright 2018 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 setting
+
+import (
+       "path/filepath"
+       "testing"
+
+       "code.gitea.io/gitea/models"
+)
+
+func TestMain(m *testing.M) {
+       models.MainTest(m, filepath.Join("..", "..", "..", ".."))
+}
diff --git a/routers/web/user/setting/oauth2.go b/routers/web/user/setting/oauth2.go
new file mode 100644 (file)
index 0000000..c8db6e8
--- /dev/null
@@ -0,0 +1,159 @@
+// Copyright 2019 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 setting
+
+import (
+       "fmt"
+       "net/http"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+)
+
+const (
+       tplSettingsOAuthApplications base.TplName = "user/settings/applications_oauth2_edit"
+)
+
+// OAuthApplicationsPost response for adding a oauth2 application
+func OAuthApplicationsPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.EditOAuth2ApplicationForm)
+       ctx.Data["Title"] = ctx.Tr("settings")
+       ctx.Data["PageIsSettingsApplications"] = true
+
+       if ctx.HasError() {
+               loadApplicationsData(ctx)
+
+               ctx.HTML(http.StatusOK, tplSettingsApplications)
+               return
+       }
+       // TODO validate redirect URI
+       app, err := models.CreateOAuth2Application(models.CreateOAuth2ApplicationOptions{
+               Name:         form.Name,
+               RedirectURIs: []string{form.RedirectURI},
+               UserID:       ctx.User.ID,
+       })
+       if err != nil {
+               ctx.ServerError("CreateOAuth2Application", err)
+               return
+       }
+       ctx.Flash.Success(ctx.Tr("settings.create_oauth2_application_success"))
+       ctx.Data["App"] = app
+       ctx.Data["ClientSecret"], err = app.GenerateClientSecret()
+       if err != nil {
+               ctx.ServerError("GenerateClientSecret", err)
+               return
+       }
+       ctx.HTML(http.StatusOK, tplSettingsOAuthApplications)
+}
+
+// OAuthApplicationsEdit response for editing oauth2 application
+func OAuthApplicationsEdit(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.EditOAuth2ApplicationForm)
+       ctx.Data["Title"] = ctx.Tr("settings")
+       ctx.Data["PageIsSettingsApplications"] = true
+
+       if ctx.HasError() {
+               loadApplicationsData(ctx)
+
+               ctx.HTML(http.StatusOK, tplSettingsApplications)
+               return
+       }
+       // TODO validate redirect URI
+       var err error
+       if ctx.Data["App"], err = models.UpdateOAuth2Application(models.UpdateOAuth2ApplicationOptions{
+               ID:           ctx.ParamsInt64("id"),
+               Name:         form.Name,
+               RedirectURIs: []string{form.RedirectURI},
+               UserID:       ctx.User.ID,
+       }); err != nil {
+               ctx.ServerError("UpdateOAuth2Application", err)
+               return
+       }
+       ctx.Flash.Success(ctx.Tr("settings.update_oauth2_application_success"))
+       ctx.HTML(http.StatusOK, tplSettingsOAuthApplications)
+}
+
+// OAuthApplicationsRegenerateSecret handles the post request for regenerating the secret
+func OAuthApplicationsRegenerateSecret(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("settings")
+       ctx.Data["PageIsSettingsApplications"] = true
+
+       app, err := models.GetOAuth2ApplicationByID(ctx.ParamsInt64("id"))
+       if err != nil {
+               if models.IsErrOAuthApplicationNotFound(err) {
+                       ctx.NotFound("Application not found", err)
+                       return
+               }
+               ctx.ServerError("GetOAuth2ApplicationByID", err)
+               return
+       }
+       if app.UID != ctx.User.ID {
+               ctx.NotFound("Application not found", nil)
+               return
+       }
+       ctx.Data["App"] = app
+       ctx.Data["ClientSecret"], err = app.GenerateClientSecret()
+       if err != nil {
+               ctx.ServerError("GenerateClientSecret", err)
+               return
+       }
+       ctx.Flash.Success(ctx.Tr("settings.update_oauth2_application_success"))
+       ctx.HTML(http.StatusOK, tplSettingsOAuthApplications)
+}
+
+// OAuth2ApplicationShow displays the given application
+func OAuth2ApplicationShow(ctx *context.Context) {
+       app, err := models.GetOAuth2ApplicationByID(ctx.ParamsInt64("id"))
+       if err != nil {
+               if models.IsErrOAuthApplicationNotFound(err) {
+                       ctx.NotFound("Application not found", err)
+                       return
+               }
+               ctx.ServerError("GetOAuth2ApplicationByID", err)
+               return
+       }
+       if app.UID != ctx.User.ID {
+               ctx.NotFound("Application not found", nil)
+               return
+       }
+       ctx.Data["App"] = app
+       ctx.HTML(http.StatusOK, tplSettingsOAuthApplications)
+}
+
+// DeleteOAuth2Application deletes the given oauth2 application
+func DeleteOAuth2Application(ctx *context.Context) {
+       if err := models.DeleteOAuth2Application(ctx.QueryInt64("id"), ctx.User.ID); err != nil {
+               ctx.ServerError("DeleteOAuth2Application", err)
+               return
+       }
+       log.Trace("OAuth2 Application deleted: %s", ctx.User.Name)
+
+       ctx.Flash.Success(ctx.Tr("settings.remove_oauth2_application_success"))
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": setting.AppSubURL + "/user/settings/applications",
+       })
+}
+
+// RevokeOAuth2Grant revokes the grant with the given id
+func RevokeOAuth2Grant(ctx *context.Context) {
+       if ctx.User.ID == 0 || ctx.QueryInt64("id") == 0 {
+               ctx.ServerError("RevokeOAuth2Grant", fmt.Errorf("user id or grant id is zero"))
+               return
+       }
+       if err := models.RevokeOAuth2Grant(ctx.QueryInt64("id"), ctx.User.ID); err != nil {
+               ctx.ServerError("RevokeOAuth2Grant", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("settings.revoke_oauth2_grant_success"))
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": setting.AppSubURL + "/user/settings/applications",
+       })
+}
diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go
new file mode 100644 (file)
index 0000000..20042ca
--- /dev/null
@@ -0,0 +1,319 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2018 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 setting
+
+import (
+       "errors"
+       "fmt"
+       "io/ioutil"
+       "net/http"
+       "os"
+       "path/filepath"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/typesniffer"
+       "code.gitea.io/gitea/modules/util"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/modules/web/middleware"
+       "code.gitea.io/gitea/services/forms"
+
+       "github.com/unknwon/i18n"
+)
+
+const (
+       tplSettingsProfile      base.TplName = "user/settings/profile"
+       tplSettingsOrganization base.TplName = "user/settings/organization"
+       tplSettingsRepositories base.TplName = "user/settings/repos"
+)
+
+// Profile render user's profile page
+func Profile(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("settings")
+       ctx.Data["PageIsSettingsProfile"] = true
+
+       ctx.HTML(http.StatusOK, tplSettingsProfile)
+}
+
+// HandleUsernameChange handle username changes from user settings and admin interface
+func HandleUsernameChange(ctx *context.Context, user *models.User, newName string) error {
+       // Non-local users are not allowed to change their username.
+       if !user.IsLocal() {
+               ctx.Flash.Error(ctx.Tr("form.username_change_not_local_user"))
+               return fmt.Errorf(ctx.Tr("form.username_change_not_local_user"))
+       }
+
+       // Check if user name has been changed
+       if user.LowerName != strings.ToLower(newName) {
+               if err := models.ChangeUserName(user, newName); err != nil {
+                       switch {
+                       case models.IsErrUserAlreadyExist(err):
+                               ctx.Flash.Error(ctx.Tr("form.username_been_taken"))
+                       case models.IsErrEmailAlreadyUsed(err):
+                               ctx.Flash.Error(ctx.Tr("form.email_been_used"))
+                       case models.IsErrNameReserved(err):
+                               ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName))
+                       case models.IsErrNamePatternNotAllowed(err):
+                               ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName))
+                       case models.IsErrNameCharsNotAllowed(err):
+                               ctx.Flash.Error(ctx.Tr("user.form.name_chars_not_allowed", newName))
+                       default:
+                               ctx.ServerError("ChangeUserName", err)
+                       }
+                       return err
+               }
+       } else {
+               if err := models.UpdateRepositoryOwnerNames(user.ID, newName); err != nil {
+                       ctx.ServerError("UpdateRepository", err)
+                       return err
+               }
+       }
+       log.Trace("User name changed: %s -> %s", user.Name, newName)
+       return nil
+}
+
+// ProfilePost response for change user's profile
+func ProfilePost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.UpdateProfileForm)
+       ctx.Data["Title"] = ctx.Tr("settings")
+       ctx.Data["PageIsSettingsProfile"] = true
+
+       if ctx.HasError() {
+               ctx.HTML(http.StatusOK, tplSettingsProfile)
+               return
+       }
+
+       if len(form.Name) != 0 && ctx.User.Name != form.Name {
+               log.Debug("Changing name for %s to %s", ctx.User.Name, form.Name)
+               if err := HandleUsernameChange(ctx, ctx.User, form.Name); err != nil {
+                       ctx.Redirect(setting.AppSubURL + "/user/settings")
+                       return
+               }
+               ctx.User.Name = form.Name
+               ctx.User.LowerName = strings.ToLower(form.Name)
+       }
+
+       ctx.User.FullName = form.FullName
+       ctx.User.KeepEmailPrivate = form.KeepEmailPrivate
+       ctx.User.Website = form.Website
+       ctx.User.Location = form.Location
+       if len(form.Language) != 0 {
+               if !util.IsStringInSlice(form.Language, setting.Langs) {
+                       ctx.Flash.Error(ctx.Tr("settings.update_language_not_found", form.Language))
+                       ctx.Redirect(setting.AppSubURL + "/user/settings")
+                       return
+               }
+               ctx.User.Language = form.Language
+       }
+       ctx.User.Description = form.Description
+       ctx.User.KeepActivityPrivate = form.KeepActivityPrivate
+       if err := models.UpdateUserSetting(ctx.User); err != nil {
+               if _, ok := err.(models.ErrEmailAlreadyUsed); ok {
+                       ctx.Flash.Error(ctx.Tr("form.email_been_used"))
+                       ctx.Redirect(setting.AppSubURL + "/user/settings")
+                       return
+               }
+               ctx.ServerError("UpdateUser", err)
+               return
+       }
+
+       // Update the language to the one we just set
+       middleware.SetLocaleCookie(ctx.Resp, ctx.User.Language, 0)
+
+       log.Trace("User settings updated: %s", ctx.User.Name)
+       ctx.Flash.Success(i18n.Tr(ctx.User.Language, "settings.update_profile_success"))
+       ctx.Redirect(setting.AppSubURL + "/user/settings")
+}
+
+// UpdateAvatarSetting update user's avatar
+// FIXME: limit size.
+func UpdateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, ctxUser *models.User) error {
+       ctxUser.UseCustomAvatar = form.Source == forms.AvatarLocal
+       if len(form.Gravatar) > 0 {
+               if form.Avatar != nil {
+                       ctxUser.Avatar = base.EncodeMD5(form.Gravatar)
+               } else {
+                       ctxUser.Avatar = ""
+               }
+               ctxUser.AvatarEmail = form.Gravatar
+       }
+
+       if form.Avatar != nil && form.Avatar.Filename != "" {
+               fr, err := form.Avatar.Open()
+               if err != nil {
+                       return fmt.Errorf("Avatar.Open: %v", err)
+               }
+               defer fr.Close()
+
+               if form.Avatar.Size > setting.Avatar.MaxFileSize {
+                       return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big"))
+               }
+
+               data, err := ioutil.ReadAll(fr)
+               if err != nil {
+                       return fmt.Errorf("ioutil.ReadAll: %v", err)
+               }
+
+               st := typesniffer.DetectContentType(data)
+               if !(st.IsImage() && !st.IsSvgImage()) {
+                       return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image"))
+               }
+               if err = ctxUser.UploadAvatar(data); err != nil {
+                       return fmt.Errorf("UploadAvatar: %v", err)
+               }
+       } else if ctxUser.UseCustomAvatar && ctxUser.Avatar == "" {
+               // No avatar is uploaded but setting has been changed to enable,
+               // generate a random one when needed.
+               if err := ctxUser.GenerateRandomAvatar(); err != nil {
+                       log.Error("GenerateRandomAvatar[%d]: %v", ctxUser.ID, err)
+               }
+       }
+
+       if err := models.UpdateUserCols(ctxUser, "avatar", "avatar_email", "use_custom_avatar"); err != nil {
+               return fmt.Errorf("UpdateUser: %v", err)
+       }
+
+       return nil
+}
+
+// AvatarPost response for change user's avatar request
+func AvatarPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.AvatarForm)
+       if err := UpdateAvatarSetting(ctx, form, ctx.User); err != nil {
+               ctx.Flash.Error(err.Error())
+       } else {
+               ctx.Flash.Success(ctx.Tr("settings.update_avatar_success"))
+       }
+
+       ctx.Redirect(setting.AppSubURL + "/user/settings")
+}
+
+// DeleteAvatar render delete avatar page
+func DeleteAvatar(ctx *context.Context) {
+       if err := ctx.User.DeleteAvatar(); err != nil {
+               ctx.Flash.Error(err.Error())
+       }
+
+       ctx.Redirect(setting.AppSubURL + "/user/settings")
+}
+
+// Organization render all the organization of the user
+func Organization(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("settings")
+       ctx.Data["PageIsSettingsOrganization"] = true
+       orgs, err := models.GetOrgsByUserID(ctx.User.ID, ctx.IsSigned)
+       if err != nil {
+               ctx.ServerError("GetOrgsByUserID", err)
+               return
+       }
+       ctx.Data["Orgs"] = orgs
+       ctx.HTML(http.StatusOK, tplSettingsOrganization)
+}
+
+// Repos display a list of all repositories of the user
+func Repos(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("settings")
+       ctx.Data["PageIsSettingsRepos"] = true
+       ctx.Data["allowAdopt"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories
+       ctx.Data["allowDelete"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories
+
+       opts := models.ListOptions{
+               PageSize: setting.UI.Admin.UserPagingNum,
+               Page:     ctx.QueryInt("page"),
+       }
+
+       if opts.Page <= 0 {
+               opts.Page = 1
+       }
+       start := (opts.Page - 1) * opts.PageSize
+       end := start + opts.PageSize
+
+       adoptOrDelete := ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories)
+
+       ctxUser := ctx.User
+       count := 0
+
+       if adoptOrDelete {
+               repoNames := make([]string, 0, setting.UI.Admin.UserPagingNum)
+               repos := map[string]*models.Repository{}
+               // We're going to iterate by pagesize.
+               root := filepath.Join(models.UserPath(ctxUser.Name))
+               if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
+                       if err != nil {
+                               if os.IsNotExist(err) {
+                                       return nil
+                               }
+                               return err
+                       }
+                       if !info.IsDir() || path == root {
+                               return nil
+                       }
+                       name := info.Name()
+                       if !strings.HasSuffix(name, ".git") {
+                               return filepath.SkipDir
+                       }
+                       name = name[:len(name)-4]
+                       if models.IsUsableRepoName(name) != nil || strings.ToLower(name) != name {
+                               return filepath.SkipDir
+                       }
+                       if count >= start && count < end {
+                               repoNames = append(repoNames, name)
+                       }
+                       count++
+                       return filepath.SkipDir
+               }); err != nil {
+                       ctx.ServerError("filepath.Walk", err)
+                       return
+               }
+
+               if err := ctxUser.GetRepositories(models.ListOptions{Page: 1, PageSize: setting.UI.Admin.UserPagingNum}, repoNames...); err != nil {
+                       ctx.ServerError("GetRepositories", err)
+                       return
+               }
+               for _, repo := range ctxUser.Repos {
+                       if repo.IsFork {
+                               if err := repo.GetBaseRepo(); err != nil {
+                                       ctx.ServerError("GetBaseRepo", err)
+                                       return
+                               }
+                       }
+                       repos[repo.LowerName] = repo
+               }
+               ctx.Data["Dirs"] = repoNames
+               ctx.Data["ReposMap"] = repos
+       } else {
+               var err error
+               var count64 int64
+               ctxUser.Repos, count64, err = models.GetUserRepositories(&models.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts})
+
+               if err != nil {
+                       ctx.ServerError("GetRepositories", err)
+                       return
+               }
+               count = int(count64)
+               repos := ctxUser.Repos
+
+               for i := range repos {
+                       if repos[i].IsFork {
+                               if err := repos[i].GetBaseRepo(); err != nil {
+                                       ctx.ServerError("GetBaseRepo", err)
+                                       return
+                               }
+                       }
+               }
+
+               ctx.Data["Repos"] = repos
+       }
+       ctx.Data["Owner"] = ctxUser
+       pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
+       pager.SetDefaultParams(ctx)
+       ctx.Data["Page"] = pager
+       ctx.HTML(http.StatusOK, tplSettingsRepositories)
+}
diff --git a/routers/web/user/setting/security.go b/routers/web/user/setting/security.go
new file mode 100644 (file)
index 0000000..7753c5c
--- /dev/null
@@ -0,0 +1,111 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2018 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 setting
+
+import (
+       "net/http"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/base"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/setting"
+)
+
+const (
+       tplSettingsSecurity    base.TplName = "user/settings/security"
+       tplSettingsTwofaEnroll base.TplName = "user/settings/twofa_enroll"
+)
+
+// Security render change user's password page and 2FA
+func Security(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("settings")
+       ctx.Data["PageIsSettingsSecurity"] = true
+       ctx.Data["RequireU2F"] = true
+
+       if ctx.Query("openid.return_to") != "" {
+               settingsOpenIDVerify(ctx)
+               return
+       }
+
+       loadSecurityData(ctx)
+
+       ctx.HTML(http.StatusOK, tplSettingsSecurity)
+}
+
+// DeleteAccountLink delete a single account link
+func DeleteAccountLink(ctx *context.Context) {
+       id := ctx.QueryInt64("id")
+       if id <= 0 {
+               ctx.Flash.Error("Account link id is not given")
+       } else {
+               if _, err := models.RemoveAccountLink(ctx.User, id); err != nil {
+                       ctx.Flash.Error("RemoveAccountLink: " + err.Error())
+               } else {
+                       ctx.Flash.Success(ctx.Tr("settings.remove_account_link_success"))
+               }
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": setting.AppSubURL + "/user/settings/security",
+       })
+}
+
+func loadSecurityData(ctx *context.Context) {
+       enrolled := true
+       _, err := models.GetTwoFactorByUID(ctx.User.ID)
+       if err != nil {
+               if models.IsErrTwoFactorNotEnrolled(err) {
+                       enrolled = false
+               } else {
+                       ctx.ServerError("SettingsTwoFactor", err)
+                       return
+               }
+       }
+       ctx.Data["TwofaEnrolled"] = enrolled
+       if enrolled {
+               ctx.Data["U2FRegistrations"], err = models.GetU2FRegistrationsByUID(ctx.User.ID)
+               if err != nil {
+                       ctx.ServerError("GetU2FRegistrationsByUID", err)
+                       return
+               }
+       }
+
+       tokens, err := models.ListAccessTokens(models.ListAccessTokensOptions{UserID: ctx.User.ID})
+       if err != nil {
+               ctx.ServerError("ListAccessTokens", err)
+               return
+       }
+       ctx.Data["Tokens"] = tokens
+
+       accountLinks, err := models.ListAccountLinks(ctx.User)
+       if err != nil {
+               ctx.ServerError("ListAccountLinks", err)
+               return
+       }
+
+       // map the provider display name with the LoginSource
+       sources := make(map[*models.LoginSource]string)
+       for _, externalAccount := range accountLinks {
+               if loginSource, err := models.GetLoginSourceByID(externalAccount.LoginSourceID); err == nil {
+                       var providerDisplayName string
+                       if loginSource.IsOAuth2() {
+                               providerTechnicalName := loginSource.OAuth2().Provider
+                               providerDisplayName = models.OAuth2Providers[providerTechnicalName].DisplayName
+                       } else {
+                               providerDisplayName = loginSource.Name
+                       }
+                       sources[loginSource] = providerDisplayName
+               }
+       }
+       ctx.Data["AccountLinks"] = sources
+
+       openid, err := models.GetUserOpenIDs(ctx.User.ID)
+       if err != nil {
+               ctx.ServerError("GetUserOpenIDs", err)
+               return
+       }
+       ctx.Data["OpenIDs"] = openid
+}
diff --git a/routers/web/user/setting/security_openid.go b/routers/web/user/setting/security_openid.go
new file mode 100644 (file)
index 0000000..74dba12
--- /dev/null
@@ -0,0 +1,129 @@
+// Copyright 2018 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 setting
+
+import (
+       "net/http"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/auth/openid"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+)
+
+// OpenIDPost response for change user's openid
+func OpenIDPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.AddOpenIDForm)
+       ctx.Data["Title"] = ctx.Tr("settings")
+       ctx.Data["PageIsSettingsSecurity"] = true
+
+       if ctx.HasError() {
+               loadSecurityData(ctx)
+
+               ctx.HTML(http.StatusOK, tplSettingsSecurity)
+               return
+       }
+
+       // WARNING: specifying a wrong OpenID here could lock
+       // a user out of her account, would be better to
+       // verify/confirm the new OpenID before storing it
+
+       // Also, consider allowing for multiple OpenID URIs
+
+       id, err := openid.Normalize(form.Openid)
+       if err != nil {
+               loadSecurityData(ctx)
+
+               ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &form)
+               return
+       }
+       form.Openid = id
+       log.Trace("Normalized id: " + id)
+
+       oids, err := models.GetUserOpenIDs(ctx.User.ID)
+       if err != nil {
+               ctx.ServerError("GetUserOpenIDs", err)
+               return
+       }
+       ctx.Data["OpenIDs"] = oids
+
+       // Check that the OpenID is not already used
+       for _, obj := range oids {
+               if obj.URI == id {
+                       loadSecurityData(ctx)
+
+                       ctx.RenderWithErr(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &form)
+                       return
+               }
+       }
+
+       redirectTo := setting.AppURL + "user/settings/security"
+       url, err := openid.RedirectURL(id, redirectTo, setting.AppURL)
+       if err != nil {
+               loadSecurityData(ctx)
+
+               ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &form)
+               return
+       }
+       ctx.Redirect(url)
+}
+
+func settingsOpenIDVerify(ctx *context.Context) {
+       log.Trace("Incoming call to: " + ctx.Req.URL.String())
+
+       fullURL := setting.AppURL + ctx.Req.URL.String()[1:]
+       log.Trace("Full URL: " + fullURL)
+
+       id, err := openid.Verify(fullURL)
+       if err != nil {
+               ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &forms.AddOpenIDForm{
+                       Openid: id,
+               })
+               return
+       }
+
+       log.Trace("Verified ID: " + id)
+
+       oid := &models.UserOpenID{UID: ctx.User.ID, URI: id}
+       if err = models.AddUserOpenID(oid); err != nil {
+               if models.IsErrOpenIDAlreadyUsed(err) {
+                       ctx.RenderWithErr(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &forms.AddOpenIDForm{Openid: id})
+                       return
+               }
+               ctx.ServerError("AddUserOpenID", err)
+               return
+       }
+       log.Trace("Associated OpenID %s to user %s", id, ctx.User.Name)
+       ctx.Flash.Success(ctx.Tr("settings.add_openid_success"))
+
+       ctx.Redirect(setting.AppSubURL + "/user/settings/security")
+}
+
+// DeleteOpenID response for delete user's openid
+func DeleteOpenID(ctx *context.Context) {
+       if err := models.DeleteUserOpenID(&models.UserOpenID{ID: ctx.QueryInt64("id"), UID: ctx.User.ID}); err != nil {
+               ctx.ServerError("DeleteUserOpenID", err)
+               return
+       }
+       log.Trace("OpenID address deleted: %s", ctx.User.Name)
+
+       ctx.Flash.Success(ctx.Tr("settings.openid_deletion_success"))
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": setting.AppSubURL + "/user/settings/security",
+       })
+}
+
+// ToggleOpenIDVisibility response for toggle visibility of user's openid
+func ToggleOpenIDVisibility(ctx *context.Context) {
+       if err := models.ToggleUserOpenIDVisibility(ctx.QueryInt64("id")); err != nil {
+               ctx.ServerError("ToggleUserOpenIDVisibility", err)
+               return
+       }
+
+       ctx.Redirect(setting.AppSubURL + "/user/settings/security")
+}
diff --git a/routers/web/user/setting/security_twofa.go b/routers/web/user/setting/security_twofa.go
new file mode 100644 (file)
index 0000000..7b08a05
--- /dev/null
@@ -0,0 +1,250 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2018 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 setting
+
+import (
+       "bytes"
+       "encoding/base64"
+       "html/template"
+       "image/png"
+       "net/http"
+       "strings"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+
+       "github.com/pquerna/otp"
+       "github.com/pquerna/otp/totp"
+)
+
+// RegenerateScratchTwoFactor regenerates the user's 2FA scratch code.
+func RegenerateScratchTwoFactor(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("settings")
+       ctx.Data["PageIsSettingsSecurity"] = true
+
+       t, err := models.GetTwoFactorByUID(ctx.User.ID)
+       if err != nil {
+               if models.IsErrTwoFactorNotEnrolled(err) {
+                       ctx.Flash.Error(ctx.Tr("setting.twofa_not_enrolled"))
+                       ctx.Redirect(setting.AppSubURL + "/user/settings/security")
+               }
+               ctx.ServerError("SettingsTwoFactor: Failed to GetTwoFactorByUID", err)
+               return
+       }
+
+       token, err := t.GenerateScratchToken()
+       if err != nil {
+               ctx.ServerError("SettingsTwoFactor: Failed to GenerateScratchToken", err)
+               return
+       }
+
+       if err = models.UpdateTwoFactor(t); err != nil {
+               ctx.ServerError("SettingsTwoFactor: Failed to UpdateTwoFactor", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("settings.twofa_scratch_token_regenerated", token))
+       ctx.Redirect(setting.AppSubURL + "/user/settings/security")
+}
+
+// DisableTwoFactor deletes the user's 2FA settings.
+func DisableTwoFactor(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("settings")
+       ctx.Data["PageIsSettingsSecurity"] = true
+
+       t, err := models.GetTwoFactorByUID(ctx.User.ID)
+       if err != nil {
+               if models.IsErrTwoFactorNotEnrolled(err) {
+                       ctx.Flash.Error(ctx.Tr("setting.twofa_not_enrolled"))
+                       ctx.Redirect(setting.AppSubURL + "/user/settings/security")
+               }
+               ctx.ServerError("SettingsTwoFactor: Failed to GetTwoFactorByUID", err)
+               return
+       }
+
+       if err = models.DeleteTwoFactorByID(t.ID, ctx.User.ID); err != nil {
+               if models.IsErrTwoFactorNotEnrolled(err) {
+                       // There is a potential DB race here - we must have been disabled by another request in the intervening period
+                       ctx.Flash.Success(ctx.Tr("settings.twofa_disabled"))
+                       ctx.Redirect(setting.AppSubURL + "/user/settings/security")
+               }
+               ctx.ServerError("SettingsTwoFactor: Failed to DeleteTwoFactorByID", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("settings.twofa_disabled"))
+       ctx.Redirect(setting.AppSubURL + "/user/settings/security")
+}
+
+func twofaGenerateSecretAndQr(ctx *context.Context) bool {
+       var otpKey *otp.Key
+       var err error
+       uri := ctx.Session.Get("twofaUri")
+       if uri != nil {
+               otpKey, err = otp.NewKeyFromURL(uri.(string))
+               if err != nil {
+                       ctx.ServerError("SettingsTwoFactor: Failed NewKeyFromURL: ", err)
+                       return false
+               }
+       }
+       // Filter unsafe character ':' in issuer
+       issuer := strings.ReplaceAll(setting.AppName+" ("+setting.Domain+")", ":", "")
+       if otpKey == nil {
+               otpKey, err = totp.Generate(totp.GenerateOpts{
+                       SecretSize:  40,
+                       Issuer:      issuer,
+                       AccountName: ctx.User.Name,
+               })
+               if err != nil {
+                       ctx.ServerError("SettingsTwoFactor: totpGenerate Failed", err)
+                       return false
+               }
+       }
+
+       ctx.Data["TwofaSecret"] = otpKey.Secret()
+       img, err := otpKey.Image(320, 240)
+       if err != nil {
+               ctx.ServerError("SettingsTwoFactor: otpKey image generation failed", err)
+               return false
+       }
+
+       var imgBytes bytes.Buffer
+       if err = png.Encode(&imgBytes, img); err != nil {
+               ctx.ServerError("SettingsTwoFactor: otpKey png encoding failed", err)
+               return false
+       }
+
+       ctx.Data["QrUri"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(imgBytes.Bytes()))
+
+       if err := ctx.Session.Set("twofaSecret", otpKey.Secret()); err != nil {
+               ctx.ServerError("SettingsTwoFactor: Failed to set session for twofaSecret", err)
+               return false
+       }
+
+       if err := ctx.Session.Set("twofaUri", otpKey.String()); err != nil {
+               ctx.ServerError("SettingsTwoFactor: Failed to set session for twofaUri", err)
+               return false
+       }
+
+       // Here we're just going to try to release the session early
+       if err := ctx.Session.Release(); err != nil {
+               // we'll tolerate errors here as they *should* get saved elsewhere
+               log.Error("Unable to save changes to the session: %v", err)
+       }
+       return true
+}
+
+// EnrollTwoFactor shows the page where the user can enroll into 2FA.
+func EnrollTwoFactor(ctx *context.Context) {
+       ctx.Data["Title"] = ctx.Tr("settings")
+       ctx.Data["PageIsSettingsSecurity"] = true
+
+       t, err := models.GetTwoFactorByUID(ctx.User.ID)
+       if t != nil {
+               // already enrolled - we should redirect back!
+               log.Warn("Trying to re-enroll %-v in twofa when already enrolled", ctx.User)
+               ctx.Flash.Error(ctx.Tr("setting.twofa_is_enrolled"))
+               ctx.Redirect(setting.AppSubURL + "/user/settings/security")
+               return
+       }
+       if err != nil && !models.IsErrTwoFactorNotEnrolled(err) {
+               ctx.ServerError("SettingsTwoFactor: GetTwoFactorByUID", err)
+               return
+       }
+
+       if !twofaGenerateSecretAndQr(ctx) {
+               return
+       }
+
+       ctx.HTML(http.StatusOK, tplSettingsTwofaEnroll)
+}
+
+// EnrollTwoFactorPost handles enrolling the user into 2FA.
+func EnrollTwoFactorPost(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.TwoFactorAuthForm)
+       ctx.Data["Title"] = ctx.Tr("settings")
+       ctx.Data["PageIsSettingsSecurity"] = true
+
+       t, err := models.GetTwoFactorByUID(ctx.User.ID)
+       if t != nil {
+               // already enrolled
+               ctx.Flash.Error(ctx.Tr("setting.twofa_is_enrolled"))
+               ctx.Redirect(setting.AppSubURL + "/user/settings/security")
+               return
+       }
+       if err != nil && !models.IsErrTwoFactorNotEnrolled(err) {
+               ctx.ServerError("SettingsTwoFactor: Failed to check if already enrolled with GetTwoFactorByUID", err)
+               return
+       }
+
+       if ctx.HasError() {
+               if !twofaGenerateSecretAndQr(ctx) {
+                       return
+               }
+               ctx.HTML(http.StatusOK, tplSettingsTwofaEnroll)
+               return
+       }
+
+       secretRaw := ctx.Session.Get("twofaSecret")
+       if secretRaw == nil {
+               ctx.Flash.Error(ctx.Tr("settings.twofa_failed_get_secret"))
+               ctx.Redirect(setting.AppSubURL + "/user/settings/security/two_factor/enroll")
+               return
+       }
+
+       secret := secretRaw.(string)
+       if !totp.Validate(form.Passcode, secret) {
+               if !twofaGenerateSecretAndQr(ctx) {
+                       return
+               }
+               ctx.Flash.Error(ctx.Tr("settings.passcode_invalid"))
+               ctx.Redirect(setting.AppSubURL + "/user/settings/security/two_factor/enroll")
+               return
+       }
+
+       t = &models.TwoFactor{
+               UID: ctx.User.ID,
+       }
+       err = t.SetSecret(secret)
+       if err != nil {
+               ctx.ServerError("SettingsTwoFactor: Failed to set secret", err)
+               return
+       }
+       token, err := t.GenerateScratchToken()
+       if err != nil {
+               ctx.ServerError("SettingsTwoFactor: Failed to generate scratch token", err)
+               return
+       }
+
+       // Now we have to delete the secrets - because if we fail to insert then it's highly likely that they have already been used
+       // If we can detect the unique constraint failure below we can move this to after the NewTwoFactor
+       if err := ctx.Session.Delete("twofaSecret"); err != nil {
+               // tolerate this failure - it's more important to continue
+               log.Error("Unable to delete twofaSecret from the session: Error: %v", err)
+       }
+       if err := ctx.Session.Delete("twofaUri"); err != nil {
+               // tolerate this failure - it's more important to continue
+               log.Error("Unable to delete twofaUri from the session: Error: %v", err)
+       }
+       if err := ctx.Session.Release(); err != nil {
+               // tolerate this failure - it's more important to continue
+               log.Error("Unable to save changes to the session: %v", err)
+       }
+
+       if err = models.NewTwoFactor(t); err != nil {
+               // FIXME: We need to handle a unique constraint fail here it's entirely possible that another request has beaten us.
+               // If there is a unique constraint fail we should just tolerate the error
+               ctx.ServerError("SettingsTwoFactor: Failed to save two factor", err)
+               return
+       }
+
+       ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", token))
+       ctx.Redirect(setting.AppSubURL + "/user/settings/security")
+}
diff --git a/routers/web/user/setting/security_u2f.go b/routers/web/user/setting/security_u2f.go
new file mode 100644 (file)
index 0000000..f9e3554
--- /dev/null
@@ -0,0 +1,111 @@
+// Copyright 2018 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 setting
+
+import (
+       "errors"
+       "net/http"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/services/forms"
+
+       "github.com/tstranex/u2f"
+)
+
+// U2FRegister initializes the u2f registration procedure
+func U2FRegister(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.U2FRegistrationForm)
+       if form.Name == "" {
+               ctx.Error(http.StatusConflict)
+               return
+       }
+       challenge, err := u2f.NewChallenge(setting.U2F.AppID, setting.U2F.TrustedFacets)
+       if err != nil {
+               ctx.ServerError("NewChallenge", err)
+               return
+       }
+       if err := ctx.Session.Set("u2fChallenge", challenge); err != nil {
+               ctx.ServerError("Unable to set session key for u2fChallenge", err)
+               return
+       }
+       regs, err := models.GetU2FRegistrationsByUID(ctx.User.ID)
+       if err != nil {
+               ctx.ServerError("GetU2FRegistrationsByUID", err)
+               return
+       }
+       for _, reg := range regs {
+               if reg.Name == form.Name {
+                       ctx.Error(http.StatusConflict, "Name already taken")
+                       return
+               }
+       }
+       if err := ctx.Session.Set("u2fName", form.Name); err != nil {
+               ctx.ServerError("Unable to set session key for u2fName", err)
+               return
+       }
+       // Here we're just going to try to release the session early
+       if err := ctx.Session.Release(); err != nil {
+               // we'll tolerate errors here as they *should* get saved elsewhere
+               log.Error("Unable to save changes to the session: %v", err)
+       }
+       ctx.JSON(http.StatusOK, u2f.NewWebRegisterRequest(challenge, regs.ToRegistrations()))
+}
+
+// U2FRegisterPost receives the response of the security key
+func U2FRegisterPost(ctx *context.Context) {
+       response := web.GetForm(ctx).(*u2f.RegisterResponse)
+       challSess := ctx.Session.Get("u2fChallenge")
+       u2fName := ctx.Session.Get("u2fName")
+       if challSess == nil || u2fName == nil {
+               ctx.ServerError("U2FRegisterPost", errors.New("not in U2F session"))
+               return
+       }
+       challenge := challSess.(*u2f.Challenge)
+       name := u2fName.(string)
+       config := &u2f.Config{
+               // Chrome 66+ doesn't return the device's attestation
+               // certificate by default.
+               SkipAttestationVerify: true,
+       }
+       reg, err := u2f.Register(*response, *challenge, config)
+       if err != nil {
+               ctx.ServerError("u2f.Register", err)
+               return
+       }
+       if _, err = models.CreateRegistration(ctx.User, name, reg); err != nil {
+               ctx.ServerError("u2f.Register", err)
+               return
+       }
+       ctx.Status(200)
+}
+
+// U2FDelete deletes an security key by id
+func U2FDelete(ctx *context.Context) {
+       form := web.GetForm(ctx).(*forms.U2FDeleteForm)
+       reg, err := models.GetU2FRegistrationByID(form.ID)
+       if err != nil {
+               if models.IsErrU2FRegistrationNotExist(err) {
+                       ctx.Status(200)
+                       return
+               }
+               ctx.ServerError("GetU2FRegistrationByID", err)
+               return
+       }
+       if reg.UserID != ctx.User.ID {
+               ctx.Status(401)
+               return
+       }
+       if err := models.DeleteRegistration(reg); err != nil {
+               ctx.ServerError("DeleteRegistration", err)
+               return
+       }
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "redirect": setting.AppSubURL + "/user/settings/security",
+       })
+}
diff --git a/routers/web/user/task.go b/routers/web/user/task.go
new file mode 100644 (file)
index 0000000..b8df5d9
--- /dev/null
@@ -0,0 +1,32 @@
+// 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 user
+
+import (
+       "net/http"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/context"
+)
+
+// TaskStatus returns task's status
+func TaskStatus(ctx *context.Context) {
+       task, opts, err := models.GetMigratingTaskByID(ctx.ParamsInt64("task"), ctx.User.ID)
+       if err != nil {
+               ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
+                       "err": err,
+               })
+               return
+       }
+
+       ctx.JSON(http.StatusOK, map[string]interface{}{
+               "status":    task.Status,
+               "err":       task.Errors,
+               "repo-id":   task.RepoID,
+               "repo-name": opts.RepoName,
+               "start":     task.StartTime,
+               "end":       task.EndTime,
+       })
+}
diff --git a/routers/web/web.go b/routers/web/web.go
new file mode 100644 (file)
index 0000000..6c0141e
--- /dev/null
@@ -0,0 +1,1023 @@
+// 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 web
+
+import (
+       "encoding/gob"
+       "net/http"
+       "os"
+       "path"
+
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/httpcache"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/metrics"
+       "code.gitea.io/gitea/modules/public"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/storage"
+       "code.gitea.io/gitea/modules/templates"
+       "code.gitea.io/gitea/modules/validation"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/routers/api/v1/misc"
+       "code.gitea.io/gitea/routers/common"
+       "code.gitea.io/gitea/routers/web/admin"
+       "code.gitea.io/gitea/routers/web/dev"
+       "code.gitea.io/gitea/routers/web/events"
+       "code.gitea.io/gitea/routers/web/explore"
+       "code.gitea.io/gitea/routers/web/org"
+       "code.gitea.io/gitea/routers/web/repo"
+       "code.gitea.io/gitea/routers/web/user"
+       userSetting "code.gitea.io/gitea/routers/web/user/setting"
+       "code.gitea.io/gitea/services/forms"
+       "code.gitea.io/gitea/services/lfs"
+       "code.gitea.io/gitea/services/mailer"
+
+       // to registers all internal adapters
+       _ "code.gitea.io/gitea/modules/session"
+
+       "gitea.com/go-chi/captcha"
+       "gitea.com/go-chi/session"
+       "github.com/NYTimes/gziphandler"
+       "github.com/go-chi/chi/middleware"
+       "github.com/go-chi/cors"
+       "github.com/prometheus/client_golang/prometheus"
+       "github.com/tstranex/u2f"
+)
+
+const (
+       // GzipMinSize represents min size to compress for the body size of response
+       GzipMinSize = 1400
+)
+
+// CorsHandler return a http handler who set CORS options if enabled by config
+func CorsHandler() func(next http.Handler) http.Handler {
+       if setting.CORSConfig.Enabled {
+               return cors.Handler(cors.Options{
+                       //Scheme:           setting.CORSConfig.Scheme, // FIXME: the cors middleware needs scheme option
+                       AllowedOrigins: setting.CORSConfig.AllowDomain,
+                       //setting.CORSConfig.AllowSubdomain // FIXME: the cors middleware needs allowSubdomain option
+                       AllowedMethods:   setting.CORSConfig.Methods,
+                       AllowCredentials: setting.CORSConfig.AllowCredentials,
+                       MaxAge:           int(setting.CORSConfig.MaxAge.Seconds()),
+               })
+       }
+
+       return func(next http.Handler) http.Handler {
+               return next
+       }
+}
+
+// Routes returns all web routes
+func Routes() *web.Route {
+       routes := web.NewRoute()
+
+       routes.Use(public.AssetsHandler(&public.Options{
+               Directory:   path.Join(setting.StaticRootPath, "public"),
+               Prefix:      "/assets",
+               CorsHandler: CorsHandler(),
+       }))
+
+       routes.Use(session.Sessioner(session.Options{
+               Provider:       setting.SessionConfig.Provider,
+               ProviderConfig: setting.SessionConfig.ProviderConfig,
+               CookieName:     setting.SessionConfig.CookieName,
+               CookiePath:     setting.SessionConfig.CookiePath,
+               Gclifetime:     setting.SessionConfig.Gclifetime,
+               Maxlifetime:    setting.SessionConfig.Maxlifetime,
+               Secure:         setting.SessionConfig.Secure,
+               SameSite:       setting.SessionConfig.SameSite,
+               Domain:         setting.SessionConfig.Domain,
+       }))
+
+       routes.Use(Recovery())
+
+       // We use r.Route here over r.Use because this prevents requests that are not for avatars having to go through this additional handler
+       routes.Route("/avatars/*", "GET, HEAD", storageHandler(setting.Avatar.Storage, "avatars", storage.Avatars))
+       routes.Route("/repo-avatars/*", "GET, HEAD", storageHandler(setting.RepoAvatar.Storage, "repo-avatars", storage.RepoAvatars))
+
+       // for health check - doeesn't need to be passed through gzip handler
+       routes.Head("/", func(w http.ResponseWriter, req *http.Request) {
+               w.WriteHeader(http.StatusOK)
+       })
+
+       // this png is very likely to always be below the limit for gzip so it doesn't need to pass through gzip
+       routes.Get("/apple-touch-icon.png", func(w http.ResponseWriter, req *http.Request) {
+               http.Redirect(w, req, path.Join(setting.StaticURLPrefix, "/assets/img/apple-touch-icon.png"), 301)
+       })
+
+       gob.Register(&u2f.Challenge{})
+
+       common := []interface{}{}
+
+       if setting.EnableGzip {
+               h, err := gziphandler.GzipHandlerWithOpts(gziphandler.MinSize(GzipMinSize))
+               if err != nil {
+                       log.Fatal("GzipHandlerWithOpts failed: %v", err)
+               }
+               common = append(common, h)
+       }
+
+       mailer.InitMailRender(templates.Mailer())
+
+       if setting.Service.EnableCaptcha {
+               // The captcha http.Handler should only fire on /captcha/* so we can just mount this on that url
+               routes.Route("/captcha/*", "GET,HEAD", append(common, captcha.Captchaer(context.GetImageCaptcha()))...)
+       }
+
+       if setting.HasRobotsTxt {
+               routes.Get("/robots.txt", append(common, func(w http.ResponseWriter, req *http.Request) {
+                       filePath := path.Join(setting.CustomPath, "robots.txt")
+                       fi, err := os.Stat(filePath)
+                       if err == nil && httpcache.HandleTimeCache(req, w, fi) {
+                               return
+                       }
+                       http.ServeFile(w, req, filePath)
+               })...)
+       }
+
+       // prometheus metrics endpoint - do not need to go through contexter
+       if setting.Metrics.Enabled {
+               c := metrics.NewCollector()
+               prometheus.MustRegister(c)
+
+               routes.Get("/metrics", append(common, Metrics)...)
+       }
+
+       // Removed: toolbox.Toolboxer middleware will provide debug informations which seems unnecessary
+       common = append(common, context.Contexter())
+
+       // GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route
+       common = append(common, middleware.GetHead)
+
+       if setting.API.EnableSwagger {
+               // Note: The route moved from apiroutes because it's in fact want to render a web page
+               routes.Get("/api/swagger", append(common, misc.Swagger)...) // Render V1 by default
+       }
+
+       // TODO: These really seem like things that could be folded into Contexter or as helper functions
+       common = append(common, user.GetNotificationCount)
+       common = append(common, repo.GetActiveStopwatch)
+       common = append(common, goGet)
+
+       others := web.NewRoute()
+       for _, middle := range common {
+               others.Use(middle)
+       }
+
+       RegisterRoutes(others)
+       routes.Mount("", others)
+       return routes
+}
+
+// RegisterRoutes register routes
+func RegisterRoutes(m *web.Route) {
+       reqSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: true})
+       ignSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: setting.Service.RequireSignInView})
+       ignExploreSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView})
+       ignSignInAndCsrf := context.Toggle(&context.ToggleOptions{DisableCSRF: true})
+       reqSignOut := context.Toggle(&context.ToggleOptions{SignOutRequired: true})
+
+       //bindIgnErr := binding.BindIgnErr
+       bindIgnErr := web.Bind
+       validation.AddBindingRules()
+
+       openIDSignInEnabled := func(ctx *context.Context) {
+               if !setting.Service.EnableOpenIDSignIn {
+                       ctx.Error(http.StatusForbidden)
+                       return
+               }
+       }
+
+       openIDSignUpEnabled := func(ctx *context.Context) {
+               if !setting.Service.EnableOpenIDSignUp {
+                       ctx.Error(http.StatusForbidden)
+                       return
+               }
+       }
+
+       reqMilestonesDashboardPageEnabled := func(ctx *context.Context) {
+               if !setting.Service.ShowMilestonesDashboardPage {
+                       ctx.Error(http.StatusForbidden)
+                       return
+               }
+       }
+
+       // webhooksEnabled requires webhooks to be enabled by admin.
+       webhooksEnabled := func(ctx *context.Context) {
+               if setting.DisableWebhooks {
+                       ctx.Error(http.StatusForbidden)
+                       return
+               }
+       }
+
+       lfsServerEnabled := func(ctx *context.Context) {
+               if !setting.LFS.StartServer {
+                       ctx.Error(http.StatusNotFound)
+                       return
+               }
+       }
+
+       // FIXME: not all routes need go through same middleware.
+       // Especially some AJAX requests, we can reduce middleware number to improve performance.
+       // Routers.
+       // for health check
+       m.Get("/", Home)
+       m.Get("/.well-known/openid-configuration", user.OIDCWellKnown)
+       m.Group("/explore", func() {
+               m.Get("", func(ctx *context.Context) {
+                       ctx.Redirect(setting.AppSubURL + "/explore/repos")
+               })
+               m.Get("/repos", explore.Repos)
+               m.Get("/users", explore.Users)
+               m.Get("/organizations", explore.Organizations)
+               m.Get("/code", explore.Code)
+       }, ignExploreSignIn)
+       m.Get("/issues", reqSignIn, user.Issues)
+       m.Get("/pulls", reqSignIn, user.Pulls)
+       m.Get("/milestones", reqSignIn, reqMilestonesDashboardPageEnabled, user.Milestones)
+
+       // ***** START: User *****
+       m.Group("/user", func() {
+               m.Get("/login", user.SignIn)
+               m.Post("/login", bindIgnErr(forms.SignInForm{}), user.SignInPost)
+               m.Group("", func() {
+                       m.Combo("/login/openid").
+                               Get(user.SignInOpenID).
+                               Post(bindIgnErr(forms.SignInOpenIDForm{}), user.SignInOpenIDPost)
+               }, openIDSignInEnabled)
+               m.Group("/openid", func() {
+                       m.Combo("/connect").
+                               Get(user.ConnectOpenID).
+                               Post(bindIgnErr(forms.ConnectOpenIDForm{}), user.ConnectOpenIDPost)
+                       m.Group("/register", func() {
+                               m.Combo("").
+                                       Get(user.RegisterOpenID, openIDSignUpEnabled).
+                                       Post(bindIgnErr(forms.SignUpOpenIDForm{}), user.RegisterOpenIDPost)
+                       }, openIDSignUpEnabled)
+               }, openIDSignInEnabled)
+               m.Get("/sign_up", user.SignUp)
+               m.Post("/sign_up", bindIgnErr(forms.RegisterForm{}), user.SignUpPost)
+               m.Group("/oauth2", func() {
+                       m.Get("/{provider}", user.SignInOAuth)
+                       m.Get("/{provider}/callback", user.SignInOAuthCallback)
+               })
+               m.Get("/link_account", user.LinkAccount)
+               m.Post("/link_account_signin", bindIgnErr(forms.SignInForm{}), user.LinkAccountPostSignIn)
+               m.Post("/link_account_signup", bindIgnErr(forms.RegisterForm{}), user.LinkAccountPostRegister)
+               m.Group("/two_factor", func() {
+                       m.Get("", user.TwoFactor)
+                       m.Post("", bindIgnErr(forms.TwoFactorAuthForm{}), user.TwoFactorPost)
+                       m.Get("/scratch", user.TwoFactorScratch)
+                       m.Post("/scratch", bindIgnErr(forms.TwoFactorScratchAuthForm{}), user.TwoFactorScratchPost)
+               })
+               m.Group("/u2f", func() {
+                       m.Get("", user.U2F)
+                       m.Get("/challenge", user.U2FChallenge)
+                       m.Post("/sign", bindIgnErr(u2f.SignResponse{}), user.U2FSign)
+
+               })
+       }, reqSignOut)
+
+       m.Any("/user/events", events.Events)
+
+       m.Group("/login/oauth", func() {
+               m.Get("/authorize", bindIgnErr(forms.AuthorizationForm{}), user.AuthorizeOAuth)
+               m.Post("/grant", bindIgnErr(forms.GrantApplicationForm{}), user.GrantApplicationOAuth)
+               // TODO manage redirection
+               m.Post("/authorize", bindIgnErr(forms.AuthorizationForm{}), user.AuthorizeOAuth)
+       }, ignSignInAndCsrf, reqSignIn)
+       m.Get("/login/oauth/userinfo", ignSignInAndCsrf, user.InfoOAuth)
+       m.Post("/login/oauth/access_token", CorsHandler(), bindIgnErr(forms.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth)
+
+       m.Group("/user/settings", func() {
+               m.Get("", userSetting.Profile)
+               m.Post("", bindIgnErr(forms.UpdateProfileForm{}), userSetting.ProfilePost)
+               m.Get("/change_password", user.MustChangePassword)
+               m.Post("/change_password", bindIgnErr(forms.MustChangePasswordForm{}), user.MustChangePasswordPost)
+               m.Post("/avatar", bindIgnErr(forms.AvatarForm{}), userSetting.AvatarPost)
+               m.Post("/avatar/delete", userSetting.DeleteAvatar)
+               m.Group("/account", func() {
+                       m.Combo("").Get(userSetting.Account).Post(bindIgnErr(forms.ChangePasswordForm{}), userSetting.AccountPost)
+                       m.Post("/email", bindIgnErr(forms.AddEmailForm{}), userSetting.EmailPost)
+                       m.Post("/email/delete", userSetting.DeleteEmail)
+                       m.Post("/delete", userSetting.DeleteAccount)
+                       m.Post("/theme", bindIgnErr(forms.UpdateThemeForm{}), userSetting.UpdateUIThemePost)
+               })
+               m.Group("/security", func() {
+                       m.Get("", userSetting.Security)
+                       m.Group("/two_factor", func() {
+                               m.Post("/regenerate_scratch", userSetting.RegenerateScratchTwoFactor)
+                               m.Post("/disable", userSetting.DisableTwoFactor)
+                               m.Get("/enroll", userSetting.EnrollTwoFactor)
+                               m.Post("/enroll", bindIgnErr(forms.TwoFactorAuthForm{}), userSetting.EnrollTwoFactorPost)
+                       })
+                       m.Group("/u2f", func() {
+                               m.Post("/request_register", bindIgnErr(forms.U2FRegistrationForm{}), userSetting.U2FRegister)
+                               m.Post("/register", bindIgnErr(u2f.RegisterResponse{}), userSetting.U2FRegisterPost)
+                               m.Post("/delete", bindIgnErr(forms.U2FDeleteForm{}), userSetting.U2FDelete)
+                       })
+                       m.Group("/openid", func() {
+                               m.Post("", bindIgnErr(forms.AddOpenIDForm{}), userSetting.OpenIDPost)
+                               m.Post("/delete", userSetting.DeleteOpenID)
+                               m.Post("/toggle_visibility", userSetting.ToggleOpenIDVisibility)
+                       }, openIDSignInEnabled)
+                       m.Post("/account_link", userSetting.DeleteAccountLink)
+               })
+               m.Group("/applications/oauth2", func() {
+                       m.Get("/{id}", userSetting.OAuth2ApplicationShow)
+                       m.Post("/{id}", bindIgnErr(forms.EditOAuth2ApplicationForm{}), userSetting.OAuthApplicationsEdit)
+                       m.Post("/{id}/regenerate_secret", userSetting.OAuthApplicationsRegenerateSecret)
+                       m.Post("", bindIgnErr(forms.EditOAuth2ApplicationForm{}), userSetting.OAuthApplicationsPost)
+                       m.Post("/delete", userSetting.DeleteOAuth2Application)
+                       m.Post("/revoke", userSetting.RevokeOAuth2Grant)
+               })
+               m.Combo("/applications").Get(userSetting.Applications).
+                       Post(bindIgnErr(forms.NewAccessTokenForm{}), userSetting.ApplicationsPost)
+               m.Post("/applications/delete", userSetting.DeleteApplication)
+               m.Combo("/keys").Get(userSetting.Keys).
+                       Post(bindIgnErr(forms.AddKeyForm{}), userSetting.KeysPost)
+               m.Post("/keys/delete", userSetting.DeleteKey)
+               m.Get("/organization", userSetting.Organization)
+               m.Get("/repos", userSetting.Repos)
+               m.Post("/repos/unadopted", userSetting.AdoptOrDeleteRepository)
+       }, reqSignIn, func(ctx *context.Context) {
+               ctx.Data["PageIsUserSettings"] = true
+               ctx.Data["AllThemes"] = setting.UI.Themes
+       })
+
+       m.Group("/user", func() {
+               // r.Get("/feeds", binding.Bind(auth.FeedsForm{}), user.Feeds)
+               m.Get("/activate", user.Activate, reqSignIn)
+               m.Post("/activate", user.ActivatePost, reqSignIn)
+               m.Any("/activate_email", user.ActivateEmail)
+               m.Get("/avatar/{username}/{size}", user.Avatar)
+               m.Get("/email2user", user.Email2User)
+               m.Get("/recover_account", user.ResetPasswd)
+               m.Post("/recover_account", user.ResetPasswdPost)
+               m.Get("/forgot_password", user.ForgotPasswd)
+               m.Post("/forgot_password", user.ForgotPasswdPost)
+               m.Post("/logout", user.SignOut)
+               m.Get("/task/{task}", user.TaskStatus)
+       })
+       // ***** END: User *****
+
+       m.Get("/avatar/{hash}", user.AvatarByEmailHash)
+
+       adminReq := context.Toggle(&context.ToggleOptions{SignInRequired: true, AdminRequired: true})
+
+       // ***** START: Admin *****
+       m.Group("/admin", func() {
+               m.Get("", adminReq, admin.Dashboard)
+               m.Post("", adminReq, bindIgnErr(forms.AdminDashboardForm{}), admin.DashboardPost)
+               m.Get("/config", admin.Config)
+               m.Post("/config/test_mail", admin.SendTestMail)
+               m.Group("/monitor", func() {
+                       m.Get("", admin.Monitor)
+                       m.Post("/cancel/{pid}", admin.MonitorCancel)
+                       m.Group("/queue/{qid}", func() {
+                               m.Get("", admin.Queue)
+                               m.Post("/set", admin.SetQueueSettings)
+                               m.Post("/add", admin.AddWorkers)
+                               m.Post("/cancel/{pid}", admin.WorkerCancel)
+                               m.Post("/flush", admin.Flush)
+                       })
+               })
+
+               m.Group("/users", func() {
+                       m.Get("", admin.Users)
+                       m.Combo("/new").Get(admin.NewUser).Post(bindIgnErr(forms.AdminCreateUserForm{}), admin.NewUserPost)
+                       m.Combo("/{userid}").Get(admin.EditUser).Post(bindIgnErr(forms.AdminEditUserForm{}), admin.EditUserPost)
+                       m.Post("/{userid}/delete", admin.DeleteUser)
+               })
+
+               m.Group("/emails", func() {
+                       m.Get("", admin.Emails)
+                       m.Post("/activate", admin.ActivateEmail)
+               })
+
+               m.Group("/orgs", func() {
+                       m.Get("", admin.Organizations)
+               })
+
+               m.Group("/repos", func() {
+                       m.Get("", admin.Repos)
+                       m.Combo("/unadopted").Get(admin.UnadoptedRepos).Post(admin.AdoptOrDeleteRepository)
+                       m.Post("/delete", admin.DeleteRepo)
+               })
+
+               m.Group("/hooks", func() {
+                       m.Get("", admin.DefaultOrSystemWebhooks)
+                       m.Post("/delete", admin.DeleteDefaultOrSystemWebhook)
+                       m.Get("/{id}", repo.WebHooksEdit)
+                       m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.WebHooksEditPost)
+                       m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost)
+                       m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost)
+                       m.Post("/discord/{id}", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
+                       m.Post("/dingtalk/{id}", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost)
+                       m.Post("/telegram/{id}", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost)
+                       m.Post("/matrix/{id}", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost)
+                       m.Post("/msteams/{id}", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost)
+                       m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost)
+               }, webhooksEnabled)
+
+               m.Group("/{configType:default-hooks|system-hooks}", func() {
+                       m.Get("/{type}/new", repo.WebhooksNew)
+                       m.Post("/gitea/new", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksNewPost)
+                       m.Post("/gogs/new", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksNewPost)
+                       m.Post("/slack/new", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksNewPost)
+                       m.Post("/discord/new", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
+                       m.Post("/dingtalk/new", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost)
+                       m.Post("/telegram/new", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost)
+                       m.Post("/matrix/new", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost)
+                       m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost)
+                       m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost)
+               })
+
+               m.Group("/auths", func() {
+                       m.Get("", admin.Authentications)
+                       m.Combo("/new").Get(admin.NewAuthSource).Post(bindIgnErr(forms.AuthenticationForm{}), admin.NewAuthSourcePost)
+                       m.Combo("/{authid}").Get(admin.EditAuthSource).
+                               Post(bindIgnErr(forms.AuthenticationForm{}), admin.EditAuthSourcePost)
+                       m.Post("/{authid}/delete", admin.DeleteAuthSource)
+               })
+
+               m.Group("/notices", func() {
+                       m.Get("", admin.Notices)
+                       m.Post("/delete", admin.DeleteNotices)
+                       m.Post("/empty", admin.EmptyNotices)
+               })
+       }, adminReq)
+       // ***** END: Admin *****
+
+       m.Group("", func() {
+               m.Get("/{username}", user.Profile)
+               m.Get("/attachments/{uuid}", repo.GetAttachment)
+       }, ignSignIn)
+
+       m.Group("/{username}", func() {
+               m.Post("/action/{action}", user.Action)
+       }, reqSignIn)
+
+       if !setting.IsProd() {
+               m.Get("/template/*", dev.TemplatePreview)
+       }
+
+       reqRepoAdmin := context.RequireRepoAdmin()
+       reqRepoCodeWriter := context.RequireRepoWriter(models.UnitTypeCode)
+       reqRepoCodeReader := context.RequireRepoReader(models.UnitTypeCode)
+       reqRepoReleaseWriter := context.RequireRepoWriter(models.UnitTypeReleases)
+       reqRepoReleaseReader := context.RequireRepoReader(models.UnitTypeReleases)
+       reqRepoWikiWriter := context.RequireRepoWriter(models.UnitTypeWiki)
+       reqRepoIssueWriter := context.RequireRepoWriter(models.UnitTypeIssues)
+       reqRepoIssueReader := context.RequireRepoReader(models.UnitTypeIssues)
+       reqRepoPullsReader := context.RequireRepoReader(models.UnitTypePullRequests)
+       reqRepoIssuesOrPullsWriter := context.RequireRepoWriterOr(models.UnitTypeIssues, models.UnitTypePullRequests)
+       reqRepoIssuesOrPullsReader := context.RequireRepoReaderOr(models.UnitTypeIssues, models.UnitTypePullRequests)
+       reqRepoProjectsReader := context.RequireRepoReader(models.UnitTypeProjects)
+       reqRepoProjectsWriter := context.RequireRepoWriter(models.UnitTypeProjects)
+
+       // ***** START: Organization *****
+       m.Group("/org", func() {
+               m.Group("", func() {
+                       m.Get("/create", org.Create)
+                       m.Post("/create", bindIgnErr(forms.CreateOrgForm{}), org.CreatePost)
+               })
+
+               m.Group("/{org}", func() {
+                       m.Get("/dashboard", user.Dashboard)
+                       m.Get("/dashboard/{team}", user.Dashboard)
+                       m.Get("/issues", user.Issues)
+                       m.Get("/issues/{team}", user.Issues)
+                       m.Get("/pulls", user.Pulls)
+                       m.Get("/pulls/{team}", user.Pulls)
+                       m.Get("/milestones", reqMilestonesDashboardPageEnabled, user.Milestones)
+                       m.Get("/milestones/{team}", reqMilestonesDashboardPageEnabled, user.Milestones)
+                       m.Get("/members", org.Members)
+                       m.Post("/members/action/{action}", org.MembersAction)
+                       m.Get("/teams", org.Teams)
+               }, context.OrgAssignment(true, false, true))
+
+               m.Group("/{org}", func() {
+                       m.Get("/teams/{team}", org.TeamMembers)
+                       m.Get("/teams/{team}/repositories", org.TeamRepositories)
+                       m.Post("/teams/{team}/action/{action}", org.TeamsAction)
+                       m.Post("/teams/{team}/action/repo/{action}", org.TeamsRepoAction)
+               }, context.OrgAssignment(true, false, true))
+
+               m.Group("/{org}", func() {
+                       m.Get("/teams/new", org.NewTeam)
+                       m.Post("/teams/new", bindIgnErr(forms.CreateTeamForm{}), org.NewTeamPost)
+                       m.Get("/teams/{team}/edit", org.EditTeam)
+                       m.Post("/teams/{team}/edit", bindIgnErr(forms.CreateTeamForm{}), org.EditTeamPost)
+                       m.Post("/teams/{team}/delete", org.DeleteTeam)
+
+                       m.Group("/settings", func() {
+                               m.Combo("").Get(org.Settings).
+                                       Post(bindIgnErr(forms.UpdateOrgSettingForm{}), org.SettingsPost)
+                               m.Post("/avatar", bindIgnErr(forms.AvatarForm{}), org.SettingsAvatar)
+                               m.Post("/avatar/delete", org.SettingsDeleteAvatar)
+
+                               m.Group("/hooks", func() {
+                                       m.Get("", org.Webhooks)
+                                       m.Post("/delete", org.DeleteWebhook)
+                                       m.Get("/{type}/new", repo.WebhooksNew)
+                                       m.Post("/gitea/new", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksNewPost)
+                                       m.Post("/gogs/new", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksNewPost)
+                                       m.Post("/slack/new", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksNewPost)
+                                       m.Post("/discord/new", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
+                                       m.Post("/dingtalk/new", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost)
+                                       m.Post("/telegram/new", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost)
+                                       m.Post("/matrix/new", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost)
+                                       m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost)
+                                       m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost)
+                                       m.Get("/{id}", repo.WebHooksEdit)
+                                       m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.WebHooksEditPost)
+                                       m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost)
+                                       m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost)
+                                       m.Post("/discord/{id}", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
+                                       m.Post("/dingtalk/{id}", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost)
+                                       m.Post("/telegram/{id}", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost)
+                                       m.Post("/matrix/{id}", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost)
+                                       m.Post("/msteams/{id}", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost)
+                                       m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost)
+                               }, webhooksEnabled)
+
+                               m.Group("/labels", func() {
+                                       m.Get("", org.RetrieveLabels, org.Labels)
+                                       m.Post("/new", bindIgnErr(forms.CreateLabelForm{}), org.NewLabel)
+                                       m.Post("/edit", bindIgnErr(forms.CreateLabelForm{}), org.UpdateLabel)
+                                       m.Post("/delete", org.DeleteLabel)
+                                       m.Post("/initialize", bindIgnErr(forms.InitializeLabelsForm{}), org.InitializeLabels)
+                               })
+
+                               m.Route("/delete", "GET,POST", org.SettingsDelete)
+                       })
+               }, context.OrgAssignment(true, true))
+       }, reqSignIn)
+       // ***** END: Organization *****
+
+       // ***** START: Repository *****
+       m.Group("/repo", func() {
+               m.Get("/create", repo.Create)
+               m.Post("/create", bindIgnErr(forms.CreateRepoForm{}), repo.CreatePost)
+               m.Get("/migrate", repo.Migrate)
+               m.Post("/migrate", bindIgnErr(forms.MigrateRepoForm{}), repo.MigratePost)
+               m.Group("/fork", func() {
+                       m.Combo("/{repoid}").Get(repo.Fork).
+                               Post(bindIgnErr(forms.CreateRepoForm{}), repo.ForkPost)
+               }, context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader)
+       }, reqSignIn)
+
+       // ***** Release Attachment Download without Signin
+       m.Get("/{username}/{reponame}/releases/download/{vTag}/{fileName}", ignSignIn, context.RepoAssignment, repo.MustBeNotEmpty, repo.RedirectDownload)
+
+       m.Group("/{username}/{reponame}", func() {
+               m.Group("/settings", func() {
+                       m.Combo("").Get(repo.Settings).
+                               Post(bindIgnErr(forms.RepoSettingForm{}), repo.SettingsPost)
+                       m.Post("/avatar", bindIgnErr(forms.AvatarForm{}), repo.SettingsAvatar)
+                       m.Post("/avatar/delete", repo.SettingsDeleteAvatar)
+
+                       m.Group("/collaboration", func() {
+                               m.Combo("").Get(repo.Collaboration).Post(repo.CollaborationPost)
+                               m.Post("/access_mode", repo.ChangeCollaborationAccessMode)
+                               m.Post("/delete", repo.DeleteCollaboration)
+                               m.Group("/team", func() {
+                                       m.Post("", repo.AddTeamPost)
+                                       m.Post("/delete", repo.DeleteTeam)
+                               })
+                       })
+                       m.Group("/branches", func() {
+                               m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost)
+                               m.Combo("/*").Get(repo.SettingsProtectedBranch).
+                                       Post(bindIgnErr(forms.ProtectBranchForm{}), context.RepoMustNotBeArchived(), repo.SettingsProtectedBranchPost)
+                       }, repo.MustBeNotEmpty)
+
+                       m.Group("/hooks/git", func() {
+                               m.Get("", repo.GitHooks)
+                               m.Combo("/{name}").Get(repo.GitHooksEdit).
+                                       Post(repo.GitHooksEditPost)
+                       }, context.GitHookService())
+
+                       m.Group("/hooks", func() {
+                               m.Get("", repo.Webhooks)
+                               m.Post("/delete", repo.DeleteWebhook)
+                               m.Get("/{type}/new", repo.WebhooksNew)
+                               m.Post("/gitea/new", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksNewPost)
+                               m.Post("/gogs/new", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksNewPost)
+                               m.Post("/slack/new", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksNewPost)
+                               m.Post("/discord/new", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
+                               m.Post("/dingtalk/new", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost)
+                               m.Post("/telegram/new", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost)
+                               m.Post("/matrix/new", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost)
+                               m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost)
+                               m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost)
+                               m.Get("/{id}", repo.WebHooksEdit)
+                               m.Post("/{id}/test", repo.TestWebhook)
+                               m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.WebHooksEditPost)
+                               m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost)
+                               m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost)
+                               m.Post("/discord/{id}", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
+                               m.Post("/dingtalk/{id}", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost)
+                               m.Post("/telegram/{id}", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost)
+                               m.Post("/matrix/{id}", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost)
+                               m.Post("/msteams/{id}", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost)
+                               m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost)
+                       }, webhooksEnabled)
+
+                       m.Group("/keys", func() {
+                               m.Combo("").Get(repo.DeployKeys).
+                                       Post(bindIgnErr(forms.AddKeyForm{}), repo.DeployKeysPost)
+                               m.Post("/delete", repo.DeleteDeployKey)
+                       })
+
+                       m.Group("/lfs", func() {
+                               m.Get("/", repo.LFSFiles)
+                               m.Get("/show/{oid}", repo.LFSFileGet)
+                               m.Post("/delete/{oid}", repo.LFSDelete)
+                               m.Get("/pointers", repo.LFSPointerFiles)
+                               m.Post("/pointers/associate", repo.LFSAutoAssociate)
+                               m.Get("/find", repo.LFSFileFind)
+                               m.Group("/locks", func() {
+                                       m.Get("/", repo.LFSLocks)
+                                       m.Post("/", repo.LFSLockFile)
+                                       m.Post("/{lid}/unlock", repo.LFSUnlock)
+                               })
+                       })
+
+               }, func(ctx *context.Context) {
+                       ctx.Data["PageIsSettings"] = true
+                       ctx.Data["LFSStartServer"] = setting.LFS.StartServer
+               })
+       }, reqSignIn, context.RepoAssignment, context.UnitTypes(), reqRepoAdmin, context.RepoRef())
+
+       m.Post("/{username}/{reponame}/action/{action}", reqSignIn, context.RepoAssignment, context.UnitTypes(), repo.Action)
+
+       // Grouping for those endpoints not requiring authentication
+       m.Group("/{username}/{reponame}", func() {
+               m.Group("/milestone", func() {
+                       m.Get("/{id}", repo.MilestoneIssuesAndPulls)
+               }, reqRepoIssuesOrPullsReader, context.RepoRef())
+               m.Combo("/compare/*", repo.MustBeNotEmpty, reqRepoCodeReader, repo.SetEditorconfigIfExists).
+                       Get(ignSignIn, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff).
+                       Post(reqSignIn, context.RepoMustNotBeArchived(), reqRepoPullsReader, repo.MustAllowPulls, bindIgnErr(forms.CreateIssueForm{}), repo.SetWhitespaceBehavior, repo.CompareAndPullRequestPost)
+       }, context.RepoAssignment, context.UnitTypes())
+
+       // Grouping for those endpoints that do require authentication
+       m.Group("/{username}/{reponame}", func() {
+               m.Group("/issues", func() {
+                       m.Group("/new", func() {
+                               m.Combo("").Get(context.RepoRef(), repo.NewIssue).
+                                       Post(bindIgnErr(forms.CreateIssueForm{}), repo.NewIssuePost)
+                               m.Get("/choose", context.RepoRef(), repo.NewIssueChooseTemplate)
+                       })
+               }, context.RepoMustNotBeArchived(), reqRepoIssueReader)
+               // FIXME: should use different URLs but mostly same logic for comments of issue and pull request.
+               // So they can apply their own enable/disable logic on routers.
+               m.Group("/issues", func() {
+                       m.Group("/{index}", func() {
+                               m.Post("/title", repo.UpdateIssueTitle)
+                               m.Post("/content", repo.UpdateIssueContent)
+                               m.Post("/watch", repo.IssueWatch)
+                               m.Post("/ref", repo.UpdateIssueRef)
+                               m.Group("/dependency", func() {
+                                       m.Post("/add", repo.AddDependency)
+                                       m.Post("/delete", repo.RemoveDependency)
+                               })
+                               m.Combo("/comments").Post(repo.MustAllowUserComment, bindIgnErr(forms.CreateCommentForm{}), repo.NewComment)
+                               m.Group("/times", func() {
+                                       m.Post("/add", bindIgnErr(forms.AddTimeManuallyForm{}), repo.AddTimeManually)
+                                       m.Post("/{timeid}/delete", repo.DeleteTime)
+                                       m.Group("/stopwatch", func() {
+                                               m.Post("/toggle", repo.IssueStopwatch)
+                                               m.Post("/cancel", repo.CancelStopwatch)
+                                       })
+                               })
+                               m.Post("/reactions/{action}", bindIgnErr(forms.ReactionForm{}), repo.ChangeIssueReaction)
+                               m.Post("/lock", reqRepoIssueWriter, bindIgnErr(forms.IssueLockForm{}), repo.LockIssue)
+                               m.Post("/unlock", reqRepoIssueWriter, repo.UnlockIssue)
+                       }, context.RepoMustNotBeArchived())
+                       m.Group("/{index}", func() {
+                               m.Get("/attachments", repo.GetIssueAttachments)
+                               m.Get("/attachments/{uuid}", repo.GetAttachment)
+                       })
+
+                       m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel)
+                       m.Post("/milestone", reqRepoIssuesOrPullsWriter, repo.UpdateIssueMilestone)
+                       m.Post("/projects", reqRepoIssuesOrPullsWriter, repo.UpdateIssueProject)
+                       m.Post("/assignee", reqRepoIssuesOrPullsWriter, repo.UpdateIssueAssignee)
+                       m.Post("/request_review", reqRepoIssuesOrPullsReader, repo.UpdatePullReviewRequest)
+                       m.Post("/dismiss_review", reqRepoAdmin, bindIgnErr(forms.DismissReviewForm{}), repo.DismissReview)
+                       m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus)
+                       m.Post("/resolve_conversation", reqRepoIssuesOrPullsReader, repo.UpdateResolveConversation)
+                       m.Post("/attachments", repo.UploadIssueAttachment)
+                       m.Post("/attachments/remove", repo.DeleteAttachment)
+               }, context.RepoMustNotBeArchived())
+               m.Group("/comments/{id}", func() {
+                       m.Post("", repo.UpdateCommentContent)
+                       m.Post("/delete", repo.DeleteComment)
+                       m.Post("/reactions/{action}", bindIgnErr(forms.ReactionForm{}), repo.ChangeCommentReaction)
+               }, context.RepoMustNotBeArchived())
+               m.Group("/comments/{id}", func() {
+                       m.Get("/attachments", repo.GetCommentAttachments)
+               })
+               m.Group("/labels", func() {
+                       m.Post("/new", bindIgnErr(forms.CreateLabelForm{}), repo.NewLabel)
+                       m.Post("/edit", bindIgnErr(forms.CreateLabelForm{}), repo.UpdateLabel)
+                       m.Post("/delete", repo.DeleteLabel)
+                       m.Post("/initialize", bindIgnErr(forms.InitializeLabelsForm{}), repo.InitializeLabels)
+               }, context.RepoMustNotBeArchived(), reqRepoIssuesOrPullsWriter, context.RepoRef())
+               m.Group("/milestones", func() {
+                       m.Combo("/new").Get(repo.NewMilestone).
+                               Post(bindIgnErr(forms.CreateMilestoneForm{}), repo.NewMilestonePost)
+                       m.Get("/{id}/edit", repo.EditMilestone)
+                       m.Post("/{id}/edit", bindIgnErr(forms.CreateMilestoneForm{}), repo.EditMilestonePost)
+                       m.Post("/{id}/{action}", repo.ChangeMilestoneStatus)
+                       m.Post("/delete", repo.DeleteMilestone)
+               }, context.RepoMustNotBeArchived(), reqRepoIssuesOrPullsWriter, context.RepoRef())
+               m.Group("/pull", func() {
+                       m.Post("/{index}/target_branch", repo.UpdatePullRequestTarget)
+               }, context.RepoMustNotBeArchived())
+
+               m.Group("", func() {
+                       m.Group("", func() {
+                               m.Combo("/_edit/*").Get(repo.EditFile).
+                                       Post(bindIgnErr(forms.EditRepoFileForm{}), repo.EditFilePost)
+                               m.Combo("/_new/*").Get(repo.NewFile).
+                                       Post(bindIgnErr(forms.EditRepoFileForm{}), repo.NewFilePost)
+                               m.Post("/_preview/*", bindIgnErr(forms.EditPreviewDiffForm{}), repo.DiffPreviewPost)
+                               m.Combo("/_delete/*").Get(repo.DeleteFile).
+                                       Post(bindIgnErr(forms.DeleteRepoFileForm{}), repo.DeleteFilePost)
+                               m.Combo("/_upload/*", repo.MustBeAbleToUpload).
+                                       Get(repo.UploadFile).
+                                       Post(bindIgnErr(forms.UploadRepoFileForm{}), repo.UploadFilePost)
+                       }, context.RepoRefByType(context.RepoRefBranch), repo.MustBeEditable)
+                       m.Group("", func() {
+                               m.Post("/upload-file", repo.UploadFileToServer)
+                               m.Post("/upload-remove", bindIgnErr(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer)
+                       }, context.RepoRef(), repo.MustBeEditable, repo.MustBeAbleToUpload)
+               }, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty)
+
+               m.Group("/branches", func() {
+                       m.Group("/_new", func() {
+                               m.Post("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.CreateBranch)
+                               m.Post("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.CreateBranch)
+                               m.Post("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.CreateBranch)
+                       }, bindIgnErr(forms.NewBranchForm{}))
+                       m.Post("/delete", repo.DeleteBranchPost)
+                       m.Post("/restore", repo.RestoreBranchPost)
+               }, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty)
+
+       }, reqSignIn, context.RepoAssignment, context.UnitTypes())
+
+       // Releases
+       m.Group("/{username}/{reponame}", func() {
+               m.Get("/tags", repo.TagsList, repo.MustBeNotEmpty,
+                       reqRepoCodeReader, context.RepoRefByType(context.RepoRefTag))
+               m.Group("/releases", func() {
+                       m.Get("/", repo.Releases)
+                       m.Get("/tag/*", repo.SingleRelease)
+                       m.Get("/latest", repo.LatestRelease)
+               }, repo.MustBeNotEmpty, reqRepoReleaseReader, context.RepoRefByType(context.RepoRefTag, true))
+               m.Get("/releases/attachments/{uuid}", repo.GetAttachment, repo.MustBeNotEmpty, reqRepoReleaseReader)
+               m.Group("/releases", func() {
+                       m.Get("/new", repo.NewRelease)
+                       m.Post("/new", bindIgnErr(forms.NewReleaseForm{}), repo.NewReleasePost)
+                       m.Post("/delete", repo.DeleteRelease)
+                       m.Post("/attachments", repo.UploadReleaseAttachment)
+                       m.Post("/attachments/remove", repo.DeleteAttachment)
+               }, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, context.RepoRef())
+               m.Post("/tags/delete", repo.DeleteTag, reqSignIn,
+                       repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoCodeWriter, context.RepoRef())
+               m.Group("/releases", func() {
+                       m.Get("/edit/*", repo.EditRelease)
+                       m.Post("/edit/*", bindIgnErr(forms.EditReleaseForm{}), repo.EditReleasePost)
+               }, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, func(ctx *context.Context) {
+                       var err error
+                       ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
+                       if err != nil {
+                               ctx.ServerError("GetBranchCommit", err)
+                               return
+                       }
+                       ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount()
+                       if err != nil {
+                               ctx.ServerError("GetCommitsCount", err)
+                               return
+                       }
+                       ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
+               })
+               m.Get("/attachments/{uuid}", repo.GetAttachment)
+       }, ignSignIn, context.RepoAssignment, context.UnitTypes(), reqRepoReleaseReader)
+
+       m.Group("/{username}/{reponame}", func() {
+               m.Post("/topics", repo.TopicsPost)
+       }, context.RepoAssignment, context.RepoMustNotBeArchived(), reqRepoAdmin)
+
+       m.Group("/{username}/{reponame}", func() {
+               m.Group("", func() {
+                       m.Get("/{type:issues|pulls}", repo.Issues)
+                       m.Get("/{type:issues|pulls}/{index}", repo.ViewIssue)
+                       m.Get("/labels", reqRepoIssuesOrPullsReader, repo.RetrieveLabels, repo.Labels)
+                       m.Get("/milestones", reqRepoIssuesOrPullsReader, repo.Milestones)
+               }, context.RepoRef())
+
+               m.Group("/projects", func() {
+                       m.Get("", repo.Projects)
+                       m.Get("/{id}", repo.ViewProject)
+                       m.Group("", func() {
+                               m.Get("/new", repo.NewProject)
+                               m.Post("/new", bindIgnErr(forms.CreateProjectForm{}), repo.NewProjectPost)
+                               m.Group("/{id}", func() {
+                                       m.Post("", bindIgnErr(forms.EditProjectBoardForm{}), repo.AddBoardToProjectPost)
+                                       m.Post("/delete", repo.DeleteProject)
+
+                                       m.Get("/edit", repo.EditProject)
+                                       m.Post("/edit", bindIgnErr(forms.CreateProjectForm{}), repo.EditProjectPost)
+                                       m.Post("/{action:open|close}", repo.ChangeProjectStatus)
+
+                                       m.Group("/{boardID}", func() {
+                                               m.Put("", bindIgnErr(forms.EditProjectBoardForm{}), repo.EditProjectBoard)
+                                               m.Delete("", repo.DeleteProjectBoard)
+                                               m.Post("/default", repo.SetDefaultProjectBoard)
+
+                                               m.Post("/{index}", repo.MoveIssueAcrossBoards)
+                                       })
+                               })
+                       }, reqRepoProjectsWriter, context.RepoMustNotBeArchived())
+               }, reqRepoProjectsReader, repo.MustEnableProjects)
+
+               m.Group("/wiki", func() {
+                       m.Get("/", repo.Wiki)
+                       m.Get("/{page}", repo.Wiki)
+                       m.Get("/_pages", repo.WikiPages)
+                       m.Get("/{page}/_revision", repo.WikiRevision)
+                       m.Get("/commit/{sha:[a-f0-9]{7,40}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
+                       m.Get("/commit/{sha:[a-f0-9]{7,40}}.{:patch|diff}", repo.RawDiff)
+
+                       m.Group("", func() {
+                               m.Combo("/_new").Get(repo.NewWiki).
+                                       Post(bindIgnErr(forms.NewWikiForm{}), repo.NewWikiPost)
+                               m.Combo("/{page}/_edit").Get(repo.EditWiki).
+                                       Post(bindIgnErr(forms.NewWikiForm{}), repo.EditWikiPost)
+                               m.Post("/{page}/delete", repo.DeleteWikiPagePost)
+                       }, context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter)
+               }, repo.MustEnableWiki, context.RepoRef(), func(ctx *context.Context) {
+                       ctx.Data["PageIsWiki"] = true
+               })
+
+               m.Group("/wiki", func() {
+                       m.Get("/raw/*", repo.WikiRaw)
+               }, repo.MustEnableWiki)
+
+               m.Group("/activity", func() {
+                       m.Get("", repo.Activity)
+                       m.Get("/{period}", repo.Activity)
+               }, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(models.UnitTypePullRequests, models.UnitTypeIssues, models.UnitTypeReleases))
+
+               m.Group("/activity_author_data", func() {
+                       m.Get("", repo.ActivityAuthors)
+                       m.Get("/{period}", repo.ActivityAuthors)
+               }, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(models.UnitTypeCode))
+
+               m.Group("/archive", func() {
+                       m.Get("/*", common.Download)
+                       m.Post("/*", repo.InitiateDownload)
+               }, repo.MustBeNotEmpty, reqRepoCodeReader)
+
+               m.Group("/branches", func() {
+                       m.Get("", repo.Branches)
+               }, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)
+
+               m.Group("/blob_excerpt", func() {
+                       m.Get("/{sha}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ExcerptBlob)
+               }, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)
+
+               m.Group("/pulls/{index}", func() {
+                       m.Get(".diff", repo.DownloadPullDiff)
+                       m.Get(".patch", repo.DownloadPullPatch)
+                       m.Get("/commits", context.RepoRef(), repo.ViewPullCommits)
+                       m.Post("/merge", context.RepoMustNotBeArchived(), bindIgnErr(forms.MergePullRequestForm{}), repo.MergePullRequest)
+                       m.Post("/update", repo.UpdatePullRequest)
+                       m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest)
+                       m.Group("/files", func() {
+                               m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.ViewPullFiles)
+                               m.Group("/reviews", func() {
+                                       m.Get("/new_comment", repo.RenderNewCodeCommentForm)
+                                       m.Post("/comments", bindIgnErr(forms.CodeCommentForm{}), repo.CreateCodeComment)
+                                       m.Post("/submit", bindIgnErr(forms.SubmitReviewForm{}), repo.SubmitReview)
+                               }, context.RepoMustNotBeArchived())
+                       })
+               }, repo.MustAllowPulls)
+
+               m.Group("/media", func() {
+                       m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.SingleDownloadOrLFS)
+                       m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.SingleDownloadOrLFS)
+                       m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.SingleDownloadOrLFS)
+                       m.Get("/blob/{sha}", context.RepoRefByType(context.RepoRefBlob), repo.DownloadByIDOrLFS)
+                       // "/*" route is deprecated, and kept for backward compatibility
+                       m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.SingleDownloadOrLFS)
+               }, repo.MustBeNotEmpty, reqRepoCodeReader)
+
+               m.Group("/raw", func() {
+                       m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.SingleDownload)
+                       m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.SingleDownload)
+                       m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.SingleDownload)
+                       m.Get("/blob/{sha}", context.RepoRefByType(context.RepoRefBlob), repo.DownloadByID)
+                       // "/*" route is deprecated, and kept for backward compatibility
+                       m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.SingleDownload)
+               }, repo.MustBeNotEmpty, reqRepoCodeReader)
+
+               m.Group("/commits", func() {
+                       m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RefCommits)
+                       m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RefCommits)
+                       m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.RefCommits)
+                       // "/*" route is deprecated, and kept for backward compatibility
+                       m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.RefCommits)
+               }, repo.MustBeNotEmpty, reqRepoCodeReader)
+
+               m.Group("/blame", func() {
+                       m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RefBlame)
+                       m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RefBlame)
+                       m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.RefBlame)
+               }, repo.MustBeNotEmpty, reqRepoCodeReader)
+
+               m.Group("", func() {
+                       m.Get("/graph", repo.Graph)
+                       m.Get("/commit/{sha:([a-f0-9]{7,40})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
+               }, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)
+
+               m.Group("/src", func() {
+                       m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.Home)
+                       m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.Home)
+                       m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.Home)
+                       // "/*" route is deprecated, and kept for backward compatibility
+                       m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.Home)
+               }, repo.SetEditorconfigIfExists)
+
+               m.Group("", func() {
+                       m.Get("/forks", repo.Forks)
+               }, context.RepoRef(), reqRepoCodeReader)
+               m.Get("/commit/{sha:([a-f0-9]{7,40})}.{ext:patch|diff}",
+                       repo.MustBeNotEmpty, reqRepoCodeReader, repo.RawDiff)
+       }, ignSignIn, context.RepoAssignment, context.UnitTypes())
+       m.Group("/{username}/{reponame}", func() {
+               m.Get("/stars", repo.Stars)
+               m.Get("/watchers", repo.Watchers)
+               m.Get("/search", reqRepoCodeReader, repo.Search)
+       }, ignSignIn, context.RepoAssignment, context.RepoRef(), context.UnitTypes())
+
+       m.Group("/{username}", func() {
+               m.Group("/{reponame}", func() {
+                       m.Get("", repo.SetEditorconfigIfExists, repo.Home)
+               }, ignSignIn, context.RepoAssignment, context.RepoRef(), context.UnitTypes())
+
+               m.Group("/{reponame}", func() {
+                       m.Group("/info/lfs", func() {
+                               m.Post("/objects/batch", lfs.CheckAcceptMediaType, lfs.BatchHandler)
+                               m.Put("/objects/{oid}/{size}", lfs.UploadHandler)
+                               m.Get("/objects/{oid}/{filename}", lfs.DownloadHandler)
+                               m.Get("/objects/{oid}", lfs.DownloadHandler)
+                               m.Post("/verify", lfs.CheckAcceptMediaType, lfs.VerifyHandler)
+                               m.Group("/locks", func() {
+                                       m.Get("/", lfs.GetListLockHandler)
+                                       m.Post("/", lfs.PostLockHandler)
+                                       m.Post("/verify", lfs.VerifyLockHandler)
+                                       m.Post("/{lid}/unlock", lfs.UnLockHandler)
+                               }, lfs.CheckAcceptMediaType)
+                               m.Any("/*", func(ctx *context.Context) {
+                                       ctx.NotFound("", nil)
+                               })
+                       }, ignSignInAndCsrf, lfsServerEnabled)
+
+                       m.Group("", func() {
+                               m.Post("/git-upload-pack", repo.ServiceUploadPack)
+                               m.Post("/git-receive-pack", repo.ServiceReceivePack)
+                               m.Get("/info/refs", repo.GetInfoRefs)
+                               m.Get("/HEAD", repo.GetTextFile("HEAD"))
+                               m.Get("/objects/info/alternates", repo.GetTextFile("objects/info/alternates"))
+                               m.Get("/objects/info/http-alternates", repo.GetTextFile("objects/info/http-alternates"))
+                               m.Get("/objects/info/packs", repo.GetInfoPacks)
+                               m.Get("/objects/info/{file:[^/]*}", repo.GetTextFile(""))
+                               m.Get("/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38}}", repo.GetLooseObject)
+                               m.Get("/objects/pack/pack-{file:[0-9a-f]{40}}.pack", repo.GetPackFile)
+                               m.Get("/objects/pack/pack-{file:[0-9a-f]{40}}.idx", repo.GetIdxFile)
+                       }, ignSignInAndCsrf)
+
+                       m.Head("/tasks/trigger", repo.TriggerTask)
+               })
+       })
+       // ***** END: Repository *****
+
+       m.Group("/notifications", func() {
+               m.Get("", user.Notifications)
+               m.Post("/status", user.NotificationStatusPost)
+               m.Post("/purge", user.NotificationPurgePost)
+       }, reqSignIn)
+
+       if setting.API.EnableSwagger {
+               m.Get("/swagger.v1.json", SwaggerV1Json)
+       }
+}