Browse Source

Merge branch 'main' of https://github.com/gogs/gogs into main

 Conflicts:
	internal/assets/conf/conf_gen.go
	internal/assets/public/public_gen.go
	internal/assets/templates/templates_gen.go
Lukas Fülling 8 months ago
parent
commit
40b4fd8c9e
47 changed files with 939 additions and 713 deletions
  1. 6 1
      .github/workflows/go.yml
  2. 18 4
      CHANGELOG.md
  3. 0 2
      README.md
  4. 1 1
      README_ZH.md
  5. 1 0
      conf/app.ini
  6. 9 9
      conf/locale/locale_de-DE.ini
  7. 17 17
      conf/locale/locale_uk-UA.ini
  8. 17 4
      docs/dev/database_schema.md
  9. 2 1
      docs/dev/import_locale.md
  10. 9 5
      docs/dev/release/issue_template_patch_release.md
  11. 4 3
      go.mod
  12. 18 13
      go.sum
  13. 5 6
      internal/assets/conf/conf_gen.go
  14. 113 114
      internal/assets/public/public_gen.go
  15. 113 114
      internal/assets/templates/templates_gen.go
  16. 3 3
      internal/cmd/hook.go
  17. 10 8
      internal/cmd/serv.go
  18. 1 0
      internal/conf/static.go
  19. 14 12
      internal/context/repo.go
  20. 0 240
      internal/db/access.go
  21. 6 6
      internal/db/action.go
  22. 41 0
      internal/db/action_test.go
  23. 15 2
      internal/db/backup_test.go
  24. 2 7
      internal/db/db.go
  25. 0 20
      internal/db/errors/issue.go
  26. 7 13
      internal/db/issue.go
  27. 6 6
      internal/db/mocks.go
  28. 27 8
      internal/db/models.go
  29. 32 0
      internal/db/org_team.go
  30. 63 10
      internal/db/perms.go
  31. 32 18
      internal/db/perms_test.go
  32. 124 12
      internal/db/repo.go
  33. 6 4
      internal/db/repo_branch.go
  34. 9 4
      internal/db/repo_test.go
  35. 6 4
      internal/db/ssh_key.go
  36. 2 0
      internal/db/testdata/backup/Access.golden.json
  37. 58 16
      internal/db/user.go
  38. 3 2
      internal/markup/markup.go
  39. 38 0
      internal/markup/markup_test.go
  40. 9 9
      internal/route/api/v1/api.go
  41. 7 1
      internal/route/lfs/batch.go
  42. 55 4
      internal/route/lfs/batch_test.go
  43. 16 4
      internal/route/lfs/route.go
  44. 2 2
      internal/route/lfs/route_test.go
  45. 3 1
      internal/route/repo/commit.go
  46. 6 1
      internal/route/repo/http.go
  47. 3 2
      internal/route/repo/pull.go

+ 6 - 1
.github/workflows/go.yml

@@ -1,12 +1,17 @@
 name: Go
 on:
   push:
-    branches: [main]
+    branches:
+      - main
+      - 'release/**'
     paths:
       - '**.go'
+      - 'go.mod'
   pull_request:
     paths:
       - '**.go'
+      - 'go.mod'
+      - '.github/workflows/go.yml'
 env:
   GOPROXY: "https://proxy.golang.org"
 

+ 18 - 4
CHANGELOG.md

@@ -7,6 +7,7 @@ All notable changes to Gogs are documented in this file.
 ### Added
 
 - An unlisted option is added when create or migrate a repository. Unlisted repositories are public but not being listed for users without direct access in the UI. [#5733](https://github.com/gogs/gogs/issues/5733)
+- Add new configuration option `[git.timeout] DIFF` for customizing operation timeout of `git diff`. [#6315](https://github.com/gogs/gogs/issues/6315)
 
 ### Changed
 
@@ -16,10 +17,6 @@ All notable changes to Gogs are documented in this file.
 
 ### Fixed
 
-- _Regression:_ Pages are correctly rendered when requesting `?go-get=1` for subdirectories. [#6314](https://github.com/gogs/gogs/issues/6314)
-- _Regression:_ Submodule with a relative path is linked correctly. [#6319](https://github.com/gogs/gogs/issues/6319)
-- Backup can be processed when `--target` is specified on Windows. [#6339](https://github.com/gogs/gogs/issues/6339)
-
 ### Removed
 
 - ⚠️ Migrations before 0.12 are removed, installations not on 0.12 should upgrade to it to run the migrations and then upgrade to 0.13.
@@ -40,6 +37,23 @@ All notable changes to Gogs are documented in this file.
 - Configuration option `[database] DB_TYPE` is no longer used.
 - Configuration option `[database] PASSWD` is no longer used.
 
+## 0.12.3
+
+### Fixed
+
+- _Regression:_ When running Gogs on Windows, push commits no longer fail on a daily basis with the error "pre-receive hook declined". [#6316](https://github.com/gogs/gogs/issues/6316)
+- Auto-linked commit SHAs now have correct links. [#6300](https://github.com/gogs/gogs/issues/6300)
+- Git LFS client (with version >= 2.5.0) wasn't able to upload files with known format (e.g. PNG, JPEG), and the server is expecting the HTTP Header `Content-Type` to be `application/octet-stream`. The server now tells the LFS client to always use `Content-Type: application/octet-stream` when upload files.
+
+## 0.12.2
+
+### Fixed
+
+- _Regression:_ Pages are correctly rendered when requesting `?go-get=1` for subdirectories. [#6314](https://github.com/gogs/gogs/issues/6314)
+- _Regression:_ Submodule with a relative path is linked correctly. [#6319](https://github.com/gogs/gogs/issues/6319)
+- Backup can be processed when `--target` is specified on Windows. [#6339](https://github.com/gogs/gogs/issues/6339)
+- Commit message contains keywords look like an issue reference no longer fails the push entirely. [#6289](https://github.com/gogs/gogs/issues/6289)
+
 ## 0.12.1
 
 ### Fixed

+ 0 - 2
README.md

@@ -88,8 +88,6 @@ There are 6 ways to install Gogs:
 - [Jenkins](https://plugins.jenkins.io/gogs-webhook/) (CI)
 - [Taiga](https://taiga.io/) (Project Management)
 - [Puppet](https://forge.puppet.com/Siteminds/gogs) (IT)
-- [Kanboard](https://github.com/kanboard/plugin-gogs-webhook) (Project Management)
-- [BearyChat](https://bearychat.com/) (Team Communication)
 - [GitPitch](https://gitpitch.com/) (Markdown Presentations)
 - [Synology](https://www.synology.com) (Docker)
 - [Syncloud](https://syncloud.org/) (App Store)

+ 1 - 1
README_ZH.md

@@ -13,7 +13,7 @@ Gogs(`/gɑgz/`)项目旨在打造一个以最简便的方式搭建简单、
 - 想要先睹为快?直接去[在线体验](https://try.gogs.io/gogs/gogs)吧!
 - 使用过程中遇到问题?尝试[故障排查](https://gogs.io/docs/intro/troubleshooting.html)或者前往[用户论坛](https://discuss.gogs.io/)获取帮助
 - 希望帮助多国语言的翻译吗?请查看[本地化文档](https://gogs.io/docs/features/i18n.html)
-- 准备搞点事情?请阅读[开发指南](docs/local_development.md)配置开发环境
+- 准备搞点事情?请阅读[开发指南](docs/dev/local_development.md)配置开发环境
 - 想调用 API 吗?请查看[文档](https://github.com/gogs/docs-api)吧
 
 ## 主要特性

+ 1 - 0
conf/app.ini

@@ -462,6 +462,7 @@ MIGRATE = 600
 MIRROR = 300
 CLONE = 300
 PULL = 300
+DIFF = 60
 GC = 60
 
 [mirror]

+ 9 - 9
conf/locale/locale_de-DE.ini

@@ -44,8 +44,8 @@ issues=Issues
 cancel=Abbrechen
 
 [status]
-page_not_found=Page Not Found
-internal_server_error=Internal Server Error
+page_not_found=Seite nicht gefunden
+internal_server_error=Interner Serverfehler
 
 [install]
 install=Installation
@@ -789,8 +789,8 @@ settings.remove_collaborator_success=Mitarbeiter wurde entfernt.
 settings.search_user_placeholder=Benutzer suchen...
 settings.org_not_allowed_to_be_collaborator=Eine Organisation kann nicht als Mitarbeiter hinzugefügt werden.
 settings.hooks_desc=Webhooks erlauben es Ihnen, externe Dienste zu informieren, wenn etwas Bestimmtes in Ihrem Repository passiert. Gogs sendet dann einen POST-Request an alle angegebenen URLs. Erfahren Sie mehr in unserem <a target="_blank" href="%s">Webhooks Guide</a>.
-settings.webhooks.add_new=Add a new webhook:
-settings.webhooks.choose_a_type=Choose a type...
+settings.webhooks.add_new=Einen neuen Webhook hinzufügen:
+settings.webhooks.choose_a_type=Typ auswählen...
 settings.add_webhook=Webhook hinzufügen
 settings.webhook_deletion=Webhook entfernen
 settings.webhook_deletion_desc=Das Löschen dieses Webhooks wird alle zugehörigen Informationen und den Übertragungsverlauf entfernen. Wirklich fortfahren?
@@ -805,8 +805,8 @@ settings.webhook.response=Antwort
 settings.webhook.headers=Kopfzeilen
 settings.webhook.payload=Nutzdaten
 settings.webhook.body=Inhalt
-settings.webhook.err_cannot_parse_payload_url=Cannot parse payload URL: %v
-settings.webhook.err_cannot_use_local_addresses=Non admins are not allowed to use local addresses.
+settings.webhook.err_cannot_parse_payload_url=Payload URL kann nicht analysiert werden: %v
+settings.webhook.err_cannot_use_local_addresses=Nicht-Administratoren dürfen keine lokalen Adressen verwenden.
 settings.githooks_desc=Git-Hooks werden von Git selbst bereitgestellt. Sie können die Dateien der unterstützten Hooks in der Liste unten bearbeiten, um eigene Operationen einzubinden.
 settings.githook_edit_desc=Wenn ein Hook inaktiv ist, wird der Standardinhalt benutzt. Lassen Sie den Inhalt leer, um den Hook zu deaktivieren.
 settings.githook_name=Hook-Name
@@ -1332,9 +1332,9 @@ config.git.clone_timeout=Clone-Timeout
 config.git.pull_timeout=Pull-Timeout
 config.git.gc_timeout=GC-Timeout
 
-config.lfs_config=LFS configuration
-config.lfs.storage=Storage
-config.lfs.objects_path=Objects path
+config.lfs_config=LFS-Konfiguration
+config.lfs.storage=Speicher
+config.lfs.objects_path=Objektpfad
 
 config.log_config=Konfiguration des Loggings
 config.log_file_root_path=Log-Verzeichnis

+ 17 - 17
conf/locale/locale_uk-UA.ini

@@ -1196,15 +1196,15 @@ config.ssh.root_path=Шлях до кореня
 config.ssh.keygen_path=Keygen path
 config.ssh.key_test_path=Key test path
 config.ssh.minimum_key_size_check=Minimum key size check
-config.ssh.minimum_key_sizes=Minimum key sizes
+config.ssh.minimum_key_sizes=Мінімальні розміри ключів
 config.ssh.rewrite_authorized_keys_at_start=Rewrite "authorized_keys" at start
-config.ssh.start_builtin_server=Start builtin server
-config.ssh.listen_host=Listen host
-config.ssh.listen_port=Listen port
+config.ssh.start_builtin_server=Запустити вбудований сервер
+config.ssh.listen_host=Слухати хост
+config.ssh.listen_port=Слухати порт
 config.ssh.server_ciphers=Server ciphers
 
 config.repo_config=Налаштування репозиторія
-config.repo.root_path=Root path
+config.repo.root_path=Кореневий шлях
 config.repo.script_type=Script type
 config.repo.ansi_chatset=ANSI charset
 config.repo.force_private=Force private
@@ -1293,11 +1293,11 @@ config.http_config=Налаштування HTTP
 config.http.access_control_allow_origin=Access control allow origin
 
 config.attachment_config=Attachment configuration
-config.attachment.enabled=Enabled
-config.attachment.path=Path
-config.attachment.allowed_types=Allowed types
-config.attachment.max_size=Size limit
-config.attachment.max_files=Files limit
+config.attachment.enabled=Увімкнено
+config.attachment.path=Шлях
+config.attachment.allowed_types=Дозволені типи
+config.attachment.max_size=Ліміт розміру
+config.attachment.max_files=Ліміт файлів
 
 config.release_config=Release configuration
 config.release.attachment.enabled=Attachment enabled
@@ -1308,8 +1308,8 @@ config.release.attachment.max_files=Attachment files limit
 config.picture_config=Налаштування зображень
 config.picture.avatar_upload_path=User avatar upload path
 config.picture.repo_avatar_upload_path=Repository avatar upload path
-config.picture.gravatar_source=Gravatar source
-config.picture.disable_gravatar=Disable Gravatar
+config.picture.gravatar_source=Джерело граватарів
+config.picture.disable_gravatar=Вимкнути граватари
 config.picture.enable_federated_avatar=Enable federated avatars
 
 config.mirror_config=Mirror configuration
@@ -1325,16 +1325,16 @@ config.git.disable_diff_highlight=Disable diff syntax highlight
 config.git.max_diff_lines=Diff lines limit (for a single file)
 config.git.max_diff_line_characters=Diff characters limit (for a single line)
 config.git.max_diff_files=Diff files limit (for a single diff)
-config.git.gc_args=GC arguments
+config.git.gc_args=Аргументи GC
 config.git.migrate_timeout=Migration timeout
 config.git.mirror_timeout=Mirror fetch timeout
 config.git.clone_timeout=Clone timeout
 config.git.pull_timeout=Pull timeout
 config.git.gc_timeout=GC timeout
 
-config.lfs_config=LFS configuration
-config.lfs.storage=Storage
-config.lfs.objects_path=Objects path
+config.lfs_config=Налаштування LFS
+config.lfs.storage=Сховище
+config.lfs.objects_path=Шлях об'єктів
 
 config.log_config=Конфігурація журналу
 config.log_file_root_path=Повний шлях до Log-файлу
@@ -1409,7 +1409,7 @@ months=%d місяців %s
 years=%d роки %s
 raw_seconds=секунди
 raw_minutes=хвилини
-raw_hours=hours
+raw_hours=години
 
 [dropzone]
 default_message=Перетягніть файли сюди або натисніть "завантажити".

+ 17 - 4
docs/dev/database_schema.md

@@ -1,8 +1,21 @@
+# Table "access"
+
+```
+  FIELD  | COLUMN  |   POSTGRESQL    |         MYSQL         |     SQLITE3       
+---------+---------+-----------------+-----------------------+-------------------
+  ID     | id      | BIGSERIAL       | BIGINT AUTO_INCREMENT | INTEGER           
+  UserID | user_id | BIGINT NOT NULL | BIGINT NOT NULL       | INTEGER NOT NULL  
+  RepoID | repo_id | BIGINT NOT NULL | BIGINT NOT NULL       | INTEGER NOT NULL  
+  Mode   | mode    | BIGINT NOT NULL | BIGINT NOT NULL       | INTEGER NOT NULL  
+
+Primary keys: id
+```
+
 # Table "access_token"
 
 ```
      FIELD    |    COLUMN    |     POSTGRESQL     |         MYSQL         |      SQLITE3        
-+-------------+--------------+--------------------+-----------------------+--------------------+
+--------------+--------------+--------------------+-----------------------+---------------------
   ID          | id           | BIGSERIAL          | BIGINT AUTO_INCREMENT | INTEGER             
   UserID      | uid          | BIGINT             | BIGINT                | INTEGER             
   Name        | name         | TEXT               | LONGTEXT              | TEXT                
@@ -17,7 +30,7 @@ Primary keys: id
 
 ```
     FIELD   |   COLUMN   |      POSTGRESQL      |        MYSQL         |      SQLITE3       
-+-----------+------------+----------------------+----------------------+-------------------+
+------------+------------+----------------------+----------------------+--------------------
   RepoID    | repo_id    | BIGINT               | BIGINT               | INTEGER            
   OID       | oid        | TEXT                 | VARCHAR(191)         | TEXT               
   Size      | size       | BIGINT NOT NULL      | BIGINT NOT NULL      | INTEGER NOT NULL   
@@ -31,13 +44,13 @@ Primary keys: repo_id, oid
 
 ```
      FIELD    |    COLUMN    |    POSTGRESQL    |         MYSQL         |     SQLITE3       
-+-------------+--------------+------------------+-----------------------+------------------+
+--------------+--------------+------------------+-----------------------+-------------------
   ID          | id           | BIGSERIAL        | BIGINT AUTO_INCREMENT | INTEGER           
   Type        | type         | BIGINT           | BIGINT                | INTEGER           
   Name        | name         | TEXT UNIQUE      | VARCHAR(191) UNIQUE   | TEXT UNIQUE       
   IsActived   | is_actived   | BOOLEAN NOT NULL | BOOLEAN NOT NULL      | NUMERIC NOT NULL  
   IsDefault   | is_default   | BOOLEAN          | BOOLEAN               | NUMERIC           
-  RawConfig   | cfg          | TEXT             | TEXT                  | TEXT              
+  Config      | cfg          | TEXT             | TEXT                  | TEXT              
   CreatedUnix | created_unix | BIGINT           | BIGINT                | INTEGER           
   UpdatedUnix | updated_unix | BIGINT           | BIGINT                | INTEGER           
 

+ 2 - 1
docs/dev/import_locale.md

@@ -12,5 +12,6 @@
 1. Run `task generate-bindata` to generate corresponding bindata.
 1. Run `task web` to start the web server, then visit the site in the browser to make sure nothing blows up.
 1. Check out a new branch using `git checkout -b update-locales`.
-1. Stash changes then run `git commit -m "locale: sync from Crowdin"`.
+1. Stage changes
+1. Run `git commit -m "locale: sync from Crowdin"`.
 1. Push the commit then open up a pull request on GitHub.

+ 9 - 5
docs/dev/release/issue_template_patch_release.md

@@ -1,17 +1,17 @@
 ## Before release
 
-On develop branch:
+On release branch:
 
-- [ ] Update [CHANGELOG](https://github.com/gogs/gogs/blob/main/CHANGELOG.md) to include entries for the current patch release.
+- [ ] Make sure all commits are cherry-picked from the develop branch by checking the patch milestone.
+- [ ] Update [CHANGELOG](https://github.com/gogs/gogs/blob/main/CHANGELOG.md) to include entries for the current patch release, e.g. `git log v0.12.1...HEAD --pretty=format:'- [ ] %H %s' --reverse`:
+	- [ ] _link to the commit_
 
 ## During release
 
 On release branch:
 
-- [ ] Cherry-pick commits from develop branch:
-	- [ ] _link to the commit_
-- [ ] Wait for GitHub Actions to complete and no failed jobs.
 - [ ] Update the [hard-coded version](https://github.com/gogs/gogs/blob/main/gogs.go#L21) to the current release, e.g. `0.12.0` -> `0.12.1`.
+- [ ] Wait for GitHub Actions to complete and no failed jobs.
 - [ ] Publish a new [GitHub release](https://github.com/gogs/gogs/releases) with entries from [CHANGELOG](https://github.com/gogs/gogs/blob/main/CHANGELOG.md) for the current patch release and all previous releases with same minor version.
 - [ ] Update all previous GitHub releases with same minor version with the warning:
     ```
@@ -34,5 +34,9 @@ On release branch:
 
 On develop branch:
 
+- [ ] Post the following message on issues that are included in the patch milestone:
+    ```
+    The <MAJOR>.<MINOR>.<PATCH> has been released.
+    ```
 - [ ] Update the repository mirror on [Gitee](https://gitee.com/unknwon/gogs).
 - [ ] Reply to the release topic for the minor release on [Gogs Discussion](https://discuss.gogs.io/c/announcements/5).

+ 4 - 3
go.mod

@@ -7,6 +7,7 @@ require (
 	github.com/bgentry/speakeasy v0.1.0 // indirect
 	github.com/editorconfig/editorconfig-core-go/v2 v2.3.7
 	github.com/fatih/color v1.9.0 // indirect
+	github.com/go-bindata/go-bindata v3.1.2+incompatible // indirect
 	github.com/go-macaron/binding v1.1.1
 	github.com/go-macaron/cache v0.0.0-20190810181446-10f7c57e2196
 	github.com/go-macaron/captcha v0.2.0
@@ -59,11 +60,11 @@ require (
 	gopkg.in/ini.v1 v1.60.2
 	gopkg.in/ldap.v2 v2.5.1
 	gopkg.in/macaron.v1 v1.3.9
-	gorm.io/driver/mysql v1.0.1
-	gorm.io/driver/postgres v1.0.0
+	gorm.io/driver/mysql v1.0.2
+	gorm.io/driver/postgres v1.0.2
 	gorm.io/driver/sqlite v1.1.3
 	gorm.io/driver/sqlserver v1.0.4
-	gorm.io/gorm v1.20.1
+	gorm.io/gorm v1.20.2
 	unknwon.dev/clog/v2 v2.1.2
 	xorm.io/builder v0.3.6
 	xorm.io/core v0.7.2

+ 18 - 13
go.sum

@@ -55,6 +55,9 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
 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=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/go-bindata/go-bindata v1.0.0 h1:DZ34txDXWn1DyWa+vQf7V9ANc2ILTtrEjtlsdJRF26M=
+github.com/go-bindata/go-bindata v3.1.2+incompatible h1:5vjJMVhowQdPzjE1LdxyFF7YFTXg5IgGVW4gBr5IbvE=
+github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 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=
@@ -159,8 +162,8 @@ github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsU
 github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
 github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
 github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
-github.com/jackc/pgconn v1.6.4 h1:S7T6cx5o2OqmxdHaXLH1ZeD1SbI8jBznyYE9Ec0RCQ8=
-github.com/jackc/pgconn v1.6.4/go.mod h1:w2pne1C2tZgP+TvjqLpOigGzNqjBgQW9dUw/4Chex78=
+github.com/jackc/pgconn v1.7.0 h1:pwjzcYyfmz/HQOQlENvG1OcDqauTGaqlVahq934F0/U=
+github.com/jackc/pgconn v1.7.0/go.mod h1:sF/lPpNEMEOp+IYhyQGdAvrG20gWf6A1tKlr0v7JMeA=
 github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
 github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
 github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA=
@@ -174,8 +177,8 @@ github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod
 github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
 github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
 github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
-github.com/jackc/pgproto3/v2 v2.0.2 h1:q1Hsy66zh4vuNsajBUF2PNqfAMMfxU5mk594lPE9vjY=
-github.com/jackc/pgproto3/v2 v2.0.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgproto3/v2 v2.0.5 h1:NUbEWPmCQZbMmYlTjVoNPhc0CfnYyz2bfUAh6A5ZVJM=
+github.com/jackc/pgproto3/v2 v2.0.5/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
 github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
 github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
 github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
@@ -185,20 +188,21 @@ github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrU
 github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
 github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
 github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
-github.com/jackc/pgtype v1.4.2 h1:t+6LWm5eWPLX1H5Se702JSBcirq6uWa4jiG4wV1rAWY=
-github.com/jackc/pgtype v1.4.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
+github.com/jackc/pgtype v1.5.0 h1:jzBqRk2HFG2CV4AIwgCI2PwTgm6UUoCAK2ofHHRirtc=
+github.com/jackc/pgtype v1.5.0/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
 github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
 github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
 github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
 github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
 github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
 github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
-github.com/jackc/pgx/v4 v4.8.1 h1:SUbCLP2pXvf/Sr/25KsuI4aTxiFYIvpfk4l6aTSdyCw=
-github.com/jackc/pgx/v4 v4.8.1/go.mod h1:4HOLxrl8wToZJReD04/yB20GDwf4KBYETvlHciCnwW0=
+github.com/jackc/pgx/v4 v4.9.0 h1:6STjDqppM2ROy5p1wNDcsC7zJTjSHeuCsguZmXyzx7c=
+github.com/jackc/pgx/v4 v4.9.0/go.mod h1:MNGWmViCgqbZck9ujOOBN63gK9XVGILXWCvKLGKmnms=
 github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v1.1.2/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 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/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@@ -536,19 +540,20 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gorm.io/driver/mysql v1.0.1 h1:omJoilUzyrAp0xNoio88lGJCroGdIOen9hq2A/+3ifw=
-gorm.io/driver/mysql v1.0.1/go.mod h1:KtqSthtg55lFp3S5kUXqlGaelnWpKitn4k1xZTnoiPw=
-gorm.io/driver/postgres v1.0.0 h1:Yh4jyFQ0a7F+JPU0Gtiam/eKmpT/XFc1FKxotGqc6FM=
-gorm.io/driver/postgres v1.0.0/go.mod h1:wtMFcOzmuA5QigNsgEIb7O5lhvH1tHAF1RbWmLWV4to=
+gorm.io/driver/mysql v1.0.2 h1:xm21Um8cR/Cg+nMwSrajf8aBUxOIC+WmH72ir/ByYR8=
+gorm.io/driver/mysql v1.0.2/go.mod h1:T+Fv7Rq/8+lpS3X1KKVUbj8Y/SzbPa5esK9KpPAKXR8=
+gorm.io/driver/postgres v1.0.2 h1:mB5JjD4QglbCTdMT1aZDxQzHr87XDK1qh0MKIU3P96g=
+gorm.io/driver/postgres v1.0.2/go.mod h1:FvRSYfBI9jEp6ZSjlpS9qNcSjxwYxFc03UOTrHdvvYA=
 gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc=
 gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c=
 gorm.io/driver/sqlserver v1.0.4 h1:V15fszi0XAo7fbx3/cF50ngshDSN4QT0MXpWTylyPTY=
 gorm.io/driver/sqlserver v1.0.4/go.mod h1:ciEo5btfITTBCj9BkoUVDvgQbUdLWQNqdFY5OGuGnRg=
-gorm.io/gorm v1.9.19/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
 gorm.io/gorm v1.20.0 h1:qfIlyaZvrF7kMWY3jBdEBXkXJ2M5MFYMTppjILxS3fQ=
 gorm.io/gorm v1.20.0/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
 gorm.io/gorm v1.20.1 h1:+hOwlHDqvqmBIMflemMVPLJH7tZYK4RxFDBHEfJTup0=
 gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
+gorm.io/gorm v1.20.2 h1:bZzSEnq7NDGsrd+n3evOOedDrY5oLM5QPlCjZJUK2ro=
+gorm.io/gorm v1.20.2/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
 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=

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


File diff suppressed because it is too large
+ 113 - 114
internal/assets/public/public_gen.go


File diff suppressed because it is too large
+ 113 - 114
internal/assets/templates/templates_gen.go


+ 3 - 3
internal/cmd/hook.go

@@ -66,7 +66,7 @@ func runHookPreReceive(c *cli.Context) error {
 	if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
 		return nil
 	}
-	setup(c, "hooks/pre-receive.log", true)
+	setup(c, "pre-receive.log", true)
 
 	isWiki := strings.Contains(os.Getenv(db.ENV_REPO_CUSTOM_HOOKS_PATH), ".wiki.git/")
 
@@ -159,7 +159,7 @@ func runHookUpdate(c *cli.Context) error {
 	if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
 		return nil
 	}
-	setup(c, "hooks/update.log", false)
+	setup(c, "update.log", false)
 
 	args := c.Args()
 	if len(args) != 3 {
@@ -193,7 +193,7 @@ func runHookPostReceive(c *cli.Context) error {
 	if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
 		return nil
 	}
-	setup(c, "hooks/post-receive.log", true)
+	setup(c, "post-receive.log", true)
 
 	// Post-receive hook does more than just gather Git information,
 	// so we need to setup additional services for email notifications.

+ 10 - 8
internal/cmd/serv.go

@@ -38,7 +38,7 @@ var Serv = cli.Command{
 // logs error message on the server side. When not in "prod" mode,
 // error message is also printed to the client for easier debugging.
 func fail(userMessage, errMessage string, args ...interface{}) {
-	fmt.Fprintln(os.Stderr, "Gogs:", userMessage)
+	_, _ = fmt.Fprintln(os.Stderr, "Gogs:", userMessage)
 
 	if len(errMessage) > 0 {
 		if !conf.IsProdMode() {
@@ -47,10 +47,11 @@ func fail(userMessage, errMessage string, args ...interface{}) {
 		log.Error(errMessage, args...)
 	}
 
+	log.Stop()
 	os.Exit(1)
 }
 
-func setup(c *cli.Context, logPath string, connectDB bool) {
+func setup(c *cli.Context, logFile string, connectDB bool) {
 	conf.HookMode = true
 
 	var customConf string
@@ -73,7 +74,7 @@ func setup(c *cli.Context, logPath string, connectDB bool) {
 
 	err = log.NewFile(log.FileConfig{
 		Level:    level,
-		Filename: filepath.Join(conf.Log.RootPath, logPath),
+		Filename: filepath.Join(conf.Log.RootPath, "hooks", logFile),
 		FileRotationConfig: log.FileRotationConfig{
 			Rotate:  true,
 			Daily:   true,
@@ -209,11 +210,12 @@ func runServ(c *cli.Context) error {
 				fail("Internal error", "Failed to get user by key ID '%d': %v", key.ID, err)
 			}
 
-			mode, err := db.UserAccessMode(user.ID, repo)
-			if err != nil {
-				fail("Internal error", "Failed to check access: %v", err)
-			}
-
+			mode := db.Perms.AccessMode(user.ID, repo.ID,
+				db.AccessModeOptions{
+					OwnerID: repo.OwnerID,
+					Private: repo.IsPrivate,
+				},
+			)
 			if mode < requestMode {
 				clientMessage := _ACCESS_DENIED_MESSAGE
 				if mode >= db.AccessModeRead {

+ 1 - 0
internal/conf/static.go

@@ -284,6 +284,7 @@ var (
 			Mirror  int
 			Clone   int
 			Pull    int
+			Diff    int
 			GC      int `ini:"GC"`
 		} `ini:"git.timeout"`
 	}

+ 14 - 12
internal/context/repo.go

@@ -170,22 +170,24 @@ func RepoAssignment(pages ...bool) macaron.Handler {
 		if c.IsLogged && c.User.IsAdmin {
 			c.Repo.AccessMode = db.AccessModeOwner
 		} else {
-			mode, err := db.UserAccessMode(c.UserID(), c.Repo.Repository)
-			if err != nil {
-				c.Error(err, "get user access mode")
-				return
-			}
-			c.Repo.AccessMode = mode
+			c.Repo.AccessMode = db.Perms.AccessMode(c.UserID(), repo.ID,
+				db.AccessModeOptions{
+					OwnerID: repo.OwnerID,
+					Private: repo.IsPrivate,
+				},
+			)
 		}
 
 		// If the authenticated user has no direct access, see if the repository is a fork
 		// and whether the user has access to the base repository.
-		if c.Repo.AccessMode == db.AccessModeNone && c.Repo.Repository.IsFork {
-			mode, err := db.UserAccessMode(c.UserID(), c.Repo.Repository.BaseRepo)
-			if err != nil {
-				c.Error(err, "get user access mode of base repository")
-				return
-			}
+		if c.Repo.AccessMode == db.AccessModeNone && repo.BaseRepo != nil {
+			mode := db.Perms.AccessMode(c.UserID(), repo.BaseRepo.ID,
+				db.AccessModeOptions{
+					OwnerID: repo.BaseRepo.OwnerID,
+					Private: repo.BaseRepo.IsPrivate,
+				},
+			)
+
 			// Users shouldn't have indirect access level higher than write.
 			if mode > db.AccessModeWrite {
 				mode = db.AccessModeWrite

+ 0 - 240
internal/db/access.go

@@ -1,240 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-package db
-
-import (
-	"fmt"
-
-	log "unknwon.dev/clog/v2"
-)
-
-type AccessMode int
-
-const (
-	AccessModeNone  AccessMode = iota // 0
-	AccessModeRead                    // 1
-	AccessModeWrite                   // 2
-	AccessModeAdmin                   // 3
-	AccessModeOwner                   // 4
-)
-
-func (mode AccessMode) String() string {
-	switch mode {
-	case AccessModeRead:
-		return "read"
-	case AccessModeWrite:
-		return "write"
-	case AccessModeAdmin:
-		return "admin"
-	case AccessModeOwner:
-		return "owner"
-	default:
-		return "none"
-	}
-}
-
-// ParseAccessMode returns corresponding access mode to given permission string.
-func ParseAccessMode(permission string) AccessMode {
-	switch permission {
-	case "write":
-		return AccessModeWrite
-	case "admin":
-		return AccessModeAdmin
-	default:
-		return AccessModeRead
-	}
-}
-
-// 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 {
-	ID     int64
-	UserID int64 `xorm:"UNIQUE(s)"`
-	RepoID int64 `xorm:"UNIQUE(s)"`
-	Mode   AccessMode
-}
-
-func userAccessMode(e Engine, userID int64, repo *Repository) (AccessMode, error) {
-	mode := AccessModeNone
-	// Everyone has read access to public repository
-	if !repo.IsPrivate {
-		mode = AccessModeRead
-	}
-
-	if userID <= 0 {
-		return mode, nil
-	}
-
-	if userID == repo.OwnerID {
-		return AccessModeOwner, nil
-	}
-
-	access := &Access{
-		UserID: userID,
-		RepoID: repo.ID,
-	}
-	if has, err := e.Get(access); !has || err != nil {
-		return mode, err
-	}
-	return access.Mode, nil
-}
-
-// UserAccessMode returns the access mode of given user to the repository.
-func UserAccessMode(userID int64, repo *Repository) (AccessMode, error) {
-	return userAccessMode(x, userID, repo)
-}
-
-func hasAccess(e Engine, userID int64, repo *Repository, testMode AccessMode) (bool, error) {
-	mode, err := userAccessMode(e, userID, repo)
-	return mode >= testMode, err
-}
-
-// HasAccess returns true if someone has the request access level. User can be nil!
-// Deprecated: Use Perms.Authorize instead.
-func HasAccess(userID int64, repo *Repository, testMode AccessMode) (bool, error) {
-	return hasAccess(x, userID, repo, testMode)
-}
-
-// GetRepositoryAccesses finds all repositories with their access mode where a user has access but does not own.
-func (u *User) GetRepositoryAccesses() (map[*Repository]AccessMode, error) {
-	accesses := make([]*Access, 0, 10)
-	if err := x.Find(&accesses, &Access{UserID: u.ID}); err != nil {
-		return nil, err
-	}
-
-	repos := make(map[*Repository]AccessMode, len(accesses))
-	for _, access := range accesses {
-		repo, err := GetRepositoryByID(access.RepoID)
-		if err != nil {
-			if IsErrRepoNotExist(err) {
-				log.Error("Failed to get repository by ID: %v", err)
-				continue
-			}
-			return nil, err
-		}
-		if repo.OwnerID == u.ID {
-			continue
-		}
-		repos[repo] = access.Mode
-	}
-	return repos, nil
-}
-
-// GetAccessibleRepositories finds repositories which the user has access but does not own.
-// If limit is smaller than 1 means returns all found results.
-func (user *User) GetAccessibleRepositories(limit int) (repos []*Repository, _ error) {
-	sess := x.Where("owner_id !=? ", user.ID).Desc("updated_unix")
-	if limit > 0 {
-		sess.Limit(limit)
-		repos = make([]*Repository, 0, limit)
-	} else {
-		repos = make([]*Repository, 0, 10)
-	}
-	return repos, sess.Join("INNER", "access", "access.user_id = ? AND access.repo_id = repository.id", user.ID).Find(&repos)
-}
-
-func maxAccessMode(modes ...AccessMode) AccessMode {
-	max := AccessModeNone
-	for _, mode := range modes {
-		if mode > max {
-			max = mode
-		}
-	}
-	return max
-}
-
-// Deprecated: Use Perms.SetRepoPerms instead.
-func (repo *Repository) refreshAccesses(e Engine, accessMap map[int64]AccessMode) (err error) {
-	newAccesses := make([]Access, 0, len(accessMap))
-	for userID, mode := range accessMap {
-		newAccesses = append(newAccesses, Access{
-			UserID: userID,
-			RepoID: repo.ID,
-			Mode:   mode,
-		})
-	}
-
-	// Delete old accesses and insert new ones for repository.
-	if _, err = e.Delete(&Access{RepoID: repo.ID}); err != nil {
-		return fmt.Errorf("delete old accesses: %v", err)
-	} else if _, err = e.Insert(newAccesses); err != nil {
-		return fmt.Errorf("insert new accesses: %v", err)
-	}
-	return nil
-}
-
-// refreshCollaboratorAccesses retrieves repository collaborations with their access modes.
-func (repo *Repository) refreshCollaboratorAccesses(e Engine, accessMap map[int64]AccessMode) error {
-	collaborations, err := repo.getCollaborations(e)
-	if err != nil {
-		return fmt.Errorf("getCollaborations: %v", err)
-	}
-	for _, c := range collaborations {
-		accessMap[c.UserID] = c.Mode
-	}
-	return nil
-}
-
-// recalculateTeamAccesses recalculates new accesses for teams of an organization
-// except the team whose ID is given. It is used to assign a team ID when
-// remove repository from that team.
-func (repo *Repository) recalculateTeamAccesses(e Engine, ignTeamID int64) (err error) {
-	accessMap := make(map[int64]AccessMode, 20)
-
-	if err = repo.getOwner(e); err != nil {
-		return err
-	} else if !repo.Owner.IsOrganization() {
-		return fmt.Errorf("owner is not an organization: %d", repo.OwnerID)
-	}
-
-	if err = repo.refreshCollaboratorAccesses(e, accessMap); err != nil {
-		return fmt.Errorf("refreshCollaboratorAccesses: %v", err)
-	}
-
-	if err = repo.Owner.getTeams(e); err != nil {
-		return err
-	}
-
-	for _, t := range repo.Owner.Teams {
-		if t.ID == ignTeamID {
-			continue
-		}
-
-		// Owner team gets owner access, and skip for teams that do not
-		// have relations with repository.
-		if t.IsOwnerTeam() {
-			t.Authorize = AccessModeOwner
-		} else if !t.hasRepository(e, repo.ID) {
-			continue
-		}
-
-		if err = t.getMembers(e); err != nil {
-			return fmt.Errorf("getMembers '%d': %v", t.ID, err)
-		}
-		for _, m := range t.Members {
-			accessMap[m.ID] = maxAccessMode(accessMap[m.ID], t.Authorize)
-		}
-	}
-
-	return repo.refreshAccesses(e, accessMap)
-}
-
-func (repo *Repository) recalculateAccesses(e Engine) error {
-	if repo.Owner.IsOrganization() {
-		return repo.recalculateTeamAccesses(e, 0)
-	}
-
-	accessMap := make(map[int64]AccessMode, 10)
-	if err := repo.refreshCollaboratorAccesses(e, accessMap); err != nil {
-		return fmt.Errorf("refreshCollaboratorAccesses: %v", err)
-	}
-	return repo.refreshAccesses(e, accessMap)
-}
-
-// RecalculateAccesses recalculates all accesses for repository.
-func (repo *Repository) RecalculateAccesses() error {
-	return repo.recalculateAccesses(x)
-}

+ 6 - 6
internal/db/action.go

@@ -57,9 +57,9 @@ var (
 	IssueCloseKeywords  = []string{"close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved"}
 	IssueReopenKeywords = []string{"reopen", "reopens", "reopened"}
 
-	IssueCloseKeywordsPat     = lazyregexp.New(assembleKeywordsPattern(IssueCloseKeywords))
-	IssueReopenKeywordsPat    = lazyregexp.New(assembleKeywordsPattern(IssueReopenKeywords))
-	IssueReferenceKeywordsPat = lazyregexp.New(`(?i)(?:)(^| )\S+`)
+	IssueCloseKeywordsPat  = lazyregexp.New(assembleKeywordsPattern(IssueCloseKeywords))
+	IssueReopenKeywordsPat = lazyregexp.New(assembleKeywordsPattern(IssueReopenKeywords))
+	issueReferencePattern  = lazyregexp.New(`(?i)(?:)(^| )\S*#\d+`)
 )
 
 func assembleKeywordsPattern(words []string) string {
@@ -321,8 +321,8 @@ func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit) err
 		c := commits[i]
 
 		refMarked := make(map[int64]bool)
-		for _, ref := range IssueReferenceKeywordsPat.FindAllString(c.Message, -1) {
-			ref = ref[strings.IndexByte(ref, byte(' '))+1:]
+		for _, ref := range issueReferencePattern.FindAllString(c.Message, -1) {
+			ref = strings.TrimSpace(ref)
 			ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
 
 			if len(ref) == 0 {
@@ -455,7 +455,7 @@ type CommitRepoActionOptions struct {
 	Commits     *PushCommits
 }
 
-// CommitRepoAction adds new commit actio to the repository, and prepare corresponding webhooks.
+// CommitRepoAction adds new commit action to the repository, and prepare corresponding webhooks.
 func CommitRepoAction(opts CommitRepoActionOptions) error {
 	pusher, err := GetUserByName(opts.PusherName)
 	if err != nil {

+ 41 - 0
internal/db/action_test.go

@@ -0,0 +1,41 @@
+// 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 (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_issueReferencePattern(t *testing.T) {
+	tests := []struct {
+		name       string
+		message    string
+		expStrings []string
+	}{
+		{
+			name:       "no match",
+			message:    "Hello world!",
+			expStrings: nil,
+		},
+		{
+			name:       "contains issue numbers",
+			message:    "#123 is fixed, and #456 is WIP",
+			expStrings: []string{"#123", " #456"},
+		},
+		{
+			name:       "contains full issue references",
+			message:    "#123 is fixed, and user/repo#456 is WIP",
+			expStrings: []string{"#123", " user/repo#456"},
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			strs := issueReferencePattern.FindAllString(test.message, -1)
+			assert.Equal(t, test.expStrings, strs)
+		})
+	}
+}

+ 15 - 2
internal/db/backup_test.go

@@ -29,8 +29,8 @@ func Test_dumpAndImport(t *testing.T) {
 
 	t.Parallel()
 
-	if len(Tables) != 3 {
-		t.Fatalf("New table has added (want 3 got %d), please add new tests for the table and update this check", len(Tables))
+	if len(Tables) != 4 {
+		t.Fatalf("New table has added (want 4 got %d), please add new tests for the table and update this check", len(Tables))
 	}
 
 	db := initTestDB(t, "dumpAndImport", Tables...)
@@ -46,6 +46,19 @@ func setupDBToDump(t *testing.T, db *gorm.DB) {
 	t.Helper()
 
 	vals := []interface{}{
+		&Access{
+			ID:     1,
+			UserID: 1,
+			RepoID: 11,
+			Mode:   AccessModeRead,
+		},
+		&Access{
+			ID:     2,
+			UserID: 2,
+			RepoID: 22,
+			Mode:   AccessModeWrite,
+		},
+
 		&AccessToken{
 			UserID:      1,
 			Name:        "test1",

+ 2 - 7
internal/db/db.go

@@ -141,16 +141,11 @@ func openDB(opts conf.DatabaseOpts, cfg *gorm.Config) (*gorm.DB, error) {
 //
 // NOTE: Lines are sorted in alphabetical order, each letter in its own line.
 var Tables = []interface{}{
-	new(AccessToken),
+	new(Access), new(AccessToken),
 	new(LFSObject), new(LoginSource),
 }
 
-func Init() (*gorm.DB, error) {
-	w, err := newLogWriter()
-	if err != nil {
-		return nil, errors.Wrap(err, "new log writer")
-	}
-
+func Init(w logger.Writer) (*gorm.DB, error) {
 	level := logger.Info
 	if conf.IsProdMode() {
 		level = logger.Warn

+ 0 - 20
internal/db/errors/issue.go

@@ -1,20 +0,0 @@
-// Copyright 2017 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-package errors
-
-import "fmt"
-
-type InvalidIssueReference struct {
-	Ref string
-}
-
-func IsInvalidIssueReference(err error) bool {
-	_, ok := err.(InvalidIssueReference)
-	return ok
-}
-
-func (err InvalidIssueReference) Error() string {
-	return fmt.Sprintf("invalid issue reference [ref: %s]", err.Ref)
-}

+ 7 - 13
internal/db/issue.go

@@ -679,17 +679,12 @@ func newIssue(e *xorm.Session, opts NewIssueOptions) (err error) {
 			return fmt.Errorf("get user by ID: %v", err)
 		}
 
-		// Assume assignee is invalid and drop silently.
-		opts.Issue.AssigneeID = 0
 		if assignee != nil {
-			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)
-			}
-			if valid {
-				opts.Issue.AssigneeID = assignee.ID
-				opts.Issue.Assignee = assignee
-			}
+			opts.Issue.AssigneeID = assignee.ID
+			opts.Issue.Assignee = assignee
+		} else {
+			// The assignee does not exist, drop it
+			opts.Issue.AssigneeID = 0
 		}
 	}
 
@@ -817,12 +812,11 @@ func (ErrIssueNotExist) NotFound() bool {
 	return true
 }
 
-// GetIssueByRef returns an Issue specified by a GFM reference.
-// See https://help.github.com/articles/writing-on-github#references for more information on the syntax.
+// GetIssueByRef returns an Issue specified by a GFM reference, e.g. owner/repo#123.
 func GetIssueByRef(ref string) (*Issue, error) {
 	n := strings.IndexByte(ref, byte('#'))
 	if n == -1 {
-		return nil, errors.InvalidIssueReference{Ref: ref}
+		return nil, ErrIssueNotExist{args: map[string]interface{}{"ref": ref}}
 	}
 
 	index := com.StrTo(ref[n+1:]).MustInt64()

+ 6 - 6
internal/db/mocks.go

@@ -134,17 +134,17 @@ func (m *mockLoginSourceFileStore) Save() error {
 var _ PermsStore = (*MockPermsStore)(nil)
 
 type MockPermsStore struct {
-	MockAccessMode   func(userID int64, repo *Repository) AccessMode
-	MockAuthorize    func(userID int64, repo *Repository, desired AccessMode) bool
+	MockAccessMode   func(userID, repoID int64, opts AccessModeOptions) AccessMode
+	MockAuthorize    func(userID, repoID int64, desired AccessMode, opts AccessModeOptions) bool
 	MockSetRepoPerms func(repoID int64, accessMap map[int64]AccessMode) error
 }
 
-func (m *MockPermsStore) AccessMode(userID int64, repo *Repository) AccessMode {
-	return m.MockAccessMode(userID, repo)
+func (m *MockPermsStore) AccessMode(userID, repoID int64, opts AccessModeOptions) AccessMode {
+	return m.MockAccessMode(userID, repoID, opts)
 }
 
-func (m *MockPermsStore) Authorize(userID int64, repo *Repository, desired AccessMode) bool {
-	return m.MockAuthorize(userID, repo, desired)
+func (m *MockPermsStore) Authorize(userID, repoID int64, desired AccessMode, opts AccessModeOptions) bool {
+	return m.MockAuthorize(userID, repoID, desired, opts)
 }
 
 func (m *MockPermsStore) SetRepoPerms(repoID int64, accessMap map[int64]AccessMode) error {

+ 27 - 8
internal/db/models.go

@@ -10,16 +10,20 @@ import (
 	"net/url"
 	"os"
 	"path"
+	"path/filepath"
 	"strings"
 	"time"
 
+	"github.com/pkg/errors"
 	"gorm.io/gorm"
+	"gorm.io/gorm/logger"
 	log "unknwon.dev/clog/v2"
 	"xorm.io/core"
 	"xorm.io/xorm"
 
 	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/db/migrations"
+	"gogs.io/gogs/internal/dbutil"
 )
 
 // Engine represents a XORM engine or session.
@@ -47,7 +51,7 @@ var (
 func init() {
 	legacyTables = append(legacyTables,
 		new(User), new(PublicKey), new(TwoFactor), new(TwoFactorRecoveryCode),
-		new(Repository), new(DeployKey), new(Collaboration), new(Access), new(Upload),
+		new(Repository), new(DeployKey), new(Collaboration), new(Upload),
 		new(Watch), new(Star), new(Follow), new(Action),
 		new(Issue), new(PullRequest), new(Comment), new(Attachment), new(IssueUser),
 		new(Label), new(IssueLabel), new(Milestone),
@@ -132,16 +136,21 @@ func SetEngine() (*gorm.DB, error) {
 
 	x.SetMapper(core.GonicMapper{})
 
-	// WARNING: for serv command, MUST remove the output to os.stdout,
-	// so use log file to instead print to stdout.
+	var logPath string
+	if conf.HookMode {
+		logPath = filepath.Join(conf.Log.RootPath, "hooks", "xorm.log")
+	} else {
+		logPath = filepath.Join(conf.Log.RootPath, "xorm.log")
+	}
 	sec := conf.File.Section("log.xorm")
-	logger, err := log.NewFileWriter(path.Join(conf.Log.RootPath, "xorm.log"),
+	fileWriter, err := log.NewFileWriter(logPath,
 		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, fmt.Errorf("create 'xorm.log': %v", err)
 	}
@@ -151,12 +160,22 @@ func SetEngine() (*gorm.DB, error) {
 	x.SetConnMaxLifetime(time.Second)
 
 	if conf.IsProdMode() {
-		x.SetLogger(xorm.NewSimpleLogger3(logger, xorm.DEFAULT_LOG_PREFIX, xorm.DEFAULT_LOG_FLAG, core.LOG_WARNING))
+		x.SetLogger(xorm.NewSimpleLogger3(fileWriter, xorm.DEFAULT_LOG_PREFIX, xorm.DEFAULT_LOG_FLAG, core.LOG_WARNING))
 	} else {
-		x.SetLogger(xorm.NewSimpleLogger(logger))
+		x.SetLogger(xorm.NewSimpleLogger(fileWriter))
 	}
 	x.ShowSQL(true)
-	return Init()
+
+	var gormLogger logger.Writer
+	if conf.HookMode {
+		gormLogger = &dbutil.Logger{Writer: fileWriter}
+	} else {
+		gormLogger, err = newLogWriter()
+		if err != nil {
+			return nil, errors.Wrap(err, "new log writer")
+		}
+	}
+	return Init(gormLogger)
 }
 
 func NewEngine() (err error) {

+ 32 - 0
internal/db/org_team.go

@@ -173,6 +173,38 @@ func (t *Team) removeRepository(e Engine, repo *Repository, recalculate bool) (e
 	if err = t.getMembers(e); err != nil {
 		return fmt.Errorf("get team members: %v", err)
 	}
+
+	// TODO: Delete me when this method is migrated to use GORM.
+	userAccessMode := func(e Engine, userID int64, repo *Repository) (AccessMode, error) {
+		mode := AccessModeNone
+		// Everyone has read access to public repository
+		if !repo.IsPrivate {
+			mode = AccessModeRead
+		}
+
+		if userID <= 0 {
+			return mode, nil
+		}
+
+		if userID == repo.OwnerID {
+			return AccessModeOwner, nil
+		}
+
+		access := &Access{
+			UserID: userID,
+			RepoID: repo.ID,
+		}
+		if has, err := e.Get(access); !has || err != nil {
+			return mode, err
+		}
+		return access.Mode, nil
+	}
+
+	hasAccess := func(e Engine, userID int64, repo *Repository, testMode AccessMode) (bool, error) {
+		mode, err := userAccessMode(e, userID, repo)
+		return mode >= testMode, err
+	}
+
 	for _, member := range t.Members {
 		has, err := hasAccess(e, member.ID, repo, AccessModeRead)
 		if err != nil {

+ 63 - 10
internal/db/perms.go

@@ -14,9 +14,9 @@ import (
 // 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
+	AccessMode(userID, repoID int64, opts AccessModeOptions) 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
+	Authorize(userID, repoID int64, desired AccessMode, opts AccessModeOptions) bool
 	// SetRepoPerms does a full update to which users have which level of access to given repository.
 	// Keys of the "accessMap" are user IDs.
 	SetRepoPerms(repoID int64, accessMap map[int64]AccessMode) error
@@ -24,19 +24,72 @@ type PermsStore interface {
 
 var Perms PermsStore
 
+// Access represents the highest access level of a user has 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 {
+	ID     int64
+	UserID int64      `xorm:"UNIQUE(s)" gorm:"uniqueIndex:access_user_repo_unique;NOT NULL"`
+	RepoID int64      `xorm:"UNIQUE(s)" gorm:"uniqueIndex:access_user_repo_unique;NOT NULL"`
+	Mode   AccessMode `gorm:"NOT NULL"`
+}
+
+// AccessMode is the access mode of a user has to a repository.
+type AccessMode int
+
+const (
+	AccessModeNone  AccessMode = iota // 0
+	AccessModeRead                    // 1
+	AccessModeWrite                   // 2
+	AccessModeAdmin                   // 3
+	AccessModeOwner                   // 4
+)
+
+func (mode AccessMode) String() string {
+	switch mode {
+	case AccessModeRead:
+		return "read"
+	case AccessModeWrite:
+		return "write"
+	case AccessModeAdmin:
+		return "admin"
+	case AccessModeOwner:
+		return "owner"
+	default:
+		return "none"
+	}
+}
+
+// ParseAccessMode returns corresponding access mode to given permission string.
+func ParseAccessMode(permission string) AccessMode {
+	switch permission {
+	case "write":
+		return AccessModeWrite
+	case "admin":
+		return AccessModeAdmin
+	default:
+		return AccessModeRead
+	}
+}
+
 var _ PermsStore = (*perms)(nil)
 
 type perms struct {
 	*gorm.DB
 }
 
-func (db *perms) AccessMode(userID int64, repo *Repository) (mode AccessMode) {
-	if repo == nil {
+type AccessModeOptions struct {
+	OwnerID int64 // The ID of the repository owner.
+	Private bool  // Whether the repository is private.
+}
+
+func (db *perms) AccessMode(userID, repoID int64, opts AccessModeOptions) (mode AccessMode) {
+	if repoID <= 0 {
 		return AccessModeNone
 	}
 
 	// Everyone has read access to public repository.
-	if !repo.IsPrivate {
+	if !opts.Private {
 		mode = AccessModeRead
 	}
 
@@ -45,23 +98,23 @@ func (db *perms) AccessMode(userID int64, repo *Repository) (mode AccessMode) {
 		return mode
 	}
 
-	if userID == repo.OwnerID {
+	if userID == opts.OwnerID {
 		return AccessModeOwner
 	}
 
 	access := new(Access)
-	err := db.Where("user_id = ? AND repo_id = ?", userID, repo.ID).First(access).Error
+	err := db.Where("user_id = ? AND repo_id = ?", userID, repoID).First(access).Error
 	if err != nil {
 		if err != gorm.ErrRecordNotFound {
-			log.Error("Failed to get access [user_id: %d, repo_id: %d]: %v", userID, repo.ID, err)
+			log.Error("Failed to get access [user_id: %d, repo_id: %d]: %v", userID, repoID, err)
 		}
 		return mode
 	}
 	return access.Mode
 }
 
-func (db *perms) Authorize(userID int64, repo *Repository, desired AccessMode) bool {
-	return desired <= db.AccessMode(userID, repo)
+func (db *perms) Authorize(userID, repoID int64, desired AccessMode, opts AccessModeOptions) bool {
+	return desired <= db.AccessMode(userID, repoID, opts)
 }
 
 func (db *perms) SetRepoPerms(repoID int64, accessMap map[int64]AccessMode) error {

+ 32 - 18
internal/db/perms_test.go

@@ -57,20 +57,22 @@ func test_perms_AccessMode(t *testing.T, db *perms) {
 		t.Fatal(err)
 	}
 
-	publicRepo := &Repository{
-		ID:      1,
+	publicRepoID := int64(1)
+	publicRepoOpts := AccessModeOptions{
 		OwnerID: 98,
 	}
-	privateRepo := &Repository{
-		ID:        2,
-		OwnerID:   99,
-		IsPrivate: true,
+
+	privateRepoID := int64(2)
+	privateRepoOpts := AccessModeOptions{
+		OwnerID: 99,
+		Private: true,
 	}
 
 	tests := []struct {
 		name          string
 		userID        int64
-		repo          *Repository
+		repoID        int64
+		opts          AccessModeOptions
 		expAccessMode AccessMode
 	}{
 		{
@@ -80,62 +82,71 @@ func test_perms_AccessMode(t *testing.T, db *perms) {
 
 		{
 			name:          "anonymous user has read access to public repository",
-			repo:          publicRepo,
+			repoID:        publicRepoID,
+			opts:          publicRepoOpts,
 			expAccessMode: AccessModeRead,
 		},
 		{
 			name:          "anonymous user has no access to private repository",
-			repo:          privateRepo,
+			repoID:        privateRepoID,
+			opts:          privateRepoOpts,
 			expAccessMode: AccessModeNone,
 		},
 
 		{
 			name:          "user is the owner",
 			userID:        98,
-			repo:          publicRepo,
+			repoID:        publicRepoID,
+			opts:          publicRepoOpts,
 			expAccessMode: AccessModeOwner,
 		},
 		{
 			name:          "user 1 has read access to public repo",
 			userID:        1,
-			repo:          publicRepo,
+			repoID:        publicRepoID,
+			opts:          publicRepoOpts,
 			expAccessMode: AccessModeRead,
 		},
 		{
 			name:          "user 2 has write access to public repo",
 			userID:        2,
-			repo:          publicRepo,
+			repoID:        publicRepoID,
+			opts:          publicRepoOpts,
 			expAccessMode: AccessModeWrite,
 		},
 		{
 			name:          "user 3 has admin access to public repo",
 			userID:        3,
-			repo:          publicRepo,
+			repoID:        publicRepoID,
+			opts:          publicRepoOpts,
 			expAccessMode: AccessModeAdmin,
 		},
 
 		{
 			name:          "user 1 has read access to private repo",
 			userID:        1,
-			repo:          privateRepo,
+			repoID:        privateRepoID,
+			opts:          privateRepoOpts,
 			expAccessMode: AccessModeRead,
 		},
 		{
 			name:          "user 2 has no access to private repo",
 			userID:        2,
-			repo:          privateRepo,
+			repoID:        privateRepoID,
+			opts:          privateRepoOpts,
 			expAccessMode: AccessModeNone,
 		},
 		{
 			name:          "user 3 has no access to private repo",
 			userID:        3,
-			repo:          privateRepo,
+			repoID:        privateRepoID,
+			opts:          privateRepoOpts,
 			expAccessMode: AccessModeNone,
 		},
 	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			mode := db.AccessMode(test.userID, test.repo)
+			mode := db.AccessMode(test.userID, test.repoID, test.opts)
 			assert.Equal(t, test.expAccessMode, mode)
 		})
 	}
@@ -216,7 +227,10 @@ func test_perms_Authorize(t *testing.T, db *perms) {
 	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			authorized := db.Authorize(test.userID, repo, test.desired)
+			authorized := db.Authorize(test.userID, repo.ID, test.desired, AccessModeOptions{
+				OwnerID: repo.OwnerID,
+				Private: repo.IsPrivate,
+			})
 			assert.Equal(t, test.expAuthorized, authorized)
 		})
 	}

+ 124 - 12
internal/db/repo.go

@@ -429,24 +429,29 @@ func (repo *Repository) UpdateSize() error {
 	return nil
 }
 
-// ComposeMetas composes a map of metas for rendering external issue tracker URL.
+// ComposeMetas composes a map of metas for rendering SHA1 URL and external issue tracker URL.
 func (repo *Repository) ComposeMetas() map[string]string {
-	if !repo.EnableExternalTracker {
-		return nil
-	} else if repo.ExternalMetas == nil {
-		repo.ExternalMetas = map[string]string{
-			"format": repo.ExternalTrackerFormat,
-			"user":   repo.MustOwner().Name,
-			"repo":   repo.Name,
-		}
+	if repo.ExternalMetas != nil {
+		return repo.ExternalMetas
+	}
+
+	repo.ExternalMetas = map[string]string{
+		"repoLink": repo.Link(),
+	}
+
+	if repo.EnableExternalTracker {
+		repo.ExternalMetas["user"] = repo.MustOwner().Name
+		repo.ExternalMetas["repo"] = repo.Name
+		repo.ExternalMetas["format"] = repo.ExternalTrackerFormat
+
 		switch repo.ExternalTrackerStyle {
 		case markup.ISSUE_NAME_STYLE_ALPHANUMERIC:
 			repo.ExternalMetas["style"] = markup.ISSUE_NAME_STYLE_ALPHANUMERIC
 		default:
 			repo.ExternalMetas["style"] = markup.ISSUE_NAME_STYLE_NUMERIC
 		}
-
 	}
+
 	return repo.ExternalMetas
 }
 
@@ -550,8 +555,12 @@ func (repo *Repository) ComposeCompareURL(oldCommitID, newCommitID string) strin
 }
 
 func (repo *Repository) HasAccess(userID int64) bool {
-	has, _ := HasAccess(userID, repo, AccessModeRead)
-	return has
+	return Perms.Authorize(userID, repo.ID, AccessModeRead,
+		AccessModeOptions{
+			OwnerID: repo.OwnerID,
+			Private: repo.IsPrivate,
+		},
+	)
 }
 
 func (repo *Repository) IsOwnedBy(userID int64) bool {
@@ -2506,3 +2515,106 @@ func (repo *Repository) CreateNewBranch(oldBranch, newBranch string) (err error)
 
 	return nil
 }
+
+// Deprecated: Use Perms.SetRepoPerms instead.
+func (repo *Repository) refreshAccesses(e Engine, accessMap map[int64]AccessMode) (err error) {
+	newAccesses := make([]Access, 0, len(accessMap))
+	for userID, mode := range accessMap {
+		newAccesses = append(newAccesses, Access{
+			UserID: userID,
+			RepoID: repo.ID,
+			Mode:   mode,
+		})
+	}
+
+	// Delete old accesses and insert new ones for repository.
+	if _, err = e.Delete(&Access{RepoID: repo.ID}); err != nil {
+		return fmt.Errorf("delete old accesses: %v", err)
+	} else if _, err = e.Insert(newAccesses); err != nil {
+		return fmt.Errorf("insert new accesses: %v", err)
+	}
+	return nil
+}
+
+// refreshCollaboratorAccesses retrieves repository collaborations with their access modes.
+func (repo *Repository) refreshCollaboratorAccesses(e Engine, accessMap map[int64]AccessMode) error {
+	collaborations, err := repo.getCollaborations(e)
+	if err != nil {
+		return fmt.Errorf("getCollaborations: %v", err)
+	}
+	for _, c := range collaborations {
+		accessMap[c.UserID] = c.Mode
+	}
+	return nil
+}
+
+// recalculateTeamAccesses recalculates new accesses for teams of an organization
+// except the team whose ID is given. It is used to assign a team ID when
+// remove repository from that team.
+func (repo *Repository) recalculateTeamAccesses(e Engine, ignTeamID int64) (err error) {
+	accessMap := make(map[int64]AccessMode, 20)
+
+	if err = repo.getOwner(e); err != nil {
+		return err
+	} else if !repo.Owner.IsOrganization() {
+		return fmt.Errorf("owner is not an organization: %d", repo.OwnerID)
+	}
+
+	if err = repo.refreshCollaboratorAccesses(e, accessMap); err != nil {
+		return fmt.Errorf("refreshCollaboratorAccesses: %v", err)
+	}
+
+	if err = repo.Owner.getTeams(e); err != nil {
+		return err
+	}
+
+	maxAccessMode := func(modes ...AccessMode) AccessMode {
+		max := AccessModeNone
+		for _, mode := range modes {
+			if mode > max {
+				max = mode
+			}
+		}
+		return max
+	}
+
+	for _, t := range repo.Owner.Teams {
+		if t.ID == ignTeamID {
+			continue
+		}
+
+		// Owner team gets owner access, and skip for teams that do not
+		// have relations with repository.
+		if t.IsOwnerTeam() {
+			t.Authorize = AccessModeOwner
+		} else if !t.hasRepository(e, repo.ID) {
+			continue
+		}
+
+		if err = t.getMembers(e); err != nil {
+			return fmt.Errorf("getMembers '%d': %v", t.ID, err)
+		}
+		for _, m := range t.Members {
+			accessMap[m.ID] = maxAccessMode(accessMap[m.ID], t.Authorize)
+		}
+	}
+
+	return repo.refreshAccesses(e, accessMap)
+}
+
+func (repo *Repository) recalculateAccesses(e Engine) error {
+	if repo.Owner.IsOrganization() {
+		return repo.recalculateTeamAccesses(e, 0)
+	}
+
+	accessMap := make(map[int64]AccessMode, 10)
+	if err := repo.refreshCollaboratorAccesses(e, accessMap); err != nil {
+		return fmt.Errorf("refreshCollaboratorAccesses: %v", err)
+	}
+	return repo.refreshAccesses(e, accessMap)
+}
+
+// RecalculateAccesses recalculates all accesses for repository.
+func (repo *Repository) RecalculateAccesses() error {
+	return repo.recalculateAccesses(x)
+}

+ 6 - 4
internal/db/repo_branch.go

@@ -175,10 +175,12 @@ 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, AccessModeWrite)
-			if err != nil {
-				return fmt.Errorf("HasAccess [user_id: %d, repo_id: %d]: %v", userID, protectBranch.RepoID, err)
-			} else if !has {
+			if !Perms.Authorize(userID, repo.ID, AccessModeWrite,
+				AccessModeOptions{
+					OwnerID: repo.OwnerID,
+					Private: repo.IsPrivate,
+				},
+			) {
 				continue // Drop invalid user ID
 			}
 

+ 9 - 4
internal/db/repo_test.go

@@ -19,14 +19,19 @@ func TestRepository_ComposeMetas(t *testing.T) {
 
 	t.Run("no external tracker is configured", func(t *testing.T) {
 		repo.EnableExternalTracker = false
-		assert.Equal(t, map[string]string(nil), repo.ComposeMetas())
 
-		// Should be nil even if other settings are present
-		repo.ExternalTrackerStyle = markup.ISSUE_NAME_STYLE_NUMERIC
-		assert.Equal(t, map[string]string(nil), repo.ComposeMetas())
+		metas := repo.ComposeMetas()
+		assert.Equal(t, metas["repoLink"], repo.Link())
+
+		// Should no format and style if no external tracker is configured
+		_, ok := metas["format"]
+		assert.False(t, ok)
+		_, ok = metas["style"]
+		assert.False(t, ok)
 	})
 
 	t.Run("an external issue tracker is configured", func(t *testing.T) {
+		repo.ExternalMetas = nil
 		repo.EnableExternalTracker = true
 
 		// Default to numeric issue style

+ 6 - 4
internal/db/ssh_key.go

@@ -753,10 +753,12 @@ func DeleteDeployKey(doer *User, id int64) error {
 		if err != nil {
 			return fmt.Errorf("GetRepositoryByID: %v", err)
 		}
-		yes, err := HasAccess(doer.ID, repo, AccessModeAdmin)
-		if err != nil {
-			return fmt.Errorf("HasAccess: %v", err)
-		} else if !yes {
+		if !Perms.Authorize(doer.ID, repo.ID, AccessModeAdmin,
+			AccessModeOptions{
+				OwnerID: repo.OwnerID,
+				Private: repo.IsPrivate,
+			},
+		) {
 			return ErrKeyAccessDenied{doer.ID, key.ID, "deploy"}
 		}
 	}

+ 2 - 0
internal/db/testdata/backup/Access.golden.json

@@ -0,0 +1,2 @@
+{"ID":1,"UserID":1,"RepoID":11,"Mode":1}
+{"ID":2,"UserID":2,"RepoID":22,"Mode":2}

+ 58 - 16
internal/db/user.go

@@ -369,20 +369,22 @@ 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, AccessModeAdmin)
-	if err != nil {
-		log.Error("HasAccess: %v", err)
-	}
-	return has
+	return Perms.Authorize(u.ID, repo.ID, AccessModeAdmin,
+		AccessModeOptions{
+			OwnerID: repo.OwnerID,
+			Private: repo.IsPrivate,
+		},
+	)
 }
 
 // IsWriterOfRepo returns true if user has write access to given repository.
 func (u *User) IsWriterOfRepo(repo *Repository) bool {
-	has, err := HasAccess(u.ID, repo, AccessModeWrite)
-	if err != nil {
-		log.Error("HasAccess: %v", err)
-	}
-	return has
+	return Perms.Authorize(u.ID, repo.ID, AccessModeWrite,
+		AccessModeOptions{
+			OwnerID: repo.OwnerID,
+			Private: repo.IsPrivate,
+		},
+	)
 }
 
 // IsOrganization returns true if user is actually a organization.
@@ -937,15 +939,17 @@ func GetUserByID(id int64) (*User, error) {
 	return getUserByID(x, id)
 }
 
-// GetAssigneeByID returns the user with write access of repository by given ID.
+// GetAssigneeByID returns the user with read access of repository by given ID.
 func GetAssigneeByID(repo *Repository, userID int64) (*User, error) {
-	has, err := HasAccess(userID, repo, AccessModeRead)
-	if err != nil {
-		return nil, err
-	} else if !has {
+	if !Perms.Authorize(userID, repo.ID, AccessModeRead,
+		AccessModeOptions{
+			OwnerID: repo.OwnerID,
+			Private: repo.IsPrivate,
+		},
+	) {
 		return nil, ErrUserNotExist{args: map[string]interface{}{"userID": userID}}
 	}
-	return GetUserByID(userID)
+	return Users.GetByID(userID)
 }
 
 // GetUserByName returns a user by given name.
@@ -1171,3 +1175,41 @@ func UnfollowUser(userID, followID int64) (err error) {
 	}
 	return sess.Commit()
 }
+
+// GetRepositoryAccesses finds all repositories with their access mode where a user has access but does not own.
+func (u *User) GetRepositoryAccesses() (map[*Repository]AccessMode, error) {
+	accesses := make([]*Access, 0, 10)
+	if err := x.Find(&accesses, &Access{UserID: u.ID}); err != nil {
+		return nil, err
+	}
+
+	repos := make(map[*Repository]AccessMode, len(accesses))
+	for _, access := range accesses {
+		repo, err := GetRepositoryByID(access.RepoID)
+		if err != nil {
+			if IsErrRepoNotExist(err) {
+				log.Error("Failed to get repository by ID: %v", err)
+				continue
+			}
+			return nil, err
+		}
+		if repo.OwnerID == u.ID {
+			continue
+		}
+		repos[repo] = access.Mode
+	}
+	return repos, nil
+}
+
+// GetAccessibleRepositories finds repositories which the user has access but does not own.
+// If limit is smaller than 1 means returns all found results.
+func (user *User) GetAccessibleRepositories(limit int) (repos []*Repository, _ error) {
+	sess := x.Where("owner_id !=? ", user.ID).Desc("updated_unix")
+	if limit > 0 {
+		sess.Limit(limit)
+		repos = make([]*Repository, 0, limit)
+	} else {
+		repos = make([]*Repository, 0, 10)
+	}
+	return repos, sess.Join("INNER", "access", "access.user_id = ? AND access.repo_id = repository.id", user.ID).Find(&repos)
+}

+ 3 - 2
internal/markup/markup.go

@@ -145,7 +145,8 @@ func RenderSha1CurrentPattern(rawBytes []byte, urlPrefix string) []byte {
 		if com.StrTo(m).MustInt() > 0 {
 			return m
 		}
-		return fmt.Sprintf(`<a href="%s/commit/%s"><code>%s</code></a>`, urlPrefix, m, tool.ShortSHA1(string(m)))
+
+		return fmt.Sprintf(`<a href="%s/commit/%s"><code>%s</code></a>`, urlPrefix, m, tool.ShortSHA1(m))
 	}))
 }
 
@@ -160,7 +161,7 @@ func RenderSpecialLink(rawBytes []byte, urlPrefix string, metas map[string]strin
 
 	rawBytes = RenderIssueIndexPattern(rawBytes, urlPrefix, metas)
 	rawBytes = RenderCrossReferenceIssueIndexPattern(rawBytes, urlPrefix, metas)
-	rawBytes = RenderSha1CurrentPattern(rawBytes, urlPrefix)
+	rawBytes = RenderSha1CurrentPattern(rawBytes, metas["repoLink"])
 	return rawBytes
 }
 

+ 38 - 0
internal/markup/markup_test.go

@@ -215,3 +215,41 @@ func Test_RenderIssueIndexPattern(t *testing.T) {
 		})
 	})
 }
+
+func TestRenderSha1CurrentPattern(t *testing.T) {
+	metas := map[string]string{
+		"repoLink": "/someuser/somerepo",
+	}
+
+	tests := []struct {
+		desc   string
+		input  string
+		prefix string
+		expVal string
+	}{
+		{
+			desc:   "Full SHA (40 symbols)",
+			input:  "ad8ced4f57d9068cb2874557245be3c7f341149d",
+			prefix: metas["repoLink"],
+			expVal: `<a href="/someuser/somerepo/commit/ad8ced4f57d9068cb2874557245be3c7f341149d"><code>ad8ced4f57</code></a>`,
+		},
+		{
+			desc:   "Short SHA (8 symbols)",
+			input:  "ad8ced4f",
+			prefix: metas["repoLink"],
+			expVal: `<a href="/someuser/somerepo/commit/ad8ced4f"><code>ad8ced4f</code></a>`,
+		},
+		{
+			desc:   "9 digits",
+			input:  "123456789",
+			prefix: metas["repoLink"],
+			expVal: "123456789",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			assert.Equal(t, test.expVal, string(RenderSha1CurrentPattern([]byte(test.input), test.prefix)))
+		})
+	}
+}

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

@@ -45,11 +45,11 @@ func repoAssignment() macaron.Handler {
 		}
 		c.Repo.Owner = owner
 
-		r, err := db.GetRepositoryByName(owner.ID, reponame)
+		repo, err := db.Repos.GetByName(owner.ID, reponame)
 		if err != nil {
 			c.NotFoundOrError(err, "get repository by name")
 			return
-		} else if err = r.GetOwner(); err != nil {
+		} else if err = repo.GetOwner(); err != nil {
 			c.Error(err, "get owner")
 			return
 		}
@@ -57,12 +57,12 @@ func repoAssignment() macaron.Handler {
 		if c.IsTokenAuth && c.User.IsAdmin {
 			c.Repo.AccessMode = db.AccessModeOwner
 		} else {
-			mode, err := db.UserAccessMode(c.UserID(), r)
-			if err != nil {
-				c.Error(err, "get user access mode")
-				return
-			}
-			c.Repo.AccessMode = mode
+			c.Repo.AccessMode = db.Perms.AccessMode(c.UserID(), repo.ID,
+				db.AccessModeOptions{
+					OwnerID: repo.OwnerID,
+					Private: repo.IsPrivate,
+				},
+			)
 		}
 
 		if !c.Repo.HasAccess() {
@@ -70,7 +70,7 @@ func repoAssignment() macaron.Handler {
 			return
 		}
 
-		c.Repo.Repository = r
+		c.Repo.Repository = repo
 	}
 }
 

+ 7 - 1
internal/route/lfs/batch.go

@@ -44,6 +44,11 @@ func serveBatch(c *macaron.Context, owner *db.User, repo *db.Repository) {
 				actions = batchActions{
 					Upload: &batchAction{
 						Href: fmt.Sprintf("%s/%s", baseHref, obj.Oid),
+						Header: map[string]string{
+							// NOTE: git-lfs v2.5.0 sets the Content-Type based on the uploaded file.
+							// This ensures that the client always uses the designated value for the header.
+							"Content-Type": "application/octet-stream",
+						},
 					},
 					Verify: &batchAction{
 						Href: fmt.Sprintf("%s/verify", baseHref),
@@ -136,7 +141,8 @@ type batchError struct {
 }
 
 type batchAction struct {
-	Href string `json:"href"`
+	Href   string            `json:"href"`
+	Header map[string]string `json:"header,omitempty"`
 }
 
 type batchActions struct {

+ 55 - 4
internal/route/lfs/batch_test.go

@@ -6,6 +6,7 @@ package lfs
 
 import (
 	"bytes"
+	"encoding/json"
 	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
@@ -42,7 +43,7 @@ func Test_serveBatch(t *testing.T) {
 			name:          "unrecognized operation",
 			body:          `{"operation": "update"}`,
 			expStatusCode: http.StatusBadRequest,
-			expBody:       `{"message":"Operation not recognized"}` + "\n",
+			expBody:       `{"message": "Operation not recognized"}` + "\n",
 		},
 		{
 			name: "upload: contains invalid oid",
@@ -53,7 +54,25 @@ func Test_serveBatch(t *testing.T) {
 	{"oid": "ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f", "size": 123}
 ]}`,
 			expStatusCode: http.StatusOK,
-			expBody:       `{"transfer":"basic","objects":[{"oid":"bad_oid","size":123,"actions":{"error":{"code":422,"message":"Object has invalid oid"}}},{"oid":"ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f","size":123,"actions":{"upload":{"href":"https://gogs.example.com/owner/repo.git/info/lfs/objects/basic/ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f"},"verify":{"href":"https://gogs.example.com/owner/repo.git/info/lfs/objects/basic/verify"}}}]}` + "\n",
+			expBody: `{
+	"transfer": "basic",
+	"objects": [
+		{"oid": "bad_oid", "size":123, "actions": {"error": {"code": 422, "message": "Object has invalid oid"}}},
+		{
+			"oid": "ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f",
+			"size": 123,
+			"actions": {
+				"upload": {
+					"href": "https://gogs.example.com/owner/repo.git/info/lfs/objects/basic/ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f",
+					"header": {"Content-Type": "application/octet-stream"}
+				},
+				"verify": {
+					"href": "https://gogs.example.com/owner/repo.git/info/lfs/objects/basic/verify"
+				}
+			}
+		}
+	]
+}` + "\n",
 		},
 		{
 			name: "download: contains non-existent oid and mismatched size",
@@ -78,7 +97,26 @@ func Test_serveBatch(t *testing.T) {
 				},
 			},
 			expStatusCode: http.StatusOK,
-			expBody:       `{"transfer":"basic","objects":[{"oid":"bad_oid","size":123,"actions":{"error":{"code":404,"message":"Object does not exist"}}},{"oid":"ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f","size":123,"actions":{"error":{"code":422,"message":"Object size mismatch"}}},{"oid":"5cac0a318669fadfee734fb340a5f5b70b428ac57a9f4b109cb6e150b2ba7e57","size":456,"actions":{"download":{"href":"https://gogs.example.com/owner/repo.git/info/lfs/objects/basic/5cac0a318669fadfee734fb340a5f5b70b428ac57a9f4b109cb6e150b2ba7e57"}}}]}` + "\n",
+			expBody: `{
+	"transfer": "basic",
+	"objects": [
+		{"oid": "bad_oid", "size": 123, "actions": {"error": {"code": 404, "message": "Object does not exist"}}},
+		{
+			"oid": "ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f",
+			"size": 123,
+			"actions": {"error": {"code": 422, "message": "Object size mismatch"}}
+		},
+		{
+			"oid": "5cac0a318669fadfee734fb340a5f5b70b428ac57a9f4b109cb6e150b2ba7e57",
+			"size": 456,
+			"actions": {
+				"download": {
+					"href": "https://gogs.example.com/owner/repo.git/info/lfs/objects/basic/5cac0a318669fadfee734fb340a5f5b70b428ac57a9f4b109cb6e150b2ba7e57"
+				}
+			}
+		}
+	]
+}` + "\n",
 		},
 	}
 	for _, test := range tests {
@@ -100,7 +138,20 @@ func Test_serveBatch(t *testing.T) {
 			if err != nil {
 				t.Fatal(err)
 			}
-			assert.Equal(t, test.expBody, string(body))
+
+			var expBody bytes.Buffer
+			err = json.Indent(&expBody, []byte(test.expBody), "", "  ")
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			var gotBody bytes.Buffer
+			err = json.Indent(&gotBody, body, "", "  ")
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			assert.Equal(t, expBody.String(), gotBody.String())
 		})
 	}
 }

+ 16 - 4
internal/route/lfs/route.go

@@ -131,11 +131,18 @@ func authorize(mode db.AccessMode) macaron.Handler {
 			return
 		}
 
-		if !db.Perms.Authorize(actor.ID, repo, mode) {
+		if !db.Perms.Authorize(actor.ID, repo.ID, mode,
+			db.AccessModeOptions{
+				OwnerID: repo.OwnerID,
+				Private: repo.IsPrivate,
+			},
+		) {
 			c.Status(http.StatusNotFound)
 			return
 		}
 
+		log.Trace("[LFS] Authorized user %q to %q", actor.Name, username+"/"+reponame)
+
 		c.Map(owner) // NOTE: Override actor
 		c.Map(repo)
 	}
@@ -145,10 +152,15 @@ func authorize(mode db.AccessMode) macaron.Handler {
 // When not, response given "failCode" as status code.
 func verifyHeader(key, value string, failCode int) macaron.Handler {
 	return func(c *macaron.Context) {
-		if !strings.Contains(c.Req.Header.Get(key), value) {
-			c.Status(failCode)
-			return
+		vals := c.Req.Header.Values(key)
+		for _, val := range vals {
+			if strings.Contains(val, value) {
+				return
+			}
 		}
+
+		log.Trace("[LFS] HTTP header %q does not contain value %q", key, value)
+		c.Status(failCode)
 	}
 }
 

+ 2 - 2
internal/route/lfs/route_test.go

@@ -209,7 +209,7 @@ func Test_authorize(t *testing.T) {
 				},
 			},
 			mockPermsStore: &db.MockPermsStore{
-				MockAuthorize: func(userID int64, repo *db.Repository, desired db.AccessMode) bool {
+				MockAuthorize: func(userID, repoID int64, desired db.AccessMode, opts db.AccessModeOptions) bool {
 					return desired <= db.AccessModeRead
 				},
 			},
@@ -230,7 +230,7 @@ func Test_authorize(t *testing.T) {
 				},
 			},
 			mockPermsStore: &db.MockPermsStore{
-				MockAuthorize: func(userID int64, repo *db.Repository, desired db.AccessMode) bool {
+				MockAuthorize: func(userID, repoID int64, desired db.AccessMode, opts db.AccessModeOptions) bool {
 					return desired <= db.AccessModeRead
 				},
 			},

+ 3 - 1
internal/route/repo/commit.go

@@ -6,6 +6,7 @@ package repo
 
 import (
 	"path"
+	"time"
 
 	"github.com/gogs/git-module"
 
@@ -125,6 +126,7 @@ func Diff(c *context.Context) {
 
 	diff, err := gitutil.RepoDiff(c.Repo.GitRepo,
 		commitID, conf.Git.MaxDiffFiles, conf.Git.MaxDiffLines, conf.Git.MaxDiffLineChars,
+		git.DiffOptions{Timeout: time.Duration(conf.Git.Timeout.Diff) * time.Second},
 	)
 	if err != nil {
 		c.NotFoundOrError(gitutil.NewError(err), "get diff")
@@ -193,7 +195,7 @@ func CompareDiff(c *context.Context) {
 
 	diff, err := gitutil.RepoDiff(c.Repo.GitRepo,
 		afterCommitID, conf.Git.MaxDiffFiles, conf.Git.MaxDiffLines, conf.Git.MaxDiffLineChars,
-		git.DiffOptions{Base: beforeCommitID},
+		git.DiffOptions{Base: beforeCommitID, Timeout: time.Duration(conf.Git.Timeout.Diff) * time.Second},
 	)
 	if err != nil {
 		c.NotFoundOrError(gitutil.NewError(err), "get diff")

+ 6 - 1
internal/route/repo/http.go

@@ -165,7 +165,12 @@ Please create and use personal access token on user settings page`)
 		if isPull {
 			mode = db.AccessModeRead
 		}
-		if !db.Perms.Authorize(authUser.ID, repo, mode) {
+		if !db.Perms.Authorize(authUser.ID, repo.ID, mode,
+			db.AccessModeOptions{
+				OwnerID: repo.OwnerID,
+				Private: repo.IsPrivate,
+			},
+		) {
 			askCredentials(c, http.StatusForbidden, "User permission denied")
 			return
 		}

+ 3 - 2
internal/route/repo/pull.go

@@ -8,6 +8,7 @@ import (
 	"net/http"
 	"path"
 	"strings"
+	"time"
 
 	"github.com/unknwon/com"
 	log "unknwon.dev/clog/v2"
@@ -354,7 +355,7 @@ func ViewPullFiles(c *context.Context) {
 
 	diff, err := gitutil.RepoDiff(diffGitRepo,
 		endCommitID, conf.Git.MaxDiffFiles, conf.Git.MaxDiffLines, conf.Git.MaxDiffLineChars,
-		git.DiffOptions{Base: startCommitID},
+		git.DiffOptions{Base: startCommitID, Timeout: time.Duration(conf.Git.Timeout.Diff) * time.Second},
 	)
 	if err != nil {
 		c.Error(err, "get diff")
@@ -575,7 +576,7 @@ func PrepareCompareDiff(
 
 	diff, err := gitutil.RepoDiff(headGitRepo,
 		headCommitID, conf.Git.MaxDiffFiles, conf.Git.MaxDiffLines, conf.Git.MaxDiffLineChars,
-		git.DiffOptions{Base: meta.MergeBase},
+		git.DiffOptions{Base: meta.MergeBase, Timeout: time.Duration(conf.Git.Timeout.Diff) * time.Second},
 	)
 	if err != nil {
 		c.Error(err, "get repository diff")

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