Browse Source

lfs: implement HTTP routes (#6035)

* Bootstrap with GORM

* Fix lint error

* Set conn max lifetime to one minute

* Fallback to use gorm v1

* Define HTTP routes

* Finish authentication

* Save token updated

* Add docstring

* Finish authorization

* serveBatch rundown

* Define types in lfsutil

* Finish Batch

* authutil

* Finish basic

* Formalize response error

* Fix lint errors

* authutil: add tests

* dbutil: add tests

* lfsutil: add tests

* strutil: add tests

* Formalize 401 response
ᴜɴᴋɴᴡᴏɴ 4 years ago
parent
commit
34145c990d
69 changed files with 1842 additions and 553 deletions
  1. 2 0
      .github/workflows/go.yml
  2. 18 0
      conf/app.ini
  3. 4 2
      go.mod
  4. 15 104
      go.sum
  5. 1 1
      internal/assets/conf/conf_gen.go
  6. 1 1
      internal/assets/public/public_gen.go
  7. 6 6
      internal/auth/auth.go
  8. 35 0
      internal/authutil/basic.go
  9. 72 0
      internal/authutil/basic_test.go
  10. 7 7
      internal/cmd/serv.go
  11. 8 4
      internal/cmd/web.go
  12. 2 0
      internal/conf/conf.go
  13. 25 16
      internal/conf/static.go
  14. 2 0
      internal/conf/testdata/TestInit.golden.ini
  15. 1 1
      internal/context/org.go
  16. 6 6
      internal/context/repo.go
  17. 19 18
      internal/db/access.go
  18. 65 0
      internal/db/access_tokens.go
  19. 171 0
      internal/db/db.go
  20. 0 32
      internal/db/error.go
  21. 0 13
      internal/db/errors/login_source.go
  22. 0 13
      internal/db/errors/two_factor.go
  23. 1 1
      internal/db/issue.go
  24. 129 0
      internal/db/lfs.go
  25. 63 129
      internal/db/login_source.go
  26. 36 0
      internal/db/login_sources.go
  27. 8 47
      internal/db/models.go
  28. 0 15
      internal/db/models_sqlite.go
  29. 1 1
      internal/db/org.go
  30. 2 2
      internal/db/org_team.go
  31. 56 0
      internal/db/perms.go
  32. 4 3
      internal/db/repo.go
  33. 2 2
      internal/db/repo_branch.go
  34. 8 8
      internal/db/repo_collaboration.go
  35. 38 0
      internal/db/repos.go
  36. 3 3
      internal/db/ssh_key.go
  37. 12 33
      internal/db/token.go
  38. 14 11
      internal/db/two_factor.go
  39. 33 0
      internal/db/two_factors.go
  40. 10 8
      internal/db/user.go
  41. 138 0
      internal/db/users.go
  42. 35 0
      internal/dbutil/writer.go
  43. 53 0
      internal/dbutil/writer_test.go
  44. 3 0
      internal/errutil/errutil.go
  45. 1 1
      internal/gitutil/module.go
  46. 30 0
      internal/lfsutil/oid.go
  47. 47 0
      internal/lfsutil/oid_test.go
  48. 29 0
      internal/lfsutil/storage.go
  49. 43 0
      internal/lfsutil/storage_test.go
  50. 19 19
      internal/route/admin/auths.go
  51. 6 6
      internal/route/admin/users.go
  52. 2 2
      internal/route/api/v1/admin/user.go
  53. 1 1
      internal/route/api/v1/api.go
  54. 2 2
      internal/route/api/v1/repo/repo.go
  55. 2 2
      internal/route/api/v1/user/app.go
  56. 1 1
      internal/route/home.go
  57. 2 11
      internal/route/install.go
  58. 122 0
      internal/route/lfs/basic.go
  59. 182 0
      internal/route/lfs/batch.go
  60. 159 0
      internal/route/lfs/route.go
  61. 1 1
      internal/route/org/setting.go
  62. 3 3
      internal/route/org/teams.go
  63. 11 8
      internal/route/repo/http.go
  64. 1 1
      internal/route/repo/setting.go
  65. 5 5
      internal/route/user/auth.go
  66. 3 3
      internal/route/user/setting.go
  67. 17 0
      internal/strutil/strutil.go
  68. 43 0
      internal/strutil/strutil_test.go
  69. 1 0
      internal/testutil/golden.go

+ 2 - 0
.github/workflows/go.yml

@@ -14,6 +14,8 @@ jobs:
       - uses: actions/checkout@v2
       - name: Run golangci-lint
         uses: actions-contrib/golangci-lint@v1
+        with:
+          args: 'run --timeout=30m'
 
   test:
     name: Test

+ 18 - 0
conf/app.ini

@@ -146,6 +146,10 @@ PASSWORD =
 SSL_MODE = disable
 ; For "sqlite3" only, make sure to use absolute path.
 PATH = data/gogs.db
+; The maximum open connections of the pool.
+MAX_OPEN_CONNS = 30
+; The maximum idle connections of the pool.
+MAX_IDLE_CONNS = 30
 
 [security]
 ; Whether to show the install page, set this to "true" to bypass it.
@@ -259,6 +263,10 @@ HOST =
 ; The value for "Access-Control-Allow-Origin" header, default is not to present.
 ACCESS_CONTROL_ALLOW_ORIGIN =
 
+[lfs]
+; The root path to store LFS objects.
+OBJECTS_PATH = data/lfs-objects
+
 [attachment]
 ; Whether to enabled upload attachments in general.
 ENABLED = true
@@ -391,6 +399,16 @@ MAX_SIZE = 100
 ; Maximum days to keep logger files
 MAX_DAYS = 3
 
+[log.gorm]
+; Whether to enable file rotation.
+ROTATE = true
+; Whether to rotate file every day.
+ROTATE_DAILY = true
+; The maximum file size in MB before next rotate.
+MAX_SIZE = 100
+; The maximum days to keep files.
+MAX_DAYS = 3
+
 [cron]
 ; Enable running cron tasks periodically.
 ENABLED = true

+ 4 - 2
go.mod

@@ -4,7 +4,7 @@ go 1.13
 
 require (
 	github.com/bgentry/speakeasy v0.1.0 // indirect
-	github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0
+	github.com/denisenkom/go-mssqldb v0.0.0-20200206145737-bbfc9a55622e
 	github.com/editorconfig/editorconfig-core-go/v2 v2.3.1
 	github.com/fatih/color v1.9.0 // indirect
 	github.com/go-macaron/binding v1.1.0
@@ -13,6 +13,7 @@ require (
 	github.com/go-macaron/csrf v0.0.0-20190812063352-946f6d303a4c
 	github.com/go-macaron/gzip v0.0.0-20160222043647-cad1c6580a07
 	github.com/go-macaron/i18n v0.5.0
+	github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191
 	github.com/go-macaron/session v0.0.0-20190805070824-1a3cdc6f5659
 	github.com/go-macaron/toolbox v0.0.0-20190813233741-94defb8383c6
 	github.com/go-sql-driver/mysql v1.5.0
@@ -27,13 +28,14 @@ require (
 	github.com/google/go-querystring v1.0.0 // indirect
 	github.com/issue9/identicon v1.0.1
 	github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43
+	github.com/jinzhu/gorm v1.9.12
 	github.com/json-iterator/go v1.1.9
 	github.com/klauspost/compress v1.8.6 // indirect
 	github.com/klauspost/cpuid v1.2.1 // indirect
 	github.com/lib/pq v1.3.0
 	github.com/mattn/go-isatty v0.0.12 // indirect
 	github.com/mattn/go-runewidth v0.0.4 // indirect
-	github.com/mattn/go-sqlite3 v1.13.0
+	github.com/mattn/go-sqlite3 v2.0.3+incompatible
 	github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2
 	github.com/microcosm-cc/bluemonday v1.0.2
 	github.com/msteinert/pam v0.0.0-20190215180659-f29b9f28d6f9

+ 15 - 104
go.sum

@@ -4,12 +4,6 @@ cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7h
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
 github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
-github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
-github.com/alecthomas/chroma v0.7.1 h1:G1i02OhUbRi2nJxcNkwJaY/J1gHXj9tt72qN6ZouLFQ=
-github.com/alecthomas/chroma v0.7.1/go.mod h1:gHw09mkX1Qp80JlYbmN9L3+4R5o6DJJ3GRShh+AICNc=
-github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
-github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
-github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -19,16 +13,11 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
-github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
-github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
 github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
 github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
 github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668 h1:U/lr3Dgy4WK+hNk4tyD+nuGjpVLPEHuJSFXMw11/HPA=
 github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
-github.com/cespare/xxhash/v2 v2.1.0 h1:yTUvW7Vhb89inJ+8irsUqiWjh8iT6sQPZiQzI6ReGkA=
-github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM=
 github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
@@ -38,27 +27,20 @@ github.com/couchbaselabs/go-couchbase v0.0.0-20190708161019-23e7ca2ce2b7/go.mod
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY=
-github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
-github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/denisenkom/go-mssqldb v0.0.0-20190707035753-2be1aa521ff4/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
-github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0 h1:epsH3lb7KVbXHYk7LYGN5EiE0MxcevHU85CKITJ0wUY=
-github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
-github.com/dlclark/regexp2 v1.1.6 h1:CqB4MjHw0MFCDj+PHHjiESmHX+N7t0tJzKvC6M97BRg=
-github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
+github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
+github.com/denisenkom/go-mssqldb v0.0.0-20200206145737-bbfc9a55622e h1:LzwWXEScfcTu7vUZNlDDWDARoSGEtvlDKK2BYHowNeE=
+github.com/denisenkom/go-mssqldb v0.0.0-20200206145737-bbfc9a55622e/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
 github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
 github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
-github.com/editorconfig/editorconfig-core-go/v2 v2.2.1 h1:jY5PCRQf4V0oqpim/Ympl6MwHcb9+nBHEnHOPXqNZ/A=
-github.com/editorconfig/editorconfig-core-go/v2 v2.2.1/go.mod h1:6XDmqAZsQu8ikS+onLRJfLZvTP3RWTVT8ROX6qcdkio=
-github.com/editorconfig/editorconfig-core-go/v2 v2.3.0 h1:QD1YB/rbntMEQIKM42kQOaqGdS13UvGsl9c8m/nFNWY=
-github.com/editorconfig/editorconfig-core-go/v2 v2.3.0/go.mod h1:RNdPfKd9PliYEUZ3r+GxbDsSHNnEluC1wdkQJc3jD4k=
 github.com/editorconfig/editorconfig-core-go/v2 v2.3.1 h1:8+L7G4cCtuYprGaNawfTBq20m8+VpPCH2O0vwKS7r84=
 github.com/editorconfig/editorconfig-core-go/v2 v2.3.1/go.mod h1:mJYZ8yC2PWr+pabYXwHMfcEe45fh2w2sxk8cudJdLPM=
 github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
-github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
+github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
 github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
@@ -67,8 +49,6 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
-github.com/go-macaron/binding v1.0.1 h1:4LASxd4EKsESZ6ZMyzNVX+TM4Yuex4bTHYyz/PQjsRA=
-github.com/go-macaron/binding v1.0.1/go.mod h1:AG8Z6qkQM8s47aUDJOco/SNwJ8Czif2hMm7rc0abDog=
 github.com/go-macaron/binding v1.1.0 h1:A5jpr5UdHr81Hfmb6QUAMTHyvniudOMcgtEg13TJ1ig=
 github.com/go-macaron/binding v1.1.0/go.mod h1:dJU/AtPKG0gUiFra1K5TTGduFGMNxMvfJzV/zmXwyGM=
 github.com/go-macaron/cache v0.0.0-20190810181446-10f7c57e2196 h1:fqWZxyMLF6RVGmjvsZ9FijiU9UlAjuE6nu9RfNBZ+iE=
@@ -87,12 +67,10 @@ github.com/go-macaron/session v0.0.0-20190805070824-1a3cdc6f5659 h1:YXDFNK98PgKe
 github.com/go-macaron/session v0.0.0-20190805070824-1a3cdc6f5659/go.mod h1:tLd0QEudXocQckwcpCq5pCuTCuYc24I0bRJDuRe9OuQ=
 github.com/go-macaron/toolbox v0.0.0-20190813233741-94defb8383c6 h1:x/v1iUWlqXTKVg17ulB0qCgcM2s+eysAbr/dseKLLss=
 github.com/go-macaron/toolbox v0.0.0-20190813233741-94defb8383c6/go.mod h1:YFNJ/JT4yLnpuIXTFef30SZkxGHUczjGZGFaZpPcdn0=
-github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
 github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
 github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk8V3XHWUcJmYTh+ZnlHVyc+A4oZYS3Y=
 github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuqnHyryaerycW3BfssRdxQstACi0Epw/yC5E2xM=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -100,25 +78,12 @@ github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561 h1:aBzukfDxQlCTVS0NBU
 github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
 github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14 h1:yXtpJr/LV6PFu4nTLgfjQdcMdzjbqqXMEnHfq0Or6p8=
 github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14/go.mod h1:jPoNZLWDAqA5N3G5amEoiNbhVrmM+ZQEcnQvNQ2KaZk=
-github.com/gogs/git-module v0.8.3 h1:9f8oxSs9OACWrGBYMVnnQNzyTcVN+zzcBM7CXnbmezw=
-github.com/gogs/git-module v0.8.3/go.mod h1:aj4tcm7DxaszJWpZLZIRL6gfPXyguAHiE1PDfAAPrCw=
-github.com/gogs/git-module v1.0.0-beta.4 h1:5CyCvTfrb2n5LRpHcNIaFnywHDkM/NxSZVP6t4tpTXI=
-github.com/gogs/git-module v1.0.0-beta.4/go.mod h1:oN37FFStFjdnTJXsSbhIHKJXh2YeDsEcXPATVz/oeuQ=
-github.com/gogs/git-module v1.0.0 h1:iOlCZ5kPc3RjnWRxdziL5hjCaosYyZw/Lf2odzR/kjw=
-github.com/gogs/git-module v1.0.0/go.mod h1:oN37FFStFjdnTJXsSbhIHKJXh2YeDsEcXPATVz/oeuQ=
-github.com/gogs/git-module v1.0.1 h1:Xh/sfk6zKjF3y9w2G/dN0YMfLjMhRQzqxMTUPHOL5n4=
-github.com/gogs/git-module v1.0.1/go.mod h1:oN37FFStFjdnTJXsSbhIHKJXh2YeDsEcXPATVz/oeuQ=
-github.com/gogs/git-module v1.0.2 h1:YrDZV4g489A4sOF3+gQq85UnVBjLn30+w3PF5PBoGpQ=
-github.com/gogs/git-module v1.0.2/go.mod h1:oN37FFStFjdnTJXsSbhIHKJXh2YeDsEcXPATVz/oeuQ=
-github.com/gogs/git-module v1.1.0 h1:OEQAWvhZ4TCsq6Vw/ftyA37Os1QkiPu1uMQpF6ErzG0=
-github.com/gogs/git-module v1.1.0/go.mod h1:oN37FFStFjdnTJXsSbhIHKJXh2YeDsEcXPATVz/oeuQ=
 github.com/gogs/git-module v1.1.1 h1:/taoHtOHLorlmQJ7zLBQvJGGgM9LRIoGGH1et4Upzvo=
 github.com/gogs/git-module v1.1.1/go.mod h1:oN37FFStFjdnTJXsSbhIHKJXh2YeDsEcXPATVz/oeuQ=
 github.com/gogs/go-gogs-client v0.0.0-20200128182646-c69cb7680fd4 h1:C7NryI/RQhsIWwC2bHN601P1wJKeuQ6U/UCOYTn3Cic=
 github.com/gogs/go-gogs-client v0.0.0-20200128182646-c69cb7680fd4/go.mod h1:fR6z1Ie6rtF7kl/vBYMfgD5/G5B1blui7z426/sj2DU=
 github.com/gogs/go-libravatar v0.0.0-20191106065024-33a75213d0a0 h1:K02vod+sn3M1OOkdqi2tPxN2+xESK4qyITVQ3JkGEv4=
 github.com/gogs/go-libravatar v0.0.0-20191106065024-33a75213d0a0/go.mod h1:Zas3BtO88pk1cwUfEYlvnl/CRwh0ybDxRWSwRjG8I3w=
-github.com/gogs/minwinsvc v0.0.0-20170301035411-95be6356811a h1:8DZwxETOVWIinYxDK+i6L+rMb7eGATGaakD6ZucfHVk=
 github.com/gogs/minwinsvc v0.0.0-20170301035411-95be6356811a/go.mod h1:TUIZ+29jodWQ8Gk6Pvtg4E09aMsc3C/VLZiVYfUhWQU=
 github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
 github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
@@ -133,10 +98,7 @@ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8l
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
 github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
@@ -148,26 +110,26 @@ github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OI
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
-github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
 github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/issue9/assert v1.3.1 h1:L8pRpbnzMIPFJqrMKR/oG03uWrtVeZyYBpI2U2Jx1JE=
 github.com/issue9/assert v1.3.1/go.mod h1:9Ger+iz8X7r1zMYYwEhh++2wMGWcNN2oVI+zIQXxcio=
 github.com/issue9/identicon v1.0.1 h1:pCDfjMDM6xWK0Chxo8Lif+ST/nOEtmXgMITgV1YA9Og=
 github.com/issue9/identicon v1.0.1/go.mod h1:UKNVkUFI68RPz/RlLhsAr1aX6bBSaYEWRHVfdjrMUmk=
 github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43 h1:jTkyeF7NZ5oIr0ESmcrpiDgAfoidCBF4F5kJhjtaRwE=
 github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
+github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q=
+github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
-github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
 github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
-github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@@ -177,24 +139,19 @@ github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w
 github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
-github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
-github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
+github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
 github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lunny/log v0.0.0-20160921050905-7887c61bf0de/go.mod h1:3q8WtuPQsoRbatJuy3nvq/hRSvuBJrHHr+ybPPiNvHQ=
 github.com/lunny/nodb v0.0.0-20160621015157-fc1ef06ad4af/go.mod h1:Cqz6pqow14VObJ7peltM+2n3PWOz7yTrfUuGbVFkzN0=
-github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
 github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
-github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
-github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
 github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
 github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
 github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
@@ -202,10 +159,10 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
 github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
 github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
-github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
 github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
-github.com/mattn/go-sqlite3 v1.13.0 h1:LnJI81JidiW9r7pS/hXe6cFeO5EXNq7KbfvoJLRI69c=
-github.com/mattn/go-sqlite3 v1.13.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
+github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo=
@@ -213,20 +170,16 @@ github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2 h1:YocNLcTBdEd
 github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo=
 github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
 github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
-github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/msteinert/pam v0.0.0-20190215180659-f29b9f28d6f9 h1:ZivaaKmjs9q90zi6I4gTLW6tbVGtlBjellr3hMYaly0=
 github.com/msteinert/pam v0.0.0-20190215180659-f29b9f28d6f9/go.mod h1:np1wUFZ6tyoke22qDJZY40URn9Ae51gX7ljIWXN5TJs=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
-github.com/niklasfasching/go-org v0.1.6 h1:F521WcqRNl8OJumlgAnekZgERaTA2HpfOYYfVEKOeI8=
-github.com/niklasfasching/go-org v0.1.6/go.mod h1:AsLD6X7djzRIz4/RFZu8vwRL0VGjUvGZCCH1Nz0VdrU=
 github.com/niklasfasching/go-org v0.1.9 h1:Toz8WMIt+qJb52uYEk1YD/muLuOOmRt1CfkV+bKVMkI=
 github.com/niklasfasching/go-org v0.1.9/go.mod h1:AsLD6X7djzRIz4/RFZu8vwRL0VGjUvGZCCH1Nz0VdrU=
 github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
@@ -240,7 +193,6 @@ github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJ
 github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -251,28 +203,20 @@ github.com/pquerna/otp v1.2.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
-github.com/prometheus/client_golang v1.2.1 h1:JnMpQc6ppsNgw9QPAGF6Dod479itz7lvlsMzzNayLOI=
-github.com/prometheus/client_golang v1.2.1/go.mod h1:XMU6Z2MjaRKVu/dC1qupJI9SiNkDYzz3xecMgSW/F+U=
 github.com/prometheus/client_golang v1.5.1 h1:bdHYieyGlH+6OLEk2YQha8THib30KP0/yD0YH9m6xcA=
 github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
 github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY=
-github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
 github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U=
 github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8=
-github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
 github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8=
 github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
@@ -280,12 +224,9 @@ github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNue
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
 github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
 github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
-github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
-github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
 github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
@@ -298,20 +239,16 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
-github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w=
 github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
 github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
 github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
-github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
 github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
 github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
@@ -325,13 +262,8 @@ github.com/unknwon/i18n v0.0.0-20190805065654-5c6446a380b6 h1:sRrkJEHtNoaSvyXMbR
 github.com/unknwon/i18n v0.0.0-20190805065654-5c6446a380b6/go.mod h1:+5rDk6sDGpl3azws3O+f+GpFSyN9GVr0K8cvQLQM2ZQ=
 github.com/unknwon/paginater v0.0.0-20170405233947-45e5d631308e h1:Qf3QQl/zmEbWDajFEiisbKN83hLY+eq2MhbA0I1/two=
 github.com/unknwon/paginater v0.0.0-20170405233947-45e5d631308e/go.mod h1:TBwoao3Q4Eb/cp+dHbXDfRTrZSsj/k7kLr2j1oWRWC0=
-github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
-github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
-github.com/urfave/cli v1.22.3 h1:FpNT6zq26xNpHZy08emi755QwzLPs6Pukqjlc7RfOMU=
-github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
 github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
-github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
 github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
 go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -340,8 +272,7 @@ golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 h1:Sy5bstxEqwwbYs6n0/pBuxKENqOeZUgD45Gp3Q3pqLg=
-golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 h1:/Tl7pH94bvbAAHBdZJT947M/+gp0+CqQXDtMRC0fseo=
 golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -379,7 +310,6 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -387,8 +317,6 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
-golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -416,7 +344,6 @@ google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMt
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=
 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -424,33 +351,23 @@ google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRn
 google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
-gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
 gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM=
 gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
 gopkg.in/bufio.v1 v1.0.0-20140618132640-567b2bfa514e h1:wGA78yza6bu/mWcc4QfBuIEHEtc06xdiU0X8sY36yUU=
 gopkg.in/bufio.v1 v1.0.0-20140618132640-567b2bfa514e/go.mod h1:xsQCaysVCudhrYTfzYWe577fCe7Ceci+6qjO2Rdc0Z4=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
-gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ini.v1 v1.46.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/ini.v1 v1.52.0 h1:j+Lt/M1oPPejkniCg1TkWE2J3Eh1oZTsHSXzMTzUXn4=
-gopkg.in/ini.v1 v1.52.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/ini.v1 v1.53.0 h1:c7ruDvTQi0MUTFuNpDRXLSjs7xT4TerM1icIg4uKWRg=
-gopkg.in/ini.v1 v1.53.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/ini.v1 v1.54.0 h1:oM5ElzbIi7gwLnNbPX2M25ED1vSAK3B6dex50eS/6Fs=
 gopkg.in/ini.v1 v1.54.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ=
 gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ldap.v2 v2.5.1 h1:wiu0okdNfjlBzg6UWvd1Hn8Y+Ux17/u/4nlk4CQr6tU=
 gopkg.in/ldap.v2 v2.5.1/go.mod h1:oI0cpe/D7HRtBQl8aTg+ZmzFUAvu4lsv3eLXMLGFxWk=
-gopkg.in/macaron.v1 v1.3.4 h1:HvIscOwxhFhx3swWM/979wh2QMYyuXrNmrF9l+j3HZs=
 gopkg.in/macaron.v1 v1.3.4/go.mod h1:/RoHTdC8ALpyJ3+QR36mKjwnT1F1dyYtsGM9Ate6ZFI=
 gopkg.in/macaron.v1 v1.3.5 h1:FUA16VFBojxzfU75KqWrV/6BPv9O2R1GnybSGRie9QQ=
 gopkg.in/macaron.v1 v1.3.5/go.mod h1:uMZCFccv9yr5TipIalVOyAyZQuOH3OkmXvgcWwhJuP4=
@@ -458,19 +375,13 @@ gopkg.in/redis.v2 v2.3.2 h1:GPVIIB/JnL1wvfULefy3qXmPu1nfNu2d0yA09FHgwfs=
 gopkg.in/redis.v2 v2.3.2/go.mod h1:4wl9PJ/CqzeHk3LVq1hNLHH8krm3+AXEgut4jVc++LU=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c=
 gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
-gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-unknwon.dev/clog/v2 v2.1.0 h1:4iXteBnL9ESvxoNkiaWx4UTU6yEgEmDxFaMANyiq4b8=
-unknwon.dev/clog/v2 v2.1.0/go.mod h1:zvUlyibDHI4mykYdWyWje2G9nF/nBzfDOqRo2my4mWc=
-unknwon.dev/clog/v2 v2.1.1 h1:jBmBoMfsedJ/Sirm4/TdDy00mxh1vlbr9dM+AnYsNik=
-unknwon.dev/clog/v2 v2.1.1/go.mod h1:zvUlyibDHI4mykYdWyWje2G9nF/nBzfDOqRo2my4mWc=
 unknwon.dev/clog/v2 v2.1.2 h1:+jwPPp10UtOPunFtviUmXF01Abf6q7p5GEy4jluLl8o=
 unknwon.dev/clog/v2 v2.1.2/go.mod h1:zvUlyibDHI4mykYdWyWje2G9nF/nBzfDOqRo2my4mWc=
 xorm.io/builder v0.3.6 h1:ha28mQ2M+TFx96Hxo+iq6tQgnkC9IZkM6D8w9sKHHF8=

File diff suppressed because it is too large
+ 1 - 1
internal/assets/conf/conf_gen.go


+ 1 - 1
internal/assets/public/public_gen.go

@@ -19922,7 +19922,7 @@ func jsGogsJs() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "js/gogs.js", size: 47683, mode: os.FileMode(0644), modTime: time.Unix(1585391597, 0)}
+	info := bindataFileInfo{name: "js/gogs.js", size: 47683, mode: os.FileMode(0644), modTime: time.Unix(1585394288, 0)}
 	a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x45, 0x65, 0x98, 0x95, 0x98, 0xb5, 0x1, 0xee, 0x42, 0xf2, 0x3a, 0xc2, 0x75, 0xd2, 0x72, 0x86, 0xb7, 0xa3, 0x83, 0x6c, 0x24, 0x8f, 0x7, 0x22, 0x8c, 0xc3, 0xea, 0x4e, 0x7a, 0x74, 0x64, 0xd8}}
 	return a, nil
 }

+ 6 - 6
internal/auth/auth.go

@@ -48,18 +48,18 @@ func SignedInID(c *macaron.Context, sess session.Store) (_ int64, isTokenAuth bo
 
 		// Let's see if token is valid.
 		if len(tokenSHA) > 0 {
-			t, err := db.GetAccessTokenBySHA(tokenSHA)
+			t, err := db.AccessTokens.GetBySHA(tokenSHA)
 			if err != nil {
-				if !db.IsErrAccessTokenNotExist(err) && !db.IsErrAccessTokenEmpty(err) {
+				if !db.IsErrAccessTokenNotExist(err) {
 					log.Error("GetAccessTokenBySHA: %v", err)
 				}
 				return 0, false
 			}
 			t.Updated = time.Now()
-			if err = db.UpdateAccessToken(t); err != nil {
+			if err = db.AccessTokens.Save(t); err != nil {
 				log.Error("UpdateAccessToken: %v", err)
 			}
-			return t.UID, true
+			return t.UserID, true
 		}
 	}
 
@@ -90,7 +90,7 @@ func SignedInUser(ctx *macaron.Context, sess session.Store) (_ *db.User, isBasic
 
 	if uid <= 0 {
 		if conf.Auth.EnableReverseProxyAuthentication {
-			webAuthUser := ctx.Req.Header.Get(conf.Security.ReverseProxyAuthenticationUser)
+			webAuthUser := ctx.Req.Header.Get(conf.Auth.ReverseProxyAuthenticationHeader)
 			if len(webAuthUser) > 0 {
 				u, err := db.GetUserByName(webAuthUser)
 				if err != nil {
@@ -127,7 +127,7 @@ func SignedInUser(ctx *macaron.Context, sess session.Store) (_ *db.User, isBasic
 			if len(auths) == 2 && auths[0] == "Basic" {
 				uname, passwd, _ := tool.BasicAuthDecode(auths[1])
 
-				u, err := db.UserLogin(uname, passwd, -1)
+				u, err := db.Users.Authenticate(uname, passwd, -1)
 				if err != nil {
 					if !db.IsErrUserNotExist(err) {
 						log.Error("Failed to authenticate user: %v", err)

+ 35 - 0
internal/authutil/basic.go

@@ -0,0 +1,35 @@
+// Copyright 2020 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 authutil
+
+import (
+	"encoding/base64"
+	"net/http"
+	"strings"
+)
+
+// DecodeBasic extracts username and password from given header using HTTP Basic Auth.
+// It returns empty strings if values are not presented or not valid.
+func DecodeBasic(header http.Header) (username, password string) {
+	if len(header) == 0 {
+		return "", ""
+	}
+
+	fields := strings.Fields(header.Get("Authorization"))
+	if len(fields) != 2 || fields[0] != "Basic" {
+		return "", ""
+	}
+
+	p, err := base64.StdEncoding.DecodeString(fields[1])
+	if err != nil {
+		return "", ""
+	}
+
+	creds := strings.SplitN(string(p), ":", 2)
+	if len(creds) == 1 {
+		return creds[0], ""
+	}
+	return creds[0], creds[1]
+}

+ 72 - 0
internal/authutil/basic_test.go

@@ -0,0 +1,72 @@
+// Copyright 2020 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 authutil
+
+import (
+	"net/http"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDecodeBasic(t *testing.T) {
+	tests := []struct {
+		name        string
+		header      http.Header
+		expUsername string
+		expPassword string
+	}{
+		{
+			name: "no header",
+		},
+		{
+			name: "no authorization",
+			header: http.Header{
+				"Content-Type": []string{"text/plain"},
+			},
+		},
+		{
+			name: "malformed value",
+			header: http.Header{
+				"Authorization": []string{"Basic"},
+			},
+		},
+		{
+			name: "not basic",
+			header: http.Header{
+				"Authorization": []string{"Digest dummy"},
+			},
+		},
+		{
+			name: "bad encoding",
+			header: http.Header{
+				"Authorization": []string{"Basic not_base64"},
+			},
+		},
+
+		{
+			name: "only has username",
+			header: http.Header{
+				"Authorization": []string{"Basic dXNlcm5hbWU="},
+			},
+			expUsername: "username",
+		},
+		{
+			name: "has username and password",
+			header: http.Header{
+				"Authorization": []string{"Basic dXNlcm5hbWU6cGFzc3dvcmQ="},
+			},
+			expUsername: "username",
+			expPassword: "password",
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			username, password := DecodeBasic(test.header)
+			assert.Equal(t, test.expUsername, username)
+			assert.Equal(t, test.expPassword, password)
+		})
+	}
+}

+ 7 - 7
internal/cmd/serv.go

@@ -126,9 +126,9 @@ func checkDeployKey(key *db.PublicKey, repo *db.Repository) {
 
 var (
 	allowedCommands = map[string]db.AccessMode{
-		"git-upload-pack":    db.ACCESS_MODE_READ,
-		"git-upload-archive": db.ACCESS_MODE_READ,
-		"git-receive-pack":   db.ACCESS_MODE_WRITE,
+		"git-upload-pack":    db.AccessModeRead,
+		"git-upload-archive": db.AccessModeRead,
+		"git-receive-pack":   db.AccessModeWrite,
 	}
 )
 
@@ -184,7 +184,7 @@ func runServ(c *cli.Context) error {
 	}
 
 	// Prohibit push to mirror repositories.
-	if requestMode > db.ACCESS_MODE_READ && repo.IsMirror {
+	if requestMode > db.AccessModeRead && repo.IsMirror {
 		fail("Mirror repository is read-only", "")
 	}
 
@@ -196,7 +196,7 @@ func runServ(c *cli.Context) error {
 		fail("Invalid key ID", "Invalid key ID '%s': %v", c.Args()[0], err)
 	}
 
-	if requestMode == db.ACCESS_MODE_WRITE || repo.IsPrivate {
+	if requestMode == db.AccessModeWrite || repo.IsPrivate {
 		// Check deploy key or user key.
 		if key.IsDeployKey() {
 			if key.Mode < requestMode {
@@ -216,7 +216,7 @@ func runServ(c *cli.Context) error {
 
 			if mode < requestMode {
 				clientMessage := _ACCESS_DENIED_MESSAGE
-				if mode >= db.ACCESS_MODE_READ {
+				if mode >= db.AccessModeRead {
 					clientMessage = "You do not have sufficient authorization for this action"
 				}
 				fail(clientMessage,
@@ -259,7 +259,7 @@ func runServ(c *cli.Context) error {
 	} else {
 		gitCmd = exec.Command(verb, repoFullName)
 	}
-	if requestMode == db.ACCESS_MODE_WRITE {
+	if requestMode == db.AccessModeWrite {
 		gitCmd.Env = append(os.Environ(), db.ComposeHookEnvs(db.ComposeHookEnvsOptions{
 			AuthUser:  user,
 			OwnerName: owner.Name,

+ 8 - 4
internal/cmd/web.go

@@ -41,6 +41,7 @@ import (
 	"gogs.io/gogs/internal/route/admin"
 	apiv1 "gogs.io/gogs/internal/route/api/v1"
 	"gogs.io/gogs/internal/route/dev"
+	"gogs.io/gogs/internal/route/lfs"
 	"gogs.io/gogs/internal/route/org"
 	"gogs.io/gogs/internal/route/repo"
 	"gogs.io/gogs/internal/route/user"
@@ -648,11 +649,14 @@ func runWeb(c *cli.Context) error {
 		// e.g. with or without ".git" suffix.
 		m.Group("/:reponame([\\d\\w-_\\.]+\\.git$)", func() {
 			m.Get("", ignSignIn, context.RepoAssignment(), context.RepoRef(), repo.Home)
-			m.Options("/*", ignSignInAndCsrf, repo.HTTPContexter(), repo.HTTP)
-			m.Route("/*", "GET,POST", ignSignInAndCsrf, repo.HTTPContexter(), repo.HTTP)
+
+			m.Group("/info/lfs", func() {
+				lfs.RegisterRoutes(m.Router)
+			}, ignSignInAndCsrf)
+
+			m.Route("/*", "GET,POST,OPTIONS", ignSignInAndCsrf, repo.HTTPContexter(), repo.HTTP)
 		})
-		m.Options("/:reponame/*", ignSignInAndCsrf, repo.HTTPContexter(), repo.HTTP)
-		m.Route("/:reponame/*", "GET,POST", ignSignInAndCsrf, repo.HTTPContexter(), repo.HTTP)
+		m.Route("/:reponame/*", "GET,POST,OPTIONS", ignSignInAndCsrf, repo.HTTPContexter(), repo.HTTP)
 	})
 	// ***** END: Repository *****
 

+ 2 - 0
internal/conf/conf.go

@@ -365,6 +365,8 @@ func Init(customConf string) error {
 		return errors.Wrap(err, "mapping [cache] section")
 	} else if err = File.Section("http").MapTo(&HTTP); err != nil {
 		return errors.Wrap(err, "mapping [http] section")
+	} else if err = File.Section("lfs").MapTo(&LFS); err != nil {
+		return errors.Wrap(err, "mapping [lfs] section")
 	} else if err = File.Section("release").MapTo(&Release); err != nil {
 		return errors.Wrap(err, "mapping [release] section")
 	} else if err = File.Section("webhook").MapTo(&Webhook); err != nil {

+ 25 - 16
internal/conf/static.go

@@ -128,20 +128,7 @@ var (
 	}
 
 	// Database settings
-	Database struct {
-		Type     string
-		Host     string
-		Name     string
-		User     string
-		Password string
-		SSLMode  string `ini:"SSL_MODE"`
-		Path     string
-
-		// Deprecated: Use Type instead, will be removed in 0.13.
-		DbType string
-		// Deprecated: Use Password instead, will be removed in 0.13.
-		Passwd string
-	}
+	Database DatabaseOpts
 
 	// Security settings
 	Security struct {
@@ -243,6 +230,11 @@ var (
 		AccessControlAllowOrigin string
 	}
 
+	// LFS settings
+	LFS struct {
+		ObjectsPath string
+	}
+
 	// Attachment settings
 	Attachment struct {
 		Enabled      bool
@@ -252,7 +244,7 @@ var (
 		MaxFiles     int
 	}
 
-	// Release settigns
+	// Release settings
 	Release struct {
 		Attachment struct {
 			Enabled      bool
@@ -298,7 +290,7 @@ var (
 		PagingNum      int
 	}
 
-	// Markdown sttings
+	// Markdown settings
 	Markdown struct {
 		EnableHardLineBreak bool
 		CustomURLSchemes    []string `ini:"CUSTOM_URL_SCHEMES"`
@@ -409,6 +401,23 @@ var (
 	HasRobotsTxt bool
 )
 
+type DatabaseOpts struct {
+	Type         string
+	Host         string
+	Name         string
+	User         string
+	Password     string
+	SSLMode      string `ini:"SSL_MODE"`
+	Path         string
+	MaxOpenConns int
+	MaxIdleConns int
+
+	// Deprecated: Use Type instead, will be removed in 0.13.
+	DbType string
+	// Deprecated: Use Password instead, will be removed in 0.13.
+	Passwd string
+}
+
 type i18nConf struct {
 	Langs     []string          `delim:","`
 	Names     []string          `delim:","`

+ 2 - 0
internal/conf/testdata/TestInit.golden.ini

@@ -66,6 +66,8 @@ USER=gogs
 PASSWORD=12345678
 SSL_MODE=disable
 PATH=/tmp/gogs.db
+MAX_OPEN_CONNS=30
+MAX_IDLE_CONNS=30
 DB_TYPE=
 PASSWD=
 

+ 1 - 1
internal/context/org.go

@@ -133,7 +133,7 @@ func HandleOrgAssignment(c *Context, args ...bool) {
 			return
 		}
 
-		c.Org.IsTeamAdmin = c.Org.Team.IsOwnerTeam() || c.Org.Team.Authorize >= db.ACCESS_MODE_ADMIN
+		c.Org.IsTeamAdmin = c.Org.Team.IsOwnerTeam() || c.Org.Team.Authorize >= db.AccessModeAdmin
 		c.Data["IsTeamAdmin"] = c.Org.IsTeamAdmin
 		if requireTeamAdmin && !c.Org.IsTeamAdmin {
 			c.NotFound()

+ 6 - 6
internal/context/repo.go

@@ -52,22 +52,22 @@ type Repository struct {
 
 // IsOwner returns true if current user is the owner of repository.
 func (r *Repository) IsOwner() bool {
-	return r.AccessMode >= db.ACCESS_MODE_OWNER
+	return r.AccessMode >= db.AccessModeOwner
 }
 
 // IsAdmin returns true if current user has admin or higher access of repository.
 func (r *Repository) IsAdmin() bool {
-	return r.AccessMode >= db.ACCESS_MODE_ADMIN
+	return r.AccessMode >= db.AccessModeAdmin
 }
 
 // IsWriter returns true if current user has write or higher access of repository.
 func (r *Repository) IsWriter() bool {
-	return r.AccessMode >= db.ACCESS_MODE_WRITE
+	return r.AccessMode >= db.AccessModeWrite
 }
 
 // HasAccess returns true if the current user has at least read access for this repository
 func (r *Repository) HasAccess() bool {
-	return r.AccessMode >= db.ACCESS_MODE_READ
+	return r.AccessMode >= db.AccessModeRead
 }
 
 // CanEnableEditor returns true if repository is editable and user has proper access level.
@@ -168,7 +168,7 @@ func RepoAssignment(pages ...bool) macaron.Handler {
 
 		// Admin has super access.
 		if c.IsLogged && c.User.IsAdmin {
-			c.Repo.AccessMode = db.ACCESS_MODE_OWNER
+			c.Repo.AccessMode = db.AccessModeOwner
 		} else {
 			mode, err := db.UserAccessMode(c.UserID(), repo)
 			if err != nil {
@@ -179,7 +179,7 @@ func RepoAssignment(pages ...bool) macaron.Handler {
 		}
 
 		// Check access
-		if c.Repo.AccessMode == db.ACCESS_MODE_NONE {
+		if c.Repo.AccessMode == db.AccessModeNone {
 			// Redirect to any accessible page if not yet on it
 			if repo.IsPartialPublic() &&
 				(!(isIssuesPage || isWikiPage) ||

+ 19 - 18
internal/db/access.go

@@ -13,22 +13,22 @@ import (
 type AccessMode int
 
 const (
-	ACCESS_MODE_NONE  AccessMode = iota // 0
-	ACCESS_MODE_READ                    // 1
-	ACCESS_MODE_WRITE                   // 2
-	ACCESS_MODE_ADMIN                   // 3
-	ACCESS_MODE_OWNER                   // 4
+	AccessModeNone  AccessMode = iota // 0
+	AccessModeRead                    // 1
+	AccessModeWrite                   // 2
+	AccessModeAdmin                   // 3
+	AccessModeOwner                   // 4
 )
 
 func (mode AccessMode) String() string {
 	switch mode {
-	case ACCESS_MODE_READ:
+	case AccessModeRead:
 		return "read"
-	case ACCESS_MODE_WRITE:
+	case AccessModeWrite:
 		return "write"
-	case ACCESS_MODE_ADMIN:
+	case AccessModeAdmin:
 		return "admin"
-	case ACCESS_MODE_OWNER:
+	case AccessModeOwner:
 		return "owner"
 	default:
 		return "none"
@@ -39,15 +39,15 @@ func (mode AccessMode) String() string {
 func ParseAccessMode(permission string) AccessMode {
 	switch permission {
 	case "write":
-		return ACCESS_MODE_WRITE
+		return AccessModeWrite
 	case "admin":
-		return ACCESS_MODE_ADMIN
+		return AccessModeAdmin
 	default:
-		return ACCESS_MODE_READ
+		return AccessModeRead
 	}
 }
 
-// Access represents the highest access level of a user to the repository. The only access type
+// Access represents the highest access level of a user to a repository. The only access type
 // that is not in this table is the real owner of a repository. In case of an organization
 // repository, the members of the owners team are in this table.
 type Access struct {
@@ -58,10 +58,10 @@ type Access struct {
 }
 
 func userAccessMode(e Engine, userID int64, repo *Repository) (AccessMode, error) {
-	mode := ACCESS_MODE_NONE
+	mode := AccessModeNone
 	// Everyone has read access to public repository
 	if !repo.IsPrivate {
-		mode = ACCESS_MODE_READ
+		mode = AccessModeRead
 	}
 
 	if userID <= 0 {
@@ -69,7 +69,7 @@ func userAccessMode(e Engine, userID int64, repo *Repository) (AccessMode, error
 	}
 
 	if userID == repo.OwnerID {
-		return ACCESS_MODE_OWNER, nil
+		return AccessModeOwner, nil
 	}
 
 	access := &Access{
@@ -93,6 +93,7 @@ func hasAccess(e Engine, userID int64, repo *Repository, testMode AccessMode) (b
 }
 
 // HasAccess returns true if someone has the request access level. User can be nil!
+// Deprecated: Use Perms.HasAccess instead.
 func HasAccess(userID int64, repo *Repository, testMode AccessMode) (bool, error) {
 	return hasAccess(x, userID, repo, testMode)
 }
@@ -136,7 +137,7 @@ func (user *User) GetAccessibleRepositories(limit int) (repos []*Repository, _ e
 }
 
 func maxAccessMode(modes ...AccessMode) AccessMode {
-	max := ACCESS_MODE_NONE
+	max := AccessModeNone
 	for _, mode := range modes {
 		if mode > max {
 			max = mode
@@ -205,7 +206,7 @@ func (repo *Repository) recalculateTeamAccesses(e Engine, ignTeamID int64) (err
 		// Owner team gets owner access, and skip for teams that do not
 		// have relations with repository.
 		if t.IsOwnerTeam() {
-			t.Authorize = ACCESS_MODE_OWNER
+			t.Authorize = AccessModeOwner
 		} else if !t.hasRepository(e, repo.ID) {
 			continue
 		}

+ 65 - 0
internal/db/access_tokens.go

@@ -0,0 +1,65 @@
+// Copyright 2020 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 db
+
+import (
+	"fmt"
+
+	"github.com/jinzhu/gorm"
+
+	"gogs.io/gogs/internal/errutil"
+)
+
+// AccessTokensStore is the persistent interface for access tokens.
+//
+// NOTE: All methods are sorted in alphabetical order.
+type AccessTokensStore interface {
+	// GetBySHA returns the access token with given SHA1.
+	// It returns ErrAccessTokenNotExist when not found.
+	GetBySHA(sha string) (*AccessToken, error)
+	// Save persists all values of given access token.
+	Save(t *AccessToken) error
+}
+
+var AccessTokens AccessTokensStore
+
+type accessTokens struct {
+	*gorm.DB
+}
+
+var _ errutil.NotFound = (*ErrAccessTokenNotExist)(nil)
+
+type ErrAccessTokenNotExist struct {
+	args errutil.Args
+}
+
+func IsErrAccessTokenNotExist(err error) bool {
+	_, ok := err.(ErrAccessTokenNotExist)
+	return ok
+}
+
+func (err ErrAccessTokenNotExist) Error() string {
+	return fmt.Sprintf("access token does not exist: %v", err.args)
+}
+
+func (ErrAccessTokenNotExist) NotFound() bool {
+	return true
+}
+
+func (db *accessTokens) GetBySHA(sha string) (*AccessToken, error) {
+	token := new(AccessToken)
+	err := db.Where("sha1 = ?", sha).First(token).Error
+	if err != nil {
+		if err == gorm.ErrRecordNotFound {
+			return nil, ErrAccessTokenNotExist{args: errutil.Args{"sha": sha}}
+		}
+		return nil, err
+	}
+	return token, nil
+}
+
+func (db *accessTokens) Save(t *AccessToken) error {
+	return db.DB.Save(t).Error
+}

+ 171 - 0
internal/db/db.go

@@ -0,0 +1,171 @@
+// Copyright 2020 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 db
+
+import (
+	"fmt"
+	"io"
+	"net/url"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"github.com/jinzhu/gorm"
+	_ "github.com/jinzhu/gorm/dialects/mssql"
+	_ "github.com/jinzhu/gorm/dialects/mysql"
+	_ "github.com/jinzhu/gorm/dialects/postgres"
+	_ "github.com/jinzhu/gorm/dialects/sqlite"
+	"github.com/pkg/errors"
+	log "unknwon.dev/clog/v2"
+
+	"gogs.io/gogs/internal/conf"
+	"gogs.io/gogs/internal/dbutil"
+)
+
+// parsePostgreSQLHostPort parses given input in various forms defined in
+// https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING
+// and returns proper host and port number.
+func parsePostgreSQLHostPort(info string) (string, string) {
+	host, port := "127.0.0.1", "5432"
+	if strings.Contains(info, ":") && !strings.HasSuffix(info, "]") {
+		idx := strings.LastIndex(info, ":")
+		host = info[:idx]
+		port = info[idx+1:]
+	} else if len(info) > 0 {
+		host = info
+	}
+	return host, port
+}
+
+func parseMSSQLHostPort(info string) (string, string) {
+	host, port := "127.0.0.1", "1433"
+	if strings.Contains(info, ":") {
+		host = strings.Split(info, ":")[0]
+		port = strings.Split(info, ":")[1]
+	} else if strings.Contains(info, ",") {
+		host = strings.Split(info, ",")[0]
+		port = strings.TrimSpace(strings.Split(info, ",")[1])
+	} else if len(info) > 0 {
+		host = info
+	}
+	return host, port
+}
+
+// parseDSN takes given database options and returns parsed DSN.
+func parseDSN(opts conf.DatabaseOpts) (dsn string, err error) {
+	// In case the database name contains "?" with some parameters
+	concate := "?"
+	if strings.Contains(opts.Name, concate) {
+		concate = "&"
+	}
+
+	switch opts.Type {
+	case "mysql":
+		if opts.Host[0] == '/' { // Looks like a unix socket
+			dsn = fmt.Sprintf("%s:%s@unix(%s)/%s%scharset=utf8mb4&parseTime=true",
+				opts.User, opts.Password, opts.Host, opts.Name, concate)
+		} else {
+			dsn = fmt.Sprintf("%s:%s@tcp(%s)/%s%scharset=utf8mb4&parseTime=true",
+				opts.User, opts.Password, opts.Host, opts.Name, concate)
+		}
+
+	case "postgres":
+		host, port := parsePostgreSQLHostPort(opts.Host)
+		if host[0] == '/' { // looks like a unix socket
+			dsn = fmt.Sprintf("postgres://%s:%s@:%s/%s%ssslmode=%s&host=%s",
+				url.QueryEscape(opts.User), url.QueryEscape(opts.Password), port, opts.Name, concate, opts.SSLMode, host)
+		} else {
+			dsn = fmt.Sprintf("postgres://%s:%s@%s:%s/%s%ssslmode=%s",
+				url.QueryEscape(opts.User), url.QueryEscape(opts.Password), host, port, opts.Name, concate, opts.SSLMode)
+		}
+
+	case "mssql":
+		host, port := parseMSSQLHostPort(opts.Host)
+		dsn = fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;",
+			host, port, opts.Name, opts.User, opts.Password)
+
+	case "sqlite3":
+		dsn = "file:" + opts.Path + "?cache=shared&mode=rwc"
+
+	default:
+		return "", errors.Errorf("unrecognized dialect: %s", opts.Type)
+	}
+
+	return dsn, nil
+}
+
+func openDB(opts conf.DatabaseOpts) (*gorm.DB, error) {
+	dsn, err := parseDSN(opts)
+	if err != nil {
+		return nil, errors.Wrap(err, "parse DSN")
+	}
+
+	return gorm.Open(opts.Type, dsn)
+}
+
+func getLogWriter() (io.Writer, error) {
+	sec := conf.File.Section("log.gorm")
+	w, err := log.NewFileWriter(
+		filepath.Join(conf.Log.RootPath, "gorm.log"),
+		log.FileRotationConfig{
+			Rotate:  sec.Key("ROTATE").MustBool(true),
+			Daily:   sec.Key("ROTATE_DAILY").MustBool(true),
+			MaxSize: sec.Key("MAX_SIZE").MustInt64(100) * 1024 * 1024,
+			MaxDays: sec.Key("MAX_DAYS").MustInt64(3),
+		},
+	)
+	if err != nil {
+		return nil, errors.Wrap(err, `create "gorm.log"`)
+	}
+	return w, nil
+}
+
+func Init() error {
+	db, err := openDB(conf.Database)
+	if err != nil {
+		return errors.Wrap(err, "open database")
+	}
+	db.SingularTable(true)
+	db.DB().SetMaxOpenConns(conf.Database.MaxOpenConns)
+	db.DB().SetMaxIdleConns(conf.Database.MaxIdleConns)
+	db.DB().SetConnMaxLifetime(time.Minute)
+
+	w, err := getLogWriter()
+	if err != nil {
+		return errors.Wrap(err, "get log writer")
+	}
+	db.SetLogger(&dbutil.Writer{Writer: w})
+	if !conf.IsProdMode() {
+		db = db.LogMode(true)
+	}
+
+	switch conf.Database.Type {
+	case "mysql":
+		conf.UseMySQL = true
+		db = db.Set("gorm:table_options", "ENGINE=InnoDB")
+	case "postgres":
+		conf.UsePostgreSQL = true
+	case "mssql":
+		conf.UseMSSQL = true
+	case "sqlite3":
+		conf.UseMySQL = true
+	}
+
+	err = db.AutoMigrate(new(LFSObject)).Error
+	if err != nil {
+		return errors.Wrap(err, "migrate schemes")
+	}
+
+	// Initialize stores, sorted in alphabetical order.
+	AccessTokens = &accessTokens{DB: db}
+	LoginSources = &loginSources{DB: db}
+	LFS = &lfs{DB: db}
+	Perms = &perms{DB: db}
+	Repos = &repos{DB: db}
+	TwoFactors = &twoFactors{DB: db}
+	Users = &users{DB: db}
+
+	return db.DB().Ping()
+}

+ 0 - 32
internal/db/error.go

@@ -218,38 +218,6 @@ func (err ErrDeployKeyNameAlreadyUsed) Error() string {
 	return fmt.Sprintf("public key already exists [repo_id: %d, name: %s]", err.RepoID, err.Name)
 }
 
-//    _____                                   ___________     __
-//   /  _  \   ____  ____  ____   ______ _____\__    ___/___ |  | __ ____   ____
-//  /  /_\  \_/ ___\/ ___\/ __ \ /  ___//  ___/ |    | /  _ \|  |/ // __ \ /    \
-// /    |    \  \__\  \__\  ___/ \___ \ \___ \  |    |(  <_> )    <\  ___/|   |  \
-// \____|__  /\___  >___  >___  >____  >____  > |____| \____/|__|_ \\___  >___|  /
-//         \/     \/    \/    \/     \/     \/                    \/    \/     \/
-
-type ErrAccessTokenNotExist struct {
-	SHA string
-}
-
-func IsErrAccessTokenNotExist(err error) bool {
-	_, ok := err.(ErrAccessTokenNotExist)
-	return ok
-}
-
-func (err ErrAccessTokenNotExist) Error() string {
-	return fmt.Sprintf("access token does not exist [sha: %s]", err.SHA)
-}
-
-type ErrAccessTokenEmpty struct {
-}
-
-func IsErrAccessTokenEmpty(err error) bool {
-	_, ok := err.(ErrAccessTokenEmpty)
-	return ok
-}
-
-func (err ErrAccessTokenEmpty) Error() string {
-	return fmt.Sprintf("access token is empty")
-}
-
 // ________                            .__                __  .__
 // \_____  \_______  _________    ____ |__|____________ _/  |_|__| ____   ____
 //  /   |   \_  __ \/ ___\__  \  /    \|  \___   /\__  \\   __\  |/  _ \ /    \

+ 0 - 13
internal/db/errors/login_source.go

@@ -45,16 +45,3 @@ func (err InvalidLoginSourceType) Error() string {
 	return fmt.Sprintf("invalid login source type [type: %v]", err.Type)
 }
 
-type LoginSourceMismatch struct {
-	Expect int64
-	Actual int64
-}
-
-func IsLoginSourceMismatch(err error) bool {
-	_, ok := err.(LoginSourceMismatch)
-	return ok
-}
-
-func (err LoginSourceMismatch) Error() string {
-	return fmt.Sprintf("login source mismatch [expect: %d, actual: %d]", err.Expect, err.Actual)
-}

+ 0 - 13
internal/db/errors/two_factor.go

@@ -18,16 +18,3 @@ func IsTwoFactorNotFound(err error) bool {
 func (err TwoFactorNotFound) Error() string {
 	return fmt.Sprintf("two-factor authentication does not found [user_id: %d]", err.UserID)
 }
-
-type TwoFactorRecoveryCodeNotFound struct {
-	Code string
-}
-
-func IsTwoFactorRecoveryCodeNotFound(err error) bool {
-	_, ok := err.(TwoFactorRecoveryCodeNotFound)
-	return ok
-}
-
-func (err TwoFactorRecoveryCodeNotFound) Error() string {
-	return fmt.Sprintf("two-factor recovery code does not found [code: %s]", err.Code)
-}

+ 1 - 1
internal/db/issue.go

@@ -681,7 +681,7 @@ func newIssue(e *xorm.Session, opts NewIssueOptions) (err error) {
 		// Assume assignee is invalid and drop silently.
 		opts.Issue.AssigneeID = 0
 		if assignee != nil {
-			valid, err := hasAccess(e, assignee.ID, opts.Repo, ACCESS_MODE_READ)
+			valid, err := hasAccess(e, assignee.ID, opts.Repo, AccessModeRead)
 			if err != nil {
 				return fmt.Errorf("hasAccess [user_id: %d, repo_id: %d]: %v", assignee.ID, opts.Repo.ID, err)
 			}

+ 129 - 0
internal/db/lfs.go

@@ -0,0 +1,129 @@
+// Copyright 2020 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 db
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"time"
+
+	"github.com/jinzhu/gorm"
+	"github.com/pkg/errors"
+
+	"gogs.io/gogs/internal/conf"
+	"gogs.io/gogs/internal/errutil"
+	"gogs.io/gogs/internal/lfsutil"
+)
+
+// LFSStore is the persistent interface for LFS objects.
+//
+// NOTE: All methods are sorted in alphabetical order.
+type LFSStore interface {
+	// CreateObject streams io.ReadCloser to target storage and creates a record in database.
+	CreateObject(repoID int64, oid lfsutil.OID, rc io.ReadCloser, storage lfsutil.Storage) error
+	// GetObjectByOID returns the LFS object with given OID. It returns ErrLFSObjectNotExist
+	// when not found.
+	GetObjectByOID(repoID int64, oid lfsutil.OID) (*LFSObject, error)
+	// GetObjectsByOIDs returns LFS objects found within "oids". The returned list could have
+	// less elements if some oids were not found.
+	GetObjectsByOIDs(repoID int64, oids ...lfsutil.OID) ([]*LFSObject, error)
+}
+
+var LFS LFSStore
+
+type lfs struct {
+	*gorm.DB
+}
+
+// LFSObject is the relation between an LFS object and a repository.
+type LFSObject struct {
+	RepoID    int64           `gorm:"PRIMARY_KEY;AUTO_INCREMENT:false"`
+	OID       lfsutil.OID     `gorm:"PRIMARY_KEY;column:oid"`
+	Size      int64           `gorm:"NOT NULL"`
+	Storage   lfsutil.Storage `gorm:"NOT NULL"`
+	CreatedAt time.Time       `gorm:"NOT NULL"`
+}
+
+func (db *lfs) CreateObject(repoID int64, oid lfsutil.OID, rc io.ReadCloser, storage lfsutil.Storage) (err error) {
+	if storage != lfsutil.StorageLocal {
+		return errors.New("only local storage is supported")
+	}
+
+	fpath := lfsutil.StorageLocalPath(conf.LFS.ObjectsPath, oid)
+	defer func() {
+		rc.Close()
+
+		if err != nil {
+			_ = os.Remove(fpath)
+		}
+	}()
+
+	err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm)
+	if err != nil {
+		return errors.Wrap(err, "create directories")
+	}
+	w, err := os.Create(fpath)
+	if err != nil {
+		return errors.Wrap(err, "create file")
+	}
+	defer w.Close()
+
+	written, err := io.Copy(w, rc)
+	if err != nil {
+		return errors.Wrap(err, "copy file")
+	}
+
+	object := &LFSObject{
+		RepoID:  repoID,
+		OID:     oid,
+		Size:    written,
+		Storage: storage,
+	}
+	return db.DB.Create(object).Error
+}
+
+type ErrLFSObjectNotExist struct {
+	args errutil.Args
+}
+
+func IsErrLFSObjectNotExist(err error) bool {
+	_, ok := err.(ErrLFSObjectNotExist)
+	return ok
+}
+
+func (err ErrLFSObjectNotExist) Error() string {
+	return fmt.Sprintf("LFS object does not exist: %v", err.args)
+}
+
+func (ErrLFSObjectNotExist) NotFound() bool {
+	return true
+}
+
+func (db *lfs) GetObjectByOID(repoID int64, oid lfsutil.OID) (*LFSObject, error) {
+	object := new(LFSObject)
+	err := db.Where("repo_id = ? AND oid = ?", repoID, oid).First(object).Error
+	if err != nil {
+		if err == gorm.ErrRecordNotFound {
+			return nil, ErrLFSObjectNotExist{args: errutil.Args{"repoID": repoID, "oid": oid}}
+		}
+		return nil, err
+	}
+	return object, err
+}
+
+func (db *lfs) GetObjectsByOIDs(repoID int64, oids ...lfsutil.OID) ([]*LFSObject, error) {
+	if len(oids) == 0 {
+		return []*LFSObject{}, nil
+	}
+
+	objects := make([]*LFSObject, 0, len(oids))
+	err := db.Where("repo_id = ? AND oid IN (?)", repoID, oids).Find(&objects).Error
+	if err != nil && err != gorm.ErrRecordNotFound {
+		return nil, err
+	}
+	return objects, nil
+}

+ 63 - 129
internal/db/login_source.go

@@ -35,21 +35,21 @@ type LoginType int
 
 // Note: new type must append to the end of list to maintain compatibility.
 const (
-	LOGIN_NOTYPE LoginType = iota
-	LOGIN_PLAIN            // 1
-	LOGIN_LDAP             // 2
-	LOGIN_SMTP             // 3
-	LOGIN_PAM              // 4
-	LOGIN_DLDAP            // 5
-	LOGIN_GITHUB           // 6
+	LoginNotype LoginType = iota
+	LoginPlain            // 1
+	LoginLDAP             // 2
+	LoginSMTP             // 3
+	LoginPAM              // 4
+	LoginDLDAP            // 5
+	LoginGitHub           // 6
 )
 
 var LoginNames = map[LoginType]string{
-	LOGIN_LDAP:   "LDAP (via BindDN)",
-	LOGIN_DLDAP:  "LDAP (simple auth)", // Via direct bind
-	LOGIN_SMTP:   "SMTP",
-	LOGIN_PAM:    "PAM",
-	LOGIN_GITHUB: "GitHub",
+	LoginLDAP:   "LDAP (via BindDN)",
+	LoginDLDAP:  "LDAP (simple auth)", // Via direct bind
+	LoginSMTP:   "SMTP",
+	LoginPAM:    "PAM",
+	LoginGitHub: "GitHub",
 }
 
 var SecurityProtocolNames = map[ldap.SecurityProtocol]string{
@@ -185,13 +185,13 @@ func (s *LoginSource) BeforeSet(colName string, val xorm.Cell) {
 	switch colName {
 	case "type":
 		switch LoginType(Cell2Int64(val)) {
-		case LOGIN_LDAP, LOGIN_DLDAP:
+		case LoginLDAP, LoginDLDAP:
 			s.Cfg = new(LDAPConfig)
-		case LOGIN_SMTP:
+		case LoginSMTP:
 			s.Cfg = new(SMTPConfig)
-		case LOGIN_PAM:
+		case LoginPAM:
 			s.Cfg = new(PAMConfig)
-		case LOGIN_GITHUB:
+		case LoginGitHub:
 			s.Cfg = new(GitHubConfig)
 		default:
 			panic("unrecognized login source type: " + com.ToStr(*val))
@@ -213,23 +213,23 @@ func (s *LoginSource) TypeName() string {
 }
 
 func (s *LoginSource) IsLDAP() bool {
-	return s.Type == LOGIN_LDAP
+	return s.Type == LoginLDAP
 }
 
 func (s *LoginSource) IsDLDAP() bool {
-	return s.Type == LOGIN_DLDAP
+	return s.Type == LoginDLDAP
 }
 
 func (s *LoginSource) IsSMTP() bool {
-	return s.Type == LOGIN_SMTP
+	return s.Type == LoginSMTP
 }
 
 func (s *LoginSource) IsPAM() bool {
-	return s.Type == LOGIN_PAM
+	return s.Type == LoginPAM
 }
 
 func (s *LoginSource) IsGitHub() bool {
-	return s.Type == LOGIN_GITHUB
+	return s.Type == LoginGitHub
 }
 
 func (s *LoginSource) HasTLS() bool {
@@ -240,9 +240,9 @@ func (s *LoginSource) HasTLS() bool {
 
 func (s *LoginSource) UseTLS() bool {
 	switch s.Type {
-	case LOGIN_LDAP, LOGIN_DLDAP:
+	case LoginLDAP, LoginDLDAP:
 		return s.LDAP().SecurityProtocol != ldap.SECURITY_PROTOCOL_UNENCRYPTED
-	case LOGIN_SMTP:
+	case LoginSMTP:
 		return s.SMTP().TLS
 	}
 
@@ -251,9 +251,9 @@ func (s *LoginSource) UseTLS() bool {
 
 func (s *LoginSource) SkipVerify() bool {
 	switch s.Type {
-	case LOGIN_LDAP, LOGIN_DLDAP:
+	case LoginLDAP, LoginDLDAP:
 		return s.LDAP().SkipVerify
-	case LOGIN_SMTP:
+	case LoginSMTP:
 		return s.SMTP().SkipVerify
 	}
 
@@ -293,8 +293,8 @@ func CreateLoginSource(source *LoginSource) error {
 	return nil
 }
 
-// LoginSources returns all login sources defined.
-func LoginSources() ([]*LoginSource, error) {
+// ListLoginSources returns all login sources defined.
+func ListLoginSources() ([]*LoginSource, error) {
 	sources := make([]*LoginSource, 0, 2)
 	if err := x.Find(&sources); err != nil {
 		return nil, err
@@ -312,18 +312,6 @@ func ActivatedLoginSources() ([]*LoginSource, error) {
 	return append(sources, localLoginSources.ActivatedList()...), nil
 }
 
-// GetLoginSourceByID returns login source by given ID.
-func GetLoginSourceByID(id int64) (*LoginSource, error) {
-	source := new(LoginSource)
-	has, err := x.Id(id).Get(source)
-	if err != nil {
-		return nil, err
-	} else if !has {
-		return localLoginSources.GetLoginSourceByID(id)
-	}
-	return source, nil
-}
-
 // ResetNonDefaultLoginSources clean other default source flag
 func ResetNonDefaultLoginSources(source *LoginSource) error {
 	// update changes to DB
@@ -504,19 +492,19 @@ func LoadAuthSources() {
 		authType := s.Key("type").String()
 		switch authType {
 		case "ldap_bind_dn":
-			loginSource.Type = LOGIN_LDAP
+			loginSource.Type = LoginLDAP
 			loginSource.Cfg = &LDAPConfig{}
 		case "ldap_simple_auth":
-			loginSource.Type = LOGIN_DLDAP
+			loginSource.Type = LoginDLDAP
 			loginSource.Cfg = &LDAPConfig{}
 		case "smtp":
-			loginSource.Type = LOGIN_SMTP
+			loginSource.Type = LoginSMTP
 			loginSource.Cfg = &SMTPConfig{}
 		case "pam":
-			loginSource.Type = LOGIN_PAM
+			loginSource.Type = LoginPAM
 			loginSource.Cfg = &PAMConfig{}
 		case "github":
-			loginSource.Type = LOGIN_GITHUB
+			loginSource.Type = LoginGitHub
 			loginSource.Cfg = &GitHubConfig{}
 		default:
 			log.Fatal("Failed to load authentication source: unknown type '%s'", authType)
@@ -552,15 +540,15 @@ func composeFullName(firstname, surname, username string) string {
 
 // LoginViaLDAP queries if login/password is valid against the LDAP directory pool,
 // and create a local user if success when enabled.
-func LoginViaLDAP(user *User, login, password string, source *LoginSource, autoRegister bool) (*User, error) {
-	username, fn, sn, mail, isAdmin, succeed := source.Cfg.(*LDAPConfig).SearchEntry(login, password, source.Type == LOGIN_DLDAP)
+func LoginViaLDAP(login, password string, source *LoginSource, autoRegister bool) (*User, error) {
+	username, fn, sn, mail, isAdmin, succeed := source.Cfg.(*LDAPConfig).SearchEntry(login, password, source.Type == LoginDLDAP)
 	if !succeed {
 		// User not in LDAP, do nothing
 		return nil, ErrUserNotExist{args: map[string]interface{}{"login": login}}
 	}
 
 	if !autoRegister {
-		return user, nil
+		return nil, nil
 	}
 
 	// Fallback.
@@ -576,7 +564,7 @@ func LoginViaLDAP(user *User, login, password string, source *LoginSource, autoR
 		mail = fmt.Sprintf("%s@localhost", username)
 	}
 
-	user = &User{
+	user := &User{
 		LowerName:   strings.ToLower(username),
 		Name:        username,
 		FullName:    composeFullName(fn, sn, username),
@@ -669,7 +657,7 @@ func SMTPAuth(a smtp.Auth, cfg *SMTPConfig) error {
 
 // LoginViaSMTP queries if login/password is valid against the SMTP,
 // and create a local user if success when enabled.
-func LoginViaSMTP(user *User, login, password string, sourceID int64, cfg *SMTPConfig, autoRegister bool) (*User, error) {
+func LoginViaSMTP(login, password string, sourceID int64, cfg *SMTPConfig, autoRegister bool) (*User, error) {
 	// Verify allowed domains.
 	if len(cfg.AllowedDomains) > 0 {
 		idx := strings.Index(login, "@")
@@ -701,7 +689,7 @@ func LoginViaSMTP(user *User, login, password string, sourceID int64, cfg *SMTPC
 	}
 
 	if !autoRegister {
-		return user, nil
+		return nil, nil
 	}
 
 	username := login
@@ -710,12 +698,12 @@ func LoginViaSMTP(user *User, login, password string, sourceID int64, cfg *SMTPC
 		username = login[:idx]
 	}
 
-	user = &User{
+	user := &User{
 		LowerName:   strings.ToLower(username),
 		Name:        strings.ToLower(username),
 		Email:       login,
 		Passwd:      password,
-		LoginType:   LOGIN_SMTP,
+		LoginType:   LoginSMTP,
 		LoginSource: sourceID,
 		LoginName:   login,
 		IsActive:    true,
@@ -732,7 +720,7 @@ func LoginViaSMTP(user *User, login, password string, sourceID int64, cfg *SMTPC
 
 // LoginViaPAM queries if login/password is valid against the PAM,
 // and create a local user if success when enabled.
-func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMConfig, autoRegister bool) (*User, error) {
+func LoginViaPAM(login, password string, sourceID int64, cfg *PAMConfig, autoRegister bool) (*User, error) {
 	if err := pam.PAMAuth(cfg.ServiceName, login, password); err != nil {
 		if strings.Contains(err.Error(), "Authentication failure") {
 			return nil, ErrUserNotExist{args: map[string]interface{}{"login": login}}
@@ -741,15 +729,15 @@ func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMCon
 	}
 
 	if !autoRegister {
-		return user, nil
+		return nil, nil
 	}
 
-	user = &User{
+	user := &User{
 		LowerName:   strings.ToLower(login),
 		Name:        login,
 		Email:       login,
 		Passwd:      password,
-		LoginType:   LOGIN_PAM,
+		LoginType:   LoginPAM,
 		LoginSource: sourceID,
 		LoginName:   login,
 		IsActive:    true,
@@ -757,14 +745,14 @@ func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMCon
 	return user, CreateUser(user)
 }
 
-//________.__  __     ___ ___      ___.
-///  _____/|__|/  |_  /   |   \ __ _\_ |__
-///   \  ___|  \   __\/    ~    \  |  \ __ \
-//\    \_\  \  ||  |  \    Y    /  |  / \_\ \
-//\______  /__||__|   \___|_  /|____/|___  /
-//\/                 \/           \/
+// ________.__  __     ___ ___      ___.
+// /  _____/|__|/  |_  /   |   \ __ _\_ |__
+// /   \  ___|  \   __\/    ~    \  |  \ __ \
+// \    \_\  \  ||  |  \    Y    /  |  / \_\ \
+// \______  /__||__|   \___|_  /|____/|___  /
+// \/                 \/           \/
 
-func LoginViaGitHub(user *User, login, password string, sourceID int64, cfg *GitHubConfig, autoRegister bool) (*User, error) {
+func LoginViaGitHub(login, password string, sourceID int64, cfg *GitHubConfig, autoRegister bool) (*User, error) {
 	fullname, email, url, location, err := github.Authenticate(cfg.APIEndpoint, login, password)
 	if err != nil {
 		if strings.Contains(err.Error(), "401") {
@@ -774,16 +762,16 @@ func LoginViaGitHub(user *User, login, password string, sourceID int64, cfg *Git
 	}
 
 	if !autoRegister {
-		return user, nil
+		return nil, nil
 	}
-	user = &User{
+	user := &User{
 		LowerName:   strings.ToLower(login),
 		Name:        login,
 		FullName:    fullname,
 		Email:       email,
 		Website:     url,
 		Passwd:      password,
-		LoginType:   LOGIN_GITHUB,
+		LoginType:   LoginGitHub,
 		LoginSource: sourceID,
 		LoginName:   login,
 		IsActive:    true,
@@ -792,75 +780,21 @@ func LoginViaGitHub(user *User, login, password string, sourceID int64, cfg *Git
 	return user, CreateUser(user)
 }
 
-func remoteUserLogin(user *User, login, password string, source *LoginSource, autoRegister bool) (*User, error) {
+func authenticateViaLoginSource(source *LoginSource, login, password string, autoRegister bool) (*User, error) {
 	if !source.IsActived {
 		return nil, errors.LoginSourceNotActivated{SourceID: source.ID}
 	}
 
 	switch source.Type {
-	case LOGIN_LDAP, LOGIN_DLDAP:
-		return LoginViaLDAP(user, login, password, source, autoRegister)
-	case LOGIN_SMTP:
-		return LoginViaSMTP(user, login, password, source.ID, source.Cfg.(*SMTPConfig), autoRegister)
-	case LOGIN_PAM:
-		return LoginViaPAM(user, login, password, source.ID, source.Cfg.(*PAMConfig), autoRegister)
-	case LOGIN_GITHUB:
-		return LoginViaGitHub(user, login, password, source.ID, source.Cfg.(*GitHubConfig), autoRegister)
+	case LoginLDAP, LoginDLDAP:
+		return LoginViaLDAP(login, password, source, autoRegister)
+	case LoginSMTP:
+		return LoginViaSMTP(login, password, source.ID, source.Cfg.(*SMTPConfig), autoRegister)
+	case LoginPAM:
+		return LoginViaPAM(login, password, source.ID, source.Cfg.(*PAMConfig), autoRegister)
+	case LoginGitHub:
+		return LoginViaGitHub(login, password, source.ID, source.Cfg.(*GitHubConfig), autoRegister)
 	}
 
 	return nil, errors.InvalidLoginSourceType{Type: source.Type}
 }
-
-// UserLogin validates user name and password via given login source ID.
-// If the loginSourceID is negative, it will abort login process if user is not found.
-func UserLogin(username, password string, loginSourceID int64) (*User, error) {
-	var user *User
-	if strings.Contains(username, "@") {
-		user = &User{Email: strings.ToLower(username)}
-	} else {
-		user = &User{LowerName: strings.ToLower(username)}
-	}
-
-	hasUser, err := x.Get(user)
-	if err != nil {
-		return nil, fmt.Errorf("get user record: %v", err)
-	}
-
-	if hasUser {
-		// Note: This check is unnecessary but to reduce user confusion at login page
-		// and make it more consistent at user's perspective.
-		if loginSourceID >= 0 && user.LoginSource != loginSourceID {
-			return nil, errors.LoginSourceMismatch{Expect: loginSourceID, Actual: user.LoginSource}
-		}
-
-		// Validate password hash fetched from database for local accounts
-		if user.LoginType == LOGIN_NOTYPE ||
-			user.LoginType == LOGIN_PLAIN {
-			if user.ValidatePassword(password) {
-				return user, nil
-			}
-
-			return nil, ErrUserNotExist{args: map[string]interface{}{"userID": user.ID, "name": user.Name}}
-		}
-
-		// Remote login to the login source the user is associated with
-		source, err := GetLoginSourceByID(user.LoginSource)
-		if err != nil {
-			return nil, err
-		}
-
-		return remoteUserLogin(user, user.LoginName, password, source, false)
-	}
-
-	// Non-local login source is always greater than 0
-	if loginSourceID <= 0 {
-		return nil, ErrUserNotExist{args: map[string]interface{}{"name": username}}
-	}
-
-	source, err := GetLoginSourceByID(loginSourceID)
-	if err != nil {
-		return nil, err
-	}
-
-	return remoteUserLogin(nil, username, password, source, true)
-}

+ 36 - 0
internal/db/login_sources.go

@@ -0,0 +1,36 @@
+// Copyright 2020 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 db
+
+import (
+	"github.com/jinzhu/gorm"
+)
+
+// LoginSourcesStore is the persistent interface for login sources.
+//
+// NOTE: All methods are sorted in alphabetical order.
+type LoginSourcesStore interface {
+	// GetByID returns the login source with given ID.
+	// It returns ErrLoginSourceNotExist when not found.
+	GetByID(id int64) (*LoginSource, error)
+}
+
+var LoginSources LoginSourcesStore
+
+type loginSources struct {
+	*gorm.DB
+}
+
+func (db *loginSources) GetByID(id int64) (*LoginSource, error) {
+	source := new(LoginSource)
+	err := db.Where("id = ?", id).First(source).Error
+	if err != nil {
+		if err == gorm.ErrRecordNotFound {
+			return localLoginSources.GetLoginSourceByID(id)
+		}
+		return nil, err
+	}
+	return source, nil
+}

+ 8 - 47
internal/db/models.go

@@ -7,7 +7,6 @@ package db
 import (
 	"bufio"
 	"database/sql"
-	"errors"
 	"fmt"
 	"net/url"
 	"os"
@@ -15,10 +14,7 @@ import (
 	"strings"
 	"time"
 
-	_ "github.com/denisenkom/go-mssqldb"
-	_ "github.com/go-sql-driver/mysql"
 	"github.com/json-iterator/go"
-	_ "github.com/lib/pq"
 	"github.com/unknwon/com"
 	log "unknwon.dev/clog/v2"
 	"xorm.io/core"
@@ -48,8 +44,6 @@ var (
 	x         *xorm.Engine
 	tables    []interface{}
 	HasEngine bool
-
-	EnableSQLite3 bool
 )
 
 func init() {
@@ -70,35 +64,6 @@ func init() {
 	}
 }
 
-// parsePostgreSQLHostPort parses given input in various forms defined in
-// https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING
-// and returns proper host and port number.
-func parsePostgreSQLHostPort(info string) (string, string) {
-	host, port := "127.0.0.1", "5432"
-	if strings.Contains(info, ":") && !strings.HasSuffix(info, "]") {
-		idx := strings.LastIndex(info, ":")
-		host = info[:idx]
-		port = info[idx+1:]
-	} else if len(info) > 0 {
-		host = info
-	}
-	return host, port
-}
-
-func parseMSSQLHostPort(info string) (string, string) {
-	host, port := "127.0.0.1", "1433"
-	if strings.Contains(info, ":") {
-		host = strings.Split(info, ":")[0]
-		port = strings.Split(info, ":")[1]
-	} else if strings.Contains(info, ",") {
-		host = strings.Split(info, ",")[0]
-		port = strings.TrimSpace(strings.Split(info, ",")[1])
-	} else if len(info) > 0 {
-		host = info
-	}
-	return host, port
-}
-
 func getEngine() (*xorm.Engine, error) {
 	Param := "?"
 	if strings.Contains(conf.Database.Name, Param) {
@@ -133,12 +98,9 @@ func getEngine() (*xorm.Engine, error) {
 	case "mssql":
 		conf.UseMSSQL = true
 		host, port := parseMSSQLHostPort(conf.Database.Host)
-		connStr = fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", host, port, conf.Database.Name, conf.Database.User, conf.Database.Passwd)
+		connStr = fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", host, port, conf.Database.Name, conf.Database.User, conf.Database.Password)
 
 	case "sqlite3":
-		if !EnableSQLite3 {
-			return nil, errors.New("this binary version does not build support for SQLite3")
-		}
 		if err := os.MkdirAll(path.Dir(conf.Database.Path), os.ModePerm); err != nil {
 			return nil, fmt.Errorf("create directories: %v", err)
 		}
@@ -183,9 +145,8 @@ func SetEngine() (err error) {
 		return fmt.Errorf("create 'xorm.log': %v", err)
 	}
 
-	// To prevent mystery "MySQL: invalid connection" error,
-	// see https://gogs.io/gogs/issues/5532.
-	x.SetMaxIdleConns(0)
+	x.SetMaxOpenConns(conf.Database.MaxOpenConns)
+	x.SetMaxIdleConns(conf.Database.MaxIdleConns)
 	x.SetConnMaxLifetime(time.Second)
 
 	if conf.IsProdMode() {
@@ -194,7 +155,7 @@ func SetEngine() (err error) {
 		x.SetLogger(xorm.NewSimpleLogger(logger))
 	}
 	x.ShowSQL(true)
-	return nil
+	return Init()
 }
 
 func NewEngine() (err error) {
@@ -331,13 +292,13 @@ func ImportDatabase(dirPath string, verbose bool) (err error) {
 
 				tp := LoginType(com.StrTo(com.ToStr(meta["Type"])).MustInt64())
 				switch tp {
-				case LOGIN_LDAP, LOGIN_DLDAP:
+				case LoginLDAP, LoginDLDAP:
 					bean.Cfg = new(LDAPConfig)
-				case LOGIN_SMTP:
+				case LoginSMTP:
 					bean.Cfg = new(SMTPConfig)
-				case LOGIN_PAM:
+				case LoginPAM:
 					bean.Cfg = new(PAMConfig)
-				case LOGIN_GITHUB:
+				case LoginGitHub:
 					bean.Cfg = new(GitHubConfig)
 				default:
 					return fmt.Errorf("unrecognized login source type:: %v", tp)

+ 0 - 15
internal/db/models_sqlite.go

@@ -1,15 +0,0 @@
-// +build sqlite
-
-// 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 db
-
-import (
-	_ "github.com/mattn/go-sqlite3"
-)
-
-func init() {
-	EnableSQLite3 = true
-}

+ 1 - 1
internal/db/org.go

@@ -148,7 +148,7 @@ func CreateOrganization(org, owner *User) (err error) {
 		OrgID:      org.ID,
 		LowerName:  strings.ToLower(OWNER_TEAM),
 		Name:       OWNER_TEAM,
-		Authorize:  ACCESS_MODE_OWNER,
+		Authorize:  AccessModeOwner,
 		NumMembers: 1,
 	}
 	if _, err = sess.Insert(t); err != nil {

+ 2 - 2
internal/db/org_team.go

@@ -47,7 +47,7 @@ func (t *Team) IsOwnerTeam() bool {
 
 // HasWriteAccess returns true if team has at least write level access mode.
 func (t *Team) HasWriteAccess() bool {
-	return t.Authorize >= ACCESS_MODE_WRITE
+	return t.Authorize >= AccessModeWrite
 }
 
 // IsTeamMember returns true if given user is a member of team.
@@ -174,7 +174,7 @@ func (t *Team) removeRepository(e Engine, repo *Repository, recalculate bool) (e
 		return fmt.Errorf("get team members: %v", err)
 	}
 	for _, member := range t.Members {
-		has, err := hasAccess(e, member.ID, repo, ACCESS_MODE_READ)
+		has, err := hasAccess(e, member.ID, repo, AccessModeRead)
 		if err != nil {
 			return err
 		} else if has {

+ 56 - 0
internal/db/perms.go

@@ -0,0 +1,56 @@
+// Copyright 2020 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 db
+
+import (
+	"github.com/jinzhu/gorm"
+	log "unknwon.dev/clog/v2"
+)
+
+// PermsStore is the persistent interface for permissions.
+//
+// NOTE: All methods are sorted in alphabetical order.
+type PermsStore interface {
+	// AccessMode returns the access mode of given user has to the repository.
+	AccessMode(userID int64, repo *Repository) AccessMode
+	// Authorize returns true if the user has as good as desired access mode to
+	// the repository.
+	Authorize(userID int64, repo *Repository, desired AccessMode) bool
+}
+
+var Perms PermsStore
+
+type perms struct {
+	*gorm.DB
+}
+
+func (db *perms) AccessMode(userID int64, repo *Repository) AccessMode {
+	var mode AccessMode
+	// Everyone has read access to public repository.
+	if !repo.IsPrivate {
+		mode = AccessModeRead
+	}
+
+	// Quick check to avoid a DB query.
+	if userID <= 0 {
+		return mode
+	}
+
+	if userID == repo.OwnerID {
+		return AccessModeOwner
+	}
+
+	access := new(Access)
+	err := db.Where("user_id = ? AND repo_id = ?", userID, repo.ID).First(access).Error
+	if err != nil {
+		log.Error("Failed to get access [user_id: %d, repo_id: %d]: %v", userID, repo.ID, err)
+		return mode
+	}
+	return access.Mode
+}
+
+func (db *perms) Authorize(userID int64, repo *Repository, desired AccessMode) bool {
+	return desired <= db.AccessMode(userID, repo)
+}

+ 4 - 3
internal/db/repo.go

@@ -492,7 +492,7 @@ func (repo *Repository) getUsersWithAccesMode(e Engine, mode AccessMode) (_ []*U
 
 // getAssignees returns a list of users who can be assigned to issues in this repository.
 func (repo *Repository) getAssignees(e Engine) (_ []*User, err error) {
-	return repo.getUsersWithAccesMode(e, ACCESS_MODE_READ)
+	return repo.getUsersWithAccesMode(e, AccessModeRead)
 }
 
 // GetAssignees returns all users that have read access and can be assigned to issues
@@ -508,7 +508,7 @@ func (repo *Repository) GetAssigneeByID(userID int64) (*User, error) {
 
 // GetWriters returns all users that have write access to the repository.
 func (repo *Repository) GetWriters() (_ []*User, err error) {
-	return repo.getUsersWithAccesMode(x, ACCESS_MODE_WRITE)
+	return repo.getUsersWithAccesMode(x, AccessModeWrite)
 }
 
 // GetMilestoneByID returns the milestone belongs to repository by given ID.
@@ -551,7 +551,7 @@ func (repo *Repository) ComposeCompareURL(oldCommitID, newCommitID string) strin
 }
 
 func (repo *Repository) HasAccess(userID int64) bool {
-	has, _ := HasAccess(userID, repo, ACCESS_MODE_READ)
+	has, _ := HasAccess(userID, repo, AccessModeRead)
 	return has
 }
 
@@ -1666,6 +1666,7 @@ func (ErrRepoNotExist) NotFound() bool {
 }
 
 // GetRepositoryByName returns the repository by given name under user if exists.
+// Deprecated: Use Repos.GetByName instead.
 func GetRepositoryByName(ownerID int64, name string) (*Repository, error) {
 	repo := &Repository{
 		OwnerID:   ownerID,

+ 2 - 2
internal/db/repo_branch.go

@@ -175,7 +175,7 @@ func UpdateOrgProtectBranch(repo *Repository, protectBranch *ProtectBranch, whit
 		userIDs := tool.StringsToInt64s(strings.Split(whitelistUserIDs, ","))
 		validUserIDs = make([]int64, 0, len(userIDs))
 		for _, userID := range userIDs {
-			has, err := HasAccess(userID, repo, ACCESS_MODE_WRITE)
+			has, err := HasAccess(userID, repo, AccessModeWrite)
 			if err != nil {
 				return fmt.Errorf("HasAccess [user_id: %d, repo_id: %d]: %v", userID, protectBranch.RepoID, err)
 			} else if !has {
@@ -193,7 +193,7 @@ func UpdateOrgProtectBranch(repo *Repository, protectBranch *ProtectBranch, whit
 	if protectBranch.WhitelistTeamIDs != whitelistTeamIDs {
 		hasTeamsChanged = true
 		teamIDs := tool.StringsToInt64s(strings.Split(whitelistTeamIDs, ","))
-		teams, err := GetTeamsHaveAccessToRepo(repo.OwnerID, repo.ID, ACCESS_MODE_WRITE)
+		teams, err := GetTeamsHaveAccessToRepo(repo.OwnerID, repo.ID, AccessModeWrite)
 		if err != nil {
 			return fmt.Errorf("GetTeamsHaveAccessToRepo [org_id: %d, repo_id: %d]: %v", repo.OwnerID, repo.ID, err)
 		}

+ 8 - 8
internal/db/repo_collaboration.go

@@ -22,11 +22,11 @@ type Collaboration struct {
 
 func (c *Collaboration) ModeI18nKey() string {
 	switch c.Mode {
-	case ACCESS_MODE_READ:
+	case AccessModeRead:
 		return "repo.settings.collaboration.read"
-	case ACCESS_MODE_WRITE:
+	case AccessModeWrite:
 		return "repo.settings.collaboration.write"
-	case ACCESS_MODE_ADMIN:
+	case AccessModeAdmin:
 		return "repo.settings.collaboration.admin"
 	default:
 		return "repo.settings.collaboration.undefined"
@@ -64,7 +64,7 @@ func (repo *Repository) AddCollaborator(u *User) error {
 	} else if has {
 		return nil
 	}
-	collaboration.Mode = ACCESS_MODE_WRITE
+	collaboration.Mode = AccessModeWrite
 
 	sess := x.NewSession()
 	defer sess.Close()
@@ -96,9 +96,9 @@ func (c *Collaborator) APIFormat() *api.Collaborator {
 	return &api.Collaborator{
 		User: c.User.APIFormat(),
 		Permissions: api.Permission{
-			Admin: c.Collaboration.Mode >= ACCESS_MODE_ADMIN,
-			Push:  c.Collaboration.Mode >= ACCESS_MODE_WRITE,
-			Pull:  c.Collaboration.Mode >= ACCESS_MODE_READ,
+			Admin: c.Collaboration.Mode >= AccessModeAdmin,
+			Push:  c.Collaboration.Mode >= AccessModeWrite,
+			Pull:  c.Collaboration.Mode >= AccessModeRead,
 		},
 	}
 }
@@ -131,7 +131,7 @@ func (repo *Repository) GetCollaborators() ([]*Collaborator, error) {
 // ChangeCollaborationAccessMode sets new access mode for the collaboration.
 func (repo *Repository) ChangeCollaborationAccessMode(userID int64, mode AccessMode) error {
 	// Discard invalid input
-	if mode <= ACCESS_MODE_NONE || mode > ACCESS_MODE_OWNER {
+	if mode <= AccessModeNone || mode > AccessModeOwner {
 		return nil
 	}
 

+ 38 - 0
internal/db/repos.go

@@ -0,0 +1,38 @@
+// Copyright 2020 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 db
+
+import (
+	"strings"
+
+	"github.com/jinzhu/gorm"
+)
+
+// ReposStore is the persistent interface for repositories.
+//
+// NOTE: All methods are sorted in alphabetical order.
+type ReposStore interface {
+	// GetByName returns the repository with given owner and name.
+	// It returns ErrRepoNotExist when not found.
+	GetByName(ownerID int64, name string) (*Repository, error)
+}
+
+var Repos ReposStore
+
+type repos struct {
+	*gorm.DB
+}
+
+func (db *repos) GetByName(ownerID int64, name string) (*Repository, error) {
+	repo := new(Repository)
+	err := db.Where("owner_id = ? AND lower_name = ?", ownerID, strings.ToLower(name)).First(repo).Error
+	if err != nil {
+		if err == gorm.ErrRecordNotFound {
+			return nil, ErrRepoNotExist{args: map[string]interface{}{"ownerID": ownerID, "name": name}}
+		}
+		return nil, err
+	}
+	return repo, nil
+}

+ 3 - 3
internal/db/ssh_key.go

@@ -426,7 +426,7 @@ func AddPublicKey(ownerID int64, name, content string) (*PublicKey, error) {
 		OwnerID: ownerID,
 		Name:    name,
 		Content: content,
-		Mode:    ACCESS_MODE_WRITE,
+		Mode:    AccessModeWrite,
 		Type:    KEY_TYPE_USER,
 	}
 	if err = addKey(sess, key); err != nil {
@@ -656,7 +656,7 @@ func AddDeployKey(repoID int64, name, content string) (*DeployKey, error) {
 
 	pkey := &PublicKey{
 		Content: content,
-		Mode:    ACCESS_MODE_READ,
+		Mode:    AccessModeRead,
 		Type:    KEY_TYPE_DEPLOY,
 	}
 	has, err := x.Get(pkey)
@@ -753,7 +753,7 @@ func DeleteDeployKey(doer *User, id int64) error {
 		if err != nil {
 			return fmt.Errorf("GetRepositoryByID: %v", err)
 		}
-		yes, err := HasAccess(doer.ID, repo, ACCESS_MODE_ADMIN)
+		yes, err := HasAccess(doer.ID, repo, AccessModeAdmin)
 		if err != nil {
 			return fmt.Errorf("HasAccess: %v", err)
 		} else if !yes {

+ 12 - 33
internal/db/token.go

@@ -16,17 +16,17 @@ import (
 
 // AccessToken represents a personal access token.
 type AccessToken struct {
-	ID   int64
-	UID  int64 `xorm:"INDEX"`
-	Name string
-	Sha1 string `xorm:"UNIQUE VARCHAR(40)"`
+	ID     int64
+	UserID int64 `xorm:"uid INDEX" gorm:"COLUMN:uid"`
+	Name   string
+	Sha1   string `xorm:"UNIQUE VARCHAR(40)"`
 
-	Created           time.Time `xorm:"-" json:"-"`
+	Created           time.Time `xorm:"-" gorm:"-" json:"-"`
 	CreatedUnix       int64
-	Updated           time.Time `xorm:"-" json:"-"` // Note: Updated must below Created for AfterSet.
+	Updated           time.Time `xorm:"-" gorm:"-" json:"-"` // Note: Updated must below Created for AfterSet.
 	UpdatedUnix       int64
-	HasRecentActivity bool `xorm:"-" json:"-"`
-	HasUsed           bool `xorm:"-" json:"-"`
+	HasRecentActivity bool `xorm:"-" gorm:"-" json:"-"`
+	HasUsed           bool `xorm:"-" gorm:"-" json:"-"`
 }
 
 func (t *AccessToken) BeforeInsert() {
@@ -52,8 +52,8 @@ func (t *AccessToken) AfterSet(colName string, _ xorm.Cell) {
 func NewAccessToken(t *AccessToken) error {
 	t.Sha1 = tool.SHA1(gouuid.NewV4().String())
 	has, err := x.Get(&AccessToken{
-		UID:  t.UID,
-		Name: t.Name,
+		UserID: t.UserID,
+		Name:   t.Name,
 	})
 	if err != nil {
 		return err
@@ -65,38 +65,17 @@ func NewAccessToken(t *AccessToken) error {
 	return err
 }
 
-// GetAccessTokenBySHA returns access token by given sha1.
-func GetAccessTokenBySHA(sha string) (*AccessToken, error) {
-	if sha == "" {
-		return nil, ErrAccessTokenEmpty{}
-	}
-	t := &AccessToken{Sha1: sha}
-	has, err := x.Get(t)
-	if err != nil {
-		return nil, err
-	} else if !has {
-		return nil, ErrAccessTokenNotExist{sha}
-	}
-	return t, nil
-}
-
 // ListAccessTokens returns a list of access tokens belongs to given user.
 func ListAccessTokens(uid int64) ([]*AccessToken, error) {
 	tokens := make([]*AccessToken, 0, 5)
 	return tokens, x.Where("uid=?", uid).Desc("id").Find(&tokens)
 }
 
-// UpdateAccessToken updates information of access token.
-func UpdateAccessToken(t *AccessToken) error {
-	_, err := x.Id(t.ID).AllCols().Update(t)
-	return err
-}
-
 // DeleteAccessTokenOfUserByID deletes access token by given ID.
 func DeleteAccessTokenOfUserByID(userID, id int64) error {
 	_, err := x.Delete(&AccessToken{
-		ID:  id,
-		UID: userID,
+		ID:     id,
+		UserID: userID,
 	})
 	return err
 }

+ 14 - 11
internal/db/two_factor.go

@@ -12,7 +12,6 @@ import (
 
 	"github.com/pquerna/otp/totp"
 	"github.com/unknwon/com"
-	log "unknwon.dev/clog/v2"
 	"xorm.io/xorm"
 
 	"gogs.io/gogs/internal/conf"
@@ -54,15 +53,6 @@ func (t *TwoFactor) ValidateTOTP(passcode string) (bool, error) {
 	return totp.Validate(passcode, string(decryptSecret)), nil
 }
 
-// IsUserEnabledTwoFactor returns true if user has enabled two-factor authentication.
-func IsUserEnabledTwoFactor(userID int64) bool {
-	has, err := x.Where("user_id = ?", userID).Get(new(TwoFactor))
-	if err != nil {
-		log.Error("IsUserEnabledTwoFactor [user_id: %d]: %v", userID, err)
-	}
-	return has
-}
-
 func generateRecoveryCodes(userID int64) ([]*TwoFactorRecoveryCode, error) {
 	recoveryCodes := make([]*TwoFactorRecoveryCode, 10)
 	for i := 0; i < 10; i++ {
@@ -182,6 +172,19 @@ func RegenerateRecoveryCodes(userID int64) error {
 	return sess.Commit()
 }
 
+type ErrTwoFactorRecoveryCodeNotFound struct {
+	Code string
+}
+
+func IsTwoFactorRecoveryCodeNotFound(err error) bool {
+	_, ok := err.(ErrTwoFactorRecoveryCodeNotFound)
+	return ok
+}
+
+func (err ErrTwoFactorRecoveryCodeNotFound) Error() string {
+	return fmt.Sprintf("two-factor recovery code does not found [code: %s]", err.Code)
+}
+
 // UseRecoveryCode validates recovery code of given user and marks it is used if valid.
 func UseRecoveryCode(userID int64, code string) error {
 	recoveryCode := new(TwoFactorRecoveryCode)
@@ -189,7 +192,7 @@ func UseRecoveryCode(userID int64, code string) error {
 	if err != nil {
 		return fmt.Errorf("get unused code: %v", err)
 	} else if !has {
-		return errors.TwoFactorRecoveryCodeNotFound{Code: code}
+		return ErrTwoFactorRecoveryCodeNotFound{Code: code}
 	}
 
 	recoveryCode.IsUsed = true

+ 33 - 0
internal/db/two_factors.go

@@ -0,0 +1,33 @@
+// Copyright 2020 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 db
+
+import (
+	"github.com/jinzhu/gorm"
+	log "unknwon.dev/clog/v2"
+)
+
+// TwoFactorsStore is the persistent interface for 2FA.
+//
+// NOTE: All methods are sorted in alphabetical order.
+type TwoFactorsStore interface {
+	// IsUserEnabled returns true if the user has enabled 2FA.
+	IsUserEnabled(userID int64) bool
+}
+
+var TwoFactors TwoFactorsStore
+
+type twoFactors struct {
+	*gorm.DB
+}
+
+func (db *twoFactors) IsUserEnabled(userID int64) bool {
+	var count int64
+	err := db.Model(new(TwoFactor)).Where("user_id = ?", userID).Count(&count).Error
+	if err != nil {
+		log.Error("Failed to count two factors [user_id: %d]: %v", userID, err)
+	}
+	return count > 0
+}

+ 10 - 8
internal/db/user.go

@@ -139,9 +139,9 @@ func (u *User) APIFormat() *api.User {
 	}
 }
 
-// returns true if user login type is LOGIN_PLAIN.
+// returns true if user login type is LoginPlain.
 func (u *User) IsLocal() bool {
-	return u.LoginType <= LOGIN_PLAIN
+	return u.LoginType <= LoginPlain
 }
 
 // HasForkedRepo checks if user has already forked a repository with given ID.
@@ -369,7 +369,7 @@ func (u *User) DeleteAvatar() error {
 
 // IsAdminOfRepo returns true if user has admin or higher access of repository.
 func (u *User) IsAdminOfRepo(repo *Repository) bool {
-	has, err := HasAccess(u.ID, repo, ACCESS_MODE_ADMIN)
+	has, err := HasAccess(u.ID, repo, AccessModeAdmin)
 	if err != nil {
 		log.Error("HasAccess: %v", err)
 	}
@@ -378,7 +378,7 @@ func (u *User) IsAdminOfRepo(repo *Repository) bool {
 
 // IsWriterOfRepo returns true if user has write access to given repository.
 func (u *User) IsWriterOfRepo(repo *Repository) bool {
-	has, err := HasAccess(u.ID, repo, ACCESS_MODE_WRITE)
+	has, err := HasAccess(u.ID, repo, AccessModeWrite)
 	if err != nil {
 		log.Error("HasAccess: %v", err)
 	}
@@ -402,7 +402,7 @@ func (u *User) IsPublicMember(orgId int64) bool {
 
 // IsEnabledTwoFactor returns true if user has enabled two-factor authentication.
 func (u *User) IsEnabledTwoFactor() bool {
-	return IsUserEnabledTwoFactor(u.ID)
+	return TwoFactors.IsUserEnabled(u.ID)
 }
 
 func (u *User) getOrganizationCount(e Engine) (int64, error) {
@@ -590,7 +590,7 @@ func CountUsers() int64 {
 }
 
 // Users returns number of users in given page.
-func Users(page, pageSize int) ([]*User, error) {
+func ListUsers(page, pageSize int) ([]*User, error) {
 	users := make([]*User, 0, pageSize)
 	return users, x.Limit(pageSize, (page-1)*pageSize).Where("type=0").Asc("id").Find(&users)
 }
@@ -786,7 +786,7 @@ func deleteUser(e *xorm.Session, u *User) error {
 	// ***** END: Follow *****
 
 	if err = deleteBeans(e,
-		&AccessToken{UID: u.ID},
+		&AccessToken{UserID: u.ID},
 		&Collaboration{UserID: u.ID},
 		&Access{UserID: u.ID},
 		&Watch{UserID: u.ID},
@@ -922,13 +922,14 @@ func getUserByID(e Engine, id int64) (*User, error) {
 }
 
 // GetUserByID returns the user object by given ID if exists.
+// Deprecated: Use Users.GetByID instead.
 func GetUserByID(id int64) (*User, error) {
 	return getUserByID(x, id)
 }
 
 // GetAssigneeByID returns the user with write access of repository by given ID.
 func GetAssigneeByID(repo *Repository, userID int64) (*User, error) {
-	has, err := HasAccess(userID, repo, ACCESS_MODE_READ)
+	has, err := HasAccess(userID, repo, AccessModeRead)
 	if err != nil {
 		return nil, err
 	} else if !has {
@@ -938,6 +939,7 @@ func GetAssigneeByID(repo *Repository, userID int64) (*User, error) {
 }
 
 // GetUserByName returns a user by given name.
+// Deprecated: Use Users.GetByUsername instead.
 func GetUserByName(name string) (*User, error) {
 	if len(name) == 0 {
 		return nil, ErrUserNotExist{args: map[string]interface{}{"name": name}}

+ 138 - 0
internal/db/users.go

@@ -0,0 +1,138 @@
+// Copyright 2020 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 db
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/jinzhu/gorm"
+	"github.com/pkg/errors"
+
+	"gogs.io/gogs/internal/errutil"
+)
+
+// UsersStore is the persistent interface for users.
+//
+// NOTE: All methods are sorted in alphabetical order.
+type UsersStore interface {
+	// Authenticate validates username and password via given login source ID.
+	// It returns ErrUserNotExist when the user was not found.
+	//
+	// When the "loginSourceID" is negative, it aborts the process and returns
+	// ErrUserNotExist if the user was not found in the database.
+	//
+	// When the "loginSourceID" is non-negative, it returns ErrLoginSourceMismatch
+	// if the user has different login source ID than the "loginSourceID".
+	//
+	// When the "loginSourceID" is positive, it tries to authenticate via given
+	// login source and creates a new user when not yet exists in the database.
+	Authenticate(username, password string, loginSourceID int64) (*User, error)
+	// GetByID returns the user with given ID. It returns ErrUserNotExist when not found.
+	GetByID(id int64) (*User, error)
+	// GetByUsername returns the user with given username. It returns ErrUserNotExist
+	// when not found.
+	GetByUsername(username string) (*User, error)
+}
+
+var Users UsersStore
+
+type users struct {
+	*gorm.DB
+}
+
+type ErrLoginSourceMismatch struct {
+	args errutil.Args
+}
+
+func (err ErrLoginSourceMismatch) Error() string {
+	return fmt.Sprintf("login source mismatch: %v", err.args)
+}
+
+func (db *users) Authenticate(username, password string, loginSourceID int64) (*User, error) {
+	username = strings.ToLower(username)
+
+	var query *gorm.DB
+	if strings.Contains(username, "@") {
+		query = db.Where("email = ?", username)
+	} else {
+		query = db.Where("lower_name = ?", username)
+	}
+
+	user := new(User)
+	err := query.First(user).Error
+	if err != nil && err != gorm.ErrRecordNotFound {
+		return nil, errors.Wrap(err, "get user")
+	}
+
+	// User found in the database
+	if err == nil {
+		// Note: This check is unnecessary but to reduce user confusion at login page
+		// and make it more consistent from user's perspective.
+		if loginSourceID >= 0 && user.LoginSource != loginSourceID {
+			return nil, ErrLoginSourceMismatch{args: errutil.Args{"expect": loginSourceID, "actual": user.LoginSource}}
+		}
+
+		// Validate password hash fetched from database for local accounts.
+		if user.LoginType == LoginNotype || user.LoginType == LoginPlain {
+			if user.ValidatePassword(password) {
+				return user, nil
+			}
+
+			return nil, ErrUserNotExist{args: map[string]interface{}{"userID": user.ID, "name": user.Name}}
+		}
+
+		source, err := LoginSources.GetByID(user.LoginSource)
+		if err != nil {
+			return nil, errors.Wrap(err, "get login source")
+		}
+
+		_, err = authenticateViaLoginSource(source, username, password, false)
+		if err != nil {
+			return nil, errors.Wrap(err, "authenticate via login source")
+		}
+		return user, nil
+	}
+
+	// Non-local login source is always greater than 0.
+	if loginSourceID <= 0 {
+		return nil, ErrUserNotExist{args: map[string]interface{}{"name": username}}
+	}
+
+	source, err := LoginSources.GetByID(loginSourceID)
+	if err != nil {
+		return nil, errors.Wrap(err, "get login source")
+	}
+
+	user, err = authenticateViaLoginSource(source, username, password, true)
+	if err != nil {
+		return nil, errors.Wrap(err, "authenticate via login source")
+	}
+	return user, nil
+}
+
+func (db *users) GetByID(id int64) (*User, error) {
+	user := new(User)
+	err := db.Where("id = ?", id).First(user).Error
+	if err != nil {
+		if err == gorm.ErrRecordNotFound {
+			return nil, ErrUserNotExist{args: map[string]interface{}{"userID": id}}
+		}
+		return nil, err
+	}
+	return user, nil
+}
+
+func (db *users) GetByUsername(username string) (*User, error) {
+	user := new(User)
+	err := db.Where("lower_name = ?", strings.ToLower(username)).First(user).Error
+	if err != nil {
+		if err == gorm.ErrRecordNotFound {
+			return nil, ErrUserNotExist{args: map[string]interface{}{"name": username}}
+		}
+		return nil, err
+	}
+	return user, nil
+}

+ 35 - 0
internal/dbutil/writer.go

@@ -0,0 +1,35 @@
+// Copyright 2020 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 dbutil
+
+import (
+	"fmt"
+	"io"
+)
+
+// Writer is a wrapper of io.Writer for the gorm.logger.
+type Writer struct {
+	io.Writer
+}
+
+func (w *Writer) Print(v ...interface{}) {
+	if len(v) == 0 {
+		return
+	}
+
+	if len(v) == 1 {
+		fmt.Fprint(w.Writer, v[0])
+		return
+	}
+
+	switch v[0] {
+	case "sql":
+		fmt.Fprintf(w.Writer, "[sql] [%s] [%s] %s %v (%d rows affected)", v[1:]...)
+	case "log":
+		fmt.Fprintf(w.Writer, "[log] [%s] %s", v[1:]...)
+	default:
+		fmt.Fprint(w.Writer, v...)
+	}
+}

+ 53 - 0
internal/dbutil/writer_test.go

@@ -0,0 +1,53 @@
+// Copyright 2020 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 dbutil
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestWriter_Print(t *testing.T) {
+	tests := []struct {
+		name      string
+		vs        []interface{}
+		expOutput string
+	}{
+		{
+			name: "no values",
+		},
+		{
+			name:      "only one value",
+			vs:        []interface{}{"test"},
+			expOutput: "test",
+		},
+		{
+			name:      "two values",
+			vs:        []interface{}{"test", "output"},
+			expOutput: "testoutput",
+		},
+
+		{
+			name:      "sql",
+			vs:        []interface{}{"sql", "writer.go:65", "1ms", "SELECT * FROM users WHERE user_id = $1", []int{1}, 1},
+			expOutput: "[sql] [writer.go:65] [1ms] SELECT * FROM users WHERE user_id = $1 [1] (1 rows affected)",
+		},
+		{
+			name:      "log",
+			vs:        []interface{}{"log", "writer.go:65", "something"},
+			expOutput: "[log] [writer.go:65] something",
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			var buf bytes.Buffer
+			w := &Writer{Writer: &buf}
+			w.Print(test.vs...)
+			assert.Equal(t, test.expOutput, buf.String())
+		})
+	}
+}

+ 3 - 0
internal/errutil/errutil.go

@@ -14,3 +14,6 @@ func IsNotFound(err error) bool {
 	e, ok := err.(NotFound)
 	return ok && e.NotFound()
 }
+
+// Args is a map of key-value pairs to provide additional context of an error.
+type Args map[string]interface{}

+ 1 - 1
internal/gitutil/module.go

@@ -10,7 +10,7 @@ import (
 
 // ModuleStore is the interface for Git operations.
 //
-// NOTE: All methods are sorted in alphabetically.
+// NOTE: All methods are sorted in alphabetical order.
 type ModuleStore interface {
 	// AddRemote adds a new remote to the repository in given path.
 	RepoAddRemote(repoPath, name, url string, opts ...git.AddRemoteOptions) error

+ 30 - 0
internal/lfsutil/oid.go

@@ -0,0 +1,30 @@
+// Copyright 2020 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 lfsutil
+
+import (
+	"strings"
+)
+
+// OID is an LFS object ID.
+type OID string
+
+// ValidOID returns true if given oid is valid according to spec:
+// https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md
+func ValidOID(oid OID) bool {
+	fields := strings.SplitN(string(oid), ":", 2)
+	if len(fields) != 2 {
+		return false
+	}
+	method := fields[0]
+	hash := fields[1]
+
+	switch method {
+	case "sha256":
+		// SHA256 produces 64-char lower case hexadecimal hash
+		return len(hash) == 64 && strings.ToLower(hash) == hash
+	}
+	return false
+}

+ 47 - 0
internal/lfsutil/oid_test.go

@@ -0,0 +1,47 @@
+// Copyright 2020 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 lfsutil
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestValidOID(t *testing.T) {
+	tests := []struct {
+		name   string
+		oid    OID
+		expVal bool
+	}{
+		{
+			name: "malformed",
+			oid:  OID("12345678"),
+		},
+		{
+			name: "unknown method",
+			oid:  OID("sha1:7c222fb2927d828af22f592134e8932480637c0d"),
+		},
+
+		{
+			name: "sha256: malformed",
+			oid:  OID("sha256:7c222fb2927d828af22f592134e8932480637c0d"),
+		},
+		{
+			name: "sha256: not all lower cased",
+			oid:  OID("sha256:EF797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f"),
+		},
+		{
+			name:   "sha256: valid",
+			oid:    OID("sha256:ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f"),
+			expVal: true,
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			assert.Equal(t, test.expVal, ValidOID(test.oid))
+		})
+	}
+}

+ 29 - 0
internal/lfsutil/storage.go

@@ -0,0 +1,29 @@
+// Copyright 2020 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 lfsutil
+
+import (
+	"path/filepath"
+	"strings"
+)
+
+// Storage is the storage type of an LFS object.
+type Storage string
+
+const (
+	StorageLocal Storage = "local"
+)
+
+// StorageLocalPath returns computed file path for storing object on local file system.
+// It returns empty string if given "oid" isn't valid.
+func StorageLocalPath(root string, oid OID) string {
+	if !ValidOID(oid) {
+		return ""
+	}
+
+	// Valid OID is guaranteed to have second element as hash.
+	hash := strings.SplitN(string(oid), ":", 2)[1]
+	return filepath.Join(root, string(hash[0]), string(hash[1]), hash)
+}

+ 43 - 0
internal/lfsutil/storage_test.go

@@ -0,0 +1,43 @@
+// Copyright 2020 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 lfsutil
+
+import (
+	"runtime"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestStorageLocalPath(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("Skipping testing on Windows")
+		return
+	}
+
+	tests := []struct {
+		name    string
+		root    string
+		oid     OID
+		expPath string
+	}{
+		{
+			name: "invalid oid",
+			oid:  OID("bad_oid"),
+		},
+
+		{
+			name:    "valid oid",
+			root:    "/lfs-objects",
+			oid:     OID("sha256:ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f"),
+			expPath: "/lfs-objects/e/f/ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f",
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			assert.Equal(t, test.expPath, StorageLocalPath(test.root, test.oid))
+		})
+	}
+}

+ 19 - 19
internal/route/admin/auths.go

@@ -32,7 +32,7 @@ func Authentications(c *context.Context) {
 	c.PageIs("AdminAuthentications")
 
 	var err error
-	c.Data["Sources"], err = db.LoginSources()
+	c.Data["Sources"], err = db.ListLoginSources()
 	if err != nil {
 		c.Error(err, "list login sources")
 		return
@@ -49,11 +49,11 @@ type dropdownItem struct {
 
 var (
 	authSources = []dropdownItem{
-		{db.LoginNames[db.LOGIN_LDAP], db.LOGIN_LDAP},
-		{db.LoginNames[db.LOGIN_DLDAP], db.LOGIN_DLDAP},
-		{db.LoginNames[db.LOGIN_SMTP], db.LOGIN_SMTP},
-		{db.LoginNames[db.LOGIN_PAM], db.LOGIN_PAM},
-		{db.LoginNames[db.LOGIN_GITHUB], db.LOGIN_GITHUB},
+		{db.LoginNames[db.LoginLDAP], db.LoginLDAP},
+		{db.LoginNames[db.LoginDLDAP], db.LoginDLDAP},
+		{db.LoginNames[db.LoginSMTP], db.LoginSMTP},
+		{db.LoginNames[db.LoginPAM], db.LoginPAM},
+		{db.LoginNames[db.LoginGitHub], db.LoginGitHub},
 	}
 	securityProtocols = []dropdownItem{
 		{db.SecurityProtocolNames[ldap.SECURITY_PROTOCOL_UNENCRYPTED], ldap.SECURITY_PROTOCOL_UNENCRYPTED},
@@ -67,8 +67,8 @@ func NewAuthSource(c *context.Context) {
 	c.PageIs("Admin")
 	c.PageIs("AdminAuthentications")
 
-	c.Data["type"] = db.LOGIN_LDAP
-	c.Data["CurrentTypeName"] = db.LoginNames[db.LOGIN_LDAP]
+	c.Data["type"] = db.LoginLDAP
+	c.Data["CurrentTypeName"] = db.LoginNames[db.LoginLDAP]
 	c.Data["CurrentSecurityProtocol"] = db.SecurityProtocolNames[ldap.SECURITY_PROTOCOL_UNENCRYPTED]
 	c.Data["smtp_auth"] = "PLAIN"
 	c.Data["is_active"] = true
@@ -131,17 +131,17 @@ func NewAuthSourcePost(c *context.Context, f form.Authentication) {
 	hasTLS := false
 	var config core.Conversion
 	switch db.LoginType(f.Type) {
-	case db.LOGIN_LDAP, db.LOGIN_DLDAP:
+	case db.LoginLDAP, db.LoginDLDAP:
 		config = parseLDAPConfig(f)
 		hasTLS = ldap.SecurityProtocol(f.SecurityProtocol) > ldap.SECURITY_PROTOCOL_UNENCRYPTED
-	case db.LOGIN_SMTP:
+	case db.LoginSMTP:
 		config = parseSMTPConfig(f)
 		hasTLS = true
-	case db.LOGIN_PAM:
+	case db.LoginPAM:
 		config = &db.PAMConfig{
 			ServiceName: f.PAMServiceName,
 		}
-	case db.LOGIN_GITHUB:
+	case db.LoginGitHub:
 		config = &db.GitHubConfig{
 			APIEndpoint: strings.TrimSuffix(f.GitHubAPIEndpoint, "/") + "/",
 		}
@@ -186,7 +186,7 @@ func EditAuthSource(c *context.Context) {
 	c.Data["SecurityProtocols"] = securityProtocols
 	c.Data["SMTPAuths"] = db.SMTPAuths
 
-	source, err := db.GetLoginSourceByID(c.ParamsInt64(":authid"))
+	source, err := db.LoginSources.GetByID(c.ParamsInt64(":authid"))
 	if err != nil {
 		c.Error(err, "get login source by ID")
 		return
@@ -204,7 +204,7 @@ func EditAuthSourcePost(c *context.Context, f form.Authentication) {
 
 	c.Data["SMTPAuths"] = db.SMTPAuths
 
-	source, err := db.GetLoginSourceByID(c.ParamsInt64(":authid"))
+	source, err := db.LoginSources.GetByID(c.ParamsInt64(":authid"))
 	if err != nil {
 		c.Error(err, "get login source by ID")
 		return
@@ -219,15 +219,15 @@ func EditAuthSourcePost(c *context.Context, f form.Authentication) {
 
 	var config core.Conversion
 	switch db.LoginType(f.Type) {
-	case db.LOGIN_LDAP, db.LOGIN_DLDAP:
+	case db.LoginLDAP, db.LoginDLDAP:
 		config = parseLDAPConfig(f)
-	case db.LOGIN_SMTP:
+	case db.LoginSMTP:
 		config = parseSMTPConfig(f)
-	case db.LOGIN_PAM:
+	case db.LoginPAM:
 		config = &db.PAMConfig{
 			ServiceName: f.PAMServiceName,
 		}
-	case db.LOGIN_GITHUB:
+	case db.LoginGitHub:
 		config = &db.GitHubConfig{
 			APIEndpoint: strings.TrimSuffix(f.GitHubAPIEndpoint, "/") + "/",
 		}
@@ -252,7 +252,7 @@ func EditAuthSourcePost(c *context.Context, f form.Authentication) {
 }
 
 func DeleteAuthSource(c *context.Context) {
-	source, err := db.GetLoginSourceByID(c.ParamsInt64(":authid"))
+	source, err := db.LoginSources.GetByID(c.ParamsInt64(":authid"))
 	if err != nil {
 		c.Error(err, "get login source by ID")
 		return

+ 6 - 6
internal/route/admin/users.go

@@ -32,7 +32,7 @@ func Users(c *context.Context) {
 	route.RenderUserSearch(c, &route.UserSearchOptions{
 		Type:     db.USER_TYPE_INDIVIDUAL,
 		Counter:  db.CountUsers,
-		Ranger:   db.Users,
+		Ranger:   db.ListUsers,
 		PageSize: conf.UI.Admin.UserPagingNum,
 		OrderBy:  "id ASC",
 		TplName:  USERS,
@@ -46,7 +46,7 @@ func NewUser(c *context.Context) {
 
 	c.Data["login_type"] = "0-0"
 
-	sources, err := db.LoginSources()
+	sources, err := db.ListLoginSources()
 	if err != nil {
 		c.Error(err, "list login sources")
 		return
@@ -62,7 +62,7 @@ func NewUserPost(c *context.Context, f form.AdminCrateUser) {
 	c.Data["PageIsAdmin"] = true
 	c.Data["PageIsAdminUsers"] = true
 
-	sources, err := db.LoginSources()
+	sources, err := db.ListLoginSources()
 	if err != nil {
 		c.Error(err, "list login sources")
 		return
@@ -81,7 +81,7 @@ func NewUserPost(c *context.Context, f form.AdminCrateUser) {
 		Email:     f.Email,
 		Passwd:    f.Password,
 		IsActive:  true,
-		LoginType: db.LOGIN_PLAIN,
+		LoginType: db.LoginPlain,
 	}
 
 	if len(f.LoginType) > 0 {
@@ -132,7 +132,7 @@ func prepareUserInfo(c *context.Context) *db.User {
 	c.Data["User"] = u
 
 	if u.LoginSource > 0 {
-		c.Data["LoginSource"], err = db.GetLoginSourceByID(u.LoginSource)
+		c.Data["LoginSource"], err = db.LoginSources.GetByID(u.LoginSource)
 		if err != nil {
 			c.Error(err, "get login source by ID")
 			return nil
@@ -141,7 +141,7 @@ func prepareUserInfo(c *context.Context) *db.User {
 		c.Data["LoginSource"] = &db.LoginSource{}
 	}
 
-	sources, err := db.LoginSources()
+	sources, err := db.ListLoginSources()
 	if err != nil {
 		c.Error(err, "list login sources")
 		return nil

+ 2 - 2
internal/route/api/v1/admin/user.go

@@ -23,7 +23,7 @@ func parseLoginSource(c *context.APIContext, u *db.User, sourceID int64, loginNa
 		return
 	}
 
-	source, err := db.GetLoginSourceByID(sourceID)
+	source, err := db.LoginSources.GetByID(sourceID)
 	if err != nil {
 		if errors.IsLoginSourceNotExist(err) {
 			c.ErrorStatus(http.StatusUnprocessableEntity, err)
@@ -45,7 +45,7 @@ func CreateUser(c *context.APIContext, form api.CreateUserOption) {
 		Email:     form.Email,
 		Passwd:    form.Password,
 		IsActive:  true,
-		LoginType: db.LOGIN_PLAIN,
+		LoginType: db.LoginPlain,
 	}
 
 	parseLoginSource(c, u, form.SourceID, form.LoginName)

+ 1 - 1
internal/route/api/v1/api.go

@@ -55,7 +55,7 @@ func repoAssignment() macaron.Handler {
 		}
 
 		if c.IsTokenAuth && c.User.IsAdmin {
-			c.Repo.AccessMode = db.ACCESS_MODE_OWNER
+			c.Repo.AccessMode = db.AccessModeOwner
 		} else {
 			mode, err := db.UserAccessMode(c.UserID(), r)
 			if err != nil {

+ 2 - 2
internal/route/api/v1/repo/repo.go

@@ -131,8 +131,8 @@ func listUserRepositories(c *context.APIContext, username string) {
 	i := numOwnRepos
 	for repo, access := range accessibleRepos {
 		repos[i] = repo.APIFormat(&api.Permission{
-			Admin: access >= db.ACCESS_MODE_ADMIN,
-			Push:  access >= db.ACCESS_MODE_WRITE,
+			Admin: access >= db.AccessModeAdmin,
+			Push:  access >= db.AccessModeWrite,
 			Pull:  true,
 		})
 		i++

+ 2 - 2
internal/route/api/v1/user/app.go

@@ -30,8 +30,8 @@ func ListAccessTokens(c *context.APIContext) {
 
 func CreateAccessToken(c *context.APIContext, form api.CreateAccessTokenOption) {
 	t := &db.AccessToken{
-		UID:  c.User.ID,
-		Name: form.Name,
+		UserID: c.User.ID,
+		Name:   form.Name,
 	}
 	if err := db.NewAccessToken(t); err != nil {
 		if errors.IsAccessTokenNameAlreadyExist(err) {

+ 1 - 1
internal/route/home.go

@@ -135,7 +135,7 @@ func ExploreUsers(c *context.Context) {
 	RenderUserSearch(c, &UserSearchOptions{
 		Type:     db.USER_TYPE_INDIVIDUAL,
 		Counter:  db.CountUsers,
-		Ranger:   db.Users,
+		Ranger:   db.ListUsers,
 		PageSize: conf.UI.ExplorePagingNum,
 		OrderBy:  "updated_unix DESC",
 		TplName:  EXPLORE_USERS,

+ 2 - 11
internal/route/install.go

@@ -86,9 +86,6 @@ func GlobalInit(customConf string) error {
 		db.InitDeliverHooks()
 		db.InitTestPullRequests()
 	}
-	if db.EnableSQLite3 {
-		log.Info("SQLite3 is supported")
-	}
 	if conf.HasMinWinSvc {
 		log.Info("Builtin Windows Service is supported")
 	}
@@ -125,11 +122,7 @@ func InstallInit(c *context.Context) {
 	c.Title("install.install")
 	c.PageIs("Install")
 
-	dbOpts := []string{"MySQL", "PostgreSQL", "MSSQL"}
-	if db.EnableSQLite3 {
-		dbOpts = append(dbOpts, "SQLite3")
-	}
-	c.Data["DbOptions"] = dbOpts
+	c.Data["DbOptions"] = []string{"MySQL", "PostgreSQL", "MSSQL", "SQLite3"}
 }
 
 func Install(c *context.Context) {
@@ -148,9 +141,7 @@ func Install(c *context.Context) {
 	case "mssql":
 		c.Data["CurDbOption"] = "MSSQL"
 	case "sqlite3":
-		if db.EnableSQLite3 {
-			c.Data["CurDbOption"] = "SQLite3"
-		}
+		c.Data["CurDbOption"] = "SQLite3"
 	}
 
 	// Application general settings

+ 122 - 0
internal/route/lfs/basic.go

@@ -0,0 +1,122 @@
+// Copyright 2020 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 lfs
+
+import (
+	"encoding/json"
+	"io"
+	"net/http"
+	"os"
+	"strconv"
+
+	log "unknwon.dev/clog/v2"
+
+	"gogs.io/gogs/internal/conf"
+	"gogs.io/gogs/internal/context"
+	"gogs.io/gogs/internal/db"
+	"gogs.io/gogs/internal/lfsutil"
+	"gogs.io/gogs/internal/strutil"
+)
+
+const transferBasic = "basic"
+const (
+	basicOperationUpload   = "upload"
+	basicOperationDownload = "download"
+)
+
+// GET /{owner}/{repo}.git/info/lfs/object/basic/{oid}
+func serveBasicDownload(c *context.Context, repo *db.Repository, oid lfsutil.OID) {
+	object, err := db.LFS.GetObjectByOID(repo.ID, oid)
+	if err != nil {
+		if db.IsErrLFSObjectNotExist(err) {
+			responseJSON(c.Resp, http.StatusNotFound, responseError{
+				Message: "Object does not exist",
+			})
+		} else {
+			internalServerError(c.Resp)
+			log.Error("Failed to get object [repo_id: %d, oid: %s]: %v", repo.ID, oid, err)
+		}
+		return
+	}
+
+	fpath := lfsutil.StorageLocalPath(conf.LFS.ObjectsPath, object.OID)
+	r, err := os.Open(fpath)
+	if err != nil {
+		internalServerError(c.Resp)
+		log.Error("Failed to open object file [path: %s]: %v", fpath, err)
+		return
+	}
+	defer r.Close()
+
+	c.Header().Set("Content-Type", "application/octet-stream")
+	c.Header().Set("Content-Length", strconv.FormatInt(object.Size, 10))
+
+	_, err = io.Copy(c.Resp, r)
+	if err != nil {
+		c.Status(http.StatusInternalServerError)
+		log.Error("Failed to copy object file: %v", err)
+		return
+	}
+	c.Status(http.StatusOK)
+}
+
+// PUT /{owner}/{repo}.git/info/lfs/object/basic/{oid}
+func serveBasicUpload(c *context.Context, repo *db.Repository, oid lfsutil.OID) {
+	err := db.LFS.CreateObject(repo.ID, oid, c.Req.Request.Body, lfsutil.StorageLocal)
+	if err != nil {
+		internalServerError(c.Resp)
+		log.Error("Failed to create object [repo_id: %d, oid: %s]: %v", repo.ID, oid, err)
+		return
+	}
+	c.Status(http.StatusOK)
+
+	log.Trace("[LFS] Object created %q", oid)
+}
+
+// POST /{owner}/{repo}.git/info/lfs/object/basic/verify
+func serveBasicVerify(c *context.Context, repo *db.Repository) {
+	var request basicVerifyRequest
+	defer c.Req.Request.Body.Close()
+	err := json.NewDecoder(c.Req.Request.Body).Decode(&request)
+	if err != nil {
+		responseJSON(c.Resp, http.StatusBadRequest, responseError{
+			Message: strutil.ToUpperFirst(err.Error()),
+		})
+		return
+	}
+
+	if !lfsutil.ValidOID(request.Oid) {
+		responseJSON(c.Resp, http.StatusBadRequest, responseError{
+			Message: "Invalid oid",
+		})
+		return
+	}
+
+	object, err := db.LFS.GetObjectByOID(repo.ID, lfsutil.OID(request.Oid))
+	if err != nil {
+		if db.IsErrLFSObjectNotExist(err) {
+			responseJSON(c.Resp, http.StatusNotFound, responseError{
+				Message: "Object does not exist",
+			})
+		} else {
+			internalServerError(c.Resp)
+			log.Error("Failed to get object [repo_id: %d, oid: %s]: %v", repo.ID, request.Oid, err)
+		}
+		return
+	}
+
+	if object.Size != request.Size {
+		responseJSON(c.Resp, http.StatusNotFound, responseError{
+			Message: "Object size mismatch",
+		})
+		return
+	}
+	c.Status(http.StatusOK)
+}
+
+type basicVerifyRequest struct {
+	Oid  lfsutil.OID `json:"oid"`
+	Size int64       `json:"size"`
+}

+ 182 - 0
internal/route/lfs/batch.go

@@ -0,0 +1,182 @@
+// Copyright 2020 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 lfs
+
+import (
+	"fmt"
+	"net/http"
+
+	jsoniter "github.com/json-iterator/go"
+	log "unknwon.dev/clog/v2"
+
+	"gogs.io/gogs/internal/conf"
+	"gogs.io/gogs/internal/context"
+	"gogs.io/gogs/internal/db"
+	"gogs.io/gogs/internal/lfsutil"
+	"gogs.io/gogs/internal/strutil"
+)
+
+// POST /{owner}/{repo}.git/info/lfs/object/batch
+func serveBatch(c *context.Context, owner *db.User, repo *db.Repository) {
+	var request batchRequest
+	defer c.Req.Request.Body.Close()
+	err := jsoniter.NewDecoder(c.Req.Request.Body).Decode(&request)
+	if err != nil {
+		responseJSON(c.Resp, http.StatusBadRequest, responseError{
+			Message: strutil.ToUpperFirst(err.Error()),
+		})
+		return
+	}
+
+	// NOTE: We only support basic transfer as of now.
+	transfer := transferBasic
+	// Example: https://try.gogs.io/gogs/gogs.git/info/lfs/object/basic
+	baseHref := fmt.Sprintf("%s%s/%s.git/info/lfs/objects/basic", conf.Server.ExternalURL, owner.Name, repo.Name)
+
+	objects := make([]batchObject, 0, len(request.Objects))
+	switch request.Operation {
+	case basicOperationUpload:
+		for _, obj := range request.Objects {
+			var actions batchActions
+			if lfsutil.ValidOID(obj.Oid) {
+				actions = batchActions{
+					Upload: &batchAction{
+						Href: fmt.Sprintf("%s/%s", baseHref, obj.Oid),
+					},
+					Verify: &batchAction{
+						Href: fmt.Sprintf("%s/verify", baseHref),
+					},
+				}
+			} else {
+				actions = batchActions{
+					Error: &batchError{
+						Code:    http.StatusUnprocessableEntity,
+						Message: "Object has invalid oid",
+					},
+				}
+			}
+
+			objects = append(objects, batchObject{
+				Oid:     obj.Oid,
+				Size:    obj.Size,
+				Actions: actions,
+			})
+		}
+
+	case basicOperationDownload:
+		oids := make([]lfsutil.OID, 0, len(request.Objects))
+		for _, obj := range request.Objects {
+			oids = append(oids, obj.Oid)
+		}
+		stored, err := db.LFS.GetObjectsByOIDs(repo.ID, oids...)
+		if err != nil {
+			internalServerError(c.Resp)
+			log.Error("Failed to get objects [repo_id: %d, oids: %v]: %v", repo.ID, oids, err)
+			return
+		}
+		storedSet := make(map[lfsutil.OID]*db.LFSObject, len(stored))
+		for _, obj := range stored {
+			storedSet[obj.OID] = obj
+		}
+
+		for _, obj := range request.Objects {
+			var actions batchActions
+			if stored := storedSet[obj.Oid]; stored != nil {
+				if stored.Size != obj.Size {
+					actions.Error = &batchError{
+						Code:    http.StatusUnprocessableEntity,
+						Message: "Object size mismatch",
+					}
+				} else {
+					actions.Download = &batchAction{
+						Href: fmt.Sprintf("%s/%s", baseHref, obj.Oid),
+					}
+				}
+			} else {
+				actions.Error = &batchError{
+					Code:    http.StatusNotFound,
+					Message: "Object does not exist",
+				}
+			}
+
+			objects = append(objects, batchObject{
+				Oid:     obj.Oid,
+				Size:    obj.Size,
+				Actions: actions,
+			})
+		}
+
+	default:
+		responseJSON(c.Resp, http.StatusBadRequest, responseError{
+			Message: "Operation not recognized",
+		})
+		return
+	}
+
+	responseJSON(c.Resp, http.StatusOK, batchResponse{
+		Transfer: transfer,
+		Objects:  objects,
+	})
+}
+
+// batchRequest defines the request payload for the batch endpoint.
+type batchRequest struct {
+	Operation string `json:"operation"`
+	Objects   []struct {
+		Oid  lfsutil.OID `json:"oid"`
+		Size int64       `json:"size"`
+	} `json:"objects"`
+}
+
+type batchError struct {
+	Code    int    `json:"code"`
+	Message string `json:"message"`
+}
+
+type batchAction struct {
+	Href string `json:"href"`
+}
+
+type batchActions struct {
+	Download *batchAction `json:"download,omitempty"`
+	Upload   *batchAction `json:"upload,omitempty"`
+	Verify   *batchAction `json:"verify,omitempty"`
+	Error    *batchError  `json:"error,omitempty"`
+}
+
+type batchObject struct {
+	Oid     lfsutil.OID  `json:"oid"`
+	Size    int64        `json:"size"`
+	Actions batchActions `json:"actions"`
+}
+
+// batchResponse defines the response payload for the batch endpoint.
+type batchResponse struct {
+	Transfer string        `json:"transfer"`
+	Objects  []batchObject `json:"objects"`
+}
+
+type responseError struct {
+	Message string `json:"message"`
+}
+
+const contentType = "application/vnd.git-lfs+json"
+
+func responseJSON(w http.ResponseWriter, status int, v interface{}) {
+	w.Header().Set("Content-Type", contentType)
+
+	err := jsoniter.NewEncoder(w).Encode(v)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	w.WriteHeader(status)
+}
+
+func internalServerError(w http.ResponseWriter) {
+	responseJSON(w, http.StatusInternalServerError, responseError{
+		Message: "Internal server error",
+	})
+}

+ 159 - 0
internal/route/lfs/route.go

@@ -0,0 +1,159 @@
+// Copyright 2020 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 lfs
+
+import (
+	"net/http"
+	"strings"
+	"time"
+
+	"gopkg.in/macaron.v1"
+	log "unknwon.dev/clog/v2"
+
+	"gogs.io/gogs/internal/authutil"
+	"gogs.io/gogs/internal/context"
+	"gogs.io/gogs/internal/db"
+	"gogs.io/gogs/internal/lfsutil"
+)
+
+// RegisterRoutes registers LFS routes using given router, and inherits all groups and middleware.
+func RegisterRoutes(r *macaron.Router) {
+	verifyAccept := verifyHeader("Accept", contentType, http.StatusNotAcceptable)
+	verifyContentTypeJSON := verifyHeader("Content-Type", contentType, http.StatusBadRequest)
+	verifyContentTypeStream := verifyHeader("Content-Type", "application/octet-stream", http.StatusBadRequest)
+
+	r.Group("", func() {
+		r.Post("/objects/batch", authorize(db.AccessModeRead), verifyAccept, verifyContentTypeJSON, serveBatch)
+		r.Group("/objects/basic", func() {
+			r.Combo("/:oid", verifyOID()).
+				Get(authorize(db.AccessModeRead), serveBasicDownload).
+				Put(authorize(db.AccessModeWrite), verifyContentTypeStream, serveBasicUpload)
+			r.Post("/verify", authorize(db.AccessModeWrite), verifyAccept, verifyContentTypeJSON, serveBasicVerify)
+		})
+	}, authenticate())
+}
+
+// authenticate tries to authenticate user via HTTP Basic Auth.
+func authenticate() macaron.Handler {
+	askCredentials := func(w http.ResponseWriter) {
+		w.Header().Set("LFS-Authenticate", `Basic realm="Git LFS"`)
+		responseJSON(w, http.StatusUnauthorized, responseError{
+			Message: "Credentials needed",
+		})
+	}
+
+	return func(c *context.Context) {
+		username, password := authutil.DecodeBasic(c.Req.Header)
+		if username == "" {
+			askCredentials(c.Resp)
+			return
+		}
+
+		user, err := db.Users.Authenticate(username, password, -1)
+		if err != nil && !db.IsErrUserNotExist(err) {
+			c.Status(http.StatusInternalServerError)
+			log.Error("Failed to authenticate user [name: %s]: %v", username, err)
+			return
+		}
+
+		if err == nil && user.IsEnabledTwoFactor() {
+			c.PlainText(http.StatusBadRequest, `Users with 2FA enabled are not allowed to authenticate via username and password.`)
+			return
+		}
+
+		// If username and password authentication failed, try again using username as an access token.
+		if db.IsErrUserNotExist(err) {
+			token, err := db.AccessTokens.GetBySHA(username)
+			if err != nil {
+				if db.IsErrAccessTokenNotExist(err) {
+					askCredentials(c.Resp)
+				} else {
+					c.Status(http.StatusInternalServerError)
+					log.Error("Failed to get access token [sha: %s]: %v", username, err)
+				}
+				return
+			}
+			token.Updated = time.Now()
+			if err = db.AccessTokens.Save(token); err != nil {
+				log.Error("Failed to update access token: %v", err)
+			}
+
+			user, err = db.Users.GetByID(token.UserID)
+			if err != nil {
+				// Once we found the token, we're supposed to find its related user,
+				// thus any error is unexpected.
+				c.Status(http.StatusInternalServerError)
+				log.Error("Failed to get user: %v", err)
+				return
+			}
+		}
+
+		log.Trace("[LFS] Authenticated user: %s", user.Name)
+
+		c.Map(user)
+	}
+}
+
+// authorize tries to authorize the user to the context repository with given access mode.
+func authorize(mode db.AccessMode) macaron.Handler {
+	return func(c *context.Context, user *db.User) {
+		username := c.Params(":username")
+		reponame := strings.TrimSuffix(c.Params(":reponame"), ".git")
+
+		owner, err := db.Users.GetByUsername(username)
+		if err != nil {
+			if db.IsErrUserNotExist(err) {
+				c.Status(http.StatusNotFound)
+			} else {
+				c.Status(http.StatusInternalServerError)
+				log.Error("Failed to get user [name: %s]: %v", username, err)
+			}
+			return
+		}
+
+		repo, err := db.Repos.GetByName(owner.ID, reponame)
+		if err != nil {
+			if db.IsErrRepoNotExist(err) {
+				c.Status(http.StatusNotFound)
+			} else {
+				c.Status(http.StatusInternalServerError)
+				log.Error("Failed to get repository [owner_id: %d, name: %s]: %v", owner.ID, reponame, err)
+			}
+			return
+		}
+
+		if !db.Perms.Authorize(user.ID, repo, mode) {
+			c.Status(http.StatusNotFound)
+			return
+		}
+
+		c.Map(owner)
+		c.Map(repo)
+	}
+}
+
+// verifyHeader checks if the HTTP header value is matching.
+// When not, response given "failCode" as status code.
+func verifyHeader(key, value string, failCode int) macaron.Handler {
+	return func(c *context.Context) {
+		if c.Req.Header.Get(key) != value {
+			c.Status(failCode)
+			return
+		}
+	}
+}
+
+// verifyOID checks if the ":oid" URL parameter is valid.
+func verifyOID() macaron.Handler {
+	return func(c *context.Context) {
+		oid := lfsutil.OID(c.Params(":oid"))
+		if !lfsutil.ValidOID(oid) {
+			c.PlainText(http.StatusBadRequest, "Invalid oid")
+			return
+		}
+
+		c.Map(oid)
+	}
+}

+ 1 - 1
internal/route/org/setting.go

@@ -110,7 +110,7 @@ func SettingsDelete(c *context.Context) {
 
 	org := c.Org.Organization
 	if c.Req.Method == "POST" {
-		if _, err := db.UserLogin(c.User.Name, c.Query("password"), c.User.LoginSource); err != nil {
+		if _, err := db.Users.Authenticate(c.User.Name, c.Query("password"), c.User.LoginSource); err != nil {
 			if db.IsErrUserNotExist(err) {
 				c.RenderWithErr(c.Tr("form.enterred_invalid_password"), SETTINGS_DELETE, nil)
 			} else {

+ 3 - 3
internal/route/org/teams.go

@@ -228,11 +228,11 @@ func EditTeamPost(c *context.Context, f form.CreateTeam) {
 		var auth db.AccessMode
 		switch f.Permission {
 		case "read":
-			auth = db.ACCESS_MODE_READ
+			auth = db.AccessModeRead
 		case "write":
-			auth = db.ACCESS_MODE_WRITE
+			auth = db.AccessModeWrite
 		case "admin":
-			auth = db.ACCESS_MODE_ADMIN
+			auth = db.AccessModeAdmin
 		default:
 			c.Status(http.StatusUnauthorized)
 			return

+ 11 - 8
internal/route/repo/http.go

@@ -12,6 +12,7 @@ import (
 	"os"
 	"os/exec"
 	"path"
+	"path/filepath"
 	"strconv"
 	"strings"
 	"time"
@@ -111,7 +112,7 @@ func HTTPContexter() macaron.Handler {
 			return
 		}
 
-		authUser, err := db.UserLogin(authUsername, authPassword, -1)
+		authUser, err := db.Users.Authenticate(authUsername, authPassword, -1)
 		if err != nil && !db.IsErrUserNotExist(err) {
 			c.Error(err, "authenticate user")
 			return
@@ -119,9 +120,9 @@ func HTTPContexter() macaron.Handler {
 
 		// If username and password combination failed, try again using username as a token.
 		if authUser == nil {
-			token, err := db.GetAccessTokenBySHA(authUsername)
+			token, err := db.AccessTokens.GetBySHA(authUsername)
 			if err != nil {
-				if db.IsErrAccessTokenEmpty(err) || db.IsErrAccessTokenNotExist(err) {
+				if db.IsErrAccessTokenNotExist(err) {
 					askCredentials(c, http.StatusUnauthorized, "")
 				} else {
 					c.Error(err, "get access token by SHA")
@@ -129,9 +130,11 @@ func HTTPContexter() macaron.Handler {
 				return
 			}
 			token.Updated = time.Now()
-			// TODO: verify or update token.Updated in database
+			if err = db.AccessTokens.Save(token); err != nil {
+				log.Error("Failed to update access token: %v", err)
+			}
 
-			authUser, err = db.GetUserByID(token.UID)
+			authUser, err = db.GetUserByID(token.UserID)
 			if err != nil {
 				// Once we found token, we're supposed to find its related user,
 				// thus any error is unexpected.
@@ -146,9 +149,9 @@ Please create and use personal access token on user settings page`)
 
 		log.Trace("HTTPGit - Authenticated user: %s", authUser.Name)
 
-		mode := db.ACCESS_MODE_WRITE
+		mode := db.AccessModeWrite
 		if isPull {
-			mode = db.ACCESS_MODE_READ
+			mode = db.AccessModeRead
 		}
 		has, err := db.HasAccess(authUser.ID, repo, mode)
 		if err != nil {
@@ -367,7 +370,7 @@ func getGitRepoPath(dir string) (string, error) {
 		dir += ".git"
 	}
 
-	filename := path.Join(conf.Repository.Root, dir)
+	filename := filepath.Join(conf.Repository.Root, dir)
 	if _, err := os.Stat(filename); os.IsNotExist(err) {
 		return "", err
 	}

+ 1 - 1
internal/route/repo/setting.go

@@ -513,7 +513,7 @@ func SettingsProtectedBranch(c *context.Context) {
 		c.Data["Users"] = users
 		c.Data["whitelist_users"] = protectBranch.WhitelistUserIDs
 
-		teams, err := c.Repo.Owner.TeamsHaveAccessToRepo(c.Repo.Repository.ID, db.ACCESS_MODE_WRITE)
+		teams, err := c.Repo.Owner.TeamsHaveAccessToRepo(c.Repo.Repository.ID, db.AccessModeWrite)
 		if err != nil {
 			c.Error(err, "get teams have access to the repository")
 			return

+ 5 - 5
internal/route/user/auth.go

@@ -9,12 +9,12 @@ import (
 	"net/url"
 
 	"github.com/go-macaron/captcha"
+	"github.com/pkg/errors"
 	log "unknwon.dev/clog/v2"
 
 	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/context"
 	"gogs.io/gogs/internal/db"
-	"gogs.io/gogs/internal/db/errors"
 	"gogs.io/gogs/internal/email"
 	"gogs.io/gogs/internal/form"
 	"gogs.io/gogs/internal/tool"
@@ -160,13 +160,13 @@ func LoginPost(c *context.Context, f form.SignIn) {
 		return
 	}
 
-	u, err := db.UserLogin(f.UserName, f.Password, f.LoginSource)
+	u, err := db.Users.Authenticate(f.UserName, f.Password, f.LoginSource)
 	if err != nil {
-		switch err.(type) {
+		switch errors.Cause(err).(type) {
 		case db.ErrUserNotExist:
 			c.FormErr("UserName", "Password")
 			c.RenderWithErr(c.Tr("form.username_password_incorrect"), LOGIN, &f)
-		case errors.LoginSourceMismatch:
+		case db.ErrLoginSourceMismatch:
 			c.FormErr("LoginSource")
 			c.RenderWithErr(c.Tr("form.auth_source_mismatch"), LOGIN, &f)
 
@@ -263,7 +263,7 @@ func LoginTwoFactorRecoveryCodePost(c *context.Context) {
 	}
 
 	if err := db.UseRecoveryCode(userID, c.Query("recovery_code")); err != nil {
-		if errors.IsTwoFactorRecoveryCodeNotFound(err) {
+		if db.IsTwoFactorRecoveryCodeNotFound(err) {
 			c.Flash.Error(c.Tr("auth.login_two_factor_invalid_recovery_code"))
 			c.RedirectSubpath("/user/login/two_factor_recovery_code")
 		} else {

+ 3 - 3
internal/route/user/setting.go

@@ -608,8 +608,8 @@ func SettingsApplicationsPost(c *context.Context, f form.NewAccessToken) {
 	}
 
 	t := &db.AccessToken{
-		UID:  c.User.ID,
-		Name: f.Name,
+		UserID: c.User.ID,
+		Name:   f.Name,
 	}
 	if err := db.NewAccessToken(t); err != nil {
 		if errors.IsAccessTokenNameAlreadyExist(err) {
@@ -643,7 +643,7 @@ func SettingsDelete(c *context.Context) {
 	c.PageIs("SettingsDelete")
 
 	if c.Req.Method == "POST" {
-		if _, err := db.UserLogin(c.User.Name, c.Query("password"), c.User.LoginSource); err != nil {
+		if _, err := db.Users.Authenticate(c.User.Name, c.Query("password"), c.User.LoginSource); err != nil {
 			if db.IsErrUserNotExist(err) {
 				c.RenderWithErr(c.Tr("form.enterred_invalid_password"), SETTINGS_DELETE, nil)
 			} else {

+ 17 - 0
internal/strutil/strutil.go

@@ -0,0 +1,17 @@
+// Copyright 2020 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 strutil
+
+import (
+	"unicode"
+)
+
+// ToUpperFirst returns s with only the first Unicode letter mapped to its upper case.
+func ToUpperFirst(s string) string {
+	for i, v := range s {
+		return string(unicode.ToUpper(v)) + s[i+1:]
+	}
+	return ""
+}

+ 43 - 0
internal/strutil/strutil_test.go

@@ -0,0 +1,43 @@
+// Copyright 2020 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 strutil
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestToUpperFirst(t *testing.T) {
+	tests := []struct {
+		name   string
+		s      string
+		expStr string
+	}{
+		{
+			name: "empty string",
+		},
+		{
+			name:   "first letter is a digit",
+			s:      "123 let's go",
+			expStr: "123 let's go",
+		},
+		{
+			name:   "lower to upper",
+			s:      "this is a sentence",
+			expStr: "This is a sentence",
+		},
+		{
+			name:   "already in upper case",
+			s:      "Let's go",
+			expStr: "Let's go",
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			assert.Equal(t, test.expStr, ToUpperFirst(test.s))
+		})
+	}
+}

+ 1 - 0
internal/testutil/golden.go

@@ -29,6 +29,7 @@ func Update(name string) bool {
 // the golden file on-demand. It does nothing when the runtime is "windows".
 func AssertGolden(t testing.TB, path string, update bool, got interface{}) {
 	if runtime.GOOS == "windows" {
+		t.Skip("Skipping testing on Windows")
 		return
 	}
 

Some files were not shown because too many files changed in this diff