Compare commits

...

149 Commits

Author SHA1 Message Date
Kim "BKC" Carlbäcker
b0c7a3bb5c Fix stuff 2017-04-28 21:36:35 +02:00
Kim "BKC" Carlbäcker
77ef39112d Update changelog 2017-04-28 20:11:34 +02:00
Kim "BKC" Carlbäcker
8c708202b6 Add changelog 2017-04-28 20:09:41 +02:00
Kim "BKC" Carlbäcker
3e61f49da4 Restore-command added
- cleanup cmd.Dump, with deprication notice
- don't prefic cmd-commands
2017-04-28 20:09:13 +02:00
Lunny Xiao
fca7ddc6ea improve integration test to resue models/fixtures and store git repos with tests (#1627)
* improve integration test to resue models/fixtures and store git repos with source

* use gitea-integration-meta dir instead .tar.gz and cleanup every before every test

* fix import package name
2017-04-28 21:23:28 +08:00
Antoine GIRARD
431b26f6d7 Handle display of GPG key without end date (#1628) 2017-04-28 08:44:58 +08:00
Lunny Xiao
6853bf323a fix #1521 (#1621) 2017-04-27 20:15:56 +08:00
Lunny Xiao
e3c2963222 remove unused vendor packages (#1620) 2017-04-27 18:10:34 +08:00
Jonas Östanbäck
e4a0a04829 Add create_at and updated_at in PR json (#1616)
* Move new fields to object creation (use reference)
 * Vendor: Update SDK
2017-04-27 17:29:46 +08:00
Antoine GIRARD
8371f94d06 Rework SSH key management UI to add GPG (#1293)
* Rework SSH key management UI to add GPG

* Add more detail to gpg key display

* Update CHANGELOG.md

* Implement deletion UI

* Implement adding gpg UI

* Various fixes

- Fix duplicate entry in locale
- Re-generate hash before verification since they are consumed

* Add missing translation

* Split template

* Catch not found/verified email error
2017-04-26 21:10:43 +08:00
Sandro Santilli
b7da5a6cb7 Fix service description in Debian init file (#1538) 2017-04-25 19:30:42 +02:00
Ethan Koenig
c58708d3ee Integration test framework (#1290)
* Integration test framework

* udpate drone sign

* Formatting fixes and move router.go to routers/

* update sign for drone
2017-04-25 15:24:51 +08:00
Lunny Xiao
3012971e92 fix bug on status API (#1533) 2017-04-24 22:33:54 +08:00
Antoine GIRARD
eb1075dd4c Check if missing/modified/unused deps in vendor and fix errors (#1468)
* Check if missing deps in vendor

This will catch import from other repos. (maybe by auto-import)

* Remove github.com/smartystreets unused deps

* Remove github.com/boltdb/bolt unused dep

* Fetch github.com/go-xorm/core + sync gopkg.in/ldap.v2/ldap.go

* Auto-install govendor + reinstall github.com/boltdb/bolt needed by vendor/github.com/blevesearch/bleve/index/store/boltdb

* Update go-xorm/xorm to a5cb21 in vendor.json

* Use a custom repo for implementing change to bolt dep.

* Switching bolt to github.com/go-gitea/bolt

* Switching bolt to github.com/go-gitea/bolt (fork version)

* change the drone sign
2017-04-24 22:31:46 +08:00
Morlinest
fcc7cdab11 Fix markdown rendering (#1530) 2017-04-24 12:18:36 +08:00
Morlinest
5b8fe1e181 Do not show empty collaborators segment (#1531) 2017-04-23 08:55:02 +08:00
Kim "BKC" Carlbäcker
4bea219128 Status-API (#1332) 2017-04-21 19:32:31 +08:00
Lunny Xiao
52627032bc Add markup package to prepare for org markup format (#1493) 2017-04-21 09:01:08 +02:00
Lunny Xiao
f0db3da713 fix go get sub package and add domain on installation to let go get work defaultly (#1518)
* fix go get sub package and add domain on installation to let go get work defaultly

* fix import sequence

* fix .git problem
2017-04-21 10:43:29 +08:00
Lunny Xiao
42072783c9 support health check via / and fix #969 (#1520) 2017-04-20 20:14:21 +08:00
Lauris BH
bb14c97d40 Fix empty file download (#1506) 2017-04-20 10:38:56 +08:00
Bo-Yi Wu
fa2a513c62 feat: add download count field and unit testing for attachment. (#1512)
* feat: add download count field and unit testing.

* fix: unit testing

* refactor: improve testing.

* fix: update comment

* add default value.

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-04-20 10:31:31 +08:00
Lunny Xiao
a2d365c81f fix #1501 ssh hangs caused by #1461 (#1513) 2017-04-19 19:16:36 +08:00
Jonas
f995bcc87a Add repo mirror sync API endpoint (#1508)
* API: Add repo mirror sync

* Correct error message

* Change http status to 200
2017-04-19 19:09:49 +08:00
Lunny Xiao
2eeae84cbd Add internal routes for ssh hook comands (#1471)
* add internal routes for ssh hook comands

* fix lint

* add comment on why package named private not internal but the route name is internal

* add comment above package private why package named private not internal but the route name is internal

* remove exp time on internal access

* move routes from /internal to /api/internal

* add comment and defer on UpdatePublicKeyUpdated
2017-04-19 11:45:01 +08:00
Lauris BH
f42ec6120e Better URL validation (#1507)
* Add correct git branch name validation

* Change git refname validation error constant name

* Implement URL validation based on GoLang url.Parse method

* Backward compatibility with older Go compiler

* Add git reference name validation unit tests

* Remove unused variable in unit test

* Implement URL validation based on GoLang url.Parse method

* Backward compatibility with older Go compiler

* Add url validation unit tests
2017-04-19 11:02:20 +08:00
Antoine GIRARD
941281ae12 Use MAINTAINERS file in repository in CONTRIBUTING (#1489)
* Use MAINTAINERS file in repository because teams are not publicly available

* Use relative link
2017-04-18 08:56:28 +08:00
Antoine GIRARD
c764a542c0 Adding @sapk to MAINTAINERS (#1497) 2017-04-16 20:51:38 +08:00
Antoine GIRARD
cf91cfb993 Migrate WatchInfo struct to api (#1492)
* Remove WatchInfo and link to api struct

* Add vendor temporary update

* Return to gitea vendor source
2017-04-16 20:51:04 +08:00
silverwind
237270ef50 Orgs: fix org page title when full name is not defined (#1495) 2017-04-15 10:02:46 +08:00
Kim "BKC" Carlbäcker
d409d3ab57 Sanitation fix from Gogs (#1461)
* Santiation fix from Gogs

* Linting

* Fix build-errors

* still not working

* Fix all the things!

* gofmt

* Add code-injection checks
2017-04-13 10:52:24 +08:00
Michael de Wit
21290d4e80 Optimize and fix autolink function (#1442) (#1444)
* Optimize and fix autolink function (#1442)

* Fix bug in autolink recursive fallback function
2017-04-12 16:01:08 +08:00
Bo-Yi Wu
22295944df refactor: show command help message. (#1486) 2017-04-12 15:44:54 +08:00
Jonas
be6edaddcb Repo size in admin panel (#1482)
* Implementation of the feature to view repository size in admin panel
 * Move GetRepoSize to git module
 * Repository.RepoSize -> Repository.Size
 * RepoSize -> Size in template
 * Redo a few bits and pieces
 * Update size when syncing mirror or forking
 * Remove GetRepoSize
 * Changed fatal errors to error message

* Copy migration code from Gogs

* make fmt
2017-04-11 21:30:15 +08:00
Jonas
54f0293f0a Mirror sync interval specified as duration string (#1407)
* Sync interval specifed as duration string

* Changed mirror interval text

* make fmt

* Add MinInterval for mirror sync

* Use duration internally

* Changed min default to 10m

* make fmt

* Incorrect default

* Removed defaults in MustDuration()

* Add Mirror interval migration

* Default values corrected

* Use transaction during migration

* Change http 500 to page with error message

* Cleanup session.commit()
2017-04-08 23:27:26 +08:00
Mura Li
edbb9eefd6 Fix race when running commands with timeout (#1465)
Update vendored module code.gitea.io/git
2017-04-07 19:23:39 -07:00
Lunny Xiao
5c0bee9b20 refactor update ssh key use time (#1466) 2017-04-07 17:40:38 -07:00
Antoine GIRARD
d9db188274 Fix #1418 (#1456) 2017-04-07 22:50:40 +08:00
Jonas
e0df611cbc Autofocus on 2fa passcode fields (#1460)
* Autofocus on 2fa passcode field

* Autofocus on 2fa scratch code field
2017-04-07 06:07:37 -07:00
Lunny Xiao
5acfc7c4bc fix migrate failed and org dashboard failed on MSSQL database (#1448) 2017-04-06 18:47:25 -07:00
Lunny Xiao
cf6699fb4f fix gpg API panic when no verification (#1451)
* fix gpg API panic when no verification

* fix if condition
2017-04-06 18:40:05 -07:00
Morlinest
cbeeaa1b11 Fix and simplify repo branches UI (#1435) 2017-04-06 23:33:57 +08:00
Sandro Santilli
410af6971b Set VERSION from git once, in a variable (#1447) 2017-04-06 23:32:28 +08:00
Damien Gaignon
a78a0266c4 Correct broken unaligned load/store in armv5 (#1355)
Update vendor github.com/boltdb/bolt to take care of the issue #1354.
2017-04-05 18:50:33 -07:00
Patrick G
d800305b34 Commit search appearence fixes (#1254)
* Fix button text to be consisstent with rest of UI

* Make all commits checkbox look nicer
2017-04-05 18:32:59 -07:00
Bo-Yi Wu
0cee52e0d3 fix: remove str2html from org full name (#1360)
* fix: remove str2html for security issue.

* fix: update
2017-04-05 18:31:31 -07:00
Jonas
e7493e953f Corrected Mirror.NextUpdate not set (#1388) 2017-04-05 17:56:45 -07:00
Mura Li
2d1efcc270 Add GOFLAGS and EXTRA_GOFLAGS (#1438) 2017-04-05 16:10:32 +08:00
Mura Li
b746757209 Remove unused mutex field (#1440) 2017-04-04 01:54:02 -07:00
Mura Li
93c25c9a35 Fix races in the log module by using syncmap (#1421) 2017-04-03 10:22:26 +08:00
Kuba Tyszko
5d6b71fdbb fixes pull request hanging indefinitely when it contains normal and LFS file pointers (#1425) 2017-04-03 10:21:09 +08:00
Morlinest
21fd3da6f5 Fix disabled fields in repo settings UI (#1431) 2017-04-03 10:19:51 +08:00
Andrey Nering
37a34c1a28 Merge pull request #1410 from andreynering/notification/issue-watch
[Notifications Step 6] Per issue/PR watch/unwatch
2017-04-01 15:12:24 -03:00
Andrey Nering
f6e5ce65b2 Improve tests a little 2017-04-01 10:05:58 -03:00
Andrey Nering
e5c56fe30d Code style fixes 2017-04-01 09:58:20 -03:00
agrn
88112a5324 Fix "Error: No issue number specified" when pushing (#1393)
* Fix "Error: No issue number specified"  when pushing

* update pr
2017-03-31 18:05:39 -07:00
Jonas
095e1f5155 Support for custom html meta (#1423)
* Support for custom html meta

* Changed html meta description default
2017-03-31 18:03:01 -07:00
Mura Li
6a39250579 Add length check for the return string (#1420)
* Add length check for the return string

* Differentiate error paths and logging messages

* Rectify error message
2017-03-31 16:42:23 +08:00
Andrey Nering
fac7a6fecf Merge pull request #1390 from Morlinest/commit-graph-UI
Use whole button (commit graph) as link
2017-03-30 20:29:57 -03:00
Andrey Nering
e6781d5488 Add unit tests for issue_watch 2017-03-30 20:20:08 -03:00
Andrey Nering
a90ffffb1a Use variables for times 2017-03-30 19:14:16 -03:00
Andrey Nering
18952c40f8 Add copyright headers 2017-03-30 19:11:58 -03:00
Andrey Nering
4b284f814c UI and translation improvements 2017-03-30 19:10:30 -03:00
Morlinest
4fa691c4dd Make issue in commit graph "clickable" (#1392) 2017-03-30 18:05:38 +08:00
Antoine GIRARD
912b340d0d Simplify settings pages with item list (#1389)
* Remove point column on ssh key setting page

* Remove un-used css

* Some cleaning

* Use octicon-key
2017-03-30 09:02:37 +08:00
Andrey Nering
caed86fc6e Fix lint 2017-03-29 21:18:28 -03:00
Andrey Nering
e4a33ed4d0 Add octicons to watch/unwatch buttons 2017-03-29 21:08:46 -03:00
Andrey Nering
cb362513f0 Add updated_unix column on issue_watch 2017-03-29 20:59:28 -03:00
Andrey Nering
aa6e949b3d Consider issue_watchers while sending notifications 2017-03-29 20:54:57 -03:00
Andrey Nering
b674460748 Add watch button on issue 2017-03-29 20:31:47 -03:00
Andrey Nering
a0d0de7233 Create issue_watch table 2017-03-29 19:55:40 -03:00
Sandro Santilli
129b0d6a4b Allow ENABLE_OPENID_SIGNUP to depend on DISABLE_REGISTRATION (#1369)
* Allow ENABLE_OPENID_SIGNUP to depend on DISABLE_REGISTRATION

Omit the configuration variable (the default) to be dependent.
Fixes #1363

* Move OpenID settings under Service object

* Show OpenID SignUp and SignIn status in admin panel / configuration
2017-03-29 18:57:43 +08:00
Bo-Yi Wu
08f7fded3c refactor: remove workaround after the golang 1.7 release. (#1349)
* refactor: remove workaround after the golang 1.7 release.

* remove unused import.

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

* refactor: Add remove function.

* refactor: rename

* refactor: update build flag
2017-03-29 10:05:23 +08:00
Peter Žeby
d0298ea2fb Use whole button as link 2017-03-27 12:46:46 +02:00
Andrey Nering
6a451a2b59 Minor improvements on commit graph UI (#1380)
* Make "Commit Graph" link a button and translate it

* Add title on Commit Graph page
2017-03-24 10:32:40 +08:00
Pavel Korovin
ae9b02b079 Fix FCGI (over TCP) support (#1368) 2017-03-23 15:57:43 +08:00
Philippe Kueck
fe94032f74 rewrite pre-commit, post-commit and options hooks (fixes #1250) (#1257)
* issue #1250, replace {pre,post}-receive and update hooks with a single shell script that does not require custom hooks to be a sh-script

* issue #1250, make script posix compilant

* v23, add migration script to update {pre,post}-receive and update hooks

* migration: use a more common name and rename v23 to v26 to avoid conflicts

* gofmt'ed and added copyright header

* fix SyncRepositoryHooks to also sync wiki repos
2017-03-23 09:12:51 +08:00
Lunny Xiao
d330a23ce1 fix migration failed when authorized_keys is not exist (#1180) 2017-03-23 00:04:31 +08:00
Kim "BKC" Carlbäcker
d349f059af Cleaner IMO (#1361) 2017-03-22 22:30:40 +08:00
Jonas
bbbd08edc1 Changed text when password reset disabled. (#1364)
Fixes #1340
2017-03-22 22:29:29 +08:00
aloupfor
1b1b85439e Remove href on first/last link when on first/last page (#1345)
Signed-off-by: Arthur Loupforest <arthur.loupforest@outlook.fr>
2017-03-22 19:37:47 +08:00
Antoine GIRARD
14fe9010ae GPG commit validation (#1150)
* GPG commit validation

* Add translation

+ some little fix

* Move hash calc after retrieving of potential key + missing translation

* Add some little test
2017-03-22 18:43:54 +08:00
Fabian Zaremba
9224405155 LFS: Fix SSH authentication for trailing arguments (#1328)
Fixes #1296
2017-03-22 18:43:28 +08:00
Kim "BKC" Carlbäcker
c05bd1789c Reduce conditionals in signin/signup inner forms (#1138) 2017-03-22 00:50:56 +01:00
Jonas
16732fbfde Removed email copyright year (#1348) 2017-03-21 18:00:39 +08:00
Jonas
9a9f8fa25b Fix broken table layout (#1344) 2017-03-21 09:44:59 +08:00
Sandro Santilli
f73e734411 Run "make fmt" with go-1.6 (#1333) 2017-03-21 08:55:00 +08:00
Lunny Xiao
888dee3b5f fix delete user failed on sqlite (#1321) 2017-03-20 22:13:52 +08:00
Lunny Xiao
bd8fe49076 fix wiki bugs (#1294) 2017-03-20 21:36:19 +08:00
caiwp
dbabc35e71 Delete the useless code (#1335) 2017-03-20 21:17:07 +08:00
Sandro Santilli
15f5d8e794 Reduce conditionals in signin/signup inner forms
by always using SignInLink and SignUpLink in the form action
2017-03-20 09:40:42 +01:00
Fabian Zaremba
5586445207 LFS: Return 404 for unimplemented endpoints (#1330)
Without this patch a 401 is returned for unspecified endpoints, making
the LFS client ask for HTTP credentials. This behaviour was introduced
with the new locking API:

https://github.com/git-lfs/git-lfs/blob/master/docs/api/locking.md
2017-03-20 16:40:20 +08:00
Sandro Santilli
9182a35f18 Show user OpenID URIs in their profile (#1314) 2017-03-20 09:31:08 +01:00
Sandro Santilli
e1586898b2 Add change-password admin command (#1304)
* Add change-password admin command
2017-03-20 09:23:38 +01:00
Sandro Santilli
97ee88975a Add captcha support to OpenID based signup 2017-03-20 08:58:00 +01:00
Sandro Santilli
f00a4c8078 Gitea OpenID-2.0 login has been tested with livejournal.com too 2017-03-20 08:51:56 +01:00
Sandro Santilli
a9de85d31d Improve style of user OpenID setting page (#1324)
* Improve user openid list CSS and regenerate CSS
2017-03-20 08:51:33 +01:00
Lunny Xiao
608cd54a68 fix gpg wrong column types 2017-03-20 02:55:07 +01:00
Sandro Santilli
430cc4f42a Use readonly input form to show the validated OpenID URI (#1308) 2017-03-20 09:50:24 +08:00
Lunny Xiao
8bcb643a03 remove empty file (#1326) 2017-03-19 21:43:04 +08:00
Sandro Santilli
2aad4a5f97 Use font-awesome OpenID icon more (#1320) 2017-03-19 16:25:24 +08:00
Patrick G
b57b0c6e40 Fix inconsistency in layout (#1316) 2017-03-18 19:00:09 +08:00
Bo-Yi Wu
1c3bd436cc feat: Only use issue and wiki on repo. (#1297) 2017-03-18 18:59:07 +08:00
Sandro Santilli
5ecb369dac Specify that time interval units are seconds (#1311) 2017-03-18 11:10:30 +08:00
Sandro Santilli
71d16f69ff Login via OpenID-2.0 (#618) 2017-03-17 15:16:08 +01:00
Kim "BKC" Carlbäcker
0693fbfc00 Refactor repo/issue/view_content.tmpl (#1276) 2017-03-17 13:57:43 +08:00
Kim "BKC" Carlbäcker
7a81cd16c5 Refactor admin/auth/new.tmpl (#1277) 2017-03-17 13:55:04 +08:00
Lunny Xiao
ebbcf6fe12 Allow push to init a wiki repo (#1279)
* allow push to init a wiki repo

* compact error check
2017-03-17 12:59:42 +08:00
Lunny Xiao
3cf0e513e6 fix wrong log when push to a new branch (#1220) 2017-03-17 10:15:00 +08:00
Antoine GIRARD
925b252927 Fix missing less sources for oauth (#1288) 2017-03-17 08:49:24 +08:00
Patrick G
1476bf909e Make wording of commit search more clear 2017-03-17 01:34:35 +01:00
Fabian Zaremba
f1d2f16b54 Add notice that LFS mirroring is not supported (#1251)
* Add notice that LFS mirroring is not supported

* Drop German translation
2017-03-16 19:33:22 +08:00
Sandro Santilli
447c9b428f Send notifications to partecipants in issue comments (#1217)
* Send notifications to partecipants in issue comments

Closes #1216

Includes test (still failing)

* Do not include "labelers" to participants

Fix test to expect what GetParticipants return
2017-03-16 09:34:24 +08:00
Antoine GIRARD
ca1c3f1926 Implement GPG api (#710)
* Implement GPG API

* Better handle error

* Apply review recommendation + simplify database operations

* Remove useless comments
2017-03-16 09:27:35 +08:00
Andrey Nering
43c5469f81 Merge pull request #1258 from ethantkoenig/fix/access
Fix nil-dereference bug
2017-03-15 21:21:57 -03:00
Patrick G
efbb895ebe Cleaner ui for admin, repo settings, and user settings page (#1269) (#1270) 2017-03-15 23:39:38 +01:00
Patrick G
03d79983ee Cleaner UI for explore page (#1253) (#1255)
* Take navbar out of column
* Add styles to navbar
* Changed navbar classes
* Remove unneeded !important from index.css
* Remove unneeded !important from _explore.less
2017-03-15 19:55:12 +01:00
Ethan Koenig
17f403fbcd Fix nil-dereference bug 2017-03-15 14:09:44 -04:00
Thomas Boerger
780cb692d6 Make sure both scripts/ can live side by side (#1264) 2017-03-15 16:11:26 +01:00
Thomas Boerger
42032fdecf Synced licenses with github repo (#1246)
* Added script to download licenses from github

* Synced licenses with github repo
2017-03-15 13:30:16 +01:00
Thomas Boerger
a06c3ad2c0 Synced gitignores with github repo (#1245)
* Renamed scripts directory into contrib

* Added script to download gitignores from github

* Synced gitignores with github repo
2017-03-15 13:30:00 +01:00
Ethan Koenig
09fe4a2ae9 Batch updates for issues (#926) 2017-03-15 02:10:35 +01:00
Ethan Koenig
021904e4e6 Fix typos in models/ and modules/ (#1248) 2017-03-15 08:52:01 +08:00
Ethan Koenig
ec0ae5d50c Refactor and fix incorrect comment (#1247) 2017-03-15 08:51:46 +08:00
Sandro Santilli
7d8f9d1c46 Rename /forget_password url to /forgot_password
Also renames `forgot_password` translation key to
`forgot_password_title` and `forget_password` to
`forgot_password`

Includes entry in CHANGELOG about the breaking change
(and some markdown fixes in there)
2017-03-14 22:54:07 +01:00
Sandro Santilli
8746fb3385 Fix migration comment (#1241) 2017-03-14 14:39:02 +08:00
Patrick G
79ec33fd60 Update locale_en-US.ini (#1235) 2017-03-14 11:24:46 +08:00
Lunny Xiao
1ccdf19fae fix forget migration for wiki hooks (#1227) 2017-03-13 14:27:29 +08:00
Lunny Xiao
be5738243c fix repo settings external tracker failed and check external urls (#1215) 2017-03-13 09:41:40 +08:00
Patrick G
08aae4952b Add myself to maintainers (#1229) 2017-03-13 08:48:39 +08:00
Sandro Santilli
f0efb615c5 Add LibreJS support (#1201)
See See https://www.gnu.org/software/librejs/

Closes #1200
2017-03-12 22:50:20 +08:00
Lunny Xiao
608bbedee1 fix 500 caused by branches settings introduced by #1198 (#1214) 2017-03-12 21:50:26 +08:00
Sandro Santilli
0475e7351f Reserve the "explore" user/org name (#1222) 2017-03-12 09:39:38 +08:00
Mura Li
bdcc1a23e0 Add integration test for signup (#1135)
* Add integration test for signup

* Remove unused functions

* Refactoring

* Add repo_create_test.go

* Rollback the incomplete repo create test

* Comply with linter requirements and simplify the code a little bit
2017-03-11 22:30:29 +08:00
Sandro Santilli
8a98a25d8e Show a link to password reset from password change and delete account (#862)
It's helpful when you forgot your password thus cannot change it
(can happen if you log in via OAuth2 or OpenID)

Also make sure that both the delete-account and password-change
links to forgot-password will have the primary email pre-filled
2017-03-11 17:11:54 +08:00
Ethan Koenig
c99e7e1a62 Simplify RepositoryList.loadAttributes() (#1211) 2017-03-11 16:50:12 +08:00
Ethan Koenig
3803f257fb Move user_follow to separate file (#1210)
Also add unit tests
2017-03-11 16:46:53 +08:00
Ethan Koenig
1e3548b7e7 Unit tests for issue_list (#1209) 2017-03-11 16:46:23 +08:00
Patrick G
64214a9426 Search bar fixes for #1187 and #1205 (#1207) 2017-03-11 16:45:15 +08:00
Philippe Kueck
e2b2fd6e78 fix #1189, commit messages containing a pipe (#1203) 2017-03-11 12:01:38 +08:00
Patrick G
ccc15b9e1a Make strings translatable (#1188) (#1198)
* Add strings to ini

* Make strings translatable (#1188)
2017-03-11 09:28:25 +08:00
Lunny Xiao
74cde12677 Fix upgrade failed after ever rollback (#1194)
* fix upgrade failed after rollback

* fix upgrade failed after rollback

* fix possible upgrade twice fail after rollback
2017-03-10 23:43:51 +08:00
Lunny Xiao
f5476bdbb1 bug fixed for delete repo failed (#1193) 2017-03-10 22:05:53 +08:00
Lunny Xiao
656efdc1c7 fix ini format incomiptable with crowdin (#1177) 2017-03-10 19:40:25 +08:00
mcheng89
dbcd452758 rename OSX to macOS (#1176) 2017-03-10 16:20:17 +08:00
atzoum
05f0c4bbf5 Dockerfile for aarch64 (#1128) (#1130) 2017-03-10 10:02:55 +08:00
Patrick G
5463640fe6 Fix double borders on edit page (#1152) (#1153)
* Fix double borders on edit page (#1152)

* Add border modifications to less file
2017-03-10 09:33:35 +08:00
esell
4e716fb0fa add mssql to app.ini db config comment (#1172) 2017-03-10 09:33:19 +08:00
esell
d87596aec4 Add MSSQL to issues template (#1171) 2017-03-10 09:31:57 +08:00
870 changed files with 41532 additions and 10131 deletions

View File

@@ -19,11 +19,23 @@ pipeline:
- make generate
- make vet
- make lint
- make test-vendor
- make test
- make build
when:
event: [ push, tag, pull_request ]
test-sqlite:
image: webhippie/golang:edge
pull: true
environment:
TAGS: bindata
GOPATH: /srv/app
commands:
- make test-sqlite
when:
event: [ push, tag, pull_request ]
test-mysql:
image: webhippie/golang:edge
pull: true
@@ -31,7 +43,7 @@ pipeline:
TAGS: bindata
GOPATH: /srv/app
commands:
- make test-mysql
- echo make test-mysql # Not ready yet
when:
event: [ push, tag, pull_request ]
@@ -42,7 +54,7 @@ pipeline:
TAGS: bindata
GOPATH: /srv/app
commands:
- make test-pgsql
- echo make test-pqsql # Not ready yet
when:
event: [ push, tag, pull_request ]

View File

@@ -1 +1 @@
eyJhbGciOiJIUzI1NiJ9.d29ya3NwYWNlOgogIGJhc2U6IC9zcnYvYXBwCiAgcGF0aDogc3JjL2NvZGUuZ2l0ZWEuaW8vZ2l0ZWEKCnBpcGVsaW5lOgogIGNsb25lOgogICAgaW1hZ2U6IHBsdWdpbnMvZ2l0CiAgICB0YWdzOiB0cnVlCgogIHRlc3Q6CiAgICBpbWFnZTogd2ViaGlwcGllL2dvbGFuZzplZGdlCiAgICBwdWxsOiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgVEFHUzogYmluZGF0YSBzcWxpdGUKICAgICAgR09QQVRIOiAvc3J2L2FwcAogICAgY29tbWFuZHM6CiAgICAgIC0gYXBrIC1VIGFkZCBvcGVuc3NoLWNsaWVudAogICAgICAtIG1ha2UgY2xlYW4KICAgICAgLSBtYWtlIGdlbmVyYXRlCiAgICAgIC0gbWFrZSB2ZXQKICAgICAgLSBtYWtlIGxpbnQKICAgICAgLSBtYWtlIHRlc3QKICAgICAgLSBtYWtlIGJ1aWxkCiAgICB3aGVuOgogICAgICBldmVudDogWyBwdXNoLCB0YWcsIHB1bGxfcmVxdWVzdCBdCgogIHRlc3QtbXlzcWw6CiAgICBpbWFnZTogd2ViaGlwcGllL2dvbGFuZzplZGdlCiAgICBwdWxsOiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgVEFHUzogYmluZGF0YQogICAgICBHT1BBVEg6IC9zcnYvYXBwCiAgICBjb21tYW5kczoKICAgICAgLSBtYWtlIHRlc3QtbXlzcWwKICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHB1c2gsIHRhZywgcHVsbF9yZXF1ZXN0IF0KCiAgdGVzdC1wZ3NxbDoKICAgIGltYWdlOiB3ZWJoaXBwaWUvZ29sYW5nOmVkZ2UKICAgIHB1bGw6IHRydWUKICAgIGVudmlyb25tZW50OgogICAgICBUQUdTOiBiaW5kYXRhCiAgICAgIEdPUEFUSDogL3Nydi9hcHAKICAgIGNvbW1hbmRzOgogICAgICAtIG1ha2UgdGVzdC1wZ3NxbAogICAgd2hlbjoKICAgICAgZXZlbnQ6IFsgcHVzaCwgdGFnLCBwdWxsX3JlcXVlc3QgXQoKICBzdGF0aWM6CiAgICBpbWFnZToga2FyYWxhYmUveGdvLWxhdGVzdDpsYXRlc3QKICAgIHB1bGw6IHRydWUKICAgIGVudmlyb25tZW50OgogICAgICBUQUdTOiBiaW5kYXRhIHNxbGl0ZQogICAgICBHT1BBVEg6IC9zcnYvYXBwCiAgICBjb21tYW5kczoKICAgICAgLSBtYWtlIHJlbGVhc2UKICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHB1c2gsIHRhZywgcHVsbF9yZXF1ZXN0IF0KCiAgY292ZXJhZ2U6CiAgICBpbWFnZTogcGx1Z2lucy9jb3ZlcmFnZQogICAgc2VydmVyOiBodHRwczovL2NvdmVyYWdlLmdpdGVhLmlvCiAgICB3aGVuOgogICAgICBldmVudDogWyBwdXNoLCB0YWcsIHB1bGxfcmVxdWVzdCBdCgogIGRvY2tlcjoKICAgIGltYWdlOiBwbHVnaW5zL2RvY2tlcgogICAgcmVwbzogZ2l0ZWEvZ2l0ZWEKICAgIHRhZ3M6IFsgJyR7RFJPTkVfVEFHIyN2fScgXQogICAgd2hlbjoKICAgICAgZXZlbnQ6IFsgdGFnIF0KICAgICAgYnJhbmNoOiBbIHJlZnMvdGFncy8qIF0KCiAgZG9ja2VyOgogICAgaW1hZ2U6IHBsdWdpbnMvZG9ja2VyCiAgICByZXBvOiBnaXRlYS9naXRlYQogICAgdGFnczogWyAnJHtEUk9ORV9CUkFOQ0gjI3JlbGVhc2Uvdn0nIF0KICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHB1c2ggXQogICAgICBicmFuY2g6IFsgcmVsZWFzZS8qIF0KCiAgZG9ja2VyOgogICAgaW1hZ2U6IHBsdWdpbnMvZG9ja2VyCiAgICByZXBvOiBnaXRlYS9naXRlYQogICAgdGFnczogWyAnbGF0ZXN0JyBdCiAgICB3aGVuOgogICAgICBldmVudDogWyBwdXNoIF0KICAgICAgYnJhbmNoOiBbIG1hc3RlciBdCgogIHJlbGVhc2U6CiAgICBpbWFnZTogcGx1Z2lucy9zMwogICAgcGF0aF9zdHlsZTogdHJ1ZQogICAgc3RyaXBfcHJlZml4OiBkaXN0L3JlbGVhc2UvCiAgICBzb3VyY2U6IGRpc3QvcmVsZWFzZS8qCiAgICB0YXJnZXQ6IC9naXRlYS8ke0RST05FX1RBRyMjdn0KICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHRhZyBdCiAgICAgIGJyYW5jaDogWyByZWZzL3RhZ3MvKiBdCgogIHJlbGVhc2U6CiAgICBpbWFnZTogcGx1Z2lucy9zMwogICAgcGF0aF9zdHlsZTogdHJ1ZQogICAgc3RyaXBfcHJlZml4OiBkaXN0L3JlbGVhc2UvCiAgICBzb3VyY2U6IGRpc3QvcmVsZWFzZS8qCiAgICB0YXJnZXQ6IC9naXRlYS8ke0RST05FX0JSQU5DSCMjcmVsZWFzZS92fQogICAgd2hlbjoKICAgICAgZXZlbnQ6IFsgcHVzaCBdCiAgICAgIGJyYW5jaDogWyByZWxlYXNlLyogXQoKICByZWxlYXNlOgogICAgaW1hZ2U6IHBsdWdpbnMvczMKICAgIHBhdGhfc3R5bGU6IHRydWUKICAgIHN0cmlwX3ByZWZpeDogZGlzdC9yZWxlYXNlLwogICAgc291cmNlOiBkaXN0L3JlbGVhc2UvKgogICAgdGFyZ2V0OiAvZ2l0ZWEvbWFzdGVyCiAgICB3aGVuOgogICAgICBldmVudDogWyBwdXNoIF0KICAgICAgYnJhbmNoOiBbIG1hc3RlciBdCgogIGdpdGh1YjoKICAgIGltYWdlOiBwbHVnaW5zL2dpdGh1Yi1yZWxlYXNlCiAgICBmaWxlczoKICAgICAgLSBkaXN0L3JlbGVhc2UvKgogICAgd2hlbjoKICAgICAgZXZlbnQ6IFsgdGFnIF0KICAgICAgYnJhbmNoOiBbIHJlZnMvdGFncy8qIF0KCiAgZ2l0dGVyOgogICAgaW1hZ2U6IHBsdWdpbnMvZ2l0dGVyCgpzZXJ2aWNlczoKICBteXNxbDoKICAgIGltYWdlOiBteXNxbDo1LjcKICAgIGVudmlyb25tZW50OgogICAgICAtIE1ZU1FMX0RBVEFCQVNFPXRlc3QKICAgICAgLSBNWVNRTF9BTExPV19FTVBUWV9QQVNTV09SRD15ZXMKICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHB1c2gsIHRhZywgcHVsbF9yZXF1ZXN0IF0KCiAgcGdzcWw6CiAgICBpbWFnZTogcG9zdGdyZXM6OS41CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19EQj10ZXN0CiAgICB3aGVuOgogICAgICBldmVudDogWyBwdXNoLCB0YWcsIHB1bGxfcmVxdWVzdCBdCg.hp6IsxbFIQOaxJdmGv32Vf34-Nra3KqVIWzH52W687I
eyJhbGciOiJIUzI1NiJ9.d29ya3NwYWNlOgogIGJhc2U6IC9zcnYvYXBwCiAgcGF0aDogc3JjL2NvZGUuZ2l0ZWEuaW8vZ2l0ZWEKCnBpcGVsaW5lOgogIGNsb25lOgogICAgaW1hZ2U6IHBsdWdpbnMvZ2l0CiAgICB0YWdzOiB0cnVlCgogIHRlc3Q6CiAgICBpbWFnZTogd2ViaGlwcGllL2dvbGFuZzplZGdlCiAgICBwdWxsOiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgVEFHUzogYmluZGF0YSBzcWxpdGUKICAgICAgR09QQVRIOiAvc3J2L2FwcAogICAgY29tbWFuZHM6CiAgICAgIC0gYXBrIC1VIGFkZCBvcGVuc3NoLWNsaWVudAogICAgICAtIG1ha2UgY2xlYW4KICAgICAgLSBtYWtlIGdlbmVyYXRlCiAgICAgIC0gbWFrZSB2ZXQKICAgICAgLSBtYWtlIGxpbnQKICAgICAgLSBtYWtlIHRlc3QtdmVuZG9yCiAgICAgIC0gbWFrZSB0ZXN0CiAgICAgIC0gbWFrZSBidWlsZAogICAgd2hlbjoKICAgICAgZXZlbnQ6IFsgcHVzaCwgdGFnLCBwdWxsX3JlcXVlc3QgXQoKICB0ZXN0LXNxbGl0ZToKICAgIGltYWdlOiB3ZWJoaXBwaWUvZ29sYW5nOmVkZ2UKICAgIHB1bGw6IHRydWUKICAgIGVudmlyb25tZW50OgogICAgICBUQUdTOiBiaW5kYXRhCiAgICAgIEdPUEFUSDogL3Nydi9hcHAKICAgIGNvbW1hbmRzOgogICAgICAtIG1ha2UgdGVzdC1zcWxpdGUKICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHB1c2gsIHRhZywgcHVsbF9yZXF1ZXN0IF0KCiAgdGVzdC1teXNxbDoKICAgIGltYWdlOiB3ZWJoaXBwaWUvZ29sYW5nOmVkZ2UKICAgIHB1bGw6IHRydWUKICAgIGVudmlyb25tZW50OgogICAgICBUQUdTOiBiaW5kYXRhCiAgICAgIEdPUEFUSDogL3Nydi9hcHAKICAgIGNvbW1hbmRzOgogICAgICAtIGVjaG8gbWFrZSB0ZXN0LW15c3FsICMgTm90IHJlYWR5IHlldAogICAgd2hlbjoKICAgICAgZXZlbnQ6IFsgcHVzaCwgdGFnLCBwdWxsX3JlcXVlc3QgXQoKICB0ZXN0LXBnc3FsOgogICAgaW1hZ2U6IHdlYmhpcHBpZS9nb2xhbmc6ZWRnZQogICAgcHVsbDogdHJ1ZQogICAgZW52aXJvbm1lbnQ6CiAgICAgIFRBR1M6IGJpbmRhdGEKICAgICAgR09QQVRIOiAvc3J2L2FwcAogICAgY29tbWFuZHM6CiAgICAgIC0gZWNobyBtYWtlIHRlc3QtcHFzcWwgIyBOb3QgcmVhZHkgeWV0CiAgICB3aGVuOgogICAgICBldmVudDogWyBwdXNoLCB0YWcsIHB1bGxfcmVxdWVzdCBdCgogIHN0YXRpYzoKICAgIGltYWdlOiBrYXJhbGFiZS94Z28tbGF0ZXN0OmxhdGVzdAogICAgcHVsbDogdHJ1ZQogICAgZW52aXJvbm1lbnQ6CiAgICAgIFRBR1M6IGJpbmRhdGEgc3FsaXRlCiAgICAgIEdPUEFUSDogL3Nydi9hcHAKICAgIGNvbW1hbmRzOgogICAgICAtIG1ha2UgcmVsZWFzZQogICAgd2hlbjoKICAgICAgZXZlbnQ6IFsgcHVzaCwgdGFnLCBwdWxsX3JlcXVlc3QgXQoKICBjb3ZlcmFnZToKICAgIGltYWdlOiBwbHVnaW5zL2NvdmVyYWdlCiAgICBzZXJ2ZXI6IGh0dHBzOi8vY292ZXJhZ2UuZ2l0ZWEuaW8KICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHB1c2gsIHRhZywgcHVsbF9yZXF1ZXN0IF0KCiAgZG9ja2VyOgogICAgaW1hZ2U6IHBsdWdpbnMvZG9ja2VyCiAgICByZXBvOiBnaXRlYS9naXRlYQogICAgdGFnczogWyAnJHtEUk9ORV9UQUcjI3Z9JyBdCiAgICB3aGVuOgogICAgICBldmVudDogWyB0YWcgXQogICAgICBicmFuY2g6IFsgcmVmcy90YWdzLyogXQoKICBkb2NrZXI6CiAgICBpbWFnZTogcGx1Z2lucy9kb2NrZXIKICAgIHJlcG86IGdpdGVhL2dpdGVhCiAgICB0YWdzOiBbICcke0RST05FX0JSQU5DSCMjcmVsZWFzZS92fScgXQogICAgd2hlbjoKICAgICAgZXZlbnQ6IFsgcHVzaCBdCiAgICAgIGJyYW5jaDogWyByZWxlYXNlLyogXQoKICBkb2NrZXI6CiAgICBpbWFnZTogcGx1Z2lucy9kb2NrZXIKICAgIHJlcG86IGdpdGVhL2dpdGVhCiAgICB0YWdzOiBbICdsYXRlc3QnIF0KICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHB1c2ggXQogICAgICBicmFuY2g6IFsgbWFzdGVyIF0KCiAgcmVsZWFzZToKICAgIGltYWdlOiBwbHVnaW5zL3MzCiAgICBwYXRoX3N0eWxlOiB0cnVlCiAgICBzdHJpcF9wcmVmaXg6IGRpc3QvcmVsZWFzZS8KICAgIHNvdXJjZTogZGlzdC9yZWxlYXNlLyoKICAgIHRhcmdldDogL2dpdGVhLyR7RFJPTkVfVEFHIyN2fQogICAgd2hlbjoKICAgICAgZXZlbnQ6IFsgdGFnIF0KICAgICAgYnJhbmNoOiBbIHJlZnMvdGFncy8qIF0KCiAgcmVsZWFzZToKICAgIGltYWdlOiBwbHVnaW5zL3MzCiAgICBwYXRoX3N0eWxlOiB0cnVlCiAgICBzdHJpcF9wcmVmaXg6IGRpc3QvcmVsZWFzZS8KICAgIHNvdXJjZTogZGlzdC9yZWxlYXNlLyoKICAgIHRhcmdldDogL2dpdGVhLyR7RFJPTkVfQlJBTkNIIyNyZWxlYXNlL3Z9CiAgICB3aGVuOgogICAgICBldmVudDogWyBwdXNoIF0KICAgICAgYnJhbmNoOiBbIHJlbGVhc2UvKiBdCgogIHJlbGVhc2U6CiAgICBpbWFnZTogcGx1Z2lucy9zMwogICAgcGF0aF9zdHlsZTogdHJ1ZQogICAgc3RyaXBfcHJlZml4OiBkaXN0L3JlbGVhc2UvCiAgICBzb3VyY2U6IGRpc3QvcmVsZWFzZS8qCiAgICB0YXJnZXQ6IC9naXRlYS9tYXN0ZXIKICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHB1c2ggXQogICAgICBicmFuY2g6IFsgbWFzdGVyIF0KCiAgZ2l0aHViOgogICAgaW1hZ2U6IHBsdWdpbnMvZ2l0aHViLXJlbGVhc2UKICAgIGZpbGVzOgogICAgICAtIGRpc3QvcmVsZWFzZS8qCiAgICB3aGVuOgogICAgICBldmVudDogWyB0YWcgXQogICAgICBicmFuY2g6IFsgcmVmcy90YWdzLyogXQoKICBnaXR0ZXI6CiAgICBpbWFnZTogcGx1Z2lucy9naXR0ZXIKCnNlcnZpY2VzOgogIG15c3FsOgogICAgaW1hZ2U6IG15c3FsOjUuNwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTVlTUUxfREFUQUJBU0U9dGVzdAogICAgICAtIE1ZU1FMX0FMTE9XX0VNUFRZX1BBU1NXT1JEPXllcwogICAgd2hlbjoKICAgICAgZXZlbnQ6IFsgcHVzaCwgdGFnLCBwdWxsX3JlcXVlc3QgXQoKICBwZ3NxbDoKICAgIGltYWdlOiBwb3N0Z3Jlczo5LjUKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX0RCPXRlc3QKICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHB1c2gsIHRhZywgcHVsbF9yZXF1ZXN0IF0K.dA2VK6LdoPXvBTYAUywWervhOZmgOjU32uiiPrBbVdQ

View File

@@ -11,6 +11,7 @@
- Database (use `[x]`):
- [ ] PostgreSQL
- [ ] MySQL
- [ ] MSSQL
- [ ] SQLite
- Can you reproduce the bug at https://try.gitea.io:
- [ ] Yes (provide example URL)

2
.gitignore vendored
View File

@@ -36,6 +36,7 @@ coverage.out
*.log
/gitea
/integrations.test
/bin
/dist
@@ -44,3 +45,4 @@ coverage.out
/indexers
/log
/public/img/avatar
/integrations/gitea-integration

View File

@@ -1,5 +1,12 @@
# Changelog
## Unreleased
* BREAKING
* Password reset URL changed from `/user/forget_password` to `/user/forgot_password`
* SSH keys management URL changed from `/user/settings/ssh` to `/user/settings/keys`
## [1.1.0](https://github.com/go-gitea/gitea/releases/tag/v1.1.0) - 2017-03-09
* BREAKING
@@ -72,7 +79,7 @@
* Added option to config to disable local path imports [#724](https://github.com/go-gitea/gitea/pull/724)
* Allow custom public files [#782](https://github.com/go-gitea/gitea/pull/782)
* Added pprof endpoint for debugging [#801](https://github.com/go-gitea/gitea/pull/801)
* Added X-GitHub-* headers [#809](https://github.com/go-gitea/gitea/pull/809)
* Added `X-GitHub-*` headers [#809](https://github.com/go-gitea/gitea/pull/809)
* Fill SSH key title automatically [#863](https://github.com/go-gitea/gitea/pull/863)
* Display Git version on admin panel [#921](https://github.com/go-gitea/gitea/pull/921)
* Expose URL field on issue API [#982](https://github.com/go-gitea/gitea/pull/982)
@@ -104,7 +111,7 @@
## [1.0.1](https://github.com/go-gitea/gitea/releases/tag/v1.0.1) - 2017-01-05
* BUGFIXES
* Fixed localized MIN_PASSWORD_LENGTH [#501](https://github.com/go-gitea/gitea/pull/501)
* Fixed localized `MIN_PASSWORD_LENGTH` [#501](https://github.com/go-gitea/gitea/pull/501)
* Fixed 500 error on organization delete [#507](https://github.com/go-gitea/gitea/pull/507)
* Ignore empty wiki repo on migrate [#544](https://github.com/go-gitea/gitea/pull/544)
* Proper check access for forking [#563](https://github.com/go-gitea/gitea/pull/563)

View File

@@ -81,7 +81,7 @@ The current release cycle is aligned to start on December 25 to February 24, nex
## Maintainers
To make sure every PR is checked, we have [team maintainers](https://github.com/orgs/go-gitea/teams/maintainers). Every PR **MUST** be reviewed by at least two maintainers (or owners) before it can get merged. A maintainer should be a contributor of Gitea (or Gogs) and contributed at least 4 accepted PRs. A contributor should apply as a maintainer in the [Gitter develop channel](https://gitter.im/go-gitea/develop). The owners or the team maintainers may invite the contributor. A maintainer should spend some time on code reviews. If a maintainer has no time to do that, they should apply to leave the maintainers team and we will give them the honor of being a member of the [advisors team](https://github.com/orgs/go-gitea/teams/advisors). Of course, if an advisor has time to code review, we will gladly welcome them back to the maintainers team. If a maintainer is inactive for more than 3 months and forgets to leave the maintainers team, the owners may move him or her from the maintainers team to the advisors team.
To make sure every PR is checked, we have [team maintainers](MAINTAINERS). Every PR **MUST** be reviewed by at least two maintainers (or owners) before it can get merged. A maintainer should be a contributor of Gitea (or Gogs) and contributed at least 4 accepted PRs. A contributor should apply as a maintainer in the [Gitter develop channel](https://gitter.im/go-gitea/develop). The owners or the team maintainers may invite the contributor. A maintainer should spend some time on code reviews. If a maintainer has no time to do that, they should apply to leave the maintainers team and we will give them the honor of being a member of the [advisors team](https://github.com/orgs/go-gitea/teams/advisors). Of course, if an advisor has time to code review, we will gladly welcome them back to the maintainers team. If a maintainer is inactive for more than 3 months and forgets to leave the maintainers team, the owners may move him or her from the maintainers team to the advisors team.
## Owners

43
Dockerfile.aarch64 Normal file
View File

@@ -0,0 +1,43 @@
FROM aarch64/alpine:3.5
EXPOSE 22 3000
RUN apk update && \
apk add \
su-exec \
ca-certificates \
sqlite \
bash \
git \
linux-pam \
s6 \
curl \
openssh \
tzdata && \
rm -rf \
/var/cache/apk/* && \
addgroup \
-S -g 1000 \
git && \
adduser \
-S -H -D \
-h /data/git \
-s /bin/bash \
-u 1000 \
-G git \
git && \
echo "git:$(date +%s | sha256sum | base64 | head -c 32)" | chpasswd
ENV USER git
ENV GITEA_CUSTOM /data/gitea
COPY docker /
COPY gitea /app/gitea/gitea
ENV GODEBUG=netdns=go
VOLUME ["/data"]
ENTRYPOINT ["/usr/bin/entrypoint"]
CMD ["/bin/s6-svscan", "/etc/s6"]

View File

@@ -12,3 +12,5 @@ Rémy Boulanouar <admin@dblk.org> (@DblK)
Sandro Santilli <strk@kbt.io> (@strk)
Thibault Meyer <meyer.thibault@gmail.com> (@0xbaadf00d)
Thomas Boerger <thomas@webhippie.de> (@tboerger)
Patrick G <geek1011@outlook.com> (@geek1011)
Antoine Girard <sapk@sapk.fr> (@sapk)

View File

@@ -11,7 +11,12 @@ BINDATA := modules/{options,public,templates}/bindata.go
STYLESHEETS := $(wildcard public/less/index.less public/less/_*.less)
JAVASCRIPTS :=
LDFLAGS := -X "main.Version=$(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')" -X "main.Tags=$(TAGS)"
GOFLAGS := -i -v
EXTRA_GOFLAGS ?=
VERSION := $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
LDFLAGS := -X "main.Version=$(VERSION)" -X "main.Tags=$(TAGS)"
PACKAGES ?= $(filter-out code.gitea.io/gitea/integrations,$(shell go list ./... | grep -v /vendor/))
SOURCES ?= $(shell find . -name "*.go" -type f)
@@ -74,13 +79,28 @@ integrations: build
test:
for PKG in $(PACKAGES); do go test -cover -coverprofile $$GOPATH/src/$$PKG/coverage.out $$PKG || exit 1; done;
.PHONY: test-vendor
test-vendor:
@hash govendor > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/kardianos/govendor; \
fi
govendor status +outside +unused || exit 1
.PHONY: test-sqlite
test-sqlite: integrations.test
GITEA_CONF=integrations/sqlite.ini ./integrations.test
.PHONY: test-mysql
test-mysql:
@echo "Not integrated yet!"
test-mysql: integrations.test
echo "CREATE DATABASE IF NOT EXISTS testgitea" | mysql -u root
GITEA_CONF=integrations/mysql.ini ./integrations.test
.PHONY: test-pgsql
test-pgsql:
@echo "Not integrated yet!"
test-pgsql: integrations.test
GITEA_CONF=integrations/pgsql.ini ./integrations.test
integrations.test: $(SOURCES)
go test -c code.gitea.io/gitea/integrations -tags 'sqlite'
.PHONY: check
check: test
@@ -93,7 +113,7 @@ install: $(wildcard *.go)
build: $(EXECUTABLE)
$(EXECUTABLE): $(SOURCES)
go build -i -v -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@
go build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@
.PHONY: docker
docker:

View File

@@ -10,12 +10,11 @@
[![GoDoc](https://godoc.org/code.gitea.io/gitea?status.svg)](https://godoc.org/code.gitea.io/gitea)
[![Release](https://github-release-version.herokuapp.com/github/go-gitea/gitea/release.svg?style=flat)](https://github.com/go-gitea/gitea/releases/latest)
||||
|:-------------:|:-------:|:-------:|
| | | |
|:---:|:---:|:---:|
|![Dashboard](https://i.imgur.com/3iEQsux.jpg)|![Repository](https://i.imgur.com/glqFnj8.jpg)|![Commits History](https://i.imgur.com/ad1FEpi.jpg)|
|![Profile](https://i.imgur.com/q81EcGa.jpg)|![Admin Dashboard](https://i.imgur.com/L2CQeN0.jpg)|![Diff](https://i.imgur.com/cNuvMum.jpg)|
|![Issues](https://i.imgur.com/xCYRqaF.jpg)|![Releases](https://i.imgur.com/ILpRBCe.jpg)|![Organization](https://i.imgur.com/0BHnrcL.jpg)|
||||
## Purpose

View File

@@ -10,12 +10,11 @@
[![GoDoc](https://godoc.org/code.gitea.io/gitea?status.svg)](https://godoc.org/code.gitea.io/gitea)
[![Release](https://github-release-version.herokuapp.com/github/go-gitea/gitea/release.svg?style=flat)](https://github.com/go-gitea/gitea/releases/latest)
||||
|:-------------:|:-------:|:-------:|
| | | |
|:---:|:---:|:---:|
|![Dashboard](https://i.imgur.com/3iEQsux.jpg)|![Repository](https://i.imgur.com/glqFnj8.jpg)|![Commits History](https://i.imgur.com/ad1FEpi.jpg)|
|![Profile](https://i.imgur.com/q81EcGa.jpg)|![Admin Dashboard](https://i.imgur.com/L2CQeN0.jpg)|![Diff](https://i.imgur.com/cNuvMum.jpg)|
|![Issues](https://i.imgur.com/xCYRqaF.jpg)|![Releases](https://i.imgur.com/ILpRBCe.jpg)|![Organization](https://i.imgur.com/0BHnrcL.jpg)|
||||
## 目标

View File

@@ -0,0 +1,3 @@
title: Implement proper `gitea restore` & `gitea backup`. Deprecating `gitea dump`
kind: feature
pull_request: 1637

View File

@@ -8,10 +8,10 @@ package cmd
import (
"fmt"
"github.com/urfave/cli"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/setting"
"github.com/urfave/cli"
)
var (
@@ -23,6 +23,7 @@ var (
to make automatic initialization process more smoothly`,
Subcommands: []cli.Command{
subcmdCreateUser,
subcmdChangePassword,
},
}
@@ -57,8 +58,59 @@ to make automatic initialization process more smoothly`,
},
},
}
subcmdChangePassword = cli.Command{
Name: "change-password",
Usage: "Change a user's password",
Action: runChangePassword,
Flags: []cli.Flag{
cli.StringFlag{
Name: "username,u",
Value: "",
Usage: "The user to change password for",
},
cli.StringFlag{
Name: "password,p",
Value: "",
Usage: "New password to set for user",
},
},
}
)
func runChangePassword(c *cli.Context) error {
if !c.IsSet("password") {
return fmt.Errorf("Password is not specified")
} else if !c.IsSet("username") {
return fmt.Errorf("Username is not specified")
}
setting.NewContext()
models.LoadConfigs()
setting.NewXORMLogService(false)
if err := models.SetEngine(); err != nil {
return fmt.Errorf("models.SetEngine: %v", err)
}
uname := c.String("username")
user, err := models.GetUserByName(uname)
if err != nil {
return fmt.Errorf("%v", err)
}
user.Passwd = c.String("password")
if user.Salt, err = models.GetUserSalt(); err != nil {
return fmt.Errorf("%v", err)
}
user.EncodePasswd()
if err := models.UpdateUser(user); err != nil {
return fmt.Errorf("%v", err)
}
fmt.Printf("User '%s' password has been successfully updated!\n", uname)
return nil
}
func runCreateUser(c *cli.Context) error {
if !c.IsSet("name") {
return fmt.Errorf("Username is not specified")

197
cmd/backup.go Normal file
View File

@@ -0,0 +1,197 @@
// Copyright 2017 the Gitea Authors. All rights reserved.
// Copyright 2017 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
"fmt"
"io/ioutil"
"os"
"path"
"runtime/debug"
"time"
"github.com/Unknwon/cae/zip"
"github.com/Unknwon/com"
"github.com/urfave/cli"
"gopkg.in/ini.v1"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)
// Backup files and database
var Backup = cli.Command{
Name: "backup",
Usage: "Backup files and database",
Description: `Backup dumps and compresses all related files and database into zip file,
which can be used for migrating Gitea to another server. The output format is meant to be
portable among all supported database engines.`,
Action: runBackup,
Flags: []cli.Flag{
cli.StringFlag{
Name: "config, c",
Value: "custom/conf/app.ini",
Usage: "Custom configuration `FILE` path",
},
cli.StringFlag{
Name: "tempdir, t",
Value: os.TempDir(),
Usage: "Temporary directory `PATH`",
},
cli.StringFlag{
Name: "target",
Value: "./",
Usage: "Target directory `PATH` to save backup archive",
},
cli.BoolFlag{
Name: "verbose, v",
Usage: "Show process details",
},
cli.BoolTFlag{
Name: "db",
Usage: "Backup the database (default: true)",
},
cli.BoolTFlag{
Name: "repos",
Usage: "Backup repositories (default: true)",
},
cli.BoolTFlag{
Name: "data",
Usage: "Backup attachments and avatars (default: true)",
},
cli.BoolTFlag{
Name: "custom",
Usage: "Backup custom files (default: true)",
},
},
}
const (
archiveRootDir = "gitea-backup"
backupVersion = 1
)
func runBackup(c *cli.Context) error {
zip.Verbose = c.Bool("verbose")
if c.IsSet("config") {
setting.CustomConf = c.String("config")
}
setting.NewContext()
models.LoadConfigs()
if err := models.SetEngine(); err != nil {
return err
}
// Setup temp-dir
tmpDir := c.String("tempdir")
if !com.IsExist(tmpDir) {
log.Fatal(0, "'--tempdir' does not exist: %s", tmpDir)
}
rootDir, err := ioutil.TempDir(tmpDir, "gitea-backup-")
if err != nil {
log.Fatal(0, "Fail to create backup root directory '%s': %v", rootDir, err)
}
defer func(rootDir string) {
os.RemoveAll(rootDir)
}(rootDir)
log.Info("Backup root directory: %s", rootDir)
// Metadata
metaFile := path.Join(rootDir, "metadata.ini")
metadata := ini.Empty()
metadata.Section("").Key("VERSION").SetValue(fmt.Sprintf("%d", backupVersion))
metadata.Section("").Key("DATE_TIME").SetValue(time.Now().String())
metadata.Section("").Key("GITEA_VERSION").SetValue(setting.AppVer)
if err = metadata.SaveTo(metaFile); err != nil {
log.Fatal(0, "Fail to save metadata '%s': %v", metaFile, err)
}
// Create ZIP-file
archiveName := path.Join(c.String("target"), fmt.Sprintf("gitea-backup-%d.zip", time.Now().Unix()))
log.Info("Packing backup files to: %s", archiveName)
z, err := zip.Create(archiveName)
if err != nil {
log.Fatal(0, "Fail to create backup archive '%s': %v", archiveName, err)
}
defer func(archiveName string) {
if r := recover(); r != nil {
var ok bool
err, ok = r.(error)
if !ok {
err = fmt.Errorf("pkg: %v", r)
}
debug.PrintStack()
log.Info("Removing partial backup-file %s\n", archiveName)
os.Remove(archiveName)
log.Fatal(9, "%v\n", err)
}
}(archiveName)
// Add metadata-file
if err = z.AddFile(archiveRootDir+"/metadata.ini", metaFile); err != nil {
log.Fatal(0, "Fail to include 'metadata.ini': %v", err)
}
// Database
if c.Bool("db") {
log.Info("Backing up database")
dbDir := path.Join(rootDir, "db")
if err = models.DumpDatabase(dbDir); err != nil {
log.Fatal(0, "Fail to dump database: %v", err)
}
if err = z.AddDir(archiveRootDir+"/db", dbDir); err != nil {
log.Fatal(0, "Fail to include 'db': %v", err)
}
}
// Custom files
if c.Bool("custom") {
log.Info("Backing up custom files")
if err = z.AddDir(archiveRootDir+"/custom", setting.CustomPath); err != nil {
log.Fatal(0, "Fail to include 'custom': %v", err)
}
}
// Data files
if c.Bool("data") {
log.Info("Backing up attachments and avatars")
for _, dir := range []string{"attachments", "avatars"} {
dirPath := path.Join(setting.AppDataPath, dir)
if !com.IsDir(dirPath) {
continue
}
if err = z.AddDir(path.Join(archiveRootDir+"/data", dir), dirPath); err != nil {
log.Fatal(0, "Fail to include 'data': %v", err)
}
}
}
// Repositories
if c.Bool("repos") {
log.Info("Backing up repositories")
reposDump := path.Join(rootDir, "repositories.zip")
log.Info("Dumping repositories in '%s'", setting.RepoRootPath)
if err = zip.PackTo(setting.RepoRootPath, reposDump, true); err != nil {
log.Fatal(0, "Fail to dump repositories: %v", err)
}
log.Info("Repositories dumped to: %s", reposDump)
if err = z.AddFile(archiveRootDir+"/repositories.zip", reposDump); err != nil {
log.Fatal(0, "Fail to include 'repositories.zip': %v", err)
}
}
if err = z.Close(); err != nil {
log.Fatal(0, "Fail to save backup archive '%s': %v", archiveName, err)
}
os.RemoveAll(rootDir)
log.Info("Backup succeed! Archive is located at: %s", archiveName)
return nil
}

View File

@@ -8,23 +8,24 @@ package cmd
import (
"fmt"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"time"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/Unknwon/cae/zip"
"github.com/Unknwon/com"
"github.com/urfave/cli"
)
// CmdDump represents the available dump sub-command.
var CmdDump = cli.Command{
// Dump represents the available dump sub-command.
var Dump = cli.Command{
Name: "dump",
Usage: "Dump Gitea files and database",
Usage: "DEPRICATED! Dump Gitea files and database",
Description: `Dump compresses all related files and database into zip file.
It can be used for backup and capture Gitea server image to send to maintainer`,
Action: runDump,
@@ -58,65 +59,66 @@ func runDump(ctx *cli.Context) error {
setting.NewServices() // cannot access session settings otherwise
models.LoadConfigs()
err := models.SetEngine()
if err != nil {
log.Info("")
if err := models.SetEngine(); err != nil {
return err
}
tmpDir := ctx.String("tempdir")
if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
log.Fatalf("Path does not exist: %s", tmpDir)
log.Fatal(4, "Path does not exist: %s", tmpDir)
}
TmpWorkDir, err := ioutil.TempDir(tmpDir, "gitea-dump-")
if err != nil {
log.Fatalf("Failed to create tmp work directory: %v", err)
log.Fatal(4, "Failed to create tmp work directory: %v", err)
}
log.Printf("Creating tmp work dir: %s", TmpWorkDir)
log.Info("Creating tmp work dir: %s", TmpWorkDir)
reposDump := path.Join(TmpWorkDir, "gitea-repo.zip")
dbDump := path.Join(TmpWorkDir, "gitea-db.sql")
log.Printf("Dumping local repositories...%s", setting.RepoRootPath)
log.Info("Dumping local repositories...%s", setting.RepoRootPath)
zip.Verbose = ctx.Bool("verbose")
if err := zip.PackTo(setting.RepoRootPath, reposDump, true); err != nil {
log.Fatalf("Failed to dump local repositories: %v", err)
if err = zip.PackTo(setting.RepoRootPath, reposDump, true); err != nil {
log.Fatal(4, "Failed to dump local repositories: %v", err)
}
targetDBType := ctx.String("database")
if len(targetDBType) > 0 && targetDBType != models.DbCfg.Type {
log.Printf("Dumping database %s => %s...", models.DbCfg.Type, targetDBType)
log.Info("Dumping database %s => %s...", models.DbCfg.Type, targetDBType)
} else {
log.Printf("Dumping database...")
log.Info("Dumping database...")
}
if err := models.DumpDatabase(dbDump, targetDBType); err != nil {
log.Fatalf("Failed to dump database: %v", err)
if err = models.DumpDatabaseOld(dbDump, targetDBType); err != nil {
log.Fatal(4, "Failed to dump database: %v", err)
}
fileName := fmt.Sprintf("gitea-dump-%d.zip", time.Now().Unix())
log.Printf("Packing dump files...")
log.Info("Packing dump files...")
z, err := zip.Create(fileName)
if err != nil {
log.Fatalf("Failed to create %s: %v", fileName, err)
log.Fatal(4, "Failed to create %s: %v", fileName, err)
}
if err := z.AddFile("gitea-repo.zip", reposDump); err != nil {
log.Fatalf("Failed to include gitea-repo.zip: %v", err)
if err = z.AddFile("gitea-repo.zip", reposDump); err != nil {
log.Fatal(4, "Failed to include gitea-repo.zip: %v", err)
}
if err := z.AddFile("gitea-db.sql", dbDump); err != nil {
log.Fatalf("Failed to include gitea-db.sql: %v", err)
if err = z.AddFile("gitea-db.sql", dbDump); err != nil {
log.Fatal(4, "Failed to include gitea-db.sql: %v", err)
}
customDir, err := os.Stat(setting.CustomPath)
if err == nil && customDir.IsDir() {
if err := z.AddDir("custom", setting.CustomPath); err != nil {
log.Fatalf("Failed to include custom: %v", err)
if err = z.AddDir("custom", setting.CustomPath); err != nil {
log.Fatal(4, "Failed to include custom: %v", err)
}
} else {
log.Printf("Custom dir %s doesn't exist, skipped", setting.CustomPath)
log.Info("Custom dir %s doesn't exist, skipped", setting.CustomPath)
}
if com.IsExist(setting.AppDataPath) {
log.Printf("Packing data directory...%s", setting.AppDataPath)
log.Info("Packing data directory...%s", setting.AppDataPath)
var sessionAbsPath string
if setting.SessionConfig.Provider == "file" {
@@ -125,30 +127,30 @@ func runDump(ctx *cli.Context) error {
}
sessionAbsPath, _ = filepath.Abs(setting.SessionConfig.ProviderConfig)
}
if err := zipAddDirectoryExclude(z, "data", setting.AppDataPath, sessionAbsPath); err != nil {
log.Fatalf("Failed to include data directory: %v", err)
if err = zipAddDirectoryExclude(z, "data", setting.AppDataPath, sessionAbsPath); err != nil {
log.Fatal(4, "Failed to include data directory: %v", err)
}
}
if err := z.AddDir("log", setting.LogRootPath); err != nil {
log.Fatalf("Failed to include log: %v", err)
if err = z.AddDir("log", setting.LogRootPath); err != nil {
log.Fatal(4, "Failed to include log: %v", err)
}
if err = z.Close(); err != nil {
_ = os.Remove(fileName)
log.Fatalf("Failed to save %s: %v", fileName, err)
log.Fatal(4, "Failed to save %s: %v", fileName, err)
}
if err := os.Chmod(fileName, 0600); err != nil {
log.Printf("Can't change file access permissions mask to 0600: %v", err)
log.Info("Can't change file access permissions mask to 0600: %v", err)
}
log.Printf("Removing tmp work dir: %s", TmpWorkDir)
log.Info("Removing tmp work dir: %s", TmpWorkDir)
if err := os.RemoveAll(TmpWorkDir); err != nil {
log.Fatalf("Failed to remove %s: %v", TmpWorkDir, err)
log.Fatal(4, "Failed to remove %s: %v", TmpWorkDir, err)
}
log.Printf("Finish dumping in file %s", fileName)
log.Info("Finish dumping in file %s", fileName)
return nil
}

166
cmd/restore.go Normal file
View File

@@ -0,0 +1,166 @@
// Copyright 2017 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
"os"
"path"
"github.com/Unknwon/cae/zip"
"github.com/Unknwon/com"
"github.com/mcuadros/go-version"
"github.com/urfave/cli"
"gopkg.in/ini.v1"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)
// Restore a backup
var Restore = cli.Command{
Name: "restore",
Usage: "Restore files and database from backup",
Description: `Restore imports all related files and database from a backup archive.
The backup version must lower or equal to current Gitea version. You can also import
backup from other database engines, which is useful for database migrating.
If corresponding files or database tables are not presented in the archive, they will
be skipped and remian unchanged.`,
Action: runRestore,
Flags: []cli.Flag{
cli.StringFlag{
Name: "config, c",
Value: "custom/conf/app.ini",
Usage: "Custom configuration file path",
},
cli.StringFlag{
Name: "tempdir, t",
Value: os.TempDir(),
Usage: "Temporary directory path",
},
cli.StringFlag{
Name: "from",
Value: "",
Usage: "Path to backup archive",
},
cli.BoolFlag{
Name: "verbose, v",
Usage: "Show process details",
},
cli.BoolTFlag{
Name: "repos",
Usage: "Restore repositories (default: true)",
},
cli.BoolTFlag{
Name: "data",
Usage: "Restore attachments and avatars (default: true)",
},
cli.BoolTFlag{
Name: "custom",
Usage: "Restore custom files (default: true)",
},
cli.BoolTFlag{
Name: "db",
Usage: "Restore database (default: true)",
},
},
}
func runRestore(c *cli.Context) error {
zip.Verbose = c.Bool("verbose")
tmpDir := c.String("tempdir")
if !com.IsExist(tmpDir) {
log.Fatal(0, "'--tempdir' does not exist: %s", tmpDir)
}
log.Info("Restore backup from: %s", c.String("from"))
if err := zip.ExtractTo(c.String("from"), tmpDir); err != nil {
log.Fatal(0, "Fail to extract backup archive: %v", err)
}
archivePath := path.Join(tmpDir, archiveRootDir)
// Check backup version
metaFile := path.Join(archivePath, "metadata.ini")
if !com.IsExist(metaFile) {
log.Fatal(0, "File 'metadata.ini' is missing")
}
metadata, err := ini.Load(metaFile)
if err != nil {
log.Fatal(0, "Fail to load metadata '%s': %v", metaFile, err)
}
ver := metadata.Section("").Key("VERSION").MustInt(10000000)
if ver != backupVersion {
log.Fatal(0, "Current Backup version does not match the version in the backup: %d != %d", ver, backupVersion)
}
backupVersion := metadata.Section("").Key("GITEA_VERSION").MustString("999.0")
if version.Compare(setting.AppVer, backupVersion, "<") {
log.Fatal(0, "Current Gitea version is lower than backup version: %s < %s", setting.AppVer, backupVersion)
}
// If config file is not present in backup, user must set this file via flag.
// Otherwise, it's optional to set config file flag.
configFile := path.Join(archivePath, "custom/conf/app.ini")
if c.IsSet("config") {
setting.CustomConf = c.String("config")
} else if !com.IsExist(configFile) {
log.Fatal(0, "'--config' is not specified and custom config file is not found in backup")
} else {
setting.CustomConf = configFile
}
setting.NewContext()
models.LoadConfigs()
models.SetEngine()
// Database
if c.Bool("db") {
dbDir := path.Join(archivePath, "db")
if err = models.ImportDatabase(dbDir); err != nil {
log.Fatal(0, "Fail to import database: %v", err)
}
}
// Custom files
if c.Bool("custom") {
if com.IsExist(setting.CustomPath) {
if err = os.Rename(setting.CustomPath, setting.CustomPath+".bak"); err != nil {
log.Fatal(0, "Fail to backup current 'custom': %v", err)
}
}
if err = os.Rename(path.Join(archivePath, "custom"), setting.CustomPath); err != nil {
log.Fatal(0, "Fail to import 'custom': %v", err)
}
}
// Data files
if c.Bool("data") {
for _, dir := range []string{"attachments", "avatars"} {
dirPath := path.Join(setting.AppDataPath, dir)
if com.IsExist(dirPath) {
if err = os.Rename(dirPath, dirPath+".bak"); err != nil {
log.Fatal(0, "Fail to backup current 'data': %v", err)
}
}
if err = os.Rename(path.Join(archivePath, "data", dir), dirPath); err != nil {
log.Fatal(0, "Fail to import 'data': %v", err)
}
}
}
// Repositories
if c.Bool("repos") {
reposPath := path.Join(archivePath, "repositories.zip")
if !c.Bool("exclude-repos") && !c.Bool("database-only") && com.IsExist(reposPath) {
if err := zip.ExtractTo(reposPath, path.Dir(setting.RepoRootPath)); err != nil {
log.Fatal(0, "Fail to extract 'repositories.zip': %v", err)
}
}
}
os.RemoveAll(path.Join(tmpDir, archiveRootDir))
log.Info("Restore succeed!")
return nil
}

View File

@@ -16,7 +16,9 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"
"github.com/Unknwon/com"
"github.com/dgrijalva/jwt-go"
"github.com/urfave/cli"
@@ -105,7 +107,8 @@ func runServ(c *cli.Context) error {
}
if len(c.Args()) < 1 {
fail("Not enough arguments", "Not enough arguments")
cli.ShowSubcommandHelp(c)
return nil
}
cmd := os.Getenv("SSH_ORIGINAL_COMMAND")
@@ -123,8 +126,8 @@ func runServ(c *cli.Context) error {
fail("Unknown git command", "LFS authentication request over SSH denied, LFS support is disabled")
}
if strings.Contains(args, " ") {
argsSplit := strings.SplitN(args, " ", 2)
argsSplit := strings.Split(args, " ")
if len(argsSplit) >= 2 {
args = strings.TrimSpace(argsSplit[0])
lfsVerb = strings.TrimSpace(argsSplit[1])
}
@@ -179,8 +182,10 @@ func runServ(c *cli.Context) error {
if verb == lfsAuthenticateVerb {
if lfsVerb == "upload" {
requestedMode = models.AccessModeWrite
} else {
} else if lfsVerb == "download" {
requestedMode = models.AccessModeRead
} else {
fail("Unknown LFS verb", "Unkown lfs verb %s", lfsVerb)
}
}
@@ -232,7 +237,7 @@ func runServ(c *cli.Context) error {
fail("internal error", "Failed to get user by key ID(%d): %v", keyID, err)
}
mode, err := models.AccessLevel(user, repo)
mode, err := models.AccessLevel(user.ID, repo)
if err != nil {
fail("Internal error", "Failed to check access: %v", err)
} else if mode < requestedMode {
@@ -296,6 +301,12 @@ func runServ(c *cli.Context) error {
gitcmd = exec.Command(verb, repoPath)
}
if isWiki {
if err = repo.InitWiki(); err != nil {
fail("Internal error", "Failed to init wiki repo: %v", err)
}
}
os.Setenv(models.ProtectedBranchRepoID, fmt.Sprintf("%d", repo.ID))
gitcmd.Dir = setting.RepoRootPath
@@ -308,13 +319,7 @@ func runServ(c *cli.Context) error {
// Update user key activity.
if keyID > 0 {
key, err := models.GetPublicKeyByID(keyID)
if err != nil {
fail("Internal error", "GetPublicKeyById: %v", err)
}
key.Updated = time.Now()
if err = models.UpdatePublicKey(key); err != nil {
if err = private.UpdatePublicKeyUpdated(keyID); err != nil {
fail("Internal error", "UpdatePublicKey: %v", err)
}
}

View File

@@ -11,37 +11,15 @@ import (
"net/http/fcgi"
_ "net/http/pprof" // Used for debugging if enabled and a web server is running
"os"
"path"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/options"
"code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/routers/admin"
apiv1 "code.gitea.io/gitea/routers/api/v1"
"code.gitea.io/gitea/routers/dev"
"code.gitea.io/gitea/routers/org"
"code.gitea.io/gitea/routers/repo"
"code.gitea.io/gitea/routers/user"
"code.gitea.io/gitea/routers/routes"
"github.com/go-macaron/binding"
"github.com/go-macaron/cache"
"github.com/go-macaron/captcha"
"github.com/go-macaron/csrf"
"github.com/go-macaron/gzip"
"github.com/go-macaron/i18n"
"github.com/go-macaron/session"
"github.com/go-macaron/toolbox"
context2 "github.com/gorilla/context"
"github.com/urfave/cli"
macaron "gopkg.in/macaron.v1"
)
// CmdWeb represents the available web sub-command.
@@ -70,94 +48,6 @@ and it takes care of all the other things for you`,
},
}
// newMacaron initializes Macaron instance.
func newMacaron() *macaron.Macaron {
m := macaron.New()
if !setting.DisableRouterLog {
m.Use(macaron.Logger())
}
m.Use(macaron.Recovery())
if setting.EnableGzip {
m.Use(gzip.Gziper())
}
if setting.Protocol == setting.FCGI {
m.SetURLPrefix(setting.AppSubURL)
}
m.Use(public.Custom(
&public.Options{
SkipLogging: setting.DisableRouterLog,
},
))
m.Use(public.Static(
&public.Options{
Directory: path.Join(setting.StaticRootPath, "public"),
SkipLogging: setting.DisableRouterLog,
},
))
m.Use(macaron.Static(
setting.AvatarUploadPath,
macaron.StaticOptions{
Prefix: "avatars",
SkipLogging: setting.DisableRouterLog,
ETag: true,
},
))
m.Use(templates.Renderer())
models.InitMailRender(templates.Mailer())
localeNames, err := options.Dir("locale")
if err != nil {
log.Fatal(4, "Failed to list locale files: %v", err)
}
localFiles := make(map[string][]byte)
for _, name := range localeNames {
localFiles[name], err = options.Locale(name)
if err != nil {
log.Fatal(4, "Failed to load %s locale file. %v", name, err)
}
}
m.Use(i18n.I18n(i18n.Options{
SubURL: setting.AppSubURL,
Files: localFiles,
Langs: setting.Langs,
Names: setting.Names,
DefaultLang: "en-US",
Redirect: true,
}))
m.Use(cache.Cacher(cache.Options{
Adapter: setting.CacheAdapter,
AdapterConfig: setting.CacheConn,
Interval: setting.CacheInterval,
}))
m.Use(captcha.Captchaer(captcha.Options{
SubURL: setting.AppSubURL,
}))
m.Use(session.Sessioner(setting.SessionConfig))
m.Use(csrf.Csrfer(csrf.Options{
Secret: setting.SecretKey,
Cookie: setting.CSRFCookieName,
SetCookie: true,
Header: "X-Csrf-Token",
CookiePath: setting.AppSubURL,
}))
m.Use(toolbox.Toolboxer(m, toolbox.Options{
HealthCheckFuncs: []*toolbox.HealthCheckFuncDesc{
{
Desc: "Database connection",
Func: models.Ping,
},
},
}))
m.Use(context.Contexter())
return m
}
func runWeb(ctx *cli.Context) error {
if ctx.IsSet("config") {
setting.CustomConf = ctx.String("config")
@@ -169,482 +59,8 @@ func runWeb(ctx *cli.Context) error {
routers.GlobalInit()
m := newMacaron()
reqSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: true})
ignSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: setting.Service.RequireSignInView})
ignSignInAndCsrf := context.Toggle(&context.ToggleOptions{DisableCSRF: true})
reqSignOut := context.Toggle(&context.ToggleOptions{SignOutRequired: true})
bindIgnErr := binding.BindIgnErr
m.Use(user.GetNotificationCount)
// FIXME: not all routes need go through same middlewares.
// Especially some AJAX requests, we can reduce middleware number to improve performance.
// Routers.
m.Get("/", ignSignIn, routers.Home)
m.Group("/explore", func() {
m.Get("", func(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/explore/repos")
})
m.Get("/repos", routers.ExploreRepos)
m.Get("/users", routers.ExploreUsers)
m.Get("/organizations", routers.ExploreOrganizations)
}, ignSignIn)
m.Combo("/install", routers.InstallInit).Get(routers.Install).
Post(bindIgnErr(auth.InstallForm{}), routers.InstallPost)
m.Get("/^:type(issues|pulls)$", reqSignIn, user.Issues)
// ***** START: User *****
m.Group("/user", func() {
m.Get("/login", user.SignIn)
m.Post("/login", bindIgnErr(auth.SignInForm{}), user.SignInPost)
m.Get("/sign_up", user.SignUp)
m.Post("/sign_up", bindIgnErr(auth.RegisterForm{}), user.SignUpPost)
m.Get("/reset_password", user.ResetPasswd)
m.Post("/reset_password", user.ResetPasswdPost)
m.Group("/oauth2", func() {
m.Get("/:provider", user.SignInOAuth)
m.Get("/:provider/callback", user.SignInOAuthCallback)
})
m.Get("/link_account", user.LinkAccount)
m.Post("/link_account_signin", bindIgnErr(auth.SignInForm{}), user.LinkAccountPostSignIn)
m.Post("/link_account_signup", bindIgnErr(auth.RegisterForm{}), user.LinkAccountPostRegister)
m.Group("/two_factor", func() {
m.Get("", user.TwoFactor)
m.Post("", bindIgnErr(auth.TwoFactorAuthForm{}), user.TwoFactorPost)
m.Get("/scratch", user.TwoFactorScratch)
m.Post("/scratch", bindIgnErr(auth.TwoFactorScratchAuthForm{}), user.TwoFactorScratchPost)
})
}, reqSignOut)
m.Group("/user/settings", func() {
m.Get("", user.Settings)
m.Post("", bindIgnErr(auth.UpdateProfileForm{}), user.SettingsPost)
m.Combo("/avatar").Get(user.SettingsAvatar).
Post(binding.MultipartForm(auth.AvatarForm{}), user.SettingsAvatarPost)
m.Post("/avatar/delete", user.SettingsDeleteAvatar)
m.Combo("/email").Get(user.SettingsEmails).
Post(bindIgnErr(auth.AddEmailForm{}), user.SettingsEmailPost)
m.Post("/email/delete", user.DeleteEmail)
m.Get("/password", user.SettingsPassword)
m.Post("/password", bindIgnErr(auth.ChangePasswordForm{}), user.SettingsPasswordPost)
m.Combo("/ssh").Get(user.SettingsSSHKeys).
Post(bindIgnErr(auth.AddSSHKeyForm{}), user.SettingsSSHKeysPost)
m.Post("/ssh/delete", user.DeleteSSHKey)
m.Combo("/applications").Get(user.SettingsApplications).
Post(bindIgnErr(auth.NewAccessTokenForm{}), user.SettingsApplicationsPost)
m.Post("/applications/delete", user.SettingsDeleteApplication)
m.Route("/delete", "GET,POST", user.SettingsDelete)
m.Combo("/account_link").Get(user.SettingsAccountLinks).Post(user.SettingsDeleteAccountLink)
m.Group("/two_factor", func() {
m.Get("", user.SettingsTwoFactor)
m.Post("/regenerate_scratch", user.SettingsTwoFactorRegenerateScratch)
m.Post("/disable", user.SettingsTwoFactorDisable)
m.Get("/enroll", user.SettingsTwoFactorEnroll)
m.Post("/enroll", bindIgnErr(auth.TwoFactorAuthForm{}), user.SettingsTwoFactorEnrollPost)
})
}, reqSignIn, func(ctx *context.Context) {
ctx.Data["PageIsUserSettings"] = true
})
m.Group("/user", func() {
// r.Get("/feeds", binding.Bind(auth.FeedsForm{}), user.Feeds)
m.Any("/activate", user.Activate)
m.Any("/activate_email", user.ActivateEmail)
m.Get("/email2user", user.Email2User)
m.Get("/forget_password", user.ForgotPasswd)
m.Post("/forget_password", user.ForgotPasswdPost)
m.Get("/logout", user.SignOut)
})
// ***** END: User *****
adminReq := context.Toggle(&context.ToggleOptions{SignInRequired: true, AdminRequired: true})
// ***** START: Admin *****
m.Group("/admin", func() {
m.Get("", adminReq, admin.Dashboard)
m.Get("/config", admin.Config)
m.Post("/config/test_mail", admin.SendTestMail)
m.Get("/monitor", admin.Monitor)
m.Group("/users", func() {
m.Get("", admin.Users)
m.Combo("/new").Get(admin.NewUser).Post(bindIgnErr(auth.AdminCreateUserForm{}), admin.NewUserPost)
m.Combo("/:userid").Get(admin.EditUser).Post(bindIgnErr(auth.AdminEditUserForm{}), admin.EditUserPost)
m.Post("/:userid/delete", admin.DeleteUser)
})
m.Group("/orgs", func() {
m.Get("", admin.Organizations)
})
m.Group("/repos", func() {
m.Get("", admin.Repos)
m.Post("/delete", admin.DeleteRepo)
})
m.Group("/auths", func() {
m.Get("", admin.Authentications)
m.Combo("/new").Get(admin.NewAuthSource).Post(bindIgnErr(auth.AuthenticationForm{}), admin.NewAuthSourcePost)
m.Combo("/:authid").Get(admin.EditAuthSource).
Post(bindIgnErr(auth.AuthenticationForm{}), admin.EditAuthSourcePost)
m.Post("/:authid/delete", admin.DeleteAuthSource)
})
m.Group("/notices", func() {
m.Get("", admin.Notices)
m.Post("/delete", admin.DeleteNotices)
m.Get("/empty", admin.EmptyNotices)
})
}, adminReq)
// ***** END: Admin *****
m.Group("", func() {
m.Group("/:username", func() {
m.Get("", user.Profile)
m.Get("/followers", user.Followers)
m.Get("/following", user.Following)
})
m.Get("/attachments/:uuid", func(ctx *context.Context) {
attach, err := models.GetAttachmentByUUID(ctx.Params(":uuid"))
if err != nil {
if models.IsErrAttachmentNotExist(err) {
ctx.Error(404)
} else {
ctx.Handle(500, "GetAttachmentByUUID", err)
}
return
}
fr, err := os.Open(attach.LocalPath())
if err != nil {
ctx.Handle(500, "Open", err)
return
}
defer fr.Close()
if err = repo.ServeData(ctx, attach.Name, fr); err != nil {
ctx.Handle(500, "ServeData", err)
return
}
})
m.Post("/attachments", repo.UploadAttachment)
}, ignSignIn)
m.Group("/:username", func() {
m.Get("/action/:action", user.Action)
}, reqSignIn)
if macaron.Env == macaron.DEV {
m.Get("/template/*", dev.TemplatePreview)
}
reqRepoAdmin := context.RequireRepoAdmin()
reqRepoWriter := context.RequireRepoWriter()
// ***** START: Organization *****
m.Group("/org", func() {
m.Group("", func() {
m.Get("/create", org.Create)
m.Post("/create", bindIgnErr(auth.CreateOrgForm{}), org.CreatePost)
}, func(ctx *context.Context) {
if !ctx.User.CanCreateOrganization() {
ctx.NotFound()
}
})
m.Group("/:org", func() {
m.Get("/dashboard", user.Dashboard)
m.Get("/^:type(issues|pulls)$", user.Issues)
m.Get("/members", org.Members)
m.Get("/members/action/:action", org.MembersAction)
m.Get("/teams", org.Teams)
}, context.OrgAssignment(true))
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)
}, context.OrgAssignment(true, false, true))
m.Group("/:org", func() {
m.Get("/teams/new", org.NewTeam)
m.Post("/teams/new", bindIgnErr(auth.CreateTeamForm{}), org.NewTeamPost)
m.Get("/teams/:team/edit", org.EditTeam)
m.Post("/teams/:team/edit", bindIgnErr(auth.CreateTeamForm{}), org.EditTeamPost)
m.Post("/teams/:team/delete", org.DeleteTeam)
m.Group("/settings", func() {
m.Combo("").Get(org.Settings).
Post(bindIgnErr(auth.UpdateOrgSettingForm{}), org.SettingsPost)
m.Post("/avatar", binding.MultipartForm(auth.AvatarForm{}), org.SettingsAvatar)
m.Post("/avatar/delete", org.SettingsDeleteAvatar)
m.Group("/hooks", func() {
m.Get("", org.Webhooks)
m.Post("/delete", org.DeleteWebhook)
m.Get("/:type/new", repo.WebhooksNew)
m.Post("/gogs/new", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksNewPost)
m.Post("/slack/new", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksNewPost)
m.Get("/:id", repo.WebHooksEdit)
m.Post("/gogs/:id", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksEditPost)
m.Post("/slack/:id", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksEditPost)
})
m.Route("/delete", "GET,POST", org.SettingsDelete)
})
m.Route("/invitations/new", "GET,POST", org.Invitation)
}, context.OrgAssignment(true, true))
}, reqSignIn)
// ***** END: Organization *****
// ***** START: Repository *****
m.Group("/repo", func() {
m.Get("/create", repo.Create)
m.Post("/create", bindIgnErr(auth.CreateRepoForm{}), repo.CreatePost)
m.Get("/migrate", repo.Migrate)
m.Post("/migrate", bindIgnErr(auth.MigrateRepoForm{}), repo.MigratePost)
m.Combo("/fork/:repoid").Get(repo.Fork).
Post(bindIgnErr(auth.CreateRepoForm{}), repo.ForkPost)
}, reqSignIn)
m.Group("/:username/:reponame", func() {
m.Group("/settings", func() {
m.Combo("").Get(repo.Settings).
Post(bindIgnErr(auth.RepoSettingForm{}), repo.SettingsPost)
m.Group("/collaboration", func() {
m.Combo("").Get(repo.Collaboration).Post(repo.CollaborationPost)
m.Post("/access_mode", repo.ChangeCollaborationAccessMode)
m.Post("/delete", repo.DeleteCollaboration)
})
m.Group("/branches", func() {
m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost)
m.Post("/can_push", repo.ChangeProtectedBranch)
m.Post("/delete", repo.DeleteProtectedBranch)
})
m.Group("/hooks", func() {
m.Get("", repo.Webhooks)
m.Post("/delete", repo.DeleteWebhook)
m.Get("/:type/new", repo.WebhooksNew)
m.Post("/gogs/new", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksNewPost)
m.Post("/slack/new", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksNewPost)
m.Get("/:id", repo.WebHooksEdit)
m.Post("/:id/test", repo.TestWebhook)
m.Post("/gogs/:id", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksEditPost)
m.Post("/slack/:id", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksEditPost)
m.Group("/git", func() {
m.Get("", repo.GitHooks)
m.Combo("/:name").Get(repo.GitHooksEdit).
Post(repo.GitHooksEditPost)
}, context.GitHookService())
})
m.Group("/keys", func() {
m.Combo("").Get(repo.DeployKeys).
Post(bindIgnErr(auth.AddSSHKeyForm{}), repo.DeployKeysPost)
m.Post("/delete", repo.DeleteDeployKey)
})
}, func(ctx *context.Context) {
ctx.Data["PageIsSettings"] = true
}, context.UnitTypes())
}, reqSignIn, context.RepoAssignment(), reqRepoAdmin, context.RepoRef())
m.Get("/:username/:reponame/action/:action", reqSignIn, context.RepoAssignment(), repo.Action)
m.Group("/:username/:reponame", func() {
// FIXME: should use different URLs but mostly same logic for comments of issue and pull reuqest.
// So they can apply their own enable/disable logic on routers.
m.Group("/issues", func() {
m.Combo("/new", repo.MustEnableIssues).Get(context.RepoRef(), repo.NewIssue).
Post(bindIgnErr(auth.CreateIssueForm{}), repo.NewIssuePost)
m.Group("/:index", func() {
m.Post("/label", repo.UpdateIssueLabel)
m.Post("/milestone", repo.UpdateIssueMilestone)
m.Post("/assignee", repo.UpdateIssueAssignee)
}, reqRepoWriter)
m.Group("/:index", func() {
m.Post("/title", repo.UpdateIssueTitle)
m.Post("/content", repo.UpdateIssueContent)
m.Combo("/comments").Post(bindIgnErr(auth.CreateCommentForm{}), repo.NewComment)
})
})
m.Group("/comments/:id", func() {
m.Post("", repo.UpdateCommentContent)
m.Post("/delete", repo.DeleteComment)
})
m.Group("/labels", func() {
m.Post("/new", bindIgnErr(auth.CreateLabelForm{}), repo.NewLabel)
m.Post("/edit", bindIgnErr(auth.CreateLabelForm{}), repo.UpdateLabel)
m.Post("/delete", repo.DeleteLabel)
m.Post("/initialize", bindIgnErr(auth.InitializeLabelsForm{}), repo.InitializeLabels)
}, reqRepoWriter, context.RepoRef())
m.Group("/milestones", func() {
m.Combo("/new").Get(repo.NewMilestone).
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("/delete", repo.DeleteMilestone)
}, reqRepoWriter, context.RepoRef())
m.Group("/releases", func() {
m.Get("/new", repo.NewRelease)
m.Post("/new", bindIgnErr(auth.NewReleaseForm{}), repo.NewReleasePost)
m.Post("/delete", repo.DeleteRelease)
}, reqRepoWriter, context.RepoRef())
m.Group("/releases", func() {
m.Get("/edit/*", repo.EditRelease)
m.Post("/edit/*", bindIgnErr(auth.EditReleaseForm{}), repo.EditReleasePost)
}, reqRepoWriter, func(ctx *context.Context) {
var err error
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
if err != nil {
ctx.Handle(500, "GetBranchCommit", err)
return
}
ctx.Repo.CommitsCount, err = ctx.Repo.Commit.CommitsCount()
if err != nil {
ctx.Handle(500, "CommitsCount", err)
return
}
ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
})
m.Combo("/compare/*", repo.MustAllowPulls, repo.SetEditorconfigIfExists).
Get(repo.CompareAndPullRequest).
Post(bindIgnErr(auth.CreateIssueForm{}), repo.CompareAndPullRequestPost)
m.Group("", func() {
m.Combo("/_edit/*").Get(repo.EditFile).
Post(bindIgnErr(auth.EditRepoFileForm{}), repo.EditFilePost)
m.Combo("/_new/*").Get(repo.NewFile).
Post(bindIgnErr(auth.EditRepoFileForm{}), repo.NewFilePost)
m.Post("/_preview/*", bindIgnErr(auth.EditPreviewDiffForm{}), repo.DiffPreviewPost)
m.Combo("/_delete/*").Get(repo.DeleteFile).
Post(bindIgnErr(auth.DeleteRepoFileForm{}), repo.DeleteFilePost)
m.Group("", func() {
m.Combo("/_upload/*").Get(repo.UploadFile).
Post(bindIgnErr(auth.UploadRepoFileForm{}), repo.UploadFilePost)
m.Post("/upload-file", repo.UploadFileToServer)
m.Post("/upload-remove", bindIgnErr(auth.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer)
}, func(ctx *context.Context) {
if !setting.Repository.Upload.Enabled {
ctx.Handle(404, "", nil)
return
}
})
}, reqRepoWriter, context.RepoRef(), func(ctx *context.Context) {
if !ctx.Repo.Repository.CanEnableEditor() || ctx.Repo.IsViewCommit {
ctx.Handle(404, "", nil)
return
}
})
}, reqSignIn, context.RepoAssignment(), repo.MustBeNotBare, context.UnitTypes())
m.Group("/:username/:reponame", func() {
m.Group("", func() {
m.Get("/releases", repo.Releases)
m.Get("/^:type(issues|pulls)$", repo.RetrieveLabels, repo.Issues)
m.Get("/^:type(issues|pulls)$/:index", repo.ViewIssue)
m.Get("/labels/", repo.RetrieveLabels, repo.Labels)
m.Get("/milestones", repo.Milestones)
}, context.RepoRef())
// m.Get("/branches", repo.Branches)
m.Post("/branches/:name/delete", reqSignIn, reqRepoWriter, repo.DeleteBranchPost)
m.Group("/wiki", func() {
m.Get("/?:page", repo.Wiki)
m.Get("/_pages", repo.WikiPages)
m.Group("", func() {
m.Combo("/_new").Get(repo.NewWiki).
Post(bindIgnErr(auth.NewWikiForm{}), repo.NewWikiPost)
m.Combo("/:page/_edit").Get(repo.EditWiki).
Post(bindIgnErr(auth.NewWikiForm{}), repo.EditWikiPost)
m.Post("/:page/delete", repo.DeleteWikiPagePost)
}, reqSignIn, reqRepoWriter)
}, repo.MustEnableWiki, context.RepoRef())
m.Group("/wiki", func() {
m.Get("/raw/*", repo.WikiRaw)
m.Get("/*", repo.WikiRaw)
}, repo.MustEnableWiki)
m.Get("/archive/*", repo.Download)
m.Group("/pulls/:index", func() {
m.Get("/commits", context.RepoRef(), repo.ViewPullCommits)
m.Get("/files", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ViewPullFiles)
m.Post("/merge", reqRepoWriter, repo.MergePullRequest)
}, repo.MustAllowPulls)
m.Group("", func() {
m.Get("/src/*", repo.SetEditorconfigIfExists, repo.Home)
m.Get("/raw/*", repo.SingleDownload)
m.Get("/commits/*", repo.RefCommits)
m.Get("/graph", repo.Graph)
m.Get("/commit/:sha([a-f0-9]{7,40})$", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.Diff)
m.Get("/forks", repo.Forks)
}, context.RepoRef())
m.Get("/commit/:sha([a-f0-9]{7,40})\\.:ext(patch|diff)", repo.RawDiff)
m.Get("/compare/:before([a-z0-9]{40})\\.\\.\\.:after([a-z0-9]{40})", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.CompareDiff)
}, ignSignIn, context.RepoAssignment(), repo.MustBeNotBare, context.UnitTypes())
m.Group("/:username/:reponame", func() {
m.Get("/stars", repo.Stars)
m.Get("/watchers", repo.Watchers)
}, ignSignIn, context.RepoAssignment(), context.RepoRef(), context.UnitTypes())
m.Group("/:username", func() {
m.Group("/:reponame", func() {
m.Get("", repo.SetEditorconfigIfExists, repo.Home)
m.Get("\\.git$", repo.SetEditorconfigIfExists, repo.Home)
}, ignSignIn, context.RepoAssignment(true), context.RepoRef(), context.UnitTypes())
m.Group("/:reponame", func() {
m.Group("/info/lfs", func() {
m.Post("/objects/batch", lfs.BatchHandler)
m.Get("/objects/:oid/:filename", lfs.ObjectOidHandler)
m.Any("/objects/:oid", lfs.ObjectOidHandler)
m.Post("/objects", lfs.PostHandler)
}, ignSignInAndCsrf)
m.Any("/*", ignSignInAndCsrf, repo.HTTP)
m.Head("/tasks/trigger", repo.TriggerTask)
})
})
// ***** END: Repository *****
m.Group("/notifications", func() {
m.Get("", user.Notifications)
m.Post("/status", user.NotificationStatusPost)
}, reqSignIn)
m.Group("/api", func() {
apiv1.RegisterRoutes(m)
}, ignSignIn)
// robots.txt
m.Get("/robots.txt", func(ctx *context.Context) {
if setting.HasRobotsTxt {
ctx.ServeFileContent(path.Join(setting.CustomPath, "robots.txt"))
} else {
ctx.Error(404)
}
})
// Not found handler.
m.NotFound(routers.NotFound)
m := routes.NewMacaron()
routes.RegisterRoutes(m)
// Flag for port number in case first time run conflict.
if ctx.IsSet("port") {
@@ -677,7 +93,12 @@ func runWeb(ctx *cli.Context) error {
case setting.HTTPS:
err = runHTTPS(listenAddr, setting.CertFile, setting.KeyFile, context2.ClearHandler(m))
case setting.FCGI:
err = fcgi.Serve(nil, context2.ClearHandler(m))
listener, err := net.Listen("tcp", listenAddr)
if err != nil {
log.Fatal(4, "Failed to bind %s", listenAddr, err)
}
defer listener.Close()
err = fcgi.Serve(listener, context2.ClearHandler(m))
case setting.UnixSocket:
if err := os.Remove(listenAddr); err != nil && !os.IsNotExist(err) {
log.Fatal(4, "Failed to remove unix socket directory %s: %v", listenAddr, err)

View File

@@ -11,6 +11,7 @@ import (
"net/http"
"code.gitea.io/gitea/modules/log"
"github.com/facebookgo/grace/gracehttp"
)

51
conf/app.ini vendored
View File

@@ -74,6 +74,11 @@ ORG_PAGING_NUM = 50
; Number of repos that are showed in one page
REPO_PAGING_NUM = 15
[ui.meta]
AUTHOR = Gitea - Git with a cup of tea
DESCRIPTION = Gitea (Git with a cup of tea) is a painless self-hosted Git service written in Go
KEYWORDS = go,git,self-hosted,gitea
[markdown]
; Enable hard line break extension
ENABLE_HARD_LINE_BREAK = false
@@ -147,7 +152,7 @@ RSA = 2048
DSA = 1024
[database]
; Either "mysql", "postgres" or "sqlite3", it's your choice
; Either "mysql", "postgres", "mssql" or "sqlite3", it's your choice
DB_TYPE = mysql
HOST = 127.0.0.1:3306
NAME = gitea
@@ -182,6 +187,40 @@ MIN_PASSWORD_LENGTH = 6
; True when users are allowed to import local server paths
IMPORT_LOCAL_PATHS = false
[openid]
;
; OpenID is an open standard and decentralized authentication protocol.
; Your identity is the address of a webpage you provide, which describes
; how to prove you are in control of that page.
;
; For more info: https://en.wikipedia.org/wiki/OpenID
;
; Current implementation supports OpenID-2.0
;
; Tested to work providers at the time of writing:
; - Any GNUSocial node (your.hostname.tld/username)
; - Any SimpleID provider (http://simpleid.koinic.net)
; - http://openid.org.cn/
; - openid.stackexchange.com
; - login.launchpad.net
; - <username>.livejournal.com
;
; Whether to allow signin in via OpenID
ENABLE_OPENID_SIGNIN = true
; Whether to allow registering via OpenID
; Do not include to rely on DISABLE_REGISTRATION setting
;ENABLE_OPENID_SIGNUP = true
; Allowed URI patterns (POSIX regexp).
; Space separated.
; Only these would be allowed if non-blank.
; Example value: trusted.domain.org trusted.domain.net
WHITELISTED_URIS =
; Forbidden URI patterns (POSIX regexp).
; Space sepaated.
; Only used if WHITELISTED_URIS is blank.
; Example value: loadaverage.org/badguy stackexchange.com/.*spammer
BLACKLISTED_URIS =
[service]
ACTIVE_CODE_LIVE_MINUTES = 180
RESET_PASSWD_CODE_LIVE_MINUTES = 180
@@ -274,9 +313,9 @@ COOKIE_NAME = i_like_gitea
COOKIE_SECURE = false
; Enable set cookie, default is true
ENABLE_SET_COOKIE = true
; Session GC time interval, default is 86400
; Session GC time interval in seconds, default is 86400 (1 day)
GC_INTERVAL_TIME = 86400
; Session life time, default is 86400
; Session life time in seconds, default is 86400 (1 day)
SESSION_LIFE_TIME = 86400
[picture]
@@ -422,8 +461,10 @@ PULL = 300
GC = 60
[mirror]
; Default interval in hours between each check
DEFAULT_INTERVAL = 8
; Default interval as a duration between each check
DEFAULT_INTERVAL = 8h
; Min interval as a duration must be > 1m
MIN_INTERVAL = 10m
[api]
; Max number of items will response in a page

View File

@@ -15,7 +15,7 @@
# PATH should only include /usr/* if it runs after the mountnfs.sh script
PATH=/sbin:/usr/sbin:/bin:/usr/bin
DESC="Go Git Service"
DESC="Git with a cup of tea"
NAME=gitea
SERVICEVERBOSE=yes
PIDFILE=/var/run/$NAME.pid

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Ethan Koenig
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,2 @@
# gitea-integration
Gitea integration test environment

View File

@@ -0,0 +1 @@
ref: refs/heads/master

View File

@@ -0,0 +1,4 @@
[core]
repositoryformatversion = 0
filemode = true
bare = true

View File

@@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.

View File

@@ -0,0 +1,15 @@
#!/bin/sh
#
# An example hook script to check the commit log message taken by
# applypatch from an e-mail message.
#
# The hook should exit with non-zero status after issuing an
# appropriate message if it wants to stop the commit. The hook is
# allowed to edit the commit message file.
#
# To enable this hook, rename this file to "applypatch-msg".
. git-sh-setup
commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
:

View File

@@ -0,0 +1,24 @@
#!/bin/sh
#
# An example hook script to check the commit log message.
# Called by "git commit" with one argument, the name of the file
# that has the commit message. The hook should exit with non-zero
# status after issuing an appropriate message if it wants to stop the
# commit. The hook is allowed to edit the commit message file.
#
# To enable this hook, rename this file to "commit-msg".
# Uncomment the below to add a Signed-off-by line to the message.
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
# hook is more suited to it.
#
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
# This example catches duplicate Signed-off-by lines.
test "" = "$(grep '^Signed-off-by: ' "$1" |
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
echo >&2 Duplicate Signed-off-by lines.
exit 1
}

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
ORI_DIR=`pwd`
SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
cd "$ORI_DIR"
for i in `ls "$SHELL_FOLDER/post-receive.d"`; do
sh "$SHELL_FOLDER/post-receive.d/$i"
done

View File

@@ -0,0 +1,2 @@
#!/usr/bin/env bash
"$GITEA_ROOT/gitea" hook --config='integrations/app.ini' post-receive

View File

@@ -0,0 +1,8 @@
#!/bin/sh
#
# An example hook script to prepare a packed repository for use over
# dumb transports.
#
# To enable this hook, rename this file to "post-update".
exec git update-server-info

View File

@@ -0,0 +1,14 @@
#!/bin/sh
#
# An example hook script to verify what is about to be committed
# by applypatch from an e-mail message.
#
# The hook should exit with non-zero status after issuing an
# appropriate message if it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-applypatch".
. git-sh-setup
precommit="$(git rev-parse --git-path hooks/pre-commit)"
test -x "$precommit" && exec "$precommit" ${1+"$@"}
:

View File

@@ -0,0 +1,49 @@
#!/bin/sh
#
# An example hook script to verify what is about to be committed.
# Called by "git commit" with no arguments. The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-commit".
if git rev-parse --verify HEAD >/dev/null 2>&1
then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi
# If you want to allow non-ASCII filenames set this variable to true.
allownonascii=$(git config --bool hooks.allownonascii)
# Redirect output to stderr.
exec 1>&2
# Cross platform projects tend to avoid non-ASCII filenames; prevent
# them from being added to the repository. We exploit the fact that the
# printable range starts at the space character and ends with tilde.
if [ "$allownonascii" != "true" ] &&
# Note that the use of brackets around a tr range is ok here, (it's
# even required, for portability to Solaris 10's /usr/bin/tr), since
# the square bracket bytes happen to fall in the designated range.
test $(git diff --cached --name-only --diff-filter=A -z $against |
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
then
cat <<\EOF
Error: Attempt to add a non-ASCII file name.
This can cause problems if you want to work with people on other platforms.
To be portable it is advisable to rename the file.
If you know what you are doing you can disable this check using:
git config hooks.allownonascii true
EOF
exit 1
fi
# If there are whitespace errors, print the offending file names and fail.
exec git diff-index --check --cached $against --

View File

@@ -0,0 +1,53 @@
#!/bin/sh
# An example hook script to verify what is about to be pushed. Called by "git
# push" after it has checked the remote status, but before anything has been
# pushed. If this script exits with a non-zero status nothing will be pushed.
#
# This hook is called with the following parameters:
#
# $1 -- Name of the remote to which the push is being done
# $2 -- URL to which the push is being done
#
# If pushing without using a named remote those arguments will be equal.
#
# Information about the commits which are being pushed is supplied as lines to
# the standard input in the form:
#
# <local ref> <local sha1> <remote ref> <remote sha1>
#
# This sample shows how to prevent push of commits where the log message starts
# with "WIP" (work in progress).
remote="$1"
url="$2"
z40=0000000000000000000000000000000000000000
while read local_ref local_sha remote_ref remote_sha
do
if [ "$local_sha" = $z40 ]
then
# Handle delete
:
else
if [ "$remote_sha" = $z40 ]
then
# New branch, examine all commits
range="$local_sha"
else
# Update to existing branch, examine new commits
range="$remote_sha..$local_sha"
fi
# Check for WIP commit
commit=`git rev-list -n 1 --grep '^WIP' "$range"`
if [ -n "$commit" ]
then
echo >&2 "Found WIP commit in $local_ref, not pushing"
exit 1
fi
fi
done
exit 0

View File

@@ -0,0 +1,169 @@
#!/bin/sh
#
# Copyright (c) 2006, 2008 Junio C Hamano
#
# The "pre-rebase" hook is run just before "git rebase" starts doing
# its job, and can prevent the command from running by exiting with
# non-zero status.
#
# The hook is called with the following parameters:
#
# $1 -- the upstream the series was forked from.
# $2 -- the branch being rebased (or empty when rebasing the current branch).
#
# This sample shows how to prevent topic branches that are already
# merged to 'next' branch from getting rebased, because allowing it
# would result in rebasing already published history.
publish=next
basebranch="$1"
if test "$#" = 2
then
topic="refs/heads/$2"
else
topic=`git symbolic-ref HEAD` ||
exit 0 ;# we do not interrupt rebasing detached HEAD
fi
case "$topic" in
refs/heads/??/*)
;;
*)
exit 0 ;# we do not interrupt others.
;;
esac
# Now we are dealing with a topic branch being rebased
# on top of master. Is it OK to rebase it?
# Does the topic really exist?
git show-ref -q "$topic" || {
echo >&2 "No such branch $topic"
exit 1
}
# Is topic fully merged to master?
not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
if test -z "$not_in_master"
then
echo >&2 "$topic is fully merged to master; better remove it."
exit 1 ;# we could allow it, but there is no point.
fi
# Is topic ever merged to next? If so you should not be rebasing it.
only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
only_next_2=`git rev-list ^master ${publish} | sort`
if test "$only_next_1" = "$only_next_2"
then
not_in_topic=`git rev-list "^$topic" master`
if test -z "$not_in_topic"
then
echo >&2 "$topic is already up-to-date with master"
exit 1 ;# we could allow it, but there is no point.
else
exit 0
fi
else
not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
/usr/bin/perl -e '
my $topic = $ARGV[0];
my $msg = "* $topic has commits already merged to public branch:\n";
my (%not_in_next) = map {
/^([0-9a-f]+) /;
($1 => 1);
} split(/\n/, $ARGV[1]);
for my $elem (map {
/^([0-9a-f]+) (.*)$/;
[$1 => $2];
} split(/\n/, $ARGV[2])) {
if (!exists $not_in_next{$elem->[0]}) {
if ($msg) {
print STDERR $msg;
undef $msg;
}
print STDERR " $elem->[1]\n";
}
}
' "$topic" "$not_in_next" "$not_in_master"
exit 1
fi
<<\DOC_END
This sample hook safeguards topic branches that have been
published from being rewound.
The workflow assumed here is:
* Once a topic branch forks from "master", "master" is never
merged into it again (either directly or indirectly).
* Once a topic branch is fully cooked and merged into "master",
it is deleted. If you need to build on top of it to correct
earlier mistakes, a new topic branch is created by forking at
the tip of the "master". This is not strictly necessary, but
it makes it easier to keep your history simple.
* Whenever you need to test or publish your changes to topic
branches, merge them into "next" branch.
The script, being an example, hardcodes the publish branch name
to be "next", but it is trivial to make it configurable via
$GIT_DIR/config mechanism.
With this workflow, you would want to know:
(1) ... if a topic branch has ever been merged to "next". Young
topic branches can have stupid mistakes you would rather
clean up before publishing, and things that have not been
merged into other branches can be easily rebased without
affecting other people. But once it is published, you would
not want to rewind it.
(2) ... if a topic branch has been fully merged to "master".
Then you can delete it. More importantly, you should not
build on top of it -- other people may already want to
change things related to the topic as patches against your
"master", so if you need further changes, it is better to
fork the topic (perhaps with the same name) afresh from the
tip of "master".
Let's look at this example:
o---o---o---o---o---o---o---o---o---o "next"
/ / / /
/ a---a---b A / /
/ / / /
/ / c---c---c---c B /
/ / / \ /
/ / / b---b C \ /
/ / / / \ /
---o---o---o---o---o---o---o---o---o---o---o "master"
A, B and C are topic branches.
* A has one fix since it was merged up to "next".
* B has finished. It has been fully merged up to "master" and "next",
and is ready to be deleted.
* C has not merged to "next" at all.
We would want to allow C to be rebased, refuse A, and encourage
B to be deleted.
To compute (1):
git rev-list ^master ^topic next
git rev-list ^master next
if these match, topic has not merged in next at all.
To compute (2):
git rev-list master..topic
if this is empty, it is fully merged to "master".
DOC_END

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
ORI_DIR=`pwd`
SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
cd "$ORI_DIR"
for i in `ls "$SHELL_FOLDER/pre-receive.d"`; do
sh "$SHELL_FOLDER/pre-receive.d/$i"
done

View File

@@ -0,0 +1,2 @@
#!/usr/bin/env bash
"$GITEA_ROOT/gitea" hook --config='integrations/app.ini' pre-receive

View File

@@ -0,0 +1,36 @@
#!/bin/sh
#
# An example hook script to prepare the commit log message.
# Called by "git commit" with the name of the file that has the
# commit message, followed by the description of the commit
# message's source. The hook's purpose is to edit the commit
# message file. If the hook fails with a non-zero status,
# the commit is aborted.
#
# To enable this hook, rename this file to "prepare-commit-msg".
# This hook includes three examples. The first comments out the
# "Conflicts:" part of a merge commit.
#
# The second includes the output of "git diff --name-status -r"
# into the message, just before the "git status" output. It is
# commented because it doesn't cope with --amend or with squashed
# commits.
#
# The third example adds a Signed-off-by line to the message, that can
# still be edited. This is rarely a good idea.
case "$2,$3" in
merge,)
/usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;;
# ,|template,)
# /usr/bin/perl -i.bak -pe '
# print "\n" . `git diff --cached --name-status -r`
# if /^#/ && $first++ == 0' "$1" ;;
*) ;;
esac
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
ORI_DIR=`pwd`
SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
cd "$ORI_DIR"
for i in `ls "$SHELL_FOLDER/update.d"`; do
sh "$SHELL_FOLDER/update.d/$i" $1 $2 $3
done

View File

@@ -0,0 +1,2 @@
#!/usr/bin/env bash
"$GITEA_ROOT/gitea" hook --config='integrations/app.ini' update $1 $2 $3

View File

@@ -0,0 +1,128 @@
#!/bin/sh
#
# An example hook script to block unannotated tags from entering.
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
#
# To enable this hook, rename this file to "update".
#
# Config
# ------
# hooks.allowunannotated
# This boolean sets whether unannotated tags will be allowed into the
# repository. By default they won't be.
# hooks.allowdeletetag
# This boolean sets whether deleting tags will be allowed in the
# repository. By default they won't be.
# hooks.allowmodifytag
# This boolean sets whether a tag may be modified after creation. By default
# it won't be.
# hooks.allowdeletebranch
# This boolean sets whether deleting branches will be allowed in the
# repository. By default they won't be.
# hooks.denycreatebranch
# This boolean sets whether remotely creating branches will be denied
# in the repository. By default this is allowed.
#
# --- Command line
refname="$1"
oldrev="$2"
newrev="$3"
# --- Safety check
if [ -z "$GIT_DIR" ]; then
echo "Don't run this script from the command line." >&2
echo " (if you want, you could supply GIT_DIR then run" >&2
echo " $0 <ref> <oldrev> <newrev>)" >&2
exit 1
fi
if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
echo "usage: $0 <ref> <oldrev> <newrev>" >&2
exit 1
fi
# --- Config
allowunannotated=$(git config --bool hooks.allowunannotated)
allowdeletebranch=$(git config --bool hooks.allowdeletebranch)
denycreatebranch=$(git config --bool hooks.denycreatebranch)
allowdeletetag=$(git config --bool hooks.allowdeletetag)
allowmodifytag=$(git config --bool hooks.allowmodifytag)
# check for no description
projectdesc=$(sed -e '1q' "$GIT_DIR/description")
case "$projectdesc" in
"Unnamed repository"* | "")
echo "*** Project description file hasn't been set" >&2
exit 1
;;
esac
# --- Check types
# if $newrev is 0000...0000, it's a commit to delete a ref.
zero="0000000000000000000000000000000000000000"
if [ "$newrev" = "$zero" ]; then
newrev_type=delete
else
newrev_type=$(git cat-file -t $newrev)
fi
case "$refname","$newrev_type" in
refs/tags/*,commit)
# un-annotated tag
short_refname=${refname##refs/tags/}
if [ "$allowunannotated" != "true" ]; then
echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
exit 1
fi
;;
refs/tags/*,delete)
# delete tag
if [ "$allowdeletetag" != "true" ]; then
echo "*** Deleting a tag is not allowed in this repository" >&2
exit 1
fi
;;
refs/tags/*,tag)
# annotated tag
if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
then
echo "*** Tag '$refname' already exists." >&2
echo "*** Modifying a tag is not allowed in this repository." >&2
exit 1
fi
;;
refs/heads/*,commit)
# branch
if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
echo "*** Creating a branch is not allowed in this repository" >&2
exit 1
fi
;;
refs/heads/*,delete)
# delete branch
if [ "$allowdeletebranch" != "true" ]; then
echo "*** Deleting a branch is not allowed in this repository" >&2
exit 1
fi
;;
refs/remotes/*,commit)
# tracking branch
;;
refs/remotes/*,delete)
# delete tracking branch
if [ "$allowdeletebranch" != "true" ]; then
echo "*** Deleting a tracking branch is not allowed in this repository" >&2
exit 1
fi
;;
*)
# Anything else (is there anything else?)
echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
exit 1
;;
esac
# --- Finished
exit 0

View File

@@ -0,0 +1,6 @@
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~

View File

@@ -0,0 +1 @@
65f1bf27bc3bf70f64657658635e66094edbcb4d refs/heads/master

View File

@@ -0,0 +1 @@
65f1bf27bc3bf70f64657658635e66094edbcb4d

View File

@@ -1,91 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package integration
import (
"fmt"
"net/http"
"os"
"os/user"
"testing"
"time"
"code.gitea.io/gitea/integrations/internal/utils"
)
// The HTTP port listened by the Gitea server.
const ServerHTTPPort = "3001"
const _RetryLimit = 10
func makeSimpleSettings(user, port string) map[string][]string {
return map[string][]string{
"db_type": {"SQLite3"},
"db_host": {"localhost"},
"db_path": {"data/gitea.db"},
"app_name": {"Gitea: Git with a cup of tea"},
"repo_root_path": {"repositories"},
"run_user": {user},
"domain": {"localhost"},
"ssh_port": {"22"},
"http_port": {port},
"app_url": {"http://localhost:" + port},
"log_root_path": {"log"},
}
}
func install(t *utils.T) error {
var r *http.Response
var err error
for i := 1; i <= _RetryLimit; i++ {
r, err = http.Get("http://:" + ServerHTTPPort + "/")
if err == nil {
fmt.Fprintln(os.Stderr)
break
}
// Give the server some amount of time to warm up.
time.Sleep(100 * time.Millisecond)
fmt.Fprint(os.Stderr, ".")
}
if err != nil {
return err
}
defer r.Body.Close()
_user, err := user.Current()
if err != nil {
return err
}
settings := makeSimpleSettings(_user.Username, ServerHTTPPort)
r, err = http.PostForm("http://:"+ServerHTTPPort+"/install", settings)
if err != nil {
return err
}
defer r.Body.Close()
if r.StatusCode != http.StatusOK {
return fmt.Errorf("'/install': %s", r.Status)
}
return nil
}
func TestInstall(t *testing.T) {
conf := utils.Config{
Program: "../gitea",
WorkDir: "",
Args: []string{"web", "--port", ServerHTTPPort},
LogFile: os.Stderr,
}
if err := utils.New(t, &conf).RunTest(install); err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,100 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package integrations
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"testing"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/routers/routes"
"github.com/Unknwon/com"
"github.com/stretchr/testify/assert"
"gopkg.in/macaron.v1"
"gopkg.in/testfixtures.v2"
)
var mac *macaron.Macaron
func TestMain(m *testing.M) {
appIniPath := os.Getenv("GITEA_CONF")
if appIniPath == "" {
fmt.Println("Environment variable $GITEA_CONF not set")
os.Exit(1)
}
setting.CustomConf = appIniPath
routers.GlobalInit()
mac = routes.NewMacaron()
routes.RegisterRoutes(mac)
var helper testfixtures.Helper
if setting.UseMySQL {
helper = &testfixtures.MySQL{}
} else if setting.UsePostgreSQL {
helper = &testfixtures.PostgreSQL{}
} else if setting.UseSQLite3 {
helper = &testfixtures.SQLite{}
} else {
fmt.Println("Unsupported RDBMS for integration tests")
os.Exit(1)
}
err := models.InitFixtures(
helper,
"models/fixtures/",
)
if err != nil {
fmt.Printf("Error initializing test database: %v\n", err)
os.Exit(1)
}
os.Exit(m.Run())
}
func prepareTestEnv(t *testing.T) {
assert.NoError(t, models.LoadFixtures())
assert.NoError(t, os.RemoveAll("integrations/gitea-integration"))
assert.NoError(t, com.CopyDir("integrations/gitea-integration-meta", "integrations/gitea-integration"))
}
type TestResponseWriter struct {
HeaderCode int
Writer io.Writer
}
func (w *TestResponseWriter) Header() http.Header {
return make(map[string][]string)
}
func (w *TestResponseWriter) Write(b []byte) (int, error) {
return w.Writer.Write(b)
}
func (w *TestResponseWriter) WriteHeader(n int) {
w.HeaderCode = n
}
type TestResponse struct {
HeaderCode int
Body []byte
}
func MakeRequest(req *http.Request) *TestResponse {
buffer := bytes.NewBuffer(nil)
respWriter := &TestResponseWriter{
Writer: buffer,
}
mac.ServeHTTP(respWriter, req)
return &TestResponse{
HeaderCode: respWriter.HeaderCode,
Body: buffer.Bytes(),
}
}

View File

@@ -1,125 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package utils
import (
"errors"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"syscall"
"testing"
)
// T wraps testing.T and the configurations of the testing instance.
type T struct {
*testing.T
Config *Config
}
// New create an instance of T
func New(t *testing.T, c *Config) *T {
return &T{T: t, Config: c}
}
// Config Settings of the testing program
type Config struct {
// The executable path of the tested program.
Program string
// Working directory prepared for the tested program.
// If empty, a directory named with random suffixes is picked, and created under the platform-dependent default temporary directory.
// The directory will be removed when the test finishes.
WorkDir string
// Command-line arguments passed to the tested program.
Args []string
// Where to redirect the stdout/stderr to. For debugging purposes.
LogFile *os.File
}
func redirect(cmd *exec.Cmd, f *os.File) error {
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return err
}
go io.Copy(f, stdout)
go io.Copy(f, stderr)
return nil
}
// RunTest Helper function for setting up a running Gitea server for functional testing and then gracefully terminating it.
func (t *T) RunTest(tests ...func(*T) error) (err error) {
if t.Config.Program == "" {
return errors.New("Need input file")
}
path, err := filepath.Abs(t.Config.Program)
if err != nil {
return err
}
workdir := t.Config.WorkDir
if workdir == "" {
workdir, err = ioutil.TempDir(os.TempDir(), "gitea_tests-")
if err != nil {
return err
}
defer os.RemoveAll(workdir)
}
newpath := filepath.Join(workdir, filepath.Base(path))
if err := os.Symlink(path, newpath); err != nil {
return err
}
log.Printf("Starting the server: %s args:%s workdir:%s", newpath, t.Config.Args, workdir)
cmd := exec.Command(newpath, t.Config.Args...)
cmd.Dir = workdir
if t.Config.LogFile != nil && testing.Verbose() {
if err := redirect(cmd, t.Config.LogFile); err != nil {
return err
}
}
if err := cmd.Start(); err != nil {
return err
}
log.Println("Server started.")
defer func() {
// Do not early return. We have to call Wait anyway.
_ = cmd.Process.Signal(syscall.SIGTERM)
if _err := cmd.Wait(); _err != nil {
if _err.Error() != "signal: terminated" {
err = _err
return
}
}
log.Println("Server exited")
}()
for _, fn := range tests {
if err := fn(t); err != nil {
return err
}
}
// Note that the return value 'err' may be updated by the 'defer' statement before despite it's returning nil here.
return nil
}

56
integrations/mysql.ini Normal file
View File

@@ -0,0 +1,56 @@
APP_NAME = Gitea: Git with a cup of tea
RUN_MODE = prod
[database]
DB_TYPE = mysql
HOST = 127.0.0.1:3306
NAME = testgitea
USER = root
PASSWD =
SSL_MODE = disable
PATH = data/gitea.db
[repository]
ROOT = integrations/gitea-integration/gitea-repositories
[server]
SSH_DOMAIN = localhost
HTTP_PORT = 3000
ROOT_URL = http://localhost:3000/
DISABLE_SSH = false
SSH_PORT = 22
LFS_START_SERVER = false
OFFLINE_MODE = false
[mailer]
ENABLED = false
[service]
REGISTER_EMAIL_CONFIRM = false
ENABLE_NOTIFY_MAIL = false
DISABLE_REGISTRATION = false
ENABLE_CAPTCHA = false
REQUIRE_SIGNIN_VIEW = false
DEFAULT_KEEP_EMAIL_PRIVATE = false
NO_REPLY_ADDRESS = noreply.example.org
[picture]
DISABLE_GRAVATAR = false
ENABLE_FEDERATED_AVATAR = false
[session]
PROVIDER = file
[log]
MODE = console,file
[log.console]
LEVEL = Warn
[log.file]
LEVEL = Info
ROOT_PATH = log
[security]
INSTALL_LOCK = true
SECRET_KEY = 9pCviYTWSb

56
integrations/pgsql.ini Normal file
View File

@@ -0,0 +1,56 @@
APP_NAME = Gitea: Git with a cup of tea
RUN_MODE = prod
[database]
DB_TYPE = postgres
HOST = 127.0.0.1:5432
NAME = testgitea
USER = postgres
PASSWD = postgres
SSL_MODE = disable
PATH = data/gitea.db
[repository]
ROOT = integrations/gitea-integration/gitea-repositories
[server]
SSH_DOMAIN = localhost
HTTP_PORT = 3000
ROOT_URL = http://localhost:3000/
DISABLE_SSH = false
SSH_PORT = 22
LFS_START_SERVER = false
OFFLINE_MODE = false
[mailer]
ENABLED = false
[service]
REGISTER_EMAIL_CONFIRM = false
ENABLE_NOTIFY_MAIL = false
DISABLE_REGISTRATION = false
ENABLE_CAPTCHA = false
REQUIRE_SIGNIN_VIEW = false
DEFAULT_KEEP_EMAIL_PRIVATE = false
NO_REPLY_ADDRESS = noreply.example.org
[picture]
DISABLE_GRAVATAR = false
ENABLE_FEDERATED_AVATAR = false
[session]
PROVIDER = file
[log]
MODE = console,file
[log.console]
LEVEL = Warn
[log.file]
LEVEL = Info
ROOT_PATH = log
[security]
INSTALL_LOCK = true
SECRET_KEY = 9pCviYTWSb

View File

@@ -0,0 +1,41 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package integrations
import (
"bytes"
"net/http"
"net/url"
"testing"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
func TestSignup(t *testing.T) {
prepareTestEnv(t)
setting.Service.EnableCaptcha = false
req, err := http.NewRequest("POST", "/user/sign_up",
bytes.NewBufferString(url.Values{
"user_name": []string{"exampleUser"},
"email": []string{"exampleUser@example.com"},
"password": []string{"examplePassword"},
"retype": []string{"examplePassword"},
}.Encode()),
)
assert.NoError(t, err)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
resp := MakeRequest(req)
assert.EqualValues(t, http.StatusFound, resp.HeaderCode)
// should be able to view new user's page
req, err = http.NewRequest("GET", "/exampleUser", nil)
assert.NoError(t, err)
resp = MakeRequest(req)
assert.EqualValues(t, http.StatusOK, resp.HeaderCode)
}

58
integrations/sqlite.ini Normal file
View File

@@ -0,0 +1,58 @@
APP_NAME = Gitea: Git with a cup of tea
RUN_MODE = prod
[database]
DB_TYPE = sqlite3
HOST = 127.0.0.1:3306
NAME = testgitea
USER = gitea
PASSWD =
SSL_MODE = disable
PATH = :memory:
[repository]
ROOT = integrations/gitea-integration/gitea-repositories
[server]
SSH_DOMAIN = localhost
HTTP_PORT = 3000
ROOT_URL = http://localhost:3000/
DISABLE_SSH = false
SSH_PORT = 22
LFS_START_SERVER = false
OFFLINE_MODE = false
[mailer]
ENABLED = false
[service]
REGISTER_EMAIL_CONFIRM = false
ENABLE_NOTIFY_MAIL = false
DISABLE_REGISTRATION = false
ENABLE_CAPTCHA = false
REQUIRE_SIGNIN_VIEW = false
DEFAULT_KEEP_EMAIL_PRIVATE = false
NO_REPLY_ADDRESS = noreply.example.org
[picture]
DISABLE_GRAVATAR = false
ENABLE_FEDERATED_AVATAR = false
[session]
PROVIDER = file
[log]
MODE = console,file
[log.console]
LEVEL = Warn
[log.file]
LEVEL = Info
ROOT_PATH = log
[security]
INSTALL_LOCK = true
SECRET_KEY = 9pCviYTWSb
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTI3OTU5ODN9.OQkH5UmzID2XBdwQ9TAI6Jj2t1X-wElVTjbE7aoN4I8

View File

@@ -2,81 +2,32 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package integration
package integrations
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"code.gitea.io/gitea/integrations/internal/utils"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/sdk/gitea"
"github.com/stretchr/testify/assert"
)
func version(t *utils.T) error {
var err error
path, err := filepath.Abs(t.Config.Program)
if err != nil {
return err
}
cmd := exec.Command(path, "--version")
out, err := cmd.Output()
if err != nil {
return err
}
fields := strings.Fields(string(out))
if !strings.HasPrefix(string(out), "Gitea version") {
return fmt.Errorf("unexpected version string '%s' of the gitea executable", out)
}
expected := fields[2]
var r *http.Response
r, err = http.Get("http://:" + ServerHTTPPort + "/api/v1/version")
if err != nil {
return err
}
defer r.Body.Close()
if r.StatusCode != http.StatusOK {
return fmt.Errorf("'/api/v1/version': %s\n", r.Status)
}
var v gitea.ServerVersion
dec := json.NewDecoder(r.Body)
if err := dec.Decode(&v); err != nil {
return err
}
actual := v.Version
log.Printf("Actual: \"%s\" Expected: \"%s\"\n", actual, expected)
assert.Equal(t, expected, actual)
return nil
}
func TestVersion(t *testing.T) {
conf := utils.Config{
Program: "../gitea",
WorkDir: "",
Args: []string{"web", "--port", ServerHTTPPort},
LogFile: os.Stderr,
}
prepareTestEnv(t)
if err := utils.New(t, &conf).RunTest(install, version); err != nil {
t.Fatal(err)
}
setting.AppVer = "1.1.0+dev"
req, err := http.NewRequest("GET", "/api/v1/version", nil)
assert.NoError(t, err)
resp := MakeRequest(req)
var version gitea.ServerVersion
decoder := json.NewDecoder(bytes.NewBuffer(resp.Body))
assert.NoError(t, decoder.Decode(&version))
assert.EqualValues(t, http.StatusOK, resp.HeaderCode)
assert.Equal(t, setting.AppVer, string(version.Version))
}

30
integrations/view_test.go Normal file
View File

@@ -0,0 +1,30 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package integrations
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestViewRepo(t *testing.T) {
prepareTestEnv(t)
req, err := http.NewRequest("GET", "/user2/repo1", nil)
assert.NoError(t, err)
resp := MakeRequest(req)
assert.EqualValues(t, http.StatusOK, resp.HeaderCode)
}
func TestViewUser(t *testing.T) {
prepareTestEnv(t)
req, err := http.NewRequest("GET", "/user2", nil)
assert.NoError(t, err)
resp := MakeRequest(req)
assert.EqualValues(t, http.StatusOK, resp.HeaderCode)
}

View File

@@ -36,9 +36,11 @@ func main() {
cmd.CmdWeb,
cmd.CmdServ,
cmd.CmdHook,
cmd.CmdDump,
cmd.CmdCert,
cmd.CmdAdmin,
cmd.Backup,
cmd.Restore,
cmd.Dump,
}
app.Flags = append(app.Flags, []cli.Flag{}...)
err := app.Run(os.Args)

View File

@@ -59,21 +59,21 @@ type Access struct {
Mode AccessMode
}
func accessLevel(e Engine, user *User, repo *Repository) (AccessMode, error) {
func accessLevel(e Engine, userID int64, repo *Repository) (AccessMode, error) {
mode := AccessModeNone
if !repo.IsPrivate {
mode = AccessModeRead
}
if user == nil {
if userID == 0 {
return mode, nil
}
if user.ID == repo.OwnerID {
if userID == repo.OwnerID {
return AccessModeOwner, nil
}
a := &Access{UserID: user.ID, RepoID: repo.ID}
a := &Access{UserID: userID, RepoID: repo.ID}
if has, err := e.Get(a); !has || err != nil {
return mode, err
}
@@ -81,19 +81,19 @@ func accessLevel(e Engine, user *User, repo *Repository) (AccessMode, error) {
}
// AccessLevel returns the Access a user has to a repository. Will return NoneAccess if the
// user does not have access. User can be nil!
func AccessLevel(user *User, repo *Repository) (AccessMode, error) {
return accessLevel(x, user, repo)
// user does not have access.
func AccessLevel(userID int64, repo *Repository) (AccessMode, error) {
return accessLevel(x, userID, repo)
}
func hasAccess(e Engine, user *User, repo *Repository, testMode AccessMode) (bool, error) {
mode, err := accessLevel(e, user, repo)
func hasAccess(e Engine, userID int64, repo *Repository, testMode AccessMode) (bool, error) {
mode, err := accessLevel(e, userID, repo)
return testMode <= mode, err
}
// HasAccess returns true if someone has the request access level. User can be nil!
func HasAccess(user *User, repo *Repository, testMode AccessMode) (bool, error) {
return hasAccess(x, user, repo, testMode)
// HasAccess returns true if user has access to repo
func HasAccess(userID int64, repo *Repository, testMode AccessMode) (bool, error) {
return hasAccess(x, userID, repo, testMode)
}
type repoAccess struct {

View File

@@ -25,19 +25,19 @@ func TestAccessLevel(t *testing.T) {
repo1 := AssertExistsAndLoadBean(t, &Repository{OwnerID: 2, IsPrivate: false}).(*Repository)
repo2 := AssertExistsAndLoadBean(t, &Repository{OwnerID: 3, IsPrivate: true}).(*Repository)
level, err := AccessLevel(user1, repo1)
level, err := AccessLevel(user1.ID, repo1)
assert.NoError(t, err)
assert.Equal(t, AccessModeOwner, level)
level, err = AccessLevel(user1, repo2)
level, err = AccessLevel(user1.ID, repo2)
assert.NoError(t, err)
assert.Equal(t, AccessModeWrite, level)
level, err = AccessLevel(user2, repo1)
level, err = AccessLevel(user2.ID, repo1)
assert.NoError(t, err)
assert.Equal(t, AccessModeRead, level)
level, err = AccessLevel(user2, repo2)
level, err = AccessLevel(user2.ID, repo2)
assert.NoError(t, err)
assert.Equal(t, AccessModeNone, level)
}
@@ -51,19 +51,19 @@ func TestHasAccess(t *testing.T) {
repo2 := AssertExistsAndLoadBean(t, &Repository{OwnerID: 3, IsPrivate: true}).(*Repository)
for _, accessMode := range accessModes {
has, err := HasAccess(user1, repo1, accessMode)
has, err := HasAccess(user1.ID, repo1, accessMode)
assert.NoError(t, err)
assert.True(t, has)
has, err = HasAccess(user1, repo2, accessMode)
has, err = HasAccess(user1.ID, repo2, accessMode)
assert.NoError(t, err)
assert.Equal(t, accessMode <= AccessModeWrite, has)
has, err = HasAccess(user2, repo1, accessMode)
has, err = HasAccess(user2.ID, repo1, accessMode)
assert.NoError(t, err)
assert.Equal(t, accessMode <= AccessModeRead, has)
has, err = HasAccess(user2, repo2, accessMode)
has, err = HasAccess(user2.ID, repo2, accessMode)
assert.NoError(t, err)
assert.Equal(t, accessMode <= AccessModeNone, has)
}

View File

@@ -360,7 +360,7 @@ func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit) err
issue, err := GetIssueByRef(ref)
if err != nil {
if IsErrIssueNotExist(err) {
if IsErrIssueNotExist(err) || err == errMissingIssueNumber {
continue
}
return err
@@ -398,7 +398,7 @@ func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit) err
issue, err := GetIssueByRef(ref)
if err != nil {
if IsErrIssueNotExist(err) {
if IsErrIssueNotExist(err) || err == errMissingIssueNumber {
continue
}
return err
@@ -438,7 +438,7 @@ func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit) err
issue, err := GetIssueByRef(ref)
if err != nil {
if IsErrIssueNotExist(err) {
if IsErrIssueNotExist(err) || err == errMissingIssueNumber {
continue
}
return err

View File

@@ -6,16 +6,13 @@ package models
import (
"fmt"
"os"
"os/exec"
"strings"
"time"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
"github.com/Unknwon/com"
"github.com/go-xorm/xorm"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)
//NoticeType describes the notice type
@@ -79,19 +76,7 @@ func RemoveAllWithNotice(title, path string) {
}
func removeAllWithNotice(e Engine, title, path string) {
var err error
// workaround for Go not being able to remove read-only files/folders: https://github.com/golang/go/issues/9606
// this bug should be fixed on Go 1.7, so the workaround should be removed when Gogs don't support Go 1.6 anymore:
// https://github.com/golang/go/commit/2ffb3e5d905b5622204d199128dec06cefd57790
if setting.IsWindows {
// converting "/" to "\" in path on Windows
path = strings.Replace(path, "/", "\\", -1)
err = exec.Command("cmd", "/C", "rmdir", "/S", "/Q", path).Run()
} else {
err = os.RemoveAll(path)
}
if err != nil {
if err := util.RemoveAll(path); err != nil {
desc := fmt.Sprintf("%s [%s]: %v", title, path, err)
log.Warn(desc)
if err = createNotice(e, NoticeRepository, desc); err != nil {

View File

@@ -20,15 +20,15 @@ import (
// Attachment represent a attachment of issue/comment/release.
type Attachment struct {
ID int64 `xorm:"pk autoincr"`
UUID string `xorm:"uuid UNIQUE"`
IssueID int64 `xorm:"INDEX"`
CommentID int64
ReleaseID int64 `xorm:"INDEX"`
Name string
Created time.Time `xorm:"-"`
CreatedUnix int64
ID int64 `xorm:"pk autoincr"`
UUID string `xorm:"uuid UNIQUE"`
IssueID int64 `xorm:"INDEX"`
ReleaseID int64 `xorm:"INDEX"`
CommentID int64
Name string
DownloadCount int64 `xorm:"DEFAULT 0"`
Created time.Time `xorm:"-"`
CreatedUnix int64
}
// BeforeInsert is invoked from XORM before inserting an object of this type.
@@ -45,6 +45,19 @@ func (a *Attachment) AfterSet(colName string, _ xorm.Cell) {
}
}
// IncreaseDownloadCount is update download count + 1
func (a *Attachment) IncreaseDownloadCount() error {
sess := x.NewSession()
defer sessionRelease(sess)
// Update download count.
if _, err := sess.Exec("UPDATE `attachment` SET download_count=download_count+1 WHERE id=?", a.ID); err != nil {
return fmt.Errorf("increase attachment count: %v", err)
}
return nil
}
// AttachmentLocalPath returns where attachment is stored in local file
// system based on given UUID.
func AttachmentLocalPath(uuid string) string {

60
models/attachment_test.go Normal file
View File

@@ -0,0 +1,60 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIncreaseDownloadCount(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
attachment, err := GetAttachmentByUUID("1234567890")
assert.NoError(t, err)
assert.Equal(t, int64(0), attachment.DownloadCount)
// increase download count
err = attachment.IncreaseDownloadCount()
assert.NoError(t, err)
attachment, err = GetAttachmentByUUID("1234567890")
assert.NoError(t, err)
assert.Equal(t, int64(1), attachment.DownloadCount)
}
func TestGetByCommentOrIssueID(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
// count of attachments from issue ID
attachments, err := GetAttachmentsByIssueID(1)
assert.NoError(t, err)
assert.Equal(t, 2, len(attachments))
attachments, err = GetAttachmentsByCommentID(1)
assert.NoError(t, err)
assert.Equal(t, 2, len(attachments))
}
func TestDeleteAttachments(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
count, err := DeleteAttachmentsByIssue(4, false)
assert.NoError(t, err)
assert.Equal(t, 1, count)
count, err = DeleteAttachmentsByComment(2, false)
assert.NoError(t, err)
assert.Equal(t, 2, count)
err = DeleteAttachment(&Attachment{ID: 8}, false)
assert.NoError(t, err)
attachment, err := GetAttachmentByUUID("test-12345")
assert.Error(t, err)
assert.True(t, IsErrAttachmentNotExist(err))
assert.Nil(t, attachment)
}

View File

@@ -57,7 +57,7 @@ func GetProtectedBranchBy(repoID int64, BranchName string) (*ProtectedBranch, er
return rel, nil
}
// GetProtectedBranches get all protected btanches
// GetProtectedBranches get all protected branches
func (repo *Repository) GetProtectedBranches() ([]*ProtectedBranch, error) {
protectedBranches := make([]*ProtectedBranch, 0)
return protectedBranches, x.Find(&protectedBranches, &ProtectedBranch{RepoID: repo.ID})

View File

@@ -93,6 +93,21 @@ func (err ErrEmailAlreadyUsed) Error() string {
return fmt.Sprintf("e-mail has been used [email: %s]", err.Email)
}
// ErrOpenIDAlreadyUsed represents a "OpenIDAlreadyUsed" kind of error.
type ErrOpenIDAlreadyUsed struct {
OpenID string
}
// IsErrOpenIDAlreadyUsed checks if an error is a ErrOpenIDAlreadyUsed.
func IsErrOpenIDAlreadyUsed(err error) bool {
_, ok := err.(ErrOpenIDAlreadyUsed)
return ok
}
func (err ErrOpenIDAlreadyUsed) Error() string {
return fmt.Sprintf("OpenID has been used [oid: %s]", err.OpenID)
}
// ErrUserOwnRepos represents a "UserOwnRepos" kind of error.
type ErrUserOwnRepos struct {
UID int64
@@ -245,6 +260,84 @@ func (err ErrKeyNameAlreadyUsed) Error() string {
return fmt.Sprintf("public key already exists [owner_id: %d, name: %s]", err.OwnerID, err.Name)
}
// ErrGPGEmailNotFound represents a "ErrGPGEmailNotFound" kind of error.
type ErrGPGEmailNotFound struct {
Email string
}
// IsErrGPGEmailNotFound checks if an error is a ErrGPGEmailNotFound.
func IsErrGPGEmailNotFound(err error) bool {
_, ok := err.(ErrGPGEmailNotFound)
return ok
}
func (err ErrGPGEmailNotFound) Error() string {
return fmt.Sprintf("failed to found email or is not confirmed : %s", err.Email)
}
// ErrGPGKeyParsing represents a "ErrGPGKeyParsing" kind of error.
type ErrGPGKeyParsing struct {
ParseError error
}
// IsErrGPGKeyParsing checks if an error is a ErrGPGKeyParsing.
func IsErrGPGKeyParsing(err error) bool {
_, ok := err.(ErrGPGKeyParsing)
return ok
}
func (err ErrGPGKeyParsing) Error() string {
return fmt.Sprintf("failed to parse gpg key %s", err.ParseError.Error())
}
// ErrGPGKeyNotExist represents a "GPGKeyNotExist" kind of error.
type ErrGPGKeyNotExist struct {
ID int64
}
// IsErrGPGKeyNotExist checks if an error is a ErrGPGKeyNotExist.
func IsErrGPGKeyNotExist(err error) bool {
_, ok := err.(ErrGPGKeyNotExist)
return ok
}
func (err ErrGPGKeyNotExist) Error() string {
return fmt.Sprintf("public gpg key does not exist [id: %d]", err.ID)
}
// ErrGPGKeyIDAlreadyUsed represents a "GPGKeyIDAlreadyUsed" kind of error.
type ErrGPGKeyIDAlreadyUsed struct {
KeyID string
}
// IsErrGPGKeyIDAlreadyUsed checks if an error is a ErrKeyNameAlreadyUsed.
func IsErrGPGKeyIDAlreadyUsed(err error) bool {
_, ok := err.(ErrGPGKeyIDAlreadyUsed)
return ok
}
func (err ErrGPGKeyIDAlreadyUsed) Error() string {
return fmt.Sprintf("public key already exists [key_id: %s]", err.KeyID)
}
// ErrGPGKeyAccessDenied represents a "GPGKeyAccessDenied" kind of Error.
type ErrGPGKeyAccessDenied struct {
UserID int64
KeyID int64
}
// IsErrGPGKeyAccessDenied checks if an error is a ErrGPGKeyAccessDenied.
func IsErrGPGKeyAccessDenied(err error) bool {
_, ok := err.(ErrGPGKeyAccessDenied)
return ok
}
// Error pretty-prints an error of type ErrGPGKeyAccessDenied.
func (err ErrGPGKeyAccessDenied) Error() string {
return fmt.Sprintf("user does not have access to the key [user_id: %d, key_id: %d]",
err.UserID, err.KeyID)
}
// ErrKeyAccessDenied represents a "KeyAccessDenied" kind of error.
type ErrKeyAccessDenied struct {
UserID int64

View File

@@ -9,8 +9,8 @@ import "github.com/markbates/goth"
// ExternalLoginUser makes the connecting between some existing user and additional external login sources
type ExternalLoginUser struct {
ExternalID string `xorm:"NOT NULL"`
UserID int64 `xorm:"NOT NULL"`
LoginSourceID int64 `xorm:"NOT NULL"`
UserID int64 `xorm:"NOT NULL"`
LoginSourceID int64 `xorm:"NOT NULL"`
}
// GetExternalLogin checks if a externalID in loginSourceID scope already exists
@@ -67,8 +67,8 @@ func RemoveAccountLink(user *User, loginSourceID int64) (int64, error) {
return deleted, err
}
// RemoveAllAccountLinks will remove all external login sources for the given user
func RemoveAllAccountLinks(user *User) error {
_, err := x.Delete(&ExternalLoginUser{UserID: user.ID})
// removeAllAccountLinks will remove all external login sources for the given user
func removeAllAccountLinks(e Engine, user *User) error {
_, err := e.Delete(&ExternalLoginUser{UserID: user.ID})
return err
}

View File

@@ -0,0 +1,71 @@
-
id: 1
uuid: 1234567890
issue_id: 1
comment_id: 0
name: attach1
download_count: 0
created_unix: 946684800
-
id: 2
uuid: 1122334455
issue_id: 1
comment_id: 0
name: attach2
download_count: 1
created_unix: 946684800
-
id: 3
uuid: comment-id-1
issue_id: 2
comment_id: 1
name: attach1
download_count: 0
created_unix: 946684800
-
id: 4
uuid: comment-id-2
issue_id: 3
comment_id: 1
name: attach2
download_count: 1
created_unix: 946684800
-
id: 5
uuid: comment-id-3
issue_id: 4
comment_id: 0
name: attach1
download_count: 0
created_unix: 946684800
-
id: 6
uuid: comment-id-4
issue_id: 5
comment_id: 2
name: attach1
download_count: 0
created_unix: 946684800
-
id: 7
uuid: comment-id-5
issue_id: 5
comment_id: 2
name: attach1
download_count: 0
created_unix: 946684800
-
id: 8
uuid: test-12345
issue_id: 6
comment_id: 0
name: attach1
download_count: 0
created_unix: 946684800

View File

@@ -2,6 +2,18 @@
id: 1
type: 7 # label
poster_id: 2
issue_id: 1
issue_id: 1 # in repo_id 1
label_id: 1
content: "1"
-
id: 2
type: 0 # comment
poster_id: 3 # user not watching (see watch.yml)
issue_id: 1 # in repo_id 1
content: "good work!"
-
id: 3
type: 0 # comment
poster_id: 5 # user not watching (see watch.yml)
issue_id: 1 # in repo_id 1
content: "meh..."

View File

@@ -0,0 +1,54 @@
-
id: 1
index: 1
repo_id: 1
state: "pending"
sha: "1234123412341234123412341234123412341234"
target_url: https://example.com/builds/
description: My awesome CI-service
context: ci/awesomeness
creator_id: 2
-
id: 2
index: 2
repo_id: 1
state: "warning"
sha: "1234123412341234123412341234123412341234"
target_url: https://example.com/converage/
description: My awesome Coverage service
context: cov/awesomeness
creator_id: 2
-
id: 3
index: 3
repo_id: 1
state: "success"
sha: "1234123412341234123412341234123412341234"
target_url: https://example.com/converage/
description: My awesome Coverage service
context: cov/awesomeness
creator_id: 2
-
id: 4
index: 4
repo_id: 1
state: "failure"
sha: "1234123412341234123412341234123412341234"
target_url: https://example.com/builds/
description: My awesome CI-service
context: ci/awesomeness
creator_id: 2
-
id: 5
index: 5
repo_id: 1
state: "error"
sha: "1234123412341234123412341234123412341234"
target_url: https://example.com/builds/
description: My awesome deploy service
context: deploy/awesomeness
creator_id: 2

View File

@@ -0,0 +1,4 @@
-
id: 1
user_id: 4
follow_id: 2

View File

@@ -8,7 +8,7 @@
content: content1
is_closed: false
is_pull: false
num_comments: 0
num_comments: 2
created_unix: 946684800
updated_unix: 978307200

View File

@@ -0,0 +1,15 @@
-
id: 1
user_id: 1
issue_id: 1
is_watching: true
created_unix: 946684800
updated_unix: 946684800
-
id: 2
user_id: 2
issue_id: 2
is_watching: false
created_unix: 946684800
updated_unix: 946684800

View File

@@ -26,6 +26,7 @@
avatar_email: user2@example.com
num_repos: 2
num_stars: 2
num_followers: 1
-
id: 3
@@ -56,6 +57,7 @@
avatar: avatar4
avatar_email: user4@example.com
num_repos: 0
num_following: 1
-
id: 5
@@ -72,6 +74,7 @@
num_repos: 1
allow_create_organization: false
is_active: true
num_following: 0
-
id: 6

View File

@@ -0,0 +1,17 @@
-
id: 1
uid: 1
uri: https://user1.domain1.tld/
show: false
-
id: 2
uid: 1
uri: http://user1.domain2.tld/
show: true
-
id: 3
uid: 2
uri: https://domain1.tld/user2/
show: true

View File

@@ -287,7 +287,6 @@ func ParsePatch(maxLines, maxLineCharacters, maxFiles int, reader io.Reader) (*D
curFile.IsBin = true
curFile.IsLFSFile = true
curSection.Lines = nil
break
}
}
}

471
models/gpg_key.go Normal file
View File

@@ -0,0 +1,471 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import (
"bytes"
"container/list"
"crypto"
"encoding/base64"
"fmt"
"hash"
"io"
"strings"
"time"
"code.gitea.io/git"
"code.gitea.io/gitea/modules/log"
"github.com/go-xorm/xorm"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/armor"
"golang.org/x/crypto/openpgp/packet"
)
// GPGKey represents a GPG key.
type GPGKey struct {
ID int64 `xorm:"pk autoincr"`
OwnerID int64 `xorm:"INDEX NOT NULL"`
KeyID string `xorm:"INDEX CHAR(16) NOT NULL"`
PrimaryKeyID string `xorm:"CHAR(16)"`
Content string `xorm:"TEXT NOT NULL"`
Created time.Time `xorm:"-"`
CreatedUnix int64
Expired time.Time `xorm:"-"`
ExpiredUnix int64
Added time.Time `xorm:"-"`
AddedUnix int64
SubsKey []*GPGKey `xorm:"-"`
Emails []*EmailAddress
CanSign bool
CanEncryptComms bool
CanEncryptStorage bool
CanCertify bool
}
// BeforeInsert will be invoked by XORM before inserting a record
func (key *GPGKey) BeforeInsert() {
key.AddedUnix = time.Now().Unix()
key.ExpiredUnix = key.Expired.Unix()
key.CreatedUnix = key.Created.Unix()
}
// AfterSet is invoked from XORM after setting the value of a field of this object.
func (key *GPGKey) AfterSet(colName string, _ xorm.Cell) {
switch colName {
case "key_id":
x.Where("primary_key_id=?", key.KeyID).Find(&key.SubsKey)
case "added_unix":
key.Added = time.Unix(key.AddedUnix, 0).Local()
case "expired_unix":
key.Expired = time.Unix(key.ExpiredUnix, 0).Local()
case "created_unix":
key.Created = time.Unix(key.CreatedUnix, 0).Local()
}
}
// ListGPGKeys returns a list of public keys belongs to given user.
func ListGPGKeys(uid int64) ([]*GPGKey, error) {
keys := make([]*GPGKey, 0, 5)
return keys, x.Where("owner_id=? AND primary_key_id=''", uid).Find(&keys)
}
// GetGPGKeyByID returns public key by given ID.
func GetGPGKeyByID(keyID int64) (*GPGKey, error) {
key := new(GPGKey)
has, err := x.Id(keyID).Get(key)
if err != nil {
return nil, err
} else if !has {
return nil, ErrGPGKeyNotExist{keyID}
}
return key, nil
}
// checkArmoredGPGKeyString checks if the given key string is a valid GPG armored key.
// The function returns the actual public key on success
func checkArmoredGPGKeyString(content string) (*openpgp.Entity, error) {
list, err := openpgp.ReadArmoredKeyRing(strings.NewReader(content))
if err != nil {
return nil, ErrGPGKeyParsing{err}
}
return list[0], nil
}
//addGPGKey add key and subkeys to database
func addGPGKey(e Engine, key *GPGKey) (err error) {
// Save GPG primary key.
if _, err = e.Insert(key); err != nil {
return err
}
// Save GPG subs key.
for _, subkey := range key.SubsKey {
if err := addGPGKey(e, subkey); err != nil {
return err
}
}
return nil
}
// AddGPGKey adds new public key to database.
func AddGPGKey(ownerID int64, content string) (*GPGKey, error) {
ekey, err := checkArmoredGPGKeyString(content)
if err != nil {
return nil, err
}
// Key ID cannot be duplicated.
has, err := x.Where("key_id=?", ekey.PrimaryKey.KeyIdString()).
Get(new(GPGKey))
if err != nil {
return nil, err
} else if has {
return nil, ErrGPGKeyIDAlreadyUsed{ekey.PrimaryKey.KeyIdString()}
}
//Get DB session
sess := x.NewSession()
defer sessionRelease(sess)
if err = sess.Begin(); err != nil {
return nil, err
}
key, err := parseGPGKey(ownerID, ekey)
if err != nil {
return nil, err
}
if err = addGPGKey(sess, key); err != nil {
return nil, err
}
return key, sess.Commit()
}
//base64EncPubKey encode public kay content to base 64
func base64EncPubKey(pubkey *packet.PublicKey) (string, error) {
var w bytes.Buffer
err := pubkey.Serialize(&w)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(w.Bytes()), nil
}
//parseSubGPGKey parse a sub Key
func parseSubGPGKey(ownerID int64, primaryID string, pubkey *packet.PublicKey, expiry time.Time) (*GPGKey, error) {
content, err := base64EncPubKey(pubkey)
if err != nil {
return nil, err
}
return &GPGKey{
OwnerID: ownerID,
KeyID: pubkey.KeyIdString(),
PrimaryKeyID: primaryID,
Content: content,
Created: pubkey.CreationTime,
Expired: expiry,
CanSign: pubkey.CanSign(),
CanEncryptComms: pubkey.PubKeyAlgo.CanEncrypt(),
CanEncryptStorage: pubkey.PubKeyAlgo.CanEncrypt(),
CanCertify: pubkey.PubKeyAlgo.CanSign(),
}, nil
}
//parseGPGKey parse a PrimaryKey entity (primary key + subs keys + self-signature)
func parseGPGKey(ownerID int64, e *openpgp.Entity) (*GPGKey, error) {
pubkey := e.PrimaryKey
//Extract self-sign for expire date based on : https://github.com/golang/crypto/blob/master/openpgp/keys.go#L165
var selfSig *packet.Signature
for _, ident := range e.Identities {
if selfSig == nil {
selfSig = ident.SelfSignature
} else if ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId {
selfSig = ident.SelfSignature
break
}
}
expiry := time.Time{}
if selfSig.KeyLifetimeSecs != nil {
expiry = selfSig.CreationTime.Add(time.Duration(*selfSig.KeyLifetimeSecs) * time.Second)
}
//Parse Subkeys
subkeys := make([]*GPGKey, len(e.Subkeys))
for i, k := range e.Subkeys {
subs, err := parseSubGPGKey(ownerID, pubkey.KeyIdString(), k.PublicKey, expiry)
if err != nil {
return nil, err
}
subkeys[i] = subs
}
//Check emails
userEmails, err := GetEmailAddresses(ownerID)
if err != nil {
return nil, err
}
emails := make([]*EmailAddress, len(e.Identities))
n := 0
for _, ident := range e.Identities {
for _, e := range userEmails {
if e.Email == ident.UserId.Email && e.IsActivated {
emails[n] = e
break
}
}
if emails[n] == nil {
return nil, ErrGPGEmailNotFound{ident.UserId.Email}
}
n++
}
content, err := base64EncPubKey(pubkey)
if err != nil {
return nil, err
}
return &GPGKey{
OwnerID: ownerID,
KeyID: pubkey.KeyIdString(),
PrimaryKeyID: "",
Content: content,
Created: pubkey.CreationTime,
Expired: expiry,
Emails: emails,
SubsKey: subkeys,
CanSign: pubkey.CanSign(),
CanEncryptComms: pubkey.PubKeyAlgo.CanEncrypt(),
CanEncryptStorage: pubkey.PubKeyAlgo.CanEncrypt(),
CanCertify: pubkey.PubKeyAlgo.CanSign(),
}, nil
}
// deleteGPGKey does the actual key deletion
func deleteGPGKey(e *xorm.Session, keyID string) (int64, error) {
if keyID == "" {
return 0, fmt.Errorf("empty KeyId forbidden") //Should never happen but just to be sure
}
return e.Where("key_id=?", keyID).Or("primary_key_id=?", keyID).Delete(new(GPGKey))
}
// DeleteGPGKey deletes GPG key information in database.
func DeleteGPGKey(doer *User, id int64) (err error) {
key, err := GetGPGKeyByID(id)
if err != nil {
if IsErrGPGKeyNotExist(err) {
return nil
}
return fmt.Errorf("GetPublicKeyByID: %v", err)
}
// Check if user has access to delete this key.
if !doer.IsAdmin && doer.ID != key.OwnerID {
return ErrGPGKeyAccessDenied{doer.ID, key.ID}
}
sess := x.NewSession()
defer sessionRelease(sess)
if err = sess.Begin(); err != nil {
return err
}
if _, err = deleteGPGKey(sess, key.KeyID); err != nil {
return err
}
if err = sess.Commit(); err != nil {
return err
}
return nil
}
// CommitVerification represents a commit validation of signature
type CommitVerification struct {
Verified bool
Reason string
SigningUser *User
SigningKey *GPGKey
}
// SignCommit represents a commit with validation of signature.
type SignCommit struct {
Verification *CommitVerification
*UserCommit
}
func readerFromBase64(s string) (io.Reader, error) {
bs, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return nil, err
}
return bytes.NewBuffer(bs), nil
}
func populateHash(hashFunc crypto.Hash, msg []byte) (hash.Hash, error) {
h := hashFunc.New()
if _, err := h.Write(msg); err != nil {
return nil, err
}
return h, nil
}
// readArmoredSign read an armored signature block with the given type. https://sourcegraph.com/github.com/golang/crypto/-/blob/openpgp/read.go#L24:6-24:17
func readArmoredSign(r io.Reader) (body io.Reader, err error) {
block, err := armor.Decode(r)
if err != nil {
return
}
if block.Type != openpgp.SignatureType {
return nil, fmt.Errorf("expected '" + openpgp.SignatureType + "', got: " + block.Type)
}
return block.Body, nil
}
func extractSignature(s string) (*packet.Signature, error) {
r, err := readArmoredSign(strings.NewReader(s))
if err != nil {
return nil, fmt.Errorf("Failed to read signature armor")
}
p, err := packet.Read(r)
if err != nil {
return nil, fmt.Errorf("Failed to read signature packet")
}
sig, ok := p.(*packet.Signature)
if !ok {
return nil, fmt.Errorf("Packet is not a signature")
}
return sig, nil
}
func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error {
//Check if key can sign
if !k.CanSign {
return fmt.Errorf("key can not sign")
}
//Decode key
b, err := readerFromBase64(k.Content)
if err != nil {
return err
}
//Read key
p, err := packet.Read(b)
if err != nil {
return err
}
//Check type
pkey, ok := p.(*packet.PublicKey)
if !ok {
return fmt.Errorf("key is not a public key")
}
return pkey.VerifySignature(h, s)
}
// ParseCommitWithSignature check if signature is good against keystore.
func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
if c.Signature != nil {
//Parsing signature
sig, err := extractSignature(c.Signature.Signature)
if err != nil { //Skipping failed to extract sign
log.Error(3, "SignatureRead err: %v", err)
return &CommitVerification{
Verified: false,
Reason: "gpg.error.extract_sign",
}
}
//Find Committer account
committer, err := GetUserByEmail(c.Committer.Email)
if err != nil { //Skipping not user for commiter
log.Error(3, "NoCommitterAccount: %v", err)
return &CommitVerification{
Verified: false,
Reason: "gpg.error.no_committer_account",
}
}
keys, err := ListGPGKeys(committer.ID)
if err != nil || len(keys) == 0 { //Skipping failed to get gpg keys of user
log.Error(3, "ListGPGKeys: %v", err)
return &CommitVerification{
Verified: false,
Reason: "gpg.error.failed_retrieval_gpg_keys",
}
}
for _, k := range keys {
//Generating hash of commit
hash, err := populateHash(sig.Hash, []byte(c.Signature.Payload))
if err != nil { //Skipping ailed to generate hash
log.Error(3, "PopulateHash: %v", err)
return &CommitVerification{
Verified: false,
Reason: "gpg.error.generate_hash",
}
}
//We get PK
if err := verifySign(sig, hash, k); err == nil {
return &CommitVerification{ //Everything is ok
Verified: true,
Reason: fmt.Sprintf("%s <%s> / %s", c.Committer.Name, c.Committer.Email, k.KeyID),
SigningUser: committer,
SigningKey: k,
}
}
//And test also SubsKey
for _, sk := range k.SubsKey {
//Generating hash of commit
hash, err := populateHash(sig.Hash, []byte(c.Signature.Payload))
if err != nil { //Skipping ailed to generate hash
log.Error(3, "PopulateHash: %v", err)
return &CommitVerification{
Verified: false,
Reason: "gpg.error.generate_hash",
}
}
if err := verifySign(sig, hash, sk); err == nil {
return &CommitVerification{ //Everything is ok
Verified: true,
Reason: fmt.Sprintf("%s <%s> / %s", c.Committer.Name, c.Committer.Email, sk.KeyID),
SigningUser: committer,
SigningKey: sk,
}
}
}
}
return &CommitVerification{ //Default at this stage
Verified: false,
Reason: "gpg.error.no_gpg_keys_found",
}
}
return &CommitVerification{
Verified: false, //Default value
Reason: "gpg.error.not_signed_commit", //Default value
}
}
// ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys.
func ParseCommitsWithSignature(oldCommits *list.List) *list.List {
var (
newCommits = list.New()
e = oldCommits.Front()
)
for e != nil {
c := e.Value.(UserCommit)
newCommits.PushBack(SignCommit{
UserCommit: &c,
Verification: ParseCommitWithSignature(c.Commit),
})
e = e.Next()
}
return newCommits
}

164
models/gpg_key_test.go Normal file
View File

@@ -0,0 +1,164 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCheckArmoredGPGKeyString(t *testing.T) {
testGPGArmor := `-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBFh91QoBCADciaDd7aqegYkn4ZIG7J0p1CRwpqMGjxFroJEMg6M1ZiuEVTRv
z49P4kcr1+98NvFmcNc+x5uJgvPCwr/N8ZW5nqBUs2yrklbFF4MeQomyZJJegP8m
/dsRT3BwIT8YMUtJuCj0iqD9vuKYfjrztcMgC1sYwcE9E9OlA0pWBvUdU2i0TIB1
vOq6slWGvHHa5l5gPfm09idlVxfH5+I+L1uIMx5ovbiVVU5x2f1AR1T18f0t2TVN
0agFTyuoYE1ATmvJHmMcsfgM1Gpd9hIlr9vlupT2kKTPoNzVzsJsOU6Ku/Lf/bac
mF+TfSbRCtmG7dkYZ4metLj7zG/WkW8IvJARABEBAAG0HUFudG9pbmUgR0lSQVJE
IDxzYXBrQHNhcGsuZnI+iQFUBBMBCAA+FiEEEIOwJg/1vpF1itJ4roJVuKDYKOQF
Alh91QoCGwMFCQPCZwAFCwkIBwIGFQgJCgsCBBYCAwECHgECF4AACgkQroJVuKDY
KORreggAlIkC2QjHP5tb7b0+LksB2JMXdY+UzZBcJxtNmvA7gNQaGvWRrhrbePpa
MKDP+3A4BPDBsWFbbB7N56vQ5tROpmWbNKuFOVER4S1bj0JZV0E+xkDLqt9QwQtQ
ojd7oIZJwDUwdud1PvCza2mjgBqqiFE+twbc3i9xjciCGspMniUul1eQYLxRJ0w+
sbvSOUnujnq5ByMSz9ij00O6aiPfNQS5oB5AALfpjYZDvWAAljLVrtmlQJWZ6dZo
T/YNwsW2dECPuti8+Nmu5FxPGDTXxdbnRaeJTQ3T6q1oUVAv7yTXBx5NXfXkMa5i
iEayQIH8Joq5Ev5ja/lRGQQhArMQ2bkBDQRYfdUKAQgAv7B3coLSrOQbuTZSlgWE
QeT+7DWbmqE1LAQA1pQPcUPXLBUVd60amZJxF9nzUYcY83ylDi0gUNJS+DJGOXpT
pzX2IOuOMGbtUSeKwg5s9O4SUO7f2yCc3RGaegER5zgESxelmOXG+b/hoNt7JbdU
JtxcnLr91Jw2PBO/Xf0ZKJ01CQG2Yzdrrj6jnrHyx94seHy0i6xH1o0OuvfVMLfN
/Vbb/ZHh6ym2wHNqRX62b0VAbchcJXX/MEehXGknKTkO6dDUd+mhRgWMf9ZGRFWx
ag4qALimkf1FXtAyD0vxFYeyoWUQzrOvUsm2BxIN/986R08fhkBQnp5nz07mrU02
cQARAQABiQE8BBgBCAAmFiEEEIOwJg/1vpF1itJ4roJVuKDYKOQFAlh91QoCGwwF
CQPCZwAACgkQroJVuKDYKOT32wf/UZqMdPn5OhyhffFzjQx7wolrf92WkF2JkxtH
6c3Htjlt/p5RhtKEeErSrNAxB4pqB7dznHaJXiOdWEZtRVXXjlNHjrokGTesqtKk
lHWtK62/MuyLdr+FdCl68F3ewuT2iu/MDv+D4HPqA47zma9xVgZ9ZNwJOpv3fCOo
RfY66UjGEnfgYifgtI5S84/mp2jaSc9UNvlZB6RSf8cfbJUL74kS2lq+xzSlf0yP
Av844q/BfRuVsJsK1NDNG09LC30B0l3LKBqlrRmRTUMHtgchdX2dY+p7GPOoSzlR
MkM/fdpyc2hY7Dl/+qFmN5MG5yGmMpQcX+RNNR222ibNC1D3wg==
=i9b7
-----END PGP PUBLIC KEY BLOCK-----`
key, err := checkArmoredGPGKeyString(testGPGArmor)
assert.Nil(t, err, "Could not parse a valid GPG armored key", key)
//TODO verify value of key
}
func TestExtractSignature(t *testing.T) {
testGPGArmor := `-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBFh91QoBCADciaDd7aqegYkn4ZIG7J0p1CRwpqMGjxFroJEMg6M1ZiuEVTRv
z49P4kcr1+98NvFmcNc+x5uJgvPCwr/N8ZW5nqBUs2yrklbFF4MeQomyZJJegP8m
/dsRT3BwIT8YMUtJuCj0iqD9vuKYfjrztcMgC1sYwcE9E9OlA0pWBvUdU2i0TIB1
vOq6slWGvHHa5l5gPfm09idlVxfH5+I+L1uIMx5ovbiVVU5x2f1AR1T18f0t2TVN
0agFTyuoYE1ATmvJHmMcsfgM1Gpd9hIlr9vlupT2kKTPoNzVzsJsOU6Ku/Lf/bac
mF+TfSbRCtmG7dkYZ4metLj7zG/WkW8IvJARABEBAAG0HUFudG9pbmUgR0lSQVJE
IDxzYXBrQHNhcGsuZnI+iQFUBBMBCAA+FiEEEIOwJg/1vpF1itJ4roJVuKDYKOQF
Alh91QoCGwMFCQPCZwAFCwkIBwIGFQgJCgsCBBYCAwECHgECF4AACgkQroJVuKDY
KORreggAlIkC2QjHP5tb7b0+LksB2JMXdY+UzZBcJxtNmvA7gNQaGvWRrhrbePpa
MKDP+3A4BPDBsWFbbB7N56vQ5tROpmWbNKuFOVER4S1bj0JZV0E+xkDLqt9QwQtQ
ojd7oIZJwDUwdud1PvCza2mjgBqqiFE+twbc3i9xjciCGspMniUul1eQYLxRJ0w+
sbvSOUnujnq5ByMSz9ij00O6aiPfNQS5oB5AALfpjYZDvWAAljLVrtmlQJWZ6dZo
T/YNwsW2dECPuti8+Nmu5FxPGDTXxdbnRaeJTQ3T6q1oUVAv7yTXBx5NXfXkMa5i
iEayQIH8Joq5Ev5ja/lRGQQhArMQ2bkBDQRYfdUKAQgAv7B3coLSrOQbuTZSlgWE
QeT+7DWbmqE1LAQA1pQPcUPXLBUVd60amZJxF9nzUYcY83ylDi0gUNJS+DJGOXpT
pzX2IOuOMGbtUSeKwg5s9O4SUO7f2yCc3RGaegER5zgESxelmOXG+b/hoNt7JbdU
JtxcnLr91Jw2PBO/Xf0ZKJ01CQG2Yzdrrj6jnrHyx94seHy0i6xH1o0OuvfVMLfN
/Vbb/ZHh6ym2wHNqRX62b0VAbchcJXX/MEehXGknKTkO6dDUd+mhRgWMf9ZGRFWx
ag4qALimkf1FXtAyD0vxFYeyoWUQzrOvUsm2BxIN/986R08fhkBQnp5nz07mrU02
cQARAQABiQE8BBgBCAAmFiEEEIOwJg/1vpF1itJ4roJVuKDYKOQFAlh91QoCGwwF
CQPCZwAACgkQroJVuKDYKOT32wf/UZqMdPn5OhyhffFzjQx7wolrf92WkF2JkxtH
6c3Htjlt/p5RhtKEeErSrNAxB4pqB7dznHaJXiOdWEZtRVXXjlNHjrokGTesqtKk
lHWtK62/MuyLdr+FdCl68F3ewuT2iu/MDv+D4HPqA47zma9xVgZ9ZNwJOpv3fCOo
RfY66UjGEnfgYifgtI5S84/mp2jaSc9UNvlZB6RSf8cfbJUL74kS2lq+xzSlf0yP
Av844q/BfRuVsJsK1NDNG09LC30B0l3LKBqlrRmRTUMHtgchdX2dY+p7GPOoSzlR
MkM/fdpyc2hY7Dl/+qFmN5MG5yGmMpQcX+RNNR222ibNC1D3wg==
=i9b7
-----END PGP PUBLIC KEY BLOCK-----`
ekey, err := checkArmoredGPGKeyString(testGPGArmor)
assert.Nil(t, err, "Could not parse a valid GPG armored key", ekey)
pubkey := ekey.PrimaryKey
content, err := base64EncPubKey(pubkey)
assert.Nil(t, err, "Could not base64 encode a valid PublicKey content", ekey)
key := &GPGKey{
KeyID: pubkey.KeyIdString(),
Content: content,
Created: pubkey.CreationTime,
CanSign: pubkey.CanSign(),
CanEncryptComms: pubkey.PubKeyAlgo.CanEncrypt(),
CanEncryptStorage: pubkey.PubKeyAlgo.CanEncrypt(),
CanCertify: pubkey.PubKeyAlgo.CanSign(),
}
cannotsignkey := &GPGKey{
KeyID: pubkey.KeyIdString(),
Content: content,
Created: pubkey.CreationTime,
CanSign: false,
CanEncryptComms: false,
CanEncryptStorage: false,
CanCertify: false,
}
testGoodSigArmor := `-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEEEIOwJg/1vpF1itJ4roJVuKDYKOQFAljAiQIACgkQroJVuKDY
KORvCgf6A/Ehh0r7QbO2tFEghT+/Ab+bN7jRN3zP9ed6/q/ophYmkrU0NibtbJH9
AwFVdHxCmj78SdiRjaTKyevklXw34nvMftmvnOI4lBNUdw6KWl25/n/7wN0l2oZW
rW3UawYpZgodXiLTYarfEimkDQmT67ArScjRA6lLbkEYKO0VdwDu+Z6yBUH3GWtm
45RkXpnsF6AXUfuD7YxnfyyDE1A7g7zj4vVYUAfWukJjqow/LsCUgETETJOqj9q3
52/oQDs04fVkIEtCDulcY+K/fKlukBPJf9WceNDEqiENUzN/Z1y0E+tJ07cSy4bk
yIJb+d0OAaG8bxloO7nJq4Res1Qa8Q==
=puvG
-----END PGP SIGNATURE-----`
testGoodPayload := `tree 56ae8d2799882b20381fc11659db06c16c68c61a
parent c7870c39e4e6b247235ca005797703ec4254613f
author Antoine GIRARD <sapk@sapk.fr> 1489012989 +0100
committer Antoine GIRARD <sapk@sapk.fr> 1489012989 +0100
Goog GPG
`
testBadSigArmor := `-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE5yr4rn9ulbdMxJFiPYI/ySNrtNkFAljAiYkACgkQPYI/ySNr
tNmDdQf+NXhVRiOGt0GucpjJCGrOnK/qqVUmQyRUfrqzVUdb/1/Ws84V5/wE547I
6z3oxeBKFsJa1CtIlxYaUyVhYnDzQtphJzub+Aw3UG0E2ywiE+N7RCa1Ufl7pPxJ
U0SD6gvNaeTDQV/Wctu8v8DkCtEd3N8cMCDWhvy/FQEDztVtzm8hMe0Vdm0ozEH6
P0W93sDNkLC5/qpWDN44sFlYDstW5VhMrnF0r/ohfaK2kpYHhkPk7WtOoHSUwQSg
c4gfhjvXIQrWFnII1Kr5jFGlmgNSR02qpb31VGkMzSnBhWVf2OaHS/kI49QHJakq
AhVDEnoYLCgoDGg9c3p1Ll2452/c6Q==
=uoGV
-----END PGP SIGNATURE-----`
testBadPayload := `tree 3074ff04951956a974e8b02d57733b0766f7cf6c
parent fd3577542f7ad1554c7c7c0eb86bb57a1324ad91
author Antoine GIRARD <sapk@sapk.fr> 1489013107 +0100
committer Antoine GIRARD <sapk@sapk.fr> 1489013107 +0100
Unkonwn GPG key with good email
`
//Reading Sign
goodSig, err := extractSignature(testGoodSigArmor)
assert.Nil(t, err, "Could not parse a valid GPG armored signature", testGoodSigArmor)
badSig, err := extractSignature(testBadSigArmor)
assert.Nil(t, err, "Could not parse a valid GPG armored signature", testBadSigArmor)
//Generating hash of commit
goodHash, err := populateHash(goodSig.Hash, []byte(testGoodPayload))
assert.Nil(t, err, "Could not generate a valid hash of payload", testGoodPayload)
badHash, err := populateHash(badSig.Hash, []byte(testBadPayload))
assert.Nil(t, err, "Could not generate a valid hash of payload", testBadPayload)
//Verify
err = verifySign(goodSig, goodHash, key)
assert.Nil(t, err, "Could not validate a good signature")
err = verifySign(badSig, badHash, key)
assert.NotNil(t, err, "Validate a bad signature")
err = verifySign(goodSig, goodHash, cannotsignkey)
assert.NotNil(t, err, "Validate a bad signature with a kay that can not sign")
}

View File

@@ -78,8 +78,8 @@ func graphItemFromString(s string, r *git.Repository) (GraphItem, error) {
return GraphItem{}, fmt.Errorf("Failed parsing grap line:%s. Expect 1 or two fields", s)
}
rows := strings.Split(data, "|")
if len(rows) != 8 {
rows := strings.SplitN(data, "|", 8)
if len(rows) < 8 {
return GraphItem{}, fmt.Errorf("Failed parsing grap line:%s - Should containt 8 datafields", s)
}

View File

@@ -374,7 +374,7 @@ func (issue *Issue) RemoveLabel(doer *User, label *Label) error {
return err
}
if has, err := HasAccess(doer, issue.Repo, AccessModeWrite); err != nil {
if has, err := HasAccess(doer.ID, issue.Repo, AccessModeWrite); err != nil {
return err
} else if !has {
return ErrLabelNotExist{}
@@ -415,7 +415,7 @@ func (issue *Issue) ClearLabels(doer *User) (err error) {
return err
}
if has, err := hasAccess(sess, doer, issue.Repo, AccessModeWrite); err != nil {
if has, err := hasAccess(sess, doer.ID, issue.Repo, AccessModeWrite); err != nil {
return err
} else if !has {
return ErrLabelNotExist{}
@@ -733,7 +733,7 @@ func (issue *Issue) ChangeContent(doer *User, content string) (err error) {
return nil
}
// ChangeAssignee changes the Asssignee field of this issue.
// ChangeAssignee changes the Assignee field of this issue.
func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) {
var oldAssigneeID = issue.AssigneeID
issue.AssigneeID = assigneeID
@@ -809,23 +809,14 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
}
}
if opts.Issue.AssigneeID > 0 {
assignee, err := getUserByID(e, opts.Issue.AssigneeID)
if err != nil && !IsErrUserNotExist(err) {
return fmt.Errorf("getUserByID: %v", err)
if assigneeID := opts.Issue.AssigneeID; assigneeID > 0 {
valid, err := hasAccess(e, assigneeID, opts.Repo, AccessModeWrite)
if err != nil {
return fmt.Errorf("hasAccess [user_id: %d, repo_id: %d]: %v", assigneeID, opts.Repo.ID, err)
}
// Assume assignee is invalid and drop silently.
opts.Issue.AssigneeID = 0
if assignee != nil {
valid, err := hasAccess(e, assignee, opts.Repo, AccessModeWrite)
if err != nil {
return fmt.Errorf("hasAccess [user_id: %d, repo_id: %d]: %v", assignee.ID, opts.Repo.ID, err)
}
if valid {
opts.Issue.AssigneeID = assignee.ID
opts.Issue.Assignee = assignee
}
if !valid {
opts.Issue.AssigneeID = 0
opts.Issue.Assignee = nil
}
}
@@ -1011,6 +1002,16 @@ func GetIssueByID(id int64) (*Issue, error) {
return getIssueByID(x, id)
}
func getIssuesByIDs(e Engine, issueIDs []int64) ([]*Issue, error) {
issues := make([]*Issue, 0, 10)
return issues, e.In("id", issueIDs).Find(&issues)
}
// GetIssuesByIDs return issues with the given IDs.
func GetIssuesByIDs(issueIDs []int64) ([]*Issue, error) {
return getIssuesByIDs(x, issueIDs)
}
// IssuesOptions represents options of an issue.
type IssuesOptions struct {
RepoID int64
@@ -1133,6 +1134,24 @@ func Issues(opts *IssuesOptions) ([]*Issue, error) {
return issues, nil
}
// GetParticipantsByIssueID returns all users who are participated in comments of an issue.
func GetParticipantsByIssueID(issueID int64) ([]*User, error) {
userIDs := make([]int64, 0, 5)
if err := x.Table("comment").Cols("poster_id").
Where("issue_id = ?", issueID).
And("type = ?", CommentTypeComment).
Distinct("poster_id").
Find(&userIDs); err != nil {
return nil, fmt.Errorf("get poster IDs: %v", err)
}
if len(userIDs) == 0 {
return nil, nil
}
users := make([]*User, 0, len(userIDs))
return users, x.In("id", userIDs).Find(&users)
}
// UpdateIssueMentions extracts mentioned people from content and
// updates issue-user relations for them.
func UpdateIssueMentions(e Engine, issueID int64, mentions []string) error {

View File

@@ -59,10 +59,10 @@ func (issues IssueList) loadPosters(e Engine) error {
return nil
}
postgerIDs := issues.getPosterIDs()
posterMaps := make(map[int64]*User, len(postgerIDs))
posterIDs := issues.getPosterIDs()
posterMaps := make(map[int64]*User, len(posterIDs))
err := e.
In("id", postgerIDs).
In("id", posterIDs).
Find(&posterMaps)
if err != nil {
return err

65
models/issue_list_test.go Normal file
View File

@@ -0,0 +1,65 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIssueList_LoadRepositories(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
issueList := IssueList{
AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue),
AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue),
AssertExistsAndLoadBean(t, &Issue{ID: 4}).(*Issue),
}
repos, err := issueList.LoadRepositories()
assert.NoError(t, err)
assert.Len(t, repos, 2)
for _, issue := range issueList {
assert.EqualValues(t, issue.RepoID, issue.Repo.ID)
}
}
func TestIssueList_LoadAttributes(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
issueList := IssueList{
AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue),
AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue),
AssertExistsAndLoadBean(t, &Issue{ID: 4}).(*Issue),
}
assert.NoError(t, issueList.LoadAttributes())
for _, issue := range issueList {
assert.EqualValues(t, issue.RepoID, issue.Repo.ID)
for _, label := range issue.Labels {
assert.EqualValues(t, issue.RepoID, label.RepoID)
AssertExistsAndLoadBean(t, &IssueLabel{IssueID: issue.ID, LabelID: label.ID})
}
if issue.PosterID > 0 {
assert.EqualValues(t, issue.PosterID, issue.Poster.ID)
}
if issue.AssigneeID > 0 {
assert.EqualValues(t, issue.AssigneeID, issue.Assignee.ID)
}
if issue.MilestoneID > 0 {
assert.EqualValues(t, issue.MilestoneID, issue.Milestone.ID)
}
if issue.IsPull {
assert.EqualValues(t, issue.ID, issue.PullRequest.IssueID)
}
for _, attachment := range issue.Attachments {
assert.EqualValues(t, issue.ID, attachment.IssueID)
}
for _, comment := range issue.Comments {
assert.EqualValues(t, issue.ID, comment.IssueID)
}
}
}

View File

@@ -19,15 +19,27 @@ func (issue *Issue) mailSubject() string {
}
// mailIssueCommentToParticipants can be used for both new issue creation and comment.
// This function sends two list of emails:
// 1. Repository watchers and users who are participated in comments.
// 2. Users who are not in 1. but get mentioned in current issue/comment.
func mailIssueCommentToParticipants(issue *Issue, doer *User, mentions []string) error {
if !setting.Service.EnableNotifyMail {
return nil
}
// Mail watchers.
watchers, err := GetWatchers(issue.RepoID)
if err != nil {
return fmt.Errorf("GetWatchers [%d]: %v", issue.RepoID, err)
return fmt.Errorf("GetWatchers [repo_id: %d]: %v", issue.RepoID, err)
}
participants, err := GetParticipantsByIssueID(issue.ID)
if err != nil {
return fmt.Errorf("GetParticipantsByIssueID [issue_id: %d]: %v", issue.ID, err)
}
// In case the issue poster is not watching the repository,
// even if we have duplicated in watchers, can be safely filtered out.
if issue.PosterID != doer.ID {
participants = append(participants, issue.Poster)
}
tos := make([]string, 0, len(watchers)) // List of email addresses.
@@ -48,6 +60,16 @@ func mailIssueCommentToParticipants(issue *Issue, doer *User, mentions []string)
tos = append(tos, to.Email)
names = append(names, to.Name)
}
for i := range participants {
if participants[i].ID == doer.ID {
continue
} else if com.IsSliceContainsStr(names, participants[i].Name) {
continue
}
tos = append(tos, participants[i].Email)
names = append(names, participants[i].Name)
}
SendIssueCommentMail(issue, doer, tos)
// Mail mentioned people and exclude watchers.

View File

@@ -5,6 +5,7 @@
package models
import (
"sort"
"testing"
"github.com/stretchr/testify/assert"
@@ -42,3 +43,44 @@ func TestIssueAPIURL(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/issues/1", issue.APIURL())
}
func TestGetIssuesByIDs(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
testSuccess := func(expectedIssueIDs []int64, nonExistentIssueIDs []int64) {
issues, err := GetIssuesByIDs(append(expectedIssueIDs, nonExistentIssueIDs...))
assert.NoError(t, err)
actualIssueIDs := make([]int64, len(issues))
for i, issue := range issues {
actualIssueIDs[i] = issue.ID
}
assert.Equal(t, expectedIssueIDs, actualIssueIDs)
}
testSuccess([]int64{1, 2, 3}, []int64{})
testSuccess([]int64{1, 2, 3}, []int64{NonexistentID})
}
func TestGetParticipantsByIssueID(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
checkPartecipants := func(issueID int64, userIDs []int) {
partecipants, err := GetParticipantsByIssueID(issueID)
if assert.NoError(t, err) {
partecipantsIDs := make([]int, len(partecipants))
for i, u := range partecipants {
partecipantsIDs[i] = int(u.ID)
}
sort.Ints(partecipantsIDs)
sort.Ints(userIDs)
assert.Equal(t, userIDs, partecipantsIDs)
}
}
// User 1 is issue1 poster (see fixtures/issue.yml)
// User 2 only labeled issue1 (see fixtures/comment.yml)
// Users 3 and 5 made actual comments (see fixtures/comment.yml)
checkPartecipants(1, []int{3, 5})
}

96
models/issue_watch.go Normal file
View File

@@ -0,0 +1,96 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import (
"time"
)
// IssueWatch is connection request for receiving issue notification.
type IssueWatch struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"UNIQUE(watch) NOT NULL"`
IssueID int64 `xorm:"UNIQUE(watch) NOT NULL"`
IsWatching bool `xorm:"NOT NULL"`
Created time.Time `xorm:"-"`
CreatedUnix int64 `xorm:"NOT NULL"`
Updated time.Time `xorm:"-"`
UpdatedUnix int64 `xorm:"NOT NULL"`
}
// BeforeInsert is invoked from XORM before inserting an object of this type.
func (iw *IssueWatch) BeforeInsert() {
var (
t = time.Now()
u = t.Unix()
)
iw.Created = t
iw.CreatedUnix = u
iw.Updated = t
iw.UpdatedUnix = u
}
// BeforeUpdate is invoked from XORM before updating an object of this type.
func (iw *IssueWatch) BeforeUpdate() {
var (
t = time.Now()
u = t.Unix()
)
iw.Updated = t
iw.UpdatedUnix = u
}
// CreateOrUpdateIssueWatch set watching for a user and issue
func CreateOrUpdateIssueWatch(userID, issueID int64, isWatching bool) error {
iw, exists, err := getIssueWatch(x, userID, issueID)
if err != nil {
return err
}
if !exists {
iw = &IssueWatch{
UserID: userID,
IssueID: issueID,
IsWatching: isWatching,
}
if _, err := x.Insert(iw); err != nil {
return err
}
} else {
iw.IsWatching = isWatching
if _, err := x.Id(iw.ID).Cols("is_watching", "updated_unix").Update(iw); err != nil {
return err
}
}
return nil
}
// GetIssueWatch returns an issue watch by user and issue
func GetIssueWatch(userID, issueID int64) (iw *IssueWatch, exists bool, err error) {
return getIssueWatch(x, userID, issueID)
}
func getIssueWatch(e Engine, userID, issueID int64) (iw *IssueWatch, exists bool, err error) {
iw = new(IssueWatch)
exists, err = e.
Where("user_id = ?", userID).
And("issue_id = ?", issueID).
Get(iw)
return
}
// GetIssueWatchers returns watchers/unwatchers of a given issue
func GetIssueWatchers(issueID int64) ([]*IssueWatch, error) {
return getIssueWatchers(x, issueID)
}
func getIssueWatchers(e Engine, issueID int64) (watches []*IssueWatch, err error) {
err = e.
Where("issue_id = ?", issueID).
Find(&watches)
return
}

View File

@@ -0,0 +1,51 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCreateOrUpdateIssueWatch(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
assert.NoError(t, CreateOrUpdateIssueWatch(3, 1, true))
iw := AssertExistsAndLoadBean(t, &IssueWatch{UserID: 3, IssueID: 1}).(*IssueWatch)
assert.Equal(t, true, iw.IsWatching)
assert.NoError(t, CreateOrUpdateIssueWatch(1, 1, false))
iw = AssertExistsAndLoadBean(t, &IssueWatch{UserID: 1, IssueID: 1}).(*IssueWatch)
assert.Equal(t, false, iw.IsWatching)
}
func TestGetIssueWatch(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
_, exists, err := GetIssueWatch(1, 1)
assert.Equal(t, true, exists)
assert.NoError(t, err)
_, exists, err = GetIssueWatch(2, 2)
assert.Equal(t, true, exists)
assert.NoError(t, err)
_, exists, err = GetIssueWatch(3, 1)
assert.Equal(t, false, exists)
assert.NoError(t, err)
}
func TestGetIssueWatchers(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
iws, err := GetIssueWatchers(1)
assert.NoError(t, err)
assert.Equal(t, 1, len(iws))
iws, err = GetIssueWatchers(5)
assert.NoError(t, err)
assert.Equal(t, 0, len(iws))
}

View File

@@ -92,6 +92,22 @@ var migrations = []Migration{
NewMigration("use new avatar path name for security reason", useNewNameAvatars),
// v21 -> v22
NewMigration("rewrite authorized_keys file via new format", useNewPublickeyFormat),
// v22 -> v23
NewMigration("generate and migrate wiki Git hooks", generateAndMigrateWikiGitHooks),
// v23 -> v24
NewMigration("add user openid table", addUserOpenID),
// v24 -> v25
NewMigration("change the key_id and primary_key_id type", changeGPGKeysColumns),
// v25 -> v26
NewMigration("add show field in user openid table", addUserOpenIDShow),
// v26 -> v27
NewMigration("generate and migrate repo and wiki Git hooks", generateAndMigrateGitHookChains),
// v27 -> v28
NewMigration("change mirror interval from hours to time.Duration", convertIntervalToDuration),
// v28 -> v29
NewMigration("add field for repo size", addRepoSize),
// v29 -> v30
NewMigration("add commit status table", addCommitStatus),
}
// Migrate database to current version

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