Browse Source

Merge remote-tracking branch 'upstream/master' into team-grant-all-repos

pull/8688/head
David Svantesson 4 years ago
parent
commit
3f291df116
100 changed files with 2690 additions and 1129 deletions
  1. 1
    0
      .gitignore
  2. 3
    0
      .golangci.yml
  3. 292
    7
      CHANGELOG.md
  4. 23
    2
      CONTRIBUTING.md
  5. 1
    0
      MAINTAINERS
  6. 6
    2
      Makefile
  7. 3
    2
      README.md
  8. 2
    1
      README_ZH.md
  9. 10
    9
      cmd/admin.go
  10. 2
    0
      cmd/hook.go
  11. 2
    0
      cmd/serv.go
  12. 28
    9
      cmd/web.go
  13. 18
    26
      cmd/web_graceful.go
  14. 18
    0
      cmd/web_windows.go
  15. 1
    1
      contrib/pr/checkout.go
  16. 36
    2
      contrib/systemd/gitea.service
  17. 75
    6
      custom/conf/app.ini.sample
  18. 1
    0
      docker/root/etc/s6/openssh/setup
  19. 2
    2
      docker/root/etc/templates/sshd_config
  20. 8
    0
      docs/content/doc/advanced/api-usage.en-us.md
  21. 60
    2
      docs/content/doc/advanced/config-cheat-sheet.en-us.md
  22. 13
    1
      docs/content/doc/advanced/config-cheat-sheet.zh-cn.md
  23. 1
    1
      docs/content/doc/advanced/external-renderers.en-us.md
  24. 162
    0
      docs/content/doc/advanced/signing.en-us.md
  25. 1
    0
      docs/content/doc/features/comparison.en-us.md
  26. 81
    1
      docs/content/doc/features/webhooks.en-us.md
  27. 1
    1
      docs/content/doc/help/seek-help.en-us.md
  28. 1
    1
      docs/content/doc/help/seek-help.zh-cn.md
  29. 13
    13
      docs/content/doc/installation/from-binary.en-us.md
  30. 3
    3
      docs/content/doc/installation/from-source.en-us.md
  31. 33
    0
      docs/content/doc/usage/email-setup.md
  32. 19
    3
      docs/content/doc/usage/fail2ban-setup.md
  33. 26
    0
      docs/content/doc/usage/git-lfs-support.md
  34. 2
    0
      docs/content/doc/usage/https-support.md
  35. 68
    0
      docs/content/doc/usage/reverse-proxies.en-us.md
  36. 6
    12
      go.mod
  37. 14
    32
      go.sum
  38. 35
    0
      integrations/api_helper_for_declarative_test.go
  39. 2
    1
      integrations/api_pull_test.go
  40. 1
    1
      integrations/api_repo_file_create_test.go
  41. 1
    1
      integrations/api_repo_file_update_test.go
  42. 0
    1
      integrations/api_team_user_test.go
  43. 1
    1
      integrations/api_user_heatmap_test.go
  44. 31
    0
      integrations/git_helper_for_declarative_test.go
  45. 48
    14
      integrations/git_test.go
  46. 252
    0
      integrations/gpg_git_test.go
  47. 7
    6
      integrations/issue_test.go
  48. 5
    0
      integrations/lfs_getobject_test.go
  49. 1
    1
      integrations/migration-test/migration_test.go
  50. 3
    0
      integrations/mssql.ini.tmpl
  51. 3
    0
      integrations/mysql.ini.tmpl
  52. 3
    0
      integrations/mysql8.ini.tmpl
  53. 3
    0
      integrations/pgsql.ini.tmpl
  54. 1
    1
      integrations/repofiles_delete_test.go
  55. 2
    2
      integrations/repofiles_update_test.go
  56. 3
    0
      integrations/sqlite.ini
  57. 49
    0
      models/access.go
  58. 37
    214
      models/action.go
  59. 1
    52
      models/action_test.go
  60. 1
    1
      models/attachment.go
  61. 23
    1
      models/branches.go
  62. 1
    1
      models/commit_status.go
  63. 125
    18
      models/external_login_user.go
  64. 1
    1
      models/fixtures/action.yml
  65. 0
    3
      models/fixtures/pull_request.yml
  66. 42
    1
      models/fixtures/repository.yml
  67. 295
    68
      models/gpg_key.go
  68. 2
    1
      models/graph.go
  69. 1
    1
      models/graph_test.go
  70. 174
    259
      models/issue.go
  71. 55
    85
      models/issue_assignees.go
  72. 3
    3
      models/issue_assignees_test.go
  73. 49
    6
      models/issue_comment.go
  74. 10
    6
      models/issue_label.go
  75. 13
    4
      models/issue_lock.go
  76. 69
    57
      models/issue_milestone.go
  77. 3
    3
      models/issue_milestone_test.go
  78. 1
    1
      models/issue_reaction.go
  79. 34
    49
      models/issue_test.go
  80. 1
    1
      models/issue_tracked_time.go
  81. 0
    38
      models/issue_user.go
  82. 40
    73
      models/issue_xref.go
  83. 1
    1
      models/lfs_lock.go
  84. 1
    1
      models/login_source.go
  85. 1
    1
      models/migrate.go
  86. 18
    4
      models/migrations/migrations.go
  87. 83
    0
      models/migrations/v100.go
  88. 19
    0
      models/migrations/v101.go
  89. 15
    0
      models/migrations/v102.go
  90. 18
    0
      models/migrations/v103.go
  91. 34
    0
      models/migrations/v104.go
  92. 25
    0
      models/migrations/v105.go
  93. 1
    1
      models/migrations/v13.go
  94. 1
    1
      models/migrations/v14.go
  95. 1
    1
      models/migrations/v15.go
  96. 1
    1
      models/migrations/v16.go
  97. 1
    1
      models/migrations/v17.go
  98. 1
    1
      models/migrations/v18.go
  99. 1
    1
      models/migrations/v19.go
  100. 0
    0
      models/migrations/v20.go

+ 1
- 0
.gitignore View File

@@ -77,3 +77,4 @@ prime/
*.snap
*.snap-build
*_source.tar.bz2
.DS_Store

+ 3
- 0
.golangci.yml View File

@@ -19,6 +19,9 @@ linters:
disable-all: true
fast: false

run:
timeout: 3m

linters-settings:
gocritic:
disabled-checks:

+ 292
- 7
CHANGELOG.md View File

@@ -4,6 +4,291 @@ This changelog goes through all the changes that have been made in each release
without substantial changes to our git log; to see the highlights of what has
been added to each release, please refer to the [blog](https://blog.gitea.io).

## [1.10.0-RC1](https://github.com/go-gitea/gitea/releases/tag/v1.10.0-rc1) - 2019-10-14
* BREAKING
* Remove legacy handling of drone token (#8191)
* Change repo search to use exact match for topic search. (#7941)
* Add pagination for admin api get orgs and fix only list public orgs bug (#7742)
* Implement the ability to change the ssh port to match what is in the gitea config (#7286)
* FEATURE
* Org/Members: display 2FA members states + optimize sql requests (#7621)
* SetDefaultBranch on pushing to empty repository (#7610)
* Adds side-by-side diff for images (#6784)
* API method to list all commits of a repository (#6408)
* Password Complexity Checks (#6230)
* Add option to initialize repository with labels (#6061)
* Add additional password hash algorithms (#6023)
* BUGFIXES
* Fix errors in create org UI regarding team access permission (#8506)
* Fix bug on FindExternalUsersByProvider (#8504)
* Create .ssh dir as necessary (#8486)
* IsBranchExist: return false if provided name is empty (#8485)
* Making openssh listen on SSH_LISTEN_PORT not SSH_PORT (#8477)
* Add check for empty set when dropping indexes during migration (#8471)
* LFS files are relative to LFS content path, ensure that when deleting they are made relative to this (#8455)
* Ensure Request Body Readers are closed in LFS server (#8454)
* Fix template bug on mirror repository setting page (#8438)
* Fix migration v96 to keep issue attachments (#8435)
* Update strk.kbt.io/projects/go/libravatar to latest (#8429)
* Singular form for files that has only one line (#8416)
* Check for either escaped or unescaped wiki filenames (#8408)
* Allow users with explicit read access to give approvals (#8382)
* Fix editor commit to new branch if PR disabled (#8375)
* readd .markdown class to all markup renderers (#8357)
* Upgrade xorm to v0.7.9 to fix some bugs (#8354)
* Fix column name ambiguity in GetUserIssueStats() (#8347)
* Change general form binding to gogs form (#8334)
* Fix pull request commit status in user dashboard list (#8321)
* Fix repo_admin_change_team_access always checked in org settings (#8319)
* Update to github.com/lafriks/xormstore@v1.3.0 (#8317)
* Show correct commit status in PR list (#8316)
* Bugfix for image compare and minor improvements to image compare (#8289)
* Update xorm (#8286)
* Fix API for edit and delete release attachment (#8285)
* Fix nil object access in some conditions when parsing cross references (#8281)
* Fix label count (#8267)
* Only show teams access for organization repositories on collaboration setting page (#8265)
* Test more reserved usernames (#8263)
* Rewrite reference processing code in preparation for opening/closing from comment references (#8261)
* Fix assets key on release webhook (#8253)
* Allow registration when button is hidden (#8237)
* Fix release API URL generation (#8234)
* Fix milestone num_issues (#8221)
* MS Teams webhook misses commit messages (#8209)
* Fix data race (#8204)
* Fix team user api (#8172)
* Fix pull merge 500 error caused by git-fetch breaking behaviors (#8161)
* Make show private icon when repo avatar set (#8144)
* Add reviewers as participants (#8121)
* Fix Go 1.13 private repository go get issue (#8112)
* feat: highlight issue references with : (#8101)
* Make AllowedUsers configurable in sshd_config (#8094)
* Strict name matching for Repository.GetTagID() (#8074)
* Avoid ambiguity of branch/directory names for the git-diff-tree command (#8066)
* Add change title notification for issues (#8061)
* [ssh] fix the config specification in the authorized_keys template (#8031)
* Fix reading git notes from nested trees (#8026)
* Fixes synchronize tags to releases for repository - makes sure we are only getting tag refs (#7990)
* Fix adding default Telegram webhook (#7972)
* Run CORS handler first for /api routes (#7967)
* Abort synchronization from LDAP source if there is some error. (#7960)
* Fix wrong sender when send slack webhook (#7918)
* Fix bug when migrating a private repository (#7917)
* Evaluate emojis in commit messages in list view (#7906)
* Fix upload file type check (#7890)
* lfs/lock: round locked_at timestamp to second (#7872)
* fix non existent milestone with 500 error instead of 404 (#7867)
* gpg/bugfix: Use .ExpiredUnix.IsZero to display green color of forever valid gpg key (#7846)
* Fix duplicate call of webhook (#7821)
* Enable switching to a different source branch when PR already exists (#7819)
* Convert files to utf-8 for indexing (#7814)
* Do not fetch all refs in pull-request compare (#7797)
* Fix multiple bugs with statuses endpoints at API (#7785)
* Restore functionality for early gits (#7775)
* Fix Slack webhook fork message (#7774)
* Rewrite existing repo units if setting is not included in api body (#7763)
* Fix rename failed when rewrite public keys (#7761)
* Fix approvals counting (#7757)
* Add migration step to remove old repo_indexer_status orphaned records (#7746)
* Fix repo_index_status lingering when deleting a repository (#7734)
* Remove camel case tokenization from repo indexer (#7733)
* Fix milestone completness calculation when migrating (#7725)
* Regression: Include "executable" files in the index, as they are not necessarily … (#7718)
* Fixes indexed repos keeping outdated indexes when files grow too large (#7712)
* Skip non-regular files (e.g. submodules) on repo indexing (#7711)
* Fix dropTableColumns sqlite implementation (#7710)
* Update gopkg.in/src-d/go-git.v4 to v4.13.1 (#7705)
* improve branches list performance and fix protected branch icon when no-login (#7695)
* Correct wrong datetime format for git (#7689)
* Move add to hook queue for created repo to outside xorm session. (#7675)
* sugestion to use range .Branches (#7674)
* Fix bug on migrating milestone from github (#7665)
* hide delete/restore button on archived repos (#7658)
* css: use flex to fix floating paginate (#7656)
* Fix syntax highlight initialization (#7617)
* Fix panic on push at - Merging pull request causes 500 error (#7615)
* Make PKCS8, PEM and SSH2 keys work (#7600)
* Fix mistake in arc-green.less split-diff css code. (#7587)
* Handle ErrUserProhibitLogin in http git (#7586)
* Fix bug create/edit wiki pages when code master branch protected (#7580)
* Fixes Malformed URLs in API git/commits response (#7565)
* Fix file header overflow in file and blame views (#7562)
* Improve SSH key parser to handle newlines in keys (#7522)
* Fix empty commits now showing in repo overview (#7521)
* Fix repository's pull request count error (#7518)
* Fix markdown invoke sequence (#7513)
* Remove duplicated webhook trigger (#7511)
* Update User.NumRepos atomically in createRepository (#7493)
* Fix settings page of repo you aren't admin print error - Settings pages giving UnitType error message (#7482)
* Fix redirection after file edit - Handles all redirects for Web UI File CRUD (#7478)
* cmd/serv: actually exit after fatal errors (#7458)
* Fix an issue with some pages throwing 'not defined' js exceptions (#7450)
* fix Dropzone.js integration (#7445)
* Fix regex for issues in commit messages (#7444)
* Diff: Fix indentation on unhighlighted code (#7435)
* Only show "New Pull Request" button if repo allows pulls (#7426)
* Upgrade macaron/captcha to fix random error problem (#7407)
* create class for inline positioned lists (#7393)
* Fetch refs for successful testing for tag (#7388)
* add missing template variable on organisation settings (#7385)
* fix post parameter - on issue list - unset assignee (#7380)
* fix/define autochecked checkboxes on issue list in firefox (#7320)
* only return head: null if source branch was deleted (#6705)
* ENHANCEMENT
* Add nofollow to sign in links (#8509)
* vendor: update mvdan.cc/xurls/v2 to v2.1.0 (#8495)
* Update milestone issues numbers when save milestone and other code improvements (#8411)
* Add extra user information when migrating release (#8331)
* Require overall success if no context is given for status check (#8318)
* Transaction-aware retry create issue to cope with duplicate keys (#8307)
* Change link on issue milestone (#8246)
* Alwaywas return local url for users avatar (#8245)
* Move some milestone functions to a standalone package (#8213)
* Move create issue comment to comments package (#8212)
* Disable max height property of comment textarea (#8203)
* Add 'Mentioning you' group to /issues page (#8201)
* oauth2 with remote Gitea (#8149)
* Reference issues from pull requests and other issues (#8137)
* Fix webhooks to use proxy from environment (#8116)
* Add merged commit id on pull view when it's merged (#8062)
* Add teams to repo on collaboration page. (#8045)
* Update swagger to 0.20.1 (#8010)
* Make link last commit massages in repository home page and commit tables (#8006)
* Add API endpoint for accessing repo topics (#7963)
* Include description in repository search (#7942)
* Use gitea forked macaron (#7933)
* Fix pull creation with empty changes (#7920)
* Allow token as authorization for accessing attachments (#7909)
* Retry create issue to cope with duplicate keys (#7898)
* Move git diff codes from models to services/gitdiff (#7889)
* migrate gplus to google oauth2 provider (#7885)
* Remove unique filter from repo indexer analyzer. (#7878)
* Detect delimiter in CSV rendering (#7869)
* Import topics during migration (#7851)
* Move CreateReview to modules/pull (#7841)
* vendor: update pdf.js to v2.1.266 (#7834)
* Support SSH_LISTEN_PORT env var in docker app.ini template (#7829)
* Add Ability for User to Customize Email Notification Frequency (#7813)
* Move database settings from models to setting (#7806)
* Display ui time with customize time location (#7792)
* Implement webhook branch filter (#7791)
* Restrict repository indexing by glob match (#7767)
* Api: advanced settings for repository (external wiki, issue tracker etc.) (#7756)
* Update migrated repositories' issues/comments/prs poster id if user has a github external user saved (#7751)
* deps: Upgrade gopkg.in/editorconfig/editorconfig-core-go.v1 (#7749)
* Apply emoji on commit graph page (#7743)
* Add a lot of extension to language mappings for syntax highlights (#7741)
* Add SQL execution on log and indexes on table repository and comment (#7740)
* Set DB connection error level to error (#7724)
* Check commit message hashes before making links (#7713)
* remove unnecessary fmt on generate bindata (#7706)
* Fix specific highlighting (CMakeLists.txt ...) (#7686)
* Add file status on API (#7671)
* Add support for DEFAULT_ORG_MEMBER_VISIBLE (#7669)
* Provide links in commit summaries in commits table/view list (#7659)
* Change length of some repository's columns (#7652)
* Move commit repo action from models to repofiles package (#7645)
* fix wrong email when use gitea as OAuth2 provider (#7640)
* [Branch View] add download button (#7604)
* Update to xorm@v0.7.4 (#7596)
* use 403 instead of 401 for ErrUserProhibitLogin (#7591)
* Removed unnecessary conversions (#7557)
* Un-lambda base.FileSize (#7556)
* Added missing error checks in tests (#7554)
* Move create release from models to a standalone package (#7539)
* Make default branch name link to default branch (#7519)
* Added total count of contributions to heatmap (#7517)
* Move mirror to a standalone package from models (#7486)
* Move models.PushUpdate to repofiles.PushUpdate (#7485)
* Include thread related headers in issue/coment mail (#7484)
* Refuse merge until all required status checks success (#7481)
* convert all js var to let/const (#7464)
* Only create branches for opened pull requestes when migrating from github (#7463)
* jQuery 3 (#7425)
* Add notification placeholder (#7409)
* Search Commits via Commit Hash (#7400)
* Move status table to cron package (#7370)
* wiki - page revisions list (#7369)
* Display original author and URL information when showing migrated issues/comments (#7352)
* Refactor filetype is not allowed errors (#7309)
* switch to use gliderlabs/ssh for builtin server (#7250)
* Remove settting dependency on modules/session (#7237)
* Move all mail related codes from models to services/mailer (#7200)
* Support git.PATH entry in app.ini (#6772)
* Support setting cookie domain (#6288)
* Move migrating repository from frontend to backend (#6200)
* Delete releases attachments if release is deleted (#6068)
* SECURITY
* Ignore mentions for users with no access (#8395)
* Be more strict with git arguments (#7715)
* reserve .well-known username (#7637)
* TRANSLATION
* Latvian translation for home page (#8468)
* Add home template italian translation (#8352)
* fix misprint (#7452)
* BUILD
* use go 1.13 (#8088)
* MISC
* add file line count info on UI (#8396)
* Make issues page left menu 100% width and add reponame as title attribute (#8359)
* [arc-green] white on hover for active menu items (#8344)
* Move ref (branch or tag) location on issue list page (#8157)
* apply emoji on dashboard issue list labels (#8156)
* 1148: Take up the full width when viewing the diff in split view. (#8114)
* Display description of 'make this repo private' as help text, not as tooltip (#8097)
* Fixes deformed emoji in pull request reviews (#8047)
* Add strike to old header on comment (#8046)
* Add tooltip for the visibility checkbox in /repo/create (#8025)
* Update github.com/lafriks/xormstore and tidy up mod.go (#8020)
* keep blame view buttons sequence consistent with normal view when view a file (#8007)
* Use "Pull Request" instead of "Merge Request" (#8003)
* Move line number to :before attr to hide from search on browser (#8002)
* Changed black color to white for (read) number label on issue list page (#8000)
* [Branch View] show "New Pull Request" Button only if posible (#7977)
* Fix hook problem by only setting the git environment variables if we are passed them (#7854)
* Prevent Commit Status and Message From Overflowing On Branch Page (#7800)
* Fix global search result CSS, misc CSS tweaks (#7789)
* Tweak label border CSS (#7739)
* Fix create menu item widths (#7708)
* Extract the username and password from the mirror url (#7651)
* [Branch View] Delete duplicate protection symbol (#7624)
* [Branch View] Delete Table Header (#7622)
* [Branch View] icons to buttons (#7602)
* update js dependencies (#7462)
* Add Extra Info to Branches Page (#7461)
* Bump lodash from 4.17.11 to 4.17.14 (#7459)
* wiki history improvements (#7391)
* ui fixes - compare view and archieved repo issues (#7345)
* dark theme scrollbars (#7269)
* wiki - editor - add buttons 'inline code', 'empty checkbox', 'checked checkbox' (#7243)
* Fix Statuses API only shows first 10 statuses: Add paging and extend API GetCommitStatuses (#7141)

## [1.9.4](https://github.com/go-gitea/gitea/releases/tag/v1.9.4) - 2019-10-08
* BUGFIXES
* Highlight issue references (#8101) (#8404)
* Fix bug when migrating a private repository #7917 (#8403)
* Change general form binding to gogs form (#8334) (#8402)
* Fix editor commit to new branch if PR disabled (#8375) (#8401)
* Fix milestone num_issues (#8221) (#8400)
* Allow users with explicit read access to give approvals (#8398)
* Fix commit status in PR #8316 and PR #8321 (#8339)
* Fix API for edit and delete release attachment (#8290)
* Fix assets on release webhook (#8283)
* Fix release API URL generation (#8239)
* Allow registration when button is hidden (#8238)
* MS Teams webhook misses commit messages (backport v1.9) (#8225)
* Fix data race (#8206)
* Fix pull merge 500 error caused by git-fetch breaking behaviors (#8194)
* Fix the SSH config specification in the authorized_keys template (#8193)
* Fix reading git notes from nested trees (#8189)
* Fix team user api (#8172) (#8188)
* Add reviewers as participants (#8124)
* BUILD
* Use vendored go-swagger (#8087) (#8165)
* Fix version-validation for GO 1.13 (go-macaron/cors) (#8389)
* MISC
* Make show private icon when repo avatar set (#8144) (#8175)

## [1.9.3](https://github.com/go-gitea/gitea/releases/tag/v1.9.3) - 2019-09-06
* BUGFIXES
* Fix go get from a private repository with Go 1.13 (#8100)
@@ -96,20 +381,20 @@ been added to each release, please refer to the [blog](https://blog.gitea.io).
* Move add to hook queue for created repo to outside xorm session. (#7682) (#7675)
* Show protection symbol if needed on default branch (#7660) (#7668)
* Hide delete/restore button on archived repos (#7660)
* Fix bug on migrating milestone from github (#7665) (#7666)
* Fix bug on migrating milestone from github (#7665) (#7666)
* Use flex to fix floating paginate (#7656) (#7662)
* Change length of some repository's columns (#7652) (#7655)
* Fix wrong email when use gitea as OAuth2 provider (#7640) (#7647)
* Fix syntax highlight initialization (#7617) (#7626)
* Fix syntax highlight initialization (#7617) (#7626)
* Fix bug create/edit wiki pages when code master branch protected (#7580) (#7623)
* Fix panic on push at #7611 (#7615) (#7618)
* Handle ErrUserProhibitLogin in http git (#7586, #7591) (#7590)
* Handle ErrUserProhibitLogin in http git (#7586, #7591) (#7590)
* Fix color of split-diff view in dark theme (#7587) (#7589)
* Fix file header overflow in file and blame views (#7562) (#7579)
* Fix file header overflow in file and blame views (#7562) (#7579)
* Malformed URLs in API git/commits response (#7565) (#7567)
* Fix empty commits now showing in repo overview (#7521) (#7563)
* Fix repository's pull request count error (#7518) (#7524)
* Remove duplicated webhook trigger (#7511) (#7516)
* Fix repository's pull request count error (#7518) (#7524)
* Remove duplicated webhook trigger (#7511) (#7516)
* Handles all redirects for Web UI File CRUD (#7478) (#7507)
* Fix regex for issues in commit messages (#7444) (#7466)
* cmd/serv: actually exit after fatal errors (#7458) (#7460)
@@ -710,7 +995,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.io).
## [1.7.5](https://github.com/go-gitea/gitea/releases/tag/v1.7.5) - 2019-03-27
* BUGFIXES
* Fix unitTypeCode not being used in accessLevelUnit (#6419) (#6423)
* Fix bug where manifest.json was being requested without cookies and continuously creating new sessions (#6372) (#6383)
* Fix bug where manifest.json was being requested without cookies and continuously creating new sessions (#6372) (#6383)
* Fix ParsePatch function to work with quoted diff --git strings (#6323) (#6332)

## [1.7.4](https://github.com/go-gitea/gitea/releases/tag/v1.7.4) - 2019-03-12

+ 23
- 2
CONTRIBUTING.md View File

@@ -11,6 +11,7 @@
- [Translation](#translation)
- [Code review](#code-review)
- [Styleguide](#styleguide)
- [Design guideline](#design-guideline)
- [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco)
- [Release Cycle](#release-cycle)
- [Maintainers](#maintainers)
@@ -71,13 +72,15 @@ Here's how to run the test suite:

- Install the correct version of the drone-cli package. As of this
writing, the correct drone-cli version is
[1.1.0](https://docs.drone.io/cli/install/).
[1.2.0](https://docs.drone.io/cli/install/).
- Ensure you have enough free disk space. You will need at least
15-20 Gb of free disk space to hold all of the containers drone
creates (a default AWS or GCE disk size won't work -- see
[#6243](https://github.com/go-gitea/gitea/issues/6243)).
- Change into the base directory of your copy of the gitea repository,
and run `drone exec --event pull_request`.
- At the moment `drone exec` doesn't support the Docker Toolbox on Windows 10
(see [drone-cli#135](https://github.com/drone/drone-cli/issues/135))

The drone version, command line, and disk requirements do change over
time (see [#4053](https://github.com/go-gitea/gitea/issues/4053) and
@@ -118,6 +121,8 @@ An exception are the tools to build the CSS and images.
- To build Images: ImageMagick, inkscape and zopflipng binaries must be
available in your `PATH` to run `make generate-images`.

For more details on how to generate files, build and test Gitea, see the [hacking instructions](https://docs.gitea.io/en-us/hacking-on-gitea/)

## Code review

Changes to Gitea must be reviewed before they are accepted—no matter who
@@ -157,6 +162,22 @@ import (
)
```

## Design guideline

To maintain understandable code and avoid circular dependencies it is important to have a good structure of the code. The gitea code is divided into the following parts:

- **integration:** Integrations tests
- **models:** Contains the data structures used by xorm to construct database tables. It also contains supporting functions to query and update the database. Dependecies to other code in Gitea should be avoided although some modules might be needed (for example for logging).
- **models/fixtures:** Sample model data used in integration tests.
- **models/migrations:** Handling of database migrations between versions. PRs that changes a database structure shall also have a migration step.
- **modules:** Different modules to handle specific functionality in Gitea.
- **public:** Frontend files (javascript, images, css, etc.)
- **routers:** Handling of server requests. As it uses other Gitea packages to serve the request, other packages (models, modules or services) shall not depend on routers
- **services:** Support functions for common routing operations. Uses models and modules to handle the request.
- **templates:** Golang templates for generating the html output.
- **vendor:** External code that Gitea depends on.


## Developer Certificate of Origin (DCO)

We consider the act of contributing to the code by submitting a Pull
@@ -283,7 +304,7 @@ be reviewed by two maintainers and must pass the automatic tests.
* Add a tag as `git tag -s -F release.notes v$vmaj.$vmin.$`, release.notes file could be a temporary file to only include the changelog this version which you added to `CHANGELOG.md`.
* And then push the tag as `git push origin v$vmaj.$vmin.$`. Drone CI will automatically created a release and upload all the compiled binary. (But currently it didn't add the release notes automatically. Maybe we should fix that.)
* If needed send PR for changelog on branch `master`.
* Send PR to [blog repository](https://github.com/go-gitea/blog) announcing the release.
* Send PR to [blog repository](https://gitea.com/gitea/blog) announcing the release.

## Copyright


+ 1
- 0
MAINTAINERS View File

@@ -33,3 +33,4 @@ silverwind <me@silverwind.io> (@silverwind)
Gary Kim <gary@garykim.dev> (@gary-kim)
Guillermo Prandi <gitea.maint@mailfilter.com.ar> (@guillep2k)
Mura Li <typeless@ctli.io> (@typeless)
6543 <6543@obermui.de> (@6543)

+ 6
- 2
Makefile View File

@@ -168,6 +168,10 @@ fmt-check:
test:
GO111MODULE=on $(GO) test -mod=vendor -tags='sqlite sqlite_unlock_notify' $(PACKAGES)

.PHONY: test\#%
test\#%:
GO111MODULE=on $(GO) test -mod=vendor -tags='sqlite sqlite_unlock_notify' -run $* $(PACKAGES)

.PHONY: coverage
coverage:
@hash gocovmerge > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
@@ -515,6 +519,6 @@ pr:
golangci-lint:
@hash golangci-lint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
export BINARY="golangci-lint"; \
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(GOPATH)/bin v1.18.0; \
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(GOPATH)/bin v1.20.0; \
fi
golangci-lint run --deadline=3m
golangci-lint run

+ 3
- 2
README.md View File

@@ -1,6 +1,6 @@
[简体中文](https://github.com/go-gitea/gitea/blob/master/README_ZH.md)

# Gitea - Git with a cup of tea
<h1> <img src="https://raw.githubusercontent.com/go-gitea/gitea/master/public/img/gitea-192.png" alt="logo" width="30" height="30"> Gitea - Git with a cup of tea</h1>

[![Build Status](https://drone.gitea.io/api/badges/go-gitea/gitea/status.svg)](https://drone.gitea.io/go-gitea/gitea)
[![Join the Discord chat at https://discord.gg/NsatcWJ](https://img.shields.io/discord/322538954119184384.svg)](https://discord.gg/NsatcWJ)
@@ -10,8 +10,9 @@
[![GoDoc](https://godoc.org/code.gitea.io/gitea?status.svg)](https://godoc.org/code.gitea.io/gitea)
[![GitHub release](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest)
[![Help Contribute to Open Source](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea)
[![Become a backer/sponsor of gitea](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backer&color=brightgreen)](https://opencollective.com/gitea)
[![Become a backer/sponsor of gitea](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea)

## Purpose


+ 2
- 1
README_ZH.md View File

@@ -9,8 +9,9 @@
[![Go Report Card](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea)
[![GoDoc](https://godoc.org/code.gitea.io/gitea?status.svg)](https://godoc.org/code.gitea.io/gitea)
[![GitHub release](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest)
[![Become a backer/sponsor of gitea](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backer&color=brightgreen)](https://opencollective.com/gitea)
[![Become a backer/sponsor of gitea](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea)

## 目标


+ 10
- 9
cmd/admin.go View File

@@ -13,9 +13,9 @@ import (

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth/oauth2"
"code.gitea.io/gitea/modules/generate"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
pwd "code.gitea.io/gitea/modules/password"
"code.gitea.io/gitea/modules/setting"

"github.com/urfave/cli"
@@ -233,7 +233,9 @@ func runChangePassword(c *cli.Context) error {
if err := initDB(); err != nil {
return err
}

if !pwd.IsComplexEnough(c.String("password")) {
return errors.New("Password does not meet complexity requirements")
}
uname := c.String("username")
user, err := models.GetUserByName(uname)
if err != nil {
@@ -243,6 +245,7 @@ func runChangePassword(c *cli.Context) error {
return err
}
user.HashPassword(c.String("password"))

if err := models.UpdateUserCols(user, "passwd", "salt"); err != nil {
return err
}
@@ -275,26 +278,24 @@ func runCreateUser(c *cli.Context) error {
fmt.Fprintf(os.Stderr, "--name flag is deprecated. Use --username instead.\n")
}

var password string
if err := initDB(); err != nil {
return err
}

var password string
if c.IsSet("password") {
password = c.String("password")
} else if c.IsSet("random-password") {
var err error
password, err = generate.GetRandomString(c.Int("random-password-length"))
password, err = pwd.Generate(c.Int("random-password-length"))
if err != nil {
return err
}

fmt.Printf("generated random password is '%s'\n", password)
} else {
return errors.New("must set either password or random-password flag")
}

if err := initDB(); err != nil {
return err
}

// always default to true
var changePassword = true


+ 2
- 0
cmd/hook.go View File

@@ -66,6 +66,7 @@ func runHookPreReceive(c *cli.Context) error {
reponame := os.Getenv(models.EnvRepoName)
userID, _ := strconv.ParseInt(os.Getenv(models.EnvPusherID), 10, 64)
prID, _ := strconv.ParseInt(os.Getenv(models.ProtectedBranchPRID), 10, 64)
isDeployKey, _ := strconv.ParseBool(os.Getenv(models.EnvIsDeployKey))

buf := bytes.NewBuffer(nil)
scanner := bufio.NewScanner(os.Stdin)
@@ -98,6 +99,7 @@ func runHookPreReceive(c *cli.Context) error {
GitObjectDirectory: os.Getenv(private.GitObjectDirectory),
GitQuarantinePath: os.Getenv(private.GitQuarantinePath),
ProtectedBranchID: prID,
IsDeployKey: isDeployKey,
})
switch statusCode {
case http.StatusInternalServerError:

+ 2
- 0
cmd/serv.go View File

@@ -191,6 +191,8 @@ func runServ(c *cli.Context) error {
os.Setenv(models.EnvPusherID, strconv.FormatInt(results.UserID, 10))
os.Setenv(models.ProtectedBranchRepoID, strconv.FormatInt(results.RepoID, 10))
os.Setenv(models.ProtectedBranchPRID, fmt.Sprintf("%d", 0))
os.Setenv(models.EnvIsDeployKey, fmt.Sprintf("%t", results.IsDeployKey))
os.Setenv(models.EnvKeyID, fmt.Sprintf("%d", results.KeyID))

//LFS token authentication
if verb == lfsAuthenticateVerb {

+ 28
- 9
cmd/web.go View File

@@ -13,6 +13,7 @@ import (
"os"
"strings"

"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers"
@@ -75,17 +76,13 @@ func runLetsEncrypt(listenAddr, domain, directory, email string, m http.Handler)
}
go func() {
log.Info("Running Let's Encrypt handler on %s", setting.HTTPAddr+":"+setting.PortToRedirect)
var err = http.ListenAndServe(setting.HTTPAddr+":"+setting.PortToRedirect, certManager.HTTPHandler(http.HandlerFunc(runLetsEncryptFallbackHandler))) // all traffic coming into HTTP will be redirect to HTTPS automatically (LE HTTP-01 validation happens here)
// all traffic coming into HTTP will be redirect to HTTPS automatically (LE HTTP-01 validation happens here)
var err = runHTTP(setting.HTTPAddr+":"+setting.PortToRedirect, certManager.HTTPHandler(http.HandlerFunc(runLetsEncryptFallbackHandler)))
if err != nil {
log.Fatal("Failed to start the Let's Encrypt handler on port %s: %v", setting.PortToRedirect, err)
}
}()
server := &http.Server{
Addr: listenAddr,
Handler: m,
TLSConfig: certManager.TLSConfig(),
}
return server.ListenAndServeTLS("", "")
return runHTTPSWithTLSConfig(listenAddr, certManager.TLSConfig(), context2.ClearHandler(m))
}

func runLetsEncryptFallbackHandler(w http.ResponseWriter, r *http.Request) {
@@ -101,12 +98,21 @@ func runLetsEncryptFallbackHandler(w http.ResponseWriter, r *http.Request) {
}

func runWeb(ctx *cli.Context) error {
if os.Getppid() > 1 && len(os.Getenv("LISTEN_FDS")) > 0 {
log.Info("Restarting Gitea on PID: %d from parent PID: %d", os.Getpid(), os.Getppid())
} else {
log.Info("Starting Gitea on PID: %d", os.Getpid())
}

// Set pid file setting
if ctx.IsSet("pid") {
setting.CustomPID = ctx.String("pid")
}

// Perform global initialization
routers.GlobalInit()

// Set up Macaron
m := routes.NewMacaron()
routes.RegisterRoutes(m)

@@ -164,6 +170,7 @@ func runWeb(ctx *cli.Context) error {
var err error
switch setting.Protocol {
case setting.HTTP:
NoHTTPRedirector()
err = runHTTP(listenAddr, context2.ClearHandler(m))
case setting.HTTPS:
if setting.EnableLetsEncrypt {
@@ -172,9 +179,15 @@ func runWeb(ctx *cli.Context) error {
}
if setting.RedirectOtherPort {
go runHTTPRedirector()
} else {
NoHTTPRedirector()
}
err = runHTTPS(listenAddr, setting.CertFile, setting.KeyFile, context2.ClearHandler(m))
case setting.FCGI:
NoHTTPRedirector()
// FCGI listeners are provided as stdin - this is orthogonal to the LISTEN_FDS approach
// in graceful and systemD
NoMainListener()
var listener net.Listener
listener, err = net.Listen("tcp", listenAddr)
if err != nil {
@@ -187,6 +200,10 @@ func runWeb(ctx *cli.Context) error {
}()
err = fcgi.Serve(listener, context2.ClearHandler(m))
case setting.UnixSocket:
// This could potentially be inherited using LISTEN_FDS but currently
// these cannot be inherited
NoHTTPRedirector()
NoMainListener()
if err := os.Remove(listenAddr); err != nil && !os.IsNotExist(err) {
log.Fatal("Failed to remove unix socket directory %s: %v", listenAddr, err)
}
@@ -207,8 +224,10 @@ func runWeb(ctx *cli.Context) error {
}

if err != nil {
log.Fatal("Failed to start server: %v", err)
log.Critical("Failed to start server: %v", err)
}

log.Info("HTTP Listener: %s Closed", listenAddr)
graceful.WaitForServers()
log.Close()
return nil
}

+ 18
- 26
cmd/web_graceful.go View File

@@ -10,36 +10,28 @@ import (
"crypto/tls"
"net/http"

"code.gitea.io/gitea/modules/log"

"github.com/facebookgo/grace/gracehttp"
"code.gitea.io/gitea/modules/graceful"
)

func runHTTP(listenAddr string, m http.Handler) error {
return gracehttp.Serve(&http.Server{
Addr: listenAddr,
Handler: m,
})
return graceful.HTTPListenAndServe("tcp", listenAddr, m)
}

func runHTTPS(listenAddr, certFile, keyFile string, m http.Handler) error {
config := &tls.Config{
MinVersion: tls.VersionTLS10,
}
if config.NextProtos == nil {
config.NextProtos = []string{"http/1.1"}
}

config.Certificates = make([]tls.Certificate, 1)
var err error
config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
log.Fatal("Failed to load https cert file %s: %v", listenAddr, err)
}

return gracehttp.Serve(&http.Server{
Addr: listenAddr,
Handler: m,
TLSConfig: config,
})
return graceful.HTTPListenAndServeTLS("tcp", listenAddr, certFile, keyFile, m)
}

func runHTTPSWithTLSConfig(listenAddr string, tlsConfig *tls.Config, m http.Handler) error {
return graceful.HTTPListenAndServeTLSConfig("tcp", listenAddr, tlsConfig, m)
}

// NoHTTPRedirector tells our cleanup routine that we will not be using a fallback http redirector
func NoHTTPRedirector() {
graceful.InformCleanup()
}

// NoMainListener tells our cleanup routine that we will not be using a possibly provided listener
// for our main HTTP/HTTPS service
func NoMainListener() {
graceful.InformCleanup()
}

+ 18
- 0
cmd/web_windows.go View File

@@ -7,6 +7,7 @@
package cmd

import (
"crypto/tls"
"net/http"
)

@@ -17,3 +18,20 @@ func runHTTP(listenAddr string, m http.Handler) error {
func runHTTPS(listenAddr, certFile, keyFile string, m http.Handler) error {
return http.ListenAndServeTLS(listenAddr, certFile, keyFile, m)
}

func runHTTPSWithTLSConfig(listenAddr string, tlsConfig *tls.Config, m http.Handler) error {
server := &http.Server{
Addr: listenAddr,
Handler: m,
TLSConfig: tlsConfig,
}
return server.ListenAndServeTLS("", "")
}

// NoHTTPRedirector is a no-op on Windows
func NoHTTPRedirector() {
}

// NoMainListener is a no-op on Windows
func NoMainListener() {
}

+ 1
- 1
contrib/pr/checkout.go View File

@@ -27,13 +27,13 @@ import (
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/routers/routes"

"github.com/go-xorm/xorm"
context2 "github.com/gorilla/context"
"github.com/unknwon/com"
"gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/config"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/testfixtures.v2"
"xorm.io/xorm"
)

var codeFilePath = "contrib/pr/checkout.go"

+ 36
- 2
contrib/systemd/gitea.service View File

@@ -2,11 +2,41 @@
Description=Gitea (Git with a cup of tea)
After=syslog.target
After=network.target
###
# Don't forget to add the database service requirements
###
#
#Requires=mysql.service
#Requires=mariadb.service
#Requires=postgresql.service
#Requires=memcached.service
#Requires=redis.service
#
###
# If using socket activation for main http/s
###
#
#After=gitea.main.socket
#Requires=gitea.main.socket
#
###
# (You can also provide gitea an http fallback and/or ssh socket too)
#
# An example of /etc/systemd/system/gitea.main.socket
###
##
## [Unit]
## Description=Gitea Web Socket
## PartOf=gitea.service
##
## [Socket]
## ListenStream=
## NoDelay=true
##
## [Install]
## WantedBy=sockets.target
##
###

[Service]
# Modify these two values and uncomment them if you have
@@ -20,14 +50,18 @@ Type=simple
User=git
Group=git
WorkingDirectory=/var/lib/gitea/
# If using unix socket: Tells Systemd to create /run/gitea folder to home gitea.sock
# Manual cration would vanish after reboot.
#RuntimeDirectory=gitea
ExecStart=/usr/local/bin/gitea web -c /etc/gitea/app.ini
Restart=always
Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea
# If you want to bind Gitea to a port below 1024 uncomment
# the two values below
# If you want to bind Gitea to a port below 1024, uncomment
# the two values below, or use socket activation to pass Gitea its ports as above
###
#CapabilityBoundingSet=CAP_NET_BIND_SERVICE
#AmbientCapabilities=CAP_NET_BIND_SERVICE
###

[Install]
WantedBy=multi-user.target

+ 75
- 6
custom/conf/app.ini.sample View File

@@ -74,6 +74,37 @@ WORK_IN_PROGRESS_PREFIXES=WIP:,[WIP]
; List of reasons why a Pull Request or Issue can be locked
LOCK_REASONS=Too heated,Off-topic,Resolved,Spam

[repository.signing]
; GPG key to use to sign commits, Defaults to the default - that is the value of git config --get user.signingkey
; run in the context of the RUN_USER
; Switch to none to stop signing completely
SIGNING_KEY = default
; If a SIGNING_KEY ID is provided and is not set to default, use the provided Name and Email address as the signer.
; These should match a publicized name and email address for the key. (When SIGNING_KEY is default these are set to
; the results of git config --get user.name and git config --get user.email respectively and can only be overrided
; by setting the SIGNING_KEY ID to the correct ID.)
SIGNING_NAME =
SIGNING_EMAIL =
; Determines when gitea should sign the initial commit when creating a repository
; Either:
; - never
; - pubkey: only sign if the user has a pubkey
; - twofa: only sign if the user has logged in with twofa
; - always
; options other than none and always can be combined as comma separated list
INITIAL_COMMIT = always
; Determines when to sign for CRUD actions
; - as above
; - parentsigned: requires that the parent commit is signed.
CRUD_ACTIONS = pubkey, twofa, parentsigned
; Determines when to sign Wiki commits
; - as above
WIKI = never
; Determines when to sign on merges
; - basesigned: require that the parent of commit on the base repo is signed.
; - commitssigned: require that all the commits in the head branch are signed.
MERGES = pubkey, twofa, basesigned, commitssigned

[cors]
; More information about CORS can be found here: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#The_HTTP_response_headers
; enable cors headers (disabled by default)
@@ -141,8 +172,9 @@ KEYWORDS = go,git,self-hosted,gitea
[markdown]
; Enable hard line break extension
ENABLE_HARD_LINE_BREAK = false
; List of custom URL-Schemes that are allowed as links when rendering Markdown
; for example git,magnet
; Comma separated list of custom URL-Schemes that are allowed as links when rendering Markdown
; for example git,magnet,ftp (more at https://en.wikipedia.org/wiki/List_of_URI_schemes)
; URLs starting with http and https are always displayed, whatever is put in this entry.
CUSTOM_URL_SCHEMES =
; List of file extensions that should be rendered/edited as Markdown
; Separate the extensions with a comma. To render files without any extension as markdown, just put a comma
@@ -153,6 +185,8 @@ FILE_EXTENSIONS = .md,.markdown,.mdown,.mkd
PROTOCOL = http
DOMAIN = localhost
ROOT_URL = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s/
; when STATIC_URL_PREFIX is empty it will follow APP_URL
STATIC_URL_PREFIX =
; The address to listen on. Either a IPv4/IPv6 address or the path to a unix socket.
HTTP_ADDR = 0.0.0.0
HTTP_PORT = 3000
@@ -243,6 +277,14 @@ LFS_CONTENT_PATH = data/lfs
LFS_JWT_SECRET =
; LFS authentication validity period (in time.Duration), pushes taking longer than this may fail.
LFS_HTTP_AUTH_EXPIRY = 20m
; Allow graceful restarts using SIGHUP to fork
ALLOW_GRACEFUL_RESTARTS = true
; After a restart the parent will finish ongoing requests before
; shutting down. Force shutdown if this process takes longer than this delay.
; set to a negative value to disable
GRACEFUL_HAMMER_TIME = 60s
; Static resources, includes resources on custom/, public/ and all uploaded avatars web browser cache time, default is 6h
STATIC_CACHE_TIME = 6h

; Define allowed algorithms and their minimum key length (use -1 to disable a type)
[ssh.minimum_key_sizes]
@@ -277,10 +319,12 @@ LOG_SQL = true
DB_RETRIES = 10
; Backoff time per DB retry (time.Duration)
DB_RETRY_BACKOFF = 3s
; Max idle database connections on connnection pool, default is 0
MAX_IDLE_CONNS = 0
; Database connection max life time, default is 3s
; Max idle database connections on connnection pool, default is 2
MAX_IDLE_CONNS = 2
; Database connection max life time, default is 0 or 3s mysql (See #6804 & #7071 for reasoning)
CONN_MAX_LIFETIME = 3s
; Database maximum number of open connections, default is 0 meaning no maximum
MAX_OPEN_CONNS = 0

[indexer]
; Issue indexer type, currently support: bleve or db, default is bleve
@@ -296,6 +340,9 @@ ISSUE_INDEXER_QUEUE_DIR = indexers/issues.queue
ISSUE_INDEXER_QUEUE_CONN_STR = "addrs=127.0.0.1:6379 db=0"
; Batch queue number, default is 20
ISSUE_INDEXER_QUEUE_BATCH_NUMBER = 20
; Timeout the indexer if it takes longer than this to start.
; Set to zero to disable timeout.
STARTUP_TIMEOUT=30s

; repo indexer by default disabled, since it uses a lot of disk space
REPO_INDEXER_ENABLED = false
@@ -332,6 +379,10 @@ MIN_PASSWORD_LENGTH = 6
IMPORT_LOCAL_PATHS = false
; Set to true to prevent all users (including admin) from creating custom git hooks
DISABLE_GIT_HOOKS = false
;Comma separated list of character classes required to pass minimum complexity.
;If left empty or no valid values are specified, the default values ("lower,upper,digit,spec") will be used.
;Use "off" to disable checking.
PASSWORD_COMPLEXITY = lower,upper,digit,spec
; Password Hash algorithm, either "pbkdf2", "argon2", "scrypt" or "bcrypt"
PASSWORD_HASH_ALGO = pbkdf2
; Set false to allow JavaScript to read CSRF cookie
@@ -389,6 +440,10 @@ ALLOW_ONLY_EXTERNAL_REGISTRATION = false
REQUIRE_SIGNIN_VIEW = false
; Mail notification
ENABLE_NOTIFY_MAIL = false
; This setting enables gitea to be signed in with HTTP BASIC Authentication using the user's password
; If you set this to false you will not be able to access the tokens endpoints on the API with your password
; Please note that setting this to false will not disable OAuth Basic or Basic authentication using a token
ENABLE_BASIC_AUTHENTICATION = true
; More detail: https://github.com/gogits/gogs/issues/165
ENABLE_REVERSE_PROXY_AUTHENTICATION = false
ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = false
@@ -415,7 +470,7 @@ DEFAULT_ALLOW_CREATE_ORGANIZATION = true
; Public is for everyone
DEFAULT_ORG_VISIBILITY = public
; Default value for DefaultOrgMemberVisible
; True will make the membership of the users visible when added to the organisation
; True will make the membership of the users visible when added to the organisation
DEFAULT_ORG_MEMBER_VISIBLE = false
; Default value for EnableDependencies
; Repositories will use dependencies by default depending on this setting
@@ -690,6 +745,11 @@ SCHEDULE = @every 24h
; or only create new users if UPDATE_EXISTING is set to false
UPDATE_EXISTING = true

; Update migrated repositories' issues and comments' posterid, it will always attempt synchronization when the instance starts.
[cron.update_migration_post_id]
; Interval as a duration between each synchronization. (default every 24h)
SCHEDULE = @every 24h

[git]
; The path of git executable. If empty, Gitea searches through the PATH environment.
PATH =
@@ -808,3 +868,12 @@ IS_INPUT_FILE = false
ENABLED = false
; If you want to add authorization, specify a token here
TOKEN =

[task]
; Task queue type, could be `channel` or `redis`.
QUEUE_TYPE = channel
; Task queue length, available only when `QUEUE_TYPE` is `channel`.
QUEUE_LENGTH = 1000
; Task queue connection string, available only when `QUEUE_TYPE` is `redis`.
; If there is a password of redis, use `addrs=127.0.0.1:6379 password=123 db=0`.
QUEUE_CONN_STR = "addrs=127.0.0.1:6379 db=0"

+ 1
- 0
docker/root/etc/s6/openssh/setup View File

@@ -26,6 +26,7 @@ fi

if [ -d /etc/ssh ]; then
SSH_PORT=${SSH_PORT:-"22"} \
SSH_LISTEN_PORT=${SSH_LISTEN_PORT:-"${SSH_PORT}"} \
envsubst < /etc/templates/sshd_config > /etc/ssh/sshd_config

chmod 0644 /etc/ssh/sshd_config

+ 2
- 2
docker/root/etc/templates/sshd_config View File

@@ -1,4 +1,4 @@
Port ${SSH_PORT}
Port ${SSH_LISTEN_PORT}
Protocol 2

AddressFamily any
@@ -30,4 +30,4 @@ AllowUsers ${USER}
Banner none
Subsystem sftp /usr/lib/ssh/sftp-server

AcceptEnv GIT_PROTOCOL
AcceptEnv GIT_PROTOCOL

+ 8
- 0
docs/content/doc/advanced/api-usage.en-us.md View File

@@ -68,6 +68,14 @@ curl -X POST "http://localhost:4000/api/v1/repos/test1/test1/issues" \
As mentioned above, the token used is the same one you would use in
the `token=` string in a GET request.

## API Guide:

API Reference guide is auto-generated by swagger and available on:
`https://gitea.your.host/api/swagger`
or on
[gitea demo instance](https://try.gitea.io/api/swagger)


## Listing your issued tokens via the API

As mentioned in

+ 60
- 2
docs/content/doc/advanced/config-cheat-sheet.en-us.md View File

@@ -76,6 +76,25 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.

- `LOCK_REASONS`: **Too heated,Off-topic,Resolved,Spam**: A list of reasons why a Pull Request or Issue can be locked

### Repository - Signing (`repository.signing`)

- `SIGNING_KEY`: **default**: \[none, KEYID, default \]: Key to sign with.
- `SIGNING_NAME` &amp; `SIGNING_EMAIL`: if a KEYID is provided as the `SIGNING_KEY`, use these as the Name and Email address of the signer. These should match publicized name and email address for the key.
- `INITIAL_COMMIT`: **always**: \[never, pubkey, twofa, always\]: Sign initial commit.
- `never`: Never sign
- `pubkey`: Only sign if the user has a public key
- `twofa`: Only sign if the user is logged in with twofa
- `always`: Always sign
- Options other than `never` and `always` can be combined as a comma separated list.
- `WIKI`: **never**: \[never, pubkey, twofa, always, parentsigned\]: Sign commits to wiki.
- `CRUD_ACTIONS`: **pubkey, twofa, parentsigned**: \[never, pubkey, twofa, parentsigned, always\]: Sign CRUD actions.
- Options as above, with the addition of:
- `parentsigned`: Only sign if the parent commit is signed.
- `MERGES`: **pubkey, twofa, basesigned, commitssigned**: \[never, pubkey, twofa, basesigned, commitssigned, always\]: Sign merges.
- `basesigned`: Only sign if the parent commit in the base repo is signed.
- `headsigned`: Only sign if the head commit in the head branch is signed.
- `commitssigned`: Only sign if all the commits in the head branch to the merge point are signed.

## CORS (`cors`)

- `ENABLED`: **false**: enable cors headers (disabled by default)
@@ -108,6 +127,9 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
## Markdown (`markdown`)

- `ENABLE_HARD_LINE_BREAK`: **false**: Enable Markdown's hard line break extension.
- `CUSTOM_URL_SCHEMES`: Use a comma separated list (ftp,git,svn) to indicate additional
URL hyperlinks to be rendered in Markdown. URLs beginning in http and https are
always displayed

## Server (`server`)

@@ -116,6 +138,13 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `ROOT_URL`: **%(PROTOCOL)s://%(DOMAIN)s:%(HTTP\_PORT)s/**:
Overwrite the automatically generated public URL.
This is useful if the internal and the external URL don't match (e.g. in Docker).
- `STATIC_URL_PREFIX`: **\<empty\>**:
Overwrite this option to request static resources from a different URL.
This includes CSS files, images, JS files and web fonts.
Avatar images are dynamic resources and still served by gitea.
The option can be just a different path, as in `/static`, or another domain, as in `https://cdn.example.com`.
Requests are then made as `%(ROOT_URL)s/static/css/index.css` and `https://cdn.example.com/css/index.css` respective.
The static files are located in the `public/` directory of the gitea source repository.
- `HTTP_ADDR`: **0.0.0.0**: HTTP listen address.
- If `PROTOCOL` is set to `fcgi`, Gitea will listen for FastCGI requests on TCP socket
defined by `HTTP_ADDR` and `HTTP_PORT` configuration settings.
@@ -140,6 +169,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `CERT_FILE`: **custom/https/cert.pem**: Cert file path used for HTTPS.
- `KEY_FILE`: **custom/https/key.pem**: Key file path used for HTTPS.
- `STATIC_ROOT_PATH`: **./**: Upper level of template and static files path.
- `STATIC_CACHE_TIME`: **6h**: Web browser cache time for static resources on `custom/`, `public/` and all uploaded avatars.
- `ENABLE_GZIP`: **false**: Enables application-level GZIP support.
- `LANDING_PAGE`: **home**: Landing page for unauthenticated users \[home, explore\].
- `LFS_START_SERVER`: **false**: Enables git-lfs support.
@@ -153,6 +183,8 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `LETSENCRYPT_ACCEPTTOS`: **false**: This is an explicit check that you accept the terms of service for Let's Encrypt.
- `LETSENCRYPT_DIRECTORY`: **https**: Directory that Letsencrypt will use to cache information such as certs and private keys.
- `LETSENCRYPT_EMAIL`: **email@example.com**: Email used by Letsencrypt to notify about problems with issued certificates. (No default)
- `ALLOW_GRACEFUL_RESTARTS`: **true**: Perform a graceful restart on SIGHUP
- `GRACEFUL_HAMMER_TIME`: **60s**: After a restart the parent process will stop accepting new connections and will allow requests to finish before stopping. Shutdown will be forced if it takes longer than this time.

## Database (`database`)

@@ -167,8 +199,12 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `LOG_SQL`: **true**: Log the executed SQL.
- `DB_RETRIES`: **10**: How many ORM init / DB connect attempts allowed.
- `DB_RETRY_BACKOFF`: **3s**: time.Duration to wait before trying another ORM init / DB connect attempt, if failure occured.
- `MAX_IDLE_CONNS` **0**: Max idle database connections on connnection pool, default is 0
- `CONN_MAX_LIFETIME` **3s**: Database connection max lifetime
- `MAX_OPEN_CONNS` **0**: Database maximum open connections - default is 0, meaning there is no limit.
- `MAX_IDLE_CONNS` **2**: Max idle database connections on connnection pool, default is 2 - this will be capped to `MAX_OPEN_CONNS`.
- `CONN_MAX_LIFETIME` **0 or 3s**: Sets the maximum amount of time a DB connection may be reused - default is 0, meaning there is no limit (except on MySQL where it is 3s - see #6804 & #7071).
Please see #8540 & #8273 for further discussion of the appropriate values for `MAX_OPEN_CONNS`, `MAX_IDLE_CONNS` & `CONN_MAX_LIFETIME` and their
relation to port exhaustion.

## Indexer (`indexer`)

@@ -185,6 +221,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `REPO_INDEXER_EXCLUDE`: **empty**: A comma separated list of glob patterns (see https://github.com/gobwas/glob) to **exclude** from the index. Files that match this list will not be indexed, even if they match in `REPO_INDEXER_INCLUDE`.
- `UPDATE_BUFFER_LEN`: **20**: Buffer length of index request.
- `MAX_FILE_SIZE`: **1048576**: Maximum size in bytes of files to be indexed.
- `STARTUP_TIMEOUT`: **30s**: If the indexer takes longer than this timeout to start - fail. (This timeout will be added to the hammer time above for child processes - as bleve will not start until the previous parent is shutdown.) Set to zero to never timeout.

## Admin (`admin`)
- `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
@@ -208,6 +245,12 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `INTERNAL_TOKEN_URI`: **<empty>**: Instead of defining internal token in the configuration, this configuration option can be used to give Gitea a path to a file that contains the internal token (example value: `file:/etc/gitea/internal_token`)
- `PASSWORD_HASH_ALGO`: **pbkdf2**: The hash algorithm to use \[pbkdf2, argon2, scrypt, bcrypt\].
- `CSRF_COOKIE_HTTP_ONLY`: **true**: Set false to allow JavaScript to read CSRF cookie.
- `PASSWORD_COMPLEXITY`: **lower,upper,digit,spec**: Comma separated list of character classes required to pass minimum complexity. If left empty or no valid values are specified, the default values will be used. Possible values are:
- lower - use one or more lower latin characters
- upper - use one or more upper latin characters
- digit - use one or more digits
- spec - use one or more special characters as ``!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~``
- off - do not check password complexity

## OpenID (`openid`)

@@ -233,6 +276,10 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `REQUIRE_SIGNIN_VIEW`: **false**: Enable this to force users to log in to view any page.
- `ENABLE_NOTIFY_MAIL`: **false**: Enable this to send e-mail to watchers of a repository when
something happens, like creating issues. Requires `Mailer` to be enabled.
- `ENABLE_BASIC_AUTHENTICATION`: **true**: Disable this to disallow authenticaton using HTTP
BASIC and the user's password. Please note if you disable this you will not be able to access the
tokens API endpoints using a password. Further, this only disables BASIC authentication using the
password - not tokens or OAuth Basic.
- `ENABLE_REVERSE_PROXY_AUTHENTICATION`: **false**: Enable this to allow reverse proxy authentication.
- `ENABLE_REVERSE_PROXY_AUTO_REGISTRATION`: **false**: Enable this to allow auto-registration
for reverse authentication.
@@ -419,6 +466,10 @@ NB: You must `REDIRECT_MACARON_LOG` and have `DISABLE_ROUTER_LOG` set to `false`
- `RUN_AT_START`: **true**: Run repository statistics check at start time.
- `SCHEDULE`: **@every 24h**: Cron syntax for scheduling repository statistics check.

### Cron - Update Migration Poster ID (`cron.update_migration_post_id`)

- `SCHEDULE`: **@every 24h** : Interval as a duration between each synchronization, it will always attempt synchronization when the instance starts.

## Git (`git`)

- `PATH`: **""**: The path of git executable. If empty, Gitea searches through the PATH environment.
@@ -514,9 +565,16 @@ Two special environment variables are passed to the render command:
- `GITEA_PREFIX_RAW`, which contains the current URL prefix in the `raw` path tree. To be used as prefix for image paths.

## Time (`time`)

- `FORMAT`: Time format to diplay on UI. i.e. RFC1123 or 2006-01-02 15:04:05
- `DEFAULT_UI_LOCATION`: Default location of time on the UI, so that we can display correct user's time on UI. i.e. Shanghai/Asia

## Task (`task`)

- `QUEUE_TYPE`: **channel**: Task queue type, could be `channel` or `redis`.
- `QUEUE_LENGTH`: **1000**: Task queue length, available only when `QUEUE_TYPE` is `channel`.
- `QUEUE_CONN_STR`: **addrs=127.0.0.1:6379 db=0**: Task queue connection string, available only when `QUEUE_TYPE` is `redis`. If there redis needs a password, use `addrs=127.0.0.1:6379 password=123 db=0`.

## Other (`other`)

- `SHOW_FOOTER_BRANDING`: **false**: Show Gitea branding in the footer.

+ 13
- 1
docs/content/doc/advanced/config-cheat-sheet.zh-cn.md View File

@@ -65,6 +65,7 @@ menu:
- `CERT_FILE`: 启用HTTPS的证书文件。
- `KEY_FILE`: 启用HTTPS的密钥文件。
- `STATIC_ROOT_PATH`: 存放模板和静态文件的根目录,默认是 Gitea 的根目录。
- `STATIC_CACHE_TIME`: **6h**: 静态资源文件,包括 `custom/`, `public/` 和所有上传的头像的浏览器缓存时间。
- `ENABLE_GZIP`: 启用应用级别的 GZIP 压缩。
- `LANDING_PAGE`: 未登录用户的默认页面,可选 `home` 或 `explore`。
- `LFS_START_SERVER`: 是否启用 git-lfs 支持. 可以为 `true` 或 `false`, 默认是 `false`。
@@ -196,7 +197,11 @@ menu:
### Cron - Repository Statistics Check (`cron.check_repo_stats`)

- `RUN_AT_START`: 是否启动时自动运行仓库统计。
- `SCHEDULE`: 藏亏统计时的Cron 语法,比如:`@every 24h`.
- `SCHEDULE`: 仓库统计时的Cron 语法,比如:`@every 24h`.

### Cron - Update Migration Poster ID (`cron.update_migration_post_id`)

- `SCHEDULE`: **@every 24h** : 每次同步的间隔时间。此任务总是在启动时自动进行。

## Git (`git`)

@@ -241,9 +246,16 @@ IS_INPUT_FILE = false
- IS_INPUT_FILE: 输入方式是最后一个参数为文件路径还是从标准输入读取。

## Time (`time`)

- `FORMAT`: 显示在界面上的时间格式。比如: RFC1123 或者 2006-01-02 15:04:05
- `DEFAULT_UI_LOCATION`: 默认显示在界面上的时区,默认为本地时区。比如: Asia/Shanghai

## Task (`task`)

- `QUEUE_TYPE`: **channel**: 任务队列类型,可以为 `channel` 或 `redis`。
- `QUEUE_LENGTH`: **1000**: 任务队列长度,当 `QUEUE_TYPE` 为 `channel` 时有效。
- `QUEUE_CONN_STR`: **addrs=127.0.0.1:6379 db=0**: 任务队列连接字符串,当 `QUEUE_TYPE` 为 `redis` 时有效。如果redis有密码,则可以 `addrs=127.0.0.1:6379 password=123 db=0`。

## Other (`other`)

- `SHOW_FOOTER_BRANDING`: 为真则在页面底部显示Gitea的字样。

+ 1
- 1
docs/content/doc/advanced/external-renderers.en-us.md View File

@@ -51,7 +51,7 @@ add one `[markup.XXXXX]` section per external renderer on your custom `app.ini`:
[markup.asciidoc]
ENABLED = true
FILE_EXTENSIONS = .adoc,.asciidoc
RENDER_COMMAND = "asciidoctor --out-file=- -"
RENDER_COMMAND = "asciidoctor -e -a leveloffset=-1 --out-file=- -"
; Input is not a standard input but a file
IS_INPUT_FILE = false


+ 162
- 0
docs/content/doc/advanced/signing.en-us.md View File

@@ -0,0 +1,162 @@
---
date: "2019-08-17T10:20:00+01:00"
title: "GPG Commit Signatures"
slug: "signing"
weight: 20
toc: false
draft: false
menu:
sidebar:
parent: "advanced"
name: "GPG Commit Signatures"
weight: 20
identifier: "signing"
---

# GPG Commit Signatures

Gitea will verify GPG commit signatures in the provided tree by
checking if the commits are signed by a key within the gitea database,
or if the commit matches the default key for git.

Keys are not checked to determine if they have expired or revoked.
Keys are also not checked with keyservers.

A commit will be marked with a grey unlocked icon if no key can be
found to verify it. If a commit is marked with a red unlocked icon,
it is reported to be signed with a key with an id.

Please note: The signer of a commit does not have to be an author or
committer of a commit.

This functionality requires git >= 1.7.9 but for full functionality
this requires git >= 2.0.0.

## Automatic Signing

There are a number of places where Gitea will generate commits itself:

* Repository Initialisation
* Wiki Changes
* CRUD actions using the editor or the API
* Merges from Pull Requests

Depending on configuration and server trust you may want Gitea to
sign these commits.

## General Configuration

Gitea's configuration for signing can be found with the
`[repository.signing]` section of `app.ini`:

```ini
...
[repository.signing]
SIGNING_KEY = default
SIGNING_NAME =
SIGNING_EMAIL =
INITIAL_COMMIT = always
CRUD_ACTIONS = pubkey, twofa, parentsigned
WIKI = never
MERGES = pubkey, twofa, basesigned, commitssigned

...
```

### `SIGNING_KEY`

The first option to discuss is the `SIGNING_KEY`. There are three main
options:

* `none` - this prevents Gitea from signing any commits
* `default` - Gitea will default to the key configured within
`git config`
* `KEYID` - Gitea will sign commits with the gpg key with the ID
`KEYID`. In this case you should provide a `SIGNING_NAME` and
`SIGNING_EMAIL` to be displayed for this key.

The `default` option will interrogate `git config` for
`commit.gpgsign` option - if this is set, then it will use the results
of the `user.signingkey`, `user.name` and `user.email` as appropriate.

Please note: by adjusting git's `config` file within Gitea's
repositories, `SIGNING_KEY=default` could be used to provide different
signing keys on a per-repository basis. However, this is cleary not an
ideal UI and therefore subject to change.

### `INITIAL_COMMIT`

This option determines whether Gitea should sign the initial commit
when creating a repository. The possible values are:

* `never`: Never sign
* `pubkey`: Only sign if the user has a public key
* `twofa`: Only sign if the user logs in with two factor authentication
* `always`: Always sign

Options other than `never` and `always` can be combined as a comma
separated list.

### `WIKI`

This options determines if Gitea should sign commits to the Wiki.
The possible values are:

* `never`: Never sign
* `pubkey`: Only sign if the user has a public key
* `twofa`: Only sign if the user logs in with two factor authentication
* `parentsigned`: Only sign if the parent commit is signed.
* `always`: Always sign

Options other than `never` and `always` can be combined as a comma
separated list.

### `CRUD_ACTIONS`

This option determines if Gitea should sign commits from the web
editor or API CRUD actions. The possible values are:

* `never`: Never sign
* `pubkey`: Only sign if the user has a public key
* `twofa`: Only sign if the user logs in with two factor authentication
* `parentsigned`: Only sign if the parent commit is signed.
* `always`: Always sign

Options other than `never` and `always` can be combined as a comma
separated list.

### `MERGES`

This option determines if Gitea should sign merge commits from PRs.
The possible options are:

* `never`: Never sign
* `pubkey`: Only sign if the user has a public key
* `twofa`: Only sign if the user logs in with two factor authentication
* `basesigned`: Only sign if the parent commit in the base repo is signed.
* `headsigned`: Only sign if the head commit in the head branch is signed.
* `commitssigned`: Only sign if all the commits in the head branch to the merge point are signed.
* `always`: Always sign

Options other than `never` and `always` can be combined as a comma
separated list.

## Installing and generating a GPG key for Gitea

It is up to a server administrator to determine how best to install
a signing key. Gitea generates all its commits using the server `git`
command at present - and therefore the server `gpg` will be used for
signing (if configured.) Administrators should review best-practices
for gpg - in particular it is probably advisable to only install a
signing secret subkey without the master signing and certifying secret
key.

## Obtaining the Public Key of the Signing Key

The public key used to sign Gitea's commits can be obtained from the API at:

```/api/v1/signing-key.gpg```

In cases where there is a repository specific key this can be obtained from:

```/api/v1/repos/:username/:reponame/signing-key.gpg```

+ 1
- 0
docs/content/doc/features/comparison.en-us.md View File

@@ -105,6 +105,7 @@ _Symbols used in table:_
| Revert specific commits or a merge request | [✘](https://github.com/go-gitea/gitea/issues/5158) | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
| Pull/Merge requests templates | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ |
| Cherry-picking changes | [✘](https://github.com/go-gitea/gitea/issues/5158) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
| Download Patch | ✓ | ✘ | ✓ | ✓ | ✓ | [/](https://jira.atlassian.com/plugins/servlet/mobile#issue/BCLOUD-8323) | ✘ |


#### 3rd-party integrations

+ 81
- 1
docs/content/doc/features/webhooks.en-us.md View File

@@ -17,7 +17,15 @@ menu:

Gitea supports web hooks for repository events. This can be found in the settings
page `/:username/:reponame/settings/hooks`. All event pushes are POST requests.
The two methods currently supported are Gitea and Slack.
The methods currently supported are:

- Gitea
- Gogs
- Slack
- Discord
- Dingtalk
- Telegram
- Microsoft Teams

### Event information

@@ -104,3 +112,75 @@ X-Gitea-Event: push
}
}
```

### Example

This is an example of how to use webhooks to run a php script upon push requests to the repository.
In your repository Settings, under Webhooks, Setup a Gitea webhook as follows:

- Target URL: http://mydomain.com/webhook.php
- HTTP Method: POST
- POST Content Type: application/json
- Secret: 123
- Trigger On: Push Events
- Active: Checked

Now on your server create the php file webhook.php

```
<?php

$secret_key = '123';

// check for POST request
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
error_log('FAILED - not POST - '. $_SERVER['REQUEST_METHOD']);
exit();
}

// get content type
$content_type = isset($_SERVER['CONTENT_TYPE']) ? strtolower(trim($_SERVER['CONTENT_TYPE'])) : '';

if ($content_type != 'application/json') {
error_log('FAILED - not application/json - '. $content_type);
exit();
}

// get payload
$payload = trim(file_get_contents("php://input"));

if (empty($payload)) {
error_log('FAILED - no payload');
exit();
}

// get header signature
$header_signature = isset($_SERVER['HTTP_X_GITEA_SIGNATURE']) ? $_SERVER['HTTP_X_GITEA_SIGNATURE'] : '';

if (empty($header_signature)) {
error_log('FAILED - header signature missing');
exit();
}

// calculate payload signature
$payload_signature = hash_hmac('sha256', $payload, $secret_key, false);

// check payload signature against header signature
if ($header_signature != $payload_signature) {
error_log('FAILED - payload signature');
exit();
}

// convert json to array
$decoded = json_decode($payload, true);

// check for json decode errors
if (json_last_error() !== JSON_ERROR_NONE) {
error_log('FAILED - json decode - '. json_last_error());
exit();
}

// success, do something
```

There is a Test Delivery button in the webhook settings that allows to test the configuration as well as a list of the most Recent Deliveries.

+ 1
- 1
docs/content/doc/help/seek-help.en-us.md View File

@@ -33,4 +33,4 @@ If you found a bug, please create an [issue on GitHub](https://github.com/go-git

## Chinese Support

Support for the Chinese language is provided at [gocn.io](https://gocn.io/topic/Gitea).
Support for the Chinese language is provided at [gocn.vip](https://gocn.vip/topic/gitea).

+ 1
- 1
docs/content/doc/help/seek-help.zh-cn.md View File

@@ -18,6 +18,6 @@ menu:
如果您在使用或者开发过程中遇到问题,请到以下渠道咨询:

- 到[Github issue](https://github.com/go-gitea/gitea/issues)提问(因为项目维护人员来自世界各地,为保证沟通顺畅,请使用英文提问)
- 中文问题到[gocn.io](https://gocn.io/topic/Gitea)提问
- 中文问题到[gocn.vip](https://gocn.vip/topic/gitea)提问
- 访问 [Discord server - 英文](https://discord.gg/NsatcWJ)
- 加入 QQ群 328432459 获得进一步的支持

+ 13
- 13
docs/content/doc/installation/from-binary.en-us.md View File

@@ -44,7 +44,7 @@ location. When launched manually, Gitea can be killed using `Ctrl+C`.

## Recommended server configuration

**NOTE:** Many of the following directories can be configured using [Environment Variables]({{< relref "doc/advanced/specific-variables.en-us.md" >}}) as well!
**NOTE:** Many of the following directories can be configured using [Environment Variables]({{< relref "doc/advanced/specific-variables.en-us.md" >}}) as well!
Of note, configuring `GITEA_WORK_DIR` will tell Gitea where to base its working directory, as well as ease installation.

### Prepare environment
@@ -80,7 +80,7 @@ chmod 770 /etc/gitea
**NOTE:** `/etc/gitea` is temporary set with write rights for user `git` so that Web installer could write configuration file. After installation is done, it is recommended to set rights to read-only using:
```
chmod 750 /etc/gitea
chmod 644 /etc/gitea/app.ini
chmod 640 /etc/gitea/app.ini
```
If you don't want the web installer to be able to write the config file at all, it is also possible to make the config file read-only for the gitea user (owner/group `root:root`, mode `0660`), and set `INSTALL_LOCK = true`. In that case all database configuration details must be set beforehand in the config file, as well as the `SECRET_KEY` and `INTERNAL_TOKEN` values. See the [command line documentation]({{< relref "doc/usage/command-line.en-us.md" >}}) for information on using `gitea generate secret INTERNAL_TOKEN`.

@@ -113,16 +113,16 @@ GITEA_WORK_DIR=/var/lib/gitea/ /usr/local/bin/gitea web -c /etc/gitea/app.ini

## Updating to a new version

You can update to a new version of Gitea by stopping Gitea, replacing the binary at `/usr/local/bin/gitea` and restarting the instance.
The binary file name should not be changed during the update to avoid problems
in existing repositories.
You can update to a new version of Gitea by stopping Gitea, replacing the binary at `/usr/local/bin/gitea` and restarting the instance.
The binary file name should not be changed during the update to avoid problems
in existing repositories.

It is recommended you do a [backup]({{< relref "doc/usage/backup-and-restore.en-us.md" >}}) before updating your installation.

If you have carried out the installation steps as described above, the binary should
have the generic name `gitea`. Do not change this, i.e. to include the version number.
If you have carried out the installation steps as described above, the binary should
have the generic name `gitea`. Do not change this, i.e. to include the version number.

See below for troubleshooting instructions to repair broken repositories after
See below for troubleshooting instructions to repair broken repositories after
an update of your Gitea version.

## Troubleshooting
@@ -145,7 +145,7 @@ is already running.

### Running Gitea on Raspbian

As of v1.8, there is a problem with the arm7 version of Gitea and it doesn't run on Raspberry Pi and similar devices.
As of v1.8, there is a problem with the arm7 version of Gitea and it doesn't run on Raspberry Pi and similar devices.

It is therefore recommended to switch to the arm6 version which has been tested and shown to work on Raspberry Pi and similar devices.

@@ -154,18 +154,18 @@ please remove after fixing the arm7 bug
--->
### Git error after updating to a new version of Gitea

If the binary file name has been changed during the update to a new version of Gitea,
git hooks in existing repositories will not work any more. In that case, a git
If the binary file name has been changed during the update to a new version of Gitea,
git hooks in existing repositories will not work any more. In that case, a git
error will be displayed when pushing to the repository.

```
remote: ./hooks/pre-receive.d/gitea: line 2: [...]: No such file or directory
```

The `[...]` part of the error message will contain the path to your previous Gitea
The `[...]` part of the error message will contain the path to your previous Gitea
binary.

To solve this, go to the admin options and run the task `Resynchronize pre-receive,
To solve this, go to the admin options and run the task `Resynchronize pre-receive,
update and post-receive hooks of all repositories` to update all hooks to contain
the new binary path. Please note that this overwrite all git hooks including ones
with customizations made.

+ 3
- 3
docs/content/doc/installation/from-source.en-us.md View File

@@ -118,12 +118,12 @@ launched manually from command line, it can be killed by pressing `Ctrl + C`.
./gitea web
```

## Changing the default CustomPath, CustomConf and AppWorkDir
## Changing the default CustomPath, CustomConf and AppWorkPath

Gitea will search for a number of things from the `CustomPath`. By default this is
the `custom/` directory in the current working directory when running Gitea. It will also
look for its configuration file `CustomConf` in `$CustomPath/conf/app.ini`, and will use the
current working directory as the relative base path `AppWorkDir` for a number configurable
current working directory as the relative base path `AppWorkPath` for a number configurable
values.

These values, although useful when developing, may conflict with downstream users preferences.
@@ -134,7 +134,7 @@ using the `LDFLAGS` environment variable for `make`. The appropriate settings ar

* To set the `CustomPath` use `LDFLAGS="-X \"code.gitea.io/gitea/modules/setting.CustomPath=custom-path\""`
* For `CustomConf` you should use `-X \"code.gitea.io/gitea/modules/setting.CustomConf=conf.ini\"`
* For `AppWorkDir` you should use `-X \"code.gitea.io/gitea/modules/setting.AppWorkDir=working-directory\"`
* For `AppWorkPath` you should use `-X \"code.gitea.io/gitea/modules/setting.AppWorkPath=working-path\"`

Add as many of the strings with their preceding `-X` to the `LDFLAGS` variable and run `make build`
with the appropriate `TAGS` as above.

+ 33
- 0
docs/content/doc/usage/email-setup.md View File

@@ -0,0 +1,33 @@
---
date: "2019-10-15T10:10:00+05:00"
title: "Usage: Email setup"
slug: "email-setup"
weight: 12
toc: true
draft: false
menu:
sidebar:
parent: "usage"
name: "Email setup"
weight: 12
identifier: "email-setup"
---

# Email setup

- To use Gitea's built-in Email support, update the `app.ini` config file [mailer] section:

```ini
[mailer]
ENABLED = true
HOST = mail.mydomain.com:587
FROM = gitea@mydomain.com
USER = gitea@mydomain.com
PASSWD = `password`
```

- Restart Gitea for the configuration changes to take effect.

- To send a test email to validate the settings, go to Gitea > Site Administration > Configuration > SMTP Mailer Configuration.

For the full list of options check the [Config Cheat Sheet]({{< relref "doc/advanced/config-cheat-sheet.en-us.md" >}})

+ 19
- 3
docs/content/doc/usage/fail2ban-setup.md View File

@@ -26,7 +26,7 @@ on a bad authentication:
2018/04/26 18:15:54 [I] Failed authentication attempt for user from xxx.xxx.xxx.xxx
```

So we set our filter in `/etc/fail2ban/filter.d/gitea.conf`:
Add our filter in `/etc/fail2ban/filter.d/gitea.conf`:

```ini
# gitea.conf
@@ -35,12 +35,11 @@ failregex = .*Failed authentication attempt for .* from <HOST>
ignoreregex =
```

And configure it in `/etc/fail2ban/jail.d/jail.local`:
Add our jail in `/etc/fail2ban/jail.d/gitea.conf`:

```ini
[gitea]
enabled = true
port = http,https
filter = gitea
logpath = /home/git/gitea/log/gitea.log
maxretry = 10
@@ -49,6 +48,23 @@ bantime = 900
action = iptables-allports
```

If you're using Docker, you'll also need to add an additional jail to handle the **FORWARD**
chain in **iptables**. Configure it in `/etc/fail2ban/jail.d/gitea-docker.conf`:

```ini
[gitea-docker]
enabled = true
filter = gitea
logpath = /home/git/gitea/log/gitea.log
maxretry = 10
findtime = 3600
bantime = 900
action = iptables-allports[chain="FORWARD"]
```

Then simply run `service fail2ban restart` to apply your changes. You can check to see if
fail2ban has accepted your configuration using `service fail2ban status`.

Make sure and read up on fail2ban and configure it to your needs, this bans someone
for **15 minutes** (from all ports) when they fail authentication 10 times in an hour.


+ 26
- 0
docs/content/doc/usage/git-lfs-support.md View File

@@ -0,0 +1,26 @@
---
date: "2019-10-06T08:00:00+05:00"
title: "Usage: Git LFS setup"
slug: "git-lfs-setup"
weight: 12
toc: true
draft: false
menu:
sidebar:
parent: "usage"
name: "Git LFS setup"
weight: 12
identifier: "git-lfs-setup"
---

# Git Large File Storage setup

To use Gitea's built-in LFS support, you must update the `app.ini` file:

```ini
[server]
; Enables git-lfs support. true or false, default is false.
LFS_START_SERVER = true
; Where your lfs files reside, default is data/lfs.
LFS_CONTENT_PATH = /home/gitea/data/lfs
```

+ 2
- 0
docs/content/doc/usage/https-support.md View File

@@ -20,6 +20,8 @@ menu:
Before you enable HTTPS, make sure that you have valid SSL/TLS certificates.
You could use self-generated certificates for evaluation and testing. Please run `gitea cert --host [HOST]` to generate a self signed certificate.

If you are using Apache or nginx on the server, it's recommended to check the [reverse proxy guide]({{< relref "doc/usage/reverse-proxies.en-us.md" >}}).

To use Gitea's built-in HTTPS support, you must change your `app.ini` file:

```ini

+ 68
- 0
docs/content/doc/usage/reverse-proxies.en-us.md View File

@@ -44,6 +44,74 @@ server {

Then set `[server] ROOT_URL = http://git.example.com/git/` in your configuration.

## Using Nginx as a reverse proxy and serve static resources directly
We can tune the performance in splitting requests into categories static and dynamic.

CSS files, JavaScript files, images and web fonts are static content.
The front page, a repository view or issue list is dynamic content.

Nginx can serve static resources directly and proxy only the dynamic requests to gitea.
Nginx is optimized for serving static content, while the proxying of large responses might be the opposite of that
(see https://serverfault.com/q/587386).

Download a snap shot of the gitea source repository to `/path/to/gitea/`.

We are only interested in the `public/` directory and you can delete the rest.

Depending on the scale of your user base, you might want to split the traffic to two distinct servers,
or use a cdn for the static files.

### using a single node and a single domain

Set `[server] STATIC_URL_PREFIX = /_/static` in your configuration.

```
server {
listen 80;
server_name git.example.com;

location /_/static {
alias /path/to/gitea/public;
}

location / {
proxy_pass http://localhost:3000;
}
}
```

### using two nodes and two domains

Set `[server] STATIC_URL_PREFIX = http://cdn.example.com/gitea` in your configuration.

```
# application server running gitea
server {
listen 80;
server_name git.example.com;

location / {
proxy_pass http://localhost:3000;
}
}
```

```
# static content delivery server
server {
listen 80;
server_name cdn.example.com;

location /gitea {
alias /path/to/gitea/public;
}

location / {
return 404;
}
}
```

## Using Apache HTTPD as a reverse proxy

If you want Apache HTTPD to serve your Gitea instance, you can add the following to your Apache HTTPD configuration (usually located at `/etc/apache2/httpd.conf` in Ubuntu):

+ 6
- 12
go.mod View File

@@ -29,16 +29,12 @@ require (
github.com/cznic/strutil v0.0.0-20181122101858-275e90344537 // indirect
github.com/denisenkom/go-mssqldb v0.0.0-20190924004331-208c0a498538
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/editorconfig/editorconfig-core-go/v2 v2.1.1
github.com/emirpasic/gods v1.12.0
github.com/etcd-io/bbolt v1.3.2 // indirect
github.com/ethantkoenig/rupture v0.0.0-20180203182544-0a76f03a811a
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect
github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9 // indirect
github.com/facebookgo/grace v0.0.0-20160926231715-5729e484473f
github.com/facebookgo/httpdown v0.0.0-20160323221027-a3b1354551a2 // indirect
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/facebookgo/stats v0.0.0-20151006221625-1b76add642e4 // indirect
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect
github.com/gliderlabs/ssh v0.2.2
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd // indirect
@@ -48,7 +44,6 @@ require (
github.com/go-redis/redis v6.15.2+incompatible
github.com/go-sql-driver/mysql v1.4.1
github.com/go-swagger/go-swagger v0.20.1
github.com/go-xorm/xorm v0.7.9
github.com/gobwas/glob v0.2.3
github.com/gogits/chardet v0.0.0-20150115103509-2404f7772561
github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14
@@ -64,7 +59,7 @@ require (
github.com/klauspost/compress v0.0.0-20161025140425-8df558b6cb6f
github.com/klauspost/cpuid v0.0.0-20160302075316-09cded8978dc // indirect
github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6 // indirect
github.com/lafriks/xormstore v1.3.1
github.com/lafriks/xormstore v1.3.2
github.com/lib/pq v1.2.0
github.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
github.com/lunny/levelqueue v0.0.0-20190217115915-02b525a4418e
@@ -112,16 +107,15 @@ require (
golang.org/x/tools v0.0.0-20190910221609-7f5965fd7709 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/asn1-ber.v1 v1.0.0-20150924051756-4e86f4367175 // indirect
gopkg.in/editorconfig/editorconfig-core-go.v1 v1.3.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/ini.v1 v1.46.0
gopkg.in/ini.v1 v1.48.0
gopkg.in/ldap.v3 v3.0.2
gopkg.in/src-d/go-billy.v4 v4.3.2
gopkg.in/src-d/go-git.v4 v4.13.1
gopkg.in/stretchr/testify.v1 v1.2.2 // indirect
gopkg.in/testfixtures.v2 v2.5.0
mvdan.cc/xurls/v2 v2.0.0
strk.kbt.io/projects/go/libravatar v0.0.0-20160628055650-5eed7bff870a
mvdan.cc/xurls/v2 v2.1.0
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251
xorm.io/builder v0.3.6
xorm.io/core v0.7.2
xorm.io/xorm v0.8.0
)

+ 14
- 32
go.sum View File

@@ -89,8 +89,6 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
github.com/chaseadamsio/goorgeous v0.0.0-20170901132237-098da33fde5f h1:REH9VH5ubNR0skLaOxK7TRJeRbE2dDfvaouQo8FsRcA=
github.com/chaseadamsio/goorgeous v0.0.0-20170901132237-098da33fde5f/go.mod h1:6QaC0vFoKWYDth94dHFNgRT2YkT5FHdQp/Yx15aAAi0=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/corbym/gocrest v1.0.3 h1:gwEdq6RkTmq+09CTuM29DfKOCtZ7G7bcyxs3IZ6EVdU=
github.com/corbym/gocrest v1.0.3/go.mod h1:maVFL5lbdS2PgfOQgGRWDYTeunSWQeiEgoNdTABShCs=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
@@ -134,6 +132,8 @@ github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
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.1.1 h1:mhPg/0hGebcpiiQLqJD2PWWyoHRLEdZ3sXKaEvT1EQU=
github.com/editorconfig/editorconfig-core-go/v2 v2.1.1/go.mod h1:/LuhWJiQ9Gvo1DhVpa4ssm5qeg8rrztdtI7j/iCie2k=
github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
@@ -142,20 +142,10 @@ github.com/etcd-io/bbolt v1.3.2 h1:RLRQ0TKLX7DlBRXAJHvbmXL17Q3KNnTBtZ9B6Qo+/Y0=
github.com/etcd-io/bbolt v1.3.2/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
github.com/ethantkoenig/rupture v0.0.0-20180203182544-0a76f03a811a h1:M1bRpaZAn4GSsqu3hdK2R8H0AH9O6vqCTCbm2oAFGfE=
github.com/ethantkoenig/rupture v0.0.0-20180203182544-0a76f03a811a/go.mod h1:MkKY/CB98aVE4VxO63X5vTQKUgcn+3XP15LMASe3lYs=
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw=
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA=
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9 h1:wWke/RUCl7VRjQhwPlR/v0glZXNYzBHdNUzf/Am2Nmg=
github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9/go.mod h1:uPmAp6Sws4L7+Q/OokbWDAK1ibXYhB3PXFP1kol5hPg=
github.com/facebookgo/grace v0.0.0-20160926231715-5729e484473f h1:0mlfEUWnUDVZnqWEVHGerL5bKYDKMEmT/Qk/W/3nGuo=
github.com/facebookgo/grace v0.0.0-20160926231715-5729e484473f/go.mod h1:KigFdumBXUPSwzLDbeuzyt0elrL7+CP7TKuhrhT4bcU=
github.com/facebookgo/httpdown v0.0.0-20160323221027-a3b1354551a2 h1:3Zvf9wRhl1cOhckN1oRGWPOkIhOketmEcrQ4TeFAoR4=
github.com/facebookgo/httpdown v0.0.0-20160323221027-a3b1354551a2/go.mod h1:TUV/fX3XrTtBQb5+ttSUJzcFgLNpILONFTKmBuk5RSw=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
github.com/facebookgo/stats v0.0.0-20151006221625-1b76add642e4 h1:0YtRCqIZs2+Tz49QuH6cJVw/IFqzo39gEqZ0iYLxD2M=
github.com/facebookgo/stats v0.0.0-20151006221625-1b76add642e4/go.mod h1:vsJz7uE339KUCpBXx3JAJzSRH7Uk4iGGyJzR529qDIA=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
@@ -249,11 +239,8 @@ github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013 h1:l
github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013/go.mod h1:b65mBPzqzZWxOZGxSWrqs4GInLIn+u99Q9q7p+GKni0=
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/go-xorm/xorm v0.7.9 h1:LZze6n1UvRmM5gpL9/U9Gucwqo6aWlFVlfcHKH10qA0=
github.com/go-xorm/xorm v0.7.9/go.mod h1:XiVxrMMIhFkwSkh96BW7PACl7UhLtx2iJIHMdmjh5sQ=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogits/chardet v0.0.0-20150115103509-2404f7772561 h1:deE7ritpK04PgtpyVOS2TYcQEld9qLCD5b5EbVNOuLA=
github.com/gogits/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:YgYOrVn3Nj9Tq0EvjmFbphRytDj7JNRoWSStJZWDJTQ=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -337,9 +324,6 @@ github.com/issue9/assert v1.3.2 h1:IaTa37u4m1fUuTH9K9ldO5IONKVDXjLiUO1T9vj0OF0=
github.com/issue9/assert v1.3.2/go.mod h1:9Ger+iz8X7r1zMYYwEhh++2wMGWcNN2oVI+zIQXxcio=
github.com/issue9/identicon v0.0.0-20160320065130-d36b54562f4c h1:A/PDn117UYld5mlxe58EpMguqpkeTMw5/FCo0ZPS/Ko=
github.com/issue9/identicon v0.0.0-20160320065130-d36b54562f4c/go.mod h1:5mTb/PQNkqmq2x3IxlQZE0aSnTksJg7fg/oWmJ5SKXQ=
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc=
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
github.com/jackc/pgx v3.6.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4=
github.com/jaytaylor/html2text v0.0.0-20160923191438-8fb95d837f7d h1:ig/iUfDDg06RVW8OMby+GrmW6K2nPO3AFHlEIdvJSd4=
github.com/jaytaylor/html2text v0.0.0-20160923191438-8fb95d837f7d/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
@@ -384,8 +368,8 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
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/lafriks/xormstore v1.3.1 h1:KpzRUamSV3zmA85Kzw+PZOU9wgMbYsNzuDzLuBMbxpA=
github.com/lafriks/xormstore v1.3.1/go.mod h1:qALRD4Vto2Ic7/A5eplMpu5V62mugtSqFysRwz8FETs=
github.com/lafriks/xormstore v1.3.2 h1:hqi3F8s/B4rz8GuEZZDuHuOxRjeuOpEI/cC7vcnWwH4=
github.com/lafriks/xormstore v1.3.2/go.mod h1:mVNIwIa25QIr8rfR7YlVjrqN/apswHkVdtLCyVYBzXw=
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.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@@ -511,8 +495,6 @@ 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/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shurcooL/httpfs v0.0.0-20190527155220-6a4d4a70508b h1:4kg1wyftSKxLtnPAvcRWakIPpokB9w780/KwrNLnfPA=
github.com/shurcooL/httpfs v0.0.0-20190527155220-6a4d4a70508b/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20160918041101-1dba4b3954bc h1:3wIrJvFb3Pf6B/2mDBnN1G5IfUVev4X5apadQlWOczE=
@@ -532,6 +514,7 @@ github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1
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/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
@@ -773,17 +756,18 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/editorconfig/editorconfig-core-go.v1 v1.3.0 h1:oxOEwvhxLMpWpN+0pb2r9TWrM0DCFBHxbuIlS27tmFg=
gopkg.in/editorconfig/editorconfig-core-go.v1 v1.3.0/go.mod h1:s2mQFI9McjArkyCwyEwU//+luQENTnD/Lfb/7Sj3/kQ=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
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.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.44.2/go.mod h1:M3Cogqpuv0QCi3ExAY5V4uOt4qb/R3xZubo9m8lK5wg=
gopkg.in/ini.v1 v1.46.0 h1:VeDZbLYGaupuvIrsYCEOe/L/2Pcs5n7hdO1ZTjporag=
gopkg.in/ini.v1 v1.46.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.48.0 h1:URjZc+8ugRY5mL5uUeQH/a63JcHwdX9xZaWvmNWD7z8=
gopkg.in/ini.v1 v1.48.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ldap.v3 v3.0.2 h1:R6RBtabK6e1GO0eQKtkyOFbAHO73QesLzI2w2DZ6b9w=
gopkg.in/ldap.v3 v3.0.2/go.mod h1:oxD7NyBuxchC+SgJDE1Q5Od05eGt29SDQVBmV+HYbzw=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
@@ -794,8 +778,6 @@ gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOA
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE=
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M=
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
gopkg.in/testfixtures.v2 v2.5.0 h1:N08B7l2GzFQenyYbzqthDnKAA+cmb17iAZhhFxr7JHw=
gopkg.in/testfixtures.v2 v2.5.0/go.mod h1:vyAq+MYCgNpR29qitQdLZhdbLFf4mR/2MFJRFoQZZ2M=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
@@ -812,14 +794,14 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.2/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
mvdan.cc/xurls/v2 v2.0.0 h1:r1zSOSNS/kqtpmATyMMMvaZ4/djsesbYz5kr0+qMRWc=
mvdan.cc/xurls/v2 v2.0.0/go.mod h1:2/webFPYOXN9jp/lzuj0zuAVlF+9g4KPFJANH1oJhRU=
mvdan.cc/xurls/v2 v2.1.0 h1:KaMb5GLhlcSX+e+qhbRJODnUUBvlw01jt4yrjFIHAuA=
mvdan.cc/xurls/v2 v2.1.0/go.mod h1:5GrSd9rOnKOpZaji1OZLYL/yeAAtGDlo/cFe+8K5n8E=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
strk.kbt.io/projects/go/libravatar v0.0.0-20160628055650-5eed7bff870a h1:8q33ShxKXRwQ7JVd1ZnhIU3hZhwwn0Le+4fTeAackuM=
strk.kbt.io/projects/go/libravatar v0.0.0-20160628055650-5eed7bff870a/go.mod h1:FJGmPh3vz9jSos1L/F91iAgnC/aejc0wIIrF2ZwJxdY=
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 h1:mUcz5b3FJbP5Cvdq7Khzn6J9OCUQJaBwgBkCR+MOwSs=
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251/go.mod h1:FJGmPh3vz9jSos1L/F91iAgnC/aejc0wIIrF2ZwJxdY=
xorm.io/builder v0.3.6 h1:ha28mQ2M+TFx96Hxo+iq6tQgnkC9IZkM6D8w9sKHHF8=
xorm.io/builder v0.3.6/go.mod h1:LEFAPISnRzG+zxaxj2vPicRwz67BdhFreKg8yv8/TgU=
xorm.io/core v0.7.2-0.20190928055935-90aeac8d08eb h1:msX3zG3BPl8Ti+LDzP33/9K7BzO/WqFXk610K1kYKfo=
xorm.io/core v0.7.2-0.20190928055935-90aeac8d08eb/go.mod h1:jJfd0UAEzZ4t87nbQYtVjmqpIODugN6PD2D9E+dJvdM=
xorm.io/core v0.7.2 h1:mEO22A2Z7a3fPaZMk6gKL/jMD80iiyNwRrX5HOv3XLw=
xorm.io/core v0.7.2/go.mod h1:jJfd0UAEzZ4t87nbQYtVjmqpIODugN6PD2D9E+dJvdM=
xorm.io/xorm v0.8.0 h1:iALxgJrX8O00f8Jk22GbZwPmxJNgssV5Mv4uc2HL9PM=
xorm.io/xorm v0.8.0/go.mod h1:ZkJLEYLoVyg7amJK/5r779bHyzs2AU8f8VMiP6BM7uY=

+ 35
- 0
integrations/api_helper_for_declarative_test.go View File

@@ -231,3 +231,38 @@ func doAPIMergePullRequest(ctx APITestContext, owner, repo string, index int64)
ctx.Session.MakeRequest(t, req, 200)
}
}

func doAPIGetBranch(ctx APITestContext, branch string, callback ...func(*testing.T, api.Branch)) func(*testing.T) {
return func(t *testing.T) {
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/branches/%s?token=%s", ctx.Username, ctx.Reponame, branch, ctx.Token)
if ctx.ExpectedCode != 0 {
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
return
}
resp := ctx.Session.MakeRequest(t, req, http.StatusOK)

var branch api.Branch
DecodeJSON(t, resp, &branch)
if len(callback) > 0 {
callback[0](t, branch)
}
}
}

func doAPICreateFile(ctx APITestContext, treepath string, options *api.CreateFileOptions, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) {
return func(t *testing.T) {
url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", ctx.Username, ctx.Reponame, treepath, ctx.Token)
req := NewRequestWithJSON(t, "POST", url, &options)
if ctx.ExpectedCode != 0 {
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
return
}
resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)

var contents api.FileResponse
DecodeJSON(t, resp, &contents)
if len(callback) > 0 {
callback[0](t, contents)
}
}
}

+ 2
- 1
integrations/api_pull_test.go View File

@@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
issue_service "code.gitea.io/gitea/services/issue"

"github.com/stretchr/testify/assert"
)
@@ -40,7 +41,7 @@ func TestAPIMergePullWIP(t *testing.T) {
owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)
pr := models.AssertExistsAndLoadBean(t, &models.PullRequest{Status: models.PullRequestStatusMergeable}, models.Cond("has_merged = ?", false)).(*models.PullRequest)
pr.LoadIssue()
pr.Issue.ChangeTitle(owner, setting.Repository.PullRequest.WorkInProgressPrefixes[0]+" "+pr.Issue.Title)
issue_service.ChangeTitle(pr.Issue, owner, setting.Repository.PullRequest.WorkInProgressPrefixes[0]+" "+pr.Issue.Title)

// force reload
pr.LoadAttributes()

+ 1
- 1
integrations/api_repo_file_create_test.go View File

@@ -91,7 +91,7 @@ func getExpectedFileResponseForCreate(commitID, treePath string) *api.FileRespon
},
Verification: &api.PayloadCommitVerification{
Verified: false,
Reason: "unsigned",
Reason: "gpg.error.not_signed_commit",
Signature: "",
Payload: "",
},

+ 1
- 1
integrations/api_repo_file_update_test.go View File

@@ -94,7 +94,7 @@ func getExpectedFileResponseForUpdate(commitID, treePath string) *api.FileRespon
},
Verification: &api.PayloadCommitVerification{
Verified: false,
Reason: "unsigned",
Reason: "gpg.error.not_signed_commit",
Signature: "",
Payload: "",
},

+ 0
- 1
integrations/api_team_user_test.go View File

@@ -29,7 +29,6 @@ func TestAPITeamUser(t *testing.T) {
var user2 *api.User
DecodeJSON(t, resp, &user2)
user2.Created = user2.Created.In(time.Local)
user2.LastLogin = user2.LastLogin.In(time.Local)
user := models.AssertExistsAndLoadBean(t, &models.User{Name: "user2"}).(*models.User)

assert.Equal(t, convert.ToUser(user, true, false), user2)

+ 1
- 1
integrations/api_user_heatmap_test.go View File

@@ -26,7 +26,7 @@ func TestUserHeatmap(t *testing.T) {
var heatmap []*models.UserHeatmapData
DecodeJSON(t, resp, &heatmap)
var dummyheatmap []*models.UserHeatmapData
dummyheatmap = append(dummyheatmap, &models.UserHeatmapData{Timestamp: 1540080000, Contributions: 1})
dummyheatmap = append(dummyheatmap, &models.UserHeatmapData{Timestamp: 1571616000, Contributions: 1})

assert.Equal(t, dummyheatmap, heatmap)
}

+ 31
- 0
integrations/git_helper_for_declarative_test.go View File

@@ -12,7 +12,9 @@ import (
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"testing"
"time"

@@ -37,7 +39,12 @@ func withKeyFile(t *testing.T, keyname string, callback func(string)) {
err = ssh.GenKeyPair(keyFile)
assert.NoError(t, err)

err = ioutil.WriteFile(path.Join(tmpDir, "ssh"), []byte("#!/bin/bash\n"+
"ssh -o \"UserKnownHostsFile=/dev/null\" -o \"StrictHostKeyChecking=no\" -o \"IdentitiesOnly=yes\" -i \""+keyFile+"\" \"$@\""), 0700)
assert.NoError(t, err)

//Setup ssh wrapper
os.Setenv("GIT_SSH", path.Join(tmpDir, "ssh"))
os.Setenv("GIT_SSH_COMMAND",
"ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i \""+keyFile+"\"")
os.Setenv("GIT_SSH_VARIANT", "ssh")
@@ -54,6 +61,24 @@ func createSSHUrl(gitPath string, u *url.URL) *url.URL {
return &u2
}

func allowLFSFilters() []string {
// Now here we should explicitly allow lfs filters to run
globalArgs := git.GlobalCommandArgs
filteredLFSGlobalArgs := make([]string, len(git.GlobalCommandArgs))
j := 0
for _, arg := range git.GlobalCommandArgs {
if strings.Contains(arg, "lfs") {
j--
} else {
filteredLFSGlobalArgs[j] = arg
j++
}
}
filteredLFSGlobalArgs = filteredLFSGlobalArgs[:j]
git.GlobalCommandArgs = filteredLFSGlobalArgs
return globalArgs
}

func onGiteaRun(t *testing.T, callback func(*testing.T, *url.URL)) {
prepareTestEnv(t, 1)
s := http.Server{
@@ -79,7 +104,9 @@ func onGiteaRun(t *testing.T, callback func(*testing.T, *url.URL)) {

func doGitClone(dstLocalPath string, u *url.URL) func(*testing.T) {
return func(t *testing.T) {
oldGlobals := allowLFSFilters()
assert.NoError(t, git.Clone(u.String(), dstLocalPath, git.CloneRepoOptions{}))
git.GlobalCommandArgs = oldGlobals
assert.True(t, com.IsExist(filepath.Join(dstLocalPath, "README.md")))
}
}
@@ -140,7 +167,9 @@ func doGitCreateBranch(dstPath, branch string) func(*testing.T) {

func doGitCheckoutBranch(dstPath string, args ...string) func(*testing.T) {
return func(t *testing.T) {
oldGlobals := allowLFSFilters()
_, err := git.NewCommand(append([]string{"checkout"}, args...)...).RunInDir(dstPath)
git.GlobalCommandArgs = oldGlobals
assert.NoError(t, err)
}
}
@@ -154,7 +183,9 @@ func doGitMerge(dstPath string, args ...string) func(*testing.T) {

func doGitPull(dstPath string, args ...string) func(*testing.T) {
return func(t *testing.T) {
oldGlobals := allowLFSFilters()
_, err := git.NewCommand(append([]string{"pull"}, args...)...).RunInDir(dstPath)
git.GlobalCommandArgs = oldGlobals
assert.NoError(t, err)
}
}

+ 48
- 14
integrations/git_test.go View File

@@ -19,6 +19,7 @@ import (

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"

"github.com/stretchr/testify/assert"
@@ -135,6 +136,11 @@ func standardCommitAndPushTest(t *testing.T, dstPath string) (little, big string
func lfsCommitAndPushTest(t *testing.T, dstPath string) (littleLFS, bigLFS string) {
t.Run("LFS", func(t *testing.T) {
PrintCurrentTest(t)
setting.CheckLFSVersion()
if !setting.LFS.StartServer {
t.Skip()
return
}
prefix := "lfs-data-file-"
_, err := git.NewCommand("lfs").AddArguments("install").RunInDir(dstPath)
assert.NoError(t, err)
@@ -142,6 +148,21 @@ func lfsCommitAndPushTest(t *testing.T, dstPath string) (littleLFS, bigLFS strin
assert.NoError(t, err)
err = git.AddChanges(dstPath, false, ".gitattributes")
assert.NoError(t, err)
oldGlobals := allowLFSFilters()
err = git.CommitChanges(dstPath, git.CommitChangesOptions{
Committer: &git.Signature{
Email: "user2@example.com",
Name: "User Two",
When: time.Now(),
},
Author: &git.Signature{
Email: "user2@example.com",
Name: "User Two",
When: time.Now(),
},
Message: fmt.Sprintf("Testing commit @ %v", time.Now()),
})
git.GlobalCommandArgs = oldGlobals

littleLFS, bigLFS = commitAndPushTest(t, dstPath, prefix)

@@ -185,20 +206,25 @@ func rawTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS s
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, littleSize, resp.Body.Len())

req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", littleLFS))
resp = session.MakeRequest(t, req, http.StatusOK)
assert.NotEqual(t, littleSize, resp.Body.Len())
assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier)
setting.CheckLFSVersion()
if setting.LFS.StartServer {
req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", littleLFS))
resp = session.MakeRequest(t, req, http.StatusOK)
assert.NotEqual(t, littleSize, resp.Body.Len())
assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier)
}

if !testing.Short() {
req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", big))
resp = session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, bigSize, resp.Body.Len())

req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", bigLFS))
resp = session.MakeRequest(t, req, http.StatusOK)
assert.NotEqual(t, bigSize, resp.Body.Len())
assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier)
if setting.LFS.StartServer {
req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", bigLFS))
resp = session.MakeRequest(t, req, http.StatusOK)
assert.NotEqual(t, bigSize, resp.Body.Len())
assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier)
}
}
})
}
@@ -217,18 +243,23 @@ func mediaTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS
resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
assert.Equal(t, littleSize, resp.Length)

req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", littleLFS))
resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
assert.Equal(t, littleSize, resp.Length)
setting.CheckLFSVersion()
if setting.LFS.StartServer {
req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", littleLFS))
resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
assert.Equal(t, littleSize, resp.Length)
}

if !testing.Short() {
req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", big))
resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
assert.Equal(t, bigSize, resp.Length)

req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", bigLFS))
resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
assert.Equal(t, bigSize, resp.Length)
if setting.LFS.StartServer {
req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", bigLFS))
resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
assert.Equal(t, bigSize, resp.Length)
}
}
})
}
@@ -274,6 +305,8 @@ func generateCommitWithNewData(size int, repoPath, email, fullName, prefix strin
}

//Commit
// Now here we should explicitly allow lfs filters to run
oldGlobals := allowLFSFilters()
err = git.AddChanges(repoPath, false, filepath.Base(tmpFile.Name()))
if err != nil {
return "", err
@@ -291,6 +324,7 @@ func generateCommitWithNewData(size int, repoPath, email, fullName, prefix strin
},
Message: fmt.Sprintf("Testing commit @ %v", time.Now()),
})
git.GlobalCommandArgs = oldGlobals
return filepath.Base(tmpFile.Name()), err
}


+ 252
- 0
integrations/gpg_git_test.go View File

@@ -0,0 +1,252 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package integrations

import (
"encoding/base64"
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"testing"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"github.com/stretchr/testify/assert"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/armor"
)

func TestGPGGit(t *testing.T) {
onGiteaRun(t, testGPGGit)
}

func testGPGGit(t *testing.T, u *url.URL) {
username := "user2"
baseAPITestContext := NewAPITestContext(t, username, "repo1")

u.Path = baseAPITestContext.GitPath()

// OK Set a new GPG home
tmpDir, err := ioutil.TempDir("", "temp-gpg")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)

err = os.Chmod(tmpDir, 0700)
assert.NoError(t, err)

oldGNUPGHome := os.Getenv("GNUPGHOME")
err = os.Setenv("GNUPGHOME", tmpDir)
assert.NoError(t, err)
defer os.Setenv("GNUPGHOME", oldGNUPGHome)

// Need to create a root key
rootKeyPair, err := createGPGKey(tmpDir, "gitea", "gitea@fake.local")
assert.NoError(t, err)

rootKeyID := rootKeyPair.PrimaryKey.KeyIdShortString()

oldKeyID := setting.Repository.Signing.SigningKey
oldName := setting.Repository.Signing.SigningName
oldEmail := setting.Repository.Signing.SigningEmail
defer func() {
setting.Repository.Signing.SigningKey = oldKeyID
setting.Repository.Signing.SigningName = oldName
setting.Repository.Signing.SigningEmail = oldEmail
}()

setting.Repository.Signing.SigningKey = rootKeyID
setting.Repository.Signing.SigningName = "gitea"
setting.Repository.Signing.SigningEmail = "gitea@fake.local"
user := models.AssertExistsAndLoadBean(t, &models.User{Name: username}).(*models.User)

t.Run("Unsigned-Initial", func(t *testing.T) {
PrintCurrentTest(t)
setting.Repository.Signing.InitialCommit = []string{"never"}
testCtx := NewAPITestContext(t, username, "initial-unsigned")
t.Run("CreateRepository", doAPICreateRepository(testCtx, false))
t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
assert.NotNil(t, branch.Commit)
assert.NotNil(t, branch.Commit.Verification)
assert.False(t, branch.Commit.Verification.Verified)
assert.Empty(t, branch.Commit.Verification.Signature)
}))
setting.Repository.Signing.CRUDActions = []string{"never"}
t.Run("CreateCRUDFile-Never", crudActionCreateFile(
t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) {
assert.False(t, response.Verification.Verified)
}))
t.Run("CreateCRUDFile-Never", crudActionCreateFile(
t, testCtx, user, "never", "never2", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) {
assert.False(t, response.Verification.Verified)
}))
setting.Repository.Signing.CRUDActions = []string{"parentsigned"}
t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile(
t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) {
assert.False(t, response.Verification.Verified)
}))
t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile(
t, testCtx, user, "parentsigned", "parentsigned2", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) {
assert.False(t, response.Verification.Verified)
}))
setting.Repository.Signing.CRUDActions = []string{"never"}
t.Run("CreateCRUDFile-Never", crudActionCreateFile(
t, testCtx, user, "parentsigned", "parentsigned-never", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) {
assert.False(t, response.Verification.Verified)
}))
setting.Repository.Signing.CRUDActions = []string{"always"}
t.Run("CreateCRUDFile-Always", crudActionCreateFile(
t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) {
assert.True(t, response.Verification.Verified)
assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email)
}))
t.Run("CreateCRUDFile-ParentSigned-always", crudActionCreateFile(
t, testCtx, user, "parentsigned", "parentsigned-always", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) {
assert.True(t, response.Verification.Verified)
assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email)
}))
setting.Repository.Signing.CRUDActions = []string{"parentsigned"}
t.Run("CreateCRUDFile-Always-ParentSigned", crudActionCreateFile(
t, testCtx, user, "always", "always-parentsigned", "signed-always-parentsigned.txt", func(t *testing.T, response api.FileResponse) {
assert.True(t, response.Verification.Verified)
assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email)
}))
})
t.Run("AlwaysSign-Initial", func(t *testing.T) {
PrintCurrentTest(t)
setting.Repository.Signing.InitialCommit = []string{"always"}
testCtx := NewAPITestContext(t, username, "initial-always")
t.Run("CreateRepository", doAPICreateRepository(testCtx, false))
t.Run("CheckMasterBranchSigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
assert.NotNil(t, branch.Commit)
assert.NotNil(t, branch.Commit.Verification)
assert.True(t, branch.Commit.Verification.Verified)
assert.Equal(t, "gitea@fake.local", branch.Commit.Verification.Signer.Email)
}))
setting.Repository.Signing.CRUDActions = []string{"never"}
t.Run("CreateCRUDFile-Never", crudActionCreateFile(
t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) {
assert.False(t, response.Verification.Verified)
}))
setting.Repository.Signing.CRUDActions = []string{"parentsigned"}
t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile(
t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) {
assert.True(t, response.Verification.Verified)
assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email)
}))
setting.Repository.Signing.CRUDActions = []string{"always"}
t.Run("CreateCRUDFile-Always", crudActionCreateFile(
t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) {
assert.True(t, response.Verification.Verified)
assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email)
}))

})
t.Run("UnsignedMerging", func(t *testing.T) {
PrintCurrentTest(t)
testCtx := NewAPITestContext(t, username, "initial-unsigned")
var pr api.PullRequest
var err error
t.Run("CreatePullRequest", func(t *testing.T) {
pr, err = doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "never2")(t)
assert.NoError(t, err)
})
setting.Repository.Signing.Merges = []string{"commitssigned"}
t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index))
t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
assert.NotNil(t, branch.Commit)
assert.NotNil(t, branch.Commit.Verification)
assert.False(t, branch.Commit.Verification.Verified)
assert.Empty(t, branch.Commit.Verification.Signature)
}))
setting.Repository.Signing.Merges = []string{"basesigned"}
t.Run("CreatePullRequest", func(t *testing.T) {
pr, err = doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "parentsigned2")(t)
assert.NoError(t, err)
})
t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index))
t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
assert.NotNil(t, branch.Commit)
assert.NotNil(t, branch.Commit.Verification)
assert.False(t, branch.Commit.Verification.Verified)
assert.Empty(t, branch.Commit.Verification.Signature)
}))
setting.Repository.Signing.Merges = []string{"commitssigned"}
t.Run("CreatePullRequest", func(t *testing.T) {
pr, err = doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "always-parentsigned")(t)
assert.NoError(t, err)
})
t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index))
t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
assert.NotNil(t, branch.Commit)
assert.NotNil(t, branch.Commit.Verification)
assert.True(t, branch.Commit.Verification.Verified)
}))

})
}

func crudActionCreateFile(t *testing.T, ctx APITestContext, user *models.User, from, to, path string, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) {
return doAPICreateFile(ctx, path, &api.CreateFileOptions{
FileOptions: api.FileOptions{
BranchName: from,
NewBranchName: to,
Message: fmt.Sprintf("from:%s to:%s path:%s", from, to, path),
Author: api.Identity{
Name: user.FullName,
Email: user.Email,
},
Committer: api.Identity{
Name: user.FullName,
Email: user.Email,
},
},
Content: base64.StdEncoding.EncodeToString([]byte("This is new text")),
}, callback...)
}

func createGPGKey(tmpDir, name, email string) (*openpgp.Entity, error) {
keyPair, err := openpgp.NewEntity(name, "test", email, nil)
if err != nil {
return nil, err
}

for _, id := range keyPair.Identities {
err := id.SelfSignature.SignUserId(id.UserId.Id, keyPair.PrimaryKey, keyPair.PrivateKey, nil)
if err != nil {
return nil, err
}
}

keyFile := filepath.Join(tmpDir, "temporary.key")
keyWriter, err := os.Create(keyFile)
if err != nil {
return nil, err
}
defer keyWriter.Close()
defer os.Remove(keyFile)

w, err := armor.Encode(keyWriter, openpgp.PrivateKeyType, nil)
if err != nil {
return nil, err
}
defer w.Close()

keyPair.SerializePrivate(w, nil)
if err := w.Close(); err != nil {
return nil, err
}
if err := keyWriter.Close(); err != nil {
return nil, err
}

if _, _, err := process.GetManager().Exec("gpg --import temporary.key", "gpg", "--import", keyFile); err != nil {
return nil, err
}
return keyPair, nil
}

+ 7
- 6
integrations/issue_test.go View File

@@ -13,6 +13,7 @@ import (
"testing"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/references"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"

@@ -207,7 +208,7 @@ func TestIssueCrossReference(t *testing.T) {
RefIssueID: issueRef.ID,
RefCommentID: 0,
RefIsPull: false,
RefAction: models.XRefActionNone})
RefAction: references.XRefActionNone})

// Edit title, neuter ref
testIssueChangeInfo(t, "user2", issueRefURL, "title", "Title no ref")
@@ -217,7 +218,7 @@ func TestIssueCrossReference(t *testing.T) {
RefIssueID: issueRef.ID,
RefCommentID: 0,
RefIsPull: false,
RefAction: models.XRefActionNeutered})
RefAction: references.XRefActionNeutered})

// Ref from issue content
issueRefURL, issueRef = testIssueWithBean(t, "user2", 1, "TitleXRef", fmt.Sprintf("Description ref #%d", issueBase.Index))
@@ -227,7 +228,7 @@ func TestIssueCrossReference(t *testing.T) {
RefIssueID: issueRef.ID,
RefCommentID: 0,
RefIsPull: false,
RefAction: models.XRefActionNone})
RefAction: references.XRefActionNone})

// Edit content, neuter ref
testIssueChangeInfo(t, "user2", issueRefURL, "content", "Description no ref")
@@ -237,7 +238,7 @@ func TestIssueCrossReference(t *testing.T) {
RefIssueID: issueRef.ID,
RefCommentID: 0,
RefIsPull: false,
RefAction: models.XRefActionNeutered})
RefAction: references.XRefActionNeutered})

// Ref from a comment
session := loginUser(t, "user2")
@@ -248,7 +249,7 @@ func TestIssueCrossReference(t *testing.T) {
RefIssueID: issueRef.ID,
RefCommentID: commentID,
RefIsPull: false,
RefAction: models.XRefActionNone}
RefAction: references.XRefActionNone}
models.AssertExistsAndLoadBean(t, comment)

// Ref from a different repository
@@ -259,7 +260,7 @@ func TestIssueCrossReference(t *testing.T) {
RefIssueID: issueRef.ID,
RefCommentID: 0,
RefIsPull: false,
RefAction: models.XRefActionNone})
RefAction: references.XRefActionNone})
}

func testIssueWithBean(t *testing.T, user string, repoID int64, title, content string) (string, *models.Issue) {

+ 5
- 0
integrations/lfs_getobject_test.go View File

@@ -58,6 +58,11 @@ func storeObjectInRepo(t *testing.T, repositoryID int64, content *[]byte) string

func doLfs(t *testing.T, content *[]byte, expectGzip bool) {
prepareTestEnv(t)
setting.CheckLFSVersion()
if !setting.LFS.StartServer {
t.Skip()
return
}
repo, err := models.GetRepositoryByOwnerAndName("user2", "repo1")
assert.NoError(t, err)
oid := storeObjectInRepo(t, repo.ID, content)

+ 1
- 1
integrations/migration-test/migration_test.go View File

@@ -23,8 +23,8 @@ import (
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/setting"

"github.com/go-xorm/xorm"
"github.com/stretchr/testify/assert"
"xorm.io/xorm"
)

var currentEngine *xorm.Engine

+ 3
- 0
integrations/mssql.ini.tmpl View File

@@ -21,6 +21,9 @@ ROOT = integrations/gitea-integration-mssql/gitea-repositories
LOCAL_COPY_PATH = tmp/local-repo-mssql
LOCAL_WIKI_PATH = tmp/local-wiki-mssql

[repository.signing]
SIGNING_KEY = none

[server]
SSH_DOMAIN = localhost
HTTP_PORT = 3003

+ 3
- 0
integrations/mysql.ini.tmpl View File

@@ -21,6 +21,9 @@ ROOT = integrations/gitea-integration-mysql/gitea-repositories
LOCAL_COPY_PATH = tmp/local-repo-mysql
LOCAL_WIKI_PATH = tmp/local-wiki-mysql

[repository.signing]
SIGNING_KEY = none

[server]
SSH_DOMAIN = localhost
HTTP_PORT = 3001

+ 3
- 0
integrations/mysql8.ini.tmpl View File

@@ -21,6 +21,9 @@ ROOT = integrations/gitea-integration-mysql8/gitea-repositories
LOCAL_COPY_PATH = tmp/local-repo-mysql8
LOCAL_WIKI_PATH = tmp/local-wiki-mysql8

[repository.signing]
SIGNING_KEY = none

[server]
SSH_DOMAIN = localhost
HTTP_PORT = 3004

+ 3
- 0
integrations/pgsql.ini.tmpl View File

@@ -21,6 +21,9 @@ ROOT = integrations/gitea-integration-pgsql/gitea-repositories
LOCAL_COPY_PATH = tmp/local-repo-pgsql
LOCAL_WIKI_PATH = tmp/local-wiki-pgsql

[repository.signing]
SIGNING_KEY = none

[server]
SSH_DOMAIN = localhost
HTTP_PORT = 3002

+ 1
- 1
integrations/repofiles_delete_test.go View File

@@ -53,7 +53,7 @@ func getExpectedDeleteFileResponse(u *url.URL) *api.FileResponse {
},
Verification: &api.PayloadCommitVerification{
Verified: false,
Reason: "",
Reason: "gpg.error.not_signed_commit",
Signature: "",
Payload: "",
},

+ 2
- 2
integrations/repofiles_update_test.go View File

@@ -108,7 +108,7 @@ func getExpectedFileResponseForRepofilesCreate(commitID string) *api.FileRespons
},
Verification: &api.PayloadCommitVerification{
Verified: false,
Reason: "unsigned",
Reason: "gpg.error.not_signed_commit",
Signature: "",
Payload: "",
},
@@ -175,7 +175,7 @@ func getExpectedFileResponseForRepofilesUpdate(commitID, filename string) *api.F
},
Verification: &api.PayloadCommitVerification{
Verified: false,
Reason: "unsigned",
Reason: "gpg.error.not_signed_commit",
Signature: "",
Payload: "",
},

+ 3
- 0
integrations/sqlite.ini View File

@@ -17,6 +17,9 @@ ROOT = integrations/gitea-integration-sqlite/gitea-repositories
LOCAL_COPY_PATH = tmp/local-repo-sqlite
LOCAL_WIKI_PATH = tmp/local-wiki-sqlite

[repository.signing]
SIGNING_KEY = none

[server]
SSH_DOMAIN = localhost
HTTP_PORT = 3003

+ 49
- 0
models/access.go View File

@@ -246,6 +246,55 @@ func (repo *Repository) recalculateTeamAccesses(e Engine, ignTeamID int64) (err
return repo.refreshAccesses(e, accessMap)
}

// recalculateUserAccess recalculates new access for a single user
// Usable if we know access only affected one user
func (repo *Repository) recalculateUserAccess(e Engine, uid int64) (err error) {
minMode := AccessModeRead
if !repo.IsPrivate {
minMode = AccessModeWrite
}

accessMode := AccessModeNone
collaborator, err := repo.getCollaboration(e, uid)
if err != nil {
return err
} else if collaborator != nil {
accessMode = collaborator.Mode
}

if err = repo.getOwner(e); err != nil {
return err
} else if repo.Owner.IsOrganization() {
var teams []Team
if err := e.Join("INNER", "team_repo", "team_repo.team_id = team.id").
Join("INNER", "team_user", "team_user.team_id = team.id").
Where("team.org_id = ?", repo.OwnerID).
And("team_repo.repo_id=?", repo.ID).
And("team_user.uid=?", uid).
Find(&teams); err != nil {
return err
}

for _, t := range teams {
if t.IsOwnerTeam() {
t.Authorize = AccessModeOwner
}

accessMode = maxAccessMode(accessMode, t.Authorize)
}
}

// Delete old user accesses and insert new one for repository.
if _, err = e.Delete(&Access{RepoID: repo.ID, UserID: uid}); err != nil {
return fmt.Errorf("delete old user accesses: %v", err)
} else if accessMode >= minMode {
if _, err = e.Insert(&Access{RepoID: repo.ID, UserID: uid, Mode: accessMode}); err != nil {
return fmt.Errorf("insert new user accesses: %v", err)
}
}
return nil
}

func (repo *Repository) recalculateAccesses(e Engine) error {
if repo.Owner.IsOrganization() {
return repo.recalculateTeamAccesses(e, 0)

+ 37
- 214
models/action.go View File

@@ -6,19 +6,17 @@
package models

import (
"encoding/json"
"fmt"
"html"
"path"
"regexp"
"strconv"
"strings"
"time"
"unicode"

"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/references"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
@@ -54,29 +52,6 @@ const (
ActionMirrorSyncDelete // 20
)

var (
// Same as GitHub. See
// https://help.github.com/articles/closing-issues-via-commit-messages
issueCloseKeywords = []string{"close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved"}
issueReopenKeywords = []string{"reopen", "reopens", "reopened"}

issueCloseKeywordsPat, issueReopenKeywordsPat *regexp.Regexp
issueReferenceKeywordsPat *regexp.Regexp
)

const issueRefRegexpStr = `(?:([0-9a-zA-Z-_\.]+)/([0-9a-zA-Z-_\.]+))?(#[0-9]+)+`
const issueRefRegexpStrNoKeyword = `(?:\s|^|\(|\[)(?:([0-9a-zA-Z-_\.]+)/([0-9a-zA-Z-_\.]+))?(#[0-9]+)(?:\s|$|\)|\]|:|\.(\s|$))`

func assembleKeywordsPattern(words []string) string {
return fmt.Sprintf(`(?i)(?:%s)(?::?) %s`, strings.Join(words, "|"), issueRefRegexpStr)
}

func init() {
issueCloseKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(issueCloseKeywords))
issueReopenKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(issueReopenKeywords))
issueReferenceKeywordsPat = regexp.MustCompile(issueRefRegexpStrNoKeyword)
}

// Action represents user operation type and other information to
// repository. It implemented interface base.Actioner so that can be
// used in template render.
@@ -351,10 +326,6 @@ func RenameRepoAction(actUser *User, oldRepoName string, repo *Repository) error
return renameRepoAction(x, actUser, oldRepoName, repo)
}

func issueIndexTrimRight(c rune) bool {
return !unicode.IsDigit(c)
}

// PushCommit represents a commit in a push operation.
type PushCommit struct {
Sha1 string
@@ -480,39 +451,9 @@ func (pc *PushCommits) AvatarLink(email string) string {
}

// getIssueFromRef returns the issue referenced by a ref. Returns a nil *Issue
// if the provided ref is misformatted or references a non-existent issue.
func getIssueFromRef(repo *Repository, ref string) (*Issue, error) {
ref = ref[strings.IndexByte(ref, ' ')+1:]
ref = strings.TrimRightFunc(ref, issueIndexTrimRight)

var refRepo *Repository
poundIndex := strings.IndexByte(ref, '#')
if poundIndex < 0 {
return nil, nil
} else if poundIndex == 0 {
refRepo = repo
} else {
slashIndex := strings.IndexByte(ref, '/')
if slashIndex < 0 || slashIndex >= poundIndex {
return nil, nil
}
ownerName := ref[:slashIndex]
repoName := ref[slashIndex+1 : poundIndex]
var err error
refRepo, err = GetRepositoryByOwnerAndName(ownerName, repoName)
if err != nil {
if IsErrRepoNotExist(err) {
return nil, nil
}
return nil, err
}
}
issueIndex, err := strconv.ParseInt(ref[poundIndex+1:], 10, 64)
if err != nil {
return nil, nil
}

issue, err := GetIssueByIndex(refRepo.ID, issueIndex)
// if the provided ref references a non-existent issue.
func getIssueFromRef(repo *Repository, index int64) (*Issue, error) {
issue, err := GetIssueByIndex(repo.ID, index)
if err != nil {
if IsErrIssueNotExist(err) {
return nil, nil
@@ -522,20 +463,7 @@ func getIssueFromRef(repo *Repository, ref string) (*Issue, error) {
return issue, nil
}

func changeIssueStatus(repo *Repository, doer *User, ref string, refMarked map[int64]bool, status bool) error {
issue, err := getIssueFromRef(repo, ref)
if err != nil {
return err
}

if issue == nil || refMarked[issue.ID] {
return nil
}
refMarked[issue.ID] = true

if issue.RepoID != repo.ID || issue.IsClosed == status {
return nil
}
func changeIssueStatus(repo *Repository, issue *Issue, doer *User, status bool) error {

stopTimerIfAvailable := func(doer *User, issue *Issue) error {

@@ -549,7 +477,7 @@ func changeIssueStatus(repo *Repository, doer *User, ref string, refMarked map[i
}

issue.Repo = repo
if err = issue.ChangeStatus(doer, status); err != nil {
if err := issue.ChangeStatus(doer, status); err != nil {
// Don't return an error when dependencies are open as this would let the push fail
if IsErrDependenciesLeft(err) {
return stopTimerIfAvailable(doer, issue)
@@ -566,99 +494,67 @@ func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit, bra
for i := len(commits) - 1; i >= 0; i-- {
c := commits[i]

refMarked := make(map[int64]bool)
type markKey struct {
ID int64
Action references.XRefAction
}

refMarked := make(map[markKey]bool)
var refRepo *Repository
var refIssue *Issue
var err error
for _, m := range issueReferenceKeywordsPat.FindAllStringSubmatch(c.Message, -1) {
if len(m[3]) == 0 {
continue
}
ref := m[3]
for _, ref := range references.FindAllIssueReferences(c.Message) {

// issue is from another repo
if len(m[1]) > 0 && len(m[2]) > 0 {
refRepo, err = GetRepositoryFromMatch(m[1], m[2])
if len(ref.Owner) > 0 && len(ref.Name) > 0 {
refRepo, err = GetRepositoryFromMatch(ref.Owner, ref.Name)
if err != nil {
continue
}
} else {
refRepo = repo
}
issue, err := getIssueFromRef(refRepo, ref)
if err != nil {
if refIssue, err = getIssueFromRef(refRepo, ref.Index); err != nil {
return err
}

if issue == nil || refMarked[issue.ID] {
if refIssue == nil {
continue
}
refMarked[issue.ID] = true

message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, repo.Link(), c.Sha1, html.EscapeString(c.Message))
if err = CreateRefComment(doer, refRepo, issue, message, c.Sha1); err != nil {
perm, err := GetUserRepoPermission(refRepo, doer)
if err != nil {
return err
}
}

// Change issue status only if the commit has been pushed to the default branch.
// and if the repo is configured to allow only that
if repo.DefaultBranch != branchName && !repo.CloseIssuesViaCommitInAnyBranch {
continue
}
refMarked = make(map[int64]bool)
for _, m := range issueCloseKeywordsPat.FindAllStringSubmatch(c.Message, -1) {
if len(m[3]) == 0 {
key := markKey{ID: refIssue.ID, Action: ref.Action}
if refMarked[key] {
continue
}
ref := m[3]

// issue is from another repo
if len(m[1]) > 0 && len(m[2]) > 0 {
refRepo, err = GetRepositoryFromMatch(m[1], m[2])
if err != nil {
continue
}
} else {
refRepo = repo
}
refMarked[key] = true

perm, err := GetUserRepoPermission(refRepo, doer)
if err != nil {
return err
}
// only close issues in another repo if user has push access
if perm.CanWrite(UnitTypeCode) {
if err := changeIssueStatus(refRepo, doer, ref, refMarked, true); err != nil {
// only create comments for issues if user has permission for it
if perm.IsAdmin() || perm.IsOwner() || perm.CanWrite(UnitTypeIssues) {
message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, repo.Link(), c.Sha1, html.EscapeString(c.Message))
if err = CreateRefComment(doer, refRepo, refIssue, message, c.Sha1); err != nil {
return err
}
}
}

// It is conflict to have close and reopen at same time, so refsMarked doesn't need to reinit here.
for _, m := range issueReopenKeywordsPat.FindAllStringSubmatch(c.Message, -1) {
if len(m[3]) == 0 {
// Process closing/reopening keywords
if ref.Action != references.XRefActionCloses && ref.Action != references.XRefActionReopens {
continue
}
ref := m[3]

// issue is from another repo
if len(m[1]) > 0 && len(m[2]) > 0 {
refRepo, err = GetRepositoryFromMatch(m[1], m[2])
if err != nil {
continue
}
} else {
refRepo = repo
}

perm, err := GetUserRepoPermission(refRepo, doer)
if err != nil {
return err
// Change issue status only if the commit has been pushed to the default branch.
// and if the repo is configured to allow only that
// FIXME: we should be using Issue.ref if set instead of repo.DefaultBranch
if repo.DefaultBranch != branchName && !repo.CloseIssuesViaCommitInAnyBranch {
continue
}

// only reopen issues in another repo if user has push access
if perm.CanWrite(UnitTypeCode) {
if err := changeIssueStatus(refRepo, doer, ref, refMarked, false); err != nil {
// only close issues in another repo if user has push access
if perm.IsAdmin() || perm.IsOwner() || perm.CanWrite(UnitTypeCode) {
if err := changeIssueStatus(refRepo, refIssue, doer, ref.Action == references.XRefActionCloses); err != nil {
return err
}
}
@@ -713,79 +609,6 @@ func MergePullRequestAction(actUser *User, repo *Repository, pull *Issue) error
return mergePullRequestAction(x, actUser, repo, pull)
}

func mirrorSyncAction(e Engine, opType ActionType, repo *Repository, refName string, data []byte) error {
if err := notifyWatchers(e, &Action{
ActUserID: repo.OwnerID,
ActUser: repo.MustOwner(),
OpType: opType,
RepoID: repo.ID,
Repo: repo,
IsPrivate: repo.IsPrivate,
RefName: refName,
Content: string(data),
}); err != nil {
return fmt.Errorf("notifyWatchers: %v", err)
}

defer func() {
go HookQueue.Add(repo.ID)
}()

return nil
}

// MirrorSyncPushActionOptions mirror synchronization action options.
type MirrorSyncPushActionOptions struct {
RefName string
OldCommitID string
NewCommitID string
Commits *PushCommits
}

// MirrorSyncPushAction adds new action for mirror synchronization of pushed commits.
func MirrorSyncPushAction(repo *Repository, opts MirrorSyncPushActionOptions) error {
if len(opts.Commits.Commits) > setting.UI.FeedMaxCommitNum {
opts.Commits.Commits = opts.Commits.Commits[:setting.UI.FeedMaxCommitNum]
}

apiCommits, err := opts.Commits.ToAPIPayloadCommits(repo.RepoPath(), repo.HTMLURL())
if err != nil {
return err
}

opts.Commits.CompareURL = repo.ComposeCompareURL(opts.OldCommitID, opts.NewCommitID)
apiPusher := repo.MustOwner().APIFormat()
if err := PrepareWebhooks(repo, HookEventPush, &api.PushPayload{
Ref: opts.RefName,
Before: opts.OldCommitID,
After: opts.NewCommitID,
CompareURL: setting.AppURL + opts.Commits.CompareURL,
Commits: apiCommits,
Repo: repo.APIFormat(AccessModeOwner),
Pusher: apiPusher,
Sender: apiPusher,
}); err != nil {
return fmt.Errorf("PrepareWebhooks: %v", err)
}

data, err := json.Marshal(opts.Commits)
if err != nil {
return err
}

return mirrorSyncAction(x, ActionMirrorSyncPush, repo, opts.RefName, data)
}

// MirrorSyncCreateAction adds new action for mirror synchronization of new reference.
func MirrorSyncCreateAction(repo *Repository, refName string) error {
return mirrorSyncAction(x, ActionMirrorSyncCreate, repo, refName, nil)
}

// MirrorSyncDeleteAction adds new action for mirror synchronization of delete reference.
func MirrorSyncDeleteAction(repo *Repository, refName string) error {
return mirrorSyncAction(x, ActionMirrorSyncDelete, repo, refName, nil)
}

// GetFeedsOptions options for retrieving feeds
type GetFeedsOptions struct {
RequestedUser *User

+ 1
- 52
models/action_test.go View File

@@ -1,7 +1,6 @@
package models

import (
"fmt"
"path"
"strings"
"testing"
@@ -181,56 +180,6 @@ func TestPushCommits_AvatarLink(t *testing.T) {
pushCommits.AvatarLink("nonexistent@example.com"))
}

func TestRegExp_issueReferenceKeywordsPat(t *testing.T) {
trueTestCases := []string{
"#2",
"[#2]",
"please see go-gitea/gitea#5",
"#2:",
}
falseTestCases := []string{
"kb#2",
"#2xy",
}

for _, testCase := range trueTestCases {
assert.True(t, issueReferenceKeywordsPat.MatchString(testCase))
}
for _, testCase := range falseTestCases {
assert.False(t, issueReferenceKeywordsPat.MatchString(testCase))
}
}

func Test_getIssueFromRef(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
for _, test := range []struct {
Ref string
ExpectedIssueID int64
}{
{"#2", 2},
{"reopen #2", 2},
{"user2/repo2#1", 4},
{"fixes user2/repo2#1", 4},
{"fixes: user2/repo2#1", 4},
} {
issue, err := getIssueFromRef(repo, test.Ref)
assert.NoError(t, err)
if assert.NotNil(t, issue) {
assert.EqualValues(t, test.ExpectedIssueID, issue.ID)
}
}

for _, badRef := range []string{
"doesnotexist/doesnotexist#1",
fmt.Sprintf("#%d", NonexistentID),
} {
issue, err := getIssueFromRef(repo, badRef)
assert.NoError(t, err)
assert.Nil(t, issue)
}
}

func TestUpdateIssuesCommit(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
pushCommits := []*PushCommit{
@@ -431,7 +380,7 @@ func TestUpdateIssuesCommit_AnotherRepoNoPermission(t *testing.T) {
AssertNotExistsBean(t, commentBean)
AssertNotExistsBean(t, issueBean, "is_closed=1")
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, repo.DefaultBranch))
AssertExistsAndLoadBean(t, commentBean)
AssertNotExistsBean(t, commentBean)
AssertNotExistsBean(t, issueBean, "is_closed=1")
CheckConsistencyFor(t, &Action{})
}

+ 1
- 1
models/attachment.go View File

@@ -14,8 +14,8 @@ import (
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"

"github.com/go-xorm/xorm"
gouuid "github.com/satori/go.uuid"
"xorm.io/xorm"
)

// Attachment represent a attachment of issue/comment/release.

+ 23
- 1
models/branches.go View File

@@ -34,6 +34,7 @@ type ProtectedBranch struct {
WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"`
WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"`
@@ -195,7 +196,7 @@ func UpdateProtectBranch(repo *Repository, protectBranch *ProtectedBranch, opts
}
protectBranch.MergeWhitelistUserIDs = whitelist

whitelist, err = updateUserWhitelist(repo, protectBranch.ApprovalsWhitelistUserIDs, opts.ApprovalsUserIDs)
whitelist, err = updateApprovalWhitelist(repo, protectBranch.ApprovalsWhitelistUserIDs, opts.ApprovalsUserIDs)
if err != nil {
return err
}
@@ -301,6 +302,27 @@ func (repo *Repository) IsProtectedBranchForMerging(pr *PullRequest, branchName
return false, nil
}

// updateApprovalWhitelist checks whether the user whitelist changed and returns a whitelist with
// the users from newWhitelist which have explicit read or write access to the repo.
func updateApprovalWhitelist(repo *Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {
hasUsersChanged := !util.IsSliceInt64Eq(currentWhitelist, newWhitelist)
if !hasUsersChanged {
return currentWhitelist, nil
}

whitelist = make([]int64, 0, len(newWhitelist))
for _, userID := range newWhitelist {
if reader, err := repo.IsReader(userID); err != nil {
return nil, err
} else if !reader {
continue
}
whitelist = append(whitelist, userID)
}

return
}

// updateUserWhitelist checks whether the user whitelist changed and returns a whitelist with
// the users from newWhitelist which have write access to the repo.
func updateUserWhitelist(repo *Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {

+ 1
- 1
models/commit_status.go View File

@@ -16,7 +16,7 @@ import (
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"

"github.com/go-xorm/xorm"
"xorm.io/xorm"
)

// CommitStatusState holds the state of a Status

+ 125
- 18
models/external_login_user.go View File

@@ -4,13 +4,34 @@

package models

import "github.com/markbates/goth"
import (
"time"

"code.gitea.io/gitea/modules/structs"

"github.com/markbates/goth"
"xorm.io/builder"
)

// ExternalLoginUser makes the connecting between some existing user and additional external login sources
type ExternalLoginUser struct {
ExternalID string `xorm:"pk NOT NULL"`
UserID int64 `xorm:"INDEX NOT NULL"`
LoginSourceID int64 `xorm:"pk NOT NULL"`
ExternalID string `xorm:"pk NOT NULL"`
UserID int64 `xorm:"INDEX NOT NULL"`
LoginSourceID int64 `xorm:"pk NOT NULL"`
RawData map[string]interface{} `xorm:"TEXT JSON"`
Provider string `xorm:"index VARCHAR(25)"`
Email string
Name string
FirstName string
LastName string
NickName string
Description string
AvatarURL string
Location string
AccessToken string `xorm:"TEXT"`
AccessTokenSecret string `xorm:"TEXT"`
RefreshToken string `xorm:"TEXT"`
ExpiresAt time.Time
}

// GetExternalLogin checks if a externalID in loginSourceID scope already exists
@@ -32,23 +53,15 @@ func ListAccountLinks(user *User) ([]*ExternalLoginUser, error) {
return externalAccounts, nil
}

// LinkAccountToUser link the gothUser to the user
func LinkAccountToUser(user *User, gothUser goth.User) error {
loginSource, err := GetActiveOAuth2LoginSourceByName(gothUser.Provider)
if err != nil {
return err
}

externalLoginUser := &ExternalLoginUser{
ExternalID: gothUser.UserID,
UserID: user.ID,
LoginSourceID: loginSource.ID,
}
has, err := x.Get(externalLoginUser)
// LinkExternalToUser link the external user to the user
func LinkExternalToUser(user *User, externalLoginUser *ExternalLoginUser) error {
has, err := x.Where("external_id=? AND login_source_id=?", externalLoginUser.ExternalID, externalLoginUser.LoginSourceID).
NoAutoCondition().
Exist(externalLoginUser)
if err != nil {
return err
} else if has {
return ErrExternalLoginUserAlreadyExist{gothUser.UserID, user.ID, loginSource.ID}
return ErrExternalLoginUserAlreadyExist{externalLoginUser.ExternalID, user.ID, externalLoginUser.LoginSourceID}
}

_, err = x.Insert(externalLoginUser)
@@ -72,3 +85,97 @@ func removeAllAccountLinks(e Engine, user *User) error {
_, err := e.Delete(&ExternalLoginUser{UserID: user.ID})
return err
}

// GetUserIDByExternalUserID get user id according to provider and userID
func GetUserIDByExternalUserID(provider string, userID string) (int64, error) {
var id int64
_, err := x.Table("external_login_user").
Select("user_id").
Where("provider=?", provider).
And("external_id=?", userID).
Get(&id)
if err != nil {
return 0, err
}
return id, nil
}

// UpdateExternalUser updates external user's information
func UpdateExternalUser(user *User, gothUser goth.User) error {
loginSource, err := GetActiveOAuth2LoginSourceByName(gothUser.Provider)
if err != nil {
return err
}
externalLoginUser := &ExternalLoginUser{
ExternalID: gothUser.UserID,
UserID: user.ID,
LoginSourceID: loginSource.ID,
RawData: gothUser.RawData,
Provider: gothUser.Provider,
Email: gothUser.Email,
Name: gothUser.Name,
FirstName: gothUser.FirstName,
LastName: gothUser.LastName,
NickName: gothUser.NickName,
Description: gothUser.Description,
AvatarURL: gothUser.AvatarURL,
Location: gothUser.Location,
AccessToken: gothUser.AccessToken,
AccessTokenSecret: gothUser.AccessTokenSecret,
RefreshToken: gothUser.RefreshToken,
ExpiresAt: gothUser.ExpiresAt,
}

has, err := x.Where("external_id=? AND login_source_id=?", gothUser.UserID, loginSource.ID).
NoAutoCondition().
Exist(externalLoginUser)
if err != nil {
return err
} else if !has {
return ErrExternalLoginUserNotExist{user.ID, loginSource.ID}
}

_, err = x.Where("external_id=? AND login_source_id=?", gothUser.UserID, loginSource.ID).AllCols().Update(externalLoginUser)
return err
}

// FindExternalUserOptions represents an options to find external users
type FindExternalUserOptions struct {
Provider string
Limit int
Start int
}

func (opts FindExternalUserOptions) toConds() builder.Cond {
var cond = builder.NewCond()
if len(opts.Provider) > 0 {
cond = cond.And(builder.Eq{"provider": opts.Provider})
}
return cond
}

// FindExternalUsersByProvider represents external users via provider
func FindExternalUsersByProvider(opts FindExternalUserOptions) ([]ExternalLoginUser, error) {
var users []ExternalLoginUser
err := x.Where(opts.toConds()).
Limit(opts.Limit, opts.Start).
OrderBy("login_source_id ASC, external_id ASC").
Find(&users)
if err != nil {
return nil, err
}
return users, nil
}

// UpdateMigrationsByType updates all migrated repositories' posterid from gitServiceType to replace originalAuthorID to posterID
func UpdateMigrationsByType(tp structs.GitServiceType, externalUserID string, userID int64) error {
if err := UpdateIssuesMigrationsByType(tp, externalUserID, userID); err != nil {
return err
}

if err := UpdateCommentsMigrationsByType(tp, externalUserID, userID); err != nil {
return err
}

return UpdateReleasesMigrationsByType(tp, externalUserID, userID)
}

+ 1
- 1
models/fixtures/action.yml View File

@@ -5,7 +5,7 @@
act_user_id: 2
repo_id: 2
is_private: true
created_unix: 1540139562
created_unix: 1571686356

-
id: 2

+ 0
- 3
models/fixtures/pull_request.yml View File

@@ -6,7 +6,6 @@
index: 2
head_repo_id: 1
base_repo_id: 1
head_user_name: user1
head_branch: branch1
base_branch: master
merge_base: 1234567890abcdef
@@ -21,7 +20,6 @@
index: 3
head_repo_id: 1
base_repo_id: 1
head_user_name: user1
head_branch: branch2
base_branch: master
merge_base: fedcba9876543210
@@ -35,7 +33,6 @@
index: 1
head_repo_id: 11
base_repo_id: 10
head_user_name: user13
head_branch: branch2
base_branch: master
merge_base: 0abcb056019adb83

+ 42
- 1
models/fixtures/repository.yml View File

@@ -11,6 +11,7 @@
num_milestones: 3
num_closed_milestones: 1
num_watches: 3
status: 0

-
id: 2
@@ -24,6 +25,7 @@
num_closed_pulls: 0
num_stars: 1
close_issues_via_commit_in_any_branch: true
status: 0

-
id: 3
@@ -36,6 +38,7 @@
num_pulls: 0
num_closed_pulls: 0
num_watches: 0
status: 0

-
id: 4
@@ -48,6 +51,7 @@
num_pulls: 0
num_closed_pulls: 0
num_stars: 1
status: 0

-
id: 5
@@ -61,6 +65,7 @@
num_closed_pulls: 0
num_watches: 0
is_mirror: true
status: 0

-
id: 6
@@ -73,6 +78,7 @@
num_pulls: 0
num_closed_pulls: 0
is_mirror: false
status: 0

-
id: 7
@@ -85,6 +91,7 @@
num_pulls: 0
num_closed_pulls: 0
is_mirror: false
status: 0

-
id: 8
@@ -97,6 +104,7 @@
num_pulls: 0
num_closed_pulls: 0
is_mirror: false
status: 0

-
id: 9
@@ -109,6 +117,7 @@
num_pulls: 0
num_closed_pulls: 0
is_mirror: false
status: 0

-
id: 10
@@ -122,6 +131,7 @@
num_closed_pulls: 0
is_mirror: false
num_forks: 1
status: 0

-
id: 11
@@ -135,6 +145,7 @@
num_pulls: 0
num_closed_pulls: 0
is_mirror: false
status: 0

-
id: 12
@@ -147,6 +158,7 @@
num_pulls: 0
num_closed_pulls: 0
is_mirror: false
status: 0

-
id: 13
@@ -159,6 +171,7 @@
num_pulls: 0
num_closed_pulls: 0
is_mirror: false
status: 0

-
id: 14
@@ -172,6 +185,7 @@
num_pulls: 0
num_closed_pulls: 0
is_mirror: false
status: 0

-
id: 15
@@ -179,6 +193,7 @@
lower_name: repo15
name: repo15
is_empty: true
status: 0

-
id: 16
@@ -191,6 +206,7 @@
num_pulls: 0
num_closed_pulls: 0
num_watches: 0
status: 0

-
id: 17
@@ -205,6 +221,7 @@
num_watches: 0
is_mirror: false
is_fork: false
status: 0

-
id: 18
@@ -218,6 +235,7 @@
num_closed_pulls: 0
is_mirror: false
is_fork: false
status: 0

-
id: 19
@@ -231,6 +249,7 @@
num_closed_pulls: 0
is_mirror: false
is_fork: false
status: 0

-
id: 20
@@ -244,6 +263,7 @@
num_closed_pulls: 0
is_mirror: false
is_fork: false
status: 0

-
id: 21
@@ -257,6 +277,7 @@
num_closed_pulls: 0
is_mirror: false
is_fork: false
status: 0

-
id: 22
@@ -270,6 +291,7 @@
num_closed_pulls: 0
is_mirror: false
is_fork: false
status: 0

-
id: 23
@@ -283,6 +305,7 @@
num_closed_pulls: 0
is_mirror: false
is_fork: false
status: 0

-
id: 24
@@ -296,6 +319,7 @@
num_closed_pulls: 0
is_mirror: false
is_fork: false
status: 0

-
id: 25
@@ -310,6 +334,7 @@
num_watches: 0
is_mirror: true
is_fork: false
status: 0

-
id: 26
@@ -324,6 +349,7 @@
num_watches: 0
is_mirror: true
is_fork: false
status: 0

-
id: 27
@@ -339,6 +365,7 @@
is_mirror: true
num_forks: 1
is_fork: false
status: 0

-
id: 28
@@ -354,6 +381,7 @@
is_mirror: true
num_forks: 1
is_fork: false
status: 0

-
id: 29
@@ -368,6 +396,7 @@
num_closed_pulls: 0
is_mirror: false
is_fork: true
status: 0

-
id: 30
@@ -382,6 +411,7 @@
num_closed_pulls: 0
is_mirror: false
is_fork: true
status: 0

-
id: 31
@@ -392,6 +422,7 @@
num_forks: 0
num_issues: 0
is_mirror: false
status: 0

-
id: 32 # org public repo
@@ -403,6 +434,7 @@
num_forks: 0
num_issues: 0
is_mirror: false
status: 0

-
id: 33
@@ -410,6 +442,7 @@
lower_name: utf8
name: utf8
is_private: false
status: 0

-
id: 34
@@ -421,6 +454,7 @@
num_forks: 0
num_issues: 0
is_mirror: false
status: 0

-
id: 35
@@ -432,6 +466,7 @@
num_forks: 0
num_issues: 0
is_mirror: false
status: 0

-
id: 36
@@ -443,6 +478,7 @@
num_forks: 0
num_issues: 0
is_mirror: false
status: 0

-
id: 37
@@ -454,6 +490,7 @@
num_forks: 0
num_issues: 0
is_mirror: false
status: 0

-
id: 38
@@ -465,6 +502,7 @@
num_forks: 0
num_issues: 0
is_mirror: false
status: 0

-
id: 39
@@ -476,6 +514,7 @@
num_forks: 0
num_issues: 0
is_mirror: false
status: 0

-
id: 40
@@ -487,6 +526,7 @@
num_forks: 0
num_issues: 0
is_mirror: false
status: 0

-
id: 41
@@ -519,4 +559,5 @@
num_stars: 0
num_forks: 0
num_issues: 0
is_mirror: false
is_mirror: false
status: 0

+ 295
- 68
models/gpg_key.go View File

@@ -17,12 +17,13 @@ import (

"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"

"github.com/go-xorm/xorm"
"github.com/keybase/go-crypto/openpgp"
"github.com/keybase/go-crypto/openpgp/armor"
"github.com/keybase/go-crypto/openpgp/packet"
"xorm.io/xorm"
)

// GPGKey represents a GPG key.
@@ -80,6 +81,12 @@ func GetGPGKeyByID(keyID int64) (*GPGKey, error) {
return key, nil
}

// GetGPGKeysByKeyID returns public key by given ID.
func GetGPGKeysByKeyID(keyID string) ([]*GPGKey, error) {
keys := make([]*GPGKey, 0, 1)
return keys, x.Where("key_id=?", keyID).Find(&keys)
}

// GetGPGImportByKeyID returns the import public armored key by given KeyID.
func GetGPGImportByKeyID(keyID string) (*GPGKeyImport, error) {
key := new(GPGKeyImport)
@@ -355,10 +362,13 @@ func DeleteGPGKey(doer *User, id int64) (err error) {

// CommitVerification represents a commit validation of signature
type CommitVerification struct {
Verified bool
Reason string
SigningUser *User
SigningKey *GPGKey
Verified bool
Warning bool
Reason string
SigningUser *User
CommittingUser *User
SigningEmail string
SigningKey *GPGKey
}

// SignCommit represents a commit with validation of signature.
@@ -367,6 +377,17 @@ type SignCommit struct {
*UserCommit
}

const (
// BadSignature is used as the reason when the signature has a KeyID that is in the db
// but no key that has that ID verifies the signature. This is a suspicious failure.
BadSignature = "gpg.error.probable_bad_signature"
// BadDefaultSignature is used as the reason when the signature has a KeyID that matches the
// default Key but is not verified by the default key. This is a suspicious failure.
BadDefaultSignature = "gpg.error.probable_bad_default_signature"
// NoKeyFound is used as the reason when no key can be found to verify the signature.
NoKeyFound = "gpg.error.no_gpg_keys_found"
)

func readerFromBase64(s string) (io.Reader, error) {
bs, err := base64.StdEncoding.DecodeString(s)
if err != nil {
@@ -424,49 +445,207 @@ func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error {
return pkey.VerifySignature(h, s)
}

// ParseCommitWithSignature check if signature is good against keystore.
func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
if c.Signature != nil && c.Committer != nil {
//Parsing signature
sig, err := extractSignature(c.Signature.Signature)
if err != nil { //Skipping failed to extract sign
log.Error("SignatureRead err: %v", err)
return &CommitVerification{
Verified: false,
Reason: "gpg.error.extract_sign",
func hashAndVerify(sig *packet.Signature, payload string, k *GPGKey, committer, signer *User, email string) *CommitVerification {
//Generating hash of commit
hash, err := populateHash(sig.Hash, []byte(payload))
if err != nil { //Skipping failed to generate hash
log.Error("PopulateHash: %v", err)
return &CommitVerification{
CommittingUser: committer,
Verified: false,
Reason: "gpg.error.generate_hash",
}
}

if err := verifySign(sig, hash, k); err == nil {
return &CommitVerification{ //Everything is ok
CommittingUser: committer,
Verified: true,
Reason: fmt.Sprintf("%s <%s> / %s", signer.Name, signer.Email, k.KeyID),
SigningUser: signer,
SigningKey: k,
SigningEmail: email,
}
}
return nil
}

func hashAndVerifyWithSubKeys(sig *packet.Signature, payload string, k *GPGKey, committer, signer *User, email string) *CommitVerification {
commitVerification := hashAndVerify(sig, payload, k, committer, signer, email)
if commitVerification != nil {
return commitVerification
}

//And test also SubsKey
for _, sk := range k.SubsKey {
commitVerification := hashAndVerify(sig, payload, sk, committer, signer, email)
if commitVerification != nil {
return commitVerification
}
}
return nil
}

func hashAndVerifyForKeyID(sig *packet.Signature, payload string, committer *User, keyID, name, email string) *CommitVerification {
if keyID == "" {
return nil
}
keys, err := GetGPGKeysByKeyID(keyID)
if err != nil {
log.Error("GetGPGKeysByKeyID: %v", err)
return &CommitVerification{
CommittingUser: committer,
Verified: false,
Reason: "gpg.error.failed_retrieval_gpg_keys",
}
}
if len(keys) == 0 {
return nil
}
for _, key := range keys {
activated := false
if len(email) != 0 {
for _, e := range key.Emails {
if e.IsActivated && strings.EqualFold(e.Email, email) {
activated = true
email = e.Email
break
}
}
} else {
for _, e := range key.Emails {
if e.IsActivated {
activated = true
email = e.Email
break
}
}
}
if !activated {
continue
}
signer := &User{
Name: name,
Email: email,
}
if key.OwnerID != 0 {
owner, err := GetUserByID(key.OwnerID)
if err == nil {
signer = owner
} else if !IsErrUserNotExist(err) {
log.Error("Failed to GetUserByID: %d for key ID: %d (%s) %v", key.OwnerID, key.ID, key.KeyID, err)
return &CommitVerification{
CommittingUser: committer,
Verified: false,
Reason: "gpg.error.no_committer_account",
}
}
}
commitVerification := hashAndVerifyWithSubKeys(sig, payload, key, committer, signer, email)
if commitVerification != nil {
return commitVerification
}
}
// This is a bad situation ... We have a key id that is in our database but the signature doesn't match.
return &CommitVerification{
CommittingUser: committer,
Verified: false,
Warning: true,
Reason: BadSignature,
}
}

// ParseCommitWithSignature check if signature is good against keystore.
func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
var committer *User
if c.Committer != nil {
var err error
//Find Committer account
committer, err := GetUserByEmail(c.Committer.Email) //This find the user by primary email or activated email so commit will not be valid if email is not
if err != nil { //Skipping not user for commiter
committer, err = GetUserByEmail(c.Committer.Email) //This finds the user by primary email or activated email so commit will not be valid if email is not
if err != nil { //Skipping not user for commiter
committer = &User{
Name: c.Committer.Name,
Email: c.Committer.Email,
}
// We can expect this to often be an ErrUserNotExist. in the case
// it is not, however, it is important to log it.
if !IsErrUserNotExist(err) {
log.Error("GetUserByEmail: %v", err)
return &CommitVerification{
CommittingUser: committer,
Verified: false,
Reason: "gpg.error.no_committer_account",
}
}
return &CommitVerification{
Verified: false,
Reason: "gpg.error.no_committer_account",
}

}
}

// If no signature just report the committer
if c.Signature == nil {
return &CommitVerification{
CommittingUser: committer,
Verified: false, //Default value
Reason: "gpg.error.not_signed_commit", //Default value
}
}

//Parsing signature
sig, err := extractSignature(c.Signature.Signature)
if err != nil { //Skipping failed to extract sign
log.Error("SignatureRead err: %v", err)
return &CommitVerification{
CommittingUser: committer,
Verified: false,
Reason: "gpg.error.extract_sign",
}
}

keyID := ""
if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 {
keyID = fmt.Sprintf("%X", *sig.IssuerKeyId)
}
if keyID == "" && sig.IssuerFingerprint != nil && len(sig.IssuerFingerprint) > 0 {
keyID = fmt.Sprintf("%X", sig.IssuerFingerprint[12:20])
}

defaultReason := NoKeyFound

// First check if the sig has a keyID and if so just look at that
if commitVerification := hashAndVerifyForKeyID(
sig,
c.Signature.Payload,
committer,
keyID,
setting.AppName,
""); commitVerification != nil {
if commitVerification.Reason == BadSignature {
defaultReason = BadSignature
} else {
return commitVerification
}
}

// Now try to associate the signature with the committer, if present
if committer.ID != 0 {
keys, err := ListGPGKeys(committer.ID)
if err != nil { //Skipping failed to get gpg keys of user
log.Error("ListGPGKeys: %v", err)
return &CommitVerification{
Verified: false,
Reason: "gpg.error.failed_retrieval_gpg_keys",
CommittingUser: committer,
Verified: false,
Reason: "gpg.error.failed_retrieval_gpg_keys",
}
}

for _, k := range keys {
//Pre-check (& optimization) that emails attached to key can be attached to the commiter email and can validate
canValidate := false
lowerCommiterEmail := strings.ToLower(c.Committer.Email)
email := ""
for _, e := range k.Emails {
if e.IsActivated && strings.ToLower(e.Email) == lowerCommiterEmail {
if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) {
canValidate = true
email = e.Email
break
}
}
@@ -474,56 +653,104 @@ func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
continue //Skip this key
}

//Generating hash of commit
hash, err := populateHash(sig.Hash, []byte(c.Signature.Payload))
if err != nil { //Skipping ailed to generate hash
log.Error("PopulateHash: %v", err)
return &CommitVerification{
Verified: false,
Reason: "gpg.error.generate_hash",
}
}
//We get PK
if err := verifySign(sig, hash, k); err == nil {
return &CommitVerification{ //Everything is ok
Verified: true,
Reason: fmt.Sprintf("%s <%s> / %s", c.Committer.Name, c.Committer.Email, k.KeyID),
SigningUser: committer,
SigningKey: k,
}
commitVerification := hashAndVerifyWithSubKeys(sig, c.Signature.Payload, k, committer, committer, email)
if commitVerification != nil {
return commitVerification
}
//And test also SubsKey
for _, sk := range k.SubsKey {

//Generating hash of commit
hash, err := populateHash(sig.Hash, []byte(c.Signature.Payload))
if err != nil { //Skipping ailed to generate hash
log.Error("PopulateHash: %v", err)
return &CommitVerification{
Verified: false,
Reason: "gpg.error.generate_hash",
}
}
if err := verifySign(sig, hash, sk); err == nil {
return &CommitVerification{ //Everything is ok
Verified: true,
Reason: fmt.Sprintf("%s <%s> / %s", c.Committer.Name, c.Committer.Email, sk.KeyID),
SigningUser: committer,
SigningKey: sk,
}
}
}
}

if setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" {
// OK we should try the default key
gpgSettings := git.GPGSettings{
Sign: true,
KeyID: setting.Repository.Signing.SigningKey,
Name: setting.Repository.Signing.SigningName,
Email: setting.Repository.Signing.SigningEmail,
}
if err := gpgSettings.LoadPublicKeyContent(); err != nil {
log.Error("Error getting default signing key: %s %v", gpgSettings.KeyID, err)
} else if commitVerification := verifyWithGPGSettings(&gpgSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil {
if commitVerification.Reason == BadSignature {
defaultReason = BadSignature
} else {
return commitVerification
}
}
return &CommitVerification{ //Default at this stage
Verified: false,
Reason: "gpg.error.no_gpg_keys_found",
}

defaultGPGSettings, err := c.GetRepositoryDefaultPublicGPGKey(false)
if err != nil {
log.Error("Error getting default public gpg key: %v", err)
} else if defaultGPGSettings == nil {
log.Warn("Unable to get defaultGPGSettings for unattached commit: %s", c.ID.String())
} else if defaultGPGSettings.Sign {
if commitVerification := verifyWithGPGSettings(defaultGPGSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil {
if commitVerification.Reason == BadSignature {
defaultReason = BadSignature
} else {
return commitVerification
}
}
}

return &CommitVerification{
Verified: false, //Default value
Reason: "gpg.error.not_signed_commit", //Default value
return &CommitVerification{ //Default at this stage
CommittingUser: committer,
Verified: false,
Warning: defaultReason != NoKeyFound,
Reason: defaultReason,
SigningKey: &GPGKey{
KeyID: keyID,
},
}
}

func verifyWithGPGSettings(gpgSettings *git.GPGSettings, sig *packet.Signature, payload string, committer *User, keyID string) *CommitVerification {
// First try to find the key in the db
if commitVerification := hashAndVerifyForKeyID(sig, payload, committer, gpgSettings.KeyID, gpgSettings.Name, gpgSettings.Email); commitVerification != nil {
return commitVerification
}

// Otherwise we have to parse the key
ekey, err := checkArmoredGPGKeyString(gpgSettings.PublicKeyContent)
if err != nil {
log.Error("Unable to get default signing key: %v", err)
return &CommitVerification{
CommittingUser: committer,
Verified: false,
Reason: "gpg.error.generate_hash",
}
}
pubkey := ekey.PrimaryKey
content, err := base64EncPubKey(pubkey)
if err != nil {
return &CommitVerification{
CommittingUser: committer,
Verified: false,
Reason: "gpg.error.generate_hash",
}
}
k := &GPGKey{
Content: content,
CanSign: pubkey.CanSign(),
KeyID: pubkey.KeyIdString(),
}
if commitVerification := hashAndVerifyWithSubKeys(sig, payload, k, committer, &User{
Name: gpgSettings.Name,
Email: gpgSettings.Email,
}, gpgSettings.Email); commitVerification != nil {
return commitVerification
}
if keyID == k.KeyID {
// This is a bad situation ... We have a key id that matches our default key but the signature doesn't match.
return &CommitVerification{
CommittingUser: committer,
Verified: false,
Warning: true,
Reason: BadSignature,
}
}
return nil
}

// ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys.

+ 2
- 1
models/graph.go View File

@@ -30,7 +30,7 @@ type GraphItem struct {
type GraphItems []GraphItem

// GetCommitGraph return a list of commit (GraphItems) from all branches
func GetCommitGraph(r *git.Repository) (GraphItems, error) {
func GetCommitGraph(r *git.Repository, page int) (GraphItems, error) {

var CommitGraph []GraphItem

@@ -43,6 +43,7 @@ func GetCommitGraph(r *git.Repository) (GraphItems, error) {
"-C",
"-M",
fmt.Sprintf("-n %d", setting.UI.GraphMaxCommitNum),
fmt.Sprintf("--skip=%d", setting.UI.GraphMaxCommitNum*(page-1)),
"--date=iso",
fmt.Sprintf("--pretty=format:%s", format),
)

+ 1
- 1
models/graph_test.go View File

@@ -19,7 +19,7 @@ func BenchmarkGetCommitGraph(b *testing.B) {
}

for i := 0; i < b.N; i++ {
graph, err := GetCommitGraph(currentRepo)
graph, err := GetCommitGraph(currentRepo, 1)
if err != nil {
b.Error("Could get commit graph")
}

+ 174
- 259
models/issue.go View File

@@ -14,13 +14,14 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"

"github.com/go-xorm/xorm"
"github.com/unknwon/com"
"xorm.io/builder"
"xorm.io/xorm"
)

// Issue represents an issue or pull request of repository.
@@ -32,7 +33,7 @@ type Issue struct {
PosterID int64 `xorm:"INDEX"`
Poster *User `xorm:"-"`
OriginalAuthor string
OriginalAuthorID int64
OriginalAuthorID int64 `xorm:"index"`
Title string `xorm:"name"`
Content string `xorm:"TEXT"`
RenderedContent string `xorm:"-"`
@@ -427,52 +428,6 @@ func (issue *Issue) HasLabel(labelID int64) bool {
return issue.hasLabel(x, labelID)
}

func (issue *Issue) sendLabelUpdatedWebhook(doer *User) {
var err error

if err = issue.loadRepo(x); err != nil {
log.Error("loadRepo: %v", err)
return
}

if err = issue.loadPoster(x); err != nil {
log.Error("loadPoster: %v", err)
return
}

mode, _ := AccessLevel(issue.Poster, issue.Repo)
if issue.IsPull {
if err = issue.loadPullRequest(x); err != nil {
log.Error("loadPullRequest: %v", err)
return
}
if err = issue.PullRequest.LoadIssue(); err != nil {
log.Error("LoadIssue: %v", err)
return
}
err = PrepareWebhooks(issue.Repo, HookEventPullRequest, &api.PullRequestPayload{
Action: api.HookIssueLabelUpdated,
Index: issue.Index,
PullRequest: issue.PullRequest.APIFormat(),
Repository: issue.Repo.APIFormat(AccessModeNone),
Sender: doer.APIFormat(),
})
} else {
err = PrepareWebhooks(issue.Repo, HookEventIssues, &api.IssuePayload{
Action: api.HookIssueLabelUpdated,
Index: issue.Index,
Issue: issue.APIFormat(),
Repository: issue.Repo.APIFormat(mode),
Sender: doer.APIFormat(),
})
}
if err != nil {
log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
} else {
go HookQueue.Add(issue.RepoID)
}
}

// ReplyReference returns tokenized address to use for email reply headers
func (issue *Issue) ReplyReference() string {
var path string
@@ -489,30 +444,10 @@ func (issue *Issue) addLabel(e *xorm.Session, label *Label, doer *User) error {
return newIssueLabel(e, issue, label, doer)
}

// AddLabel adds a new label to the issue.
func (issue *Issue) AddLabel(doer *User, label *Label) error {
if err := NewIssueLabel(issue, label, doer); err != nil {
return err
}

issue.sendLabelUpdatedWebhook(doer)
return nil
}

func (issue *Issue) addLabels(e *xorm.Session, labels []*Label, doer *User) error {
return newIssueLabels(e, issue, labels, doer)
}

// AddLabels adds a list of new labels to the issue.
func (issue *Issue) AddLabels(doer *User, labels []*Label) error {
if err := NewIssueLabels(issue, labels, doer); err != nil {
return err
}

issue.sendLabelUpdatedWebhook(doer)
return nil
}

func (issue *Issue) getLabels(e Engine) (err error) {
if len(issue.Labels) > 0 {
return nil
@@ -529,28 +464,6 @@ func (issue *Issue) removeLabel(e *xorm.Session, doer *User, label *Label) error
return deleteIssueLabel(e, issue, label, doer)
}

// RemoveLabel removes a label from issue by given ID.
func (issue *Issue) RemoveLabel(doer *User, label *Label) error {
if err := issue.loadRepo(x); err != nil {
return err
}

perm, err := GetUserRepoPermission(issue.Repo, doer)
if err != nil {
return err
}
if !perm.CanWriteIssuesOrPulls(issue.IsPull) {
return ErrLabelNotExist{}
}

if err := DeleteIssueLabel(issue, label, doer); err != nil {
return err
}

issue.sendLabelUpdatedWebhook(doer)
return nil
}

func (issue *Issue) clearLabels(e *xorm.Session, doer *User) (err error) {
if err = issue.getLabels(e); err != nil {
return fmt.Errorf("getLabels: %v", err)
@@ -595,40 +508,6 @@ func (issue *Issue) ClearLabels(doer *User) (err error) {
if err = sess.Commit(); err != nil {
return fmt.Errorf("Commit: %v", err)
}
sess.Close()

if err = issue.LoadPoster(); err != nil {
return fmt.Errorf("loadPoster: %v", err)
}

mode, _ := AccessLevel(issue.Poster, issue.Repo)
if issue.IsPull {
err = issue.PullRequest.LoadIssue()
if err != nil {
log.Error("LoadIssue: %v", err)
return
}
err = PrepareWebhooks(issue.Repo, HookEventPullRequest, &api.PullRequestPayload{
Action: api.HookIssueLabelCleared,
Index: issue.Index,
PullRequest: issue.PullRequest.APIFormat(),
Repository: issue.Repo.APIFormat(mode),
Sender: doer.APIFormat(),
})
} else {
err = PrepareWebhooks(issue.Repo, HookEventIssues, &api.IssuePayload{
Action: api.HookIssueLabelCleared,
Index: issue.Index,
Issue: issue.APIFormat(),
Repository: issue.Repo.APIFormat(mode),
Sender: doer.APIFormat(),
})
}
if err != nil {
log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
} else {
go HookQueue.Add(issue.RepoID)
}

return nil
}
@@ -714,11 +593,6 @@ func updateIssueCols(e Engine, issue *Issue, cols ...string) error {
return nil
}

// UpdateIssueCols only updates values of specific columns for given issue.
func UpdateIssueCols(issue *Issue, cols ...string) error {
return updateIssueCols(x, issue, cols...)
}

func (issue *Issue) changeStatus(e *xorm.Session, doer *User, isClosed bool) (err error) {
// Reload the issue
currentIssue, err := getIssueByID(e, issue.ID)
@@ -766,7 +640,7 @@ func (issue *Issue) changeStatus(e *xorm.Session, doer *User, isClosed bool) (er
}

// Update issue count of milestone
if err = changeMilestoneIssueStats(e, issue); err != nil {
if err := updateMilestoneClosedNum(e, issue.MilestoneID); err != nil {
return err
}

@@ -844,9 +718,7 @@ func (issue *Issue) ChangeStatus(doer *User, isClosed bool) (err error) {
}

// ChangeTitle changes the title of this issue, as the given user.
func (issue *Issue) ChangeTitle(doer *User, title string) (err error) {
oldTitle := issue.Title
issue.Title = title
func (issue *Issue) ChangeTitle(doer *User, oldTitle string) (err error) {
sess := x.NewSession()
defer sess.Close()

@@ -862,7 +734,7 @@ func (issue *Issue) ChangeTitle(doer *User, title string) (err error) {
return fmt.Errorf("loadRepo: %v", err)
}

if _, err = createChangeTitleComment(sess, doer, issue.Repo, issue, oldTitle, title); err != nil {
if _, err = createChangeTitleComment(sess, doer, issue.Repo, issue, oldTitle, issue.Title); err != nil {
return fmt.Errorf("createChangeTitleComment: %v", err)
}

@@ -874,51 +746,7 @@ func (issue *Issue) ChangeTitle(doer *User, title string) (err error) {
return err
}

if err = sess.Commit(); err != nil {
return err
}
sess.Close()

mode, _ := AccessLevel(issue.Poster, issue.Repo)
if issue.IsPull {
if err = issue.loadPullRequest(sess); err != nil {
return fmt.Errorf("loadPullRequest: %v", err)
}
issue.PullRequest.Issue = issue
err = PrepareWebhooks(issue.Repo, HookEventPullRequest, &api.PullRequestPayload{
Action: api.HookIssueEdited,
Index: issue.Index,
Changes: &api.ChangesPayload{
Title: &api.ChangesFromPayload{
From: oldTitle,
},
},
PullRequest: issue.PullRequest.APIFormat(),
Repository: issue.Repo.APIFormat(mode),
Sender: doer.APIFormat(),
})
} else {
err = PrepareWebhooks(issue.Repo, HookEventIssues, &api.IssuePayload{
Action: api.HookIssueEdited,
Index: issue.Index,
Changes: &api.ChangesPayload{
Title: &api.ChangesFromPayload{
From: oldTitle,
},
},
Issue: issue.APIFormat(),
Repository: issue.Repo.APIFormat(mode),
Sender: issue.Poster.APIFormat(),
})
}

if err != nil {
log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
} else {
go HookQueue.Add(issue.RepoID)
}

return nil
return sess.Commit()
}

// AddDeletePRBranchComment adds delete branch comment for pull request issue
@@ -939,6 +767,26 @@ func AddDeletePRBranchComment(doer *User, repo *Repository, issueID int64, branc
return sess.Commit()
}

// UpdateAttachments update attachments by UUIDs for the issue
func (issue *Issue) UpdateAttachments(uuids []string) (err error) {
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
attachments, err := getAttachmentsByUUIDs(sess, uuids)
if err != nil {
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", uuids, err)
}
for i := 0; i < len(attachments); i++ {
attachments[i].IssueID = issue.ID
if err := updateAttachment(sess, attachments[i]); err != nil {
return fmt.Errorf("update attachment [id: %d]: %v", attachments[i].ID, err)
}
}
return sess.Commit()
}

// ChangeContent changes issue content, as the given user.
func (issue *Issue) ChangeContent(doer *User, content string) (err error) {
oldContent := issue.Content
@@ -1048,7 +896,6 @@ type NewIssueOptions struct {
Repo *Repository
Issue *Issue
LabelIDs []int64
AssigneeIDs []int64
Attachments []string // In UUID format.
IsPull bool
}
@@ -1070,40 +917,7 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
}
}

// Keep the old assignee id thingy for compatibility reasons
if opts.Issue.AssigneeID > 0 {
isAdded := false
// Check if the user has already been passed to issue.AssigneeIDs, if not, add it
for _, aID := range opts.AssigneeIDs {
if aID == opts.Issue.AssigneeID {
isAdded = true
break
}
}

if !isAdded {
opts.AssigneeIDs = append(opts.AssigneeIDs, opts.Issue.AssigneeID)
}
}

// Check for and validate assignees
if len(opts.AssigneeIDs) > 0 {
for _, assigneeID := range opts.AssigneeIDs {
user, err := getUserByID(e, assigneeID)
if err != nil {
return fmt.Errorf("getUserByID [user_id: %d, repo_id: %d]: %v", assigneeID, opts.Repo.ID, err)
}
valid, err := canBeAssigned(e, user, opts.Repo)
if err != nil {
return fmt.Errorf("canBeAssigned [user_id: %d, repo_id: %d]: %v", assigneeID, opts.Repo.ID, err)
}
if !valid {
return ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: opts.Repo.Name}
}
}
}

// Milestone and assignee validation should happen before insert actual object.
// Milestone validation should happen before insert actual object.
if _, err := e.SetExpr("`index`", "coalesce(MAX(`index`),0)+1").
Where("repo_id=?", opts.Issue.RepoID).
Insert(opts.Issue); err != nil {
@@ -1119,15 +933,11 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
opts.Issue.Index = inserted.Index

if opts.Issue.MilestoneID > 0 {
if err = changeMilestoneAssign(e, doer, opts.Issue, -1); err != nil {
if _, err = e.Exec("UPDATE `milestone` SET num_issues=num_issues+1 WHERE id=?", opts.Issue.MilestoneID); err != nil {
return err
}
}

// Insert the assignees
for _, assigneeID := range opts.AssigneeIDs {
err = opts.Issue.changeAssignee(e, doer, assigneeID, true)
if err != nil {
if _, err = createMilestoneComment(e, doer, opts.Repo, opts.Issue, 0, opts.Issue.MilestoneID); err != nil {
return err
}
}
@@ -1189,11 +999,11 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
}

// NewIssue creates new issue with labels for repository.
func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, assigneeIDs []int64, uuids []string) (err error) {
func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
// Retry several times in case INSERT fails due to duplicate key for (repo_id, index); see #7887
i := 0
for {
if err = newIssueAttempt(repo, issue, labelIDs, assigneeIDs, uuids); err == nil {
if err = newIssueAttempt(repo, issue, labelIDs, uuids); err == nil {
return nil
}
if !IsErrNewIssueInsert(err) {
@@ -1207,7 +1017,7 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, assigneeIDs []in
return fmt.Errorf("NewIssue: too many errors attempting to insert the new issue. Last error was: %v", err)
}

func newIssueAttempt(repo *Repository, issue *Issue, labelIDs []int64, assigneeIDs []int64, uuids []string) (err error) {
func newIssueAttempt(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
@@ -1219,7 +1029,6 @@ func newIssueAttempt(repo *Repository, issue *Issue, labelIDs []int64, assigneeI
Issue: issue,
LabelIDs: labelIDs,
Attachments: uuids,
AssigneeIDs: assigneeIDs,
}); err != nil {
if IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) {
return err
@@ -1400,8 +1209,12 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) {

if opts.LabelIDs != nil {
for i, labelID := range opts.LabelIDs {
sess.Join("INNER", fmt.Sprintf("issue_label il%d", i),
fmt.Sprintf("issue.id = il%[1]d.issue_id AND il%[1]d.label_id = %[2]d", i, labelID))
if labelID > 0 {
sess.Join("INNER", fmt.Sprintf("issue_label il%d", i),
fmt.Sprintf("issue.id = il%[1]d.issue_id AND il%[1]d.label_id = %[2]d", i, labelID))
} else {
sess.Where("issue.id not in (select issue_id from issue_label where label_id = ?)", -labelID)
}
}
}
}
@@ -1477,46 +1290,18 @@ func getParticipantsByIssueID(e Engine, issueID int64) ([]*User, error) {
return users, e.In("id", userIDs).Find(&users)
}

// UpdateIssueMentions extracts mentioned people from content and
// updates issue-user relations for them.
func UpdateIssueMentions(ctx DBContext, issueID int64, mentions []string) error {
// UpdateIssueMentions updates issue-user relations for mentioned users.
func UpdateIssueMentions(ctx DBContext, issueID int64, mentions []*User) error {
if len(mentions) == 0 {
return nil
}
for i := range mentions {
mentions[i] = strings.ToLower(mentions[i])
ids := make([]int64, len(mentions))
for i, u := range mentions {
ids[i] = u.ID
}
users := make([]*User, 0, len(mentions))

if err := ctx.e.In("lower_name", mentions).Asc("lower_name").Find(&users); err != nil {
return fmt.Errorf("find mentioned users: %v", err)
}

ids := make([]int64, 0, len(mentions))
for _, user := range users {
ids = append(ids, user.ID)
if !user.IsOrganization() || user.NumMembers == 0 {
continue
}

memberIDs := make([]int64, 0, user.NumMembers)
orgUsers, err := getOrgUsersByOrgID(ctx.e, user.ID)
if err != nil {
return fmt.Errorf("GetOrgUsersByOrgID [%d]: %v", user.ID, err)
}

for _, orgUser := range orgUsers {
memberIDs = append(memberIDs, orgUser.ID)
}

ids = append(ids, memberIDs...)
}

if err := UpdateIssueUsersByMentions(ctx, issueID, ids); err != nil {
return fmt.Errorf("UpdateIssueUsersByMentions: %v", err)
}

return nil
}

@@ -1909,3 +1694,133 @@ func (issue *Issue) updateClosedNum(e Engine) (err error) {
}
return
}

// ResolveMentionsByVisibility returns the users mentioned in an issue, removing those that
// don't have access to reading it. Teams are expanded into their users, but organizations are ignored.
func (issue *Issue) ResolveMentionsByVisibility(ctx DBContext, doer *User, mentions []string) (users []*User, err error) {
if len(mentions) == 0 {
return
}
if err = issue.loadRepo(ctx.e); err != nil {
return
}
resolved := make(map[string]bool, 20)
names := make([]string, 0, 20)
resolved[doer.LowerName] = true
for _, name := range mentions {
name := strings.ToLower(name)
if _, ok := resolved[name]; ok {
continue
}
resolved[name] = false
names = append(names, name)
}

if err := issue.Repo.getOwner(ctx.e); err != nil {
return nil, err
}

if issue.Repo.Owner.IsOrganization() {
// Since there can be users with names that match the name of a team,
// if the team exists and can read the issue, the team takes precedence.
teams := make([]*Team, 0, len(names))
if err := ctx.e.
Join("INNER", "team_repo", "team_repo.team_id = team.id").
Where("team_repo.repo_id=?", issue.Repo.ID).
In("team.lower_name", names).
Find(&teams); err != nil {
return nil, fmt.Errorf("find mentioned teams: %v", err)
}
if len(teams) != 0 {
checked := make([]int64, 0, len(teams))
unittype := UnitTypeIssues
if issue.IsPull {
unittype = UnitTypePullRequests
}
for _, team := range teams {
if team.Authorize >= AccessModeOwner {
checked = append(checked, team.ID)
resolved[team.LowerName] = true
continue
}
has, err := ctx.e.Get(&TeamUnit{OrgID: issue.Repo.Owner.ID, TeamID: team.ID, Type: unittype})
if err != nil {
return nil, fmt.Errorf("get team units (%d): %v", team.ID, err)
}
if has {
checked = append(checked, team.ID)
resolved[team.LowerName] = true
}
}
if len(checked) != 0 {
teamusers := make([]*User, 0, 20)
if err := ctx.e.
Join("INNER", "team_user", "team_user.uid = `user`.id").
In("`team_user`.team_id", checked).
And("`user`.is_active = ?", true).
And("`user`.prohibit_login = ?", false).
Find(&teamusers); err != nil {
return nil, fmt.Errorf("get teams users: %v", err)
}
if len(teamusers) > 0 {
users = make([]*User, 0, len(teamusers))
for _, user := range teamusers {
if already, ok := resolved[user.LowerName]; !ok || !already {
users = append(users, user)
resolved[user.LowerName] = true
}
}
}
}
}

// Remove names already in the list to avoid querying the database if pending names remain
names = make([]string, 0, len(resolved))
for name, already := range resolved {
if !already {
names = append(names, name)
}
}
if len(names) == 0 {
return
}
}

unchecked := make([]*User, 0, len(names))
if err := ctx.e.
Where("`user`.is_active = ?", true).
And("`user`.prohibit_login = ?", false).
In("`user`.lower_name", names).
Find(&unchecked); err != nil {
return nil, fmt.Errorf("find mentioned users: %v", err)
}
for _, user := range unchecked {
if already := resolved[user.LowerName]; already || user.IsOrganization() {
continue
}
// Normal users must have read access to the referencing issue
perm, err := getUserRepoPermission(ctx.e, issue.Repo, user)
if err != nil {
return nil, fmt.Errorf("getUserRepoPermission [%d]: %v", user.ID, err)
}
if !perm.CanReadIssuesOrPulls(issue.IsPull) {
continue
}
users = append(users, user)
}

return
}

// UpdateIssuesMigrationsByType updates all migrated repositories' issues from gitServiceType to replace originalAuthorID to posterID
func UpdateIssuesMigrationsByType(gitServiceType structs.GitServiceType, originalAuthorID string, posterID int64) error {
_, err := x.Table("issue").
Where("repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)", gitServiceType).
And("original_author_id = ?", originalAuthorID).
Update(map[string]interface{}{
"poster_id": posterID,
"original_author": "",
"original_author_id": 0,
})
return err
}

+ 55
- 85
models/issue_assignees.go View File

@@ -10,7 +10,7 @@ import (
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"

"github.com/go-xorm/xorm"
"xorm.io/xorm"
)

// IssueAssignees saves all issue assignees
@@ -58,8 +58,11 @@ func getAssigneesByIssue(e Engine, issue *Issue) (assignees []*User, err error)

// IsUserAssignedToIssue returns true when the user is assigned to the issue
func IsUserAssignedToIssue(issue *Issue, user *User) (isAssigned bool, err error) {
isAssigned, err = x.Exist(&IssueAssignees{IssueID: issue.ID, AssigneeID: user.ID})
return
return isUserAssignedToIssue(x, issue, user)
}

func isUserAssignedToIssue(e Engine, issue *Issue, user *User) (isAssigned bool, err error) {
return e.Get(&IssueAssignees{IssueID: issue.ID, AssigneeID: user.ID})
}

// DeleteNotPassedAssignee deletes all assignees who aren't passed via the "assignees" array
@@ -78,7 +81,7 @@ func DeleteNotPassedAssignee(issue *Issue, doer *User, assignees []*User) (err e

if !found {
// This function also does comments and hooks, which is why we call it seperatly instead of directly removing the assignees here
if err := UpdateAssignee(issue, doer, assignee.ID); err != nil {
if _, _, err := issue.ToggleAssignee(doer, assignee.ID); err != nil {
return err
}
}
@@ -110,73 +113,56 @@ func clearAssigneeByUserID(sess *xorm.Session, userID int64) (err error) {
return
}

// AddAssigneeIfNotAssigned adds an assignee only if he isn't aleady assigned to the issue
func AddAssigneeIfNotAssigned(issue *Issue, doer *User, assigneeID int64) (err error) {
// Check if the user is already assigned
isAssigned, err := IsUserAssignedToIssue(issue, &User{ID: assigneeID})
if err != nil {
return err
}

if !isAssigned {
return issue.ChangeAssignee(doer, assigneeID)
}
return nil
}

// UpdateAssignee deletes or adds an assignee to an issue
func UpdateAssignee(issue *Issue, doer *User, assigneeID int64) (err error) {
return issue.ChangeAssignee(doer, assigneeID)
}

// ChangeAssignee changes the Assignee of this issue.
func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) {
// ToggleAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it.
func (issue *Issue) ToggleAssignee(doer *User, assigneeID int64) (removed bool, comment *Comment, err error) {
sess := x.NewSession()
defer sess.Close()

if err := sess.Begin(); err != nil {
return err
return false, nil, err
}

if err := issue.changeAssignee(sess, doer, assigneeID, false); err != nil {
return err
removed, comment, err = issue.toggleAssignee(sess, doer, assigneeID, false)
if err != nil {
return false, nil, err
}

if err := sess.Commit(); err != nil {
return err
return false, nil, err
}

go HookQueue.Add(issue.RepoID)
return nil

return removed, comment, nil
}

func (issue *Issue) changeAssignee(sess *xorm.Session, doer *User, assigneeID int64, isCreate bool) (err error) {
// Update the assignee
removed, err := updateIssueAssignee(sess, issue, assigneeID)
func (issue *Issue) toggleAssignee(sess *xorm.Session, doer *User, assigneeID int64, isCreate bool) (removed bool, comment *Comment, err error) {
removed, err = toggleUserAssignee(sess, issue, assigneeID)
if err != nil {
return fmt.Errorf("UpdateIssueUserByAssignee: %v", err)
return false, nil, fmt.Errorf("UpdateIssueUserByAssignee: %v", err)
}

// Repo infos
if err = issue.loadRepo(sess); err != nil {
return fmt.Errorf("loadRepo: %v", err)
return false, nil, fmt.Errorf("loadRepo: %v", err)
}

// Comment
if _, err = createAssigneeComment(sess, doer, issue.Repo, issue, assigneeID, removed); err != nil {
return fmt.Errorf("createAssigneeComment: %v", err)
comment, err = createAssigneeComment(sess, doer, issue.Repo, issue, assigneeID, removed)
if err != nil {
return false, nil, fmt.Errorf("createAssigneeComment: %v", err)
}

// if pull request is in the middle of creation - don't call webhook
if isCreate {
return nil
return removed, comment, err
}

if issue.IsPull {
mode, _ := accessLevelUnit(sess, doer, issue.Repo, UnitTypePullRequests)

if err = issue.loadPullRequest(sess); err != nil {
return fmt.Errorf("loadPullRequest: %v", err)
return false, nil, fmt.Errorf("loadPullRequest: %v", err)
}
issue.PullRequest.Issue = issue
apiPullRequest := &api.PullRequestPayload{
@@ -190,9 +176,10 @@ func (issue *Issue) changeAssignee(sess *xorm.Session, doer *User, assigneeID in
} else {
apiPullRequest.Action = api.HookIssueAssigned
}
// Assignee comment triggers a webhook
if err := prepareWebhooks(sess, issue.Repo, HookEventPullRequest, apiPullRequest); err != nil {
log.Error("PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, removed, err)
return nil
return false, nil, err
}
} else {
mode, _ := accessLevelUnit(sess, doer, issue.Repo, UnitTypeIssues)
@@ -208,67 +195,50 @@ func (issue *Issue) changeAssignee(sess *xorm.Session, doer *User, assigneeID in
} else {
apiIssue.Action = api.HookIssueAssigned
}
// Assignee comment triggers a webhook
if err := prepareWebhooks(sess, issue.Repo, HookEventIssues, apiIssue); err != nil {
log.Error("PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, removed, err)
return nil
return false, nil, err
}
}
return nil
return removed, comment, nil
}

// UpdateAPIAssignee is a helper function to add or delete one or multiple issue assignee(s)
// Deleting is done the GitHub way (quote from their api documentation):
// https://developer.github.com/v3/issues/#edit-an-issue
// "assignees" (array): Logins for Users to assign to this issue.
// Pass one or more user logins to replace the set of assignees on this Issue.
// Send an empty array ([]) to clear all assignees from the Issue.
func UpdateAPIAssignee(issue *Issue, oneAssignee string, multipleAssignees []string, doer *User) (err error) {
var allNewAssignees []*User
// toggles user assignee state in database
func toggleUserAssignee(e *xorm.Session, issue *Issue, assigneeID int64) (removed bool, err error) {

// Keep the old assignee thingy for compatibility reasons
if oneAssignee != "" {
// Prevent double adding assignees
var isDouble bool
for _, assignee := range multipleAssignees {
if assignee == oneAssignee {
isDouble = true
break
}
}

if !isDouble {
multipleAssignees = append(multipleAssignees, oneAssignee)
}
// Check if the user exists
assignee, err := getUserByID(e, assigneeID)
if err != nil {
return false, err
}

// Loop through all assignees to add them
for _, assigneeName := range multipleAssignees {
assignee, err := GetUserByName(assigneeName)
if err != nil {
return err
// Check if the submitted user is already assigned, if yes delete him otherwise add him
var i int
for i = 0; i < len(issue.Assignees); i++ {
if issue.Assignees[i].ID == assigneeID {
break
}

allNewAssignees = append(allNewAssignees, assignee)
}

// Delete all old assignees not passed
if err = DeleteNotPassedAssignee(issue, doer, allNewAssignees); err != nil {
return err
}
assigneeIn := IssueAssignees{AssigneeID: assigneeID, IssueID: issue.ID}

// Add all new assignees
// Update the assignee. The function will check if the user exists, is already
// assigned (which he shouldn't as we deleted all assignees before) and
// has access to the repo.
for _, assignee := range allNewAssignees {
// Extra method to prevent double adding (which would result in removing)
err = AddAssigneeIfNotAssigned(issue, doer, assignee.ID)
toBeDeleted := i < len(issue.Assignees)
if toBeDeleted {
issue.Assignees = append(issue.Assignees[:i], issue.Assignees[i:]...)
_, err = e.Delete(assigneeIn)
if err != nil {
return err
return toBeDeleted, err
}
} else {
issue.Assignees = append(issue.Assignees, assignee)
_, err = e.Insert(assigneeIn)
if err != nil {
return toBeDeleted, err
}
}

return
return toBeDeleted, nil
}

// MakeIDsFromAPIAssigneesToAdd returns an array with all assignee IDs
@@ -292,7 +262,7 @@ func MakeIDsFromAPIAssigneesToAdd(oneAssignee string, multipleAssignees []string
}

// Get the IDs of all assignees
assigneeIDs = GetUserIDsByNames(multipleAssignees)
assigneeIDs, err = GetUserIDsByNames(multipleAssignees, false)

return
}

+ 3
- 3
models/issue_assignees_test.go View File

@@ -20,17 +20,17 @@ func TestUpdateAssignee(t *testing.T) {
// Assign multiple users
user2, err := GetUserByID(2)
assert.NoError(t, err)
err = UpdateAssignee(issue, &User{ID: 1}, user2.ID)
_, _, err = issue.ToggleAssignee(&User{ID: 1}, user2.ID)
assert.NoError(t, err)

user3, err := GetUserByID(3)
assert.NoError(t, err)
err = UpdateAssignee(issue, &User{ID: 1}, user3.ID)
_, _, err = issue.ToggleAssignee(&User{ID: 1}, user3.ID)
assert.NoError(t, err)

user1, err := GetUserByID(1) // This user is already assigned (see the definition in fixtures), so running UpdateAssignee should unassign him
assert.NoError(t, err)
err = UpdateAssignee(issue, &User{ID: 1}, user1.ID)
_, _, err = issue.ToggleAssignee(&User{ID: 1}, user1.ID)
assert.NoError(t, err)

// Check if he got removed

+ 49
- 6
models/issue_comment.go View File

@@ -13,12 +13,14 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/references"
"code.gitea.io/gitea/modules/structs"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"

"github.com/go-xorm/xorm"
"github.com/unknwon/com"
"xorm.io/builder"
"xorm.io/xorm"
)

// CommentType defines whether a comment is just a simple comment, an action (like close) or a reference.
@@ -144,10 +146,10 @@ type Comment struct {

// Reference an issue or pull from another comment, issue or PR
// All information is about the origin of the reference
RefRepoID int64 `xorm:"index"` // Repo where the referencing
RefIssueID int64 `xorm:"index"`
RefCommentID int64 `xorm:"index"` // 0 if origin is Issue title or content (or PR's)
RefAction XRefAction `xorm:"SMALLINT"` // What hapens if RefIssueID resolves
RefRepoID int64 `xorm:"index"` // Repo where the referencing
RefIssueID int64 `xorm:"index"`
RefCommentID int64 `xorm:"index"` // 0 if origin is Issue title or content (or PR's)
RefAction references.XRefAction `xorm:"SMALLINT"` // What hapens if RefIssueID resolves
RefIsPull bool

RefRepo *Repository `xorm:"-"`
@@ -355,6 +357,27 @@ func (c *Comment) LoadAttachments() error {
return nil
}

// UpdateAttachments update attachments by UUIDs for the comment
func (c *Comment) UpdateAttachments(uuids []string) error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
attachments, err := getAttachmentsByUUIDs(sess, uuids)
if err != nil {
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", uuids, err)
}
for i := 0; i < len(attachments); i++ {
attachments[i].IssueID = c.IssueID
attachments[i].CommentID = c.ID
if err := updateAttachment(sess, attachments[i]); err != nil {
return fmt.Errorf("update attachment [id: %d]: %v", attachments[i].ID, err)
}
}
return sess.Commit()
}

// LoadAssigneeUser if comment.Type is CommentTypeAssignees, then load assignees
func (c *Comment) LoadAssigneeUser() error {
var err error
@@ -773,7 +796,7 @@ type CreateCommentOptions struct {
RefRepoID int64
RefIssueID int64
RefCommentID int64
RefAction XRefAction
RefAction references.XRefAction
RefIsPull bool
}

@@ -1021,3 +1044,23 @@ func fetchCodeCommentsByReview(e Engine, issue *Issue, currentUser *User, review
func FetchCodeComments(issue *Issue, currentUser *User) (CodeComments, error) {
return fetchCodeComments(x, issue, currentUser)
}

// UpdateCommentsMigrationsByType updates comments' migrations information via given git service type and original id and poster id
func UpdateCommentsMigrationsByType(tp structs.GitServiceType, originalAuthorID string, posterID int64) error {
_, err := x.Table("comment").
Where(builder.In("issue_id",
builder.Select("issue.id").
From("issue").
InnerJoin("repository", "issue.repo_id = repository.id").
Where(builder.Eq{
"repository.original_service_type": tp,
}),
)).
And("comment.original_author_id = ?", originalAuthorID).
Update(map[string]interface{}{
"poster_id": posterID,
"original_author": "",
"original_author_id": 0,
})
return err
}

+ 10
- 6
models/issue_label.go View File

@@ -13,8 +13,8 @@ import (

api "code.gitea.io/gitea/modules/structs"

"github.com/go-xorm/xorm"
"xorm.io/builder"
"xorm.io/xorm"
)

var labelColorPattern = regexp.MustCompile("#([a-fA-F0-9]{6})")
@@ -68,10 +68,11 @@ type Label struct {
Color string `xorm:"VARCHAR(7)"`
NumIssues int
NumClosedIssues int
NumOpenIssues int `xorm:"-"`
IsChecked bool `xorm:"-"`
QueryString string
IsSelected bool
NumOpenIssues int `xorm:"-"`
IsChecked bool `xorm:"-"`
QueryString string `xorm:"-"`
IsSelected bool `xorm:"-"`
IsExcluded bool `xorm:"-"`
}

// APIFormat converts a Label to the api.Label format
@@ -97,7 +98,10 @@ func (label *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64)
for _, s := range currentSelectedLabels {
if s == label.ID {
labelSelected = true
} else if s > 0 {
} else if -s == label.ID {
labelSelected = true
label.IsExcluded = true
} else if s != 0 {
labelQuerySlice = append(labelQuerySlice, strconv.FormatInt(s, 10))
}
}

+ 13
- 4
models/issue_lock.go View File

@@ -28,7 +28,6 @@ func updateIssueLock(opts *IssueLockOptions, lock bool) error {
}

opts.Issue.IsLocked = lock

var commentType CommentType
if opts.Issue.IsLocked {
commentType = CommentTypeLock
@@ -36,16 +35,26 @@ func updateIssueLock(opts *IssueLockOptions, lock bool) error {
commentType = CommentTypeUnlock
}

if err := UpdateIssueCols(opts.Issue, "is_locked"); err != nil {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}

if err := updateIssueCols(sess, opts.Issue, "is_locked"); err != nil {
return err
}

_, err := CreateComment(&CreateCommentOptions{
_, err := createComment(sess, &CreateCommentOptions{
Doer: opts.Doer,
Issue: opts.Issue,
Repo: opts.Issue.Repo,
Type: commentType,
Content: opts.Reason,
})
return err
if err != nil {
return err
}

return sess.Commit()
}

+ 69
- 57
models/issue_milestone.go View File

@@ -10,8 +10,9 @@ import (
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/builder"

"github.com/go-xorm/xorm"
"xorm.io/xorm"
)

// Milestone represents a milestone of repository.
@@ -191,7 +192,6 @@ func (milestones MilestoneList) getMilestoneIDs() []int64 {

// GetMilestonesByRepoID returns all opened milestones of a repository.
func GetMilestonesByRepoID(repoID int64, state api.StateType) (MilestoneList, error) {

sess := x.Where("repo_id = ?", repoID)

switch state {
@@ -238,13 +238,34 @@ func GetMilestones(repoID int64, page int, isClosed bool, sortType string) (Mile
}

func updateMilestone(e Engine, m *Milestone) error {
_, err := e.ID(m.ID).AllCols().Update(m)
_, err := e.ID(m.ID).AllCols().
SetExpr("num_issues", builder.Select("count(*)").From("issue").Where(
builder.Eq{"milestone_id": m.ID},
)).
SetExpr("num_closed_issues", builder.Select("count(*)").From("issue").Where(
builder.Eq{
"milestone_id": m.ID,
"is_closed": true,
},
)).
Update(m)
return err
}

// UpdateMilestone updates information of given milestone.
func UpdateMilestone(m *Milestone) error {
return updateMilestone(x, m)
if err := updateMilestone(x, m); err != nil {
return err
}

return updateMilestoneCompleteness(x, m.ID)
}

func updateMilestoneCompleteness(e Engine, milestoneID int64) error {
_, err := e.Exec("UPDATE `milestone` SET completeness=100*num_closed_issues/(CASE WHEN num_issues > 0 THEN num_issues ELSE 1 END) WHERE id=?",
milestoneID,
)
return err
}

func countRepoMilestones(e Engine, repoID int64) (int64, error) {
@@ -278,11 +299,6 @@ func MilestoneStats(repoID int64) (open int64, closed int64, err error) {

// ChangeMilestoneStatus changes the milestone open/closed status.
func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) {
repo, err := GetRepositoryByID(m.RepoID)
if err != nil {
return err
}

sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
@@ -290,92 +306,88 @@ func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) {
}

m.IsClosed = isClosed
if err = updateMilestone(sess, m); err != nil {
if _, err := sess.ID(m.ID).Cols("is_closed").Update(m); err != nil {
return err
}

numMilestones, err := countRepoMilestones(sess, repo.ID)
if err != nil {
if err := updateRepoMilestoneNum(sess, m.RepoID); err != nil {
return err
}
numClosedMilestones, err := countRepoClosedMilestones(sess, repo.ID)
if err != nil {
return err
}
repo.NumMilestones = int(numMilestones)
repo.NumClosedMilestones = int(numClosedMilestones)

if _, err = sess.ID(repo.ID).Cols("num_milestones, num_closed_milestones").Update(repo); err != nil {
return err
}
return sess.Commit()
}

func changeMilestoneIssueStats(e *xorm.Session, issue *Issue) error {
if issue.MilestoneID == 0 {
return nil
}
func updateRepoMilestoneNum(e Engine, repoID int64) error {
_, err := e.Exec("UPDATE `repository` SET num_milestones=(SELECT count(*) FROM milestone WHERE repo_id=?),num_closed_milestones=(SELECT count(*) FROM milestone WHERE repo_id=? AND is_closed=?) WHERE id=?",
repoID,
repoID,
true,
repoID,
)
return err
}

m, err := getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID)
if err != nil {
return err
func updateMilestoneTotalNum(e Engine, milestoneID int64) (err error) {
if _, err = e.Exec("UPDATE `milestone` SET num_issues=(SELECT count(*) FROM issue WHERE milestone_id=?) WHERE id=?",
milestoneID,
milestoneID,
); err != nil {
return
}

if issue.IsClosed {
m.NumOpenIssues--
m.NumClosedIssues++
} else {
m.NumOpenIssues++
m.NumClosedIssues--
return updateMilestoneCompleteness(e, milestoneID)
}

func updateMilestoneClosedNum(e Engine, milestoneID int64) (err error) {
if _, err = e.Exec("UPDATE `milestone` SET num_closed_issues=(SELECT count(*) FROM issue WHERE milestone_id=? AND is_closed=?) WHERE id=?",
milestoneID,
true,
milestoneID,
); err != nil {
return
}

return updateMilestone(e, m)
return updateMilestoneCompleteness(e, milestoneID)
}

func changeMilestoneAssign(e *xorm.Session, doer *User, issue *Issue, oldMilestoneID int64) error {
if err := updateIssueCols(e, issue, "milestone_id"); err != nil {
return err
}

if oldMilestoneID > 0 {
m, err := getMilestoneByRepoID(e, issue.RepoID, oldMilestoneID)
if err != nil {
if err := updateMilestoneTotalNum(e, oldMilestoneID); err != nil {
return err
}

m.NumIssues--
if issue.IsClosed {
m.NumClosedIssues--
}

if err = updateMilestone(e, m); err != nil {
return err
if err := updateMilestoneClosedNum(e, oldMilestoneID); err != nil {
return err
}
}
}

if issue.MilestoneID > 0 {
m, err := getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID)
if err != nil {
if err := updateMilestoneTotalNum(e, issue.MilestoneID); err != nil {
return err
}

m.NumIssues++
if issue.IsClosed {
m.NumClosedIssues++
if err := updateMilestoneClosedNum(e, issue.MilestoneID); err != nil {
return err
}
}
}

if err = updateMilestone(e, m); err != nil {
if oldMilestoneID > 0 || issue.MilestoneID > 0 {
if err := issue.loadRepo(e); err != nil {
return err
}
}

if err := issue.loadRepo(e); err != nil {
return err
}

if oldMilestoneID > 0 || issue.MilestoneID > 0 {
if _, err := createMilestoneComment(e, doer, issue.Repo, issue, oldMilestoneID, issue.MilestoneID); err != nil {
return err
}
}

return updateIssueCols(e, issue, "milestone_id")
return nil
}

// ChangeMilestoneAssign changes assignment of milestone for issue.

+ 3
- 3
models/issue_milestone_test.go View File

@@ -231,7 +231,7 @@ func TestChangeMilestoneStatus(t *testing.T) {
CheckConsistencyFor(t, &Repository{ID: milestone.RepoID}, &Milestone{})
}

func TestChangeMilestoneIssueStats(t *testing.T) {
func TestUpdateMilestoneClosedNum(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
issue := AssertExistsAndLoadBean(t, &Issue{MilestoneID: 1},
"is_closed=0").(*Issue)
@@ -240,14 +240,14 @@ func TestChangeMilestoneIssueStats(t *testing.T) {
issue.ClosedUnix = timeutil.TimeStampNow()
_, err := x.Cols("is_closed", "closed_unix").Update(issue)
assert.NoError(t, err)
assert.NoError(t, changeMilestoneIssueStats(x.NewSession(), issue))
assert.NoError(t, updateMilestoneClosedNum(x, issue.MilestoneID))
CheckConsistencyFor(t, &Milestone{})

issue.IsClosed = false
issue.ClosedUnix = 0
_, err = x.Cols("is_closed", "closed_unix").Update(issue)
assert.NoError(t, err)
assert.NoError(t, changeMilestoneIssueStats(x.NewSession(), issue))
assert.NoError(t, updateMilestoneClosedNum(x, issue.MilestoneID))
CheckConsistencyFor(t, &Milestone{})
}


+ 1
- 1
models/issue_reaction.go View File

@@ -11,8 +11,8 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"

"github.com/go-xorm/xorm"
"xorm.io/builder"
"xorm.io/xorm"
)

// Reaction represents a reactions on issues and comments.

+ 34
- 49
models/issue_test.go View File

@@ -84,53 +84,6 @@ func TestGetParticipantsByIssueID(t *testing.T) {
checkParticipants(1, []int{5})
}

func TestIssue_AddLabel(t *testing.T) {
var tests = []struct {
issueID int64
labelID int64
doerID int64
}{
{1, 2, 2}, // non-pull-request, not-already-added label
{1, 1, 2}, // non-pull-request, already-added label
{2, 2, 2}, // pull-request, not-already-added label
{2, 1, 2}, // pull-request, already-added label
}
for _, test := range tests {
assert.NoError(t, PrepareTestDatabase())
issue := AssertExistsAndLoadBean(t, &Issue{ID: test.issueID}).(*Issue)
label := AssertExistsAndLoadBean(t, &Label{ID: test.labelID}).(*Label)
doer := AssertExistsAndLoadBean(t, &User{ID: test.doerID}).(*User)
assert.NoError(t, issue.AddLabel(doer, label))
AssertExistsAndLoadBean(t, &IssueLabel{IssueID: test.issueID, LabelID: test.labelID})
}
}

func TestIssue_AddLabels(t *testing.T) {
var tests = []struct {
issueID int64
labelIDs []int64
doerID int64
}{
{1, []int64{1, 2}, 2}, // non-pull-request
{1, []int64{}, 2}, // non-pull-request, empty
{2, []int64{1, 2}, 2}, // pull-request
{2, []int64{}, 1}, // pull-request, empty
}
for _, test := range tests {
assert.NoError(t, PrepareTestDatabase())
issue := AssertExistsAndLoadBean(t, &Issue{ID: test.issueID}).(*Issue)
labels := make([]*Label, len(test.labelIDs))
for i, labelID := range test.labelIDs {
labels[i] = AssertExistsAndLoadBean(t, &Label{ID: labelID}).(*Label)
}
doer := AssertExistsAndLoadBean(t, &User{ID: test.doerID}).(*User)
assert.NoError(t, issue.AddLabels(doer, labels))
for _, labelID := range test.labelIDs {
AssertExistsAndLoadBean(t, &IssueLabel{IssueID: test.issueID, LabelID: labelID})
}
}
}

func TestIssue_ClearLabels(t *testing.T) {
var tests = []struct {
issueID int64
@@ -160,7 +113,7 @@ func TestUpdateIssueCols(t *testing.T) {
issue.Content = "This should have no effect"

now := time.Now().Unix()
assert.NoError(t, UpdateIssueCols(issue, "name"))
assert.NoError(t, updateIssueCols(x, issue, "name"))
then := time.Now().Unix()

updatedIssue := AssertExistsAndLoadBean(t, &Issue{ID: issue.ID}).(*Issue)
@@ -344,7 +297,7 @@ func testInsertIssue(t *testing.T, title, content string) {
Title: title,
Content: content,
}
err := NewIssue(repo, &issue, nil, nil, nil)
err := NewIssue(repo, &issue, nil, nil)
assert.NoError(t, err)

var newIssue Issue
@@ -366,3 +319,35 @@ func TestIssue_InsertIssue(t *testing.T) {
testInsertIssue(t, "my issue1", "special issue's comments?")
testInsertIssue(t, `my issue2, this is my son's love \n \r \ `, "special issue's '' comments?")
}

func TestIssue_ResolveMentions(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())

testSuccess := func(owner, repo, doer string, mentions []string, expected []int64) {
o := AssertExistsAndLoadBean(t, &User{LowerName: owner}).(*User)
r := AssertExistsAndLoadBean(t, &Repository{OwnerID: o.ID, LowerName: repo}).(*Repository)
issue := &Issue{RepoID: r.ID}
d := AssertExistsAndLoadBean(t, &User{LowerName: doer}).(*User)
resolved, err := issue.ResolveMentionsByVisibility(DefaultDBContext(), d, mentions)
assert.NoError(t, err)
ids := make([]int64, len(resolved))
for i, user := range resolved {
ids[i] = user.ID
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
assert.EqualValues(t, expected, ids)
}

// Public repo, existing user
testSuccess("user2", "repo1", "user1", []string{"user5"}, []int64{5})
// Public repo, non-existing user
testSuccess("user2", "repo1", "user1", []string{"nonexisting"}, []int64{})
// Public repo, doer
testSuccess("user2", "repo1", "user1", []string{"user1"}, []int64{})
// Private repo, team member
testSuccess("user17", "big_test_private_4", "user20", []string{"user2"}, []int64{2})
// Private repo, not a team member
testSuccess("user17", "big_test_private_4", "user20", []string{"user5"}, []int64{})
// Private repo, whole team
testSuccess("user17", "big_test_private_4", "user15", []string{"owners"}, []int64{18})
}

+ 1
- 1
models/issue_tracked_time.go View File

@@ -10,8 +10,8 @@ import (
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"

"github.com/go-xorm/xorm"
"xorm.io/builder"
"xorm.io/xorm"
)

// TrackedTime represents a time that was spent for a specific issue.

+ 0
- 38
models/issue_user.go View File

@@ -6,8 +6,6 @@ package models

import (
"fmt"

"github.com/go-xorm/xorm"
)

// IssueUser represents an issue-user relation.
@@ -51,42 +49,6 @@ func newIssueUsers(e Engine, repo *Repository, issue *Issue) error {
return nil
}

func updateIssueAssignee(e *xorm.Session, issue *Issue, assigneeID int64) (removed bool, err error) {

// Check if the user exists
assignee, err := getUserByID(e, assigneeID)
if err != nil {
return false, err
}

// Check if the submitted user is already assigne, if yes delete him otherwise add him
var i int
for i = 0; i < len(issue.Assignees); i++ {
if issue.Assignees[i].ID == assigneeID {
break
}
}

assigneeIn := IssueAssignees{AssigneeID: assigneeID, IssueID: issue.ID}

toBeDeleted := i < len(issue.Assignees)
if toBeDeleted {
issue.Assignees = append(issue.Assignees[:i], issue.Assignees[i:]...)
_, err = e.Delete(assigneeIn)
if err != nil {
return toBeDeleted, err
}
} else {
issue.Assignees = append(issue.Assignees, assignee)
_, err = e.Insert(assigneeIn)
if err != nil {
return toBeDeleted, err
}
}

return toBeDeleted, nil
}

// UpdateIssueUserByRead updates issue-user relation for reading.
func UpdateIssueUserByRead(uid, issueID int64) error {
_, err := x.Exec("UPDATE `issue_user` SET is_read=? WHERE uid=? AND issue_id=?", true, uid, issueID)

+ 40
- 73
models/issue_xref.go View File

@@ -5,42 +5,16 @@
package models

import (
"regexp"
"strconv"

"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/references"

"github.com/go-xorm/xorm"
"github.com/unknwon/com"
)

var (
// TODO: Unify all regexp treatment of cross references in one place

// issueNumericPattern matches string that references to a numeric issue, e.g. #1287
issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(?:#)([0-9]+)(?:\s|$|\)|\]|:|\.(\s|$))`)
// crossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository
// e.g. gogits/gogs#12345
crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+)/([0-9a-zA-Z-_\.]+)#([0-9]+)(?:\s|$|\)|\]|\.(\s|$))`)
)

// XRefAction represents the kind of effect a cross reference has once is resolved
type XRefAction int64

const (
// XRefActionNone means the cross-reference is a mention (commit, etc.)
XRefActionNone XRefAction = iota // 0
// XRefActionCloses means the cross-reference should close an issue if it is resolved
XRefActionCloses // 1 - not implemented yet
// XRefActionReopens means the cross-reference should reopen an issue if it is resolved
XRefActionReopens // 2 - Not implemented yet
// XRefActionNeutered means the cross-reference will no longer affect the source
XRefActionNeutered // 3
"xorm.io/xorm"
)

type crossReference struct {
Issue *Issue
Action XRefAction
Action references.XRefAction
}

// crossReferencesContext is context to pass along findCrossReference functions
@@ -72,7 +46,7 @@ func newCrossReference(e *xorm.Session, ctx *crossReferencesContext, xref *cross

func neuterCrossReferences(e Engine, issueID int64, commentID int64) error {
active := make([]*Comment, 0, 10)
sess := e.Where("`ref_action` IN (?, ?, ?)", XRefActionNone, XRefActionCloses, XRefActionReopens)
sess := e.Where("`ref_action` IN (?, ?, ?)", references.XRefActionNone, references.XRefActionCloses, references.XRefActionReopens)
if issueID != 0 {
sess = sess.And("`ref_issue_id` = ?", issueID)
}
@@ -86,7 +60,7 @@ func neuterCrossReferences(e Engine, issueID int64, commentID int64) error {
for i, c := range active {
ids[i] = c.ID
}
_, err := e.In("id", ids).Cols("`ref_action`").Update(&Comment{RefAction: XRefActionNeutered})
_, err := e.In("id", ids).Cols("`ref_action`").Update(&Comment{RefAction: references.XRefActionNeutered})
return err
}

@@ -110,11 +84,11 @@ func (issue *Issue) addCrossReferences(e *xorm.Session, doer *User) error {
Doer: doer,
OrigIssue: issue,
}
return issue.createCrossReferences(e, ctx, issue.Title+"\n"+issue.Content)
return issue.createCrossReferences(e, ctx, issue.Title, issue.Content)
}

func (issue *Issue) createCrossReferences(e *xorm.Session, ctx *crossReferencesContext, content string) error {
xreflist, err := ctx.OrigIssue.getCrossReferences(e, ctx, content)
func (issue *Issue) createCrossReferences(e *xorm.Session, ctx *crossReferencesContext, plaincontent, mdcontent string) error {
xreflist, err := ctx.OrigIssue.getCrossReferences(e, ctx, plaincontent, mdcontent)
if err != nil {
return err
}
@@ -126,47 +100,43 @@ func (issue *Issue) createCrossReferences(e *xorm.Session, ctx *crossReferencesC
return nil
}

func (issue *Issue) getCrossReferences(e *xorm.Session, ctx *crossReferencesContext, content string) ([]*crossReference, error) {
func (issue *Issue) getCrossReferences(e *xorm.Session, ctx *crossReferencesContext, plaincontent, mdcontent string) ([]*crossReference, error) {
xreflist := make([]*crossReference, 0, 5)
var xref *crossReference

// Issues in the same repository
// FIXME: Should we support IssueNameStyleAlphanumeric?
matches := issueNumericPattern.FindAllStringSubmatch(content, -1)
for _, match := range matches {
if index, err := strconv.ParseInt(match[1], 10, 64); err == nil {
if err = ctx.OrigIssue.loadRepo(e); err != nil {
var (
refRepo *Repository
refIssue *Issue
err error
)

allrefs := append(references.FindAllIssueReferences(plaincontent), references.FindAllIssueReferencesMarkdown(mdcontent)...)

for _, ref := range allrefs {
if ref.Owner == "" && ref.Name == "" {
// Issues in the same repository
if err := ctx.OrigIssue.loadRepo(e); err != nil {
return nil, err
}
if xref, err = ctx.OrigIssue.isValidCommentReference(e, ctx, issue.Repo, index); err != nil {
return nil, err
}
if xref != nil {
xreflist = ctx.OrigIssue.updateCrossReferenceList(xreflist, xref)
}
}
}

// Issues in other repositories
matches = crossReferenceIssueNumericPattern.FindAllStringSubmatch(content, -1)
for _, match := range matches {
if index, err := strconv.ParseInt(match[3], 10, 64); err == nil {
repo, err := getRepositoryByOwnerAndName(e, match[1], match[2])
refRepo = ctx.OrigIssue.Repo
} else {
// Issues in other repositories
refRepo, err = getRepositoryByOwnerAndName(e, ref.Owner, ref.Name)
if err != nil {
if IsErrRepoNotExist(err) {
continue
}
return nil, err
}
if err = ctx.OrigIssue.loadRepo(e); err != nil {
return nil, err
}
if xref, err = issue.isValidCommentReference(e, ctx, repo, index); err != nil {
return nil, err
}
if xref != nil {
xreflist = issue.updateCrossReferenceList(xreflist, xref)
}
}
if refIssue, err = ctx.OrigIssue.findReferencedIssue(e, ctx, refRepo, ref.Index); err != nil {
return nil, err
}
if refIssue != nil {
xreflist = ctx.OrigIssue.updateCrossReferenceList(xreflist, &crossReference{
Issue: refIssue,
// FIXME: currently ignore keywords
// Action: ref.Action,
Action: references.XRefActionNone,
})
}
}

@@ -179,7 +149,7 @@ func (issue *Issue) updateCrossReferenceList(list []*crossReference, xref *cross
}
for i, r := range list {
if r.Issue.ID == xref.Issue.ID {
if xref.Action != XRefActionNone {
if xref.Action != references.XRefActionNone {
list[i].Action = xref.Action
}
return list
@@ -188,7 +158,7 @@ func (issue *Issue) updateCrossReferenceList(list []*crossReference, xref *cross
return append(list, xref)
}

func (issue *Issue) isValidCommentReference(e Engine, ctx *crossReferencesContext, repo *Repository, index int64) (*crossReference, error) {
func (issue *Issue) findReferencedIssue(e Engine, ctx *crossReferencesContext, repo *Repository, index int64) (*Issue, error) {
refIssue := &Issue{RepoID: repo.ID, Index: index}
if has, _ := e.Get(refIssue); !has {
return nil, nil
@@ -206,10 +176,7 @@ func (issue *Issue) isValidCommentReference(e Engine, ctx *crossReferencesContex
return nil, nil
}
}
return &crossReference{
Issue: refIssue,
Action: XRefActionNone,
}, nil
return refIssue, nil
}

func (issue *Issue) neuterCrossReferences(e Engine) error {
@@ -237,7 +204,7 @@ func (comment *Comment) addCrossReferences(e *xorm.Session, doer *User) error {
OrigIssue: comment.Issue,
OrigComment: comment,
}
return comment.Issue.createCrossReferences(e, ctx, comment.Content)
return comment.Issue.createCrossReferences(e, ctx, "", comment.Content)
}

func (comment *Comment) neuterCrossReferences(e Engine) error {

+ 1
- 1
models/lfs_lock.go View File

@@ -14,7 +14,7 @@ import (
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"

"github.com/go-xorm/xorm"
"xorm.io/xorm"
)

// LFSLock represents a git lfs lock of repository.

+ 1
- 1
models/login_source.go View File

@@ -21,9 +21,9 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"

"github.com/go-xorm/xorm"
"github.com/unknwon/com"
"xorm.io/core"
"xorm.io/xorm"
)

// LoginType represents an login type.

+ 1
- 1
models/migrate.go View File

@@ -4,7 +4,7 @@

package models

import "github.com/go-xorm/xorm"
import "xorm.io/xorm"

// InsertMilestones creates milestones of repository.
func InsertMilestones(ms ...*Milestone) (err error) {

+ 18
- 4
models/migrations/migrations.go View File

@@ -21,10 +21,10 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"

"github.com/go-xorm/xorm"
gouuid "github.com/satori/go.uuid"
"github.com/unknwon/com"
ini "gopkg.in/ini.v1"
"xorm.io/xorm"
)

const minDBVersion = 4
@@ -253,6 +253,18 @@ var migrations = []Migration{
// v98 -> v99
NewMigration("add original author name and id on migrated release", addOriginalAuthorOnMigratedReleases),
// v99 -> v100
NewMigration("add task table and status column for repository table", addTaskTable),
// v100 -> v101
NewMigration("update migration repositories' service type", updateMigrationServiceTypes),
// v101 -> v102
NewMigration("change length of some external login users columns", changeSomeColumnsLengthOfExternalLoginUser),
// v102 -> v103
NewMigration("update migration repositories' service type", dropColumnHeadUserNameOnPullRequest),
// v103 -> v104
NewMigration("Add WhitelistDeployKeys to protected branch", addWhitelistDeployKeysToBranches),
// v104 -> v105
NewMigration("remove unnecessary columns from label", removeLabelUneededCols),
// v105 -> v106
NewMigration("add includes_all_repositories to teams", addTeamIncludesAllRepositories),
}

@@ -406,9 +418,11 @@ func dropTableColumns(sess *xorm.Session, tableName string, columnNames ...strin
}
for _, index := range res {
indexName := index["column_name"]
_, err := sess.Exec(fmt.Sprintf("DROP INDEX `%s` ON `%s`", indexName, tableName))
if err != nil {
return err
if len(indexName) > 0 {
_, err := sess.Exec(fmt.Sprintf("DROP INDEX `%s` ON `%s`", indexName, tableName))
if err != nil {
return err
}
}
}


+ 83
- 0
models/migrations/v100.go View File

@@ -0,0 +1,83 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package migrations

import (
"net/url"
"strings"
"time"

"xorm.io/xorm"
)

func updateMigrationServiceTypes(x *xorm.Engine) error {
type Repository struct {
ID int64
OriginalServiceType int `xorm:"index default(0)"`
OriginalURL string `xorm:"VARCHAR(2048)"`
}

if err := x.Sync2(new(Repository)); err != nil {
return err
}

var last int
const batchSize = 50
for {
var results = make([]Repository, 0, batchSize)
err := x.Where("original_url <> '' AND original_url IS NOT NULL").
And("original_service_type = 0 OR original_service_type IS NULL").
OrderBy("id").
Limit(batchSize, last).
Find(&results)
if err != nil {
return err
}
if len(results) == 0 {
break
}
last += len(results)

const PlainGitService = 1 // 1 plain git service
const GithubService = 2 // 2 github.com

for _, res := range results {
u, err := url.Parse(res.OriginalURL)
if err != nil {
return err
}
var serviceType = PlainGitService
if strings.EqualFold(u.Host, "github.com") {
serviceType = GithubService
}
_, err = x.Exec("UPDATE repository SET original_service_type = ? WHERE id = ?", serviceType, res.ID)
if err != nil {
return err
}
}
}

type ExternalLoginUser struct {
ExternalID string `xorm:"pk NOT NULL"`
UserID int64 `xorm:"INDEX NOT NULL"`
LoginSourceID int64 `xorm:"pk NOT NULL"`
RawData map[string]interface{} `xorm:"TEXT JSON"`
Provider string `xorm:"index VARCHAR(25)"`
Email string
Name string
FirstName string
LastName string
NickName string
Description string
AvatarURL string
Location string
AccessToken string
AccessTokenSecret string
RefreshToken string
ExpiresAt time.Time
}

return x.Sync2(new(ExternalLoginUser))
}

+ 19
- 0
models/migrations/v101.go View File

@@ -0,0 +1,19 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package migrations

import (
"xorm.io/xorm"
)

func changeSomeColumnsLengthOfExternalLoginUser(x *xorm.Engine) error {
type ExternalLoginUser struct {
AccessToken string `xorm:"TEXT"`
AccessTokenSecret string `xorm:"TEXT"`
RefreshToken string `xorm:"TEXT"`
}

return x.Sync2(new(ExternalLoginUser))
}

+ 15
- 0
models/migrations/v102.go View File

@@ -0,0 +1,15 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package migrations

import (
"xorm.io/xorm"
)

func dropColumnHeadUserNameOnPullRequest(x *xorm.Engine) error {
sess := x.NewSession()
defer sess.Close()
return dropTableColumns(sess, "pull_request", "head_user_name")
}

+ 18
- 0
models/migrations/v103.go View File

@@ -0,0 +1,18 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package migrations

import (
"xorm.io/xorm"
)

func addWhitelistDeployKeysToBranches(x *xorm.Engine) error {
type ProtectedBranch struct {
ID int64
WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
}

return x.Sync2(new(ProtectedBranch))
}

+ 34
- 0
models/migrations/v104.go View File

@@ -0,0 +1,34 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package migrations

import (
"xorm.io/xorm"
)

func removeLabelUneededCols(x *xorm.Engine) error {

// Make sure the columns exist before dropping them
type Label struct {
QueryString string
IsSelected bool
}
if err := x.Sync2(new(Label)); err != nil {
return err
}

sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
if err := dropTableColumns(sess, "label", "query_string"); err != nil {
return err
}
if err := dropTableColumns(sess, "label", "is_selected"); err != nil {
return err
}
return sess.Commit()
}

+ 25
- 0
models/migrations/v105.go View File

@@ -0,0 +1,25 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package migrations

import (
"xorm.io/xorm"
)

func addTeamIncludesAllRepositories(x *xorm.Engine) error {

type Team struct {
ID int64 `xorm:"pk autoincr"`
IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"`
}

if err := x.Sync2(new(Team)); err != nil {
return err
}

_, err := x.Exec("UPDATE `team` SET `includes_all_repositories` = ? WHERE `name`=?",
true, "Owners")
return err
}

+ 1
- 1
models/migrations/v13.go View File

@@ -9,8 +9,8 @@ import (
"fmt"
"strings"

"github.com/go-xorm/xorm"
"github.com/unknwon/com"
"xorm.io/xorm"
)

func ldapUseSSLToSecurityProtocol(x *xorm.Engine) error {

+ 1
- 1
models/migrations/v14.go View File

@@ -7,7 +7,7 @@ package migrations
import (
"fmt"

"github.com/go-xorm/xorm"
"xorm.io/xorm"
)

func setCommentUpdatedWithCreated(x *xorm.Engine) (err error) {

+ 1
- 1
models/migrations/v15.go View File

@@ -7,7 +7,7 @@ package migrations
import (
"fmt"

"github.com/go-xorm/xorm"
"xorm.io/xorm"
)

func createAllowCreateOrganizationColumn(x *xorm.Engine) error {

+ 1
- 1
models/migrations/v16.go View File

@@ -10,7 +10,7 @@ import (

"code.gitea.io/gitea/modules/markup"

"github.com/go-xorm/xorm"
"xorm.io/xorm"
)

// Enumerate all the unit types

+ 1
- 1
models/migrations/v17.go View File

@@ -8,7 +8,7 @@ import (
"fmt"
"time"

"github.com/go-xorm/xorm"
"xorm.io/xorm"
)

func setProtectedBranchUpdatedWithCreated(x *xorm.Engine) (err error) {

+ 1
- 1
models/migrations/v18.go View File

@@ -7,7 +7,7 @@ package migrations
import (
"fmt"

"github.com/go-xorm/xorm"
"xorm.io/xorm"
)

// ExternalLoginUser makes the connecting between some existing user and additional external login sources

+ 1
- 1
models/migrations/v19.go View File

@@ -13,8 +13,8 @@ import (

"code.gitea.io/gitea/modules/setting"

"github.com/go-xorm/xorm"
"github.com/unknwon/com"
"xorm.io/xorm"
)

func generateAndMigrateGitHooks(x *xorm.Engine) (err error) {

+ 0
- 0
models/migrations/v20.go View File


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

Loading…
Cancel
Save