Compare commits

..

61 Commits

Author SHA1 Message Date
Lunny Xiao
f3cacf1332 Add changelog for v1.11.3 (#10700)
* Add changelog for v1.11.3

* Apply suggestions from code review

Co-Authored-By: John Olheiser <john.olheiser@gmail.com>

* Update CHANGELOG.md

Co-Authored-By: Antoine GIRARD <sapk@users.noreply.github.com>

Co-authored-by: John Olheiser <john.olheiser@gmail.com>
Co-authored-by: Antoine GIRARD <sapk@users.noreply.github.com>
2020-03-10 17:11:15 +00:00
techknowlogick
a15dc93011 cross compile using go 1.13.x (#10684) 2020-03-09 21:11:02 +02:00
zeripath
66b31786d3 Prevent panic in stopwatch (#10670) (#10673)
Signed-off-by: Andrew Thornton <art27@cantab.net>
2020-03-08 21:14:27 +02:00
Lunny Xiao
931ddfec6d add changelog for v1.10.5 (#10628) (#10632) 2020-03-08 09:54:23 -05:00
Lunny Xiao
7e0a5b17db Fix bug on pull view when required status check no ci result (#10648) (#10651) 2020-03-07 17:32:08 +08:00
zeripath
07688231c2 Changelog for v1.11.2 (#10627)
* Changelog for v1.11.2

* Update CHANGELOG.md

Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>

* Update CHANGELOG.md

Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>

* Update CHANGELOG.md

Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>

* Update CHANGELOG.md

Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>

* Update CHANGELOG.md

Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>

* Update CHANGELOG.md

Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>

* Update CHANGELOG.md

Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>

* as per @jolheiser

* as per @jolheiser x2

* Update CHANGELOG.md

Co-Authored-By: John Olheiser <john.olheiser@gmail.com>

* Update CHANGELOG.md

Co-Authored-By: John Olheiser <john.olheiser@gmail.com>

* another security pr

* another security pr

Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>
Co-authored-by: John Olheiser <john.olheiser@gmail.com>
2020-03-06 15:38:11 +08:00
John Olheiser
21eaeb8418 Issue writers perms can modify issues (#10623) (#10626)
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2020-03-06 04:42:20 +00:00
Lauris BH
9a929ad17f Handle deleted base branch in PR (#10618) (#10619)
Signed-off-by: Andrew Thornton <art27@cantab.net>
2020-03-05 22:23:34 +02:00
zeripath
c19ac41b34 Delete dependencies when deleting a repository (#10608) (#10616)
Co-authored-by: zeripath <art27@cantab.net>
Co-authored-by: Lauris BH <lauris@nix.lv>

Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: zeripath <art27@cantab.net>
2020-03-05 13:56:53 +00:00
Lunny Xiao
fd85d31cb4 Ensure executable bit is kept on the web editor (#10607) (#10614)
Co-authored-by: Antoine GIRARD <sapk@users.noreply.github.com>

Co-authored-by: Antoine GIRARD <sapk@users.noreply.github.com>
2020-03-05 16:55:21 +08:00
guillep2k
c9e4d7a564 Update mergebase in pr checker (#10586) (#10605) 2020-03-04 18:56:09 -03:00
techknowlogick
9990430e32 Build gitea1.11 with go1.13 (#10584) 2020-03-03 07:09:27 +00:00
John Olheiser
6f5656ab0e Logout POST action (#10582) (#10585)
* Change logout to POST

* Update for redirect

* Revert octicon to font

Signed-off-by: jolheiser <john.olheiser@gmail.com>
2020-03-03 13:54:15 +08:00
guillep2k
e4a876cee1 Admin page for managing user e-mail activation (#10557) (#10579)
* Admin page for managing user e-mail activation (#10557)

* Implement mail activation admin panel

* Apply suggestions by @lunny

* Add UI for user activated emails

* Prevent admin from self-deactivate; add modal

Co-authored-by: zeripath <art27@cantab.net>

* Fix pagination options downgrade

Co-authored-by: zeripath <art27@cantab.net>
2020-03-02 17:09:37 -03:00
guillep2k
abb534ba7a Fix migration bug on v96.go (#10572) (#10573)
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2020-03-02 10:32:20 -06:00
James Lakin
65dceb6a40 Fix redirection path if Slack webhook channel is invalid (#10566)
The path to the hook config is already defined by orgRepoCtx
2020-03-02 13:54:07 +02:00
6543
db26f0aca9 head.tmpl og:image picture location (#10531) (#10556)
* head.tmpl og:image picture location (#10531)

og:image picture location

* CI.restart()

* CI.restart()

Co-authored-by: FreeCipher <admin@freecipher.com>
2020-03-01 15:33:18 -05:00
Andreas Shimokawa
76878fd69b Fix 404 after activating secondary email (backport of #10547) (#10553) 2020-03-01 13:27:13 +01:00
zeripath
3444fa2dd7 Fix appearance of unsigned sha box in view_list (#10543) (#10544)
Unfortunately the fix in #10511 was slightly incorrect and placed the
detail box at one level too far out.

Signed-off-by: Andrew Thornton <art27@cantab.net>
2020-02-29 17:20:32 +02:00
zeripath
caa2aeaa52 Show Signer in commit lists and add basic trust (#10425) (#10524)
Backport #10425
Backport #10511

* Show Signer in commit lists and add basic trust (#10425)

Show the avatar of the signer in the commit list pages as we do not
enforce that the signer is an author or committer. This makes it
clearer who has signed the commit.

Also display commits signed by non-members differently from
members and in particular make it clear when a non-member signer
is different from the committer to help reduce the risk of
spoofing.

Signed-off-by: Andrew Thornton <art27@cantab.net>

Fix the signing icon in the  view_list.tmpl page (#10511)

Co-Authored-By: silverwind <me@silverwind.io>
Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>
2020-02-28 14:18:02 -03:00
6543
11300ee582 Fix potential bugs (#10513) (#10518)
* use e if it is an option
* potential nil so check err first
* check err first
* m == nil already checked
2020-02-28 00:12:23 -03:00
John Olheiser
c6b78c3d31 Org action fixes and form cleanup (#10512) (#10514)
* More org fixes

* Move form action query to inputs

Signed-off-by: jolheiser <john.olheiser@gmail.com>
2020-02-27 19:38:21 -06:00
zeripath
4c40aa5be9 Use [:space:] instead of \s (#10508) (#10509)
Backport #10508
2020-02-27 19:12:10 +00:00
6543
50f2e90b76 [BugFix] Avoid mailing explicit unwatched (#10475) (#10500)
* [BugFix] Avoid mailing explicit unwatched (#10475)
2020-02-27 11:42:51 -06:00
zeripath
5d11ccc9e1 Handle push rejection message in Merge & Web Editor (#10373) (#10497)
Backport #10373

* Handle push rejection message in Merge

* Fix sanitize, adjust message handling

* Handle push-rejection in webeditor CRUD too

Co-authored-by: Lauris BH <lauris@nix.lv>

Co-authored-by: Lauris BH <lauris@nix.lv>
2020-02-27 15:37:11 +02:00
guillep2k
93860af542 Fix SQLite concurrency problems by using BEGIN IMMEDIATE (#10368) (#10493) 2020-02-26 23:30:57 -03:00
James Lakin
7bf5834f2c Show the username as a fallback on feeds if full name is blank (#10461)
Co-authored-by: Lauris BH <lauris@nix.lv>
2020-02-26 20:59:07 -05:00
John Olheiser
1fbdd9335f Fix double PR notification from API (#10482) (#10486)
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2020-02-26 22:39:39 +02:00
John Olheiser
e9061a537c Fix admin notices (#10480) (#10483)
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2020-02-26 11:14:37 -06:00
John Olheiser
ed664a9e1d Change admin dashboard to POST (#10465) (#10466)
* Change admin dashboard to POST (#10465)

* Add form and convert to POST

* Redirect for flash

* Convert octicons back to fa for 1.11

Signed-off-by: jolheiser <john.olheiser@gmail.com>
2020-02-25 17:42:43 -06:00
John Olheiser
4cb18601ff Change action GETs to POST (#10462) (#10464)
* Change action GETs to POST

* submite = submit + smite

* No more # href

* Fix test

* Match other tests

* Explicit csrf

Signed-off-by: jolheiser <john.olheiser@gmail.com>

Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>
2020-02-25 15:08:21 -06:00
oscar.lofwenhamn
3abb25166c Update markbates/goth (backport) (#10445)
Update markbates/goth to v1.61.2
- Fixes a JWT decoding issue in the OpenID provider
- Updates the GitHub provider to use the authorization header for authentication
- Updates the Twitch provider for Twitch's v5 API changes
- Adds the email and is_private_email fields to the Apple provider's GetUser implementation
- Modifies gothic to export a non-collidable context key for setting the Provider in a context.Context
- Adds new scopes to the Spotify provider
- Adds the IDToken from OpenID providers on the user struct
- Make Apple provider's SecretParams public
- Adds support for sign in with Apple, and drops support for Go versions 1.7 and 1.8
- Fixes the Slack provider's FetchURL logic to use the appropriate scope for the info it needs
Signed-off-by: Oscar LÃfwenhamn <oscar.lofwenhamn@cgi.com>
2020-02-24 13:19:25 -05:00
6543
9e6ad64d48 Trigger webhooks on issue label-change via API too (#10421) (#10439)
* trigger webhooks with api too

* fix comment

* notify report old too

* CI restart

* restart CI again

* remove duplicated code
2020-02-24 14:30:59 +00:00
Lunny Xiao
b51d7c459e Fix webhook bug (#10427) (#10432)
Co-authored-by: techknowlogick <matti@mdranta.net>
2020-02-24 01:48:30 -03:00
zeripath
d3b6f001fe Various fixes in login sources (#10428) (#10429)
Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>
2020-02-23 22:46:17 +02:00
silverwind
e938f1d945 Add frontend/backend make targets, fix source release (#10325) (#10414)
* Add frontend/backend make targets, fix source release

- Add 'make backend' and 'make frontend' make targets which are used to
  build go and js/css/svg files respectively.

- The 'backend' target can be invoked without requiring Node.js to be
  present on the system if pre-built frontend assets are present like
  in the release source tarballs.

- Fix source releases missing 'dist' folders inside 'node_modules' which
  were erronously excluded from tar.

- Store VERSION in file VERSION for the release tarballs and prefer that
  file over git-derived version.

* fix release task

* fix typo

* fix another typo

Fixes: https://github.com/go-gitea/gitea/issues/10253
2020-02-22 08:51:58 -03:00
guillep2k
7284327a00 Prevent panic on merge to PR (#10403) (#10408)
If you attempt to merge to a branch which on a PR there will be a nil pointer error in the pull request checker.

This panic is uncaught and will bring down the gitea server.

This PR adds protection to prevent this.

Co-authored-by: zeripath <art27@cantab.net>
2020-02-21 22:53:32 +00:00
guillep2k
919f3f11e2 update crypto vendors (#10398)
Co-authored-by: @techknowlogick
2020-02-21 11:27:18 -03:00
guillep2k
3cee15e6f9 Ensure only own addresses are updated (#10397) (#10399) 2020-02-21 10:35:17 -03:00
Lunny Xiao
34e3644ada Fix wrong num closed issues on repository when close issue via commit (#10364) (#10380)
Co-authored-by: Lauris BH <lauris@nix.lv>

Co-authored-by: Lauris BH <lauris@nix.lv>
2020-02-20 15:52:19 +01:00
guillep2k
14bd120cdc Reading pull attachments should depend on read UnitTypePullRequests (#10346) (#10354)
Co-authored-by: zeripath <art27@cantab.net>
2020-02-18 21:02:50 -06:00
6543
3e40f8bebc Set max-width on review-box comment box (#10348) (#10353)
Co-authored-by: zeripath <art27@cantab.net>
2020-02-18 18:59:14 -06:00
zeripath
df5f1d9dca Prevent nil pointer in GetPullRequestCommitStatusState (#10342) (#10344)
Backport #10344 

Ensure that pr.HeadRepo is loaded before using it in GetPullRequestCommitStatusState.

Fixes error on merging with successful commit merge statuses.
2020-02-18 20:04:10 +00:00
John Olheiser
457ee1ab5a Fix status check enable (#10341) (#10343)
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2020-02-18 19:34:50 +00:00
zeripath
4f64688902 Truncate long commit message header (#10301) (#10319)
* Truncate long commit message header

* Fix overflow in view commit table

* Use @media less

* Further improvements

* Fix the commit message on small screens

* adjust width of minimal table
2020-02-18 08:51:39 +01:00
zeripath
117dcf1c02 Set the initial commit status to Success otherwise it will always be Pending (#10317) (#10318)
Backport #10317

The commit status code has a bug whereby setting the initial status to Pending means you can never have the status of Success - it should be set to Success.
2020-02-17 20:50:59 +00:00
mrsdizzie
4529a262c0 Don't manually replace whitespace during render (#10291) (#10315)
* Don't manually replace whitespace during render

For historical reasons Gitea manually alters the urlPrefix and replaces
a whitespace with a +. This Works for URLs, but we're also passing
urlPrefix to git calls and adding the + is breaking the tree path.

Goldmark will automatically convert a white space to the proper %20, so
we should leave the string as is which lets us pass it to git unmodified
and then let Goldmark fix it.

Also fixed separate bug in URLJoin I noticed while testing where it will
silently discard sections of a path that have # in them (possibly
others). We should just escape it first.

Fixes 10156

* Escape elems as well

* Revert "Escape elems as well"

This reverts commit 8bf49596fe.

* restart ci

* remove changes to URLJoin

* restart ci

Co-authored-by: techknowlogick <matti@mdranta.net>

Co-authored-by: techknowlogick <matti@mdranta.net>
2020-02-17 19:46:28 +02:00
zeripath
ff24f81a05 Create Changelog for v1.11.1 release (#10286)
* Create Changelog

* Update CHANGELOG.md

Co-Authored-By: techknowlogick <matti@mdranta.net>

* Include

Co-authored-by: techknowlogick <matti@mdranta.net>
Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: Antoine GIRARD <sapk@users.noreply.github.com>
2020-02-17 01:22:39 +02:00
Antoine GIRARD
e3cb4f9d0e fix: set explicit POSTGRES_PASSWORD for pgsql on drone CI (#10303) 2020-02-16 16:38:54 -05:00
zeripath
9b7890f1cc v1.10.4 Changelog (#10294) (#10305)
* v1.10.4 Changelog

* Add backport identifier for #10261

* Update CHANGELOG.md entry for #9884
2020-02-16 16:38:06 -05:00
Antoine GIRARD
eb064dfda2 make: add release-sources #10295 (#10298)
* make: add release-sources

* make: release-sources exclude .git and make_evidence

* fix: skip exclude MAKE_EVIDENCE_DIR not present in 1.11
2020-02-16 13:23:41 -05:00
zeripath
f9e66e5a46 Blacklist manifest.json & milestones user (#10292) (#10293)
Fix #10290

Co-authored-by: techknowlogick <matti@mdranta.net>
2020-02-16 13:20:59 +02:00
zeripath
ef89260cf1 Repo name added to automatically generated commit message when mergin… (#9997) (#10285)
* Repo name added to automatically generated commit message when merging pull request

* As per @lunny

Co-authored-by: Shashvat Kedia <sk261@snu.edu.in>
Co-authored-by: Lauris BH <lauris@nix.lv>
2020-02-15 21:17:01 +00:00
zeripath
315d928626 Fix Workerpool deadlock (#10283) (#10284)
* Prevent deadlock on boost

* Force a boost in testchannelqueue
2020-02-15 19:28:25 +00:00
guillep2k
5525452bdf Divide GetIssueStats query in smaller chunks (#10176) (#10282)
* Divide GetIssueStats query in smaller chunks

* Skip chunking if count is low enough

* Fix lint

* Define maxQueryParameters

* Remove absMaxQueryParameters because of lint

* Restart CI

* Restart CI

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: zeripath <art27@cantab.net>
Co-authored-by: Lauris BH <lauris@nix.lv>

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: zeripath <art27@cantab.net>
Co-authored-by: Lauris BH <lauris@nix.lv>
2020-02-15 19:07:09 +02:00
Lunny Xiao
987cd277f6 Fix reply on code review (#10227) (#10257)
Co-authored-by: zeripath <art27@cantab.net>

Co-authored-by: zeripath <art27@cantab.net>
2020-02-13 11:32:30 +02:00
zeripath
5cdfde2ebf Stop hanging issue indexer initialisation from preventing shutdown (#10243) (#10249)
Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: Antoine GIRARD <sapk@users.noreply.github.com>
2020-02-12 12:22:10 +02:00
jaqra
1cd6233cef fix filter label emoji width (#10241) (#10244) 2020-02-12 00:39:06 +01:00
James Lakin
cb81e39f7a Fix issue sidebar menus having an infinite height (#10240) 2020-02-11 22:26:33 +01:00
Lauris BH
8efd6b32e2 Fix commit between two commits calculation if there is only last commit (#10225) (#10226) 2020-02-11 11:10:12 +08:00
6543
c95d9603ea Only check for conflicts/merging if the PR has not been merged in the interim (#10132) (#10206)
* Only check for conflicts/merging if the PR has not been merged in the interim (#10132)

* Only check for merging if the PR has not been merged in the interim

* fixup! Only check for merging if the PR has not been merged in the interim

* Try to fix test failure

* Use PR2 not PR1 in tests as PR1 merges automatically

* return already merged error

* enforce locking

* move pullrequest checking to after merge

This might improve the chance that the race does not affect us but does not prevent it.

* Remove minor race with getting merge commit id

move check pr after merge

* Remove unnecessary prepareTestEnv - onGiteaRun does this for us

* Add information about when merging occuring

* More logging

Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>

* re order

Co-authored-by: zeripath <art27@cantab.net>
Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>
2020-02-10 13:09:08 +00:00
244 changed files with 5746 additions and 1812 deletions
+3 -2
View File
@@ -232,6 +232,7 @@ services:
image: postgres:9.5
environment:
POSTGRES_DB: test
POSTGRES_PASSWORD: postgres
- name: ldap
pull: default
@@ -386,7 +387,7 @@ steps:
- name: static
pull: always
image: techknowlogick/xgo:latest
image: techknowlogick/xgo:go-1.13.x
commands:
- apt update && apt -y install curl
- curl -sL https://deb.nodesource.com/setup_12.x | bash - && apt -y install nodejs
@@ -484,7 +485,7 @@ steps:
- name: static
pull: always
image: techknowlogick/xgo:latest
image: techknowlogick/xgo:go-1.13.x
commands:
- apt update && apt -y install curl
- curl -sL https://deb.nodesource.com/setup_12.x | bash - && apt -y install nodejs
+1
View File
@@ -69,6 +69,7 @@ coverage.all
/yarn.lock
/public/js
/public/css
/VERSION
# Snapcraft
snap/.snapcraft/
+84
View File
@@ -4,6 +4,72 @@ 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.11.3](https://github.com/go-gitea/gitea/releases/tag/v1.11.3) - 2020-03-10
* BUGFIXES
* Prevent panic in stopwatch (#10670) (#10673)
* Fix bug on pull view when required status check no ci result (#10648) (#10651)
* Build explicitly with Go 1.13 (#10684)
## [1.11.2](https://github.com/go-gitea/gitea/releases/tag/v1.11.2) - 2020-03-06
* BREAKING
* Various fixes in login sources (#10428) (#10429)
* SECURITY
* Ensure only own addresses are updated (#10397) (#10399)
* Logout POST action (#10582) (#10585)
* Org action fixes and form cleanup (#10512) (#10514)
* Change action GETs to POST (#10462) (#10464)
* Fix admin notices (#10480) (#10483)
* Change admin dashboard to POST (#10465) (#10466)
* Update markbates/goth (#10444) (#10445)
* Update crypto vendors (#10385) (#10398)
* BUGFIXES
* Allow users with write permissions to modify issue descriptions and comments. (#10623) (#10626)
* Handle deleted base branch in PR (#10618) (#10619)
* Delete dependencies when deleting a repository (#10608) (#10616)
* Ensure executable bit is kept on the web editor (#10607) (#10614)
* Update mergebase in pr checker (#10586) (#10605)
* Fix release attachments being deleted while upgrading (#10572) (#10573)
* Fix redirection path if Slack webhook channel is invalid (#10566)
* Fix head.tmpl og:image picture location (#10531) (#10556)
* Fix 404 after activating secondary email (#10547) (#10553)
* Show Signer in commit lists and add basic trust (#10425 & #10511) (#10524)
* Fix potential bugs (#10513) (#10518)
* Use \[:space:\] instead of \\s (#10508) (#10509)
* Avoid mailing users that have explicitly unwatched an issue (#10475) (#10500)
* Handle push rejection message in Merge & Web Editor (#10373) (#10497)
* Fix SQLite concurrency problems by using BEGIN IMMEDIATE (#10368) (#10493)
* Fix double PR notification from API (#10482) (#10486)
* Show the username as a fallback on feeds if full name is blank (#10461)
* Trigger webhooks on issue label-change via API too (#10421) (#10439)
* Fix git reference type in webhooks (#10427) (#10432)
* Prevent panic on merge to PR (#10403) (#10408)
* Fix wrong num closed issues on repository when close issue via commit… (#10364) (#10380)
* Reading pull attachments should depend on read UnitTypePullRequests (#10346) (#10354)
* Set max-width on review-box comment box (#10348) (#10353)
* Prevent nil pointer in GetPullRequestCommitStatusState (#10342) (#10344)
* Fix protected branch status check settings (#10341) (#10343)
* Truncate long commit message header (#10301) (#10319)
* Set the initial commit status to Success otherwise it will always be Pending (#10317) (#10318)
* Don't manually replace whitespace during render (#10291) (#10315)
* ENHANCEMENT
* Admin page for managing user e-mail activation (#10557) (#10579)
## [1.11.1](https://github.com/go-gitea/gitea/releases/tag/v1.11.1) - 2020-02-15
* BUGFIXES
* Repo name added to automatically generated commit message when merging (#9997) (#10285)
* Fix Workerpool deadlock (#10283) (#10284)
* Divide GetIssueStats query in smaller chunks (#10176) (#10282)
* Fix reply on code review (#10257)
* Stop hanging issue indexer initialisation from preventing shutdown (#10243) (#10249)
* Fix filter label emoji width (#10241) (#10244)
* Fix issue sidebar menus having an infinite height (#10239) (#10240)
* Fix commit between two commits calculation if there is only last commit (#10225) (#10226)
* Only check for conflicts/merging if the PR has not been merged in the interim (#10132) (#10206)
* Blacklist manifest.json & milestones user (#10292) (#10293)
## [1.11.0](https://github.com/go-gitea/gitea/releases/tag/v1.11.0) - 2020-02-10
* BREAKING
* Fix followers and following tabs in profile (#10202) (#10203)
@@ -453,6 +519,24 @@ been added to each release, please refer to the [blog](https://blog.gitea.io).
* Update CodeMirror to version 5.49.0 (#8381)
* Wiki editor: enable side-by-side button (#7242)
## [1.10.5](https://github.com/go-gitea/gitea/releases/tag/v1.10.5) - 2020-03-06
* BUGFIXES
* Fix release attachments being deleted while upgrading (#10572) (#10574)
## [1.10.4](https://github.com/go-gitea/gitea/releases/tag/v1.10.4) - 2020-02-16
* FEATURE
* Prevent empty LDAP search from deactivating all users (#9879) (#9890)
* BUGFIXES
* Fix reply on code review (#10261) (#10227)
* Fix branch page pull request title and link error (#10092) (#10098)
* Fix milestone API state parameter unhandled (#10049) (#10053)
* Fix wiki raw view on sub path (#10002) (#10041)
* Fix RocketChat Webhook (#9908) (#9921) (#9925)
* Fix bug about wrong dependencies permissions check and other wrong permissions check (#9884) (Partial backport #9842)
* Ensure that 2fa is checked on reset-password (#9857) (#9877)
## [1.10.3](https://github.com/go-gitea/gitea/releases/tag/v1.10.3) - 2020-01-17
* SECURITY
* Hide credentials when submitting migration (#9102) (#9704)
+38 -16
View File
@@ -29,6 +29,8 @@ EXTRA_GOFLAGS ?=
MAKE_VERSION := $(shell $(MAKE) -v | head -n 1)
STORED_VERSION_FILE := VERSION
ifneq ($(DRONE_TAG),)
VERSION ?= $(subst v,,$(DRONE_TAG))
GITEA_VERSION ?= $(VERSION)
@@ -38,7 +40,13 @@ else
else
VERSION ?= master
endif
GITEA_VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
STORED_VERSION=$(shell cat $(STORED_VERSION_FILE) 2>/dev/null)
ifneq ($(STORED_VERSION),)
GITEA_VERSION ?= $(STORED_VERSION)
else
GITEA_VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
endif
endif
LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)"
@@ -96,13 +104,15 @@ include docker/Makefile
help:
@echo "Make Routines:"
@echo " - \"\" equivalent to \"build\""
@echo " - build creates the entire project"
@echo " - clean delete integration files and build files but not css and js files"
@echo " - clean-all delete all generated files (integration test, build, css and js files)"
@echo " - build build everything"
@echo " - frontend build frontend files"
@echo " - backend build backend files"
@echo " - clean delete backend and integration files"
@echo " - clean-all delete backend, frontend and integration files"
@echo " - css rebuild only css files"
@echo " - js rebuild only js files"
@echo " - generate run \"make css js\" and \"go generate\""
@echo " - fmt format the code"
@echo " - generate run \"go generate\""
@echo " - fmt format the Go code"
@echo " - generate-swagger generate the swagger spec from code comments"
@echo " - swagger-validate check if the swagger spec is valide"
@echo " - revive run code linter revive"
@@ -113,7 +123,7 @@ help:
.PHONY: go-check
go-check:
$(eval GO_VERSION := $(shell printf "%03d%03d%03d" $(shell go version | grep -Eo '[0-9]+\.?[0-9]+?\.?[0-9]?\s' | tr '.' ' ');))
$(eval GO_VERSION := $(shell printf "%03d%03d%03d" $(shell go version | grep -Eo '[0-9]+\.?[0-9]+?\.?[0-9]?[[:space:]]' | tr '.' ' ');))
@if [ "$(GO_VERSION)" -lt "001011000" ]; then \
echo "Gitea requires Go 1.11.0 or greater to build. You can get it at https://golang.org/dl/"; \
exit 1; \
@@ -156,10 +166,6 @@ fmt:
vet:
$(GO) vet $(PACKAGES)
.PHONY: generate
generate: js css
GO111MODULE=on $(GO) generate -mod=vendor $(PACKAGES)
.PHONY: generate-swagger
generate-swagger:
$(SWAGGER) generate spec -o './$(SWAGGER_SPEC)'
@@ -414,13 +420,23 @@ install: $(wildcard *.go)
$(GO) install -v -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)'
.PHONY: build
build: go-check generate $(EXECUTABLE)
build: frontend backend
.PHONY: frontend
frontend: node-check js css
.PHONY: backend
backend: go-check generate $(EXECUTABLE)
.PHONY: generate
generate:
GO111MODULE=on $(GO) generate -mod=vendor $(PACKAGES)
$(EXECUTABLE): $(GO_SOURCES)
GO111MODULE=on $(GO) build -mod=vendor $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@
.PHONY: release
release: generate release-dirs release-windows release-linux release-darwin release-copy release-compress release-check
release: frontend generate release-dirs release-windows release-linux release-darwin release-copy release-compress release-sources release-check
.PHONY: release-dirs
release-dirs:
@@ -431,7 +447,7 @@ release-windows:
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u src.techknowlogick.com/xgo; \
fi
xgo -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
xgo -go go-1.13 -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
ifeq ($(CI),drone)
cp /build/* $(DIST)/binaries
endif
@@ -441,7 +457,7 @@ release-linux:
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u src.techknowlogick.com/xgo; \
fi
xgo -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/mips64le,linux/mips,linux/mipsle' -out gitea-$(VERSION) .
xgo -go go-1.13 -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/mips64le,linux/mips,linux/mipsle' -out gitea-$(VERSION) .
ifeq ($(CI),drone)
cp /build/* $(DIST)/binaries
endif
@@ -451,7 +467,7 @@ release-darwin:
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u src.techknowlogick.com/xgo; \
fi
xgo -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'darwin/*' -out gitea-$(VERSION) .
xgo -go go-1.13 -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'darwin/*' -out gitea-$(VERSION) .
ifeq ($(CI),drone)
cp /build/* $(DIST)/binaries
endif
@@ -471,6 +487,12 @@ release-compress:
fi
cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "compressing $${file}" && gxz -k -9 $${file}; done;
.PHONY: release-sources
release-sources: | node_modules
echo $(VERSION) > $(STORED_VERSION_FILE)
tar --exclude=./$(DIST) --exclude=./.git --exclude=./node_modules/.cache -czf $(DIST)/release/gitea-src-$(VERSION).tar.gz .
rm -f $(STORED_VERSION_FILE)
node_modules: package-lock.json
npm install --no-save
+9
View File
@@ -33,6 +33,15 @@ From the root of the source tree, run:
TAGS="bindata" make build
The `build` target is split into two sub-targets:
- `make backend` which requires [Go 1.11](https://golang.org/dl/) or greater.
- `make frontend` which requires [Node.js 10.0.0](https://nodejs.org/en/download/) or greater.
If pre-built frontend files are present it is possible to only build the backend:
TAGS="bindata" make backend
More info: https://docs.gitea.io/en-us/install-from-source/
## Using
+1 -1
View File
@@ -18,7 +18,7 @@ params:
description: Git with a cup of tea
author: The Gitea Authors
website: https://docs.gitea.io
version: 1.11.0
version: 1.11.2
outputs:
home:
@@ -60,7 +60,7 @@ _Symbols used in table:_
| Git LFS 2.0 | ✓ | ✘ | ✓ | ✓ | ✓ | | ✓ |
| Group Milestones | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
| Granular user roles (Code, Issues, Wiki etc) | ✓ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
| Verified Committer | | ✘ | ? | ✓ | ✓ | ✓ | ✘ |
| Verified Committer | | ✘ | ? | ✓ | ✓ | ✓ | ✘ |
| GPG Signed Commits | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Reject unsigned commits | [](https://github.com/go-gitea/gitea/issues/2770) | ✘ | ✓ | ✓ | ✓ | ✘ | ✓ |
| Repository Activity page | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
@@ -114,6 +114,17 @@ recommended way to build from source is therefore:
TAGS="bindata sqlite sqlite_unlock_notify" make build
```
The `build` target is split into two sub-targets:
- `make backend` which requires [Go 1.11](https://golang.org/dl/) or greater.
- `make frontend` which requires [Node.js 10.0.0](https://nodejs.org/en/download/) or greater.
If pre-built frontend files are present it is possible to only build the backend:
```bash
TAGS="bindata" make backend
``
## Test
After following the steps above, a `gitea` binary will be available in the working directory.
+3 -3
View File
@@ -62,7 +62,7 @@ require (
github.com/lib/pq v1.2.0
github.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
github.com/mailru/easyjson v0.7.0 // indirect
github.com/markbates/goth v1.56.0
github.com/markbates/goth v1.61.2
github.com/mattn/go-isatty v0.0.7
github.com/mattn/go-oci8 v0.0.0-20190320171441-14ba190cf52d // indirect
github.com/mattn/go-sqlite3 v1.11.0
@@ -95,10 +95,10 @@ require (
github.com/yohcop/openid-go v0.0.0-20160914080427-2c050d2dae53
github.com/yuin/goldmark v1.1.19
go.etcd.io/bbolt v1.3.3 // indirect
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876
golang.org/x/crypto v0.0.0-20200219234226-1ad67e1f0ef4
golang.org/x/net v0.0.0-20191101175033-0deb6923b6d9
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
golang.org/x/sys v0.0.0-20191127021746-63cb32ae39b2
golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c
golang.org/x/text v0.3.2
golang.org/x/tools v0.0.0-20191213221258-04c2e8eff935 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
+7 -6
View File
@@ -351,6 +351,7 @@ 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.2 h1:hqi3F8s/B4rz8GuEZZDuHuOxRjeuOpEI/cC7vcnWwH4=
github.com/lafriks/xormstore v1.3.2/go.mod h1:mVNIwIa25QIr8rfR7YlVjrqN/apswHkVdtLCyVYBzXw=
github.com/lestrrat-go/jwx v0.9.0/go.mod h1:iEoxlYfZjvoGpuWwxUz+eR5e6KTJGsaRcy/YNA/UnBk=
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=
@@ -370,8 +371,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN
github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
github.com/markbates/goth v1.56.0 h1:XEYedCgMNz5pi3ojXI8z2XUmXtBnMeuKUpx4Z6HlNj8=
github.com/markbates/goth v1.56.0/go.mod h1:zZmAw0Es0Dpm7TT/4AdN14QrkiWLMrrU9Xei1o+/mdA=
github.com/markbates/goth v1.61.2 h1:jDowrUH5qw8KGuQdKwFhLzkXkTYCIPfz3LHADJsiPIs=
github.com/markbates/goth v1.61.2/go.mod h1:qh2QfwZoWRucQ+DR5KVKC6dUGkNCToWh4vS45GIzFsY=
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-oci8 v0.0.0-20190320171441-14ba190cf52d h1:m+dSK37rFf2fqppZhg15yI2IwC9BtucBiRwSDm9VL8g=
@@ -581,8 +582,8 @@ golang.org/x/crypto v0.0.0-20190907121410-71b5226ff739/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad h1:5E5raQxcv+6CZ11RrBYQe5WRbUIWpScjh0kvHZkZIrQ=
golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 h1:sKJQZMuxjOAR/Uo2LBfU90onWEf1dF4C+0hPJCc9Mpc=
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200219234226-1ad67e1f0ef4 h1:4icQlpeqbz3WxfgP6Eq3szTj95KTrlH/CwzBzoxuFd0=
golang.org/x/crypto v0.0.0-20200219234226-1ad67e1f0ef4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
@@ -652,8 +653,8 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190907184412-d223b2b6db03/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191127021746-63cb32ae39b2 h1:/J2nHFg1MTqaRLFO7M+J78ASNsJoz3r0cvHBPQ77fsE=
golang.org/x/sys v0.0.0-20191127021746-63cb32ae39b2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c h1:jceGD5YNJGgGMkJz79agzOln1K9TaZUjv5ird16qniQ=
golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+15 -7
View File
@@ -39,12 +39,12 @@ func TestAPICreateIssue(t *testing.T) {
defer prepareTestEnv(t)()
const body, title = "apiTestBody", "apiTestTitle"
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)
repoBefore := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repoBefore.OwnerID}).(*models.User)
session := loginUser(t, owner.Name)
token := getTokenForLoggedInUser(t, session)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues?state=all&token=%s", owner.Name, repo.Name, token)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues?state=all&token=%s", owner.Name, repoBefore.Name, token)
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
Body: body,
Title: title,
@@ -57,19 +57,23 @@ func TestAPICreateIssue(t *testing.T) {
assert.Equal(t, apiIssue.Title, title)
models.AssertExistsAndLoadBean(t, &models.Issue{
RepoID: repo.ID,
RepoID: repoBefore.ID,
AssigneeID: owner.ID,
Content: body,
Title: title,
})
repoAfter := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
assert.Equal(t, repoBefore.NumIssues+1, repoAfter.NumIssues)
assert.Equal(t, repoBefore.NumClosedIssues, repoAfter.NumClosedIssues)
}
func TestAPIEditIssue(t *testing.T) {
defer prepareTestEnv(t)()
issueBefore := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 10}).(*models.Issue)
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: issueBefore.RepoID}).(*models.Repository)
owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)
repoBefore := models.AssertExistsAndLoadBean(t, &models.Repository{ID: issueBefore.RepoID}).(*models.Repository)
owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repoBefore.OwnerID}).(*models.User)
assert.NoError(t, issueBefore.LoadAttributes())
assert.Equal(t, int64(1019307200), int64(issueBefore.DeadlineUnix))
assert.Equal(t, api.StateOpen, issueBefore.State())
@@ -84,7 +88,7 @@ func TestAPIEditIssue(t *testing.T) {
body := "new content!"
title := "new title from api set"
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d?token=%s", owner.Name, repo.Name, issueBefore.Index, token)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d?token=%s", owner.Name, repoBefore.Name, issueBefore.Index, token)
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
State: &issueState,
RemoveDeadline: &removeDeadline,
@@ -99,6 +103,7 @@ func TestAPIEditIssue(t *testing.T) {
DecodeJSON(t, resp, &apiIssue)
issueAfter := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 10}).(*models.Issue)
repoAfter := models.AssertExistsAndLoadBean(t, &models.Repository{ID: issueBefore.RepoID}).(*models.Repository)
// check deleted user
assert.Equal(t, int64(500), issueAfter.PosterID)
@@ -107,6 +112,9 @@ func TestAPIEditIssue(t *testing.T) {
assert.Equal(t, int64(-1), issueBefore.PosterID)
assert.Equal(t, int64(-1), apiIssue.Poster.ID)
// check repo change
assert.Equal(t, repoBefore.NumClosedIssues+1, repoAfter.NumClosedIssues)
// API response
assert.Equal(t, api.StateClosed, apiIssue.State)
assert.Equal(t, milestone, apiIssue.Milestone.ID)
+11
View File
@@ -351,6 +351,17 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "protected", "unprotected")(t)
assert.NoError(t, err)
})
t.Run("GenerateCommit", func(t *testing.T) {
_, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
assert.NoError(t, err)
})
t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected-2"))
var pr2 api.PullRequest
t.Run("CreatePullRequest", func(t *testing.T) {
pr2, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "unprotected", "unprotected-2")(t)
assert.NoError(t, err)
})
t.Run("MergePR2", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr2.Index))
t.Run("MergePR", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
t.Run("PullProtected", doGitPull(dstPath, "origin", "protected"))
t.Run("ProtectProtectedBranchWhitelist", doProtectBranch(ctx, "protected", baseCtx.Username))
@@ -126,7 +126,7 @@ func restoreOldDB(t *testing.T, version string) bool {
err := os.MkdirAll(path.Dir(setting.Database.Path), os.ModePerm)
assert.NoError(t, err)
db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&mode=rwc&_busy_timeout=%d", setting.Database.Path, setting.Database.Timeout))
db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&mode=rwc&_busy_timeout=%d&_txlock=immediate", setting.Database.Path, setting.Database.Timeout))
assert.NoError(t, err)
defer db.Close()
+1 -9
View File
@@ -105,8 +105,6 @@ func TestPullRebase(t *testing.T) {
func TestPullRebaseMerge(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
defer prepareTestEnv(t)()
hookTasks, err := models.HookTasks(1, 1) //Retrieve previous hook number
assert.NoError(t, err)
hookTasksLenBefore := len(hookTasks)
@@ -129,8 +127,6 @@ func TestPullRebaseMerge(t *testing.T) {
func TestPullSquash(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
defer prepareTestEnv(t)()
hookTasks, err := models.HookTasks(1, 1) //Retrieve previous hook number
assert.NoError(t, err)
hookTasksLenBefore := len(hookTasks)
@@ -154,10 +150,9 @@ func TestPullSquash(t *testing.T) {
func TestPullCleanUpAfterMerge(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
defer prepareTestEnv(t)()
session := loginUser(t, "user1")
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited)\n")
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited - TestPullCleanUpAfterMerge)\n")
resp := testPullCreate(t, session, "user1", "repo1", "feature/test", "This is a pull title")
@@ -190,7 +185,6 @@ func TestPullCleanUpAfterMerge(t *testing.T) {
func TestCantMergeWorkInProgress(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
defer prepareTestEnv(t)()
session := loginUser(t, "user1")
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
@@ -212,7 +206,6 @@ func TestCantMergeWorkInProgress(t *testing.T) {
func TestCantMergeConflict(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
defer prepareTestEnv(t)()
session := loginUser(t, "user1")
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
@@ -258,7 +251,6 @@ func TestCantMergeConflict(t *testing.T) {
func TestCantMergeUnrelated(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
defer prepareTestEnv(t)()
session := loginUser(t, "user1")
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
+1 -1
View File
@@ -20,7 +20,7 @@ func createNewRelease(t *testing.T, session *TestSession, repoURL, tag, title st
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
link, exists := htmlDoc.doc.Find("form").Attr("action")
link, exists := htmlDoc.doc.Find("form.ui.form").Attr("action")
assert.True(t, exists, "The template has changed")
postData := map[string]string{
+1 -1
View File
@@ -14,7 +14,7 @@ func TestSignOut(t *testing.T) {
session := loginUser(t, "user2")
req := NewRequest(t, "GET", "/user/logout")
req := NewRequest(t, "POST", "/user/logout")
session.MakeRequest(t, req, http.StatusFound)
// try to view a private repo, should fail
+6 -3
View File
@@ -122,10 +122,13 @@ func (a *Action) ShortActUserName() string {
return base.EllipsisString(a.GetActUserName(), 20)
}
// GetDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME
// GetDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank.
func (a *Action) GetDisplayName() string {
if setting.UI.DefaultShowFullName {
return a.GetActFullName()
trimmedFullName := strings.TrimSpace(a.GetActFullName())
if len(trimmedFullName) > 0 {
return trimmedFullName
}
}
return a.ShortActUserName()
}
@@ -212,7 +215,7 @@ func (a *Action) getCommentLink(e Engine) string {
return "#"
}
if a.Comment == nil && a.CommentID != 0 {
a.Comment, _ = GetCommentByID(a.CommentID)
a.Comment, _ = getCommentByID(e, a.CommentID)
}
if a.Comment != nil {
return a.Comment.HTMLURL()
+6 -2
View File
@@ -79,7 +79,11 @@ func (a *Attachment) LinkedRepository() (*Repository, UnitType, error) {
return nil, UnitTypeIssues, err
}
repo, err := GetRepositoryByID(iss.RepoID)
return repo, UnitTypeIssues, err
unitType := UnitTypeIssues
if iss.IsPull {
unitType = UnitTypePullRequests
}
return repo, unitType, err
} else if a.ReleaseID != 0 {
rel, err := GetReleaseByID(a.ReleaseID)
if err != nil {
@@ -195,7 +199,7 @@ func GetAttachmentsByCommentID(commentID int64) ([]*Attachment, error) {
func getAttachmentsByCommentID(e Engine, commentID int64) ([]*Attachment, error) {
attachments := make([]*Attachment, 0, 10)
return attachments, x.Where("comment_id=?", commentID).Find(&attachments)
return attachments, e.Where("comment_id=?", commentID).Find(&attachments)
}
// getAttachmentByReleaseIDFileName return a file based on the the following infos:
+1 -1
View File
@@ -138,7 +138,7 @@ func TestLinkedRepository(t *testing.T) {
expectedUnitType UnitType
}{
{"LinkedIssue", 1, &Repository{ID: 1}, UnitTypeIssues},
{"LinkedComment", 3, &Repository{ID: 1}, UnitTypeIssues},
{"LinkedComment", 3, &Repository{ID: 1}, UnitTypePullRequests},
{"LinkedRelease", 9, &Repository{ID: 1}, UnitTypeReleases},
{"Notlinked", 10, nil, -1},
}
+63
View File
@@ -7,6 +7,7 @@ package models
import (
"fmt"
"strings"
"code.gitea.io/gitea/modules/git"
)
@@ -56,6 +57,21 @@ func (err ErrNamePatternNotAllowed) Error() string {
return fmt.Sprintf("name pattern is not allowed [pattern: %s]", err.Pattern)
}
// ErrNameCharsNotAllowed represents a "character not allowed in name" error.
type ErrNameCharsNotAllowed struct {
Name string
}
// IsErrNameCharsNotAllowed checks if an error is an ErrNameCharsNotAllowed.
func IsErrNameCharsNotAllowed(err error) bool {
_, ok := err.(ErrNameCharsNotAllowed)
return ok
}
func (err ErrNameCharsNotAllowed) Error() string {
return fmt.Sprintf("User name is invalid [%s]: must be valid alpha or numeric or dash(-_) or dot characters", err.Name)
}
// ErrSSHDisabled represents an "SSH disabled" error.
type ErrSSHDisabled struct {
}
@@ -1355,6 +1371,53 @@ func (err ErrMergePushOutOfDate) Error() string {
return fmt.Sprintf("Merge PushOutOfDate Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
}
// ErrPushRejected represents an error if merging fails due to rejection from a hook
type ErrPushRejected struct {
Style MergeStyle
Message string
StdOut string
StdErr string
Err error
}
// IsErrPushRejected checks if an error is a ErrPushRejected.
func IsErrPushRejected(err error) bool {
_, ok := err.(ErrPushRejected)
return ok
}
func (err ErrPushRejected) Error() string {
return fmt.Sprintf("Merge PushRejected Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
}
// GenerateMessage generates the remote message from the stderr
func (err *ErrPushRejected) GenerateMessage() {
messageBuilder := &strings.Builder{}
i := strings.Index(err.StdErr, "remote: ")
if i < 0 {
err.Message = ""
return
}
for {
if len(err.StdErr) <= i+8 {
break
}
if err.StdErr[i:i+8] != "remote: " {
break
}
i += 8
nl := strings.IndexByte(err.StdErr[i:], '\n')
if nl > 0 {
messageBuilder.WriteString(err.StdErr[i : i+nl+1])
i = i + nl + 1
} else {
messageBuilder.WriteString(err.StdErr[i:])
i = len(err.StdErr)
}
}
err.Message = strings.TrimSpace(messageBuilder.String())
}
// ErrRebaseConflicts represents an error if rebase fails with a conflict
type ErrRebaseConflicts struct {
Style MergeStyle
+40 -3
View File
@@ -369,6 +369,7 @@ type CommitVerification struct {
CommittingUser *User
SigningEmail string
SigningKey *GPGKey
TrustStatus string
}
// SignCommit represents a commit with validation of signature.
@@ -754,18 +755,54 @@ func verifyWithGPGSettings(gpgSettings *git.GPGSettings, sig *packet.Signature,
}
// ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys.
func ParseCommitsWithSignature(oldCommits *list.List) *list.List {
func ParseCommitsWithSignature(oldCommits *list.List, repository *Repository) *list.List {
var (
newCommits = list.New()
e = oldCommits.Front()
)
memberMap := map[int64]bool{}
for e != nil {
c := e.Value.(UserCommit)
newCommits.PushBack(SignCommit{
signCommit := SignCommit{
UserCommit: &c,
Verification: ParseCommitWithSignature(c.Commit),
})
}
_ = CalculateTrustStatus(signCommit.Verification, repository, &memberMap)
newCommits.PushBack(signCommit)
e = e.Next()
}
return newCommits
}
// CalculateTrustStatus will calculate the TrustStatus for a commit verification within a repository
func CalculateTrustStatus(verification *CommitVerification, repository *Repository, memberMap *map[int64]bool) (err error) {
if verification.Verified {
verification.TrustStatus = "trusted"
if verification.SigningUser.ID != 0 {
var isMember bool
if memberMap != nil {
var has bool
isMember, has = (*memberMap)[verification.SigningUser.ID]
if !has {
isMember, err = repository.IsOwnerMemberCollaborator(verification.SigningUser.ID)
(*memberMap)[verification.SigningUser.ID] = isMember
}
} else {
isMember, err = repository.IsOwnerMemberCollaborator(verification.SigningUser.ID)
}
if !isMember {
verification.TrustStatus = "untrusted"
if verification.CommittingUser.ID != verification.SigningUser.ID {
// The committing user and the signing user are not the same and are not the default key
// This should be marked as questionable unless the signing user is a collaborator/team member etc.
verification.TrustStatus = "unmatched"
}
}
}
}
return
}
+34
View File
@@ -673,6 +673,10 @@ func (issue *Issue) changeStatus(e *xorm.Session, doer *User, isClosed bool) (*C
return nil, err
}
if err := issue.updateClosedNum(e); err != nil {
return nil, err
}
// New action comment
cmtType := CommentTypeClose
if !issue.IsClosed {
@@ -1336,6 +1340,36 @@ type IssueStatsOptions struct {
// GetIssueStats returns issue statistic information by given conditions.
func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) {
if len(opts.IssueIDs) <= maxQueryParameters {
return getIssueStatsChunk(opts, opts.IssueIDs)
}
// If too long a list of IDs is provided, we get the statistics in
// smaller chunks and get accumulates. Note: this could potentially
// get us invalid results. The alternative is to insert the list of
// ids in a temporary table and join from them.
accum := &IssueStats{}
for i := 0; i < len(opts.IssueIDs); {
chunk := i + maxQueryParameters
if chunk > len(opts.IssueIDs) {
chunk = len(opts.IssueIDs)
}
stats, err := getIssueStatsChunk(opts, opts.IssueIDs[i:chunk])
if err != nil {
return nil, err
}
accum.OpenCount += stats.OpenCount
accum.ClosedCount += stats.ClosedCount
accum.YourRepositoriesCount += stats.YourRepositoriesCount
accum.AssignCount += stats.AssignCount
accum.CreateCount += stats.CreateCount
accum.OpenCount += stats.MentionCount
i = chunk
}
return accum, nil
}
func getIssueStatsChunk(opts *IssueStatsOptions, issueIDs []int64) (*IssueStats, error) {
stats := &IssueStats{}
countSession := func(opts *IssueStatsOptions) *xorm.Session {
+5 -1
View File
@@ -749,8 +749,12 @@ func CreateRefComment(doer *User, repo *Repository, issue *Issue, content, commi
// GetCommentByID returns the comment by given ID.
func GetCommentByID(id int64) (*Comment, error) {
return getCommentByID(x, id)
}
func getCommentByID(e Engine, id int64) (*Comment, error) {
c := new(Comment)
has, err := x.ID(id).Get(c)
has, err := e.ID(id).Get(c)
if err != nil {
return nil, err
} else if !has {
+8 -4
View File
@@ -64,14 +64,18 @@ func getIssueWatch(e Engine, userID, issueID int64) (iw *IssueWatch, exists bool
return
}
// GetIssueWatchersIDs returns IDs of subscribers to a given issue id
// GetIssueWatchersIDs returns IDs of subscribers or explicit unsubscribers to a given issue id
// but avoids joining with `user` for performance reasons
// User permissions must be verified elsewhere if required
func GetIssueWatchersIDs(issueID int64) ([]int64, error) {
func GetIssueWatchersIDs(issueID int64, watching bool) ([]int64, error) {
return getIssueWatchersIDs(x, issueID, watching)
}
func getIssueWatchersIDs(e Engine, issueID int64, watching bool) ([]int64, error) {
ids := make([]int64, 0, 64)
return ids, x.Table("issue_watch").
return ids, e.Table("issue_watch").
Where("issue_id=?", issueID).
And("is_watching = ?", true).
And("is_watching = ?", watching).
Select("user_id").
Find(&ids)
}
+13 -14
View File
@@ -12,7 +12,6 @@ import (
"fmt"
"net/smtp"
"net/textproto"
"regexp"
"strings"
"code.gitea.io/gitea/modules/auth/ldap"
@@ -455,10 +454,6 @@ func composeFullName(firstname, surname, username string) string {
}
}
var (
alphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`)
)
// LoginViaLDAP queries if login/password is valid against the LDAP directory pool,
// and create a local user if success when enabled.
func LoginViaLDAP(user *User, login, password string, source *LoginSource) (*User, error) {
@@ -503,10 +498,6 @@ func LoginViaLDAP(user *User, login, password string, source *LoginSource) (*Use
if len(sr.Username) == 0 {
sr.Username = login
}
// Validate username make sure it satisfies requirement.
if alphaDashDotPattern.MatchString(sr.Username) {
return nil, fmt.Errorf("Invalid pattern for attribute 'username' [%s]: must be valid alpha or numeric or dash(-_) or dot characters", sr.Username)
}
if len(sr.Mail) == 0 {
sr.Mail = fmt.Sprintf("%s@localhost", sr.Username)
@@ -666,7 +657,8 @@ func LoginViaSMTP(user *User, login, password string, sourceID int64, cfg *SMTPC
// LoginViaPAM queries if login/password is valid against the PAM,
// and create a local user if success when enabled.
func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMConfig) (*User, error) {
if err := pam.Auth(cfg.ServiceName, login, password); err != nil {
pamLogin, err := pam.Auth(cfg.ServiceName, login, password)
if err != nil {
if strings.Contains(err.Error(), "Authentication failure") {
return nil, ErrUserNotExist{0, login, 0}
}
@@ -677,14 +669,21 @@ func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMCon
return user, nil
}
// Allow PAM sources with `@` in their name, like from Active Directory
username := pamLogin
idx := strings.Index(pamLogin, "@")
if idx > -1 {
username = pamLogin[:idx]
}
user = &User{
LowerName: strings.ToLower(login),
Name: login,
Email: login,
LowerName: strings.ToLower(username),
Name: username,
Email: pamLogin,
Passwd: password,
LoginType: LoginPAM,
LoginSource: sourceID,
LoginName: login,
LoginName: login, // This is what the user typed in
IsActive: true,
}
return user, CreateUser(user)
+32 -17
View File
@@ -26,23 +26,38 @@ func deleteOrphanedAttachments(x *xorm.Engine) error {
sess := x.NewSession()
defer sess.Close()
err := sess.BufferSize(setting.Database.IterateBufferSize).
Where("`issue_id` = 0 and (`release_id` = 0 or `release_id` not in (select `id` from `release`))").Cols("uuid").
Iterate(new(Attachment),
func(idx int, bean interface{}) error {
attachment := bean.(*Attachment)
if err := os.RemoveAll(models.AttachmentLocalPath(attachment.UUID)); err != nil {
return err
}
_, err := sess.ID(attachment.ID).NoAutoCondition().Delete(attachment)
return err
})
if err != nil {
return err
var limit = setting.Database.IterateBufferSize
if limit <= 0 {
limit = 50
}
return sess.Commit()
for {
attachements := make([]Attachment, 0, limit)
if err := sess.Where("`issue_id` = 0 and (`release_id` = 0 or `release_id` not in (select `id` from `release`))").
Cols("id, uuid").Limit(limit).
Asc("id").
Find(&attachements); err != nil {
return err
}
if len(attachements) == 0 {
return nil
}
var ids = make([]int64, 0, limit)
for _, attachment := range attachements {
ids = append(ids, attachment.ID)
}
if _, err := sess.In("id", ids).Delete(new(Attachment)); err != nil {
return err
}
for _, attachment := range attachements {
if err := os.RemoveAll(models.AttachmentLocalPath(attachment.UUID)); err != nil {
return err
}
}
if len(attachements) < limit {
return nil
}
}
}
+6
View File
@@ -46,6 +46,12 @@ type Engine interface {
Asc(colNames ...string) *xorm.Session
}
const (
// When queries are broken down in parts because of the number
// of parameters, attempt to break by this amount
maxQueryParameters = 300
)
var (
x *xorm.Engine
tables []interface{}
+1 -1
View File
@@ -399,7 +399,7 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
// Create org.
org := &User{
Name: "All repo",
Name: "All_repo",
IsActive: true,
Type: UserTypeOrganization,
Visibility: structs.VisibleTypePublic,
+58 -21
View File
@@ -180,7 +180,16 @@ func (pr *PullRequest) GetDefaultMergeMessage() string {
return ""
}
}
return fmt.Sprintf("Merge branch '%s' of %s/%s into %s", pr.HeadBranch, pr.MustHeadUserName(), pr.HeadRepo.Name, pr.BaseBranch)
if err := pr.LoadIssue(); err != nil {
log.Error("Cannot load issue %d for PR id %d: Error: %v", pr.IssueID, pr.ID, err)
return ""
}
if pr.BaseRepoID == pr.HeadRepoID {
return fmt.Sprintf("Merge pull request '%s' (#%d) from %s into %s", pr.Issue.Title, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch)
}
return fmt.Sprintf("Merge pull request '%s' (#%d) from %s:%s into %s", pr.Issue.Title, pr.Issue.Index, pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseBranch)
}
// GetCommitMessages returns the commit messages between head and merge base (if there is one)
@@ -649,44 +658,66 @@ func (pr *PullRequest) CheckUserAllowedToMerge(doer *User) (err error) {
}
// SetMerged sets a pull request to merged and closes the corresponding issue
func (pr *PullRequest) SetMerged() (err error) {
func (pr *PullRequest) SetMerged() (bool, error) {
if pr.HasMerged {
return fmt.Errorf("PullRequest[%d] already merged", pr.Index)
return false, fmt.Errorf("PullRequest[%d] already merged", pr.Index)
}
if pr.MergedCommitID == "" || pr.MergedUnix == 0 || pr.Merger == nil {
return fmt.Errorf("Unable to merge PullRequest[%d], some required fields are empty", pr.Index)
return false, fmt.Errorf("Unable to merge PullRequest[%d], some required fields are empty", pr.Index)
}
pr.HasMerged = true
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
if err := sess.Begin(); err != nil {
return false, err
}
if err = pr.loadIssue(sess); err != nil {
return err
if _, err := sess.Exec("UPDATE `issue` SET `repo_id` = `repo_id` WHERE `id` = ?", pr.IssueID); err != nil {
return false, err
}
if err = pr.Issue.loadRepo(sess); err != nil {
return err
}
if err = pr.Issue.Repo.getOwner(sess); err != nil {
return err
if _, err := sess.Exec("UPDATE `pull_request` SET `issue_id` = `issue_id` WHERE `id` = ?", pr.ID); err != nil {
return false, err
}
if _, err = pr.Issue.changeStatus(sess, pr.Merger, true); err != nil {
return fmt.Errorf("Issue.changeStatus: %v", err)
}
if _, err = sess.ID(pr.ID).Cols("has_merged, status, merged_commit_id, merger_id, merged_unix").Update(pr); err != nil {
return fmt.Errorf("update pull request: %v", err)
pr.Issue = nil
if err := pr.loadIssue(sess); err != nil {
return false, err
}
if err = sess.Commit(); err != nil {
return fmt.Errorf("Commit: %v", err)
if tmpPr, err := getPullRequestByID(sess, pr.ID); err != nil {
return false, err
} else if tmpPr.HasMerged {
if pr.Issue.IsClosed {
return false, nil
}
return false, fmt.Errorf("PullRequest[%d] already merged but it's associated issue [%d] is not closed", pr.Index, pr.IssueID)
} else if pr.Issue.IsClosed {
return false, fmt.Errorf("PullRequest[%d] already closed", pr.Index)
}
return nil
if err := pr.Issue.loadRepo(sess); err != nil {
return false, err
}
if err := pr.Issue.Repo.getOwner(sess); err != nil {
return false, err
}
if _, err := pr.Issue.changeStatus(sess, pr.Merger, true); err != nil {
return false, fmt.Errorf("Issue.changeStatus: %v", err)
}
if _, err := sess.Where("id = ?", pr.ID).Cols("has_merged, status, merged_commit_id, merger_id, merged_unix").Update(pr); err != nil {
return false, fmt.Errorf("Failed to update pr[%d]: %v", pr.ID, err)
}
if err := sess.Commit(); err != nil {
return false, fmt.Errorf("Commit: %v", err)
}
return true, nil
}
// NewPullRequest creates new pull request with labels for repository.
@@ -845,6 +876,12 @@ func (pr *PullRequest) UpdateCols(cols ...string) error {
return err
}
// UpdateColsIfNotMerged updates specific fields of a pull request if it has not been merged
func (pr *PullRequest) UpdateColsIfNotMerged(cols ...string) error {
_, err := x.Where("id = ? AND has_merged = ?", pr.ID, false).Cols(cols...).Update(pr)
return err
}
// IsWorkInProgress determine if the Pull Request is a Work In Progress by its title
func (pr *PullRequest) IsWorkInProgress() bool {
if err := pr.LoadIssue(); err != nil {
+12
View File
@@ -1856,6 +1856,18 @@ func DeleteRepository(doer *User, uid, repoID int64) error {
return err
}
// Dependencies for issues in this repository
if _, err = sess.In("issue_id", deleteCond).
Delete(&IssueDependency{}); err != nil {
return err
}
// Delete dependencies for issues in other repositories
if _, err = sess.In("dependency_id", deleteCond).
Delete(&IssueDependency{}); err != nil {
return err
}
if _, err = sess.In("issue_id", deleteCond).
Delete(&IssueUser{}); err != nil {
return err
+20
View File
@@ -202,3 +202,23 @@ func (repo *Repository) getRepoTeams(e Engine) (teams []*Team, err error) {
func (repo *Repository) GetRepoTeams() ([]*Team, error) {
return repo.getRepoTeams(x)
}
// IsOwnerMemberCollaborator checks if a provided user is the owner, a collaborator or a member of a team in a repository
func (repo *Repository) IsOwnerMemberCollaborator(userID int64) (bool, error) {
if repo.OwnerID == userID {
return true, nil
}
teamMember, err := x.Join("INNER", "team_repo", "team_repo.team_id = team_user.team_id").
Join("INNER", "team_unit", "team_unit.team_id = team_user.team_id").
Where("team_repo.repo_id = ?", repo.ID).
And("team_unit.`type` = ?", UnitTypeCode).
And("team_user.uid = ?", userID).Table("team_user").Exist(&TeamUser{})
if err != nil {
return false, err
}
if teamMember {
return true, nil
}
return x.Get(&Collaboration{RepoID: repo.ID, UserID: userID})
}
+1 -1
View File
@@ -84,7 +84,7 @@ func MainTest(m *testing.M, pathToGiteaRoot string) {
func createTestEngine(fixturesDir string) error {
var err error
x, err = xorm.NewEngine("sqlite3", "file::memory:?cache=shared")
x, err = xorm.NewEngine("sqlite3", "file::memory:?cache=shared&_txlock=immediate")
if err != nil {
return err
}
+17 -4
View File
@@ -18,6 +18,7 @@ import (
"image/png"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
@@ -87,6 +88,9 @@ var (
// ErrUnsupportedLoginType login source is unknown error
ErrUnsupportedLoginType = errors.New("Login source is unknown")
// Characters prohibited in a user name (anything except A-Za-z0-9_.-)
alphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`)
)
// User represents the object of individual and member of organization.
@@ -708,9 +712,11 @@ func (u *User) DisplayName() string {
// GetDisplayName returns full name if it's not empty and DEFAULT_SHOW_FULL_NAME is set,
// returns username otherwise.
func (u *User) GetDisplayName() string {
trimmed := strings.TrimSpace(u.FullName)
if len(trimmed) > 0 && setting.UI.DefaultShowFullName {
return trimmed
if setting.UI.DefaultShowFullName {
trimmed := strings.TrimSpace(u.FullName)
if len(trimmed) > 0 {
return trimmed
}
}
return u.Name
}
@@ -819,7 +825,9 @@ var (
"issues",
"js",
"less",
"manifest.json",
"metrics",
"milestones",
"new",
"notifications",
"org",
@@ -868,6 +876,11 @@ func isUsableName(names, patterns []string, name string) error {
// IsUsableUsername returns an error when a username is reserved
func IsUsableUsername(name string) error {
// Validate username make sure it satisfies requirement.
if alphaDashDotPattern.MatchString(name) {
// Note: usually this error is normally caught up earlier in the UI
return ErrNameCharsNotAllowed{Name: name}
}
return isUsableName(reservedUsernames, reservedUserPatterns, name)
}
@@ -987,7 +1000,7 @@ func VerifyActiveEmailCode(code, email string) *EmailAddress {
data := com.ToStr(user.ID) + email + user.LowerName + user.Passwd + user.Rands
if base.VerifyTimeLimitCode(data, minutes, prefix) {
emailAddress := &EmailAddress{Email: email}
emailAddress := &EmailAddress{UID: user.ID, Email: email}
if has, _ := x.Get(emailAddress); has {
return emailAddress
}
+274 -19
View File
@@ -1,4 +1,5 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
@@ -8,6 +9,12 @@ import (
"errors"
"fmt"
"strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
)
var (
@@ -54,13 +61,66 @@ func GetEmailAddresses(uid int64) ([]*EmailAddress, error) {
if !isPrimaryFound {
emails = append(emails, &EmailAddress{
Email: u.Email,
IsActivated: true,
IsActivated: u.IsActive,
IsPrimary: true,
})
}
return emails, nil
}
// GetEmailAddressByID gets a user's email address by ID
func GetEmailAddressByID(uid, id int64) (*EmailAddress, error) {
// User ID is required for security reasons
email := &EmailAddress{ID: id, UID: uid}
if has, err := x.Get(email); err != nil {
return nil, err
} else if !has {
return nil, nil
}
return email, nil
}
func isEmailActive(e Engine, email string, userID, emailID int64) (bool, error) {
if len(email) == 0 {
return true, nil
}
// Can't filter by boolean field unless it's explicit
cond := builder.NewCond()
cond = cond.And(builder.Eq{"email": email}, builder.Neq{"id": emailID})
if setting.Service.RegisterEmailConfirm {
// Inactive (unvalidated) addresses don't count as active if email validation is required
cond = cond.And(builder.Eq{"is_activated": true})
}
em := EmailAddress{}
if has, err := e.Where(cond).Get(&em); has || err != nil {
if has {
log.Info("isEmailActive('%s',%d,%d) found duplicate in email ID %d", email, userID, emailID, em.ID)
}
return has, err
}
// Can't filter by boolean field unless it's explicit
cond = builder.NewCond()
cond = cond.And(builder.Eq{"email": email}, builder.Neq{"id": userID})
if setting.Service.RegisterEmailConfirm {
cond = cond.And(builder.Eq{"is_active": true})
}
us := User{}
if has, err := e.Where(cond).Get(&us); has || err != nil {
if has {
log.Info("isEmailActive('%s',%d,%d) found duplicate in user ID %d", email, userID, emailID, us.ID)
}
return has, err
}
return false, nil
}
func isEmailUsed(e Engine, email string) (bool, error) {
if len(email) == 0 {
return true, nil
@@ -118,31 +178,30 @@ func AddEmailAddresses(emails []*EmailAddress) error {
// Activate activates the email address to given user.
func (email *EmailAddress) Activate() error {
user, err := GetUserByID(email.UID)
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
if err := email.updateActivation(sess, true); err != nil {
return err
}
return sess.Commit()
}
func (email *EmailAddress) updateActivation(e Engine, activate bool) error {
user, err := getUserByID(e, email.UID)
if err != nil {
return err
}
if user.Rands, err = GetUserSalt(); err != nil {
return err
}
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
email.IsActivated = activate
if _, err := e.ID(email.ID).Cols("is_activated").Update(email); err != nil {
return err
}
email.IsActivated = true
if _, err := sess.
ID(email.ID).
Cols("is_activated").
Update(email); err != nil {
return err
} else if err = updateUserCols(sess, user, "rands"); err != nil {
return err
}
return sess.Commit()
return updateUserCols(e, user, "rands")
}
// DeleteEmailAddress deletes an email address of given user.
@@ -201,7 +260,7 @@ func MakeEmailPrimary(email *EmailAddress) error {
}
// Make sure the former primary email doesn't disappear.
formerPrimaryEmail := &EmailAddress{Email: user.Email}
formerPrimaryEmail := &EmailAddress{UID: user.ID, Email: user.Email}
has, err = x.Get(formerPrimaryEmail)
if err != nil {
return err
@@ -228,3 +287,199 @@ func MakeEmailPrimary(email *EmailAddress) error {
return sess.Commit()
}
// SearchEmailOrderBy is used to sort the results from SearchEmails()
type SearchEmailOrderBy string
func (s SearchEmailOrderBy) String() string {
return string(s)
}
// Strings for sorting result
const (
SearchEmailOrderByEmail SearchEmailOrderBy = "emails.email ASC, is_primary DESC, sortid ASC"
SearchEmailOrderByEmailReverse SearchEmailOrderBy = "emails.email DESC, is_primary ASC, sortid DESC"
SearchEmailOrderByName SearchEmailOrderBy = "`user`.lower_name ASC, is_primary DESC, sortid ASC"
SearchEmailOrderByNameReverse SearchEmailOrderBy = "`user`.lower_name DESC, is_primary ASC, sortid DESC"
)
// SearchEmailOptions are options to search e-mail addresses for the admin panel
type SearchEmailOptions struct {
Page int
PageSize int // Can be smaller than or equal to setting.UI.ExplorePagingNum
Keyword string
SortType SearchEmailOrderBy
IsPrimary util.OptionalBool
IsActivated util.OptionalBool
}
// SearchEmailResult is an e-mail address found in the user or email_address table
type SearchEmailResult struct {
UID int64
Email string
IsActivated bool
IsPrimary bool
// From User
Name string
FullName string
}
// SearchEmails takes options i.e. keyword and part of email name to search,
// it returns results in given range and number of total results.
func SearchEmails(opts *SearchEmailOptions) ([]*SearchEmailResult, int64, error) {
// Unfortunately, UNION support for SQLite in xorm is currently broken, so we must
// build the SQL ourselves.
where := make([]string, 0, 5)
args := make([]interface{}, 0, 5)
emailsSQL := "(SELECT id as sortid, uid, email, is_activated, 0 as is_primary " +
"FROM email_address " +
"UNION ALL " +
"SELECT id as sortid, id AS uid, email, is_active AS is_activated, 1 as is_primary " +
"FROM `user` " +
"WHERE type = ?) AS emails"
args = append(args, UserTypeIndividual)
if len(opts.Keyword) > 0 {
// Note: % can be injected in the Keyword parameter, but it won't do any harm.
where = append(where, "(lower(`user`.full_name) LIKE ? OR `user`.lower_name LIKE ? OR emails.email LIKE ?)")
likeStr := "%" + strings.ToLower(opts.Keyword) + "%"
args = append(args, likeStr)
args = append(args, likeStr)
args = append(args, likeStr)
}
switch {
case opts.IsPrimary.IsTrue():
where = append(where, "emails.is_primary = ?")
args = append(args, true)
case opts.IsPrimary.IsFalse():
where = append(where, "emails.is_primary = ?")
args = append(args, false)
}
switch {
case opts.IsActivated.IsTrue():
where = append(where, "emails.is_activated = ?")
args = append(args, true)
case opts.IsActivated.IsFalse():
where = append(where, "emails.is_activated = ?")
args = append(args, false)
}
var whereStr string
if len(where) > 0 {
whereStr = "WHERE " + strings.Join(where, " AND ")
}
joinSQL := "FROM " + emailsSQL + " INNER JOIN `user` ON `user`.id = emails.uid " + whereStr
count, err := x.SQL("SELECT count(*) "+joinSQL, args...).Count()
if err != nil {
return nil, 0, fmt.Errorf("Count: %v", err)
}
orderby := opts.SortType.String()
if orderby == "" {
orderby = SearchEmailOrderByEmail.String()
}
querySQL := "SELECT emails.uid, emails.email, emails.is_activated, emails.is_primary, " +
"`user`.name, `user`.full_name " + joinSQL + " ORDER BY " + orderby
if opts.PageSize == 0 || opts.PageSize > setting.UI.ExplorePagingNum {
opts.PageSize = setting.UI.ExplorePagingNum
}
if opts.Page <= 0 {
opts.Page = 1
}
rows, err := x.SQL(querySQL, args...).Rows(new(SearchEmailResult))
if err != nil {
return nil, 0, fmt.Errorf("Emails: %v", err)
}
// Page manually because xorm can't handle Limit() with raw SQL
defer rows.Close()
emails := make([]*SearchEmailResult, 0, opts.PageSize)
skip := (opts.Page - 1) * opts.PageSize
for rows.Next() {
var email SearchEmailResult
if err := rows.Scan(&email); err != nil {
return nil, 0, err
}
if skip > 0 {
skip--
continue
}
emails = append(emails, &email)
if len(emails) == opts.PageSize {
break
}
}
return emails, count, err
}
// ActivateUserEmail will change the activated state of an email address,
// either primary (in the user table) or secondary (in the email_address table)
func ActivateUserEmail(userID int64, email string, primary, activate bool) (err error) {
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
if primary {
// Activate/deactivate a user's primary email address
user := User{ID: userID, Email: email}
if has, err := sess.Get(&user); err != nil {
return err
} else if !has {
return fmt.Errorf("no such user: %d (%s)", userID, email)
}
if user.IsActive == activate {
// Already in the desired state; no action
return nil
}
if activate {
if used, err := isEmailActive(sess, email, userID, 0); err != nil {
return fmt.Errorf("isEmailActive(): %v", err)
} else if used {
return ErrEmailAlreadyUsed{Email: email}
}
}
user.IsActive = activate
if user.Rands, err = GetUserSalt(); err != nil {
return fmt.Errorf("generate salt: %v", err)
}
if err = updateUserCols(sess, &user, "is_active", "rands"); err != nil {
return fmt.Errorf("updateUserCols(): %v", err)
}
} else {
// Activate/deactivate a user's secondary email address
// First check if there's another user active with the same address
addr := EmailAddress{UID: userID, Email: email}
if has, err := sess.Get(&addr); err != nil {
return err
} else if !has {
return fmt.Errorf("no such email: %d (%s)", userID, email)
}
if addr.IsActivated == activate {
// Already in the desired state; no action
return nil
}
if activate {
if used, err := isEmailActive(sess, email, 0, addr.ID); err != nil {
return fmt.Errorf("isEmailActive(): %v", err)
} else if used {
return ErrEmailAlreadyUsed{Email: email}
}
}
if err = addr.updateActivation(sess, activate); err != nil {
return fmt.Errorf("updateActivation(): %v", err)
}
}
return sess.Commit()
}
+64
View File
@@ -7,6 +7,8 @@ package models
import (
"testing"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
)
@@ -169,3 +171,65 @@ func TestActivate(t *testing.T) {
assert.True(t, emails[2].IsActivated)
assert.True(t, emails[2].IsPrimary)
}
func TestListEmails(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
// Must find all users and their emails
opts := &SearchEmailOptions{}
emails, count, err := SearchEmails(opts)
assert.NoError(t, err)
assert.NotEqual(t, int64(0), count)
assert.True(t, count > 5)
contains := func(match func(s *SearchEmailResult) bool) bool {
for _, v := range emails {
if match(v) {
return true
}
}
return false
}
assert.True(t, contains(func(s *SearchEmailResult) bool { return s.UID == 18 }))
// 'user3' is an organization
assert.False(t, contains(func(s *SearchEmailResult) bool { return s.UID == 3 }))
// Must find no records
opts = &SearchEmailOptions{Keyword: "NOTFOUND"}
emails, count, err = SearchEmails(opts)
assert.NoError(t, err)
assert.Equal(t, int64(0), count)
// Must find users 'user2', 'user28', etc.
opts = &SearchEmailOptions{Keyword: "user2"}
emails, count, err = SearchEmails(opts)
assert.NoError(t, err)
assert.NotEqual(t, int64(0), count)
assert.True(t, contains(func(s *SearchEmailResult) bool { return s.UID == 2 }))
assert.True(t, contains(func(s *SearchEmailResult) bool { return s.UID == 27 }))
// Must find only primary addresses (i.e. from the `user` table)
opts = &SearchEmailOptions{IsPrimary: util.OptionalBoolTrue}
emails, count, err = SearchEmails(opts)
assert.NoError(t, err)
assert.True(t, contains(func(s *SearchEmailResult) bool { return s.IsPrimary }))
assert.False(t, contains(func(s *SearchEmailResult) bool { return !s.IsPrimary }))
// Must find only inactive addresses (i.e. not validated)
opts = &SearchEmailOptions{IsActivated: util.OptionalBoolFalse}
emails, count, err = SearchEmails(opts)
assert.NoError(t, err)
assert.True(t, contains(func(s *SearchEmailResult) bool { return !s.IsActivated }))
assert.False(t, contains(func(s *SearchEmailResult) bool { return s.IsActivated }))
// Must find more than one page, but retrieve only one
opts = &SearchEmailOptions{
PageSize: 5,
Page: 1,
}
emails, count, err = SearchEmails(opts)
assert.NoError(t, err)
assert.Equal(t, 5, len(emails))
assert.True(t, count > int64(len(emails)))
}
+10
View File
@@ -47,3 +47,13 @@ type AdminEditUserForm struct {
func (f *AdminEditUserForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, f, ctx.Locale)
}
// AdminDashboardForm form for admin dashboard operations
type AdminDashboardForm struct {
Op int `binding:"required"`
}
// Validate validates form fields
func (f *AdminDashboardForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, f, ctx.Locale)
}
+6 -4
View File
@@ -13,7 +13,7 @@ import (
)
// Auth pam auth service
func Auth(serviceName, userName, passwd string) error {
func Auth(serviceName, userName, passwd string) (string, error) {
t, err := pam.StartFunc(serviceName, userName, func(s pam.Style, msg string) (string, error) {
switch s {
case pam.PromptEchoOff:
@@ -25,12 +25,14 @@ func Auth(serviceName, userName, passwd string) error {
})
if err != nil {
return err
return "", err
}
if err = t.Authenticate(0); err != nil {
return err
return "", err
}
return nil
// PAM login names might suffer transformations in the PAM stack.
// We should take whatever the PAM stack returns for it.
return t.GetItem(pam.User)
}
+2 -2
View File
@@ -11,6 +11,6 @@ import (
)
// Auth not supported lack of pam tag
func Auth(serviceName, userName, passwd string) error {
return errors.New("PAM not supported")
func Auth(serviceName, userName, passwd string) (string, error) {
return "", errors.New("PAM not supported")
}
+1 -1
View File
@@ -318,7 +318,7 @@ func (repo *Repository) CommitsBetween(last *Commit, before *Commit) (*list.List
var stdout []byte
var err error
if before == nil {
stdout, err = NewCommand("rev-list", before.ID.String()).RunInDirBytes(repo.Path)
stdout, err = NewCommand("rev-list", last.ID.String()).RunInDirBytes(repo.Path)
} else {
stdout, err = NewCommand("rev-list", before.ID.String()+"..."+last.ID.String()).RunInDirBytes(repo.Path)
}
+15 -17
View File
@@ -143,25 +143,23 @@ func InitIssueIndexer(syncReindex bool) {
var populate bool
switch setting.Indexer.IssueType {
case "bleve":
graceful.GetManager().RunWithShutdownFns(func(_, atTerminate func(context.Context, func())) {
issueIndexer := NewBleveIndexer(setting.Indexer.IssuePath)
exist, err := issueIndexer.Init()
if err != nil {
holder.cancel()
log.Fatal("Unable to initialize Bleve Issue Indexer: %v", err)
issueIndexer := NewBleveIndexer(setting.Indexer.IssuePath)
exist, err := issueIndexer.Init()
if err != nil {
holder.cancel()
log.Fatal("Unable to initialize Bleve Issue Indexer: %v", err)
}
populate = !exist
holder.set(issueIndexer)
graceful.GetManager().RunAtTerminate(context.Background(), func() {
log.Debug("Closing issue indexer")
issueIndexer := holder.get()
if issueIndexer != nil {
issueIndexer.Close()
}
populate = !exist
holder.set(issueIndexer)
atTerminate(context.Background(), func() {
log.Debug("Closing issue indexer")
issueIndexer := holder.get()
if issueIndexer != nil {
issueIndexer.Close()
}
log.Info("PID: %d Issue Indexer closed", os.Getpid())
})
log.Debug("Created Bleve Indexer")
log.Info("PID: %d Issue Indexer closed", os.Getpid())
})
log.Debug("Created Bleve Indexer")
case "db":
issueIndexer := &DBIndexer{}
holder.set(issueIndexer)
+1 -1
View File
@@ -108,7 +108,7 @@ func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont
}
at := bytes.IndexByte(line, '@')
m = []int{0, stop, at, stop - 1}
if m == nil || bytes.IndexByte(line[m[2]:m[3]], '.') < 0 {
if bytes.IndexByte(line[m[2]:m[3]], '.') < 0 {
return nil
}
lastChar := line[m[1]-1]
-1
View File
@@ -52,7 +52,6 @@ func (g *GiteaASTTransformer) Transform(node *ast.Document, reader text.Reader,
lnk := string(link)
lnk = giteautil.URLJoin(prefix, lnk)
lnk = strings.Replace(lnk, " ", "+", -1)
link = []byte(lnk)
}
v.Destination = link
-1
View File
@@ -81,7 +81,6 @@ func RenderWiki(filename string, rawBytes []byte, urlPrefix string, metas map[st
}
func render(parser Parser, rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte {
urlPrefix = strings.Replace(urlPrefix, " ", "+", -1)
result := parser.Render(rawBytes, urlPrefix, metas, isWiki)
// TODO: one day the error should be returned.
result, err := PostProcess(result, urlPrefix, metas, isWiki)
+2 -2
View File
@@ -689,12 +689,12 @@ func (m *webhookNotifier) NotifyDeleteRef(pusher *models.User, repo *models.Repo
if err := webhook_module.PrepareWebhooks(repo, models.HookEventDelete, &api.DeletePayload{
Ref: refName,
RefType: "branch",
RefType: refType,
PusherType: api.PusherTypeUser,
Repo: apiRepo,
Sender: apiPusher,
}); err != nil {
log.Error("PrepareWebhooks.(delete branch): %v", err)
log.Error("PrepareWebhooks.(delete %s): %v", refType, err)
}
}
+3 -2
View File
@@ -25,12 +25,13 @@ func TestChannelQueue(t *testing.T) {
queue, err := NewChannelQueue(handle,
ChannelQueueConfiguration{
QueueLength: 20,
Workers: 1,
QueueLength: 0,
MaxWorkers: 10,
BlockTimeout: 1 * time.Second,
BoostTimeout: 5 * time.Minute,
BoostWorkers: 5,
Workers: 0,
Name: "TestChannelQueue",
}, &testData{})
assert.NoError(t, err)
+1 -1
View File
@@ -96,8 +96,8 @@ func (p *WorkerPool) pushBoost(data Data) {
p.blockTimeout /= 2
p.lock.Unlock()
}()
p.addWorkers(ctx, boost)
p.lock.Unlock()
p.addWorkers(ctx, boost)
p.dataChan <- data
}
}
+23 -3
View File
@@ -242,10 +242,30 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(author, committer *models
func (t *TemporaryUploadRepository) Push(doer *models.User, commitHash string, branch string) error {
// Because calls hooks we need to pass in the environment
env := models.PushingEnvironment(doer, t.repo)
stdout := &strings.Builder{}
stderr := &strings.Builder{}
if _, err := git.NewCommand("push", t.repo.RepoPath(), strings.TrimSpace(commitHash)+":refs/heads/"+strings.TrimSpace(branch)).RunInDirWithEnv(t.basePath, env); err != nil {
log.Error("Unable to push back to repo from temporary repo: %s (%s) Error: %v",
t.repo.FullName(), t.basePath, err)
if err := git.NewCommand("push", t.repo.RepoPath(), strings.TrimSpace(commitHash)+":refs/heads/"+strings.TrimSpace(branch)).RunInDirTimeoutEnvPipeline(env, -1, t.basePath, stdout, stderr); err != nil {
errString := stderr.String()
if strings.Contains(errString, "non-fast-forward") {
return models.ErrMergePushOutOfDate{
StdOut: stdout.String(),
StdErr: errString,
Err: err,
}
} else if strings.Contains(errString, "! [remote rejected]") {
log.Error("Unable to push back to repo from temporary repo due to rejection: %s (%s)\nStdout: %s\nStderr: %s\nError: %v",
t.repo.FullName(), t.basePath, stdout, errString, err)
err := models.ErrPushRejected{
StdOut: stdout.String(),
StdErr: errString,
Err: err,
}
err.GenerateMessage()
return err
}
log.Error("Unable to push back to repo from temporary repo: %s (%s)\nStdout: %s\nError: %v",
t.repo.FullName(), t.basePath, stdout, err)
return fmt.Errorf("Unable to push back to repo from temporary repo: %s (%s) Error: %v",
t.repo.FullName(), t.basePath, err)
}
+10 -2
View File
@@ -210,6 +210,7 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up
encoding := "UTF-8"
bom := false
executable := false
if !opts.IsNewFile {
fromEntry, err := commit.GetTreeEntryByPath(fromTreePath)
@@ -245,6 +246,7 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up
return nil, models.ErrSHAOrCommitIDNotProvided{}
}
encoding, bom = detectEncodingAndBOM(fromEntry, repo)
executable = fromEntry.IsExecutable()
}
// For the path where this file will be created/updated, we need to make
@@ -368,8 +370,14 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up
}
// Add the object to the index
if err := t.AddObjectToIndex("100644", objectHash, treePath); err != nil {
return nil, err
if executable {
if err := t.AddObjectToIndex("100755", objectHash, treePath); err != nil {
return nil, err
}
} else {
if err := t.AddObjectToIndex("100644", objectHash, treePath); err != nil {
return nil, err
}
}
// Now write the tree
+1 -1
View File
@@ -124,7 +124,7 @@ func DBConnStr() (string, error) {
if err := os.MkdirAll(path.Dir(Database.Path), os.ModePerm); err != nil {
return "", fmt.Errorf("Failed to create directories: %v", err)
}
connStr = fmt.Sprintf("file:%s?cache=shared&mode=rwc&_busy_timeout=%d", Database.Path, Database.Timeout)
connStr = fmt.Sprintf("file:%s?cache=shared&mode=rwc&_busy_timeout=%d&_txlock=immediate", Database.Path, Database.Timeout)
default:
return "", fmt.Errorf("Unknown database type: %s", Database.Type)
}
+26
View File
@@ -374,6 +374,7 @@ user_bio = Biography
form.name_reserved = The username '%s' is reserved.
form.name_pattern_not_allowed = The pattern '%s' is not allowed in a username.
form.name_chars_not_allowed = User name '%s' contains invalid characters.
[settings]
profile = Profile
@@ -434,7 +435,11 @@ manage_openid = Manage OpenID Addresses
email_desc = Your primary email address will be used for notifications and other operations.
theme_desc = This will be your default theme across the site.
primary = Primary
activated = Activated
requires_activation = Requires activation
primary_email = Make Primary
activate_email = Send Activation
activations_pending = Activations Pending
delete_email = Remove
email_deletion = Remove Email Address
email_deletion_desc = The email address and related information will be removed from your account. Git commits by this email address will remain unchanged. Continue?
@@ -775,6 +780,8 @@ editor.commit_empty_file_header = Commit an empty file
editor.commit_empty_file_text = The file you're about commit is empty. Proceed?
editor.no_changes_to_show = There are no changes to show.
editor.fail_to_update_file = Failed to update/create file '%s' with error: %v
editor.push_rejected_no_message = The change was rejected by the server without a message. Please check githooks.
editor.push_rejected = The change was rejected by the server with the following message:<br>%s<br> Please check githooks.
editor.add_subdir = Add a directory…
editor.unable_to_upload_files = Failed to upload files to '%s' with error: %v
editor.upload_file_is_locked = File '%s' is locked by %s.
@@ -794,6 +801,8 @@ commits.date = Date
commits.older = Older
commits.newer = Newer
commits.signed_by = Signed by
commits.signed_by_untrusted_user = Signed by untrusted user
commits.signed_by_untrusted_user_unmatched = Signed by untrusted user who does not match committer
commits.gpg_key_id = GPG Key ID
ext_issues = Ext. Issues
@@ -1074,6 +1083,8 @@ pulls.merge_conflict = Merge Failed: There was a conflict whilst merging: %[1]s<
pulls.rebase_conflict = Merge Failed: There was a conflict whilst rebasing commit: %[1]s<br>%[2]s<br>%[3]s<br>Hint:Try a different strategy
pulls.unrelated_histories = Merge Failed: The merge head and base do not share a common history. Hint: Try a different strategy
pulls.merge_out_of_date = Merge Failed: Whilst generating the merge, the base was updated. Hint: Try again.
pulls.push_rejected = Merge Failed: The push was rejected with the following message:<br>%s<br>Review the githooks for this repository
pulls.push_rejected_no_message = Merge Failed: The push was rejected but there was no remote message.<br>Review the githooks for this repository
pulls.open_unmerged_pull_exists = `You cannot perform a reopen operation because there is a pending pull request (#%d) with identical properties.`
pulls.status_checking = Some checks are pending
pulls.status_checks_success = All checks were successful
@@ -1681,6 +1692,7 @@ organizations = Organizations
repositories = Repositories
hooks = Default Webhooks
authentication = Authentication Sources
emails = User Emails
config = Configuration
notices = System Notices
monitor = Monitoring
@@ -1750,6 +1762,7 @@ dashboard.gc_times = GC Times
users.user_manage_panel = User Account Management
users.new_account = Create User Account
users.name = Username
users.full_name = Full Name
users.activated = Activated
users.admin = Admin
users.restricted = Restricted
@@ -1781,6 +1794,19 @@ users.still_own_repo = This user still owns one or more repositories. Delete or
users.still_has_org = This user is a member of an organization. Remove the user from any organizations first.
users.deletion_success = The user account has been deleted.
emails.email_manage_panel = User Email Management
emails.primary = Primary
emails.activated = Activated
emails.filter_sort.email = Email
emails.filter_sort.email_reverse = Email (reverse)
emails.filter_sort.name = User Name
emails.filter_sort.name_reverse = User Name (reverse)
emails.updated = Email updated
emails.not_updated = Failed to update the requested email address: %v
emails.duplicate_active = This email address is already active for a different user.
emails.change_email_header = Update Email Properties
emails.change_email_text = Are your sure you want to update this email address?
orgs.org_manage_panel = Organization Management
orgs.name = Name
orgs.teams = Teams
+19 -11
View File
@@ -16,6 +16,7 @@ import (
"time"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/cron"
@@ -30,7 +31,6 @@ import (
"gitea.com/macaron/macaron"
"gitea.com/macaron/session"
"github.com/unknwon/com"
)
const (
@@ -144,14 +144,28 @@ func Dashboard(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.dashboard")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminDashboard"] = true
ctx.Data["Stats"] = models.GetStatistic()
// FIXME: update periodically
updateSystemStatus()
ctx.Data["SysStatus"] = sysStatus
ctx.HTML(200, tplDashboard)
}
// DashboardPost run an admin operation
func DashboardPost(ctx *context.Context, form auth.AdminDashboardForm) {
ctx.Data["Title"] = ctx.Tr("admin.dashboard")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminDashboard"] = true
ctx.Data["Stats"] = models.GetStatistic()
updateSystemStatus()
ctx.Data["SysStatus"] = sysStatus
// Run operation.
op, _ := com.StrTo(ctx.Query("op")).Int()
if op > 0 {
if form.Op > 0 {
var err error
var success string
switch Operation(op) {
switch Operation(form.Op) {
case cleanInactivateUser:
success = ctx.Tr("admin.dashboard.delete_inactivate_accounts_success")
err = models.DeleteInactivateUsers()
@@ -189,15 +203,9 @@ func Dashboard(ctx *context.Context) {
} else {
ctx.Flash.Success(success)
}
ctx.Redirect(setting.AppSubURL + "/admin")
return
}
ctx.Data["Stats"] = models.GetStatistic()
// FIXME: update periodically
updateSystemStatus()
ctx.Data["SysStatus"] = sysStatus
ctx.HTML(200, tplDashboard)
ctx.Redirect(setting.AppSubURL + "/admin")
}
// SendTestMail send test mail to confirm mail service is OK
+155
View File
@@ -0,0 +1,155 @@
// Copyright 2020 The Gitea Authors.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package admin
import (
"bytes"
"net/url"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/unknwon/com"
)
const (
tplEmails base.TplName = "admin/emails/list"
)
// Emails show all emails
func Emails(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.emails")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminEmails"] = true
opts := &models.SearchEmailOptions{
PageSize: setting.UI.Admin.UserPagingNum,
Page: ctx.QueryInt("page"),
}
if opts.Page <= 1 {
opts.Page = 1
}
type ActiveEmail struct {
models.SearchEmailResult
CanChange bool
}
var (
baseEmails []*models.SearchEmailResult
emails []ActiveEmail
count int64
err error
orderBy models.SearchEmailOrderBy
)
ctx.Data["SortType"] = ctx.Query("sort")
switch ctx.Query("sort") {
case "email":
orderBy = models.SearchEmailOrderByEmail
case "reverseemail":
orderBy = models.SearchEmailOrderByEmailReverse
case "username":
orderBy = models.SearchEmailOrderByName
case "reverseusername":
orderBy = models.SearchEmailOrderByNameReverse
default:
ctx.Data["SortType"] = "email"
orderBy = models.SearchEmailOrderByEmail
}
opts.Keyword = ctx.QueryTrim("q")
opts.SortType = orderBy
if len(ctx.Query("is_activated")) != 0 {
opts.IsActivated = util.OptionalBoolOf(ctx.QueryBool("activated"))
}
if len(ctx.Query("is_primary")) != 0 {
opts.IsPrimary = util.OptionalBoolOf(ctx.QueryBool("primary"))
}
if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
baseEmails, count, err = models.SearchEmails(opts)
if err != nil {
ctx.ServerError("SearchEmails", err)
return
}
emails = make([]ActiveEmail, len(baseEmails))
for i := range baseEmails {
emails[i].SearchEmailResult = *baseEmails[i]
// Don't let the admin deactivate its own primary email address
// We already know the user is admin
emails[i].CanChange = ctx.User.ID != emails[i].UID || !emails[i].IsPrimary
}
}
ctx.Data["Keyword"] = opts.Keyword
ctx.Data["Total"] = count
ctx.Data["Emails"] = emails
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager
ctx.HTML(200, tplEmails)
}
var (
nullByte = []byte{0x00}
)
func isKeywordValid(keyword string) bool {
return !bytes.Contains([]byte(keyword), nullByte)
}
// ActivateEmail serves a POST request for activating/deactivating a user's email
func ActivateEmail(ctx *context.Context) {
truefalse := map[string]bool{"1": true, "0": false}
uid := com.StrTo(ctx.Query("uid")).MustInt64()
email := ctx.Query("email")
primary, okp := truefalse[ctx.Query("primary")]
activate, oka := truefalse[ctx.Query("activate")]
if uid == 0 || len(email) == 0 || !okp || !oka {
ctx.Error(400)
return
}
log.Info("Changing activation for User ID: %d, email: %s, primary: %v to %v", uid, email, primary, activate)
if err := models.ActivateUserEmail(uid, email, primary, activate); err != nil {
log.Error("ActivateUserEmail(%v,%v,%v,%v): %v", uid, email, primary, activate, err)
if models.IsErrEmailAlreadyUsed(err) {
ctx.Flash.Error(ctx.Tr("admin.emails.duplicate_active"))
} else {
ctx.Flash.Error(ctx.Tr("admin.emails.not_updated", err))
}
} else {
log.Info("Activation for User ID: %d, email: %s, primary: %v changed to %v", uid, email, primary, activate)
ctx.Flash.Info(ctx.Tr("admin.emails.updated"))
}
redirect, _ := url.Parse(setting.AppSubURL + "/admin/emails")
q := url.Values{}
if val := ctx.QueryTrim("q"); len(val) > 0 {
q.Set("q", val)
}
if val := ctx.QueryTrim("sort"); len(val) > 0 {
q.Set("sort", val)
}
if val := ctx.QueryTrim("is_primary"); len(val) > 0 {
q.Set("is_primary", val)
}
if val := ctx.QueryTrim("is_activated"); len(val) > 0 {
q.Set("is_activated", val)
}
redirect.RawQuery = q.Encode()
ctx.Redirect(redirect.String())
}
+3
View File
@@ -121,6 +121,9 @@ func NewUserPost(ctx *context.Context, form auth.AdminCreateUserForm) {
case models.IsErrNamePatternNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplUserNew, &form)
case models.IsErrNameCharsNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(models.ErrNameCharsNotAllowed).Name), tplUserNew, &form)
default:
ctx.ServerError("CreateUser", err)
}
+1
View File
@@ -66,6 +66,7 @@ func CreateOrg(ctx *context.APIContext, form api.CreateOrgOption) {
if err := models.CreateOrganization(org, u); err != nil {
if models.IsErrUserAlreadyExist(err) ||
models.IsErrNameReserved(err) ||
models.IsErrNameCharsNotAllowed(err) ||
models.IsErrNamePatternNotAllowed(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
} else {
+1
View File
@@ -90,6 +90,7 @@ func CreateUser(ctx *context.APIContext, form api.CreateUserOption) {
if models.IsErrUserAlreadyExist(err) ||
models.IsErrEmailAlreadyUsed(err) ||
models.IsErrNameReserved(err) ||
models.IsErrNameCharsNotAllowed(err) ||
models.IsErrNamePatternNotAllowed(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
} else {
+1
View File
@@ -112,6 +112,7 @@ func Create(ctx *context.APIContext, form api.CreateOrgOption) {
if err := models.CreateOrganization(org, ctx.User); err != nil {
if models.IsErrUserAlreadyExist(err) ||
models.IsErrNameReserved(err) ||
models.IsErrNameCharsNotAllowed(err) ||
models.IsErrNamePatternNotAllowed(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
} else {
+29 -36
View File
@@ -102,24 +102,8 @@ func AddIssueLabels(ctx *context.APIContext, form api.IssueLabelsOption) {
// "403":
// "$ref": "#/responses/forbidden"
issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
issue, labels, err := prepareForReplaceOrAdd(ctx, form)
if err != nil {
if models.IsErrIssueNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
}
return
}
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.Status(http.StatusForbidden)
return
}
labels, err := models.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDs", err)
return
}
@@ -204,7 +188,7 @@ func DeleteIssueLabel(ctx *context.APIContext) {
return
}
if err := models.DeleteIssueLabel(issue, label, ctx.User); err != nil {
if err := issue_service.RemoveLabel(issue, ctx.User, label); err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteIssueLabel", err)
return
}
@@ -248,28 +232,12 @@ func ReplaceIssueLabels(ctx *context.APIContext, form api.IssueLabelsOption) {
// "403":
// "$ref": "#/responses/forbidden"
issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
issue, labels, err := prepareForReplaceOrAdd(ctx, form)
if err != nil {
if models.IsErrIssueNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
}
return
}
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.Status(http.StatusForbidden)
return
}
labels, err := models.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDs", err)
return
}
if err := issue.ReplaceLabels(labels, ctx.User); err != nil {
if err := issue_service.ReplaceLabels(issue, ctx.User, labels); err != nil {
ctx.Error(http.StatusInternalServerError, "ReplaceLabels", err)
return
}
@@ -339,3 +307,28 @@ func ClearIssueLabels(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent)
}
func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) (issue *models.Issue, labels []*models.Label, err error) {
issue, err = models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
if models.IsErrIssueNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
}
return
}
labels, err = models.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDs", err)
return
}
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.Status(http.StatusForbidden)
return
}
return
}
+5 -2
View File
@@ -5,6 +5,7 @@
package repo
import (
"errors"
"net/http"
"code.gitea.io/gitea/models"
@@ -172,19 +173,21 @@ func prepareIssueStopwatch(ctx *context.APIContext, shouldExist bool) (*models.I
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.Status(http.StatusForbidden)
return nil, err
return nil, errors.New("Unable to write to PRs")
}
if !ctx.Repo.CanUseTimetracker(issue, ctx.User) {
ctx.Status(http.StatusForbidden)
return nil, err
return nil, errors.New("Cannot use time tracker")
}
if models.StopwatchExists(ctx.User.ID, issue.ID) != shouldExist {
if shouldExist {
ctx.Error(http.StatusConflict, "StopwatchExists", "cannot stop/cancel a non existent stopwatch")
err = errors.New("cannot stop/cancel a non existent stopwatch")
} else {
ctx.Error(http.StatusConflict, "StopwatchExists", "cannot start a stopwatch again if it already exists")
err = errors.New("cannot start a stopwatch again if it already exists")
}
return nil, err
}
+8 -3
View File
@@ -15,7 +15,6 @@ import (
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
issue_service "code.gitea.io/gitea/services/issue"
@@ -316,8 +315,6 @@ func CreatePullRequest(ctx *context.APIContext, form api.CreatePullRequestOption
return
}
notification.NotifyNewPullRequest(pr)
log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID)
ctx.JSON(http.StatusCreated, pr.APIFormat())
}
@@ -648,6 +645,14 @@ func MergePullRequest(ctx *context.APIContext, form auth.MergePullRequestForm) {
} else if models.IsErrMergePushOutOfDate(err) {
ctx.Error(http.StatusConflict, "Merge", "merge push out of date")
return
} else if models.IsErrPushRejected(err) {
errPushRej := err.(models.ErrPushRejected)
if len(errPushRej.Message) == 0 {
ctx.Error(http.StatusConflict, "Merge", "PushRejected without remote error message")
return
}
ctx.Error(http.StatusConflict, "Merge", "PushRejected with remote message: "+errPushRej.Message)
return
}
ctx.Error(http.StatusInternalServerError, "Merge", err)
return
+2
View File
@@ -511,6 +511,8 @@ func handleMigrateError(ctx *context.APIContext, repoOwner *models.User, remoteA
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("You have already reached your limit of %d repositories.", repoOwner.MaxCreationLimit()))
case models.IsErrNameReserved(err):
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The username '%s' is reserved.", err.(models.ErrNameReserved).Name))
case models.IsErrNameCharsNotAllowed(err):
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The username '%s' contains invalid characters.", err.(models.ErrNameCharsNotAllowed).Name))
case models.IsErrNamePatternNotAllowed(err):
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The pattern '%s' is not allowed in a username.", err.(models.ErrNamePatternNotAllowed).Pattern))
default:
+4 -4
View File
@@ -70,14 +70,14 @@ func UploadAttachment(ctx *context.Context) {
func DeleteAttachment(ctx *context.Context) {
file := ctx.Query("file")
attach, err := models.GetAttachmentByUUID(file)
if !ctx.IsSigned || (ctx.User.ID != attach.UploaderID) {
ctx.Error(403)
return
}
if err != nil {
ctx.Error(400, err.Error())
return
}
if !ctx.IsSigned || (ctx.User.ID != attach.UploaderID) {
ctx.Error(403)
return
}
err = models.DeleteAttachment(attach, true)
if err != nil {
ctx.Error(500, fmt.Sprintf("DeleteAttachment: %v", err))
+11 -5
View File
@@ -65,7 +65,7 @@ func Commits(ctx *context.Context) {
return
}
commits = models.ValidateCommitsWithEmails(commits)
commits = models.ParseCommitsWithSignature(commits)
commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
ctx.Data["Commits"] = commits
@@ -134,7 +134,7 @@ func SearchCommits(ctx *context.Context) {
return
}
commits = models.ValidateCommitsWithEmails(commits)
commits = models.ParseCommitsWithSignature(commits)
commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
ctx.Data["Commits"] = commits
@@ -180,7 +180,7 @@ func FileHistory(ctx *context.Context) {
return
}
commits = models.ValidateCommitsWithEmails(commits)
commits = models.ParseCommitsWithSignature(commits)
commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
ctx.Data["Commits"] = commits
@@ -237,11 +237,11 @@ func Diff(ctx *context.Context) {
parents := make([]string, commit.ParentCount())
for i := 0; i < commit.ParentCount(); i++ {
sha, err := commit.ParentID(i)
parents[i] = sha.String()
if err != nil {
ctx.NotFound("repo.Diff", err)
return
}
parents[i] = sha.String()
}
ctx.Data["CommitID"] = commitID
@@ -262,12 +262,18 @@ func Diff(ctx *context.Context) {
setPathsCompareContext(ctx, parentCommit, commit, headTarget)
ctx.Data["Title"] = commit.Summary() + " · " + base.ShortSha(commitID)
ctx.Data["Commit"] = commit
ctx.Data["Verification"] = models.ParseCommitWithSignature(commit)
verification := models.ParseCommitWithSignature(commit)
ctx.Data["Verification"] = verification
ctx.Data["Author"] = models.ValidateCommitWithEmail(commit)
ctx.Data["Diff"] = diff
ctx.Data["Parents"] = parents
ctx.Data["DiffNotAvailable"] = diff.NumFiles() == 0
if err := models.CalculateTrustStatus(verification, ctx.Repo.Repository, nil); err != nil {
ctx.ServerError("CalculateTrustStatus", err)
return
}
note := &git.Note{}
err = git.GetNote(ctx.Repo.GitRepo, commitID, note)
if err == nil {
+1 -1
View File
@@ -316,7 +316,7 @@ func PrepareCompareDiff(
}
compareInfo.Commits = models.ValidateCommitsWithEmails(compareInfo.Commits)
compareInfo.Commits = models.ParseCommitsWithSignature(compareInfo.Commits)
compareInfo.Commits = models.ParseCommitsWithSignature(compareInfo.Commits, headRepo)
compareInfo.Commits = models.ParseCommitsWithStatus(compareInfo.Commits, headRepo)
ctx.Data["Commits"] = compareInfo.Commits
ctx.Data["CommitCount"] = compareInfo.Commits.Len()
+18 -3
View File
@@ -22,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/utils"
)
const (
@@ -262,10 +263,17 @@ func editFilePost(ctx *context.Context, form auth.EditRepoFileForm, isNewFile bo
} else {
ctx.Error(500, err.Error())
}
} else if models.IsErrCommitIDDoesNotMatch(err) {
} else if models.IsErrCommitIDDoesNotMatch(err) || models.IsErrMergePushOutOfDate(err) {
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplEditFile, &form)
} else if models.IsErrPushRejected(err) {
errPushRej := err.(models.ErrPushRejected)
if len(errPushRej.Message) == 0 {
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form)
} else {
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected", utils.SanitizeFlashErrorString(errPushRej.Message)), tplEditFile, &form)
}
} else {
ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_update_file", form.TreePath, err), tplEditFile, &form)
ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_update_file", form.TreePath, utils.SanitizeFlashErrorString(err.Error())), tplEditFile, &form)
}
}
@@ -426,8 +434,15 @@ func DeleteFilePost(ctx *context.Context, form auth.DeleteRepoFileForm) {
} else {
ctx.Error(500, err.Error())
}
} else if models.IsErrCommitIDDoesNotMatch(err) {
} else if models.IsErrCommitIDDoesNotMatch(err) || models.IsErrMergePushOutOfDate(err) {
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_deleting", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplDeleteFile, &form)
} else if models.IsErrPushRejected(err) {
errPushRej := err.(models.ErrPushRejected)
if len(errPushRej.Message) == 0 {
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form)
} else {
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected", utils.SanitizeFlashErrorString(errPushRej.Message)), tplDeleteFile, &form)
}
} else {
ctx.ServerError("DeleteRepoFile", err)
}
+27 -13
View File
@@ -10,7 +10,6 @@ import (
"container/list"
"crypto/subtle"
"fmt"
"html"
"net/http"
"path"
"strings"
@@ -25,6 +24,7 @@ import (
"code.gitea.io/gitea/modules/repofiles"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/utils"
"code.gitea.io/gitea/services/gitdiff"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
@@ -354,6 +354,16 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare
return nil
}
defer baseGitRepo.Close()
if !baseGitRepo.IsBranchExist(pull.BaseBranch) {
ctx.Data["IsPullRequestBroken"] = true
ctx.Data["BaseTarget"] = pull.BaseBranch
ctx.Data["HeadTarget"] = pull.HeadBranch
ctx.Data["NumCommits"] = 0
ctx.Data["NumFiles"] = 0
return nil
}
var headBranchExist bool
var headBranchSha string
// HeadRepo may be missing
@@ -477,7 +487,7 @@ func ViewPullCommits(ctx *context.Context) {
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
commits = prInfo.Commits
commits = models.ValidateCommitsWithEmails(commits)
commits = models.ParseCommitsWithSignature(commits)
commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
ctx.Data["Commits"] = commits
ctx.Data["CommitCount"] = commits.Len()
@@ -586,6 +596,8 @@ func ViewPullFiles(ctx *context.Context) {
return
}
getBranchData(ctx, issue)
ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.User.ID)
ctx.Data["IsIssueWriter"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
ctx.HTML(200, tplPullFiles)
}
@@ -663,27 +675,18 @@ func MergePullRequest(ctx *context.Context, form auth.MergePullRequestForm) {
}
if err = pull_service.Merge(pr, ctx.User, ctx.Repo.GitRepo, models.MergeStyle(form.Do), message); err != nil {
sanitize := func(x string) string {
runes := []rune(x)
if len(runes) > 512 {
x = "..." + string(runes[len(runes)-512:])
}
return strings.Replace(html.EscapeString(x), "\n", "<br>", -1)
}
if models.IsErrInvalidMergeStyle(err) {
ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option"))
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
return
} else if models.IsErrMergeConflicts(err) {
conflictError := err.(models.ErrMergeConflicts)
ctx.Flash.Error(ctx.Tr("repo.pulls.merge_conflict", sanitize(conflictError.StdErr), sanitize(conflictError.StdOut)))
ctx.Flash.Error(ctx.Tr("repo.pulls.merge_conflict", utils.SanitizeFlashErrorString(conflictError.StdErr), utils.SanitizeFlashErrorString(conflictError.StdOut)))
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
return
} else if models.IsErrRebaseConflicts(err) {
conflictError := err.(models.ErrRebaseConflicts)
ctx.Flash.Error(ctx.Tr("repo.pulls.rebase_conflict", sanitize(conflictError.CommitSHA), sanitize(conflictError.StdErr), sanitize(conflictError.StdOut)))
ctx.Flash.Error(ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA), utils.SanitizeFlashErrorString(conflictError.StdErr), utils.SanitizeFlashErrorString(conflictError.StdOut)))
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
return
} else if models.IsErrMergeUnrelatedHistories(err) {
@@ -696,6 +699,17 @@ func MergePullRequest(ctx *context.Context, form auth.MergePullRequestForm) {
ctx.Flash.Error(ctx.Tr("repo.pulls.merge_out_of_date"))
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
return
} else if models.IsErrPushRejected(err) {
log.Debug("MergePushRejected error: %v", err)
pushrejErr := err.(models.ErrPushRejected)
message := pushrejErr.Message
if len(message) == 0 {
ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected_no_message"))
} else {
ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected", utils.SanitizeFlashErrorString(pushrejErr.Message)))
}
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
return
}
ctx.ServerError("Merge", err)
return
+8 -1
View File
@@ -181,7 +181,14 @@ func renderDirectory(ctx *context.Context, treeLink string) {
// Show latest commit info of repository in table header,
// or of directory if not in root directory.
ctx.Data["LatestCommit"] = latestCommit
ctx.Data["LatestCommitVerification"] = models.ParseCommitWithSignature(latestCommit)
verification := models.ParseCommitWithSignature(latestCommit)
if err := models.CalculateTrustStatus(verification, ctx.Repo.Repository, nil); err != nil {
ctx.ServerError("CalculateTrustStatus", err)
return
}
ctx.Data["LatestCommitVerification"] = verification
ctx.Data["LatestCommitUser"] = models.ValidateCommitWithEmail(latestCommit)
statuses, err := models.GetLatestCommitStatus(ctx.Repo.Repository, ctx.Repo.Commit.ID.String(), 0)
+2 -2
View File
@@ -448,7 +448,7 @@ func SlackHooksNewPost(ctx *context.Context, form auth.NewSlackHookForm) {
if form.HasInvalidChannel() {
ctx.Flash.Error(ctx.Tr("repo.settings.add_webhook.invalid_channel_name"))
ctx.Redirect(orCtx.Link + "/settings/hooks/slack/new")
ctx.Redirect(orCtx.Link + "/slack/new")
return
}
@@ -642,7 +642,7 @@ func SlackHooksEditPost(ctx *context.Context, form auth.NewSlackHookForm) {
if form.HasInvalidChannel() {
ctx.Flash.Error(ctx.Tr("repo.settings.add_webhook.invalid_channel_name"))
ctx.Redirect(fmt.Sprintf("%s/settings/hooks/%d", orCtx.Link, w.ID))
ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
return
}
+1 -1
View File
@@ -284,7 +284,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
return nil, nil
}
commitsHistory = models.ValidateCommitsWithEmails(commitsHistory)
commitsHistory = models.ParseCommitsWithSignature(commitsHistory)
commitsHistory = models.ParseCommitsWithSignature(commitsHistory, ctx.Repo.Repository)
ctx.Data["Commits"] = commitsHistory
+14 -8
View File
@@ -399,7 +399,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Post("/recover_account", user.ResetPasswdPost)
m.Get("/forgot_password", user.ForgotPasswd)
m.Post("/forgot_password", user.ForgotPasswdPost)
m.Get("/logout", user.SignOut)
m.Post("/logout", user.SignOut)
})
// ***** END: User *****
@@ -408,6 +408,7 @@ func RegisterRoutes(m *macaron.Macaron) {
// ***** START: Admin *****
m.Group("/admin", func() {
m.Get("", adminReq, admin.Dashboard)
m.Post("", adminReq, bindIgnErr(auth.AdminDashboardForm{}), admin.DashboardPost)
m.Get("/config", admin.Config)
m.Post("/config/test_mail", admin.SendTestMail)
m.Group("/monitor", func() {
@@ -428,6 +429,11 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Post("/:userid/delete", admin.DeleteUser)
})
m.Group("/emails", func() {
m.Get("", admin.Emails)
m.Post("/activate", admin.ActivateEmail)
})
m.Group("/orgs", func() {
m.Get("", admin.Organizations)
})
@@ -469,7 +475,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Group("/notices", func() {
m.Get("", admin.Notices)
m.Post("/delete", admin.DeleteNotices)
m.Get("/empty", admin.EmptyNotices)
m.Post("/empty", admin.EmptyNotices)
})
}, adminReq)
// ***** END: Admin *****
@@ -485,7 +491,7 @@ func RegisterRoutes(m *macaron.Macaron) {
}, reqSignIn)
m.Group("/:username", func() {
m.Get("/action/:action", user.Action)
m.Post("/action/:action", user.Action)
}, reqSignIn)
if macaron.Env == macaron.DEV {
@@ -517,7 +523,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get("/^:type(issues|pulls)$", user.Issues)
m.Get("/milestones", reqMilestonesDashboardPageEnabled, user.Milestones)
m.Get("/members", org.Members)
m.Get("/members/action/:action", org.MembersAction)
m.Post("/members/action/:action", org.MembersAction)
m.Get("/teams", org.Teams)
}, context.OrgAssignment(true))
@@ -525,8 +531,8 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Group("/:org", func() {
m.Get("/teams/:team", org.TeamMembers)
m.Get("/teams/:team/repositories", org.TeamRepositories)
m.Route("/teams/:team/action/:action", "GET,POST", org.TeamsAction)
m.Route("/teams/:team/action/repo/:action", "GET,POST", org.TeamsRepoAction)
m.Post("/teams/:team/action/:action", org.TeamsAction)
m.Post("/teams/:team/action/repo/:action", org.TeamsRepoAction)
}, context.OrgAssignment(true, false, true))
m.Group("/:org", func() {
@@ -660,7 +666,7 @@ func RegisterRoutes(m *macaron.Macaron) {
})
}, reqSignIn, context.RepoAssignment(), context.UnitTypes(), reqRepoAdmin, context.RepoRef())
m.Get("/:username/:reponame/action/:action", reqSignIn, context.RepoAssignment(), context.UnitTypes(), repo.Action)
m.Post("/:username/:reponame/action/:action", reqSignIn, context.RepoAssignment(), context.UnitTypes(), repo.Action)
m.Group("/:username/:reponame", func() {
m.Group("/issues", func() {
@@ -714,7 +720,7 @@ func RegisterRoutes(m *macaron.Macaron) {
Post(bindIgnErr(auth.CreateMilestoneForm{}), repo.NewMilestonePost)
m.Get("/:id/edit", repo.EditMilestone)
m.Post("/:id/edit", bindIgnErr(auth.CreateMilestoneForm{}), repo.EditMilestonePost)
m.Get("/:id/:action", repo.ChangeMilestonStatus)
m.Post("/:id/:action", repo.ChangeMilestonStatus)
m.Post("/delete", repo.DeleteMilestone)
}, context.RepoMustNotBeArchived(), reqRepoIssuesOrPullsWriter, context.RepoRef())
m.Group("/milestone", func() {
+15 -1
View File
@@ -928,6 +928,7 @@ func LinkAccountPostRegister(ctx *context.Context, cpt *captcha.Captcha, form au
LoginName: gothUser.(goth.User).UserID,
}
//nolint: dupl
if err := models.CreateUser(u); err != nil {
switch {
case models.IsErrUserAlreadyExist(err):
@@ -942,6 +943,9 @@ func LinkAccountPostRegister(ctx *context.Context, cpt *captcha.Captcha, form au
case models.IsErrNamePatternNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplLinkAccount, &form)
case models.IsErrNameCharsNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(models.ErrNameCharsNotAllowed).Name), tplLinkAccount, &form)
default:
ctx.ServerError("CreateUser", err)
}
@@ -1213,9 +1217,19 @@ func ActivateEmail(ctx *context.Context) {
log.Trace("Email activated: %s", email.Email)
ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
if u, err := models.GetUserByID(email.UID); err != nil {
log.Warn("GetUserByID: %d", email.UID)
} else {
// Allow user to validate more emails
_ = ctx.Cache.Delete("MailResendLimit_" + u.LowerName)
}
}
ctx.Redirect(setting.AppSubURL + "/user/settings/email")
// FIXME: e-mail verification does not require the user to be logged in,
// so this could be redirecting to the login page.
// Should users be logged in automatically here? (consider 2FA requirements, etc.)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
}
// ForgotPasswd render the forget pasword page
+4
View File
@@ -400,6 +400,7 @@ func RegisterOpenIDPost(ctx *context.Context, cpt *captcha.Captcha, form auth.Si
Passwd: password,
IsActive: !setting.Service.RegisterEmailConfirm,
}
//nolint: dupl
if err := models.CreateUser(u); err != nil {
switch {
case models.IsErrUserAlreadyExist(err):
@@ -414,6 +415,9 @@ func RegisterOpenIDPost(ctx *context.Context, cpt *captcha.Captcha, form auth.Si
case models.IsErrNamePatternNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplSignUpOID, &form)
case models.IsErrNameCharsNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(models.ErrNameCharsNotAllowed).Name), tplSignUpOID, &form)
default:
ctx.ServerError("CreateUser", err)
}
+60 -2
View File
@@ -88,6 +88,51 @@ func EmailPost(ctx *context.Context, form auth.AddEmailForm) {
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
// Send activation Email
if ctx.Query("_method") == "SENDACTIVATION" {
var address string
if ctx.Cache.IsExist("MailResendLimit_" + ctx.User.LowerName) {
log.Error("Send activation: activation still pending")
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
if ctx.Query("id") == "PRIMARY" {
if ctx.User.IsActive {
log.Error("Send activation: email not set for activation")
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
mailer.SendActivateAccountMail(ctx.Locale, ctx.User)
address = ctx.User.Email
} else {
id := ctx.QueryInt64("id")
email, err := models.GetEmailAddressByID(ctx.User.ID, id)
if err != nil {
log.Error("GetEmailAddressByID(%d,%d) error: %v", ctx.User.ID, id, err)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
if email == nil {
log.Error("Send activation: EmailAddress not found; user:%d, id: %d", ctx.User.ID, id)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
if email.IsActivated {
log.Error("Send activation: email not set for activation")
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
mailer.SendActivateEmailMail(ctx.Locale, ctx.User, email)
address = email.Email
}
if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err)
}
ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", address, timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())))
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
// Set Email Notification Preference
if ctx.Query("_method") == "NOTIFICATION" {
preference := ctx.Query("preference")
@@ -134,7 +179,6 @@ func EmailPost(ctx *context.Context, form auth.AddEmailForm) {
// Send confirmation email
if setting.Service.RegisterEmailConfirm {
mailer.SendActivateEmailMail(ctx.Locale, ctx.User, email)
if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err)
}
@@ -223,11 +267,25 @@ func UpdateUIThemePost(ctx *context.Context, form auth.UpdateThemeForm) {
}
func loadAccountData(ctx *context.Context) {
emails, err := models.GetEmailAddresses(ctx.User.ID)
emlist, err := models.GetEmailAddresses(ctx.User.ID)
if err != nil {
ctx.ServerError("GetEmailAddresses", err)
return
}
type UserEmail struct {
models.EmailAddress
CanBePrimary bool
}
pendingActivation := ctx.Cache.IsExist("MailResendLimit_" + ctx.User.LowerName)
emails := make([]*UserEmail, len(emlist))
for i, em := range emlist {
var email UserEmail
email.EmailAddress = *em
email.CanBePrimary = em.IsActivated
emails[i] = &email
}
ctx.Data["Emails"] = emails
ctx.Data["EmailNotificationsPreference"] = ctx.User.EmailNotifications()
ctx.Data["ActivationsPending"] = pendingActivation
ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm
}
+3
View File
@@ -58,6 +58,9 @@ func handleUsernameChange(ctx *context.Context, newName string) {
case models.IsErrNamePatternNotAllowed(err):
ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName))
ctx.Redirect(setting.AppSubURL + "/user/settings")
case models.IsErrNameCharsNotAllowed(err):
ctx.Flash.Error(ctx.Tr("user.form.name_chars_not_allowed", newName))
ctx.Redirect(setting.AppSubURL + "/user/settings")
default:
ctx.ServerError("ChangeUserName", err)
}
+12
View File
@@ -5,6 +5,7 @@
package utils
import (
"html"
"strings"
)
@@ -34,3 +35,14 @@ func IsValidSlackChannel(channelName string) bool {
return true
}
// SanitizeFlashErrorString will sanitize a flash error string
func SanitizeFlashErrorString(x string) string {
runes := []rune(x)
if len(runes) > 512 {
x = "..." + string(runes[len(runes)-512:])
}
return strings.Replace(html.EscapeString(x), "\n", "<br>", -1)
}
+15
View File
@@ -61,3 +61,18 @@ func RemoveLabel(issue *models.Issue, doer *models.User, label *models.Label) er
notification.NotifyIssueChangeLabels(doer, issue, nil, []*models.Label{label})
return nil
}
// ReplaceLabels removes all current labels and add new labels to the issue.
func ReplaceLabels(issue *models.Issue, doer *models.User, labels []*models.Label) error {
old, err := models.GetLabelsByIssueID(issue.ID)
if err != nil {
return err
}
if err := issue.ReplaceLabels(labels, doer); err != nil {
return err
}
notification.NotifyIssueChangeLabels(doer, issue, labels, old)
return nil
}
+9 -1
View File
@@ -62,7 +62,7 @@ func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []int64) e
unfiltered = append(unfiltered, ids...)
// =========== Issue watchers ===========
ids, err = models.GetIssueWatchersIDs(ctx.Issue.ID)
ids, err = models.GetIssueWatchersIDs(ctx.Issue.ID, true)
if err != nil {
return fmt.Errorf("GetIssueWatchersIDs(%d): %v", ctx.Issue.ID, err)
}
@@ -80,6 +80,14 @@ func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []int64) e
// Avoid mailing the doer
visited[ctx.Doer.ID] = true
// Avoid mailing explicit unwatched
ids, err = models.GetIssueWatchersIDs(ctx.Issue.ID, false)
if err != nil {
return fmt.Errorf("GetIssueWatchersIDs(%d): %v", ctx.Issue.ID, err)
}
for _, i := range ids {
visited[i] = true
}
if err = mailIssueCommentBatch(ctx, unfiltered, visited, false); err != nil {
return fmt.Errorf("mailIssueCommentBatch(): %v", err)
+6 -4
View File
@@ -31,7 +31,7 @@ var pullRequestQueue = sync.NewUniqueQueue(setting.Repository.PullRequestQueueLe
func AddToTaskQueue(pr *models.PullRequest) {
go pullRequestQueue.AddFunc(pr.ID, func() {
pr.Status = models.PullRequestStatusChecking
if err := pr.UpdateCols("status"); err != nil {
if err := pr.UpdateColsIfNotMerged("status"); err != nil {
log.Error("AddToTaskQueue.UpdateCols[%d].(add to queue): %v", pr.ID, err)
}
})
@@ -47,7 +47,7 @@ func checkAndUpdateStatus(pr *models.PullRequest) {
// Make sure there is no waiting test to process before leaving the checking status.
if !pullRequestQueue.Exist(pr.ID) {
if err := pr.UpdateCols("status, conflicted_files"); err != nil {
if err := pr.UpdateCols("merge_base", "status", "conflicted_files"); err != nil {
log.Error("Update[%d]: %v", pr.ID, err)
}
}
@@ -142,9 +142,11 @@ func manuallyMerged(pr *models.PullRequest) bool {
pr.Merger = merger
pr.MergerID = merger.ID
if err = pr.SetMerged(); err != nil {
if merged, err := pr.SetMerged(); err != nil {
log.Error("PullRequest[%d].setMerged : %v", pr.ID, err)
return false
} else if !merged {
return false
}
baseGitRepo, err := git.OpenRepository(pr.BaseRepo.RepoPath())
@@ -194,7 +196,7 @@ func TestPullRequests(ctx context.Context) {
if err != nil {
log.Error("GetPullRequestByID[%s]: %v", prID, err)
continue
} else if pr.Status != models.PullRequestStatusChecking {
} else if pr.HasMerged {
continue
} else if manuallyMerged(pr) {
continue
+2 -2
View File
@@ -18,7 +18,7 @@ import (
func TestPullRequest_AddToTaskQueue(t *testing.T) {
assert.NoError(t, models.PrepareTestDatabase())
pr := models.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 1}).(*models.PullRequest)
pr := models.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2}).(*models.PullRequest)
AddToTaskQueue(pr)
select {
@@ -29,6 +29,6 @@ func TestPullRequest_AddToTaskQueue(t *testing.T) {
}
assert.True(t, pullRequestQueue.Exist(pr.ID))
pr = models.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 1}).(*models.PullRequest)
pr = models.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2}).(*models.PullRequest)
assert.Equal(t, models.PullRequestStatusChecking, pr.Status)
}
+11 -1
View File
@@ -23,7 +23,7 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*models.CommitStatus, re
return structs.CommitStatusSuccess
}
var returnedStatus = structs.CommitStatusPending
var returnedStatus = structs.CommitStatusSuccess
for _, ctx := range requiredContexts {
var targetStatus structs.CommitStatusState
for _, commitStatus := range commitStatuses {
@@ -35,6 +35,11 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*models.CommitStatus, re
if targetStatus == "" {
targetStatus = structs.CommitStatusPending
commitStatuses = append(commitStatuses, &models.CommitStatus{
State: targetStatus,
Context: ctx,
Description: "Pending",
})
}
if targetStatus.NoBetterThan(returnedStatus) {
returnedStatus = targetStatus
@@ -91,6 +96,11 @@ func IsPullCommitStatusPass(pr *models.PullRequest) (bool, error) {
// GetPullRequestCommitStatusState returns pull request merged commit status state
func GetPullRequestCommitStatusState(pr *models.PullRequest) (structs.CommitStatusState, error) {
// Ensure HeadRepo is loaded
if err := pr.LoadHeadRepo(); err != nil {
return "", errors.Wrap(err, "LoadHeadRepo")
}
// check if all required status checks are successful
headGitRepo, err := git.OpenRepository(pr.HeadRepo.RepoPath())
if err != nil {
+23 -3
View File
@@ -335,25 +335,45 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor
StdErr: errbuf.String(),
Err: err,
}
} else if strings.Contains(errbuf.String(), "! [remote rejected]") {
err := models.ErrPushRejected{
Style: mergeStyle,
StdOut: outbuf.String(),
StdErr: errbuf.String(),
Err: err,
}
err.GenerateMessage()
return err
}
return fmt.Errorf("git push: %s", errbuf.String())
}
outbuf.Reset()
errbuf.Reset()
pr.MergedCommitID, err = baseGitRepo.GetBranchCommitID(pr.BaseBranch)
pr.MergedCommitID, err = git.GetFullCommitID(tmpBasePath, baseBranch)
if err != nil {
return fmt.Errorf("GetBranchCommit: %v", err)
return fmt.Errorf("Failed to get full commit id for the new merge: %v", err)
}
pr.MergedUnix = timeutil.TimeStampNow()
pr.Merger = doer
pr.MergerID = doer.ID
if err = pr.SetMerged(); err != nil {
if _, err = pr.SetMerged(); err != nil {
log.Error("setMerged [%d]: %v", pr.ID, err)
}
if err := pr.LoadIssue(); err != nil {
log.Error("loadIssue [%d]: %v", pr.ID, err)
}
if err := pr.Issue.LoadRepo(); err != nil {
log.Error("loadRepo for issue [%d]: %v", pr.ID, err)
}
if err := pr.Issue.Repo.GetOwner(); err != nil {
log.Error("GetOwner for issue repo [%d]: %v", pr.ID, err)
}
notification.NotifyMergePullRequest(pr, doer, baseGitRepo)
// Reset cached commit count
+8
View File
@@ -231,6 +231,10 @@ func PushToBaseRepo(pr *models.PullRequest) (err error) {
}
}()
if err := pr.LoadHeadRepo(); err != nil {
log.Error("Unable to load head repository for PR[%d] Error: %v", pr.ID, err)
return err
}
headRepoPath := pr.HeadRepo.RepoPath()
if err := git.Clone(headRepoPath, tmpBasePath, git.CloneRepoOptions{
@@ -247,6 +251,10 @@ func PushToBaseRepo(pr *models.PullRequest) (err error) {
return fmt.Errorf("OpenRepository: %v", err)
}
if err := pr.LoadBaseRepo(); err != nil {
log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err)
return err
}
if err := gitRepo.AddRemote("base", pr.BaseRepo.RepoPath(), false); err != nil {
return fmt.Errorf("tmpGitRepo.AddRemote: %v", err)
}
+47 -44
View File
@@ -15,50 +15,53 @@
{{.i18n.Tr "admin.dashboard.operations"}}
</h4>
<div class="ui attached table segment">
<table class="ui very basic table">
<tbody>
<tr>
<td>{{.i18n.Tr "admin.dashboard.delete_inactivate_accounts"}}</td>
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=1">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.delete_repo_archives"}}</td>
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=2">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.delete_missing_repos"}}</td>
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=3">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.git_gc_repos"}}</td>
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=4">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.resync_all_sshkeys"}}</td>
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=5">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.resync_all_hooks"}}</td>
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=6">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.reinit_missing_repos"}}</td>
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=7">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.sync_external_users"}}</td>
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=8">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.git_fsck"}}</td>
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=9">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.delete_generated_repository_avatars"}}</td>
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=10">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
</tr>
</tbody>
</table>
<form method="post" action="{{AppSubUrl}}/admin">
{{.CsrfTokenHtml}}
<table class="ui very basic table">
<tbody>
<tr>
<td>{{.i18n.Tr "admin.dashboard.delete_inactivate_accounts"}}</td>
<td><button type="submit" class="ui green button" name="op" value="1"><i class="fa fa-caret-square-o-right"></i> {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.delete_repo_archives"}}</td>
<td><button type="submit" class="ui green button" name="op" value="2"><i class="fa fa-caret-square-o-right"></i> {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.delete_missing_repos"}}</td>
<td><button type="submit" class="ui green button" name="op" value="3"><i class="fa fa-caret-square-o-right"></i> {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.git_gc_repos"}}</td>
<td><button type="submit" class="ui green button" name="op" value="4"><i class="fa fa-caret-square-o-right"></i> {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.resync_all_sshkeys"}}</td>
<td><button type="submit" class="ui green button" name="op" value="5"><i class="fa fa-caret-square-o-right"></i> {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.resync_all_hooks"}}</td>
<td><button type="submit" class="ui green button" name="op" value="6"><i class="fa fa-caret-square-o-right"></i> {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.reinit_missing_repos"}}</td>
<td><button type="submit" class="ui green button" name="op" value="7"><i class="fa fa-caret-square-o-right"></i> {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.sync_external_users"}}</td>
<td><button type="submit" class="ui green button" name="op" value="8"><i class="fa fa-caret-square-o-right"></i> {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.git_fsck"}}</td>
<td><button type="submit" class="ui green button" name="op" value="9"><i class="fa fa-caret-square-o-right"></i> {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.delete_generated_repository_avatars"}}</td>
<td><button type="submit" class="ui green button" name="op" value="10"><i class="fa fa-caret-square-o-right"></i> {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td>
</tr>
</tbody>
</table>
</form>
</div>
<h4 class="ui top attached header">
+101
View File
@@ -0,0 +1,101 @@
{{template "base/head" .}}
<div class="admin user">
{{template "admin/navbar" .}}
<div class="ui container">
{{template "base/alert" .}}
<h4 class="ui top attached header">
{{.i18n.Tr "admin.emails.email_manage_panel"}} ({{.i18n.Tr "admin.total" .Total}})
</h4>
<div class="ui attached segment">
<div class="ui right floated secondary filter menu">
<!-- Sort -->
<div class="ui dropdown type jump item">
<span class="text">
{{.i18n.Tr "repo.issues.filter_sort"}}
<i class="dropdown icon"></i>
</span>
<div class="menu">
<a class="{{if or (eq .SortType "email") (not .SortType)}}active{{end}} item" href="{{$.Link}}?sort=email&q={{$.Keyword}}">{{.i18n.Tr "admin.emails.filter_sort.email"}}</a>
<a class="{{if eq .SortType "reverseemail"}}active{{end}} item" href="{{$.Link}}?sort=reverseemail&q={{$.Keyword}}">{{.i18n.Tr "admin.emails.filter_sort.email_reverse"}}</a>
<a class="{{if eq .SortType "username"}}active{{end}} item" href="{{$.Link}}?sort=username&q={{$.Keyword}}">{{.i18n.Tr "admin.emails.filter_sort.name"}}</a>
<a class="{{if eq .SortType "reverseusername"}}active{{end}} item" href="{{$.Link}}?sort=reverseusername&q={{$.Keyword}}">{{.i18n.Tr "admin.emails.filter_sort.name_reverse"}}</a>
</div>
</div>
</div>
<form class="ui form ignore-dirty" style="max-width: 90%">
<div class="ui fluid action input">
<input name="q" value="{{.Keyword}}" placeholder="{{.i18n.Tr "explore.search"}}..." autofocus>
<button class="ui blue button">{{.i18n.Tr "explore.search"}}</button>
</div>
</form>
</div>
<div class="ui attached table segment">
<table class="ui very basic striped table">
<thead>
<tr>
<th>{{.i18n.Tr "admin.users.name"}}</th>
<th>{{.i18n.Tr "admin.users.full_name"}}</th>
<th>{{.i18n.Tr "email"}}</th>
<th>{{.i18n.Tr "admin.emails.primary"}}</th>
<th>{{.i18n.Tr "admin.emails.activated"}}</th>
</tr>
</thead>
<tbody>
{{range .Emails}}
<tr>
<td><a href="{{AppSubUrl}}/{{.Name}}">{{.Name}}</a></td>
<td><span class="text truncate">{{.FullName}}</span></td>
<td><span class="text email">{{.Email}}</span></td>
<td><i class="fa fa{{if .IsPrimary}}-check{{end}}-square-o"></i></td>
<td>
{{if .CanChange}}
<a class="link-email-action" href data-uid="{{.UID}}"
data-email="{{.Email}}"
data-primary="{{if .IsPrimary}}1{{else}}0{{end}}"
data-activate="{{if .IsActivated}}0{{else}}1{{end}}">
<i class="fa fa{{if .IsActivated}}-check{{end}}-square-o"></i>
</a>
{{else}}
<i class="fa fa{{if .IsActivated}}-check{{end}}-square-o"></i>
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{template "base/paginate" .}}
<div class="ui basic modal" id="change-email-modal">
<div class="ui icon header">
{{.i18n.Tr "admin.emails.change_email_header"}}
</div>
<div class="content center">
<p>{{.i18n.Tr "admin.emails.change_email_text"}}</p>
<form class="ui form" id="email-action-form" action="{{AppSubUrl}}/admin/emails/activate" method="post">
{{$.CsrfTokenHtml}}
<input type="hidden" id="query-sort" name="sort" value="{{.SortType}}">
<input type="hidden" id="query-keyword" name="q" value="{{.Keyword}}">
<input type="hidden" id="query-primary" name="is_primary" value="{{.IsPrimary}}" required>
<input type="hidden" id="query-activated" name="is_activated" value="{{.IsActivated}}" required>
<input type="hidden" id="form-uid" name="uid" value="" required>
<input type="hidden" id="form-email" name="email" value="" required>
<input type="hidden" id="form-primary" name="primary" value="" required>
<input type="hidden" id="form-activate" name="activate" value="" required>
<div class="center actions">
<div class="ui basic cancel inverted button">{{$.i18n.Tr "settings.cancel"}}</div>
<button class="ui basic inverted yellow button">{{$.i18n.Tr "modal.yes"}}</button>
</div>
</form>
</div>
</div>
</div>
</div>
{{template "base/footer" .}}
+1
View File
@@ -8,6 +8,7 @@
<li {{if .PageIsAdminRepositories}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/repos">{{.i18n.Tr "admin.repositories"}}</a></li>
<li {{if .PageIsAdminHooks}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/hooks">{{.i18n.Tr "admin.hooks"}}</a></li>
<li {{if .PageIsAdminAuthentications}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/auths">{{.i18n.Tr "admin.authentication"}}</a></li>
<li {{if .PageIsAdminEmails}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/emails">{{.i18n.Tr "admin.emails"}}</a></li>
<li {{if .PageIsAdminConfig}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/config">{{.i18n.Tr "admin.config"}}</a></li>
<li {{if .PageIsAdminNotices}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/notices">{{.i18n.Tr "admin.notices"}}</a></li>
<li {{if .PageIsAdminMonitor}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/monitor">{{.i18n.Tr "admin.monitor"}}</a></li>
+3
View File
@@ -17,6 +17,9 @@
<a class="{{if .PageIsAdminAuthentications}}active{{end}} item" href="{{AppSubUrl}}/admin/auths">
{{.i18n.Tr "admin.authentication"}}
</a>
<a class="{{if .PageIsAdminEmails}}active{{end}} item" href="{{AppSubUrl}}/admin/emails">
{{.i18n.Tr "admin.emails"}}
</a>
<a class="{{if .PageIsAdminConfig}}active{{end}} item" href="{{AppSubUrl}}/admin/config">
{{.i18n.Tr "admin.config"}}
</a>
+4 -1
View File
@@ -40,7 +40,10 @@
<th></th>
<th colspan="5">
<div class="ui right">
<a class="ui red small button" href="{{AppSubUrl}}/admin/notices/empty">{{.i18n.Tr "admin.notices.delete_all"}}</a>
<form method="post" action="{{AppSubUrl}}/admin/notices/empty">
{{.CsrfTokenHtml}}
<button type="submit" class="ui red small button">{{.i18n.Tr "admin.notices.delete_all"}}</button>
</form>
</div>
<div class="ui floating upward dropdown small button">
<span class="text">{{.i18n.Tr "admin.notices.actions"}}</span>
+1 -1
View File
@@ -156,7 +156,7 @@
{{else}}
<meta property="og:title" content="{{AppName}}">
<meta property="og:type" content="website" />
<meta property="og:image" content="{{StaticUrlPrefix}}img/gitea-lg.png" />
<meta property="og:image" content="{{StaticUrlPrefix}}/img/gitea-lg.png" />
<meta property="og:url" content="{{AppUrl}}" />
<meta property="og:description" content="{{MetaDescription}}">
{{end}}
+1 -1
View File
@@ -109,7 +109,7 @@
{{end}}
<div class="divider"></div>
<a class="item" href="{{AppSubUrl}}/user/logout">
<a class="item link-action" href data-url="{{AppSubUrl}}/user/logout" data-redirect="{{AppSubUrl}}/">
<i class="octicon octicon-sign-out"></i>
{{.i18n.Tr "sign_out"}}<!-- Sign Out -->
</a>
+10 -4
View File
@@ -22,10 +22,10 @@
{{ $isPublic := index $.MembersIsPublicMember .ID}}
{{if $isPublic}}
<strong>{{$.i18n.Tr "org.members.public"}}</strong>
{{if or (eq $.SignedUser.ID .ID) $.IsOrganizationOwner}}(<a href="{{$.OrgLink}}/members/action/private?uid={{.ID}}">{{$.i18n.Tr "org.members.public_helper"}}</a>){{end}}
{{if or (eq $.SignedUser.ID .ID) $.IsOrganizationOwner}}(<a class="link-action" href data-url="{{$.OrgLink}}/members/action/private?uid={{.ID}}">{{$.i18n.Tr "org.members.public_helper"}}</a>){{end}}
{{else}}
<strong>{{$.i18n.Tr "org.members.private"}}</strong>
{{if or (eq $.SignedUser.ID .ID) $.IsOrganizationOwner}}(<a href="{{$.OrgLink}}/members/action/public?uid={{.ID}}">{{$.i18n.Tr "org.members.private_helper"}}</a>){{end}}
{{if or (eq $.SignedUser.ID .ID) $.IsOrganizationOwner}}(<a class="link-action" href data-url="{{$.OrgLink}}/members/action/public?uid={{.ID}}">{{$.i18n.Tr "org.members.private_helper"}}</a>){{end}}
{{end}}
</div>
</div>
@@ -48,9 +48,15 @@
<div class="ui four wide column">
<div class="text right">
{{if eq $.SignedUser.ID .ID}}
<a class="ui red small button" href="{{$.OrgLink}}/members/action/leave?uid={{.ID}}">{{$.i18n.Tr "org.members.leave"}}</a>
<form method="post" action="{{$.OrgLink}}/members/action/leave">
{{$.CsrfTokenHtml}}
<button type="submit" class="ui red small button" name="uid" value="{{.ID}}">{{$.i18n.Tr "org.members.leave"}}</button>
</form>
{{else if $.IsOrganizationOwner}}
<a class="ui red small button" href="{{$.OrgLink}}/members/action/remove?uid={{.ID}}">{{$.i18n.Tr "org.members.remove"}}</a>
<form method="post" action="{{$.OrgLink}}/members/action/remove">
{{$.CsrfTokenHtml}}
<button type="submit" class="ui red small button" name="uid" value="{{.ID}}">{{$.i18n.Tr "org.members.remove"}}</button>
</form>
{{end}}
</div>
</div>
+4 -1
View File
@@ -27,7 +27,10 @@
{{range .Team.Members}}
<div class="item">
{{if $.IsOrganizationOwner}}
<a class="ui red small button right" href="{{$.OrgLink}}/teams/{{$.Team.LowerName}}/action/remove?uid={{.ID}}">{{$.i18n.Tr "org.members.remove"}}</a>
<form method="post" action="{{$.OrgLink}}/teams/{{$.Team.LowerName}}/action/remove">
{{$.CsrfTokenHtml}}
<button type="submit" class="ui red small button right" name="uid" value="{{.ID}}">{{$.i18n.Tr "org.members.remove"}}</button>
</form>
{{end}}
<a href="{{.HomeLink}}">
<img class="ui avatar image" src="{{.RelAvatarLink}}">
+4 -1
View File
@@ -35,7 +35,10 @@
{{range .Team.Repos}}
<div class="item">
{{if $canAddRemove}}
<a class="ui red small button right" href="{{$.OrgLink}}/teams/{{$.Team.LowerName}}/action/repo/remove?repoid={{.ID}}">{{$.i18n.Tr "remove"}}</a>
<form method="post" action="{{$.OrgLink}}/teams/{{$.Team.LowerName}}/action/repo/remove">
{{$.CsrfTokenHtml}}
<button type="submit" class="ui red small button right" name="repoid" value="{{.ID}}">{{$.i18n.Tr "remove"}}</button>
</form>
{{end}}
<a class="member" href="{{AppSubUrl}}/{{$.Org.Name}}/{{.Name}}">
<i class="octicon octicon-{{if .IsPrivate}}lock{{else if .IsFork}}repo-forked{{else if .IsMirror}}repo-clone{{else}}repo{{end}}"></i>
+10 -2
View File
@@ -3,9 +3,17 @@
<strong>{{.Team.Name}}</strong>
<div class="ui right">
{{if .Team.IsMember $.SignedUser.ID}}
<a class="ui red tiny button" href="{{.OrgLink}}/teams/{{.Team.LowerName}}/action/leave?uid={{$.SignedUser.ID}}&page=home">{{$.i18n.Tr "org.teams.leave"}}</a>
<form method="post" action="{{.OrgLink}}/teams/{{.Team.LowerName}}/action/leave">
{{$.CsrfTokenHtml}}
<input type="hidden" name="page" value="home"/>
<button type="submit" class="ui red tiny button" name="uid" value="{{$.SignedUser.ID}}">{{$.i18n.Tr "org.teams.leave"}}</button>
</form>
{{else if .IsOrganizationOwner}}
<a class="ui blue tiny button" href="{{.OrgLink}}/teams/{{.Team.LowerName}}/action/join?uid={{$.SignedUser.ID}}&page=team">{{$.i18n.Tr "org.teams.join"}}</a>
<form method="post" action="{{.OrgLink}}/teams/{{.Team.LowerName}}/action/join">
{{$.CsrfTokenHtml}}
<input type="hidden" name="page" value="team"/>
<button type="submit" class="ui blue tiny button" name="uid" value="{{$.SignedUser.ID}}">{{$.i18n.Tr "org.teams.join"}}</button>
</form>
{{end}}
</div>
</h4>
+8 -2
View File
@@ -17,9 +17,15 @@
<a class="text black" href="{{$.OrgLink}}/teams/{{.LowerName}}"><strong>{{.Name}}</strong></a>
<div class="ui right">
{{if .IsMember $.SignedUser.ID}}
<a class="ui red small button" href="{{$.OrgLink}}/teams/{{.LowerName}}/action/leave?uid={{$.SignedUser.ID}}">{{$.i18n.Tr "org.teams.leave"}}</a>
<form method="post" action="{{$.OrgLink}}/teams/{{.LowerName}}/action/leave">
{{$.CsrfTokenHtml}}
<button type="submit" class="ui red small button" name="uid" value="{{$.SignedUser.ID}}">{{$.i18n.Tr "org.teams.leave"}}</button>
</form>
{{else if $.IsOrganizationOwner}}
<a class="ui blue small button" href="{{$.OrgLink}}/teams/{{.LowerName}}/action/join?uid={{$.SignedUser.ID}}">{{$.i18n.Tr "org.teams.join"}}</a>
<form method="post" action="{{$.OrgLink}}/teams/{{.LowerName}}/action/join">
{{$.CsrfTokenHtml}}
<button type="submit" class="ui blue small button" name="uid" value="{{$.SignedUser.ID}}">{{$.i18n.Tr "org.teams.join"}}</button>
</form>
{{end}}
</div>
</div>
+45 -28
View File
@@ -2,25 +2,40 @@
<div class="repository diff">
{{template "repo/header" .}}
<div class="ui container {{if .IsSplitStyle}}fluid padded{{end}}">
<div class="ui top attached info clearing segment {{if .Commit.Signature}} isSigned {{if .Verification.Verified }} isVerified {{end}}{{end}}">
{{$class := ""}}
{{if .Commit.Signature}}
{{$class = (printf "%s%s" $class " isSigned")}}
{{if .Verification.Verified}}
{{if eq .Verification.TrustStatus "trusted"}}
{{$class = (printf "%s%s" $class " isVerified")}}
{{else if eq .Verification.TrustStatus "untrusted"}}
{{$class = (printf "%s%s" $class " isVerifiedUntrusted")}}
{{else}}
{{$class = (printf "%s%s" $class " isVerifiedUnmatched")}}
{{end}}
{{else if .Verification.Warning}}
{{$class = (printf "%s%s" $class " isWarning")}}
{{end}}
{{end}}
<div class="ui top attached info clearing segment {{$class}}">
<a class="ui floated right blue tiny button" href="{{EscapePound .SourcePath}}">
{{.i18n.Tr "repo.diff.browse_source"}}
</a>
<h3 class="has-emoji">{{RenderCommitMessage .Commit.Message $.RepoLink $.Repository.ComposeMetas}}{{template "repo/commit_status" .CommitStatus}}</h3>
<h3 class="has-emoji"><span class="message-wrapper"><span class="commit-summary" title="{{.Commit.Summary}}">{{RenderCommitMessage .Commit.Message $.RepoLink $.Repository.ComposeMetas}}</span></span>{{template "repo/commit_status" .CommitStatus}}</h3>
{{if IsMultilineCommitMessage .Commit.Message}}
<pre class="commit-body">{{RenderCommitBody .Commit.Message $.RepoLink $.Repository.ComposeMetas}}</pre>
{{end}}
<span class="text grey"><i class="octicon octicon-git-branch"></i>{{.BranchName}}</span>
</div>
<div class="ui attached info segment {{if .Commit.Signature}} isSigned {{if .Verification.Verified }} isVerified {{end}}{{end}}">
<div class="ui attached info segment {{$class}}">
<div class="ui stackable grid">
<div class="nine wide column">
{{if .Author}}
<img class="ui avatar image" src="{{.Author.RelAvatarLink}}" />
{{if .Author.FullName}}
<a href="{{.Author.HomeLink}}"><strong>{{.Author.FullName}}</strong></a> {{if .IsSigned}}<{{.Commit.Author.Email}}>{{end}}
<a href="{{.Author.HomeLink}}"><strong>{{.Author.FullName}}</strong> {{if .IsSigned}}&lt;{{.Commit.Author.Email}}&gt;{{end}}</a>
{{else}}
<a href="{{.Author.HomeLink}}"><strong>{{.Commit.Author.Name}}</strong></a> {{if .IsSigned}}<{{.Commit.Author.Email}}>{{end}}
<a href="{{.Author.HomeLink}}"><strong>{{.Commit.Author.Name}}</strong> {{if .IsSigned}}&lt;{{.Commit.Author.Email}}&gt;{{end}}</a>
{{end}}
{{else}}
<img class="ui avatar image" src="{{AvatarLink .Commit.Author.Email}}" />
@@ -30,7 +45,7 @@
<span> </span>
{{if ne .Verification.CommittingUser.ID 0}}
<img class="ui avatar image" src="{{.Verification.CommittingUser.RelAvatarLink}}" />
<a href="{{.Verification.CommittingUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong></a> <{{.Commit.Committer.Email}}>
<a href="{{.Verification.CommittingUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong> &lt;{{.Commit.Committer.Email}}&gt;</a>
{{else}}
<img class="ui avatar image" src="{{AvatarLink .Commit.Committer.Email}}" />
<strong>{{.Commit.Committer.Name}}</strong>
@@ -58,40 +73,42 @@
</div><!-- end grid -->
</div>
{{if .Commit.Signature}}
{{if .Verification.Verified }}
<div class="ui bottom attached positive message">
<div class="ui bottom attached message {{$class}}">
{{if .Verification.Verified }}
{{if ne .Verification.SigningUser.ID 0}}
<i class="green lock icon"></i>
<span>{{.i18n.Tr "repo.commits.signed_by"}}:</span>
<i class="lock icon"></i>
{{if eq .Verification.TrustStatus "trusted"}}
<span class="ui text">{{.i18n.Tr "repo.commits.signed_by"}}:</span>
{{else if eq .Verification.TrustStatus "untrusted"}}
<span class="ui text">{{.i18n.Tr "repo.commits.signed_by_untrusted_user"}}:</span>
{{else}}
<span class="ui text">{{.i18n.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}:</span>
{{end}}
<img class="ui avatar image" src="{{.Verification.SigningUser.RelAvatarLink}}" />
<a href="{{.Verification.SigningUser.HomeLink}}"><strong>{{.Verification.SigningUser.Name}}</strong></a> <{{.Verification.SigningEmail}}>
<span class="pull-right"><span>{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> {{.Verification.SigningKey.KeyID}}</span>
<a href="{{.Verification.SigningUser.HomeLink}}"><strong>{{.Verification.SigningUser.Name}}</strong> <{{.Verification.SigningEmail}}></a>
<span class="pull-right"><span class="ui text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> {{.Verification.SigningKey.KeyID}}</span>
{{else}}
<i class="icons" title="{{.i18n.Tr "gpg.default_key"}}">
<i class="green lock icon"></i>
<i class="lock icon"></i>
<i class="tiny inverted cog icon centerlock"></i>
</i>
<span>{{.i18n.Tr "repo.commits.signed_by"}}:</span>
<span class="ui text">{{.i18n.Tr "repo.commits.signed_by"}}:</span>
<img class="ui avatar image" src="{{AvatarLink .Verification.SigningEmail}}" />
<strong>{{.Verification.SigningUser.Name}}</strong> <{{.Verification.SigningEmail}}>
<span class="pull-right"><span>{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="cogs icon" title="{{.i18n.Tr "gpg.default_key"}}"></i>{{.Verification.SigningKey.KeyID}}</span>
<span class="pull-right"><span class="ui text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="cogs icon" title="{{.i18n.Tr "gpg.default_key"}}"></i>{{.Verification.SigningKey.KeyID}}</span>
{{end}}
</div>
{{else if .Verification.Warning}}
<div class="ui bottom attached message">
<i class="red unlock icon"></i>
<span class="red text">{{.i18n.Tr .Verification.Reason}}</span>
<span class="pull-right"><span class="red text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="red warning icon"></i>{{.Verification.SigningKey.KeyID}}</span>
</div>
{{else}}
<div class="ui bottom attached message">
<i class="grey unlock icon"></i>
{{else if .Verification.Warning}}
<i class="unlock icon"></i>
<span class="ui text">{{.i18n.Tr .Verification.Reason}}</span>
<span class="pull-right"><span class="ui text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="warning icon"></i>{{.Verification.SigningKey.KeyID}}</span>
{{else}}
<i class="unlock icon"></i>
{{.i18n.Tr .Verification.Reason}}
{{if and .Verification.SigningKey (ne .Verification.SigningKey.KeyID "")}}
<span class="pull-right"><span class="red text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="red warning icon"></i>{{.Verification.SigningKey.KeyID}}</span>
<span class="pull-right"><span class="ui text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="warning icon"></i>{{.Verification.SigningKey.KeyID}}</span>
{{end}}
</div>
{{end}}
{{end}}
</div>
{{end}}
{{if .Note}}
<div class="ui top attached info segment message git-notes">
+20 -12
View File
@@ -28,7 +28,13 @@
{{if .Signature}}
{{$class = (printf "%s%s" $class " isSigned")}}
{{if .Verification.Verified}}
{{$class = (printf "%s%s" $class " isVerified")}}
{{if eq .Verification.TrustStatus "trusted"}}
{{$class = (printf "%s%s" $class " isVerified")}}
{{else if eq .Verification.TrustStatus "untrusted"}}
{{$class = (printf "%s%s" $class " isVerifiedUntrusted")}}
{{else}}
{{$class = (printf "%s%s" $class " isVerifiedUnmatched")}}
{{end}}
{{else if .Verification.Warning}}
{{$class = (printf "%s%s" $class " isWarning")}}
{{end}}
@@ -38,20 +44,22 @@
{{else}}
<span class="{{$class}}">
{{end}}
{{ShortSha .ID.String}}
<span class="shortsha">{{ShortSha .ID.String}}</span>
{{if .Signature}}
<div class="ui detail icon button">
{{if .Verification.Verified}}
{{if ne .Verification.SigningUser.ID 0}}
<i title="{{.Verification.Reason}}" class="lock green icon"></i>
{{else}}
<i title="{{.Verification.Reason}}" class="icons">
<i class="green lock icon"></i>
<i class="tiny inverted cog icon centerlock"></i>
</i>
{{end}}
{{else if .Verification.Warning}}
<i title="{{$.i18n.Tr .Verification.Reason}}" class="red unlock icon"></i>
<div title="{{if eq .Verification.TrustStatus "trusted"}}{{else if eq .Verification.TrustStatus "untrusted"}}{{$.i18n.Tr "repo.commits.signed_by_untrusted_user"}}: {{else}}{{$.i18n.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}: {{end}}{{.Verification.Reason}}">
{{if ne .Verification.SigningUser.ID 0}}
<i class="lock icon"></i>
<img class="ui signature avatar image" src="{{.Verification.SigningUser.RelAvatarLink}}" />
{{else}}
<i title="{{.Verification.Reason}}" class="icons">
<i class="lock icon"></i>
<i class="tiny inverted cog icon centerlock"></i>
</i>
<img class="ui signature avatar image" src="{{AvatarLink .Verification.SigningEmail}}" />
{{end}}
</div>
{{else}}
<i title="{{$.i18n.Tr .Verification.Reason}}" class="unlock icon"></i>
{{end}}
+2 -1
View File
@@ -26,7 +26,8 @@
<span class="markdown-info"><i class="octicon octicon-markdown"></i> {{$.root.i18n.Tr "repo.diff.comment.markdown_info"}}</span>
<div class="ui right floated">
{{if $.reply}}
<button name="reply" value="{{$.reply}}" class="ui submit green tiny button btn-reply">{{$.root.i18n.Tr "repo.diff.comment.reply"}}</button>
<input type="hidden" name="reply" value="{{$.reply}}">
<button class="ui submit green tiny button btn-reply" onclick="window.submitReply(this);">{{$.root.i18n.Tr "repo.diff.comment.reply"}}</button>
{{else}}
{{if $.root.CurrentReview}}
<button name="is_review" value="true" type="submit"
+22 -16
View File
@@ -20,22 +20,28 @@
</div>
{{if not .IsBeingCreated}}
<div class="repo-buttons">
<div class="ui labeled button" tabindex="0">
<a class="ui compact basic button" href="{{$.RepoLink}}/action/{{if $.IsWatchingRepo}}un{{end}}watch?redirect_to={{$.Link}}">
<i class="icon fa-eye{{if not $.IsWatchingRepo}}-slash{{end}}"></i>{{if $.IsWatchingRepo}}{{$.i18n.Tr "repo.unwatch"}}{{else}}{{$.i18n.Tr "repo.watch"}}{{end}}
</a>
<a class="ui basic label" href="{{.Link}}/watchers">
{{.NumWatches}}
</a>
</div>
<div class="ui labeled button" tabindex="0">
<a class="ui compact basic button" href="{{$.RepoLink}}/action/{{if $.IsStaringRepo}}un{{end}}star?redirect_to={{$.Link}}">
<i class="icon star{{if not $.IsStaringRepo}} outline{{end}}"></i>{{if $.IsStaringRepo}}{{$.i18n.Tr "repo.unstar"}}{{else}}{{$.i18n.Tr "repo.star"}}{{end}}
</a>
<a class="ui basic label" href="{{.Link}}/stars">
{{.NumStars}}
</a>
</div>
<form method="post" action="{{$.RepoLink}}/action/{{if $.IsWatchingRepo}}un{{end}}watch?redirect_to={{$.Link}}">
{{$.CsrfTokenHtml}}
<div class="ui labeled button" tabindex="0">
<button type="submit" class="ui compact basic button">
<i class="icon fa-eye{{if not $.IsWatchingRepo}}-slash{{end}}"></i>{{if $.IsWatchingRepo}}{{$.i18n.Tr "repo.unwatch"}}{{else}}{{$.i18n.Tr "repo.watch"}}{{end}}
</button>
<a class="ui basic label" href="{{.Link}}/watchers">
{{.NumWatches}}
</a>
</div>
</form>
<form method="post" action="{{$.RepoLink}}/action/{{if $.IsStaringRepo}}un{{end}}star?redirect_to={{$.Link}}">
{{$.CsrfTokenHtml}}
<div class="ui labeled button" tabindex="0">
<button type="submit" class="ui compact basic button">
<i class="icon star{{if not $.IsStaringRepo}} outline{{end}}"></i>{{if $.IsStaringRepo}}{{$.i18n.Tr "repo.unstar"}}{{else}}{{$.i18n.Tr "repo.star"}}{{end}}
</button>
<a class="ui basic label" href="{{.Link}}/stars">
{{.NumStars}}
</a>
</div>
</form>
{{if and (not .IsEmpty) ($.Permission.CanRead $.UnitTypeCode)}}
<div class="ui labeled button {{if and ($.IsSigned) (not $.CanSignedUserFork)}}disabled-repo-button{{end}}" tabindex="0">
<a class="ui compact basic button {{if or (not $.IsSigned) (not $.CanSignedUserFork)}}poping up{{end}}" {{if $.CanSignedUserFork}}href="{{AppSubUrl}}/repo/fork/{{.ID}}"{{else if $.IsSigned}} data-content="{{$.i18n.Tr "repo.fork_from_self"}}" {{ else }} data-content="{{$.i18n.Tr "repo.fork_guest_user" }}" rel="nofollow" href="{{AppSubUrl}}/user/login?redirect_to={{AppSubUrl}}/repo/fork/{{.ID}}" {{end}} data-position="top center" data-variation="tiny">
+2 -2
View File
@@ -71,9 +71,9 @@
<div class="ui right operate">
<a href="{{$.Link}}/{{.ID}}/edit" data-id={{.ID}} data-title={{.Name}}><i class="octicon octicon-pencil"></i> {{$.i18n.Tr "repo.issues.label_edit"}}</a>
{{if .IsClosed}}
<a href="{{$.Link}}/{{.ID}}/open" data-id={{.ID}} data-title={{.Name}}><i class="octicon octicon-check"></i> {{$.i18n.Tr "repo.milestones.open"}}</a>
<a class="link-action" href data-url="{{$.Link}}/{{.ID}}/open" data-id={{.ID}} data-title={{.Name}}><i class="octicon octicon-check"></i> {{$.i18n.Tr "repo.milestones.open"}}</a>
{{else}}
<a href="{{$.Link}}/{{.ID}}/close" data-id={{.ID}} data-title={{.Name}}><i class="octicon octicon-x"></i> {{$.i18n.Tr "repo.milestones.close"}}</a>
<a class="link-action" href data-url="{{$.Link}}/{{.ID}}/close" data-id={{.ID}} data-title={{.Name}}><i class="octicon octicon-x"></i> {{$.i18n.Tr "repo.milestones.close"}}</a>
{{end}}
<a class="delete-button" href="#" data-url="{{$.RepoLink}}/milestones/delete" data-id="{{.ID}}"><i class="octicon octicon-trashcan"></i> {{$.i18n.Tr "repo.issues.label_delete"}}</a>
</div>
@@ -10,7 +10,7 @@
<div class="item context clipboard" data-clipboard-text="{{Printf "%s%s/issues/%d#%s" AppUrl .ctx.Repository.FullName .ctx.Issue.Index .item.HashTag}}">{{.ctx.i18n.Tr "repo.issues.context.copy_link"}}</div>
{{end}}
<div class="item context quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.ID}}">{{.ctx.i18n.Tr "repo.issues.context.quote_reply"}}</div>
{{if or .ctx.Permission.IsAdmin (eq .item.Poster.ID .ctx.SignedUserID)}}
{{if or .ctx.Permission.IsAdmin .ctx.IsIssuePoster .ctx.IsIssueWriter}}
<div class="divider"></div>
<div class="item context edit-content">{{.ctx.i18n.Tr "repo.issues.context.edit"}}</div>
{{if .delete}}
+23 -12
View File
@@ -15,21 +15,32 @@
<strong>{{.LatestCommit.Author.Name}}</strong>
{{end}}
{{end}}
<a rel="nofollow" class="ui sha label {{if .LatestCommit.Signature}} isSigned {{if .LatestCommitVerification.Verified }} isVerified {{end}}{{end}}" href="{{.RepoLink}}/commit/{{.LatestCommit.ID}}">
{{ShortSha .LatestCommit.ID.String}}
{{if .LatestCommit.Signature}}
<div class="ui detail icon button">
{{if .LatestCommitVerification.Verified}}
<i title="{{.LatestCommitVerification.Reason}}" class="lock green icon"></i>
{{else}}
<i title="{{$.i18n.Tr .LatestCommitVerification.Reason}}" class="unlock icon"></i>
{{end}}
</div>
{{end}}
<a rel="nofollow" class="ui sha label {{if .LatestCommit.Signature}} isSigned {{if .LatestCommitVerification.Verified }} isVerified{{if eq .LatestCommitVerification.TrustStatus "trusted"}}{{else if eq .LatestCommitVerification.TrustStatus "untrusted"}}Untrusted{{else}}Unmatched{{end}}{{else if .LatestCommitVerification.Warning}} isWarning{{end}}{{end}}" href="{{.RepoLink}}/commit/{{.LatestCommit.ID}}">
<span class="shortsha">{{ShortSha .LatestCommit.ID.String}}</span>
{{if .LatestCommit.Signature}}
<div class="ui detail icon button">
{{if .LatestCommitVerification.Verified}}
<div title="{{if eq .LatestCommitVerification.TrustStatus "trusted"}}{{else if eq .LatestCommitVerification.TrustStatus "untrusted"}}{{.i18n.Tr "repo.commits.signed_by_untrusted_user"}}: {{else}}{{.i18n.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}: {{end}}{{.LatestCommitVerification.Reason}}">
{{if ne .LatestCommitVerification.SigningUser.ID 0}}
<i class="lock icon"></i>
<img class="ui signature avatar image" src="{{.LatestCommitVerification.SigningUser.RelAvatarLink}}" />
{{else}}
<i title="{{.LatestCommitVerification.Reason}}" class="icons">
<i class="lock icon"></i>
<i class="tiny inverted cog icon centerlock"></i>
</i>
<img class="ui signature avatar image" src="{{AvatarLink .LatestCommitVerification.SigningEmail}}" />
{{end}}
</div>
{{else}}
<i title="{{$.i18n.Tr .LatestCommitVerification.Reason}}" class="unlock icon"></i>
{{end}}
</div>
{{end}}
</a>
{{template "repo/commit_status" .LatestCommitStatus}}
{{ $commitLink:= printf "%s/commit/%s" .RepoLink .LatestCommit.ID }}
<span class="grey has-emoji commit-summary" title="{{.LatestCommit.Summary}}">{{RenderCommitMessageLinkSubject .LatestCommit.Message $.RepoLink $commitLink $.Repository.ComposeMetas}}
<span class="grey has-emoji commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{RenderCommitMessageLinkSubject .LatestCommit.Message $.RepoLink $commitLink $.Repository.ComposeMetas}}</span>
{{if IsMultilineCommitMessage .LatestCommit.Message}}
<button class="basic compact mini ui icon button commit-button"><i class="ellipsis horizontal icon"></i></button>
<pre class="commit-body" style="display: none;">{{RenderCommitBody .LatestCommit.Message $.RepoLink $.Repository.ComposeMetas}}</pre>

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